All Downloads are FREE. Search and download functionalities are using the official Maven repository.

zio.http.codec.HttpContentCodec.scala Maven / Gradle / Ivy

package zio.http.codec

import scala.collection.immutable.ListMap

import zio._

import zio.stream.ZPipeline

import zio.schema.codec._
import zio.schema.{DeriveSchema, Schema}

import zio.http.Header.Accept.MediaTypeWithQFactor
import zio.http._
import zio.http.internal.HeaderOps
import zio.http.template._

sealed trait HttpContentCodec[A] { self =>
  def choices: ListMap[MediaType, BinaryCodecWithSchema[A]]

  /**
   * A right biased merge of two HttpContentCodecs.
   */
  def ++(that: HttpContentCodec[A]): HttpContentCodec[A] =
    HttpContentCodec.Choices(choices ++ that.choices)

  def decodeRequest(request: Request, config: CodecConfig): Task[A] = {
    val contentType = mediaTypeFromContentTypeHeader(request)
    lookup(contentType) match {
      case Some((_, codec)) =>
        request.body.asChunk.flatMap { bytes =>
          ZIO.fromEither(codec.codec(config).decode(bytes))
        }
      case None             =>
        ZIO.fail(throw new IllegalArgumentException(s"No codec found for content type $contentType"))
    }
  }

  def decodeRequest(request: Request): Task[A] =
    CodecConfig.codecRef.getWith(decodeRequest(request, _))

  def decodeResponse(response: Response, config: CodecConfig): Task[A] = {
    val contentType = mediaTypeFromContentTypeHeader(response)
    lookup(contentType) match {
      case Some((_, codec)) =>
        response.body.asChunk.flatMap { bytes =>
          ZIO.fromEither(codec.codec(config).decode(bytes))
        }
      case None             =>
        ZIO.fail(throw new IllegalArgumentException(s"No codec found for content type $contentType"))
    }
  }

  def decodeResponse(response: Response): Task[A] =
    CodecConfig.codecRef.getWith(decodeResponse(response, _))

  private def mediaTypeFromContentTypeHeader(header: HeaderOps[_]) = {
    if (header.headers.contains(Header.ContentType.name)) {
      val contentType = header.headers.getUnsafe(Header.ContentType.name)
      if (MediaType.contentTypeMap.contains(contentType)) {
        MediaType.contentTypeMap(contentType)
      } else {
        MediaType.unsafeParseCustomMediaType(contentType)
      }
    } else {
      MediaType.application.`json`
    }
  }

  def encode(value: A, config: CodecConfig = CodecConfig.defaultConfig): Either[String, Body] = {
    if (choices.isEmpty) {
      Left("No codec defined")
    } else {
      Right(Body.fromChunk(choices.head._2.codec(config).encode(value), mediaType = choices.head._1))
    }
  }

  def only(mediaType: MediaType): HttpContentCodec[A] =
    if (lookup(mediaType).isEmpty) {
      throw new IllegalArgumentException(s"MediaType $mediaType is not supported by $self")
    } else {
      HttpContentCodec.Filtered(self, mediaType)
    }

  def only(mediaType: Option[MediaType]): HttpContentCodec[A] =
    mediaType match {
      case Some(value) => only(value)
      case None        => self
    }

  private[http] def chooseFirst(mediaTypes: Chunk[MediaTypeWithQFactor]): (MediaType, BinaryCodecWithSchema[A]) =
    if (mediaTypes.isEmpty) {
      (defaultMediaType, defaultBinaryCodecWithSchema)
    } else {
      var i                                             = 0
      var result: (MediaType, BinaryCodecWithSchema[A]) = null
      while (i < mediaTypes.size && result == null) {
        val mediaType    = mediaTypes(i)
        val lookupResult = lookup(mediaType.mediaType)
        if (lookupResult.isDefined) result = lookupResult.get
        i += 1
      }
      if (result == null) {
        throw new IllegalArgumentException(s"None of the media types $mediaTypes are supported by $self")
      } else {
        result
      }
    }

  private[http] def chooseFirstOrDefault(
    mediaTypes: Chunk[MediaTypeWithQFactor],
  ): (MediaType, BinaryCodecWithSchema[A]) =
    if (mediaTypes.isEmpty) {
      (defaultMediaType, defaultBinaryCodecWithSchema)
    } else {
      var i                                             = 0
      var result: (MediaType, BinaryCodecWithSchema[A]) = null
      while (i < mediaTypes.size && result == null) {
        val mediaType    = mediaTypes(i)
        val lookupResult = lookup(mediaType.mediaType)
        if (lookupResult.isDefined) result = lookupResult.get
        i += 1
      }
      if (result == null) (defaultMediaType, defaultBinaryCodecWithSchema)
      else result
    }

  def lookup(mediaType: MediaType): Option[(MediaType, BinaryCodecWithSchema[A])]

  private[http] val defaultMediaType: MediaType =
    choices.headOption.map(_._1).getOrElse {
      throw new IllegalArgumentException(s"No codec defined")
    }

  private[http] val defaultCodec: BinaryCodec[A] =
    choices.headOption.map(_._2.codec(CodecConfig.defaultConfig)).getOrElse {
      throw new IllegalArgumentException(s"No codec defined")
    }

  private[http] val defaultSchema: Schema[A] = choices.headOption.map(_._2.schema).getOrElse {
    throw new IllegalArgumentException(s"No codec defined")
  }

  private[http] val defaultBinaryCodecWithSchema: BinaryCodecWithSchema[A] =
    choices.headOption.map(_._2).getOrElse {
      throw new IllegalArgumentException(s"No codec defined")
    }
}

object HttpContentCodec {
  final case class Choices[A](
    choices: ListMap[MediaType, BinaryCodecWithSchema[A]],
  ) extends HttpContentCodec[A] {
    private var lookupCache: Map[MediaType, Option[(MediaType, BinaryCodecWithSchema[A])]] = Map.empty

    override def lookup(mediaType: MediaType): Option[(MediaType, BinaryCodecWithSchema[A])] = {
      if (lookupCache.contains(mediaType)) {
        lookupCache(mediaType)
      } else {
        val codec = choices.collectFirst { case (mt, codec) if mt.matches(mediaType) => mt -> codec }
        lookupCache = lookupCache + (mediaType -> codec)
        codec
      }
    }
  }

  final case class Filtered[A](codec: HttpContentCodec[A], mediaType: MediaType) extends HttpContentCodec[A] {
    self =>
    override lazy val choices: ListMap[MediaType, BinaryCodecWithSchema[A]] =
      codec.choices.filter(_._1 == mediaType)

    private val choice = choices.headOption

    override def lookup(mediaType: MediaType): Option[(MediaType, BinaryCodecWithSchema[A])] =
      if (self.mediaType.matches(mediaType)) choice else None
  }

  private final case class DefaultCodecError(name: String, message: String)

  private object DefaultCodecError {
    implicit val schema: Schema[DefaultCodecError] = DeriveSchema.gen[DefaultCodecError]
  }

  private val NameExtractor    = """.*

([^<]+)

.*""".r private val MessageExtractor = """.*

([^<]+)

.*""".r private val domBasedSchema: Schema[HttpCodecError] = Schema[Dom].transformOrFail[HttpCodecError]( dom => { val encoded = dom.encode val name = encoded match { case NameExtractor(name) => Some(name) case _ => None } val message = encoded match { case MessageExtractor(message) => Some(message) case _ => None } (name, message) match { case (Some(name), Some(message)) => Right(HttpCodecError.CustomError(name, message)) case _ => Left("Could not extract name and message from the DOM") } }, { case HttpCodecError.CustomError(name, message) => Right( html( body( h1("Codec Error"), p("There was an error en-/decoding the request/response"), p(name, idAttr := "name"), p(message, idAttr := "message"), ), ), ) case e: HttpCodecError => Right( html( body( h1("Codec Error"), p("There was an error en-/decoding the request/response"), p(e.productPrefix, idAttr := "name"), p(e.getMessage(), idAttr := "message"), ), ), ) }, ) private val defaultCodecErrorSchema: Schema[HttpCodecError] = Schema[DefaultCodecError].transformOrFail[HttpCodecError]( codecError => Right(HttpCodecError.CustomError(codecError.name, codecError.message)), { case HttpCodecError.CustomError(name, message) => Right(DefaultCodecError(name, message)) case e: HttpCodecError => Right(DefaultCodecError(e.productPrefix, e.getMessage())) }, ) private val defaultHttpContentCodec: HttpContentCodec[HttpCodecError] = HttpContentCodec.from( MediaType.text.`html` -> BinaryCodecWithSchema(TextBinaryCodec.fromSchema(domBasedSchema), domBasedSchema), MediaType.application.json -> BinaryCodecWithSchema( JsonCodec.schemaBasedBinaryCodec(defaultCodecErrorSchema), defaultCodecErrorSchema, ), ) val responseErrorCodec: HttpCodec[HttpCodecType.ResponseType, HttpCodecError] = ContentCodec.content(defaultHttpContentCodec) ++ StatusCodec.BadRequest private var fromSchemaCache: Map[Schema[_], HttpContentCodec[_]] = Map.empty def apply[A](choices: ListMap[MediaType, BinaryCodecWithSchema[A]]): HttpContentCodec[A] = Choices(choices) def from[A]( codec: (MediaType, BinaryCodecWithSchema[A]), codecs: (MediaType, BinaryCodecWithSchema[A])*, ): HttpContentCodec[A] = HttpContentCodec.Choices(ListMap((codec +: codecs): _*)) implicit def fromSchema[A](implicit schema: Schema[A]): HttpContentCodec[A] = { if (fromSchemaCache.contains(schema)) { fromSchemaCache(schema).asInstanceOf[HttpContentCodec[A]] } else { val codec = json.only[A] ++ protobuf.only[A] ++ text.only[A] fromSchemaCache = fromSchemaCache + (schema -> codec) codec } } object json { private var jsonCodecCache: Map[Schema[_], HttpContentCodec[_]] = Map.empty def only[A](implicit schema: Schema[A]): HttpContentCodec[A] = if (jsonCodecCache.contains(schema)) { jsonCodecCache(schema).asInstanceOf[HttpContentCodec[A]] } else { val codec = HttpContentCodec.Choices( ListMap( MediaType.application.`json` -> BinaryCodecWithSchema( config => JsonCodec.schemaBasedBinaryCodec[A]( JsonCodec.Config(ignoreEmptyCollections = config.ignoreEmptyCollections), )(schema), schema, ), ), ) jsonCodecCache = jsonCodecCache + (schema -> codec) codec } } object protobuf { private var protobufCodecCache: Map[Schema[_], HttpContentCodec[_]] = Map.empty def only[A](implicit schema: Schema[A]): HttpContentCodec[A] = if (protobufCodecCache.contains(schema)) { protobufCodecCache(schema).asInstanceOf[HttpContentCodec[A]] } else { val codec = HttpContentCodec.Choices( ListMap( MediaType.parseCustomMediaType("application/protobuf").get -> BinaryCodecWithSchema(ProtobufCodec.protobufCodec[A], schema), ), ) protobufCodecCache = protobufCodecCache + (schema -> codec) codec } } object text { private var textCodecCache: Map[Schema[_], HttpContentCodec[_]] = Map.empty def only[A](implicit schema: Schema[A]): HttpContentCodec[A] = if (textCodecCache.contains(schema)) { textCodecCache(schema).asInstanceOf[HttpContentCodec[A]] } else { val codec = HttpContentCodec.Choices( ListMap( MediaType.text.`plain` -> BinaryCodecWithSchema(http.codec.TextBinaryCodec.fromSchema[A](schema), schema), MediaType.application.`octet-stream` -> BinaryCodecWithSchema(http.codec.TextBinaryCodec.fromSchema[A](schema), schema), ), ) textCodecCache = textCodecCache + (schema -> codec) codec } } private val ByteChunkBinaryCodec: BinaryCodec[Chunk[Byte]] = new BinaryCodec[Chunk[Byte]] { override def encode(value: Chunk[Byte]): Chunk[Byte] = value override def decode(bytes: Chunk[Byte]): Either[DecodeError, Chunk[Byte]] = Right(bytes) override def streamDecoder: ZPipeline[Any, DecodeError, Byte, Chunk[Byte]] = ZPipeline.identity[Byte].chunks override def streamEncoder: ZPipeline[Any, Nothing, Chunk[Byte], Byte] = ZPipeline.identity[Chunk[Byte]].flattenChunks } implicit val byteChunkCodec: HttpContentCodec[Chunk[Byte]] = { HttpContentCodec.Choices( ListMap( MediaType.allMediaTypes .filter(_.binary) .map(mt => mt -> BinaryCodecWithSchema(ByteChunkBinaryCodec, Schema.chunk[Byte])): _*, ), ) } private val ByteBinaryCodec: BinaryCodec[Byte] = new BinaryCodec[Byte] { override def encode(value: Byte): Chunk[Byte] = Chunk.single(value) override def decode(bytes: Chunk[Byte]): Either[DecodeError, Byte] = if (bytes.size == 1) Right(bytes.head) else Left(DecodeError.ReadError(Cause.empty, "Expected a single byte")) override def streamDecoder: ZPipeline[Any, DecodeError, Byte, Byte] = ZPipeline.identity[Byte] override def streamEncoder: ZPipeline[Any, Nothing, Byte, Byte] = ZPipeline.identity[Byte] } implicit val byteCodec: HttpContentCodec[Byte] = { HttpContentCodec.Choices( ListMap( MediaType.allMediaTypes .filter(_.binary) .map(mt => mt -> BinaryCodecWithSchema(ByteBinaryCodec, Schema[Byte])): _*, ), ) } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy