foyer-dsi-assure-2035/api/src/lu/foyer/JsonApiController.scala

144 lines
4.4 KiB
Scala
Raw Normal View History

2025-03-03 00:24:13 +01:00
package lu.foyer
import zio.*
import zio.schema.*
import zio.http.*
import zio.http.codec.*
import zio.http.codec.PathCodec.path
import zio.http.endpoint.*
import lu.foyer.JsonApiResponse.Many
import lu.foyer.JsonApiResponse.One
import java.util.UUID
import scala.annotation.targetName
import zio.schema.annotation.discriminatorName
object JsonApiResponse:
case class One[T](
data: Entity[T],
links: Links)
derives Schema
case class Many[T](
data: List[Entity[T]],
links: Links,
meta: Meta)
derives Schema
case class Links(
self: String,
first: Option[String] = None,
prev: Option[String] = None,
next: Option[String] = None,
last: Option[String] = None)
derives Schema
case class Meta(totalRecords: Option[Long], totalPages: Option[Long]) derives Schema
case class Entity[T](id: UUID, `type`: String, attributes: T) 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)
object Error:
given Schema[Error.NotFound] = DeriveSchema.gen
given Schema[Error.InternalServerError] = DeriveSchema.gen
end JsonApiResponse
trait JsonApiController:
def onthology: String
extension [PathInput, Input, Auth <: AuthType](
endpoint: Endpoint[PathInput, Input, ZNothing, ZNothing, AuthType.None]
)
def jsonApiOne[Output: Schema] =
endpoint
.out[JsonApiResponse.One[Output]]
.outErrors[JsonApiResponse.Error](
HttpCodec.error[JsonApiResponse.Error.NotFound](Status.NotFound),
HttpCodec.error[JsonApiResponse.Error.InternalServerError](Status.InternalServerError)
)
def jsonApiMany[Output: Schema] =
endpoint
.out[JsonApiResponse.Many[Output]]
.outError[JsonApiResponse.Error](Status.InternalServerError)
extension [PathInput, Input, Output, Auth <: AuthType](
endpoint: Endpoint[PathInput, Input, JsonApiResponse.Error, One[Output], AuthType.None]
)
def implementJsonApiOne[Env, A](
f: Input => RIO[Env, Option[A]],
getId: A => UUID,
getEntity: A => Output
)(implicit trace: Trace
): Route[Env, Nothing] =
endpoint.implement(input =>
f(input)
.mapError(e => JsonApiResponse.Error.InternalServerError(e.getMessage))
.someOrFail(JsonApiResponse.Error.NotFound(input.toString))
.map(item =>
JsonApiResponse.One(
JsonApiResponse.Entity(
id = getId(item),
`type` = onthology,
attributes = getEntity(item)
),
JsonApiResponse.Links("https://api.example.org") // TODO
)
)
)
def implementJsonApiOneEntity[Env](
f: Input => RIO[Env, Option[Entity[Output]]]
)(implicit trace: Trace
): Route[Env, Nothing] =
implementJsonApiOne(f, _.entityId, _.data)
def implementJsonApiOneEvent[Env](
f: Input => RIO[Env, Option[Event[Output]]]
)(implicit trace: Trace
): Route[Env, Nothing] =
implementJsonApiOne(f, _.eventId, _.data)
end extension
extension [PathInput, Input, Output, Auth <: AuthType](
endpoint: Endpoint[PathInput, Input, JsonApiResponse.Error, Many[Output], AuthType.None]
)
def implementJsonApiMany[Env, A](
f: Input => RIO[Env, Paged[A]],
getId: A => UUID,
getEntity: A => Output
)(implicit trace: Trace
): Route[Env, Nothing] =
endpoint.implement(
f(_)
.map(paged =>
JsonApiResponse.Many(
paged.items.map(item =>
JsonApiResponse.Entity(id = getId(item), `type` = onthology, getEntity(item))
),
JsonApiResponse.Links("https://api.example.org"),
meta = JsonApiResponse.Meta(paged.totals, None) // TODO
)
)
.mapError(e => JsonApiResponse.Error.InternalServerError(e.getMessage))
)
inline def implementJsonApiManyEntity[Env](
f: Input => RIO[Env, Paged[Entity[Output]]]
)(implicit trace: Trace
): Route[Env, Nothing] =
implementJsonApiMany(f, _.entityId, _.data)
inline def implementJsonApiManyEvent[Env](
f: Input => RIO[Env, Paged[Event[Output]]]
)(implicit trace: Trace
): Route[Env, Nothing] =
implementJsonApiMany(f, _.eventId, _.data)
end extension
end JsonApiController