229 lines
7.7 KiB
Scala
229 lines
7.7 KiB
Scala
package lu.foyer
|
|
|
|
import zio.*
|
|
import zio.http.*
|
|
import zio.http.codec.*
|
|
import zio.http.endpoint.*
|
|
import zio.schema.*
|
|
|
|
import lu.foyer.JsonApiResponse.Many
|
|
import lu.foyer.JsonApiResponse.One
|
|
|
|
object JsonApiResponse:
|
|
|
|
case class One[T](data: Entity[T]) derives Schema
|
|
|
|
case class Many[T](data: List[Entity[T]], links: Map[String, String], meta: Meta) derives Schema
|
|
|
|
case class Meta(totalRecords: Option[Long], totalPages: Option[Long], page: Page) derives Schema
|
|
case class Page(number: Int, size: Int) derives Schema
|
|
|
|
case class Entity[T](
|
|
`type`: String,
|
|
id: String,
|
|
attributes: T,
|
|
relationships: Option[Relationships] = None,
|
|
links: Map[String, String])
|
|
derives Schema
|
|
|
|
case class Relationships(_entity: RelationshipsEntity) derives Schema
|
|
object 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: String, `type`: String) derives Schema
|
|
case class RelationshipsLinks(related: String) derives Schema
|
|
|
|
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("")
|
|
|
|
trait JsonApiController:
|
|
def onthology: String
|
|
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")
|
|
)
|
|
)
|
|
)
|
|
)
|
|
|
|
extension [PathInput, Input, Auth <: AuthType](
|
|
endpoint: Endpoint[PathInput, Input, ZNothing, ZNothing, AuthType.None]
|
|
)
|
|
inline def jsonApiOne[Output: Schema]
|
|
: Endpoint[PathInput, Input, JsonApiError, One[Output], AuthType.None.type] =
|
|
jsonApiOneWithStatus(Status.Ok)
|
|
|
|
def jsonApiOneWithStatus[Output: Schema](status: Status) =
|
|
endpoint
|
|
.out[JsonApiResponse.One[Output]](status)
|
|
.outErrors[JsonApiError](
|
|
HttpCodec.error[JsonApiError.NotFound](Status.NotFound),
|
|
HttpCodec.error[JsonApiError.InternalServerError](Status.InternalServerError)
|
|
)
|
|
def jsonApiMany[Output: Schema] =
|
|
endpoint
|
|
.out[JsonApiResponse.Many[Output]]
|
|
.outError[JsonApiError](Status.InternalServerError)
|
|
|
|
extension [PathInput, Input, Output, Auth <: AuthType](
|
|
endpoint: Endpoint[PathInput, Input, JsonApiError, One[Output], AuthType.None]
|
|
)
|
|
def implementJsonApiOne[Env, A](
|
|
f: Input => ZIO[Env, JsonApiError | Throwable, Option[A]],
|
|
getId: A => String,
|
|
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
|
|
)(implicit trace: Trace
|
|
): Route[Env & ProxyHeaders, Nothing] =
|
|
endpoint.implement(input =>
|
|
for
|
|
item <- f(input)
|
|
.tapError {
|
|
case t: Throwable => ZIO.logErrorCause(Cause.fail(t))
|
|
case _: JsonApiError => ZIO.unit
|
|
}
|
|
.mapError {
|
|
case e: Throwable => JsonApiError.InternalServerError(e.getMessage)
|
|
case e: JsonApiError => e
|
|
}
|
|
.someOrFail(JsonApiError.NotFound(input.toString))
|
|
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)
|
|
)
|
|
)
|
|
)
|
|
|
|
def implementJsonApiOneEntity[Env](
|
|
f: Input => RIO[Env, Option[Entity[Output]]]
|
|
)(implicit trace: Trace
|
|
): 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"
|
|
)
|
|
)
|
|
|
|
def implementJsonApiOneEvent[Env](
|
|
f: Input => ZIO[Env, JsonApiError | Throwable, Option[Event[Output]]]
|
|
)(implicit trace: Trace
|
|
): 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}"
|
|
)
|
|
)
|
|
)
|
|
end extension
|
|
|
|
extension [PathInput, Input, Output, Auth <: AuthType](
|
|
endpoint: Endpoint[PathInput, Input, JsonApiError, Many[Output], AuthType.None]
|
|
)
|
|
def implementJsonApiMany[Env, A](
|
|
f: Input => RIO[Env, Paged[A]],
|
|
getId: A => String,
|
|
getEntity: A => Output,
|
|
links: (A, ProxyHeaders) => Map[String, String] = (_: A, _: ProxyHeaders) => Map.empty
|
|
)(implicit trace: Trace
|
|
): 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)
|
|
)
|
|
),
|
|
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"
|
|
)
|
|
++ allEntities.map(e => e -> s"${headers.rootUrl}/$e"),
|
|
meta = JsonApiResponse
|
|
.Meta(
|
|
totalRecords = paged.totals,
|
|
totalPages = Some(1),
|
|
page = JsonApiResponse.Page(number = 0, size = 10)
|
|
)
|
|
))
|
|
.tapErrorCause(ZIO.logErrorCause(_))
|
|
.mapError(e => JsonApiError.InternalServerError(e.getMessage))
|
|
)
|
|
|
|
inline def implementJsonApiManyEntity[Env](
|
|
f: Input => RIO[Env, Paged[Entity[Output]]]
|
|
)(implicit trace: Trace
|
|
): 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"
|
|
)
|
|
)
|
|
|
|
inline def implementJsonApiManyEvent[Env](
|
|
f: Input => RIO[Env, Paged[Event[Output]]]
|
|
)(implicit trace: Trace
|
|
): Route[Env & ProxyHeaders, Nothing] =
|
|
implementJsonApiMany(f, _.eventId, _.data)
|
|
|
|
end extension
|
|
|
|
end JsonApiController
|