A functional, compile-time and type-safe models layer separator
libraryDependencies += "com.github.geirolz" % "scope-core" % "0.0.11"
libraryDependencies += "com.github.geirolz" % "scope-generic" % "0.0.11"//optional - for scala 2 and 3
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
)
})
}
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
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]
}
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")
// )
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(...)
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]
// ^