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