Showcase
Datasource access
Business logic (simple)
Decrel provides a simple syntax to describe data access; there is no more need to declare methods for each data access pattern.
In this example, I am trying to fetch a Rental
object, plus the associated Book
and User
object from a given rentalId
.
for {
rental <- rentals.get(rentalId)
book <- books.get(rental.bookId)
user <- users.get(rental.userId)
// ... do something with 3 objets
becomes
for {
(rental, book, user) <- (Rental.fetch <>: (Rental.book & Rental.user)).toZIO(rentalId)
// ... do something with 3 objets
for {
(rental, book, user) <- (Rental.fetch <>: (Rental.book & Rental.user)).toF(rentalId)
// ... do something with 3 objets
With the Decrel example, two queries to fetch the book and the user are parallelized by default.
Business logic (complex)
In this example, I am trying to fetch the list of books the user is currently renting, identified by the given userId
.
for {
user <- users.get(userId)
currentRentals <- rentals.currentForUser(user)
books <- currentRentals.traverse(rental => books.get(rental.bookId))
// ... do something with books
for {
user <- users.get(userId)
currentRentals <- rentals.currentForUser(user)
books <- books.getForRentals(currentRentals)
// ... do something with books
In the inefficient example, only the simple books.get
operation was used, but calls to the datasource are not batched, resulting in an inefficient query.
In the efficient example, a custom method (getForRentals
) was used, requiring additional implementation and tests for your application.
for {
books <- (User.fetch >>: User.currentRentals >>: Rental.book).toZIO(userId)
// ... do something with books
for {
books <- (User.fetch >>: User.currentRentals >>: Rental.book).toF(userId)
// ... do something with books
With Decrel, your queries remain efficient while maintaining clarity. Datasource accesses are automatically batched, deduplicated, and parallelized by the underlying ZQuery or Fetch library.
Implementing REST APIs
Decrel removes the need to maintain duplicate versions of the same method to support different data requirement patterns for a single operation.
In this example, we expose a create rental
route that returns additional information on top of created rental, based on the expand
query parameter:
val route = HttpRoutes.of {
case req @ GET -> POST / "rental" :? Expand(expand) =>
val (userId, bookId) = extractDetails(req)
if (expand) // Need rental and book info
rentalService
.createRentalReturningExpanded(userId, bookId)
.map(ExpandedRentalResponse.make)
.flatMap(Ok(_))
else // Need only rental info
rentalService
.createRental(userId, bookId) // Need to maintain additional method
.map(RentalResponse.make)
.flatMap(Ok(_))
}
Notice that without the ability to abstract over return types, there is no choice but to duplicate methods that essentially does the same thing.
val route = HttpRoutes.of {
case req @ GET -> POST / "rental" :? Expand(expand) =>
val (userId, bookId) = extractDetails(req)
if (expand) // Need rental and book info
rentalService
.createRental(
userId,
bookId,
(Rental.self & Rental.user & Rental.book).reify
) // Returns `(Rental, User, Book)`
.map(ExpandedRentalResponse.make)
.flatMap(Ok(_))
else // Need only rental info
rentalService
.createRental( // Same method
userId,
bookId,
Rental.self.reify
) // Returns `Rental`
.map(RentalResponse.make)
.flatMap(Ok(_))
}
With Decrel, your methods can be polymorphic, letting the callsite decide what the exact return type would be. This removes the need to duplicate methods.
Caliban integration
You can easily build ZQuery values for your Caliban protocols.
def getRental(id: Rental.Id): zio.query.Query[Err, Rental] =
Rental.fetch.toQuery(id).map { domainRental =>
protocol.Rental(
id = domainRental.id,
book = (Rental.fetch >>: Rental.book).toQuery(id).map(protocol.Book.make),
user = (Rental.fetch >>: Rental.user).toQuery(id).map(protocol.User.make)
)
}
Queries(
getRental = getRental
)