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

227 lines
7.6 KiB
Scala
Raw Normal View History

2025-03-03 00:24:13 +01:00
package lu.foyer
import zio.*
import zio.http.*
import zio.http.codec.*
import zio.http.endpoint.*
2025-10-06 18:30:22 +02:00
import zio.schema.*
2025-10-13 15:46:22 +02:00
import lu.foyer.JsonApiResponse.Many
import lu.foyer.JsonApiResponse.One
2025-03-03 00:24:13 +01:00
object JsonApiResponse:
2025-10-06 18:30:22 +02:00
case class One[T](data: Entity[T]) derives Schema
2025-03-03 00:24:13 +01:00
2025-10-06 18:30:22 +02:00
case class Many[T](data: List[Entity[T]], links: Map[String, String], meta: Meta) derives Schema
2025-03-03 00:24:13 +01:00
2025-10-06 18:30:22 +02:00
case class Meta(totalRecords: Option[Long], totalPages: Option[Long], page: Page) derives Schema
case class Page(number: Int, size: Int) derives Schema
2025-03-03 00:24:13 +01:00
2025-10-06 18:30:22 +02:00
case class Entity[T](
`type`: String,
2025-10-13 15:46:22 +02:00
id: String,
2025-10-06 18:30:22 +02:00
attributes: T,
relationships: Option[Relationships] = None,
links: Map[String, String])
derives Schema
2025-03-03 00:24:13 +01:00
2025-10-06 18:30:22 +02:00
case class Relationships(_entity: RelationshipsEntity) derives Schema
object Relationships:
2025-10-13 15:46:22 +02:00
def apply(id: String, `type`: String, entityUrl: String): Relationships = Relationships(
2025-10-06 18:30:22 +02:00
RelationshipsEntity(RelationshipsData(id, `type`), RelationshipsLinks(entityUrl))
)
case class RelationshipsEntity(data: RelationshipsData, links: RelationshipsLinks) derives Schema
2025-10-13 15:46:22 +02:00
case class RelationshipsData(id: String, `type`: String) derives Schema
2025-10-06 18:30:22 +02:00
case class RelationshipsLinks(related: String) derives Schema
2025-03-03 00:24:13 +01:00
2025-10-06 18:30:22 +02:00
final case class ProxyHeaders(
protocol: Option[String],
host: Option[String],
prefix: Option[String])
derives Schema:
def rootUrl =
(for
proto <- protocol
host <- host
prefix <- prefix
yield s"$proto://$host$prefix")
.getOrElse("")
2025-03-03 00:24:13 +01:00
2025-10-06 18:30:22 +02:00
trait JsonApiController:
2025-03-03 00:24:13 +01:00
def onthology: String
2025-10-06 18:30:22 +02:00
def entityName: String
def allEntities: List[String]
val proxyHeadersAspect = HandlerAspect.interceptIncomingHandler(
Handler.fromFunction[Request](request =>
(
request,
ProxyHeaders(
protocol = request.headers.get("X-Forwarded-Proto"),
host = request.headers.get("X-Forwarded-Host"),
prefix = request.headers.get("X-Forwarded-Prefix")
)
)
)
)
2025-03-03 00:24:13 +01:00
extension [PathInput, Input, Auth <: AuthType](
endpoint: Endpoint[PathInput, Input, ZNothing, ZNothing, AuthType.None]
)
2025-10-06 18:30:22 +02:00
inline def jsonApiOne[Output: Schema]
2025-10-22 15:30:36 +02:00
: Endpoint[PathInput, Input, JsonApiError, One[Output], AuthType.None.type] =
2025-10-06 18:30:22 +02:00
jsonApiOneWithStatus(Status.Ok)
def jsonApiOneWithStatus[Output: Schema](status: Status) =
2025-03-03 00:24:13 +01:00
endpoint
2025-10-06 18:30:22 +02:00
.out[JsonApiResponse.One[Output]](status)
2025-10-22 15:30:36 +02:00
.outErrors[JsonApiError](
HttpCodec.error[JsonApiError.NotFound](Status.NotFound),
HttpCodec.error[JsonApiError.InternalServerError](Status.InternalServerError)
2025-03-03 00:24:13 +01:00
)
def jsonApiMany[Output: Schema] =
endpoint
.out[JsonApiResponse.Many[Output]]
2025-10-22 15:30:36 +02:00
.outError[JsonApiError](Status.InternalServerError)
2025-03-03 00:24:13 +01:00
extension [PathInput, Input, Output, Auth <: AuthType](
2025-10-22 15:30:36 +02:00
endpoint: Endpoint[PathInput, Input, JsonApiError, One[Output], AuthType.None]
2025-03-03 00:24:13 +01:00
)
def implementJsonApiOne[Env, A](
2025-10-22 15:30:36 +02:00
f: Input => ZIO[Env, JsonApiError | Throwable, Option[A]],
2025-10-13 15:46:22 +02:00
getId: A => String,
2025-10-06 18:30:22 +02:00
getEntity: A => Output,
onthology: String = this.onthology,
links: (A, ProxyHeaders) => Map[String, String] = (_: A, _: ProxyHeaders) => Map.empty,
relationships: (A, ProxyHeaders) => Option[JsonApiResponse.Relationships] = (
_: A,
_: ProxyHeaders
) => None
2025-03-03 00:24:13 +01:00
)(implicit trace: Trace
2025-10-06 18:30:22 +02:00
): Route[Env & ProxyHeaders, Nothing] =
2025-03-03 00:24:13 +01:00
endpoint.implement(input =>
2025-10-06 18:30:22 +02:00
for
item <- f(input)
2025-10-22 15:30:36 +02:00
.tapErrorCause(ZIO.logErrorCause(_))
.mapError {
case e: Throwable => JsonApiError.InternalServerError(e.getMessage)
case e: JsonApiError => e
2025-10-06 18:30:22 +02:00
}
2025-10-22 15:30:36 +02:00
.someOrFail(JsonApiError.NotFound(input.toString))
2025-10-06 18:30:22 +02:00
headers <- ZIO.service[ProxyHeaders]
yield JsonApiResponse.One(
JsonApiResponse.Entity(
id = getId(item),
`type` = onthology,
attributes = getEntity(item),
relationships = relationships(item, headers),
links = links(item, headers)
2025-03-03 00:24:13 +01:00
)
2025-10-06 18:30:22 +02:00
)
2025-03-03 00:24:13 +01:00
)
def implementJsonApiOneEntity[Env](
f: Input => RIO[Env, Option[Entity[Output]]]
)(implicit trace: Trace
2025-10-06 18:30:22 +02:00
): Route[Env & ProxyHeaders, Nothing] =
implementJsonApiOne(
f,
_.entityId,
_.data,
links = (e, headers) =>
Map(
"self" -> s"${headers.rootUrl}/$entityName/${e.entityId}",
"events" -> s"${headers.rootUrl}/$entityName/${e.entityId}/events"
)
)
2025-03-03 00:24:13 +01:00
def implementJsonApiOneEvent[Env](
2025-10-22 15:30:36 +02:00
f: Input => ZIO[Env, JsonApiError | Throwable, Option[Event[Output]]]
2025-03-03 00:24:13 +01:00
)(implicit trace: Trace
2025-10-06 18:30:22 +02:00
): Route[Env & ProxyHeaders, Nothing] =
implementJsonApiOne(
f,
_.eventId,
_.data,
onthology + ":event",
links = (e, headers) =>
Map("self" -> s"${headers.rootUrl}/$entityName/${e.entityId}/events/${e.eventId}"),
relationships = (e, headers) =>
Some(
JsonApiResponse.Relationships(
e.entityId,
onthology,
s"${headers.rootUrl}/$entityName/${e.entityId}"
)
)
)
2025-03-03 00:24:13 +01:00
end extension
extension [PathInput, Input, Output, Auth <: AuthType](
2025-10-22 15:30:36 +02:00
endpoint: Endpoint[PathInput, Input, JsonApiError, Many[Output], AuthType.None]
2025-03-03 00:24:13 +01:00
)
def implementJsonApiMany[Env, A](
f: Input => RIO[Env, Paged[A]],
2025-10-13 15:46:22 +02:00
getId: A => String,
2025-10-06 18:30:22 +02:00
getEntity: A => Output,
links: (A, ProxyHeaders) => Map[String, String] = (_: A, _: ProxyHeaders) => Map.empty
2025-03-03 00:24:13 +01:00
)(implicit trace: Trace
2025-10-06 18:30:22 +02:00
): Route[Env & ProxyHeaders, Nothing] =
endpoint.implement(input =>
(for
paged <- f(input)
headers <- ZIO.service[ProxyHeaders]
yield JsonApiResponse.Many(
paged.items.map(item =>
JsonApiResponse.Entity(
id = getId(item),
`type` = onthology,
attributes = getEntity(item),
links = links(item, headers)
2025-03-03 00:24:13 +01:00
)
2025-10-06 18:30:22 +02:00
),
links = Map( // TODO compute proper pagination
"self" -> s"${headers.rootUrl}/$entityName",
"prev" -> s"${headers.rootUrl}/$entityName?page[number]=0&page[size]=10",
"first" -> s"${headers.rootUrl}/$entityName?page[number]=0&page[size]=10",
"next" -> s"${headers.rootUrl}/$entityName?page[number]=0&page[size]=10",
"last" -> s"${headers.rootUrl}/$entityName?page[number]=0&page[size]=10"
2025-03-03 00:24:13 +01:00
)
2025-10-06 18:30:22 +02:00
++ allEntities.map(e => e -> s"${headers.rootUrl}/$e"),
meta = JsonApiResponse
.Meta(
totalRecords = paged.totals,
totalPages = Some(1),
page = JsonApiResponse.Page(number = 0, size = 10)
)
))
2025-10-22 15:30:36 +02:00
.tapErrorCause(ZIO.logErrorCause(_))
.mapError(e => JsonApiError.InternalServerError(e.getMessage))
2025-03-03 00:24:13 +01:00
)
inline def implementJsonApiManyEntity[Env](
f: Input => RIO[Env, Paged[Entity[Output]]]
)(implicit trace: Trace
2025-10-06 18:30:22 +02:00
): Route[Env & ProxyHeaders, Nothing] =
implementJsonApiMany(
f,
_.entityId,
_.data,
links = (e, headers) =>
Map(
"self" -> s"${headers.rootUrl}/$entityName/${e.entityId}",
"events" -> s"${headers.rootUrl}/$entityName/${e.entityId}/events"
)
)
2025-03-03 00:24:13 +01:00
inline def implementJsonApiManyEvent[Env](
f: Input => RIO[Env, Paged[Event[Output]]]
)(implicit trace: Trace
2025-10-06 18:30:22 +02:00
): Route[Env & ProxyHeaders, Nothing] =
2025-03-03 00:24:13 +01:00
implementJsonApiMany(f, _.eventId, _.data)
end extension
end JsonApiController