Fix tests

This commit is contained in:
Paul-Henri Froidmont 2025-10-13 15:46:22 +02:00
parent efdc50eb1d
commit 87bd780f9f
Signed by: phfroidmont
GPG key ID: BE948AFD7E7873BE
34 changed files with 230 additions and 303 deletions

24
.scalafix.conf Normal file
View file

@ -0,0 +1,24 @@
rules = [
DisableSyntax,
LeakingImplicitClassVal,
NoAutoTupling,
NoValInForComprehension,
OrganizeImports
]
OrganizeImports {
targetDialect = Scala3
blankLines = Manual
expandRelative = true
removeUnused=false
groupedImports=Keep
groups = [
"---"
"re:(javax?|scala)\\."
"---"
"*"
"---"
"lu.foyer"
"---"
]
}

View file

@ -1,23 +1,18 @@
package lu.foyer package lu.foyer
import zio.*
import zio.http.*
import zio.http.Header.AccessControlAllowOrigin
import zio.http.Middleware.CorsConfig
import zio.http.Middleware.cors
import zio.http.codec.*
import zio.http.codec.PathCodec.path
import zio.http.endpoint.openapi.OpenAPIGen
import zio.http.endpoint.openapi.SwaggerUI
import lu.foyer.clients.* import lu.foyer.clients.*
import lu.foyer.contracts.* import lu.foyer.contracts.*
import zio.* import zio.schema.codec.JsonCodec.ExplicitConfig
import zio.Console.*
import zio.http.*
import zio.http.codec.*
import zio.http.codec.PathCodec.path
import zio.http.endpoint.*
import zio.http.endpoint.openapi.OpenAPIGen
import zio.http.endpoint.openapi.SwaggerUI
import zio.schema.*
import zio.http.Middleware.cors
import zio.http.Middleware.CorsConfig
import zio.http.Header.AccessControlAllowOrigin
import java.net.URI
import java.time.LocalDate
import java.util.UUID
object HttpServer: object HttpServer:
@ -33,6 +28,11 @@ object HttpServer:
++ SwaggerUI.routes("docs" / "openapi", openAPI) ++ SwaggerUI.routes("docs" / "openapi", openAPI)
object App extends ZIOAppDefault: object App extends ZIOAppDefault:
override val bootstrap = CodecConfig.configLayer(
CodecConfig(explicitNulls = ExplicitConfig(encoding = false, decoding = false))
)
val app = val app =
for for
routes <- HttpServer.routes routes <- HttpServer.routes

View file

@ -1,39 +1,32 @@
package lu.foyer package lu.foyer
import lu.foyer.JsonApiResponse.One
import zio.* import zio.*
import zio.Console.*
import zio.http.* import zio.http.*
import zio.http.codec.* import zio.http.codec.*
import zio.http.codec.PathCodec.path import zio.http.codec.PathCodec.path
import zio.http.endpoint.* import zio.http.endpoint.*
import zio.schema.* import zio.schema.*
import java.net.URI trait CommandEngineController[Command, Event: Schema, State: Schema] extends JsonApiController:
import java.time.LocalDate
import java.util.UUID
trait CommandEngineController[Command: Schema, Event: Schema, State: Schema]
extends JsonApiController:
def commandEngine: CommandEngine[Command, Event, State] def commandEngine: CommandEngine[Command, Event, State]
private lazy val fetchMany = private lazy val fetchMany =
Endpoint(Method.GET / entityName) Endpoint(Method.GET / entityName)
// .query(HttpCodec.query[Page]) .query(HttpCodec.query[Page])
.jsonApiMany[State] .jsonApiMany[State]
private lazy val fetchOne = private lazy val fetchOne =
Endpoint(Method.GET / entityName / uuid("entityId")) Endpoint(Method.GET / entityName / string("entityId"))
.jsonApiOne[State] .jsonApiOne[State]
private lazy val fetchEventsMany = private lazy val fetchEventsMany =
Endpoint(Method.GET / entityName / uuid("entityId") / "events") Endpoint(Method.GET / entityName / string("entityId") / "events")
// .query(HttpCodec.query[Page]) .query(HttpCodec.query[Page])
.jsonApiMany[Event] .jsonApiMany[Event]
private lazy val fetchEventsOne = private lazy val fetchEventsOne =
Endpoint(Method.GET / entityName / uuid("entityId") / "events" / uuid("eventId")) Endpoint(Method.GET / entityName / string("entityId") / "events" / string("eventId"))
.jsonApiOne[Event] .jsonApiOne[Event]
private def generateCommands = commandEngine.handlers.map(handler => private def generateCommands = commandEngine.handlers.map(handler =>
@ -49,15 +42,17 @@ trait CommandEngineController[Command: Schema, Event: Schema, State: Schema]
val route = endpoint.implementJsonApiOneEvent(command => val route = endpoint.implementJsonApiOneEvent(command =>
for for
entityId <- Random.nextUUID entityId <- Random.nextUUID
(event, state) <- commandEngine (event, _) <- commandEngine
.handleCommand(command, handler.name, entityId) .handleCommand(command, handler.name, entityId.toString)
yield Some(event) yield Some(event)
) )
(endpoint, route) (endpoint, route)
private def generateUpdateCommand(handler: CommandHandler[Command, Event, State]) = private def generateUpdateCommand(handler: CommandHandler[Command, Event, State]) =
given Schema[Command] = handler.commandSchema.asInstanceOf[Schema[Command]] given Schema[Command] = handler.commandSchema.asInstanceOf[Schema[Command]]
val endpoint = Endpoint(Method.PUT / entityName / uuid("entityId") / "commands" / handler.name) val endpoint = Endpoint(
Method.PUT / entityName / string("entityId") / "commands" / handler.name
)
.in[Command] .in[Command]
.jsonApiOne[Event] .jsonApiOne[Event]
val route = endpoint.implementJsonApiOneEvent((entityId, command) => val route = endpoint.implementJsonApiOneEvent((entityId, command) =>
@ -70,17 +65,13 @@ trait CommandEngineController[Command: Schema, Event: Schema, State: Schema]
private lazy val (commands, commandsRoutes) = generateCommands.unzip private lazy val (commands, commandsRoutes) = generateCommands.unzip
private lazy val fetchManyRoute = private lazy val fetchManyRoute =
fetchMany.implementJsonApiManyEntity(_ => fetchMany.implementJsonApiManyEntity(commandEngine.stateRepo.fetchMany)
commandEngine.stateRepo.fetchMany(Page(None, None, totals = Some(true)))
)
private lazy val fetchOneRoute = private lazy val fetchOneRoute =
fetchOne.implementJsonApiOneEntity(commandEngine.stateRepo.fetchOne) fetchOne.implementJsonApiOneEntity(commandEngine.stateRepo.fetchOne)
private lazy val fetchEventsManyRoute = private lazy val fetchEventsManyRoute =
fetchEventsMany.implementJsonApiManyEvent(entityId => fetchEventsMany.implementJsonApiManyEvent(commandEngine.eventRepo.fetchMany(_, _))
commandEngine.eventRepo.fetchMany(entityId, Page(None, None, totals = Some(true)))
)
private lazy val fetchEventsOneRoute = private lazy val fetchEventsOneRoute =
fetchEventsOne.implementJsonApiOneEvent(commandEngine.eventRepo.fetchOne(_, _)) fetchEventsOne.implementJsonApiOneEvent(commandEngine.eventRepo.fetchOne(_, _))

View file

@ -1,19 +1,14 @@
package lu.foyer package lu.foyer
import lu.foyer.JsonApiResponse.Many
import lu.foyer.JsonApiResponse.One
import zio.* import zio.*
import zio.http.* import zio.http.*
import zio.http.Header.Forwarded
import zio.http.codec.* import zio.http.codec.*
import zio.http.codec.PathCodec.path
import zio.http.endpoint.* import zio.http.endpoint.*
import zio.schema.* import zio.schema.*
import zio.schema.annotation.discriminatorName import zio.schema.annotation.discriminatorName
import zio.schema.annotation.fieldName
import java.util.UUID import lu.foyer.JsonApiResponse.Many
import scala.annotation.targetName import lu.foyer.JsonApiResponse.One
object JsonApiResponse: object JsonApiResponse:
@ -26,7 +21,7 @@ object JsonApiResponse:
case class Entity[T]( case class Entity[T](
`type`: String, `type`: String,
id: UUID, id: String,
attributes: T, attributes: T,
relationships: Option[Relationships] = None, relationships: Option[Relationships] = None,
links: Map[String, String]) links: Map[String, String])
@ -34,17 +29,17 @@ object JsonApiResponse:
case class Relationships(_entity: RelationshipsEntity) derives Schema case class Relationships(_entity: RelationshipsEntity) derives Schema
object Relationships: object Relationships:
def apply(id: UUID, `type`: String, entityUrl: String): Relationships = Relationships( def apply(id: String, `type`: String, entityUrl: String): Relationships = Relationships(
RelationshipsEntity(RelationshipsData(id, `type`), RelationshipsLinks(entityUrl)) RelationshipsEntity(RelationshipsData(id, `type`), RelationshipsLinks(entityUrl))
) )
case class RelationshipsEntity(data: RelationshipsData, links: RelationshipsLinks) derives Schema case class RelationshipsEntity(data: RelationshipsData, links: RelationshipsLinks) derives Schema
case class RelationshipsData(id: UUID, `type`: String) derives Schema case class RelationshipsData(id: String, `type`: String) derives Schema
case class RelationshipsLinks(related: String) derives Schema case class RelationshipsLinks(related: String) derives Schema
@discriminatorName("errorType") @discriminatorName("errorType")
enum Error(title: String) derives Schema: enum Error derives Schema:
case NotFound(id: String) extends Error(s"Entity $id not found") case NotFound(id: String)
case InternalServerError(title: String) extends Error(title) case InternalServerError(title: String)
object Error: object Error:
given Schema[Error.NotFound] = DeriveSchema.gen given Schema[Error.NotFound] = DeriveSchema.gen
given Schema[Error.InternalServerError] = DeriveSchema.gen given Schema[Error.InternalServerError] = DeriveSchema.gen
@ -105,7 +100,7 @@ trait JsonApiController:
) )
def implementJsonApiOne[Env, A]( def implementJsonApiOne[Env, A](
f: Input => RIO[Env, Option[A]], f: Input => RIO[Env, Option[A]],
getId: A => UUID, getId: A => String,
getEntity: A => Output, getEntity: A => Output,
onthology: String = this.onthology, onthology: String = this.onthology,
links: (A, ProxyHeaders) => Map[String, String] = (_: A, _: ProxyHeaders) => Map.empty, links: (A, ProxyHeaders) => Map[String, String] = (_: A, _: ProxyHeaders) => Map.empty,
@ -177,7 +172,7 @@ trait JsonApiController:
) )
def implementJsonApiMany[Env, A]( def implementJsonApiMany[Env, A](
f: Input => RIO[Env, Paged[A]], f: Input => RIO[Env, Paged[A]],
getId: A => UUID, getId: A => String,
getEntity: A => Output, getEntity: A => Output,
links: (A, ProxyHeaders) => Map[String, String] = (_: A, _: ProxyHeaders) => Map.empty links: (A, ProxyHeaders) => Map[String, String] = (_: A, _: ProxyHeaders) => Map.empty
)(implicit trace: Trace )(implicit trace: Trace

View file

@ -2,16 +2,6 @@ package lu.foyer
package clients package clients
import zio.* 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
class ClientController(val commandEngine: CommandEngine[ClientCommand, ClientEvent, ClientState]) class ClientController(val commandEngine: CommandEngine[ClientCommand, ClientEvent, ClientState])
extends CommandEngineController[ClientCommand, ClientEvent, ClientState]: extends CommandEngineController[ClientCommand, ClientEvent, ClientState]:

View file

@ -1,22 +1,21 @@
package lu.foyer package lu.foyer
package clients package clients
import java.util.UUID
import zio.* import zio.*
class ClientEventRepositoryInMemory(events: Ref[Map[UUID, Event[ClientEvent]]]) class ClientEventRepositoryInMemory(events: Ref[Map[String, Event[ClientEvent]]])
extends EventRepository[ClientEvent] extends EventRepository[ClientEvent]
with InMemoryRepository[Event[ClientEvent]](events): with InMemoryRepository[Event[ClientEvent]](events):
def fetchOne(entityId: UUID, eventId: UUID): Task[Option[Event[ClientEvent]]] = def fetchOne(entityId: String, eventId: String): Task[Option[Event[ClientEvent]]] =
events.get.map(_.get(eventId)) events.get.map(_.get(eventId))
def fetchMany(entityId: UUID, page: Page): Task[Paged[Event[ClientEvent]]] = def fetchMany(entityId: String, page: Page): Task[Paged[Event[ClientEvent]]] =
events.get events.get
.map(entities => .map(entities =>
val items = entities.values val items = entities.values
.filter(_.entityId == entityId) .filter(_.entityId == entityId)
.drop(page.number.getOrElse(0) * page.size.getOrElse(50)) .drop(page.number.getOrElse(0) * page.size.getOrElse(50))
.take(page.size.getOrElse(50)) .take(page.size.getOrElse(50))
Paged(items.toList, if page.totals.getOrElse(false) then Some(entities.size) else None) Paged(items.toList, if page.totals.getOrElse(true) then Some(entities.size) else None)
) )
object ClientEventRepositoryInMemory: object ClientEventRepositoryInMemory:

View file

@ -1,10 +1,9 @@
package lu.foyer package lu.foyer
package clients package clients
import java.util.UUID
import zio.* import zio.*
class ClientStateRepositoryInMemory(clients: Ref[Map[UUID, Entity[ClientState]]]) class ClientStateRepositoryInMemory(clients: Ref[Map[String, Entity[ClientState]]])
extends StateRepository[ClientState] extends StateRepository[ClientState]
with InMemoryRepository[Entity[ClientState]](clients) with InMemoryRepository[Entity[ClientState]](clients)

View file

@ -2,16 +2,6 @@ package lu.foyer
package contracts package contracts
import zio.* 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
class ContractController( class ContractController(
val commandEngine: CommandEngine[ContractCommand, ContractEvent, ContractState]) val commandEngine: CommandEngine[ContractCommand, ContractEvent, ContractState])

View file

@ -1,22 +1,21 @@
package lu.foyer package lu.foyer
package contracts package contracts
import java.util.UUID
import zio.* import zio.*
class ContractEventRepositoryInMemory(events: Ref[Map[UUID, Event[ContractEvent]]]) class ContractEventRepositoryInMemory(events: Ref[Map[String, Event[ContractEvent]]])
extends EventRepository[ContractEvent] extends EventRepository[ContractEvent]
with InMemoryRepository[Event[ContractEvent]](events): with InMemoryRepository[Event[ContractEvent]](events):
def fetchOne(entityId: UUID, eventId: UUID): Task[Option[Event[ContractEvent]]] = def fetchOne(entityId: String, eventId: String): Task[Option[Event[ContractEvent]]] =
events.get.map(_.get(eventId)) events.get.map(_.get(eventId))
def fetchMany(entityId: UUID, page: Page): Task[Paged[Event[ContractEvent]]] = def fetchMany(entityId: String, page: Page): Task[Paged[Event[ContractEvent]]] =
events.get events.get
.map(entities => .map(entities =>
val items = entities.values val items = entities.values
.filter(_.entityId == entityId) .filter(_.entityId == entityId)
.drop(page.number.getOrElse(0) * page.size.getOrElse(50)) .drop(page.number.getOrElse(0) * page.size.getOrElse(50))
.take(page.size.getOrElse(50)) .take(page.size.getOrElse(50))
Paged(items.toList, if page.totals.getOrElse(false) then Some(entities.size) else None) Paged(items.toList, if page.totals.getOrElse(true) then Some(entities.size) else None)
) )
object ContractEventRepositoryInMemory: object ContractEventRepositoryInMemory:

View file

@ -1,10 +1,9 @@
package lu.foyer package lu.foyer
package contracts package contracts
import java.util.UUID
import zio.* import zio.*
class ContractStateRepositoryInMemory(clients: Ref[Map[UUID, Entity[ContractState]]]) class ContractStateRepositoryInMemory(clients: Ref[Map[String, Entity[ContractState]]])
extends StateRepository[ContractState] extends StateRepository[ContractState]
with InMemoryRepository[Entity[ContractState]](clients) with InMemoryRepository[Entity[ContractState]](clients)

View file

@ -1,12 +1,7 @@
package lu.foyer package lu.foyer
package contracts package contracts
import java.util.UUID
import zio.* import zio.*
import lu.foyer.clients.Address
import scala.math.BigDecimal.RoundingMode
import java.util.Currency
import lu.foyer.clients.Country
object EmployeeServiceImpl extends EmployeeService: object EmployeeServiceImpl extends EmployeeService:

View file

@ -1,11 +1,12 @@
package lu.foyer package lu.foyer
package contracts package contracts
import java.util.UUID
import zio.*
import lu.foyer.clients.Address
import scala.math.BigDecimal.RoundingMode
import java.util.Currency import java.util.Currency
import scala.math.BigDecimal.RoundingMode
import zio.*
import lu.foyer.clients.Address
import lu.foyer.clients.Country import lu.foyer.clients.Country
object PremiumServiceImpl extends PremiumService: object PremiumServiceImpl extends PremiumService:

48
build.mill Normal file
View file

@ -0,0 +1,48 @@
//| mvnDeps:
//| - com.goyeau::mill-scalafix::0.6.0
package build
import mill.*, scalalib.*
import com.goyeau.mill.scalafix.ScalafixModule
object Versions:
val zio = "2.1.21"
val zioJson = "0.7.44"
val zioSchema = "1.7.5"
val zioHttp = "3.5.1"
val zioPrelude = "1.0.0-RC42"
trait CommonModule extends ScalaModule with ScalafixModule:
def scalaVersion = "3.7.2"
def scalacOptions = Seq(
"-Wunused:all",
"-preview",
"-feature",
"-language:implicitConversions",
"-Wvalue-discard",
"-Wnonunit-statement"
)
def mvnDeps = Seq(
mvn"dev.zio::zio:${Versions.zio}",
mvn"dev.zio::zio-json:${Versions.zioJson}",
mvn"dev.zio::zio-schema-json:${Versions.zioSchema}",
mvn"dev.zio::zio-schema:${Versions.zioSchema}",
mvn"dev.zio::zio-schema-derivation:${Versions.zioSchema}",
mvn"dev.zio::zio-prelude:${Versions.zioPrelude}"
)
object model extends CommonModule
object core extends CommonModule :
def moduleDeps = Seq(model)
object api extends CommonModule :
def moduleDeps = Seq(core)
def mvnDeps = Seq(
mvn"dev.zio::zio:${Versions.zio}",
mvn"dev.zio::zio-http:${Versions.zioHttp}"
)

View file

@ -1,45 +0,0 @@
// scalafmt: { runner.dialect = scala213 }
package build
import mill._, scalalib._
import coursier.maven.MavenRepository
val sonatypeSnapshots = Seq(
MavenRepository("https://oss.sonatype.org/content/repositories/snapshots")
)
object Versions {
val zio = "2.1.15"
val zioJson = "0.7.33"
val zioSchema = "1.6.3"
val zioHttp = "3.1.0"
val zioPrelude = "1.0.0-RC39"
}
trait CommonModule extends ScalaModule {
def scalaVersion = "3.6.3"
def ivyDeps = Agg(
ivy"dev.zio::zio:${Versions.zio}",
ivy"dev.zio::zio-json:${Versions.zioJson}",
ivy"dev.zio::zio-schema-json:${Versions.zioSchema}",
ivy"dev.zio::zio-schema:${Versions.zioSchema}",
ivy"dev.zio::zio-schema-derivation:${Versions.zioSchema}",
ivy"dev.zio::zio-prelude:${Versions.zioPrelude}"
)
def repositoriesTask = Task.Anon {
super.repositoriesTask() ++ sonatypeSnapshots
}
}
object model extends CommonModule
object core extends CommonModule {
def moduleDeps = Seq(model)
}
object api extends CommonModule {
def moduleDeps = Seq(core)
def ivyDeps = Agg(
ivy"dev.zio::zio:${Versions.zio}",
ivy"dev.zio::zio-http:${Versions.zioHttp}"
)
}

View file

@ -3,15 +3,13 @@ package lu.foyer
import zio.* import zio.*
import zio.schema.Schema import zio.schema.Schema
import java.util.UUID final case class Entity[T](entityId: String, data: T, version: Long)
final case class Event[T](entityId: String, data: T, eventId: String)
final case class Entity[T](entityId: UUID, data: T, version: Long) trait StateRepository[Data] extends Repository[Entity[Data], String]
final case class Event[T](entityId: UUID, data: T, eventId: UUID) trait EventRepository[Data] extends Repository[Event[Data], String]:
def fetchOne(entityId: String, eventId: String): Task[Option[Event[Data]]]
trait StateRepository[Data] extends Repository[Entity[Data], UUID] def fetchMany(entityId: String, page: Page): Task[Paged[Event[Data]]]
trait EventRepository[Data] extends Repository[Event[Data], UUID]:
def fetchOne(entityId: UUID, eventId: UUID): Task[Option[Event[Data]]]
def fetchMany(entityId: UUID, page: Page): Task[Paged[Event[Data]]]
trait Reducer[Event, State]: trait Reducer[Event, State]:
def fromEmpty: PartialFunction[Event, State] def fromEmpty: PartialFunction[Event, State]
@ -30,13 +28,13 @@ trait CommandHandler[+Command, +Event, +State]:
def commandSchema: Schema[?] def commandSchema: Schema[?]
trait CommandHandlerCreate[Command: Schema, Event] extends CommandHandler[Command, Event, Nothing]: trait CommandHandlerCreate[Command: Schema, Event] extends CommandHandler[Command, Event, Nothing]:
def onCommand(entityId: UUID, command: Command): Task[Event] def onCommand(entityId: String, command: Command): Task[Event]
val isCreate = true val isCreate = true
val commandSchema = summon[Schema[Command]] val commandSchema = summon[Schema[Command]]
trait CommandHandlerUpdate[Command: Schema, Event, State] trait CommandHandlerUpdate[Command: Schema, Event, State]
extends CommandHandler[Command, Event, State]: extends CommandHandler[Command, Event, State]:
def onCommand(entityId: UUID, state: State, command: Command): Task[Event] def onCommand(entityId: String, state: State, command: Command): Task[Event]
val isCreate = false val isCreate = false
val commandSchema = summon[Schema[Command]] val commandSchema = summon[Schema[Command]]
@ -46,7 +44,7 @@ class CommandEngine[Command, Event, State](
val eventRepo: EventRepository[Event], val eventRepo: EventRepository[Event],
val stateRepo: StateRepository[State]): val stateRepo: StateRepository[State]):
def handleCommand(command: Command, name: String, entityId: UUID) def handleCommand(command: Command, name: String, entityId: String)
: Task[(lu.foyer.Event[Event], lu.foyer.Entity[State])] = : Task[(lu.foyer.Event[Event], lu.foyer.Entity[State])] =
for for
handler <- ZIO handler <- ZIO
@ -61,14 +59,14 @@ class CommandEngine[Command, Event, State](
newEntity = Entity(entityId, newState, entityOption.map(_.version).getOrElse(1)) newEntity = Entity(entityId, newState, entityOption.map(_.version).getOrElse(1))
_ <- if entityOption.isEmpty then stateRepo.insert(newEntity.entityId, newEntity) _ <- if entityOption.isEmpty then stateRepo.insert(newEntity.entityId, newEntity)
else stateRepo.update(newEntity.entityId, newEntity) else stateRepo.update(newEntity.entityId, newEntity)
eventEntity <- Random.nextUUID.map(Event(newEntity.entityId, event, _)) eventEntity <- Random.nextUUID.map(id => Event(newEntity.entityId, event, id.toString))
_ <- eventRepo.insert(eventEntity.eventId, eventEntity) _ <- eventRepo.insert(eventEntity.eventId, eventEntity)
yield (eventEntity, newEntity) yield (eventEntity, newEntity)
private def transition( private def transition(
command: Command, command: Command,
name: String, name: String,
entityId: UUID, entityId: String,
entityOption: Option[Entity[State]], entityOption: Option[Entity[State]],
handler: CommandHandler[Command, Event, State] handler: CommandHandler[Command, Event, State]
): Task[(Event, Option[State])] = (entityOption, handler) match ): Task[(Event, Option[State])] = (entityOption, handler) match

View file

@ -4,8 +4,6 @@ import zio.*
import zio.schema.* import zio.schema.*
import zio.schema.annotation.fieldName import zio.schema.annotation.fieldName
import java.util.UUID
final case class Page( final case class Page(
@fieldName("page[number]") @fieldName("page[number]")
number: Option[Int], number: Option[Int],
@ -14,6 +12,9 @@ final case class Page(
@fieldName("page[totals]") @fieldName("page[totals]")
totals: Option[Boolean]) totals: Option[Boolean])
derives Schema derives Schema
object Page:
val default = Page(None, None, totals = Some(true))
final case class Paged[T](items: List[T], totals: Option[Long]) final case class Paged[T](items: List[T], totals: Option[Long])
trait Repository[Entity, Id]: trait Repository[Entity, Id]:
@ -22,15 +23,16 @@ trait Repository[Entity, Id]:
def insert(id: Id, entity: Entity): Task[Unit] def insert(id: Id, entity: Entity): Task[Unit]
def update(id: Id, entity: Entity): Task[Unit] def update(id: Id, entity: Entity): Task[Unit]
trait InMemoryRepository[State](entities: Ref[Map[UUID, State]]) extends Repository[State, UUID]: trait InMemoryRepository[State](entities: Ref[Map[String, State]])
def fetchOne(id: UUID): Task[Option[State]] = entities.get.map(_.get(id)) extends Repository[State, String]:
def fetchOne(id: String): Task[Option[State]] = entities.get.map(_.get(id))
def fetchMany(page: Page): Task[Paged[State]] = entities.get.map(entities => def fetchMany(page: Page): Task[Paged[State]] = entities.get.map(entities =>
val items = val items =
entities.values entities.values
.drop(page.number.getOrElse(0) * page.size.getOrElse(50)).take(page.size.getOrElse(50)) .drop(page.number.getOrElse(0) * page.size.getOrElse(50)).take(page.size.getOrElse(50))
Paged(items.toList, if page.totals.getOrElse(false) then Some(entities.size) else None) Paged(items.toList, if page.totals.getOrElse(true) then Some(entities.size) else None)
) )
def insert(id: UUID, entity: State): Task[Unit] = def insert(id: String, entity: State): Task[Unit] =
entities.update(entities => entities.updated(id, entity)) entities.update(entities => entities.updated(id, entity))
def update(id: UUID, entity: State): Task[Unit] = def update(id: String, entity: State): Task[Unit] =
entities.update(entities => entities.updated(id, entity)) entities.update(entities => entities.updated(id, entity))

View file

@ -3,8 +3,6 @@ package clients
import zio.schema.* import zio.schema.*
import java.time.LocalDate
enum ClientCommand derives Schema: enum ClientCommand derives Schema:
case Create( case Create(
lastName: ClientLastName, lastName: ClientLastName,

View file

@ -5,8 +5,6 @@ import zio.schema.*
import zio.schema.annotation.caseName import zio.schema.annotation.caseName
import zio.schema.annotation.discriminatorName import zio.schema.annotation.discriminatorName
import java.time.LocalDate
@discriminatorName("eventType") @discriminatorName("eventType")
sealed trait ClientEvent derives Schema sealed trait ClientEvent derives Schema
object ClientEvent: object ClientEvent:

View file

@ -2,7 +2,6 @@ package lu.foyer
package clients package clients
import zio.* import zio.*
import java.util.UUID
object ClientHandlers: object ClientHandlers:
val layer: ULayer[List[CommandHandler[ClientCommand, ClientEvent, ClientState]]] = val layer: ULayer[List[CommandHandler[ClientCommand, ClientEvent, ClientState]]] =
@ -16,7 +15,7 @@ object ClientHandlers:
object CreateHandler extends CommandHandlerCreate[ClientCommand.Create, ClientEvent.Created]: object CreateHandler extends CommandHandlerCreate[ClientCommand.Create, ClientEvent.Created]:
val name = "create" val name = "create"
def onCommand(entityId: UUID, command: ClientCommand.Create): Task[ClientEvent.Created] = def onCommand(entityId: String, command: ClientCommand.Create): Task[ClientEvent.Created] =
ZIO.succeed( ZIO.succeed(
ClientEvent.Created( ClientEvent.Created(
command.lastName, command.lastName,
@ -32,7 +31,7 @@ object CreateHandler extends CommandHandlerCreate[ClientCommand.Create, ClientEv
object UpdateHandler object UpdateHandler
extends CommandHandlerUpdate[ClientCommand.Update, ClientEvent.Updated, ClientState.Actif]: extends CommandHandlerUpdate[ClientCommand.Update, ClientEvent.Updated, ClientState.Actif]:
val name = "update" val name = "update"
def onCommand(entityId: UUID, state: ClientState.Actif, command: ClientCommand.Update) def onCommand(entityId: String, state: ClientState.Actif, command: ClientCommand.Update)
: Task[ClientEvent.Updated] = : Task[ClientEvent.Updated] =
ZIO.succeed( ZIO.succeed(
ClientEvent.Updated( ClientEvent.Updated(
@ -49,6 +48,6 @@ object UpdateHandler
object DisableHandler object DisableHandler
extends CommandHandlerUpdate[ClientCommand.Disable, ClientEvent.Disabled, ClientState.Actif]: extends CommandHandlerUpdate[ClientCommand.Disable, ClientEvent.Disabled, ClientState.Actif]:
val name = "disable" val name = "disable"
def onCommand(entityId: UUID, state: ClientState.Actif, command: ClientCommand.Disable) def onCommand(entityId: String, state: ClientState.Actif, command: ClientCommand.Disable)
: Task[ClientEvent.Disabled] = : Task[ClientEvent.Disabled] =
ZIO.succeed(ClientEvent.Disabled(command.reason)) ZIO.succeed(ClientEvent.Disabled(command.reason))

View file

@ -10,7 +10,7 @@ class ClientReducer() extends Reducer[ClientEvent, ClientState]:
override val fromState = override val fromState =
case (s: ClientState.Actif, e: ClientEvent.Updated) => s.update(e) case (s: ClientState.Actif, e: ClientEvent.Updated) => s.update(e)
case (s: ClientState.Actif, e: ClientEvent.Disabled) => s.disable(e) case (s: ClientState.Actif, _: ClientEvent.Disabled) => s.disable()
object ClientReducer: object ClientReducer:
val layer: ULayer[Reducer[ClientEvent, ClientState]] = ZLayer.succeed(ClientReducer()) val layer: ULayer[Reducer[ClientEvent, ClientState]] = ZLayer.succeed(ClientReducer())

View file

@ -5,8 +5,6 @@ import zio.schema.*
import zio.schema.annotation.caseName import zio.schema.annotation.caseName
import zio.schema.annotation.discriminatorName import zio.schema.annotation.discriminatorName
import java.time.LocalDate
@discriminatorName("statusType") @discriminatorName("statusType")
sealed trait ClientState derives Schema sealed trait ClientState derives Schema
object ClientState: object ClientState:
@ -32,7 +30,7 @@ object ClientState:
e.email.orElse(email), e.email.orElse(email),
e.address.orElse(address) e.address.orElse(address)
) )
def disable(e: ClientEvent.Disabled) = def disable() =
ClientState.Inactif( ClientState.Inactif(
lastName, lastName,
firstName, firstName,

View file

@ -3,9 +3,6 @@ package contracts
import zio.schema.* import zio.schema.*
import java.time.LocalDate
import java.util.UUID
enum ContractCommand derives Schema: enum ContractCommand derives Schema:
case Subscribe( case Subscribe(
product: ProductType, product: ProductType,

View file

@ -5,8 +5,6 @@ import zio.schema.*
import zio.schema.annotation.caseName import zio.schema.annotation.caseName
import zio.schema.annotation.discriminatorName import zio.schema.annotation.discriminatorName
import java.time.LocalDate
@discriminatorName("eventType") @discriminatorName("eventType")
sealed trait ContractEvent derives Schema sealed trait ContractEvent derives Schema
object ContractEvent: object ContractEvent:

View file

@ -1,9 +1,9 @@
package lu.foyer package lu.foyer
package contracts package contracts
import lu.foyer.clients.*
import zio.* import zio.*
import java.util.UUID
import lu.foyer.clients.*
object ContractHandlers: object ContractHandlers:
val layer = val layer =
@ -29,7 +29,7 @@ class SubscribeHandler(
val name = "create" val name = "create"
def onCommand(entityId: UUID, command: ContractCommand.Subscribe) def onCommand(entityId: String, command: ContractCommand.Subscribe)
: Task[ContractEvent.Subscribed] = : Task[ContractEvent.Subscribed] =
for for
holder <- clientStateRepo.fetchOne(command.holder).someOrFailException holder <- clientStateRepo.fetchOne(command.holder).someOrFailException
@ -39,7 +39,6 @@ class SubscribeHandler(
ZIO.succeed(address) ZIO.succeed(address)
case _ => case _ =>
ZIO.fail(new IllegalArgumentException(s"No active holder found for ${command.holder}")) ZIO.fail(new IllegalArgumentException(s"No active holder found for ${command.holder}"))
premium = premiumService.computePremium(command.formula, command.vehicle, residentialAddress)
premium <- premium <-
ZIO ZIO
.fromEither( .fromEither(
@ -60,7 +59,7 @@ class AmendHandler(clientStateRepo: StateRepository[ClientState], premiumService
val name = "amend" val name = "amend"
def onCommand(entityId: UUID, state: ContractState, command: ContractCommand.Amend) def onCommand(entityId: String, state: ContractState, command: ContractCommand.Amend)
: Task[ContractEvent.Amended] = : Task[ContractEvent.Amended] =
for for
holder <- clientStateRepo.fetchOne(state.holder).someOrFailException holder <- clientStateRepo.fetchOne(state.holder).someOrFailException
@ -96,7 +95,7 @@ class ApproveHandler(employeeService: EmployeeService)
val name = "approve" val name = "approve"
def onCommand(entityId: UUID, state: ContractState.Pending, command: ContractCommand.Approve) def onCommand(entityId: String, state: ContractState.Pending, command: ContractCommand.Approve)
: Task[ContractEvent.Approved] = : Task[ContractEvent.Approved] =
for for
user <- ZIO.succeed("") // TODO current user user <- ZIO.succeed("") // TODO current user
@ -114,7 +113,7 @@ class RejectHandler(employeeService: EmployeeService)
val name = "reject" val name = "reject"
def onCommand(entityId: UUID, state: ContractState.Pending, command: ContractCommand.Reject) def onCommand(entityId: String, state: ContractState.Pending, command: ContractCommand.Reject)
: Task[ContractEvent.Rejected] = : Task[ContractEvent.Rejected] =
for for
user <- ZIO.succeed("") // TODO current user user <- ZIO.succeed("") // TODO current user
@ -132,6 +131,6 @@ class TerminateHandler()
val name = "reject" val name = "reject"
def onCommand(entityId: UUID, state: ContractState, command: ContractCommand.Terminate) def onCommand(entityId: String, state: ContractState, command: ContractCommand.Terminate)
: Task[ContractEvent.Terminated] = : Task[ContractEvent.Terminated] =
ZIO.succeed(ContractEvent.Terminated(command.reason)) ZIO.succeed(ContractEvent.Terminated(command.reason))

View file

@ -10,17 +10,17 @@ class ContractReducer() extends Reducer[ContractEvent, ContractState]:
override val fromState = override val fromState =
case (s: ContractState.Actif, e: ContractEvent.Amended) => s.amend(e) case (s: ContractState.Actif, e: ContractEvent.Amended) => s.amend(e)
case (s: ContractState.Actif, e: ContractEvent.Terminated) => s.terminate(e) case (s: ContractState.Actif, _: ContractEvent.Terminated) => s.terminate()
case (s: ContractState.PendingSubscription, e: ContractEvent.Amended) => s.amend(e) case (s: ContractState.PendingSubscription, e: ContractEvent.Amended) => s.amend(e)
case (s: ContractState.PendingSubscription, e: ContractEvent.Approved) => s.approve(e) case (s: ContractState.PendingSubscription, _: ContractEvent.Approved) => s.approve()
case (s: ContractState.PendingSubscription, e: ContractEvent.Rejected) => s.reject(e) case (s: ContractState.PendingSubscription, _: ContractEvent.Rejected) => s.reject()
case (s: ContractState.PendingSubscription, e: ContractEvent.Terminated) => s.terminate(e) case (s: ContractState.PendingSubscription, _: ContractEvent.Terminated) => s.terminate()
case (s: ContractState.PendingAmendment, e: ContractEvent.Amended) => s.amend(e) case (s: ContractState.PendingAmendment, e: ContractEvent.Amended) => s.amend(e)
case (s: ContractState.PendingAmendment, e: ContractEvent.Approved) => s.approve(e) case (s: ContractState.PendingAmendment, _: ContractEvent.Approved) => s.approve()
case (s: ContractState.PendingAmendment, e: ContractEvent.Rejected) => s.reject(e) case (s: ContractState.PendingAmendment, _: ContractEvent.Rejected) => s.reject()
case (s: ContractState.PendingAmendment, e: ContractEvent.Terminated) => s.terminate(e) case (s: ContractState.PendingAmendment, _: ContractEvent.Terminated) => s.terminate()
object ContractReducer: object ContractReducer:
val layer: ULayer[Reducer[ContractEvent, ContractState]] = ZLayer.succeed(ContractReducer()) val layer: ULayer[Reducer[ContractEvent, ContractState]] = ZLayer.succeed(ContractReducer())

View file

@ -5,8 +5,6 @@ import zio.schema.*
import zio.schema.annotation.caseName import zio.schema.annotation.caseName
import zio.schema.annotation.discriminatorName import zio.schema.annotation.discriminatorName
import java.time.LocalDate
@discriminatorName("statusType") @discriminatorName("statusType")
sealed trait ContractState derives Schema: sealed trait ContractState derives Schema:
def product: ProductType def product: ProductType
@ -32,7 +30,7 @@ object ContractState:
def amend(e: ContractEvent.Amended) = def amend(e: ContractEvent.Amended) =
Actif(e.product, holder, e.vehicle, e.formula, e.premium) Actif(e.product, holder, e.vehicle, e.formula, e.premium)
def terminate(e: ContractEvent.Terminated) = def terminate() =
Terminated(product, holder, vehicle, formula, premium) Terminated(product, holder, vehicle, formula, premium)
@discriminatorName("statusType") @discriminatorName("statusType")
@ -50,13 +48,13 @@ object ContractState:
def amend(e: ContractEvent.Amended) = def amend(e: ContractEvent.Amended) =
PendingSubscription(e.product, holder, e.vehicle, e.formula, e.premium) PendingSubscription(e.product, holder, e.vehicle, e.formula, e.premium)
def approve(e: ContractEvent.Approved) = def approve() =
Actif(product, holder, vehicle, formula, premium) Actif(product, holder, vehicle, formula, premium)
def reject(e: ContractEvent.Rejected) = def reject() =
Terminated(product, holder, vehicle, formula, premium) Terminated(product, holder, vehicle, formula, premium)
def terminate(e: ContractEvent.Terminated) = def terminate() =
Terminated(product, holder, vehicle, formula, premium) Terminated(product, holder, vehicle, formula, premium)
final case class PendingChanges( final case class PendingChanges(
@ -79,7 +77,7 @@ object ContractState:
def amend(e: ContractEvent.Amended) = def amend(e: ContractEvent.Amended) =
copy(pendingChanges = PendingChanges(e.product, e.vehicle, e.formula, e.premium)) copy(pendingChanges = PendingChanges(e.product, e.vehicle, e.formula, e.premium))
def approve(e: ContractEvent.Approved) = def approve() =
Actif( Actif(
pendingChanges.product, pendingChanges.product,
holder, holder,
@ -88,10 +86,10 @@ object ContractState:
pendingChanges.premium pendingChanges.premium
) )
def reject(e: ContractEvent.Rejected) = def reject() =
Actif(product, holder, vehicle, formula, premium) Actif(product, holder, vehicle, formula, premium)
def terminate(e: ContractEvent.Terminated) = def terminate() =
ContractState.Terminated(product, holder, vehicle, formula, premium) ContractState.Terminated(product, holder, vehicle, formula, premium)
@caseName("terminated") @caseName("terminated")

View file

@ -1,7 +1,5 @@
package lu.foyer package lu.foyer
package contracts package contracts
import lu.foyer.clients.Address
trait EmployeeService: trait EmployeeService:
def fetchOne(subject: String): Either[String, Employee] def fetchOne(subject: String): Either[String, Employee]

132
flake.lock generated
View file

@ -9,16 +9,20 @@
"devenv" "devenv"
], ],
"git-hooks": [ "git-hooks": [
"devenv" "devenv",
"git-hooks"
], ],
"nixpkgs": "nixpkgs" "nixpkgs": [
"devenv",
"nixpkgs"
]
}, },
"locked": { "locked": {
"lastModified": 1737621947, "lastModified": 1752264895,
"narHash": "sha256-8HFvG7fvIFbgtaYAY2628Tb89fA55nPm2jSiNs0/Cws=", "narHash": "sha256-1zBPE/PNAkPNUsOWFET4J0cjlvziH8DOekesDmjND+w=",
"owner": "cachix", "owner": "cachix",
"repo": "cachix", "repo": "cachix",
"rev": "f65a3cd5e339c223471e64c051434616e18cc4f5", "rev": "47053aef762f452e816e44eb9a23fbc3827b241a",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -32,6 +36,7 @@
"inputs": { "inputs": {
"cachix": "cachix", "cachix": "cachix",
"flake-compat": "flake-compat", "flake-compat": "flake-compat",
"flake-parts": "flake-parts",
"git-hooks": "git-hooks", "git-hooks": "git-hooks",
"nix": "nix", "nix": "nix",
"nixpkgs": [ "nixpkgs": [
@ -39,11 +44,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1742480343, "lastModified": 1759767645,
"narHash": "sha256-AN6X0t0pX0GLDh6CMG474aNffJUKPQtSEJ9aCZed474=", "narHash": "sha256-36Cm0InaiezqXYmzM7sNmLChWV2jfL4OWccIbL1n3nU=",
"owner": "cachix", "owner": "cachix",
"repo": "devenv", "repo": "devenv",
"rev": "56fe80518d1c949520a2cea8e9c2a83ec3f9bdc5", "rev": "692a6db69ca067f9555f736b92c9d83468053f13",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -55,11 +60,11 @@
"flake-compat": { "flake-compat": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1733328505, "lastModified": 1747046372,
"narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", "narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=",
"owner": "edolstra", "owner": "edolstra",
"repo": "flake-compat", "repo": "flake-compat",
"rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -72,16 +77,15 @@
"inputs": { "inputs": {
"nixpkgs-lib": [ "nixpkgs-lib": [
"devenv", "devenv",
"nix",
"nixpkgs" "nixpkgs"
] ]
}, },
"locked": { "locked": {
"lastModified": 1712014858, "lastModified": 1756770412,
"narHash": "sha256-sB4SWl2lX95bExY2gMFG5HIzvva5AVMJd4Igm+GpZNw=", "narHash": "sha256-+uWLQZccFHwqpGqr2Yt5VsW/PbeJVTn9Dk6SHWhNRPw=",
"owner": "hercules-ci", "owner": "hercules-ci",
"repo": "flake-parts", "repo": "flake-parts",
"rev": "9126214d0a59633752a136528f5f3b9aa8565b7d", "rev": "4524271976b625a4a605beefd893f270620fd751",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -93,7 +97,8 @@
"git-hooks": { "git-hooks": {
"inputs": { "inputs": {
"flake-compat": [ "flake-compat": [
"devenv" "devenv",
"flake-compat"
], ],
"gitignore": "gitignore", "gitignore": "gitignore",
"nixpkgs": [ "nixpkgs": [
@ -102,11 +107,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1740849354, "lastModified": 1758108966,
"narHash": "sha256-oy33+t09FraucSZ2rZ6qnD1Y1c8azKKmQuCvF2ytUko=", "narHash": "sha256-ytw7ROXaWZ7OfwHrQ9xvjpUWeGVm86pwnEd1QhzawIo=",
"owner": "cachix", "owner": "cachix",
"repo": "git-hooks.nix", "repo": "git-hooks.nix",
"rev": "4a709a8ce9f8c08fa7ddb86761fe488ff7858a07", "rev": "54df955a695a84cd47d4a43e08e1feaf90b1fd9b",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -137,94 +142,53 @@
"type": "github" "type": "github"
} }
}, },
"libgit2": {
"flake": false,
"locked": {
"lastModified": 1697646580,
"narHash": "sha256-oX4Z3S9WtJlwvj0uH9HlYcWv+x1hqp8mhXl7HsLu2f0=",
"owner": "libgit2",
"repo": "libgit2",
"rev": "45fd9ed7ae1a9b74b957ef4f337bc3c8b3df01b5",
"type": "github"
},
"original": {
"owner": "libgit2",
"repo": "libgit2",
"type": "github"
}
},
"nix": { "nix": {
"inputs": { "inputs": {
"flake-compat": [ "flake-compat": [
"devenv" "devenv",
"flake-compat"
],
"flake-parts": [
"devenv",
"flake-parts"
],
"git-hooks-nix": [
"devenv",
"git-hooks"
],
"nixpkgs": [
"devenv",
"nixpkgs"
], ],
"flake-parts": "flake-parts",
"libgit2": "libgit2",
"nixpkgs": "nixpkgs_2",
"nixpkgs-23-11": [ "nixpkgs-23-11": [
"devenv" "devenv"
], ],
"nixpkgs-regression": [ "nixpkgs-regression": [
"devenv" "devenv"
],
"pre-commit-hooks": [
"devenv"
] ]
}, },
"locked": { "locked": {
"lastModified": 1741798497, "lastModified": 1758763079,
"narHash": "sha256-E3j+3MoY8Y96mG1dUIiLFm2tZmNbRvSiyN7CrSKuAVg=", "narHash": "sha256-Bx1A+lShhOWwMuy3uDzZQvYiBKBFcKwy6G6NEohhv6A=",
"owner": "domenkozar", "owner": "cachix",
"repo": "nix", "repo": "nix",
"rev": "f3f44b2baaf6c4c6e179de8cbb1cc6db031083cd", "rev": "6f0140527c2b0346df4afad7497baa08decb929f",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "domenkozar", "owner": "cachix",
"ref": "devenv-2.24", "ref": "devenv-2.30.5",
"repo": "nix", "repo": "nix",
"type": "github" "type": "github"
} }
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1733212471, "lastModified": 1758532697,
"narHash": "sha256-M1+uCoV5igihRfcUKrr1riygbe73/dzNnzPsmaLCmpo=", "narHash": "sha256-bhop0bR3u7DCw9/PtLCwr7GwEWDlBSxHp+eVQhCW9t4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "55d15ad12a74eb7d4646254e13638ad0c4128776",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1717432640,
"narHash": "sha256-+f9c4/ZX5MWDOuB1rKoWj+lBNm0z0rs4CK47HBLxy1o=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "88269ab3044128b7c2f4c7d68448b2fb50456870",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "release-24.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1733477122,
"narHash": "sha256-qamMCz5mNpQmgBwc8SB5tVMlD5sbwVIToVZtSxMph9s=",
"owner": "cachix", "owner": "cachix",
"repo": "devenv-nixpkgs", "repo": "devenv-nixpkgs",
"rev": "7bd9e84d0452f6d2e63b6e6da29fe73fac951857", "rev": "207a4cb0e1253c7658c6736becc6eb9cace1f25f",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -237,7 +201,7 @@
"root": { "root": {
"inputs": { "inputs": {
"devenv": "devenv", "devenv": "devenv",
"nixpkgs": "nixpkgs_3", "nixpkgs": "nixpkgs",
"systems": "systems" "systems": "systems"
} }
}, },

View file

@ -1,7 +1,5 @@
package lu.foyer package lu.foyer
import zio.schema.* enum AppError(description: String) extends Throwable(description):
enum AppError(description: String) extends Throwable:
case NotFound(desc: String) extends AppError(desc) case NotFound(desc: String) extends AppError(desc)
case Unexpected(desc: String) extends AppError(desc) case Unexpected(desc: String) extends AppError(desc)

View file

@ -1,10 +1,7 @@
package lu.foyer package lu.foyer
import zio.schema.* opaque type ClientEntityId <: String = String
import java.util.UUID object ClientEntityId extends NonBlankString[ClientEntityId]
opaque type ClientEntityId <: UUID = UUID
object ClientEntityId extends RefinedUUID[ClientEntityId]
opaque type Email <: String = String opaque type Email <: String = String
object Email extends NonBlankString[Email] object Email extends NonBlankString[Email]

View file

@ -1,11 +1,11 @@
package lu.foyer package lu.foyer
import zio.prelude.*
import zio.schema.Schema
import java.time.LocalDate import java.time.LocalDate
import java.util.UUID import java.util.UUID
import zio.prelude.*
import zio.schema.Schema
trait RefinedType[Base, New]: trait RefinedType[Base, New]:
inline def assume(value: Base): New = value.asInstanceOf[New] inline def assume(value: Base): New = value.asInstanceOf[New]
def validation(value: Base): Validation[String, New] def validation(value: Base): Validation[String, New]

View file

@ -1,11 +1,11 @@
package lu.foyer package lu.foyer
package clients package clients
import zio.schema.*
import java.time.Instant import java.time.Instant
import java.time.LocalDate import java.time.LocalDate
import zio.schema.*
opaque type ClientLastName <: String = String opaque type ClientLastName <: String = String
object ClientLastName extends NonBlankString[ClientLastName] object ClientLastName extends NonBlankString[ClientLastName]

View file

@ -2,7 +2,6 @@ package lu.foyer
package contracts package contracts
import zio.schema.* import zio.schema.*
import java.util.Currency
opaque type EmployeeDisplayName <: String = String opaque type EmployeeDisplayName <: String = String
object EmployeeDisplayName extends NonBlankString[EmployeeDisplayName] object EmployeeDisplayName extends NonBlankString[EmployeeDisplayName]

View file

@ -1,9 +1,10 @@
package lu.foyer package lu.foyer
package contracts package contracts
import zio.schema.*
import java.util.Currency import java.util.Currency
import zio.schema.*
opaque type VehiclePlate <: String = String opaque type VehiclePlate <: String = String
object VehiclePlate extends NonBlankString[VehiclePlate] object VehiclePlate extends NonBlankString[VehiclePlate]