package lu.foyer import zio.* import zio.http.* import zio.http.codec.* import zio.http.endpoint.* import zio.schema.* import zio.schema.annotation.discriminatorName 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 @discriminatorName("errorType") enum Error derives Schema: case NotFound(id: String) case InternalServerError(title: String) object Error: given Schema[Error.NotFound] = DeriveSchema.gen given Schema[Error.InternalServerError] = DeriveSchema.gen end JsonApiResponse 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, JsonApiResponse.Error, One[Output], AuthType.None.type] = jsonApiOneWithStatus(Status.Ok) def jsonApiOneWithStatus[Output: Schema](status: Status) = endpoint .out[JsonApiResponse.One[Output]](status) .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 => 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) .mapError(e => JsonApiResponse.Error.InternalServerError(e.getMessage)) .flatMap { case Some(paged) => ZIO.succeed(paged) case None => ZIO.fail(JsonApiResponse.Error.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 => RIO[Env, 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, JsonApiResponse.Error, 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) ) )) .mapError(e => JsonApiResponse.Error.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