Improve error handling
This commit is contained in:
parent
87bd780f9f
commit
e6a8150483
7 changed files with 78 additions and 62 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
version = "3.8.3"
|
version = "3.10.1"
|
||||||
|
|
||||||
preset=defaultWithAlign
|
preset=defaultWithAlign
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,15 @@ import zio.http.codec.*
|
||||||
import zio.http.codec.PathCodec.path
|
import zio.http.codec.PathCodec.path
|
||||||
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.logging.ConsoleLoggerConfig
|
||||||
|
import zio.logging.LogColor
|
||||||
|
import zio.logging.LogFilter
|
||||||
|
import zio.logging.LogFormat.*
|
||||||
|
import zio.logging.consoleLogger
|
||||||
|
import zio.schema.codec.JsonCodec.ExplicitConfig
|
||||||
|
|
||||||
import lu.foyer.clients.*
|
import lu.foyer.clients.*
|
||||||
import lu.foyer.contracts.*
|
import lu.foyer.contracts.*
|
||||||
import zio.schema.codec.JsonCodec.ExplicitConfig
|
|
||||||
|
|
||||||
object HttpServer:
|
object HttpServer:
|
||||||
|
|
||||||
|
|
@ -28,8 +33,19 @@ object HttpServer:
|
||||||
++ SwaggerUI.routes("docs" / "openapi", openAPI)
|
++ SwaggerUI.routes("docs" / "openapi", openAPI)
|
||||||
|
|
||||||
object App extends ZIOAppDefault:
|
object App extends ZIOAppDefault:
|
||||||
|
private val logFormat =
|
||||||
|
label("timestamp", timestamp.fixed(32)).color(LogColor.BLUE) |-|
|
||||||
|
label("level", level.fixed(5)).highlight |-|
|
||||||
|
label("thread", fiberId).color(LogColor.WHITE) |-|
|
||||||
|
label("message", quoted(line)).highlight |-|
|
||||||
|
cause
|
||||||
|
|
||||||
override val bootstrap = CodecConfig.configLayer(
|
val logFilter = LogFilter.LogLevelByNameConfig(LogLevel.Debug)
|
||||||
|
|
||||||
|
override val bootstrap =
|
||||||
|
Runtime.removeDefaultLoggers >>>
|
||||||
|
consoleLogger(ConsoleLoggerConfig(logFormat, logFilter)) >>>
|
||||||
|
CodecConfig.configLayer(
|
||||||
CodecConfig(explicitNulls = ExplicitConfig(encoding = false, decoding = false))
|
CodecConfig(explicitNulls = ExplicitConfig(encoding = false, decoding = false))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -55,3 +71,4 @@ object App extends ZIOAppDefault:
|
||||||
PremiumServiceImpl.layer,
|
PremiumServiceImpl.layer,
|
||||||
EmployeeServiceImpl.layer
|
EmployeeServiceImpl.layer
|
||||||
)
|
)
|
||||||
|
end App
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import zio.http.*
|
||||||
import zio.http.codec.*
|
import zio.http.codec.*
|
||||||
import zio.http.endpoint.*
|
import zio.http.endpoint.*
|
||||||
import zio.schema.*
|
import zio.schema.*
|
||||||
import zio.schema.annotation.discriminatorName
|
|
||||||
|
|
||||||
import lu.foyer.JsonApiResponse.Many
|
import lu.foyer.JsonApiResponse.Many
|
||||||
import lu.foyer.JsonApiResponse.One
|
import lu.foyer.JsonApiResponse.One
|
||||||
|
|
@ -36,15 +35,6 @@ object JsonApiResponse:
|
||||||
case class RelationshipsData(id: String, `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")
|
|
||||||
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
|
|
||||||
end JsonApiResponse
|
|
||||||
|
|
||||||
final case class ProxyHeaders(
|
final case class ProxyHeaders(
|
||||||
protocol: Option[String],
|
protocol: Option[String],
|
||||||
host: Option[String],
|
host: Option[String],
|
||||||
|
|
@ -80,26 +70,26 @@ trait JsonApiController:
|
||||||
endpoint: Endpoint[PathInput, Input, ZNothing, ZNothing, AuthType.None]
|
endpoint: Endpoint[PathInput, Input, ZNothing, ZNothing, AuthType.None]
|
||||||
)
|
)
|
||||||
inline def jsonApiOne[Output: Schema]
|
inline def jsonApiOne[Output: Schema]
|
||||||
: Endpoint[PathInput, Input, JsonApiResponse.Error, One[Output], AuthType.None.type] =
|
: Endpoint[PathInput, Input, JsonApiError, One[Output], AuthType.None.type] =
|
||||||
jsonApiOneWithStatus(Status.Ok)
|
jsonApiOneWithStatus(Status.Ok)
|
||||||
|
|
||||||
def jsonApiOneWithStatus[Output: Schema](status: Status) =
|
def jsonApiOneWithStatus[Output: Schema](status: Status) =
|
||||||
endpoint
|
endpoint
|
||||||
.out[JsonApiResponse.One[Output]](status)
|
.out[JsonApiResponse.One[Output]](status)
|
||||||
.outErrors[JsonApiResponse.Error](
|
.outErrors[JsonApiError](
|
||||||
HttpCodec.error[JsonApiResponse.Error.NotFound](Status.NotFound),
|
HttpCodec.error[JsonApiError.NotFound](Status.NotFound),
|
||||||
HttpCodec.error[JsonApiResponse.Error.InternalServerError](Status.InternalServerError)
|
HttpCodec.error[JsonApiError.InternalServerError](Status.InternalServerError)
|
||||||
)
|
)
|
||||||
def jsonApiMany[Output: Schema] =
|
def jsonApiMany[Output: Schema] =
|
||||||
endpoint
|
endpoint
|
||||||
.out[JsonApiResponse.Many[Output]]
|
.out[JsonApiResponse.Many[Output]]
|
||||||
.outError[JsonApiResponse.Error](Status.InternalServerError)
|
.outError[JsonApiError](Status.InternalServerError)
|
||||||
|
|
||||||
extension [PathInput, Input, Output, Auth <: AuthType](
|
extension [PathInput, Input, Output, Auth <: AuthType](
|
||||||
endpoint: Endpoint[PathInput, Input, JsonApiResponse.Error, One[Output], AuthType.None]
|
endpoint: Endpoint[PathInput, Input, JsonApiError, One[Output], AuthType.None]
|
||||||
)
|
)
|
||||||
def implementJsonApiOne[Env, A](
|
def implementJsonApiOne[Env, A](
|
||||||
f: Input => RIO[Env, Option[A]],
|
f: Input => ZIO[Env, JsonApiError | Throwable, Option[A]],
|
||||||
getId: A => String,
|
getId: A => String,
|
||||||
getEntity: A => Output,
|
getEntity: A => Output,
|
||||||
onthology: String = this.onthology,
|
onthology: String = this.onthology,
|
||||||
|
|
@ -113,11 +103,12 @@ trait JsonApiController:
|
||||||
endpoint.implement(input =>
|
endpoint.implement(input =>
|
||||||
for
|
for
|
||||||
item <- f(input)
|
item <- f(input)
|
||||||
.mapError(e => JsonApiResponse.Error.InternalServerError(e.getMessage))
|
.tapErrorCause(ZIO.logErrorCause(_))
|
||||||
.flatMap {
|
.mapError {
|
||||||
case Some(paged) => ZIO.succeed(paged)
|
case e: Throwable => JsonApiError.InternalServerError(e.getMessage)
|
||||||
case None => ZIO.fail(JsonApiResponse.Error.NotFound(input.toString))
|
case e: JsonApiError => e
|
||||||
}
|
}
|
||||||
|
.someOrFail(JsonApiError.NotFound(input.toString))
|
||||||
headers <- ZIO.service[ProxyHeaders]
|
headers <- ZIO.service[ProxyHeaders]
|
||||||
yield JsonApiResponse.One(
|
yield JsonApiResponse.One(
|
||||||
JsonApiResponse.Entity(
|
JsonApiResponse.Entity(
|
||||||
|
|
@ -146,7 +137,7 @@ trait JsonApiController:
|
||||||
)
|
)
|
||||||
|
|
||||||
def implementJsonApiOneEvent[Env](
|
def implementJsonApiOneEvent[Env](
|
||||||
f: Input => RIO[Env, Option[Event[Output]]]
|
f: Input => ZIO[Env, JsonApiError | Throwable, Option[Event[Output]]]
|
||||||
)(implicit trace: Trace
|
)(implicit trace: Trace
|
||||||
): Route[Env & ProxyHeaders, Nothing] =
|
): Route[Env & ProxyHeaders, Nothing] =
|
||||||
implementJsonApiOne(
|
implementJsonApiOne(
|
||||||
|
|
@ -168,7 +159,7 @@ trait JsonApiController:
|
||||||
end extension
|
end extension
|
||||||
|
|
||||||
extension [PathInput, Input, Output, Auth <: AuthType](
|
extension [PathInput, Input, Output, Auth <: AuthType](
|
||||||
endpoint: Endpoint[PathInput, Input, JsonApiResponse.Error, Many[Output], AuthType.None]
|
endpoint: Endpoint[PathInput, Input, JsonApiError, Many[Output], AuthType.None]
|
||||||
)
|
)
|
||||||
def implementJsonApiMany[Env, A](
|
def implementJsonApiMany[Env, A](
|
||||||
f: Input => RIO[Env, Paged[A]],
|
f: Input => RIO[Env, Paged[A]],
|
||||||
|
|
@ -205,7 +196,8 @@ trait JsonApiController:
|
||||||
page = JsonApiResponse.Page(number = 0, size = 10)
|
page = JsonApiResponse.Page(number = 0, size = 10)
|
||||||
)
|
)
|
||||||
))
|
))
|
||||||
.mapError(e => JsonApiResponse.Error.InternalServerError(e.getMessage))
|
.tapErrorCause(ZIO.logErrorCause(_))
|
||||||
|
.mapError(e => JsonApiError.InternalServerError(e.getMessage))
|
||||||
)
|
)
|
||||||
|
|
||||||
inline def implementJsonApiManyEntity[Env](
|
inline def implementJsonApiManyEntity[Env](
|
||||||
|
|
|
||||||
11
build.mill
11
build.mill
|
|
@ -6,15 +6,14 @@ import mill.*, scalalib.*
|
||||||
import com.goyeau.mill.scalafix.ScalafixModule
|
import com.goyeau.mill.scalafix.ScalafixModule
|
||||||
|
|
||||||
object Versions:
|
object Versions:
|
||||||
val zio = "2.1.21"
|
val zio = "2.1.22"
|
||||||
val zioJson = "0.7.44"
|
val zioJson = "0.7.44"
|
||||||
val zioSchema = "1.7.5"
|
val zioSchema = "1.7.5"
|
||||||
val zioHttp = "3.5.1"
|
val zioHttp = "3.5.1"
|
||||||
val zioPrelude = "1.0.0-RC42"
|
val zioPrelude = "1.0.0-RC42"
|
||||||
|
|
||||||
|
|
||||||
trait CommonModule extends ScalaModule with ScalafixModule:
|
trait CommonModule extends ScalaModule with ScalafixModule:
|
||||||
def scalaVersion = "3.7.2"
|
def scalaVersion = "3.7.3"
|
||||||
def scalacOptions = Seq(
|
def scalacOptions = Seq(
|
||||||
"-Wunused:all",
|
"-Wunused:all",
|
||||||
"-preview",
|
"-preview",
|
||||||
|
|
@ -32,17 +31,15 @@ trait CommonModule extends ScalaModule with ScalafixModule:
|
||||||
mvn"dev.zio::zio-prelude:${Versions.zioPrelude}"
|
mvn"dev.zio::zio-prelude:${Versions.zioPrelude}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
object model extends CommonModule
|
object model extends CommonModule
|
||||||
|
|
||||||
object core extends CommonModule:
|
object core extends CommonModule:
|
||||||
def moduleDeps = Seq(model)
|
def moduleDeps = Seq(model)
|
||||||
|
|
||||||
|
|
||||||
object api extends CommonModule:
|
object api extends CommonModule:
|
||||||
def moduleDeps = Seq(core)
|
def moduleDeps = Seq(core)
|
||||||
def mvnDeps = Seq(
|
def mvnDeps = Seq(
|
||||||
mvn"dev.zio::zio:${Versions.zio}",
|
mvn"dev.zio::zio:${Versions.zio}",
|
||||||
mvn"dev.zio::zio-http:${Versions.zioHttp}"
|
mvn"dev.zio::zio-http:${Versions.zioHttp}",
|
||||||
|
mvn"dev.zio::zio-logging:2.5.1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,13 +28,14 @@ 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: String, command: Command): Task[Event]
|
def onCommand(entityId: String, command: Command): IO[JsonApiError | Throwable, 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: String, state: State, command: Command): Task[Event]
|
def onCommand(entityId: String, state: State, command: Command)
|
||||||
|
: IO[JsonApiError | Throwable, Event]
|
||||||
val isCreate = false
|
val isCreate = false
|
||||||
val commandSchema = summon[Schema[Command]]
|
val commandSchema = summon[Schema[Command]]
|
||||||
|
|
||||||
|
|
@ -45,7 +46,7 @@ class CommandEngine[Command, Event, State](
|
||||||
val stateRepo: StateRepository[State]):
|
val stateRepo: StateRepository[State]):
|
||||||
|
|
||||||
def handleCommand(command: Command, name: String, entityId: String)
|
def handleCommand(command: Command, name: String, entityId: String)
|
||||||
: Task[(lu.foyer.Event[Event], lu.foyer.Entity[State])] =
|
: IO[JsonApiError | Throwable, (lu.foyer.Event[Event], lu.foyer.Entity[State])] =
|
||||||
for
|
for
|
||||||
handler <- ZIO
|
handler <- ZIO
|
||||||
.succeed(handlers.find(_.name == name))
|
.succeed(handlers.find(_.name == name))
|
||||||
|
|
@ -57,7 +58,8 @@ class CommandEngine[Command, Event, State](
|
||||||
.succeed(newStateOption)
|
.succeed(newStateOption)
|
||||||
.someOrFail(new IllegalArgumentException("Reducer cannot resolve state transition"))
|
.someOrFail(new IllegalArgumentException("Reducer cannot resolve state transition"))
|
||||||
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(id => Event(newEntity.entityId, event, id.toString))
|
eventEntity <- Random.nextUUID.map(id => Event(newEntity.entityId, event, id.toString))
|
||||||
_ <- eventRepo.insert(eventEntity.eventId, eventEntity)
|
_ <- eventRepo.insert(eventEntity.eventId, eventEntity)
|
||||||
|
|
@ -69,7 +71,7 @@ class CommandEngine[Command, Event, State](
|
||||||
entityId: String,
|
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
|
): IO[JsonApiError | Throwable, (Event, Option[State])] = (entityOption, handler) match
|
||||||
case (None, h) if !h.isUpdate =>
|
case (None, h) if !h.isUpdate =>
|
||||||
h.asInstanceOf[CommandHandlerCreate[Command, Event]]
|
h.asInstanceOf[CommandHandlerCreate[Command, Event]]
|
||||||
.onCommand(entityId, command)
|
.onCommand(entityId, command)
|
||||||
|
|
|
||||||
12
core/src/lu/foyer/JsonApiError.scala
Normal file
12
core/src/lu/foyer/JsonApiError.scala
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
package lu.foyer
|
||||||
|
|
||||||
|
import zio.schema.*
|
||||||
|
import zio.schema.annotation.discriminatorName
|
||||||
|
|
||||||
|
@discriminatorName("errorType")
|
||||||
|
enum JsonApiError derives Schema:
|
||||||
|
case NotFound(id: String)
|
||||||
|
case InternalServerError(e: String)
|
||||||
|
object JsonApiError:
|
||||||
|
given Schema[JsonApiError.NotFound] = DeriveSchema.gen
|
||||||
|
given Schema[JsonApiError.InternalServerError] = DeriveSchema.gen
|
||||||
|
|
@ -29,22 +29,21 @@ class SubscribeHandler(
|
||||||
|
|
||||||
val name = "create"
|
val name = "create"
|
||||||
|
|
||||||
def onCommand(entityId: String, command: ContractCommand.Subscribe)
|
override def onCommand(entityId: String, command: ContractCommand.Subscribe) =
|
||||||
: Task[ContractEvent.Subscribed] =
|
|
||||||
for
|
for
|
||||||
holder <- clientStateRepo.fetchOne(command.holder).someOrFailException
|
holder <- clientStateRepo
|
||||||
|
.fetchOne(command.holder)
|
||||||
|
.someOrFail(JsonApiError.NotFound(command.holder))
|
||||||
residentialAddress <-
|
residentialAddress <-
|
||||||
holder.data match
|
holder.data match
|
||||||
case ClientState.Actif(_, _, _, _, _, _, Some(address)) =>
|
case ClientState.Actif(address = Some(address)) => ZIO.succeed(address)
|
||||||
ZIO.succeed(address)
|
case _ => ZIO.fail(JsonApiError.NotFound(command.holder))
|
||||||
case _ =>
|
|
||||||
ZIO.fail(new IllegalArgumentException(s"No active holder found for ${command.holder}"))
|
|
||||||
premium <-
|
premium <-
|
||||||
ZIO
|
ZIO
|
||||||
.fromEither(
|
.fromEither(
|
||||||
premiumService.computePremium(command.formula, command.vehicle, residentialAddress)
|
premiumService.computePremium(command.formula, command.vehicle, residentialAddress)
|
||||||
)
|
)
|
||||||
.mapError(new IllegalArgumentException(_))
|
.mapError(JsonApiError.InternalServerError(_))
|
||||||
yield ContractEvent.Subscribed(
|
yield ContractEvent.Subscribed(
|
||||||
command.product,
|
command.product,
|
||||||
command.holder,
|
command.holder,
|
||||||
|
|
@ -52,7 +51,6 @@ class SubscribeHandler(
|
||||||
command.formula,
|
command.formula,
|
||||||
premium
|
premium
|
||||||
)
|
)
|
||||||
end SubscribeHandler
|
|
||||||
|
|
||||||
class AmendHandler(clientStateRepo: StateRepository[ClientState], premiumService: PremiumService)
|
class AmendHandler(clientStateRepo: StateRepository[ClientState], premiumService: PremiumService)
|
||||||
extends CommandHandlerUpdate[ContractCommand.Amend, ContractEvent.Amended, ContractState]:
|
extends CommandHandlerUpdate[ContractCommand.Amend, ContractEvent.Amended, ContractState]:
|
||||||
|
|
@ -65,8 +63,7 @@ class AmendHandler(clientStateRepo: StateRepository[ClientState], premiumService
|
||||||
holder <- clientStateRepo.fetchOne(state.holder).someOrFailException
|
holder <- clientStateRepo.fetchOne(state.holder).someOrFailException
|
||||||
residentialAddress <-
|
residentialAddress <-
|
||||||
holder.data match
|
holder.data match
|
||||||
case ClientState.Actif(_, _, _, _, _, _, Some(address)) =>
|
case ClientState.Actif(address = Some(address)) => ZIO.succeed(address)
|
||||||
ZIO.succeed(address)
|
|
||||||
case _ =>
|
case _ =>
|
||||||
ZIO.fail(new IllegalArgumentException(s"No active holder found for ${state.holder}"))
|
ZIO.fail(new IllegalArgumentException(s"No active holder found for ${state.holder}"))
|
||||||
premium <- ZIO
|
premium <- ZIO
|
||||||
|
|
@ -84,7 +81,6 @@ class AmendHandler(clientStateRepo: StateRepository[ClientState], premiumService
|
||||||
command.formula.getOrElse(state.formula),
|
command.formula.getOrElse(state.formula),
|
||||||
premium
|
premium
|
||||||
)
|
)
|
||||||
end AmendHandler
|
|
||||||
|
|
||||||
class ApproveHandler(employeeService: EmployeeService)
|
class ApproveHandler(employeeService: EmployeeService)
|
||||||
extends CommandHandlerUpdate[
|
extends CommandHandlerUpdate[
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue