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

zio.http.endpoint.http.HttpGen.scala Maven / Gradle / Ivy

package zio.http.endpoint.http

import zio.Unsafe

import zio.schema.Schema
import zio.schema.codec.BinaryCodec

import zio.http.MediaType
import zio.http.codec._
import zio.http.endpoint.Endpoint
import zio.http.endpoint.openapi.OpenAPIGen.{AtomizedMetaCodecs, MetaCodec}
import zio.http.endpoint.openapi.{JsonSchema, OpenAPIGen}

object HttpGen {

  private val PathWildcard = "pathWildcard"

  def fromEndpoints(
    config: CodecConfig,
    endpoint1: Endpoint[_, _, _, _, _],
    endpoints: Endpoint[_, _, _, _, _]*,
  ): HttpFile = {
    HttpFile((endpoint1 +: endpoints).map(fromEndpoint(config, _)).toList)
  }

  def fromEndpoints(
    endpoint1: Endpoint[_, _, _, _, _],
    endpoints: Endpoint[_, _, _, _, _]*,
  ): HttpFile = {
    HttpFile((endpoint1 +: endpoints).map(fromEndpoint(CodecConfig.defaultConfig, _)).toList)
  }

  def fromEndpoint(endpoint: Endpoint[_, _, _, _, _]): HttpEndpoint =
    fromEndpoint(CodecConfig.defaultConfig, endpoint)

  def fromEndpoint(config: CodecConfig, endpoint: Endpoint[_, _, _, _, _]): HttpEndpoint = {
    val atomizedInput = AtomizedMetaCodecs.flatten(endpoint.input)
    HttpEndpoint(
      OpenAPIGen.method(atomizedInput.method),
      buildPath(config, endpoint.input),
      headersVariables(atomizedInput).map(_.name),
      bodySchema(atomizedInput),
      variables(config, atomizedInput),
      doc(endpoint),
    )
  }

  private def bodySchema(inAtoms: AtomizedMetaCodecs) = {
    // currently only json support. No multipart/form-data or x-www-form-urlencoded
    if (inAtoms.content.size != 1) None
    else
      inAtoms.content.collect {
        case MetaCodec(HttpCodec.Content(codec, _, _), _) if codec.choices.contains(MediaType.application.json) =>
          val schema     = codec.choices(MediaType.application.json).schema
          val jsonSchema = JsonSchema.fromZSchema(schema)
          jsonSchema
      }.headOption
  }

  private def doc(endpoint: Endpoint[_, _, _, _, _]) =
    if (endpoint.documentation == Doc.empty) None else Some(endpoint.documentation.toPlaintext(color = false))

  def variables(config: CodecConfig, inAtoms: AtomizedMetaCodecs): Seq[HttpVariable] =
    pathVariables(inAtoms) ++ queryVariables(config, inAtoms) ++ headersVariables(inAtoms) ++ bodyVariables(inAtoms)

  def bodyVariables(inAtoms: AtomizedMetaCodecs): Seq[HttpVariable] = {
    val bodySchema0 = bodySchema(inAtoms)

    def loop(schema: JsonSchema, name: Option[String]): Seq[HttpVariable] = schema match {
      case JsonSchema.AnnotatedSchema(schema, _)     => loop(schema, name)
      case JsonSchema.RefSchema(_)                   => throw new Exception("RefSchema not supported")
      case JsonSchema.OneOfSchema(_)                 => throw new Exception("OneOfSchema not supported")
      case JsonSchema.AllOfSchema(_)                 => throw new Exception("AllOfSchema not supported")
      case JsonSchema.AnyOfSchema(_)                 => throw new Exception("AnyOfSchema not supported")
      // TODO: add comments for validation restrictions
      case JsonSchema.Number(format, _, _, _, _, _)  =>
        val typeHint = format match {
          case JsonSchema.NumberFormat.Float  => "type: Float"
          case JsonSchema.NumberFormat.Double => "type: Double"
        }
        Seq(HttpVariable(getName(name), None, Some(typeHint)))
      // TODO: add comments for validation restrictions
      case JsonSchema.Integer(format, _, _, _, _, _) =>
        val typeHint = format match {
          case JsonSchema.IntegerFormat.Int32     => "type: Int"
          case JsonSchema.IntegerFormat.Int64     => "type: Long"
          case JsonSchema.IntegerFormat.Timestamp => "type: Timestamp in milliseconds"
        }
        Seq(HttpVariable(getName(name), None, Some(typeHint)))
      // TODO: add comments for validation restrictions
      case JsonSchema.String(format, pattern, _, _)  =>
        val formatHint: String  = format match {
          case Some(value) => s" format: ${value.value}"
          case None        => ""
        }
        val patternHint: String = pattern match {
          case Some(value) => s" pattern: ${value.value}"
          case None        => ""
        }
        Seq(HttpVariable(getName(name), None, Some(s"type: String$formatHint$patternHint")))
      case JsonSchema.Boolean                        => Seq(HttpVariable(getName(name), None, Some("type: Boolean")))
      case JsonSchema.ArrayType(items, _, _)         =>
        val typeHint =
          items match {
            case Some(schema) =>
              loop(schema, Some("notUsed")).map(_.render).mkString(";")
            case None         =>
              ""
          }

        Seq(HttpVariable(getName(name), None, Some(s"type: array of $typeHint")))
      case JsonSchema.Object(properties, _, _)       =>
        properties.flatMap { case (key, value) => loop(value, Some(key)) }.toSeq
      case JsonSchema.Enum(values) => Seq(HttpVariable(getName(name), None, Some(s"enum: ${values.mkString(",")}")))
      case JsonSchema.Null         => Seq.empty
      case JsonSchema.AnyJson      => Seq.empty
    }

    bodySchema0 match {
      case Some(schema) => loop(schema, None)
      case None         => Seq.empty
    }
  }

  private def getName(name: Option[String]) = { name.getOrElse(throw new IllegalArgumentException("name is required")) }

  def headersVariables(inAtoms: AtomizedMetaCodecs): Seq[HttpVariable] =
    inAtoms.header.collect { case mc @ MetaCodec(HttpCodec.Header(name, codec, _), _) =>
      HttpVariable(
        name.capitalize,
        mc.examples.values.headOption.map(e => codec.asInstanceOf[TextCodec[Any]].encode(e)),
      )
    }

  def queryVariables(config: CodecConfig, inAtoms: AtomizedMetaCodecs): Seq[HttpVariable] = {
    inAtoms.query.collect {
      case mc @ MetaCodec(HttpCodec.Query(HttpCodec.Query.QueryType.Primitive(name, codec), _), _)  =>
        HttpVariable(
          name,
          mc.examples.values.headOption.map((e: Any) =>
            codec.codec(config).asInstanceOf[BinaryCodec[Any]].encode(e).asString,
          ),
        ) :: Nil
      case mc @ MetaCodec(HttpCodec.Query(record @ HttpCodec.Query.QueryType.Record(schema), _), _) =>
        val recordSchema = (schema match {
          case value if value.isInstanceOf[Schema.Optional[_]] => value.asInstanceOf[Schema.Optional[Any]].schema
          case _                                               => schema
        }).asInstanceOf[Schema.Record[Any]]
        val examples     = mc.examples.values.headOption.map { ex =>
          recordSchema.deconstruct(ex)(Unsafe.unsafe)
        }
        record.fieldAndCodecs.zipWithIndex.map { case ((field, codec), index) =>
          HttpVariable(
            field.name,
            examples.map(values => {
              val fieldValue = values(index)
                .orElse(field.defaultValue)
                .getOrElse(throw new Exception(s"No value or default value for field ${field.name}"))
              codec.codec(config).encode(fieldValue).asString
            }),
          )
        }
    }.flatten
  }

  private def pathVariables(inAtoms: AtomizedMetaCodecs) = {
    inAtoms.path.collect {
      case mc @ MetaCodec(codec, _) if codec != SegmentCodec.Empty && !codec.isInstanceOf[SegmentCodec.Literal] =>
        HttpVariable(
          mc.name.getOrElse(throw new Exception("Path parameter must have a name")),
          mc.examples.values.headOption.map(_.toString),
        )
    }
  }

  def buildPath(config: CodecConfig, in: HttpCodec[_, _]): String = {

    def pathCodec(in1: HttpCodec[_, _]): Option[HttpCodec.Path[_]] = in1 match {
      case atom: HttpCodec.Atom[_, _]            =>
        atom match {
          case codec @ HttpCodec.Path(_, _) => Some(codec)
          case _                            => None
        }
      case HttpCodec.Annotated(in, _)            => pathCodec(in)
      case HttpCodec.TransformOrFail(api, _, _)  => pathCodec(api)
      case HttpCodec.Empty                       => None
      case HttpCodec.Halt                        => None
      case HttpCodec.Combine(left, right, _)     => pathCodec(left).orElse(pathCodec(right))
      case HttpCodec.Fallback(left, right, _, _) => pathCodec(left).orElse(pathCodec(right))
    }

    val atomizedInput = AtomizedMetaCodecs.flatten(in)
    val queryNames    = queryVariables(config, atomizedInput).map(_.name)

    val pathString = {
      val codec = pathCodec(in).getOrElse(throw new Exception("No path found.")).pathCodec
      if (codec.render("{{", "}}").endsWith(SegmentCodec.Trailing.render))
        codec.renderIgnoreTrailing("{{", "}}") + s"{{$PathWildcard}}"
      else codec.render("{{", "}}")
    }

    if (queryNames.nonEmpty) pathString + "?" + queryNames.map(name => s"$name={{$name}}").mkString("&")
    else pathString
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy