diff --git a/.scalafix.conf b/.scalafix.conf new file mode 100644 index 0000000..a1dbef1 --- /dev/null +++ b/.scalafix.conf @@ -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" + "---" + ] +} diff --git a/api/src/lu/foyer/App.scala b/api/src/lu/foyer/App.scala index 7f27fe5..6d3eadc 100644 --- a/api/src/lu/foyer/App.scala +++ b/api/src/lu/foyer/App.scala @@ -1,23 +1,18 @@ 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.contracts.* -import zio.* -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 +import zio.schema.codec.JsonCodec.ExplicitConfig object HttpServer: @@ -33,6 +28,11 @@ object HttpServer: ++ SwaggerUI.routes("docs" / "openapi", openAPI) object App extends ZIOAppDefault: + + override val bootstrap = CodecConfig.configLayer( + CodecConfig(explicitNulls = ExplicitConfig(encoding = false, decoding = false)) + ) + val app = for routes <- HttpServer.routes diff --git a/api/src/lu/foyer/CommandEngineController.scala b/api/src/lu/foyer/CommandEngineController.scala index 0e1d393..21ed66d 100644 --- a/api/src/lu/foyer/CommandEngineController.scala +++ b/api/src/lu/foyer/CommandEngineController.scala @@ -1,39 +1,32 @@ package lu.foyer -import lu.foyer.JsonApiResponse.One 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 - -trait CommandEngineController[Command: Schema, Event: Schema, State: Schema] - extends JsonApiController: +trait CommandEngineController[Command, Event: Schema, State: Schema] extends JsonApiController: def commandEngine: CommandEngine[Command, Event, State] private lazy val fetchMany = Endpoint(Method.GET / entityName) - // .query(HttpCodec.query[Page]) + .query(HttpCodec.query[Page]) .jsonApiMany[State] private lazy val fetchOne = - Endpoint(Method.GET / entityName / uuid("entityId")) + Endpoint(Method.GET / entityName / string("entityId")) .jsonApiOne[State] private lazy val fetchEventsMany = - Endpoint(Method.GET / entityName / uuid("entityId") / "events") - // .query(HttpCodec.query[Page]) + Endpoint(Method.GET / entityName / string("entityId") / "events") + .query(HttpCodec.query[Page]) .jsonApiMany[Event] private lazy val fetchEventsOne = - Endpoint(Method.GET / entityName / uuid("entityId") / "events" / uuid("eventId")) + Endpoint(Method.GET / entityName / string("entityId") / "events" / string("eventId")) .jsonApiOne[Event] private def generateCommands = commandEngine.handlers.map(handler => @@ -49,15 +42,17 @@ trait CommandEngineController[Command: Schema, Event: Schema, State: Schema] val route = endpoint.implementJsonApiOneEvent(command => for entityId <- Random.nextUUID - (event, state) <- commandEngine - .handleCommand(command, handler.name, entityId) + (event, _) <- commandEngine + .handleCommand(command, handler.name, entityId.toString) yield Some(event) ) (endpoint, route) private def generateUpdateCommand(handler: CommandHandler[Command, Event, State]) = 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] .jsonApiOne[Event] 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 fetchManyRoute = - fetchMany.implementJsonApiManyEntity(_ => - commandEngine.stateRepo.fetchMany(Page(None, None, totals = Some(true))) - ) + fetchMany.implementJsonApiManyEntity(commandEngine.stateRepo.fetchMany) private lazy val fetchOneRoute = fetchOne.implementJsonApiOneEntity(commandEngine.stateRepo.fetchOne) private lazy val fetchEventsManyRoute = - fetchEventsMany.implementJsonApiManyEvent(entityId => - commandEngine.eventRepo.fetchMany(entityId, Page(None, None, totals = Some(true))) - ) + fetchEventsMany.implementJsonApiManyEvent(commandEngine.eventRepo.fetchMany(_, _)) private lazy val fetchEventsOneRoute = fetchEventsOne.implementJsonApiOneEvent(commandEngine.eventRepo.fetchOne(_, _)) diff --git a/api/src/lu/foyer/JsonApiController.scala b/api/src/lu/foyer/JsonApiController.scala index 9c5f1c7..645d271 100644 --- a/api/src/lu/foyer/JsonApiController.scala +++ b/api/src/lu/foyer/JsonApiController.scala @@ -1,19 +1,14 @@ package lu.foyer -import lu.foyer.JsonApiResponse.Many -import lu.foyer.JsonApiResponse.One import zio.* import zio.http.* -import zio.http.Header.Forwarded import zio.http.codec.* -import zio.http.codec.PathCodec.path import zio.http.endpoint.* import zio.schema.* import zio.schema.annotation.discriminatorName -import zio.schema.annotation.fieldName -import java.util.UUID -import scala.annotation.targetName +import lu.foyer.JsonApiResponse.Many +import lu.foyer.JsonApiResponse.One object JsonApiResponse: @@ -26,7 +21,7 @@ object JsonApiResponse: case class Entity[T]( `type`: String, - id: UUID, + id: String, attributes: T, relationships: Option[Relationships] = None, links: Map[String, String]) @@ -34,17 +29,17 @@ object JsonApiResponse: case class Relationships(_entity: RelationshipsEntity) derives Schema 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)) ) 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 @discriminatorName("errorType") - enum Error(title: String) derives Schema: - case NotFound(id: String) extends Error(s"Entity $id not found") - case InternalServerError(title: String) extends Error(title) + enum Error derives Schema: + case NotFound(id: String) + case InternalServerError(title: String) object Error: given Schema[Error.NotFound] = DeriveSchema.gen given Schema[Error.InternalServerError] = DeriveSchema.gen @@ -105,7 +100,7 @@ trait JsonApiController: ) def implementJsonApiOne[Env, A]( f: Input => RIO[Env, Option[A]], - getId: A => UUID, + getId: A => String, getEntity: A => Output, onthology: String = this.onthology, links: (A, ProxyHeaders) => Map[String, String] = (_: A, _: ProxyHeaders) => Map.empty, @@ -177,7 +172,7 @@ trait JsonApiController: ) def implementJsonApiMany[Env, A]( f: Input => RIO[Env, Paged[A]], - getId: A => UUID, + getId: A => String, getEntity: A => Output, links: (A, ProxyHeaders) => Map[String, String] = (_: A, _: ProxyHeaders) => Map.empty )(implicit trace: Trace diff --git a/api/src/lu/foyer/clients/ClientController.scala b/api/src/lu/foyer/clients/ClientController.scala index ca97dfe..cfba862 100644 --- a/api/src/lu/foyer/clients/ClientController.scala +++ b/api/src/lu/foyer/clients/ClientController.scala @@ -2,16 +2,6 @@ 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 class ClientController(val commandEngine: CommandEngine[ClientCommand, ClientEvent, ClientState]) extends CommandEngineController[ClientCommand, ClientEvent, ClientState]: diff --git a/api/src/lu/foyer/clients/ClientEventRepositoryInMemory.scala b/api/src/lu/foyer/clients/ClientEventRepositoryInMemory.scala index c9e0018..4ca8796 100644 --- a/api/src/lu/foyer/clients/ClientEventRepositoryInMemory.scala +++ b/api/src/lu/foyer/clients/ClientEventRepositoryInMemory.scala @@ -1,22 +1,21 @@ package lu.foyer package clients -import java.util.UUID import zio.* -class ClientEventRepositoryInMemory(events: Ref[Map[UUID, Event[ClientEvent]]]) +class ClientEventRepositoryInMemory(events: Ref[Map[String, Event[ClientEvent]]]) extends EventRepository[ClientEvent] 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)) - def fetchMany(entityId: UUID, page: Page): Task[Paged[Event[ClientEvent]]] = + def fetchMany(entityId: String, page: Page): Task[Paged[Event[ClientEvent]]] = events.get .map(entities => val items = entities.values .filter(_.entityId == entityId) .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) ) object ClientEventRepositoryInMemory: diff --git a/api/src/lu/foyer/clients/ClientStateRepositoryInMemory.scala b/api/src/lu/foyer/clients/ClientStateRepositoryInMemory.scala index f6f5266..fdb9fc6 100644 --- a/api/src/lu/foyer/clients/ClientStateRepositoryInMemory.scala +++ b/api/src/lu/foyer/clients/ClientStateRepositoryInMemory.scala @@ -1,10 +1,9 @@ package lu.foyer package clients -import java.util.UUID import zio.* -class ClientStateRepositoryInMemory(clients: Ref[Map[UUID, Entity[ClientState]]]) +class ClientStateRepositoryInMemory(clients: Ref[Map[String, Entity[ClientState]]]) extends StateRepository[ClientState] with InMemoryRepository[Entity[ClientState]](clients) diff --git a/api/src/lu/foyer/contracts/ContractController.scala b/api/src/lu/foyer/contracts/ContractController.scala index 12e0d63..1eda322 100644 --- a/api/src/lu/foyer/contracts/ContractController.scala +++ b/api/src/lu/foyer/contracts/ContractController.scala @@ -2,16 +2,6 @@ package lu.foyer package contracts 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( val commandEngine: CommandEngine[ContractCommand, ContractEvent, ContractState]) diff --git a/api/src/lu/foyer/contracts/ContractEventRepositoryInMemory.scala b/api/src/lu/foyer/contracts/ContractEventRepositoryInMemory.scala index 43358db..c9881cd 100644 --- a/api/src/lu/foyer/contracts/ContractEventRepositoryInMemory.scala +++ b/api/src/lu/foyer/contracts/ContractEventRepositoryInMemory.scala @@ -1,22 +1,21 @@ package lu.foyer package contracts -import java.util.UUID import zio.* -class ContractEventRepositoryInMemory(events: Ref[Map[UUID, Event[ContractEvent]]]) +class ContractEventRepositoryInMemory(events: Ref[Map[String, Event[ContractEvent]]]) extends EventRepository[ContractEvent] 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)) - def fetchMany(entityId: UUID, page: Page): Task[Paged[Event[ContractEvent]]] = + def fetchMany(entityId: String, page: Page): Task[Paged[Event[ContractEvent]]] = events.get .map(entities => val items = entities.values .filter(_.entityId == entityId) .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) ) object ContractEventRepositoryInMemory: diff --git a/api/src/lu/foyer/contracts/ContractStateRepositoryInMemory.scala b/api/src/lu/foyer/contracts/ContractStateRepositoryInMemory.scala index 4769c98..fa3ee64 100644 --- a/api/src/lu/foyer/contracts/ContractStateRepositoryInMemory.scala +++ b/api/src/lu/foyer/contracts/ContractStateRepositoryInMemory.scala @@ -1,10 +1,9 @@ package lu.foyer package contracts -import java.util.UUID import zio.* -class ContractStateRepositoryInMemory(clients: Ref[Map[UUID, Entity[ContractState]]]) +class ContractStateRepositoryInMemory(clients: Ref[Map[String, Entity[ContractState]]]) extends StateRepository[ContractState] with InMemoryRepository[Entity[ContractState]](clients) diff --git a/api/src/lu/foyer/contracts/EmployeeServiceImpl.scala b/api/src/lu/foyer/contracts/EmployeeServiceImpl.scala index 3194122..faf96bd 100644 --- a/api/src/lu/foyer/contracts/EmployeeServiceImpl.scala +++ b/api/src/lu/foyer/contracts/EmployeeServiceImpl.scala @@ -1,12 +1,7 @@ package lu.foyer package contracts -import java.util.UUID 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: diff --git a/api/src/lu/foyer/contracts/PremiumServiceImpl.scala b/api/src/lu/foyer/contracts/PremiumServiceImpl.scala index c584f29..b90f122 100644 --- a/api/src/lu/foyer/contracts/PremiumServiceImpl.scala +++ b/api/src/lu/foyer/contracts/PremiumServiceImpl.scala @@ -1,11 +1,12 @@ package lu.foyer package contracts -import java.util.UUID -import zio.* -import lu.foyer.clients.Address -import scala.math.BigDecimal.RoundingMode import java.util.Currency +import scala.math.BigDecimal.RoundingMode + +import zio.* + +import lu.foyer.clients.Address import lu.foyer.clients.Country object PremiumServiceImpl extends PremiumService: diff --git a/build.mill b/build.mill new file mode 100644 index 0000000..025d4db --- /dev/null +++ b/build.mill @@ -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}" + ) + diff --git a/build.sc b/build.sc deleted file mode 100644 index f08ff54..0000000 --- a/build.sc +++ /dev/null @@ -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}" - ) -} diff --git a/core/src/lu/foyer/EventSourcing.scala b/core/src/lu/foyer/EventSourcing.scala index de4c54d..aaf304c 100644 --- a/core/src/lu/foyer/EventSourcing.scala +++ b/core/src/lu/foyer/EventSourcing.scala @@ -3,15 +3,13 @@ package lu.foyer import zio.* 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) -final case class Event[T](entityId: UUID, data: T, eventId: UUID) - -trait StateRepository[Data] extends Repository[Entity[Data], UUID] -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 StateRepository[Data] extends Repository[Entity[Data], String] +trait EventRepository[Data] extends Repository[Event[Data], String]: + def fetchOne(entityId: String, eventId: String): Task[Option[Event[Data]]] + def fetchMany(entityId: String, page: Page): Task[Paged[Event[Data]]] trait Reducer[Event, State]: def fromEmpty: PartialFunction[Event, State] @@ -30,13 +28,13 @@ trait CommandHandler[+Command, +Event, +State]: def commandSchema: Schema[?] 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 commandSchema = summon[Schema[Command]] trait CommandHandlerUpdate[Command: Schema, 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 commandSchema = summon[Schema[Command]] @@ -46,7 +44,7 @@ class CommandEngine[Command, Event, State]( val eventRepo: EventRepository[Event], 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])] = for handler <- ZIO @@ -61,14 +59,14 @@ class CommandEngine[Command, Event, State]( newEntity = Entity(entityId, newState, entityOption.map(_.version).getOrElse(1)) _ <- if entityOption.isEmpty then stateRepo.insert(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) yield (eventEntity, newEntity) private def transition( command: Command, name: String, - entityId: UUID, + entityId: String, entityOption: Option[Entity[State]], handler: CommandHandler[Command, Event, State] ): Task[(Event, Option[State])] = (entityOption, handler) match diff --git a/core/src/lu/foyer/Repository.scala b/core/src/lu/foyer/Repository.scala index b259e68..dd6365f 100644 --- a/core/src/lu/foyer/Repository.scala +++ b/core/src/lu/foyer/Repository.scala @@ -4,8 +4,6 @@ import zio.* import zio.schema.* import zio.schema.annotation.fieldName -import java.util.UUID - final case class Page( @fieldName("page[number]") number: Option[Int], @@ -14,6 +12,9 @@ final case class Page( @fieldName("page[totals]") totals: Option[Boolean]) derives Schema +object Page: + val default = Page(None, None, totals = Some(true)) + final case class Paged[T](items: List[T], totals: Option[Long]) trait Repository[Entity, Id]: @@ -22,15 +23,16 @@ trait Repository[Entity, Id]: def insert(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]: - def fetchOne(id: UUID): Task[Option[State]] = entities.get.map(_.get(id)) +trait InMemoryRepository[State](entities: Ref[Map[String, State]]) + 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 => val items = entities.values .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)) - def update(id: UUID, entity: State): Task[Unit] = + def update(id: String, entity: State): Task[Unit] = entities.update(entities => entities.updated(id, entity)) diff --git a/core/src/lu/foyer/clients/ClientCommand.scala b/core/src/lu/foyer/clients/ClientCommand.scala index 44355db..921e243 100644 --- a/core/src/lu/foyer/clients/ClientCommand.scala +++ b/core/src/lu/foyer/clients/ClientCommand.scala @@ -3,8 +3,6 @@ package clients import zio.schema.* -import java.time.LocalDate - enum ClientCommand derives Schema: case Create( lastName: ClientLastName, diff --git a/core/src/lu/foyer/clients/ClientEvent.scala b/core/src/lu/foyer/clients/ClientEvent.scala index 2e32140..b419d9d 100644 --- a/core/src/lu/foyer/clients/ClientEvent.scala +++ b/core/src/lu/foyer/clients/ClientEvent.scala @@ -5,8 +5,6 @@ import zio.schema.* import zio.schema.annotation.caseName import zio.schema.annotation.discriminatorName -import java.time.LocalDate - @discriminatorName("eventType") sealed trait ClientEvent derives Schema object ClientEvent: diff --git a/core/src/lu/foyer/clients/ClientHandlers.scala b/core/src/lu/foyer/clients/ClientHandlers.scala index 2e80a41..4d9c122 100644 --- a/core/src/lu/foyer/clients/ClientHandlers.scala +++ b/core/src/lu/foyer/clients/ClientHandlers.scala @@ -2,7 +2,6 @@ package lu.foyer package clients import zio.* -import java.util.UUID object ClientHandlers: val layer: ULayer[List[CommandHandler[ClientCommand, ClientEvent, ClientState]]] = @@ -16,7 +15,7 @@ object ClientHandlers: object CreateHandler extends CommandHandlerCreate[ClientCommand.Create, ClientEvent.Created]: 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( ClientEvent.Created( command.lastName, @@ -32,7 +31,7 @@ object CreateHandler extends CommandHandlerCreate[ClientCommand.Create, ClientEv object UpdateHandler extends CommandHandlerUpdate[ClientCommand.Update, ClientEvent.Updated, ClientState.Actif]: 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] = ZIO.succeed( ClientEvent.Updated( @@ -49,6 +48,6 @@ object UpdateHandler object DisableHandler extends CommandHandlerUpdate[ClientCommand.Disable, ClientEvent.Disabled, ClientState.Actif]: 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] = ZIO.succeed(ClientEvent.Disabled(command.reason)) diff --git a/core/src/lu/foyer/clients/ClientReducer.scala b/core/src/lu/foyer/clients/ClientReducer.scala index aa53dbf..1188b02 100644 --- a/core/src/lu/foyer/clients/ClientReducer.scala +++ b/core/src/lu/foyer/clients/ClientReducer.scala @@ -10,7 +10,7 @@ class ClientReducer() extends Reducer[ClientEvent, ClientState]: override val fromState = 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: val layer: ULayer[Reducer[ClientEvent, ClientState]] = ZLayer.succeed(ClientReducer()) diff --git a/core/src/lu/foyer/clients/ClientState.scala b/core/src/lu/foyer/clients/ClientState.scala index ebad0f4..5f2d758 100644 --- a/core/src/lu/foyer/clients/ClientState.scala +++ b/core/src/lu/foyer/clients/ClientState.scala @@ -5,8 +5,6 @@ import zio.schema.* import zio.schema.annotation.caseName import zio.schema.annotation.discriminatorName -import java.time.LocalDate - @discriminatorName("statusType") sealed trait ClientState derives Schema object ClientState: @@ -32,7 +30,7 @@ object ClientState: e.email.orElse(email), e.address.orElse(address) ) - def disable(e: ClientEvent.Disabled) = + def disable() = ClientState.Inactif( lastName, firstName, diff --git a/core/src/lu/foyer/contracts/ContractCommand.scala b/core/src/lu/foyer/contracts/ContractCommand.scala index 1786a72..0b06c90 100644 --- a/core/src/lu/foyer/contracts/ContractCommand.scala +++ b/core/src/lu/foyer/contracts/ContractCommand.scala @@ -3,9 +3,6 @@ package contracts import zio.schema.* -import java.time.LocalDate -import java.util.UUID - enum ContractCommand derives Schema: case Subscribe( product: ProductType, diff --git a/core/src/lu/foyer/contracts/ContractEvent.scala b/core/src/lu/foyer/contracts/ContractEvent.scala index 3c362fd..d96d384 100644 --- a/core/src/lu/foyer/contracts/ContractEvent.scala +++ b/core/src/lu/foyer/contracts/ContractEvent.scala @@ -5,8 +5,6 @@ import zio.schema.* import zio.schema.annotation.caseName import zio.schema.annotation.discriminatorName -import java.time.LocalDate - @discriminatorName("eventType") sealed trait ContractEvent derives Schema object ContractEvent: diff --git a/core/src/lu/foyer/contracts/ContractHandlers.scala b/core/src/lu/foyer/contracts/ContractHandlers.scala index c3b7957..cb377f1 100644 --- a/core/src/lu/foyer/contracts/ContractHandlers.scala +++ b/core/src/lu/foyer/contracts/ContractHandlers.scala @@ -1,9 +1,9 @@ package lu.foyer package contracts -import lu.foyer.clients.* import zio.* -import java.util.UUID + +import lu.foyer.clients.* object ContractHandlers: val layer = @@ -29,7 +29,7 @@ class SubscribeHandler( val name = "create" - def onCommand(entityId: UUID, command: ContractCommand.Subscribe) + def onCommand(entityId: String, command: ContractCommand.Subscribe) : Task[ContractEvent.Subscribed] = for holder <- clientStateRepo.fetchOne(command.holder).someOrFailException @@ -39,7 +39,6 @@ class SubscribeHandler( ZIO.succeed(address) case _ => ZIO.fail(new IllegalArgumentException(s"No active holder found for ${command.holder}")) - premium = premiumService.computePremium(command.formula, command.vehicle, residentialAddress) premium <- ZIO .fromEither( @@ -60,7 +59,7 @@ class AmendHandler(clientStateRepo: StateRepository[ClientState], premiumService val name = "amend" - def onCommand(entityId: UUID, state: ContractState, command: ContractCommand.Amend) + def onCommand(entityId: String, state: ContractState, command: ContractCommand.Amend) : Task[ContractEvent.Amended] = for holder <- clientStateRepo.fetchOne(state.holder).someOrFailException @@ -96,7 +95,7 @@ class ApproveHandler(employeeService: EmployeeService) 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] = for user <- ZIO.succeed("") // TODO current user @@ -114,7 +113,7 @@ class RejectHandler(employeeService: EmployeeService) 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] = for user <- ZIO.succeed("") // TODO current user @@ -132,6 +131,6 @@ class TerminateHandler() val name = "reject" - def onCommand(entityId: UUID, state: ContractState, command: ContractCommand.Terminate) + def onCommand(entityId: String, state: ContractState, command: ContractCommand.Terminate) : Task[ContractEvent.Terminated] = ZIO.succeed(ContractEvent.Terminated(command.reason)) diff --git a/core/src/lu/foyer/contracts/ContractReducer.scala b/core/src/lu/foyer/contracts/ContractReducer.scala index 1df35d4..136edd0 100644 --- a/core/src/lu/foyer/contracts/ContractReducer.scala +++ b/core/src/lu/foyer/contracts/ContractReducer.scala @@ -10,17 +10,17 @@ class ContractReducer() extends Reducer[ContractEvent, ContractState]: override val fromState = 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.Approved) => s.approve(e) - case (s: ContractState.PendingSubscription, e: ContractEvent.Rejected) => s.reject(e) - case (s: ContractState.PendingSubscription, e: ContractEvent.Terminated) => s.terminate(e) + case (s: ContractState.PendingSubscription, _: ContractEvent.Approved) => s.approve() + case (s: ContractState.PendingSubscription, _: ContractEvent.Rejected) => s.reject() + case (s: ContractState.PendingSubscription, _: ContractEvent.Terminated) => s.terminate() case (s: ContractState.PendingAmendment, e: ContractEvent.Amended) => s.amend(e) - case (s: ContractState.PendingAmendment, e: ContractEvent.Approved) => s.approve(e) - case (s: ContractState.PendingAmendment, e: ContractEvent.Rejected) => s.reject(e) - case (s: ContractState.PendingAmendment, e: ContractEvent.Terminated) => s.terminate(e) + case (s: ContractState.PendingAmendment, _: ContractEvent.Approved) => s.approve() + case (s: ContractState.PendingAmendment, _: ContractEvent.Rejected) => s.reject() + case (s: ContractState.PendingAmendment, _: ContractEvent.Terminated) => s.terminate() object ContractReducer: val layer: ULayer[Reducer[ContractEvent, ContractState]] = ZLayer.succeed(ContractReducer()) diff --git a/core/src/lu/foyer/contracts/ContractState.scala b/core/src/lu/foyer/contracts/ContractState.scala index 6349139..ae29611 100644 --- a/core/src/lu/foyer/contracts/ContractState.scala +++ b/core/src/lu/foyer/contracts/ContractState.scala @@ -5,8 +5,6 @@ import zio.schema.* import zio.schema.annotation.caseName import zio.schema.annotation.discriminatorName -import java.time.LocalDate - @discriminatorName("statusType") sealed trait ContractState derives Schema: def product: ProductType @@ -32,7 +30,7 @@ object ContractState: def amend(e: ContractEvent.Amended) = Actif(e.product, holder, e.vehicle, e.formula, e.premium) - def terminate(e: ContractEvent.Terminated) = + def terminate() = Terminated(product, holder, vehicle, formula, premium) @discriminatorName("statusType") @@ -50,13 +48,13 @@ object ContractState: def amend(e: ContractEvent.Amended) = PendingSubscription(e.product, holder, e.vehicle, e.formula, e.premium) - def approve(e: ContractEvent.Approved) = + def approve() = Actif(product, holder, vehicle, formula, premium) - def reject(e: ContractEvent.Rejected) = + def reject() = Terminated(product, holder, vehicle, formula, premium) - def terminate(e: ContractEvent.Terminated) = + def terminate() = Terminated(product, holder, vehicle, formula, premium) final case class PendingChanges( @@ -79,7 +77,7 @@ object ContractState: def amend(e: ContractEvent.Amended) = copy(pendingChanges = PendingChanges(e.product, e.vehicle, e.formula, e.premium)) - def approve(e: ContractEvent.Approved) = + def approve() = Actif( pendingChanges.product, holder, @@ -88,10 +86,10 @@ object ContractState: pendingChanges.premium ) - def reject(e: ContractEvent.Rejected) = + def reject() = Actif(product, holder, vehicle, formula, premium) - def terminate(e: ContractEvent.Terminated) = + def terminate() = ContractState.Terminated(product, holder, vehicle, formula, premium) @caseName("terminated") diff --git a/core/src/lu/foyer/contracts/EmployeeService.scala b/core/src/lu/foyer/contracts/EmployeeService.scala index c2e1a25..fc41bdc 100644 --- a/core/src/lu/foyer/contracts/EmployeeService.scala +++ b/core/src/lu/foyer/contracts/EmployeeService.scala @@ -1,7 +1,5 @@ package lu.foyer package contracts -import lu.foyer.clients.Address - trait EmployeeService: def fetchOne(subject: String): Either[String, Employee] diff --git a/flake.lock b/flake.lock index 5334d2c..a963ffb 100644 --- a/flake.lock +++ b/flake.lock @@ -9,16 +9,20 @@ "devenv" ], "git-hooks": [ - "devenv" + "devenv", + "git-hooks" ], - "nixpkgs": "nixpkgs" + "nixpkgs": [ + "devenv", + "nixpkgs" + ] }, "locked": { - "lastModified": 1737621947, - "narHash": "sha256-8HFvG7fvIFbgtaYAY2628Tb89fA55nPm2jSiNs0/Cws=", + "lastModified": 1752264895, + "narHash": "sha256-1zBPE/PNAkPNUsOWFET4J0cjlvziH8DOekesDmjND+w=", "owner": "cachix", "repo": "cachix", - "rev": "f65a3cd5e339c223471e64c051434616e18cc4f5", + "rev": "47053aef762f452e816e44eb9a23fbc3827b241a", "type": "github" }, "original": { @@ -32,6 +36,7 @@ "inputs": { "cachix": "cachix", "flake-compat": "flake-compat", + "flake-parts": "flake-parts", "git-hooks": "git-hooks", "nix": "nix", "nixpkgs": [ @@ -39,11 +44,11 @@ ] }, "locked": { - "lastModified": 1742480343, - "narHash": "sha256-AN6X0t0pX0GLDh6CMG474aNffJUKPQtSEJ9aCZed474=", + "lastModified": 1759767645, + "narHash": "sha256-36Cm0InaiezqXYmzM7sNmLChWV2jfL4OWccIbL1n3nU=", "owner": "cachix", "repo": "devenv", - "rev": "56fe80518d1c949520a2cea8e9c2a83ec3f9bdc5", + "rev": "692a6db69ca067f9555f736b92c9d83468053f13", "type": "github" }, "original": { @@ -55,11 +60,11 @@ "flake-compat": { "flake": false, "locked": { - "lastModified": 1733328505, - "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", + "lastModified": 1747046372, + "narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=", "owner": "edolstra", "repo": "flake-compat", - "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", + "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885", "type": "github" }, "original": { @@ -72,16 +77,15 @@ "inputs": { "nixpkgs-lib": [ "devenv", - "nix", "nixpkgs" ] }, "locked": { - "lastModified": 1712014858, - "narHash": "sha256-sB4SWl2lX95bExY2gMFG5HIzvva5AVMJd4Igm+GpZNw=", + "lastModified": 1756770412, + "narHash": "sha256-+uWLQZccFHwqpGqr2Yt5VsW/PbeJVTn9Dk6SHWhNRPw=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "9126214d0a59633752a136528f5f3b9aa8565b7d", + "rev": "4524271976b625a4a605beefd893f270620fd751", "type": "github" }, "original": { @@ -93,7 +97,8 @@ "git-hooks": { "inputs": { "flake-compat": [ - "devenv" + "devenv", + "flake-compat" ], "gitignore": "gitignore", "nixpkgs": [ @@ -102,11 +107,11 @@ ] }, "locked": { - "lastModified": 1740849354, - "narHash": "sha256-oy33+t09FraucSZ2rZ6qnD1Y1c8azKKmQuCvF2ytUko=", + "lastModified": 1758108966, + "narHash": "sha256-ytw7ROXaWZ7OfwHrQ9xvjpUWeGVm86pwnEd1QhzawIo=", "owner": "cachix", "repo": "git-hooks.nix", - "rev": "4a709a8ce9f8c08fa7ddb86761fe488ff7858a07", + "rev": "54df955a695a84cd47d4a43e08e1feaf90b1fd9b", "type": "github" }, "original": { @@ -137,94 +142,53 @@ "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": { "inputs": { "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": [ "devenv" ], "nixpkgs-regression": [ "devenv" - ], - "pre-commit-hooks": [ - "devenv" ] }, "locked": { - "lastModified": 1741798497, - "narHash": "sha256-E3j+3MoY8Y96mG1dUIiLFm2tZmNbRvSiyN7CrSKuAVg=", - "owner": "domenkozar", + "lastModified": 1758763079, + "narHash": "sha256-Bx1A+lShhOWwMuy3uDzZQvYiBKBFcKwy6G6NEohhv6A=", + "owner": "cachix", "repo": "nix", - "rev": "f3f44b2baaf6c4c6e179de8cbb1cc6db031083cd", + "rev": "6f0140527c2b0346df4afad7497baa08decb929f", "type": "github" }, "original": { - "owner": "domenkozar", - "ref": "devenv-2.24", + "owner": "cachix", + "ref": "devenv-2.30.5", "repo": "nix", "type": "github" } }, "nixpkgs": { "locked": { - "lastModified": 1733212471, - "narHash": "sha256-M1+uCoV5igihRfcUKrr1riygbe73/dzNnzPsmaLCmpo=", - "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=", + "lastModified": 1758532697, + "narHash": "sha256-bhop0bR3u7DCw9/PtLCwr7GwEWDlBSxHp+eVQhCW9t4=", "owner": "cachix", "repo": "devenv-nixpkgs", - "rev": "7bd9e84d0452f6d2e63b6e6da29fe73fac951857", + "rev": "207a4cb0e1253c7658c6736becc6eb9cace1f25f", "type": "github" }, "original": { @@ -237,7 +201,7 @@ "root": { "inputs": { "devenv": "devenv", - "nixpkgs": "nixpkgs_3", + "nixpkgs": "nixpkgs", "systems": "systems" } }, diff --git a/model/src/lu/foyer/AppError.scala b/model/src/lu/foyer/AppError.scala index 77206a4..ea037c5 100644 --- a/model/src/lu/foyer/AppError.scala +++ b/model/src/lu/foyer/AppError.scala @@ -1,7 +1,5 @@ package lu.foyer -import zio.schema.* - -enum AppError(description: String) extends Throwable: +enum AppError(description: String) extends Throwable(description): case NotFound(desc: String) extends AppError(desc) case Unexpected(desc: String) extends AppError(desc) diff --git a/model/src/lu/foyer/CommonTypes.scala b/model/src/lu/foyer/CommonTypes.scala index d01ab8f..dc75cde 100644 --- a/model/src/lu/foyer/CommonTypes.scala +++ b/model/src/lu/foyer/CommonTypes.scala @@ -1,10 +1,7 @@ package lu.foyer -import zio.schema.* -import java.util.UUID - -opaque type ClientEntityId <: UUID = UUID -object ClientEntityId extends RefinedUUID[ClientEntityId] +opaque type ClientEntityId <: String = String +object ClientEntityId extends NonBlankString[ClientEntityId] opaque type Email <: String = String object Email extends NonBlankString[Email] diff --git a/model/src/lu/foyer/RefinedType.scala b/model/src/lu/foyer/RefinedType.scala index 72a8ec1..31ad82a 100644 --- a/model/src/lu/foyer/RefinedType.scala +++ b/model/src/lu/foyer/RefinedType.scala @@ -1,11 +1,11 @@ package lu.foyer -import zio.prelude.* -import zio.schema.Schema - import java.time.LocalDate import java.util.UUID +import zio.prelude.* +import zio.schema.Schema + trait RefinedType[Base, New]: inline def assume(value: Base): New = value.asInstanceOf[New] def validation(value: Base): Validation[String, New] diff --git a/model/src/lu/foyer/clients/Client.scala b/model/src/lu/foyer/clients/Client.scala index 69dd8ef..3054bd8 100644 --- a/model/src/lu/foyer/clients/Client.scala +++ b/model/src/lu/foyer/clients/Client.scala @@ -1,11 +1,11 @@ package lu.foyer package clients -import zio.schema.* - import java.time.Instant import java.time.LocalDate +import zio.schema.* + opaque type ClientLastName <: String = String object ClientLastName extends NonBlankString[ClientLastName] diff --git a/model/src/lu/foyer/contracts/Employee.scala b/model/src/lu/foyer/contracts/Employee.scala index b50d690..c258fa6 100644 --- a/model/src/lu/foyer/contracts/Employee.scala +++ b/model/src/lu/foyer/contracts/Employee.scala @@ -2,7 +2,6 @@ package lu.foyer package contracts import zio.schema.* -import java.util.Currency opaque type EmployeeDisplayName <: String = String object EmployeeDisplayName extends NonBlankString[EmployeeDisplayName] diff --git a/model/src/lu/foyer/contracts/Vehicle.scala b/model/src/lu/foyer/contracts/Vehicle.scala index 020c489..3699621 100644 --- a/model/src/lu/foyer/contracts/Vehicle.scala +++ b/model/src/lu/foyer/contracts/Vehicle.scala @@ -1,9 +1,10 @@ package lu.foyer package contracts -import zio.schema.* import java.util.Currency +import zio.schema.* + opaque type VehiclePlate <: String = String object VehiclePlate extends NonBlankString[VehiclePlate]