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

Scope

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


How to install

libraryDependencies += "com.github.geirolz" % "scope-core" % "0.0.5"

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,
        )
      })
}

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@538493f9

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@22ee7292

user.scoped.as[UserContract]
// res0: UserContract = UserContract(id = 1L, name = "Foo", surname = "Bar")

If the conversion has side effect you have to write

import scala.util.Try

user.scoped.as[Try[UserContract]]
// res1: Try[UserContract] = Success(
//   value = UserContract(id = 1L, name = "Foo", surname = "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,repl.MdocSession.App.User,repl.MdocSession.App.UserContract]
// starting with method liftPureModelMapper in trait ModelMapperKInstances
// user.scoped.as[UserContract]
//               ^