arussel / decrel

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Decrel

Continuous Integration Project stage: Experimental Release Artifacts Snapshot Artifacts

Decrel is a library for declarative data access using relations between your data.

Warning: Although the basic concepts and most of the usecases I aimed for seems to work as intended, this project is in its early stages. There is no good test coverage, and the API will change a lot in the future. That said, please ask any questions in the discussions tab, I will be happy to reply.

Usecases

For a given domain:

case class Book(id: Book.Id, name: String, author: Author.Id)
object Book {
  case class Id(value: String)
}

case class Author(id: Author.Id, name: String, books: List[Book.Id])
object Author {
  case class Id(value: String)
}

You can declare relations between your entities by extending the appropriate Relation types.

case class Book(id: Book.Id, name: String, author: Author.Id)
object Book {
  case class Id(value: String)
  
  // Define a relation to itself by extending Relation.Self
  // This is useful when composing with other relations later
  case object self extends Relation.Self[Book]
  
  // Define the relation and the kind of relation that exists between two entities
  // Relation.Single means for a book there is a single author
  // depending on your domain, you may want to choose different kinds
  case object author extends Relation.Single[Book, Author]
}

case class Author(id: Author.Id, name: String, books: List[Book.Id])
object Author {
  case class Id(value: String)
  
  case object self extends Relation.Self[Author]

  // Extending Relation.Many means for a given author, there is a list of books
  case object book extends Relation.Many[Author, List, Book]
}

Accessing your data source

To express "given a book, get the author && all the books written by them", looks like this:

val getAuthorAndTheirBooks = Book.author :>: Author.books

But how would you run this with an instance of Book that you have?

val exampleBook = Book(Book.Id("book_id"), "bookname", Author.Id("author_id"))

If your application uses ZIO, there is an integration with ZIO through ZQuery:

import decrel.reify.zquery._
import proofs._  // Datasource implementation defined elsewhere in your code

// Exception is user defined in the datasource implementation
val output: zio.IO[AppError, (Author, List[Book])] = 
  getAuthorAndTheirBooks.toZIO(exampleBook)

Or if you use cats-effect, there is an integration with any effect type that implements cats.effect.Concurrent (including cats.effect.IO) through the Fetch library:

class BookServiceImpl[F[_]](
  // contains your datasource implementations
  proofs: Proofs[F]
) {
  import proofs._

  val output: F[(Author, List[Book])] =
    getAuthorAndTheirBooks.toF(exampleBook) 
}

By default, queries made by decrel will be efficiently batched and deduplicated, thanks to the underlying1 ZQuery or Fetch data types which are based on Haxl.

Generating mock data

You can combine generators defined using scalacheck or zio-test. 2

To express generating an author and a list of books by the author, you can write the following:

val authorAndBooks: Gen[(Author, Book)] =
  gen.author // This is your existing generator for Author
    .expand(Author.self & Author.books) // Give me the generated author,
                                        // additionally list of books for the author

Now you can simply use the composed generator in your test suite.

The benefit of using decrel to compose generators is twofold:

  • less boilerplate compared to specifying generators one-by-one (especially when options/lists are involved)
  • values generated are more consistent compared to generating values independently
    • In this case, all books will have the authorId fields set to the generated author.

Adding decrel to your sbt build

decrel is published for Scala 2.13 and 3, for JVM and JS platforms.

decrel-fetch is NOT published for Scala 3 on JS platform, because fetch is not published for that platform.

Scala 2.12- support may come if there are enough interests.

scala-native support will come once a ZIO version is released against scala-native v0.4.8+

Release Artifacts

"com.yoohaemin" %% "decrel-core"       % decrelVersion // Defines Relation and derivations
"com.yoohaemin" %% "decrel-zquery"     % decrelVersion // Integration with ZQuery
"com.yoohaemin" %% "decrel-fetch"      % decrelVersion // Integration with Fetch
"com.yoohaemin" %% "decrel-scalacheck" % decrelVersion // Integration with ScalaCheck
"com.yoohaemin" %% "decrel-ziotest"    % decrelVersion // Integration with ZIO-Test Gen 
"com.yoohaemin" %% "decrel-cats"       % decrelVersion // Integration with F[_]: Monad

Notice to all Scala 3 users

Any method that requires an implicit (given) instance of Proof needs to be called against a val value.

See this commit for examples.

Acknowledgements

Thanks to @ghostdogpr for critical piece of insight regarding the design of the api and the initial feedback.

Thanks to @benrbray for all the helpful discussions.

Thanks to @benetis for pointing out there was a problem that needs fixing.

Thanks to all of my friends and colleagues who provided valuable initial feedback.

License

decrel is copyright Haemin Yoo, and is licensed under Mozilla Public License v2.0

modules/core/src/main/scala/decrel/Zippable.scala is based on https://github.com/zio/zio/blob/v2.0.2/core/shared/src/main/scala/zio/Zippable.scala , licensed under the Apache License v2.0

Changelog

0.1.0-M1

Initial non-snapshot release.

Footnotes

  1. You are not required to interact with ZQuery or Fetch datatypes in your application -- simply use the APIs that exposes ZIO or F[_].

  2. Even if your testing library is not supported, adding one is done easily. See decrel.scalacheck.gen or decrel.ziotest.gen. The implementation code should work for a different Gen type with minimal changes.

About

License:Mozilla Public License 2.0


Languages

Language:Scala 99.9%Language:JavaScript 0.1%