Add commands engine
This commit is contained in:
parent
1919e4b72c
commit
91584c18d5
9 changed files with 336 additions and 58 deletions
|
|
@ -1,71 +1,22 @@
|
||||||
package lu.foyer
|
package lu.foyer
|
||||||
|
|
||||||
import lu.foyer.clients.ClientState
|
import lu.foyer.clients.*
|
||||||
|
|
||||||
import zio.*
|
import zio.*
|
||||||
import zio.Console.*
|
import zio.Console.*
|
||||||
import zio.http.*
|
import zio.http.*
|
||||||
import zio.schema.*
|
|
||||||
import zio.http.endpoint.*
|
|
||||||
import zio.http.codec.*
|
import zio.http.codec.*
|
||||||
import java.net.URI
|
import zio.http.codec.PathCodec.path
|
||||||
|
import zio.http.endpoint.*
|
||||||
import zio.http.endpoint.openapi.OpenAPIGen
|
import zio.http.endpoint.openapi.OpenAPIGen
|
||||||
import zio.http.endpoint.openapi.SwaggerUI
|
import zio.http.endpoint.openapi.SwaggerUI
|
||||||
import zio.http.codec.PathCodec.path
|
import zio.schema.*
|
||||||
|
|
||||||
case class JsonApiResponseSingle[T](
|
import java.net.URI
|
||||||
data: JsonApiResponseEntity[T],
|
import java.time.LocalDate
|
||||||
links: JsonApiResponseLinks)
|
import java.util.UUID
|
||||||
derives Schema
|
|
||||||
|
|
||||||
case class JsonApiResponseMultiple[T](
|
|
||||||
data: List[JsonApiResponseEntity[T]],
|
|
||||||
links: JsonApiResponseLinks,
|
|
||||||
meta: JsonApiResponseMeta)
|
|
||||||
derives Schema
|
|
||||||
|
|
||||||
case class JsonApiResponseLinks(
|
|
||||||
self: String,
|
|
||||||
first: Option[String] = None,
|
|
||||||
prev: Option[String] = None,
|
|
||||||
next: Option[String] = None,
|
|
||||||
last: Option[String] = None)
|
|
||||||
derives Schema
|
|
||||||
|
|
||||||
case class JsonApiResponseMeta(totalRecords: Int, totalPages: Int) derives Schema
|
|
||||||
|
|
||||||
final case class JsonApiResponseEntity[T](id: String, `type`: String, attributes: T) derives Schema
|
|
||||||
|
|
||||||
case class Page(number: Int, size: Int, totals: Boolean)
|
|
||||||
|
|
||||||
val pageParams =
|
|
||||||
(HttpCodec.query[Option[Int]]("page[number]")
|
|
||||||
& HttpCodec.query[Option[Int]]("page[size]")
|
|
||||||
& HttpCodec.query[Option[Boolean]]("page[totals]"))
|
|
||||||
.transform[Page]((number, size, totals) =>
|
|
||||||
Page(number.getOrElse(0), size.getOrElse(50), totals.getOrElse(false))
|
|
||||||
)(p => (Some(p._1), Some(p._2), Some(p._3)))
|
|
||||||
|
|
||||||
object ClientsController:
|
|
||||||
private val fetchMany =
|
|
||||||
Endpoint(Method.GET / "clients").query(pageParams).out[JsonApiResponseMultiple[ClientState]]
|
|
||||||
|
|
||||||
private val fetchManyRoute =
|
|
||||||
fetchMany.implement(page =>
|
|
||||||
ZIO.succeed(
|
|
||||||
JsonApiResponseMultiple[ClientState](
|
|
||||||
List.empty,
|
|
||||||
JsonApiResponseLinks("https://api.example.org"),
|
|
||||||
meta = JsonApiResponseMeta(0, 1)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val endpoints = List(fetchMany)
|
|
||||||
val routes = Routes(fetchManyRoute)
|
|
||||||
|
|
||||||
object App extends ZIOAppDefault:
|
object App extends ZIOAppDefault:
|
||||||
val openAPI = OpenAPIGen.fromEndpoints(ClientsController.endpoints)
|
val openAPI = OpenAPIGen.fromEndpoints(ClientController.endpoints)
|
||||||
val routes = ClientsController.routes ++ SwaggerUI.routes("docs" / "openapi", openAPI)
|
val routes = ClientController.routes ++ SwaggerUI.routes("docs" / "openapi", openAPI)
|
||||||
|
|
||||||
override def run = Server.serve(routes).provide(Server.default)
|
override def run = Server.serve(routes).provide(Server.default)
|
||||||
|
|
|
||||||
174
api/src/lu/foyer/clients/ClientController.scala
Normal file
174
api/src/lu/foyer/clients/ClientController.scala
Normal file
|
|
@ -0,0 +1,174 @@
|
||||||
|
package lu.foyer
|
||||||
|
package clients
|
||||||
|
|
||||||
|
import zio.*
|
||||||
|
import zio.Console.*
|
||||||
|
import zio.http.*
|
||||||
|
import zio.http.codec.*
|
||||||
|
import zio.http.codec.PathCodec.path
|
||||||
|
import zio.http.endpoint.*
|
||||||
|
import zio.schema.*
|
||||||
|
|
||||||
|
import java.net.URI
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
object JsonApiResponse:
|
||||||
|
|
||||||
|
case class One[T](
|
||||||
|
data: Entity[T],
|
||||||
|
links: Links)
|
||||||
|
derives Schema
|
||||||
|
|
||||||
|
case class Many[T](
|
||||||
|
data: List[Entity[T]],
|
||||||
|
links: Links,
|
||||||
|
meta: Meta)
|
||||||
|
derives Schema
|
||||||
|
|
||||||
|
case class Links(
|
||||||
|
self: String,
|
||||||
|
first: Option[String] = None,
|
||||||
|
prev: Option[String] = None,
|
||||||
|
next: Option[String] = None,
|
||||||
|
last: Option[String] = None)
|
||||||
|
derives Schema
|
||||||
|
|
||||||
|
case class Meta(totalRecords: Option[Long], totalPages: Option[Long]) derives Schema
|
||||||
|
|
||||||
|
case class Entity[T](id: String, `type`: String, attributes: T) derives Schema
|
||||||
|
|
||||||
|
case class Error(message: String) derives Schema // TODO
|
||||||
|
|
||||||
|
val pageParams =
|
||||||
|
(HttpCodec.query[Option[Int]]("page[number]")
|
||||||
|
& HttpCodec.query[Option[Int]]("page[size]")
|
||||||
|
& HttpCodec.query[Option[Boolean]]("page[totals]"))
|
||||||
|
.transform[Page]((number, size, totals) =>
|
||||||
|
Page(number.getOrElse(0), size.getOrElse(50), totals.getOrElse(false))
|
||||||
|
)(p => (Some(p._1), Some(p._2), Some(p._3)))
|
||||||
|
|
||||||
|
trait CommandEngineController[Command: Schema, Event: Schema, State: Schema](
|
||||||
|
entityName: String,
|
||||||
|
commands: List[Command],
|
||||||
|
commandEngine: CommandEngine[Command, Event, State]):
|
||||||
|
|
||||||
|
private val fetchMany =
|
||||||
|
Endpoint(Method.GET / entityName)
|
||||||
|
.query(pageParams)
|
||||||
|
.out[JsonApiResponse.Many[State]]
|
||||||
|
.outError[JsonApiResponse.Error](Status.InternalServerError)
|
||||||
|
|
||||||
|
private val fetchOne =
|
||||||
|
Endpoint(Method.GET / entityName / string("entityId"))
|
||||||
|
.out[JsonApiResponse.One[State]]
|
||||||
|
|
||||||
|
private val fetchEventsMany =
|
||||||
|
Endpoint(Method.GET / entityName / string("entityId") / "events")
|
||||||
|
.query(pageParams)
|
||||||
|
.out[JsonApiResponse.Many[Event]]
|
||||||
|
|
||||||
|
private val fetchEventsOne =
|
||||||
|
Endpoint(Method.GET / entityName / string("entityId") / "events" / string("eventId"))
|
||||||
|
.out[JsonApiResponse.One[Event]]
|
||||||
|
|
||||||
|
private val commandsEndpoints = commands.map(command =>
|
||||||
|
Endpoint(Method.POST / "clients" / "commands" / command.toString.toLowerCase)
|
||||||
|
.in[Command]
|
||||||
|
.out[JsonApiResponse.One[Event]]
|
||||||
|
)
|
||||||
|
|
||||||
|
private val fetchManyRoute =
|
||||||
|
fetchMany.implement(page =>
|
||||||
|
commandEngine.stateRepo
|
||||||
|
.fetchMany(page)
|
||||||
|
.map(paged =>
|
||||||
|
JsonApiResponse.Many(
|
||||||
|
paged.items.map(entity =>
|
||||||
|
JsonApiResponse.Entity(entity.entityId.toString, "todo", entity.data)
|
||||||
|
),
|
||||||
|
JsonApiResponse.Links("https://api.example.org"),
|
||||||
|
meta = JsonApiResponse.Meta(paged.totals, None) // TODO
|
||||||
|
)
|
||||||
|
).mapError(e => JsonApiResponse.Error(e.getMessage))
|
||||||
|
)
|
||||||
|
end CommandEngineController
|
||||||
|
|
||||||
|
object ClientController:
|
||||||
|
private val fetchMany =
|
||||||
|
Endpoint(Method.GET / "clients")
|
||||||
|
.query(pageParams)
|
||||||
|
.out[JsonApiResponse.Many[ClientState]]
|
||||||
|
|
||||||
|
private val fetchOne =
|
||||||
|
Endpoint(Method.GET / "clients" / string("entityId"))
|
||||||
|
.out[JsonApiResponse.One[ClientState]]
|
||||||
|
|
||||||
|
private val createCommand =
|
||||||
|
Endpoint(Method.POST / "clients" / "commands" / "create")
|
||||||
|
.in[ClientCommand.Create]
|
||||||
|
.out[JsonApiResponse.One[ClientEvent]]
|
||||||
|
|
||||||
|
private val updateCommand =
|
||||||
|
Endpoint(Method.PUT / "clients" / string("entityId") / "commands" / "udpate")
|
||||||
|
.in[ClientCommand.Update]
|
||||||
|
.out[JsonApiResponse.One[ClientEvent]]
|
||||||
|
|
||||||
|
private val disableCommand =
|
||||||
|
Endpoint(Method.PUT / "clients" / string("entityId") / "commands" / "disable")
|
||||||
|
.in[ClientCommand.Disable]
|
||||||
|
.out[JsonApiResponse.One[ClientEvent]]
|
||||||
|
|
||||||
|
private val fetchEventsMany =
|
||||||
|
Endpoint(Method.GET / "clients" / string("entityId") / "events")
|
||||||
|
.query(pageParams)
|
||||||
|
.out[JsonApiResponse.Many[ClientEvent]]
|
||||||
|
|
||||||
|
private val fetchEventsOne =
|
||||||
|
Endpoint(Method.GET / "clients" / string("entityId") / "events" / string("eventId"))
|
||||||
|
.out[JsonApiResponse.One[ClientEvent]]
|
||||||
|
|
||||||
|
private val fetchManyRoute =
|
||||||
|
fetchMany.implement(page =>
|
||||||
|
ZIO.succeed(
|
||||||
|
JsonApiResponse.Many[ClientState](
|
||||||
|
List.empty,
|
||||||
|
JsonApiResponse.Links("https://api.example.org"),
|
||||||
|
meta = JsonApiResponse.Meta(None, None)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
private val createCommandRoute =
|
||||||
|
createCommand.implement(command =>
|
||||||
|
ZIO.succeed(
|
||||||
|
JsonApiResponse.One[ClientEvent](
|
||||||
|
JsonApiResponse.Entity(
|
||||||
|
id = "todo",
|
||||||
|
`type` = "todo",
|
||||||
|
ClientEvent.Created(
|
||||||
|
lastName = command.lastName,
|
||||||
|
firstName = command.firstName,
|
||||||
|
birthDate = command.birthDate,
|
||||||
|
drivingLicenseDate = command.drivingLicenseDate,
|
||||||
|
phoneNumber = command.phoneNumber,
|
||||||
|
email = command.email,
|
||||||
|
address = command.address
|
||||||
|
)
|
||||||
|
),
|
||||||
|
JsonApiResponse.Links("https://api.example.org")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val endpoints = List(
|
||||||
|
fetchMany,
|
||||||
|
fetchOne,
|
||||||
|
createCommand,
|
||||||
|
updateCommand,
|
||||||
|
disableCommand,
|
||||||
|
fetchEventsMany,
|
||||||
|
fetchEventsOne
|
||||||
|
)
|
||||||
|
val routes = Routes(fetchManyRoute)
|
||||||
|
end ClientController
|
||||||
74
core/src/lu/foyer/EventSourcing.scala
Normal file
74
core/src/lu/foyer/EventSourcing.scala
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
package lu.foyer
|
||||||
|
|
||||||
|
import zio.*
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
final case class Entity[T](entityId: UUID, data: T, version: Long)
|
||||||
|
final case class Event[T](entityId: UUID, data: T, eventId: UUID)
|
||||||
|
|
||||||
|
trait StateRepository[State] extends Repository[Entity[State], UUID]
|
||||||
|
trait EventRepository[State] extends Repository[Event[State], UUID]
|
||||||
|
|
||||||
|
trait Reducer[Event, State]:
|
||||||
|
def reduce(event: Event): Option[State]
|
||||||
|
def reduce(state: State, event: Event): Option[State]
|
||||||
|
|
||||||
|
trait CommandHandler[Command, Event, State]:
|
||||||
|
def name: String
|
||||||
|
|
||||||
|
trait CommandHandlerCreate[Command, Event, State] extends CommandHandler[Command, Event, State]:
|
||||||
|
def onCommand(entityId: UUID, command: Command): Task[Event]
|
||||||
|
|
||||||
|
trait CommandHandlerUpdate[Command, Event, State] extends CommandHandler[Command, Event, State]:
|
||||||
|
def onCommand(entityId: UUID, state: State, command: Command): Task[Event]
|
||||||
|
|
||||||
|
class CommandEngine[Command, Event, State](
|
||||||
|
val handlers: List[CommandHandler[Command, Event, State]],
|
||||||
|
reducer: Reducer[Event, State],
|
||||||
|
val eventRepo: EventRepository[Event],
|
||||||
|
val stateRepo: StateRepository[State]):
|
||||||
|
|
||||||
|
def handleCommand(command: Command, name: String, entityId: UUID): Task[(Event, State)] =
|
||||||
|
for
|
||||||
|
handler <- ZIO
|
||||||
|
.succeed(handlers.find(_.name == name))
|
||||||
|
.someOrFail(new IllegalArgumentException(s"No handler found for command $name"))
|
||||||
|
entityOption <- stateRepo.fetchOne(entityId)
|
||||||
|
(event, newStateOption) <- transition(command, name, entityId, entityOption, handler)
|
||||||
|
newState <-
|
||||||
|
ZIO
|
||||||
|
.succeed(newStateOption)
|
||||||
|
.someOrFail(new IllegalArgumentException("Reducer cannot resolve state transition"))
|
||||||
|
newEntity <-
|
||||||
|
Random.nextUUID.map(Entity(_, newState, entityOption.map(_.version).getOrElse(1)))
|
||||||
|
_ <- if entityOption.isEmpty then stateRepo.insert(newEntity)
|
||||||
|
else stateRepo.update(newEntity.entityId, newEntity)
|
||||||
|
eventEntity <- Random.nextUUID.map(Event(newEntity.entityId, event, _))
|
||||||
|
_ <- eventRepo.insert(eventEntity)
|
||||||
|
yield (event, newState)
|
||||||
|
|
||||||
|
private def transition(
|
||||||
|
command: Command,
|
||||||
|
name: String,
|
||||||
|
entityId: UUID,
|
||||||
|
entityOption: Option[Entity[State]],
|
||||||
|
handler: CommandHandler[Command, Event, State]
|
||||||
|
): Task[(Event, Option[State])] = (entityOption, handler) match
|
||||||
|
case (None, h: CommandHandlerCreate[Command, Event, State]) =>
|
||||||
|
h.onCommand(entityId, command)
|
||||||
|
.map(event => (event, reducer.reduce(event)))
|
||||||
|
case (Some(entity), h: CommandHandlerUpdate[Command, Event, State]) =>
|
||||||
|
h.onCommand(entityId, entity.data, command)
|
||||||
|
.map(event => (event, reducer.reduce(entity.data, event)))
|
||||||
|
case (Some(_), _: CommandHandlerCreate[Command, Event, State]) =>
|
||||||
|
ZIO.fail(
|
||||||
|
new IllegalArgumentException(s"State already exists when applying create command $name")
|
||||||
|
)
|
||||||
|
case (None, _: CommandHandlerUpdate[Command, Event, State]) =>
|
||||||
|
ZIO.fail(new IllegalArgumentException(s"No state found to apply the update command $name"))
|
||||||
|
case _ => ZIO.fail(new IllegalArgumentException("Impossible state"))
|
||||||
|
end CommandEngine
|
||||||
|
|
||||||
|
object CommandEngine:
|
||||||
|
def layer[Command, Event, State](using Tag[Command], Tag[Event], Tag[State]) =
|
||||||
|
ZLayer.fromFunction(CommandEngine[Command, Event, State](_, _, _, _))
|
||||||
13
core/src/lu/foyer/Repository.scala
Normal file
13
core/src/lu/foyer/Repository.scala
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
package lu.foyer
|
||||||
|
|
||||||
|
import zio.*
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
final case class Page(number: Int, size: Int, totals: Boolean)
|
||||||
|
final case class Paged[T](items: List[T], totals: Option[Long])
|
||||||
|
|
||||||
|
trait Repository[Entity, Id]:
|
||||||
|
def fetchOne(id: Id): Task[Option[Entity]]
|
||||||
|
def fetchMany(page: Page): Task[Paged[Entity]]
|
||||||
|
def insert(entity: Entity): Task[Unit]
|
||||||
|
def update(id: Id, entity: Entity): Task[Unit]
|
||||||
|
|
@ -9,6 +9,7 @@ enum ClientCommand derives Schema:
|
||||||
case Create(
|
case Create(
|
||||||
lastName: ClientLastName,
|
lastName: ClientLastName,
|
||||||
firstName: ClientFirstName,
|
firstName: ClientFirstName,
|
||||||
|
birthDate: ClientBirthDate,
|
||||||
drivingLicenseDate: Option[ClientDrivingLicenseDate],
|
drivingLicenseDate: Option[ClientDrivingLicenseDate],
|
||||||
phoneNumber: Option[PhoneNumberInput],
|
phoneNumber: Option[PhoneNumberInput],
|
||||||
email: Option[Email],
|
email: Option[Email],
|
||||||
|
|
@ -16,8 +17,14 @@ enum ClientCommand derives Schema:
|
||||||
case Update(
|
case Update(
|
||||||
lastName: Option[ClientLastName],
|
lastName: Option[ClientLastName],
|
||||||
firstName: Option[ClientFirstName],
|
firstName: Option[ClientFirstName],
|
||||||
|
birthDate: Option[ClientBirthDate],
|
||||||
drivingLicenseDate: Option[ClientDrivingLicenseDate],
|
drivingLicenseDate: Option[ClientDrivingLicenseDate],
|
||||||
phoneNumber: Option[PhoneNumberInput],
|
phoneNumber: Option[PhoneNumberInput],
|
||||||
email: Option[Email],
|
email: Option[Email],
|
||||||
address: Option[Address])
|
address: Option[Address])
|
||||||
case Disable(reason: ClientDisabledReason)
|
case Disable(reason: ClientDisabledReason)
|
||||||
|
|
||||||
|
object ClientCommand:
|
||||||
|
given Schema[ClientCommand.Create] = DeriveSchema.gen
|
||||||
|
given Schema[ClientCommand.Update] = DeriveSchema.gen
|
||||||
|
given Schema[ClientCommand.Disable] = DeriveSchema.gen
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@ package clients
|
||||||
import zio.schema.*
|
import zio.schema.*
|
||||||
|
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
|
import zio.schema.annotation.discriminatorName
|
||||||
|
|
||||||
|
@discriminatorName("eventType")
|
||||||
enum ClientEvent derives Schema:
|
enum ClientEvent derives Schema:
|
||||||
case Created(
|
case Created(
|
||||||
lastName: ClientLastName,
|
lastName: ClientLastName,
|
||||||
|
|
@ -22,3 +24,9 @@ enum ClientEvent derives Schema:
|
||||||
phoneNumber: Option[PhoneNumberInput],
|
phoneNumber: Option[PhoneNumberInput],
|
||||||
email: Option[Email],
|
email: Option[Email],
|
||||||
address: Option[Address])
|
address: Option[Address])
|
||||||
|
case Disabled(reason: ClientDisabledReason)
|
||||||
|
|
||||||
|
object ClientEvent:
|
||||||
|
given Schema[ClientEvent.Created] = DeriveSchema.gen
|
||||||
|
given Schema[ClientEvent.Updated] = DeriveSchema.gen
|
||||||
|
given Schema[ClientEvent.Disabled] = DeriveSchema.gen
|
||||||
|
|
|
||||||
48
core/src/lu/foyer/clients/ClientReducer.scala
Normal file
48
core/src/lu/foyer/clients/ClientReducer.scala
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
package lu.foyer
|
||||||
|
package clients
|
||||||
|
|
||||||
|
object ClientReducer extends Reducer[ClientEvent, ClientState]:
|
||||||
|
|
||||||
|
def reduce(event: ClientEvent): Option[ClientState] = event match
|
||||||
|
case e: ClientEvent.Created =>
|
||||||
|
Some(
|
||||||
|
ClientState.Actif(
|
||||||
|
e.lastName,
|
||||||
|
e.firstName,
|
||||||
|
e.birthDate,
|
||||||
|
e.drivingLicenseDate,
|
||||||
|
e.phoneNumber,
|
||||||
|
e.email,
|
||||||
|
e.address
|
||||||
|
)
|
||||||
|
)
|
||||||
|
case _: ClientEvent.Updated => None
|
||||||
|
case _: ClientEvent.Disabled => None
|
||||||
|
|
||||||
|
def reduce(state: ClientState, event: ClientEvent): Option[ClientState] = (state, event) match
|
||||||
|
case (s: ClientState.Actif, e: ClientEvent.Updated) =>
|
||||||
|
Some(
|
||||||
|
ClientState.Actif(
|
||||||
|
e.lastName.getOrElse(s.lastName),
|
||||||
|
e.firstName.getOrElse(s.firstName),
|
||||||
|
e.birthDate.getOrElse(s.birthDate),
|
||||||
|
e.drivingLicenseDate.orElse(s.drivingLicenseDate),
|
||||||
|
e.phoneNumber.orElse(s.phoneNumber),
|
||||||
|
e.email.orElse(s.email),
|
||||||
|
e.address.orElse(s.address)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
case (s: ClientState.Actif, _: ClientEvent.Disabled) =>
|
||||||
|
Some(
|
||||||
|
ClientState.Inactif(
|
||||||
|
s.lastName,
|
||||||
|
s.firstName,
|
||||||
|
s.birthDate,
|
||||||
|
s.drivingLicenseDate,
|
||||||
|
s.phoneNumber,
|
||||||
|
s.email,
|
||||||
|
s.address
|
||||||
|
)
|
||||||
|
)
|
||||||
|
case _ => None
|
||||||
|
end ClientReducer
|
||||||
|
|
@ -2,9 +2,11 @@ package lu.foyer
|
||||||
package clients
|
package clients
|
||||||
|
|
||||||
import zio.schema.*
|
import zio.schema.*
|
||||||
|
import zio.schema.annotation.discriminatorName
|
||||||
|
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
@discriminatorName("statusType")
|
||||||
enum ClientState derives Schema:
|
enum ClientState derives Schema:
|
||||||
case Actif(
|
case Actif(
|
||||||
lastName: ClientLastName,
|
lastName: ClientLastName,
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ trait RefinedString[New <: String] extends RefinedType[String, New]:
|
||||||
)
|
)
|
||||||
|
|
||||||
trait RefinedLocalDate[New <: LocalDate] extends RefinedType[LocalDate, New]:
|
trait RefinedLocalDate[New <: LocalDate] extends RefinedType[LocalDate, New]:
|
||||||
|
def apply(value: LocalDate): New = assume(value)
|
||||||
override def validation(value: LocalDate): Validation[String, New] =
|
override def validation(value: LocalDate): Validation[String, New] =
|
||||||
Validation.succeed(assume(value))
|
Validation.succeed(assume(value))
|
||||||
given Schema[New] = Schema[LocalDate].transform(assume, identity)
|
given Schema[New] = Schema[LocalDate].transform(assume, identity)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue