144 lines
4.4 KiB
Scala
144 lines
4.4 KiB
Scala
|
|
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
|