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

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

/*
 * Copyright 2021 - 2023 Sporta Technologies PVT LTD & the ZIO HTTP contributors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package zio.http.codec

import scala.annotation.tailrec
import scala.reflect.ClassTag

import zio._

import zio.stream.ZStream

import zio.schema.Schema
import zio.schema.annotation._

import zio.http.Header.Accept.MediaTypeWithQFactor
import zio.http._
import zio.http.codec.HttpCodec.Query.QueryType
import zio.http.codec.HttpCodec.{Annotated, Metadata}
import zio.http.codec.internal._

/**
 * A [[zio.http.codec.HttpCodec]] represents a codec for a part of an HTTP
 * request. HttpCodec the HTTP protocol, these parts may be the unconsumed
 * portion of the HTTP path (a route codec), the query string parameters (a
 * query codec), the request headers (a header codec), or the request body (a
 * body codec).
 *
 * A HttpCodec is a purely declarative description of an input, and therefore,
 * it can be used to generate documentation, clients, and client libraries.
 *
 * HttpCodecs are a bit like invertible multi-channel parsers.
 */
sealed trait HttpCodec[-AtomTypes, Value] {
  self =>

  private lazy val encoderDecoder: EncoderDecoder[AtomTypes, Value] = EncoderDecoder(self)

  private def statusCodecs: Chunk[SimpleCodec[Status, _]] =
    self.asInstanceOf[HttpCodec[_, _]] match {
      case HttpCodec.Fallback(left, right, _, _)  => left.statusCodecs ++ right.statusCodecs
      case HttpCodec.Combine(left, right, _)      => left.statusCodecs ++ right.statusCodecs
      case HttpCodec.Annotated(codec, _)          => codec.statusCodecs
      case HttpCodec.TransformOrFail(codec, _, _) => codec.statusCodecs
      case HttpCodec.Empty                        => Chunk.empty
      case HttpCodec.Halt                         => Chunk.empty
      case atom: HttpCodec.Atom[_, _]             =>
        atom match {
          case HttpCodec.Status(codec, _) => Chunk.single(codec)
          case _                          => Chunk.empty
        }
    }

  private lazy val statusCodes: Set[Status] =
    statusCodecs.collect { case SimpleCodec.Specified(status) => status }.toSet

  private lazy val matchesAnyStatus: Boolean =
    statusCodecs.contains(SimpleCodec.Unspecified[Status]())

  /**
   * Returns a new codec that is the same as this one, but has attached docs,
   * which will render whenever docs are generated from the codec.
   */
  final def ??(doc: Doc): HttpCodec[AtomTypes, Value] =
    HttpCodec.Annotated(self, Metadata.Documented(doc))

  final def |[AtomTypes1 <: AtomTypes, Value2](
    that: HttpCodec[AtomTypes1, Value2],
  )(implicit alternator: Alternator[Value, Value2]): HttpCodec[AtomTypes1, alternator.Out] = {
    if (self eq HttpCodec.Halt) that.asInstanceOf[HttpCodec[AtomTypes1, alternator.Out]]
    else if (that eq HttpCodec.Halt) self.asInstanceOf[HttpCodec[AtomTypes1, alternator.Out]]
    else {
      HttpCodec
        .Fallback(self, that, alternator, HttpCodec.Fallback.Condition.IsHttpCodecError)
        .transform[alternator.Out](either => either.fold(alternator.left(_), alternator.right(_)))(value =>
          alternator
            .unleft(value)
            .map(Left(_))
            .orElse(alternator.unright(value).map(Right(_)))
            .get, // TODO: Solve with partiality
        )
    }
  }

  /**
   * Returns a new codec that is the composition of this codec and the specified
   * codec. For codecs that include route codecs, the routes will be decoded
   * sequentially from left to right.
   */
  final def ++[AtomTypes1 <: AtomTypes, Value2](that: HttpCodec[AtomTypes1, Value2])(implicit
    combiner: Combiner[Value, Value2],
  ): HttpCodec[AtomTypes1, combiner.Out] =
    HttpCodec.Combine[AtomTypes1, AtomTypes1, Value, Value2, combiner.Out](self, that, combiner)

  /**
   * To end the route codec and begin with query codec, and re-interprets as
   * PathQueryCodec
   *
   * GET /ab/c :? paramStr("") &
   */
  final def ^?[Value2](
    that: QueryCodec[Value2],
  )(implicit
    combiner: Combiner[Value, Value2],
    ev: HttpCodecType.Path <:< AtomTypes,
  ): HttpCodec[HttpCodecType.PathQuery, combiner.Out] =
    (self ++ that).asInstanceOf[HttpCodec[HttpCodecType.PathQuery, combiner.Out]]

  /**
   * Append more query parameters to either a query codec, or to a pathQuery
   * codec which is a combination of path and query
   */
  final def &[Value2](
    that: QueryCodec[Value2],
  )(implicit
    combiner: Combiner[Value, Value2],
    ev: HttpCodecType.Query <:< AtomTypes,
  ): HttpCodec[HttpCodecType.Query, combiner.Out] =
    self.asQuery ++ that

  /**
   * Produces a flattened collection of alternatives. Once flattened, each codec
   * inside the returned collection is guaranteed to contain no nested
   * alternatives.
   */
  final def alternatives: Chunk[(HttpCodec[AtomTypes, Value], HttpCodec.Fallback.Condition)] =
    HttpCodec.flattenFallbacks(self)

  /**
   * Returns a new codec that is the same as this one, but has attached
   * metadata, such as docs.
   */
  def annotate(metadata: Metadata[Value]): HttpCodec[AtomTypes, Value] =
    HttpCodec.Annotated(self, metadata)

  /**
   * Reinterprets this codec as a query codec assuming evidence that this
   * interpretation is sound.
   */
  final def asQuery(implicit ev: HttpCodecType.Query <:< AtomTypes): QueryCodec[Value] =
    self.asInstanceOf[QueryCodec[Value]]

  /**
   * Transforms the type parameter to `Unit` by specifying that all possible
   * values that can be decoded from this `HttpCodec` are in fact equivalent to
   * the provided canonical value.
   *
   * Note: You should NOT use this method on any codec which can decode
   * semantically distinct values.
   */
  final def const(canonical: => Value): HttpCodec[AtomTypes, Unit] =
    self.transform(_ => ())(_ => canonical)

  final def const[Value2](value2: => Value2)(implicit ev: Unit <:< Value): HttpCodec[AtomTypes, Value2] =
    self.transform(_ => value2)(_ => ev(()))

  /**
   * Uses this codec to decode the Scala value from a request.
   */
  final def decodeRequest(request: Request)(implicit trace: Trace): Task[Value] =
    CodecConfig.codecRef.getWith(
      encoderDecoder.decode(_, request.url, Status.Ok, request.method, request.headers, request.body),
    )

  /**
   * Uses this codec to decode the Scala value from a request.
   */
  final def decodeRequest(request: Request, config: CodecConfig)(implicit trace: Trace): Task[Value] =
    encoderDecoder.decode(config, request.url, Status.Ok, request.method, request.headers, request.body)

  /**
   * Uses this codec to decode the Scala value from a response.
   */
  final def decodeResponse(response: Response, config: CodecConfig = CodecConfig.defaultConfig)(implicit
    trace: Trace,
  ): Task[Value] =
    encoderDecoder.decode(config, URL.empty, response.status, Method.GET, response.headers, response.body)

  def doc: Option[Doc] = {
    @tailrec
    def loop(codec: HttpCodec[_, _]): Option[Doc] =
      codec match {
        case Annotated(_, Metadata.Documented(doc)) => Some(doc)
        case Annotated(codec, _)                    => loop(codec)
        case _                                      => None
      }

    loop(self)
  }

  /**
   * Uses this codec and [[CodecConfig.defaultConfig]] to encode the Scala value
   * into a request.
   */
  final def encodeRequest(value: Value): Request =
    encodeRequest(value, CodecConfig.defaultConfig)

  /**
   * Uses this codec to encode the Scala value into a request.
   */
  final def encodeRequest(value: Value, config: CodecConfig): Request =
    encodeWith(config, value, Chunk.empty)((url, _, method, headers, body) =>
      Request(
        url = url,
        method = method.getOrElse(Method.GET),
        headers = headers,
        body = body,
        version = Version.Default,
        remoteAddress = None,
      ),
    )

  /**
   * Uses this codec to encode the Scala value as a response.
   */
  final def encodeResponse[Z](value: Value, outputTypes: Chunk[MediaTypeWithQFactor], config: CodecConfig): Response =
    encodeWith(config, value, outputTypes)((_, status, _, headers, body) =>
      Response(headers = headers, body = body, status = status.getOrElse(Status.Ok)),
    )

  private final def encodeWith[Z](config: CodecConfig, value: Value, outputTypes: Chunk[MediaTypeWithQFactor])(
    f: (URL, Option[Status], Option[Method], Headers, Body) => Z,
  ): Z =
    encoderDecoder.encodeWith(config, value, outputTypes.sorted)(f)

  def examples(examples: Iterable[(String, Value)]): HttpCodec[AtomTypes, Value] =
    HttpCodec.Annotated(self, Metadata.Examples(Chunk.fromIterable(examples).toMap))

  def examples(example1: (String, Value), examples: (String, Value)*): HttpCodec[AtomTypes, Value] =
    HttpCodec.Annotated(self, Metadata.Examples((example1 +: Chunk.fromIterable(examples)).toMap))

  def examples: Map[String, Value] = Map.empty

  /**
   * Returns a new codec that will expect the value to be equal to the specified
   * value.
   */
  def expect(expected: Value): HttpCodec[AtomTypes, Unit] =
    transformOrFailLeft(actual =>
      if (actual == expected) Right(())
      else Left(s"Expected ${expected} but found ${actual}"),
    )(_ => expected)

  private[http] def matchesStatus(status: Status) =
    matchesAnyStatus || statusCodes.contains(status)

  def named(name: String): HttpCodec[AtomTypes, Value] =
    HttpCodec.Annotated(self, Metadata.Named(name))

  /**
   * Returns a new codec that is the same as this one, but has attached a name.
   * This name is used for documentation generation.
   */
  def named(named: Metadata.Named[Value]): HttpCodec[AtomTypes, Value] =
    HttpCodec.Annotated(self, Metadata.Named(named.name))

  /**
   * Returns a new codec, where the value produced by this one is optional.
   */
  def optional: HttpCodec[AtomTypes, Option[Value]] =
    Annotated(
      if (self eq HttpCodec.Halt) HttpCodec.empty.asInstanceOf[HttpCodec[AtomTypes, Option[Value]]]
      else {
        HttpCodec
          .Fallback(self, HttpCodec.empty, Alternator.either, HttpCodec.Fallback.Condition.isMissingDataOnly)
          .transform[Option[Value]](either => either.fold(Some(_), _ => None))(_.toLeft(()))
      },
      Metadata.Optional(),
    )

  final def orElseEither[AtomTypes1 <: AtomTypes, Value2](
    that: HttpCodec[AtomTypes1, Value2],
  )(implicit alternator: Alternator[Value, Value2]): HttpCodec[AtomTypes1, alternator.Out] =
    self | that

  final def toLeft[R]: HttpCodec[AtomTypes, Either[Value, R]] =
    self.transformOrFail[Either[Value, R]](value => Right(Left(value)))(either =>
      either.swap.left.map(_ => "Error!"),
    ) // TODO: Solve with partiality

  final def toRight[L]: HttpCodec[AtomTypes, Either[L, Value]] =
    self.transformOrFail[Either[L, Value]](value => Right(Right(value)))(either =>
      either.left.map(_ => "Error!"),
    ) // TODO: Solve with partiality

  /**
   * Transforms the type parameter of this HttpCodec from `Value` to `Value2`.
   * Due to the fact that HttpCodec is invariant in its type parameter, the
   * transformation requires not just a function from `Value` to `Value2`, but
   * also, a function from `Value2` to `Value`.
   *
   * One of these functions will be used in decoding, for example, when the
   * endpoint is invoked on the server. The other of these functions will be
   * used in encoding, for example, when a client calls the endpoint on the
   * server.
   */
  final def transform[Value2](f: Value => Value2)(g: Value2 => Value): HttpCodec[AtomTypes, Value2] =
    HttpCodec.TransformOrFail[AtomTypes, Value, Value2](self, in => Right(f(in)), output => Right(g(output)))

  final def transformOrFail[Value2](f: Value => Either[String, Value2])(
    g: Value2 => Either[String, Value],
  ): HttpCodec[AtomTypes, Value2] =
    HttpCodec.TransformOrFail[AtomTypes, Value, Value2](self, f, g)

  final def transformOrFailLeft[Value2](f: Value => Either[String, Value2])(
    g: Value2 => Value,
  ): HttpCodec[AtomTypes, Value2] =
    HttpCodec.TransformOrFail[AtomTypes, Value, Value2](self, f, output => Right(g(output)))

  final def transformOrFailRight[Value2](f: Value => Value2)(
    g: Value2 => Either[String, Value],
  ): HttpCodec[AtomTypes, Value2] =
    HttpCodec.TransformOrFail[AtomTypes, Value, Value2](self, in => Right(f(in)), g)
}

object HttpCodec extends ContentCodecs with HeaderCodecs with MethodCodecs with QueryCodecs with StatusCodecs {
  def enumeration[Value]: Enumeration[Value] =
    new Enumeration[Value](())

  def error[Body0](status: zio.http.Status)(implicit
    schema: Schema[Body0],
  ): HttpCodec[HttpCodecType.Status with HttpCodecType.Content, Body0] =
    content[Body0]("error-response") ++ this.status(status)

  private[http] sealed trait AtomTag
  private[http] object AtomTag {
    case object Status  extends AtomTag
    case object Path    extends AtomTag
    case object Content extends AtomTag
    case object Query   extends AtomTag
    case object Header  extends AtomTag
    case object Method  extends AtomTag
  }

  def empty: HttpCodec[Any, Unit] =
    Empty

  def unused: HttpCodec[Any, ZNothing] = Halt

  final case class Enumeration[Value](unit: Unit) extends AnyVal {
    def f2[AtomTypes, Sub1 <: Value: ClassTag, Sub2 <: Value: ClassTag](
      codec1: HttpCodec[AtomTypes, Sub1],
      codec2: HttpCodec[AtomTypes, Sub2],
    ): HttpCodec[AtomTypes, Value] =
      (codec1 | codec2).transformOrFail(either => Right(either.merge))((value: Value) =>
        value match {
          case sub1: Sub1 => Right(Left(sub1))
          case sub2: Sub2 => Right(Right(sub2))
          case _          => Left(s"Unexpected error type")
        },
      )

    def f3[AtomTypes, Sub1 <: Value: ClassTag, Sub2 <: Value: ClassTag, Sub3 <: Value: ClassTag](
      codec1: HttpCodec[AtomTypes, Sub1],
      codec2: HttpCodec[AtomTypes, Sub2],
      codec3: HttpCodec[AtomTypes, Sub3],
    ): HttpCodec[AtomTypes, Value] =
      (codec1 | codec2 | codec3).transformOrFail(either => Right(either.left.map(_.merge).merge))((value: Value) =>
        value match {
          case sub1: Sub1 => Right(Left(Left(sub1)))
          case sub2: Sub2 => Right(Left(Right(sub2)))
          case sub3: Sub3 => Right(Right(sub3))
          case _          => Left(s"Unexpected error type")
        },
      )

    def f4[
      AtomTypes,
      Sub1 <: Value: ClassTag,
      Sub2 <: Value: ClassTag,
      Sub3 <: Value: ClassTag,
      Sub4 <: Value: ClassTag,
    ](
      codec1: HttpCodec[AtomTypes, Sub1],
      codec2: HttpCodec[AtomTypes, Sub2],
      codec3: HttpCodec[AtomTypes, Sub3],
      codec4: HttpCodec[AtomTypes, Sub4],
    ): HttpCodec[AtomTypes, Value] =
      (codec1 | codec2 | codec3 | codec4).transformOrFail(either =>
        Right(either.left.map(_.left.map(_.merge).merge).merge),
      )((value: Value) =>
        value match {
          case sub1: Sub1 => Right(Left(Left(Left(sub1))))
          case sub2: Sub2 => Right(Left(Left(Right(sub2))))
          case sub3: Sub3 => Right(Left(Right(sub3)))
          case sub4: Sub4 => Right(Right(sub4))
          case _          => Left(s"Unexpected error type")
        },
      )

    def f5[
      AtomTypes,
      Sub1 <: Value: ClassTag,
      Sub2 <: Value: ClassTag,
      Sub3 <: Value: ClassTag,
      Sub4 <: Value: ClassTag,
      Sub5 <: Value: ClassTag,
    ](
      codec1: HttpCodec[AtomTypes, Sub1],
      codec2: HttpCodec[AtomTypes, Sub2],
      codec3: HttpCodec[AtomTypes, Sub3],
      codec4: HttpCodec[AtomTypes, Sub4],
      codec5: HttpCodec[AtomTypes, Sub5],
    ): HttpCodec[AtomTypes, Value] =
      (codec1 | codec2 | codec3 | codec4 | codec5).transformOrFail(either =>
        Right(either.left.map(_.left.map(_.left.map(_.merge).merge).merge).merge),
      )((value: Value) =>
        value match {
          case sub1: Sub1 => Right(Left(Left(Left(Left(sub1)))))
          case sub2: Sub2 => Right(Left(Left(Left(Right(sub2)))))
          case sub3: Sub3 => Right(Left(Left(Right(sub3))))
          case sub4: Sub4 => Right(Left(Right(sub4)))
          case sub5: Sub5 => Right(Right(sub5))
          case _          => Left(s"Unexpected error type")
        },
      )

    def f6[
      AtomTypes,
      Sub1 <: Value: ClassTag,
      Sub2 <: Value: ClassTag,
      Sub3 <: Value: ClassTag,
      Sub4 <: Value: ClassTag,
      Sub5 <: Value: ClassTag,
      Sub6 <: Value: ClassTag,
    ](
      codec1: HttpCodec[AtomTypes, Sub1],
      codec2: HttpCodec[AtomTypes, Sub2],
      codec3: HttpCodec[AtomTypes, Sub3],
      codec4: HttpCodec[AtomTypes, Sub4],
      codec5: HttpCodec[AtomTypes, Sub5],
      codec6: HttpCodec[AtomTypes, Sub6],
    ): HttpCodec[AtomTypes, Value] =
      (codec1 | codec2 | codec3 | codec4 | codec5 | codec6).transformOrFail(either =>
        Right(either.left.map(_.left.map(_.left.map(_.left.map(_.merge).merge).merge).merge).merge),
      )((value: Value) =>
        value match {
          case sub1: Sub1 => Right(Left(Left(Left(Left(Left(sub1))))))
          case sub2: Sub2 => Right(Left(Left(Left(Left(Right(sub2))))))
          case sub3: Sub3 => Right(Left(Left(Left(Right(sub3)))))
          case sub4: Sub4 => Right(Left(Left(Right(sub4))))
          case sub5: Sub5 => Right(Left(Right(sub5)))
          case sub6: Sub6 => Right(Right(sub6))
          case _          => Left(s"Unexpected error type")
        },
      )

    def f7[
      AtomTypes,
      Sub1 <: Value: ClassTag,
      Sub2 <: Value: ClassTag,
      Sub3 <: Value: ClassTag,
      Sub4 <: Value: ClassTag,
      Sub5 <: Value: ClassTag,
      Sub6 <: Value: ClassTag,
      Sub7 <: Value: ClassTag,
    ](
      codec1: HttpCodec[AtomTypes, Sub1],
      codec2: HttpCodec[AtomTypes, Sub2],
      codec3: HttpCodec[AtomTypes, Sub3],
      codec4: HttpCodec[AtomTypes, Sub4],
      codec5: HttpCodec[AtomTypes, Sub5],
      codec6: HttpCodec[AtomTypes, Sub6],
      codec7: HttpCodec[AtomTypes, Sub7],
    ): HttpCodec[AtomTypes, Value] =
      (codec1 | codec2 | codec3 | codec4 | codec5 | codec6 | codec7).transformOrFail(either =>
        Right(either.left.map(_.left.map(_.left.map(_.left.map(_.left.map(_.merge).merge).merge).merge).merge).merge),
      )((value: Value) =>
        value match {
          case sub1: Sub1 => Right(Left(Left(Left(Left(Left(Left(sub1)))))))
          case sub2: Sub2 => Right(Left(Left(Left(Left(Left(Right(sub2)))))))
          case sub3: Sub3 => Right(Left(Left(Left(Left(Right(sub3))))))
          case sub4: Sub4 => Right(Left(Left(Left(Right(sub4)))))
          case sub5: Sub5 => Right(Left(Left(Right(sub5))))
          case sub6: Sub6 => Right(Left(Right(sub6)))
          case sub7: Sub7 => Right(Right(sub7))
          case _          => Left(s"Unexpected error type")
        },
      )

    def f8[
      AtomTypes,
      Sub1 <: Value: ClassTag,
      Sub2 <: Value: ClassTag,
      Sub3 <: Value: ClassTag,
      Sub4 <: Value: ClassTag,
      Sub5 <: Value: ClassTag,
      Sub6 <: Value: ClassTag,
      Sub7 <: Value: ClassTag,
      Sub8 <: Value: ClassTag,
    ](
      codec1: HttpCodec[AtomTypes, Sub1],
      codec2: HttpCodec[AtomTypes, Sub2],
      codec3: HttpCodec[AtomTypes, Sub3],
      codec4: HttpCodec[AtomTypes, Sub4],
      codec5: HttpCodec[AtomTypes, Sub5],
      codec6: HttpCodec[AtomTypes, Sub6],
      codec7: HttpCodec[AtomTypes, Sub7],
      codec8: HttpCodec[AtomTypes, Sub8],
    ): HttpCodec[AtomTypes, Value] =
      (codec1 | codec2 | codec3 | codec4 | codec5 | codec6 | codec7 | codec8).transformOrFail(either =>
        Right(
          either.left
            .map(_.left.map(_.left.map(_.left.map(_.left.map(_.left.map(_.merge).merge).merge).merge).merge).merge)
            .merge,
        ),
      )((value: Value) =>
        value match {
          case sub1: Sub1 => Right(Left(Left(Left(Left(Left(Left(Left(sub1))))))))
          case sub2: Sub2 => Right(Left(Left(Left(Left(Left(Left(Right(sub2))))))))
          case sub3: Sub3 => Right(Left(Left(Left(Left(Left(Right(sub3)))))))
          case sub4: Sub4 => Right(Left(Left(Left(Left(Right(sub4))))))
          case sub5: Sub5 => Right(Left(Left(Left(Right(sub5)))))
          case sub6: Sub6 => Right(Left(Left(Right(sub6))))
          case sub7: Sub7 => Right(Left(Right(sub7)))
          case sub8: Sub8 => Right(Right(sub8))
          case _          => Left(s"Unexpected error type")
        },
      )
  }

  private[http] sealed trait Atom[-AtomTypes, Value0] extends HttpCodec[AtomTypes, Value0] {
    def tag: AtomTag

    def index: Int

    def index(index: Int): Atom[AtomTypes, Value0]
  }

  private[http] final case class Status[A](codec: SimpleCodec[zio.http.Status, A], index: Int = 0)
      extends Atom[HttpCodecType.Status, A] {
    self =>
    def erase: Status[Any] = self.asInstanceOf[Status[Any]]

    def tag: AtomTag = AtomTag.Status

    def index(index: Int): Status[A] = copy(index = index)
  }
  private[http] final case class Path[A](pathCodec: PathCodec[A], index: Int = 0) extends Atom[HttpCodecType.Path, A] {
    self =>
    def erase: Path[Any] = self.asInstanceOf[Path[Any]]

    def tag: AtomTag = AtomTag.Path

    def index(index: Int): Path[A] = copy(index = index)
  }
  private[http] final case class Content[A](
    codec: HttpContentCodec[A],
    name: Option[String],
    index: Int = 0,
  ) extends Atom[HttpCodecType.Content, A] {
    self =>
    def tag: AtomTag = AtomTag.Content

    def index(index: Int): Content[A] = copy(index = index)
  }
  private[http] final case class ContentStream[A](
    codec: HttpContentCodec[A],
    name: Option[String],
    index: Int = 0,
  ) extends Atom[HttpCodecType.Content, ZStream[Any, Nothing, A]] {
    def tag: AtomTag = AtomTag.Content

    def index(index: Int): ContentStream[A] = copy(index = index)
  }
  private[http] final case class Query[A, Out](
    queryType: Query.QueryType[A],
    index: Int = 0,
  ) extends Atom[HttpCodecType.Query, Out] {
    self =>
    def erase: Query[Any, Any] = self.asInstanceOf[Query[Any, Any]]

    def tag: AtomTag = AtomTag.Query

    def index(index: Int): Query[A, Out] = copy(index = index)

    def isOptional: Boolean =
      queryType match {
        case QueryType.Primitive(_, BinaryCodecWithSchema(_, schema)) if schema.isInstanceOf[Schema.Optional[_]] =>
          true
        case QueryType.Record(recordSchema)                                                                      =>
          recordSchema match {
            case s if s.isInstanceOf[Schema.Optional[_]]                      => true
            case record: Schema.Record[_] if record.fields.forall(_.optional) => true
            case _                                                            => false
          }
        case _ => false
      }

    /**
     * Returns a new codec, where the value produced by this one is optional.
     */
    override def optional: HttpCodec[HttpCodecType.Query, Option[Out]] =
      queryType match {
        case QueryType.Primitive(name, codec) if codec.schema.isInstanceOf[Schema.Optional[_]] =>
          throw new IllegalArgumentException(
            s"Cannot make an optional query parameter optional. Name: $name schema: ${codec.schema}",
          )
        case QueryType.Primitive(name, codec)                                                  =>
          val optionalSchema = codec.schema.optional
          copy(queryType =
            QueryType.Primitive(name, BinaryCodecWithSchema(TextBinaryCodec.fromSchema(optionalSchema), optionalSchema)),
          )
        case QueryType.Record(recordSchema) if recordSchema.isInstanceOf[Schema.Optional[_]]   =>
          throw new IllegalArgumentException(s"Cannot make an optional query parameter optional")
        case QueryType.Record(recordSchema)                                                    =>
          val optionalSchema = recordSchema.optional
          copy(queryType = QueryType.Record(optionalSchema))
        case queryType @ QueryType.Collection(_, _, false)                                     =>
          copy(queryType = QueryType.Collection(queryType.colSchema, queryType.elements, optional = true))
        case queryType @ QueryType.Collection(_, _, true)                                      =>
          throw new IllegalArgumentException(s"Cannot make an optional query parameter optional: $queryType")

      }

  }

  private[http] object Query {
    sealed trait QueryType[A]
    object QueryType {
      case class Primitive[A](name: String, codec: BinaryCodecWithSchema[A]) extends QueryType[A]
      case class Collection[A](colSchema: Schema.Collection[_, _], elements: QueryType.Primitive[A], optional: Boolean)
          extends QueryType[A] {
        def toCollection(values: Chunk[Any]): A =
          colSchema match {
            case Schema.Sequence(_, fromChunk, _, _, _) =>
              fromChunk.asInstanceOf[Chunk[Any] => Any](values).asInstanceOf[A]
            case Schema.Set(_, _)                       =>
              values.toSet.asInstanceOf[A]
            case _                                      =>
              throw new IllegalArgumentException(
                s"Unsupported collection schema for query object field of type: $colSchema",
              )
          }
      }
      case class Record[A](recordSchema: Schema[A]) extends QueryType[A] {
        private var namesAndCodecs: Chunk[(Schema.Field[_, _], BinaryCodecWithSchema[Any])]       = _
        private[http] def fieldAndCodecs: Chunk[(Schema.Field[_, _], BinaryCodecWithSchema[Any])] =
          if (namesAndCodecs == null) {
            namesAndCodecs = recordSchema match {
              case record: Schema.Record[A]                =>
                record.fields.map { field =>
                  validateSchema(field.name, field.schema)
                  val codec = binaryCodecForField(field.annotations.foldLeft(field.schema)(_ annotate _))
                  (unlazy(field.asInstanceOf[Schema.Field[Any, Any]]), codec)
                }
              case s if s.isInstanceOf[Schema.Optional[_]] =>
                val record = s.asInstanceOf[Schema.Optional[A]].schema.asInstanceOf[Schema.Record[A]]
                record.fields.map { field =>
                  validateSchema(field.name, field.annotations.foldLeft(field.schema)(_ annotate _))
                  val codec = binaryCodecForField(field.schema)
                  (field, codec)
                }
              case s => throw new IllegalArgumentException(s"Unsupported schema for query object field of type: $s")
            }
            namesAndCodecs
          } else {
            namesAndCodecs
          }
      }

      private def unlazy(field: Schema.Field[Any, Any]): Schema.Field[Any, Any] = field.schema match {
        case Schema.Lazy(schema) =>
          Schema.Field(
            field.name,
            schema(),
            field.annotations,
            field.validation,
            field.get,
            field.set,
          )
        case _                   => field
      }

      private def binaryCodecForField[A](schema: Schema[A]): BinaryCodecWithSchema[Any] = (schema match {
        case schema @ Schema.Primitive(_, _)     => BinaryCodecWithSchema(TextBinaryCodec.fromSchema(schema), schema)
        case Schema.Transform(_, _, _, _, _)     => BinaryCodecWithSchema(TextBinaryCodec.fromSchema(schema), schema)
        case Schema.Optional(_, _)               => BinaryCodecWithSchema(TextBinaryCodec.fromSchema(schema), schema)
        case e: Schema.Enum[_] if isSimple(e)    => BinaryCodecWithSchema(TextBinaryCodec.fromSchema(schema), schema)
        case l @ Schema.Lazy(_)                  => binaryCodecForField(l.schema)
        case Schema.Set(schema, _)               => binaryCodecForField(schema)
        case Schema.Sequence(schema, _, _, _, _) => binaryCodecForField(schema)
        case schema => throw new IllegalArgumentException(s"Unsupported schema for query object field of type: $schema")
      }).asInstanceOf[BinaryCodecWithSchema[Any]]

      def isSimple(schema: Schema.Enum[_]): Boolean =
        schema.annotations.exists(_.isInstanceOf[simpleEnum])

      @tailrec
      private def validateSchema[A](name: String, schema: Schema[A]): Unit = schema match {
        case _: Schema.Primitive[A]               => ()
        case Schema.Transform(schema, _, _, _, _) => validateSchema(name, schema)
        case Schema.Optional(schema, _)           => validateSchema(name, schema)
        case Schema.Lazy(schema)                  => validateSchema(name, schema())
        case Schema.Set(schema, _)                => validateSchema(name, schema)
        case Schema.Sequence(schema, _, _, _, _)  => validateSchema(name, schema)
        case s => throw new IllegalArgumentException(s"Unsupported schema for query object field of type: $s")
      }

    }
  }

  private[http] final case class Method[A](codec: SimpleCodec[zio.http.Method, A], index: Int = 0)
      extends Atom[HttpCodecType.Method, A] {
    self =>
    def erase: Method[Any] = self.asInstanceOf[Method[Any]]
    def tag: AtomTag       = AtomTag.Method

    def index(index: Int): Method[A] = copy(index = index)
  }

  private[http] final case class Header[A](name: String, textCodec: TextCodec[A], index: Int = 0)
      extends Atom[HttpCodecType.Header, A] {
    self =>
    def erase: Header[Any] = self.asInstanceOf[Header[Any]]

    def tag: AtomTag = AtomTag.Header

    def index(index: Int): Header[A] = copy(index = index)
  }

  private[http] final case class Annotated[AtomTypes, Value](
    codec: HttpCodec[AtomTypes, Value],
    metadata: Metadata[Value],
  ) extends HttpCodec[AtomTypes, Value] {
    override def examples: Map[String, Value] =
      metadata match {
        case value: Metadata.Examples[Value] =>
          value.examples ++ codec.examples
        case _                               =>
          codec.examples
      }
  }

  sealed trait Metadata[Value] {
    def transform[Value2](f: Value => Value2): Metadata[Value2] =
      this match {
        case Metadata.Named(name)     => Metadata.Named(name)
        case Metadata.Optional()      => Metadata.Optional()
        case Metadata.Examples(ex)    => Metadata.Examples(ex.map { case (k, v) => k -> f(v) })
        case Metadata.Documented(doc) => Metadata.Documented(doc)
        case Metadata.Deprecated(doc) => Metadata.Deprecated(doc)
      }

    def transformOrFail[Value2](f: Value => Either[String, Value2]): Metadata[Value2] =
      this match {
        case Metadata.Named(name)     => Metadata.Named(name)
        case Metadata.Optional()      => Metadata.Optional()
        case Metadata.Examples(ex)    =>
          Metadata.Examples(ex.collect {
            case (k, v) if f(v).isRight =>
              k -> f(v).toOption.get
          })
        case Metadata.Documented(doc) => Metadata.Documented(doc)
        case Metadata.Deprecated(doc) => Metadata.Deprecated(doc)
      }
  }

  object Metadata {
    final case class Named[A](name: String) extends Metadata[A]

    final case class Optional[A]() extends Metadata[A]

    final case class Examples[A](examples: Map[String, A]) extends Metadata[A]

    final case class Documented[A](doc: Doc) extends Metadata[A]

    final case class Deprecated[A](doc: Doc) extends Metadata[A]
  }

  private[http] final case class TransformOrFail[AtomType, X, A](
    api: HttpCodec[AtomType, X],
    f: X => Either[String, A],
    g: A => Either[String, X],
  ) extends HttpCodec[AtomType, A] {
    type In  = X
    type Out = A
  }

  private[http] case object Empty extends HttpCodec[Any, Unit]

  private[http] case object Halt extends HttpCodec[Any, Nothing]

  private[http] final case class Combine[AtomType1, AtomType2, A1, A2, A](
    left: HttpCodec[AtomType1, A1],
    right: HttpCodec[AtomType2, A2],
    inputCombiner: Combiner.WithOut[A1, A2, A],
  ) extends HttpCodec[AtomType1 with AtomType2, A] {
    type Left  = A1
    type Right = A2
    type Out   = A
  }

  private[http] final case class Fallback[AtomType, A, B](
    left: HttpCodec[AtomType, A],
    right: HttpCodec[AtomType, B],
    alternator: Alternator[A, B],
    condition: Fallback.Condition,
  ) extends HttpCodec[AtomType, Either[A, B]] {
    type Left  = A
    type Right = B
    type Out   = Either[A, B]
  }

  private[http] object Fallback {

    /**
     * `Condition` describes the circumstances under which the `right` codec in
     * a `Fallback` is willing to attempt to recover from a failure of the
     * `left` codec. All implementations of `Fallback` other than `optional` are
     * willing to attempt to recover from any `HttpCodecError`. Implementations
     * of `Fallback` constructed from `optional` are only willing to attempt to
     * recover from `MissingHeader` or `MissingQueryParam` errors.
     */
    sealed trait Condition { self =>
      def apply(cause: Cause[Any]): Boolean   =
        self match {
          case Condition.IsHttpCodecError  => HttpCodecError.isHttpCodecError(cause)
          case Condition.isMissingDataOnly => HttpCodecError.isMissingDataOnly(cause)
        }
      def combine(that: Condition): Condition =
        (self, that) match {
          case (Condition.isMissingDataOnly, _) => Condition.isMissingDataOnly
          case (_, Condition.isMissingDataOnly) => Condition.isMissingDataOnly
          case _                                => Condition.IsHttpCodecError
        }
      def isHttpCodecError: Boolean           = self match {
        case Condition.IsHttpCodecError => true
        case _                          => false
      }
      def isMissingDataOnly: Boolean          = self match {
        case Condition.isMissingDataOnly => true
        case _                           => false
      }
    }
    object Condition       {
      case object IsHttpCodecError  extends Condition
      case object isMissingDataOnly extends Condition
    }
  }

  private[http] def flattenFallbacks[AtomTypes, A](
    api: HttpCodec[AtomTypes, A],
  ): Chunk[(HttpCodec[AtomTypes, A], Fallback.Condition)] = {

    def rewrite[T, B](
      api: HttpCodec[T, B],
      annotations: Chunk[HttpCodec.Metadata[B]],
    ): Chunk[(HttpCodec[T, B], Fallback.Condition)] =
      api match {
        case fallback @ HttpCodec.Fallback(left, right, alternator, condition) =>
          rewrite[T, fallback.Left](left, reduceExamplesLeft(annotations, alternator)).map { case (codec, condition) =>
            codec.toLeft[fallback.Right] -> condition
          } ++
            rewrite[T, fallback.Right](right, reduceExamplesRight(annotations, alternator)).map { case (codec, _) =>
              codec.toRight[fallback.Left] -> condition
            }

        case transform @ HttpCodec.TransformOrFail(codec, f, g) =>
          rewrite[T, transform.In](
            codec,
            annotations.map(_.transform { v =>
              g(v) match {
                case Left(error)  => throw new Exception(error)
                case Right(value) => value
              }
            }),
          ).map { case (codec, condition) =>
            HttpCodec.TransformOrFail(codec, f, g) -> condition
          }

        case combine @ HttpCodec.Combine(left, right, combiner) =>
          for {
            (l, lCondition) <- rewrite[T, combine.Left](left, reduceExamplesLeft(annotations, combiner))
            (r, rCondition) <- rewrite[T, combine.Right](right, reduceExamplesRight(annotations, combiner))
          } yield HttpCodec.Combine(l, r, combiner) -> lCondition.combine(rCondition)

        case HttpCodec.Annotated(in, metadata) =>
          rewrite[T, B](in, metadata +: annotations)

        case HttpCodec.Empty => Chunk.single(HttpCodec.Empty -> Fallback.Condition.IsHttpCodecError)

        case HttpCodec.Halt => Chunk.empty

        case atom: Atom[_, _] =>
          Chunk.single(annotations.foldLeft[HttpCodec[T, B]](atom)(_ annotate _) -> Fallback.Condition.IsHttpCodecError)
      }

    rewrite(api, Chunk.empty)
  }

  private[http] def reduceExamplesLeft[T, L, R](
    annotations: Chunk[HttpCodec.Metadata[T]],
    combiner: Combiner[L, R],
  ): Chunk[HttpCodec.Metadata[L]] =
    annotations.map {
      case HttpCodec.Metadata.Examples(examples) =>
        HttpCodec.Metadata.Examples(examples.map { case (name, value) =>
          name -> combiner.separate(value.asInstanceOf[combiner.Out])._1
        })
      case other                                 =>
        other.asInstanceOf[HttpCodec.Metadata[L]]
    }

  private[http] def reduceExamplesLeft[T, L, R](
    annotations: Chunk[HttpCodec.Metadata[T]],
    alternator: Alternator[L, R],
  ): Chunk[HttpCodec.Metadata[L]] =
    annotations.map {
      case HttpCodec.Metadata.Examples(examples) =>
        HttpCodec.Metadata.Examples(examples.flatMap { case (name, value) =>
          alternator.unleft(value.asInstanceOf[alternator.Out]).map(name -> _)
        })
      case other                                 =>
        other.asInstanceOf[HttpCodec.Metadata[L]]
    }

  private[http] def reduceExamplesRight[T, L, R](
    annotations: Chunk[HttpCodec.Metadata[T]],
    combiner: Combiner[L, R],
  ): Chunk[HttpCodec.Metadata[R]] =
    annotations.map {
      case HttpCodec.Metadata.Examples(examples) =>
        HttpCodec.Metadata.Examples(examples.map { case (name, value) =>
          name -> combiner.separate(value.asInstanceOf[combiner.Out])._2
        })
      case other                                 =>
        other.asInstanceOf[HttpCodec.Metadata[R]]
    }

  private[http] def reduceExamplesRight[T, L, R](
    annotations: Chunk[HttpCodec.Metadata[T]],
    alternator: Alternator[L, R],
  ): Chunk[HttpCodec.Metadata[R]] =
    annotations.map {
      case HttpCodec.Metadata.Examples(examples) =>
        HttpCodec.Metadata.Examples(examples.flatMap { case (name, value) =>
          alternator.unright(value.asInstanceOf[alternator.Out]).map(name -> _)
        })
      case other                                 =>
        other.asInstanceOf[HttpCodec.Metadata[R]]
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy