geirolz / scope   0.0.12

Apache License 2.0 Website GitHub

A functional, compile-time and type-safe models layer separator

Scala versions: 3.x 2.13

Scope

Build Status Sonatype Nexus (Releases) Scala Steward badge Mergify Status GitHub license

A functional, compile-time and type-safe models layer separator

How to install

libraryDependencies += "com.github.geirolz" % "scope-core" % "0.0.11"
libraryDependencies += "com.github.geirolz" % "scope-generic" % "0.0.11"//optional - for scala 2 and 3

How to use

Defining the ModelMapper

Given

import scope.*
import scope.syntax.*

//datatypes
case class UserId(value: Long)
case class Name(value: String)
case class Surname(value: String)

//doman models
case class User(id: UserId, name: Name, surname: Surname)

//http rest contracts
case class UserContract(id: Long, name: String, surname: String)
object UserContract{    
    implicit val modelMapperForUserContract: ModelMapper[Scope.Endpoint, User, UserContract] =
      ModelMapper.scoped[Scope.Endpoint](user => {
        UserContract(
            user.id.value,
            user.name.value,
            user.surname.value
        )
      })
}
Side effects

If the conversion has side effects you can use ModelMapperK instead.

import scala.util.Try

implicit val modelMapperKForUserContract: ModelMapperK[Try, Scope.Endpoint, User, UserContract] =
  ModelMapperK.scoped[Scope.Endpoint](user => Try {
    UserContract(
        user.id.value,
        user.name.value,
        user.surname.value,
    )
  })
// modelMapperKForUserContract: ModelMapperK[Try, Scope.Endpoint, User, UserContract] = scope.ModelMapperK@47cbc06c
Same fields different model

Often in order to decouple things we just duplicate the same model changing just the name. For example we could find UserContract form the endpoint and User from the domain that are actually equals deferring only on the name.

In these case macros can same us some boilerplate, importing the scope-generic module you can use deriveCaseClassIdMap to derive the ModelMapper that map the object using the same fields. If the objects aren't equals from the signature point of view the compilation will fail. Keep in mind that this macro only supports the primary constructor, smart constructors are not supported.

case class User(id: UserId, name: Name, surname: Surname)

case class UserContract(id: UserId, name: Name, surname: Surname)
object UserContract{    
        
    import scope.*
    import scope.generic.syntax.*
        
    implicit val modelMapperForUserContract: ModelMapper[Scope.Endpoint, User, UserContract] =
      ModelMapper.scoped[Scope.Endpoint].deriveCaseClassIdMap[User, UserContract]
}

Using the ModelMapper

To use the ModelMapper you have to provide the right ScopeContext implicitly

Given

val user: User = User(
    UserId(1),
    Name("Foo"),
    Surname("Bar"),
)
implicit val scopeCtx: TypedScopeContext[Scope.Endpoint] = ScopeContext.of[Scope.Endpoint]
// scopeCtx: TypedScopeContext[Scope.Endpoint] = scope.TypedScopeContext@2517b11d

user.scoped.as[UserContract]
// res0: UserContract = UserContract(
//   id = UserId(value = 1L),
//   name = Name(value = "Foo"),
//   surname = Surname(value = "Bar")
// )
Side effects

If the conversion has side effects you have to write

import scala.util.Try

user.scoped.as[Try[UserContract]]
// res1: Try[UserContract] = Success(
//   value = UserContract(
//     id = UserId(value = 1L),
//     name = Name(value = "Foo"),
//     surname = Surname(value = "Bar")
//   )
// )

In this case if you don't have a ModelMapperK defined but just a ModelMapper if an Applicative instance is available in the scope for your effect F[_] the pure ModelMapper will be lifted using Applicative[F].pure(...)

ScopeContext

If the ScopeContext is wrong or is missing the compilation will fail

implicit val scopeCtx: TypedScopeContext[Scope.Event] = ScopeContext.of[Scope.Event]

user.scoped.as[UserContract]
// error: diverging implicit expansion for type scope.ModelMapper[scopeCtx.ScopeType,User,UserContract]
// starting with method liftPureModelMapper in trait ModelMapperKInstances
// user.scoped.as[UserContract]
//               ^