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

sttp.apispec.internal.JsonSchemaCirceEncoders.scala Maven / Gradle / Ivy

The newest version!
package sttp.apispec
package internal

import io.circe._
import io.circe.generic.semiauto.deriveEncoder
import io.circe.parser.parse
import io.circe.syntax._

import scala.collection.immutable.ListMap

trait JsonSchemaCirceEncoders {
  def anyObjectEncoding: AnySchema.Encoding

  def openApi30: Boolean = false

  implicit lazy val encoderSchema: Encoder[Schema] = Encoder.AsObject
    .instance { (s: Schema) =>
      val nullSchema = Schema(`type` = Some(List(SchemaType.Null)))

      // Nullable $ref Schema is represented as {"anyOf": [{"$ref": "some-ref"}, {"type": "null"}]}
      // In OpenAPI 3.0, we need to translate it to {"allOf": [{"$ref": "some-ref"}], "nullable": true}
      val wrappedNullableRef30 = s.anyOf match {
        case List(refSchema: Schema, `nullSchema`) if refSchema.$ref.isDefined && openApi30 =>
          Some(refSchema)
        case _ => None
      }

      val typeAndNullable = s.`type` match {
        case Some(List(tpe)) =>
          List("type" := tpe)
        case Some(List(tpe, SchemaType.Null)) if openApi30 =>
          List("type" := tpe, "nullable" := true)
        case None if wrappedNullableRef30.isDefined =>
          List("nullable" := true)
        case t =>
          List("type" := t)
      }

      val minFields = (s.minimum, s.exclusiveMinimum) match {
        case (None, Some(min)) if openApi30 =>
          Vector("minimum" := min, "exclusiveMinimum" := true)
        case _ =>
          Vector("minimum" := s.minimum, "exclusiveMinimum" := s.exclusiveMinimum)
      }

      val maxFields = (s.maximum, s.exclusiveMaximum) match {
        case (None, Some(max)) if openApi30 =>
          Vector("maximum" := max, "exclusiveMaximum" := true)
        case _ =>
          Vector("maximum" := s.maximum, "exclusiveMaximum" := s.exclusiveMaximum)
      }

      val exampleFields = s.examples match {
        case Some(List(example)) if openApi30 =>
          Vector("example" := example)
        case _ =>
          Vector("examples" := s.examples)
      }

      JsonObject.fromIterable(
        Vector(
          "$schema" := s.$schema,
          "$vocabulary" := s.$vocabulary,
          "$id" := s.$id,
          "$anchor" := s.$anchor,
          "$dynamicAnchor" := s.$dynamicAnchor,
          "$ref" := s.$ref,
          "$dynamicRef" := s.$dynamicRef,
          "$comment" := s.$comment,
          "$defs" := s.$defs,
          "title" := s.title,
          "description" := s.description,
          "default" := s.default,
          "deprecated" := s.deprecated,
          "readOnly" := s.readOnly,
          "writeOnly" := s.writeOnly
        ) ++ exampleFields ++ typeAndNullable ++ Vector(
          "enum" := s.`enum`,
          "const" := s.const,
          "format" := s.format,
          "allOf" := wrappedNullableRef30.map(List(_)).getOrElse(s.allOf),
          "anyOf" := (if (wrappedNullableRef30.isDefined) Nil else s.anyOf),
          "oneOf" := s.oneOf,
          "not" := s.not,
          "if" := s.`if`,
          "then" := s.`then`,
          "else" := s.`else`,
          "dependentSchemas" := s.dependentSchemas,
          "multipleOf" := s.multipleOf
        ) ++ minFields ++ maxFields ++ Vector(
          "maxLength" := s.maxLength,
          "minLength" := s.minLength,
          "pattern" := s.pattern,
          "maxItems" := s.maxItems,
          "minItems" := s.minItems,
          "uniqueItems" := s.uniqueItems,
          "maxContains" := s.maxContains,
          "minContains" := s.minContains,
          "prefixItems" := s.prefixItems,
          "items" := s.items,
          "contains" := s.contains,
          "unevaluatedItems" := s.unevaluatedItems,
          "maxProperties" := s.maxProperties,
          "minProperties" := s.minProperties,
          "required" := s.required,
          "dependentRequired" := s.dependentRequired,
          "discriminator" := s.discriminator,
          "properties" := s.properties,
          "patternProperties" := s.patternProperties,
          "additionalProperties" := s.additionalProperties,
          "propertyNames" := s.propertyNames,
          "unevaluatedProperties" := s.unevaluatedProperties,
          "externalDocs" := s.externalDocs,
          "extensions" := s.extensions
        )
      )
    }
    .mapJsonObject(expandExtensions)

  // note: these are strict val-s, order matters!
  implicit val extensionValue: Encoder[ExtensionValue] =
    Encoder.instance(e => parse(e.value).getOrElse(Json.fromString(e.value)))

  implicit val encoderExampleSingleValue: Encoder[ExampleSingleValue] = {
    case ExampleSingleValue(value: String)     => parse(value).getOrElse(Json.fromString(value))
    case ExampleSingleValue(value: Int)        => Json.fromInt(value)
    case ExampleSingleValue(value: Long)       => Json.fromLong(value)
    case ExampleSingleValue(value: Float)      => Json.fromFloatOrString(value)
    case ExampleSingleValue(value: Double)     => Json.fromDoubleOrString(value)
    case ExampleSingleValue(value: Boolean)    => Json.fromBoolean(value)
    case ExampleSingleValue(value: BigDecimal) => Json.fromBigDecimal(value)
    case ExampleSingleValue(value: BigInt)     => Json.fromBigInt(value)
    case ExampleSingleValue(null)              => Json.Null
    case ExampleSingleValue(value)             => Json.fromString(value.toString)
  }

  implicit val encoderMultipleExampleValue: Encoder[ExampleMultipleValue] = { e =>
    Json.arr(e.values.map(v => encoderExampleSingleValue(ExampleSingleValue(v))): _*)
  }

  implicit val encoderExampleValue: Encoder[ExampleValue] = {
    case e: ExampleMultipleValue => encoderMultipleExampleValue.apply(e)
    case e: ExampleSingleValue   => encoderExampleSingleValue.apply(e)
  }

  implicit val encoderSchemaType: Encoder[SchemaType] =
    _.value.asJson

  implicit val encoderKeyPattern: KeyEncoder[Pattern] =
    KeyEncoder.encodeKeyString.contramap(_.value)

  implicit val encoderPattern: Encoder[Pattern] =
    Encoder.encodeString.contramap(_.value)

  implicit val encoderDiscriminator: Encoder[Discriminator] =
    deriveEncoder[Discriminator]

  implicit val encoderExternalDocumentation: Encoder[ExternalDocumentation] =
    deriveEncoder[ExternalDocumentation].mapJsonObject(expandExtensions)

  implicit val encoderAnySchema: Encoder[AnySchema] = Encoder.instance {
    case AnySchema.Anything =>
      anyObjectEncoding match {
        case AnySchema.Encoding.Object  => Json.obj()
        case AnySchema.Encoding.Boolean => Json.True
      }
    case AnySchema.Nothing =>
      anyObjectEncoding match {
        case AnySchema.Encoding.Object =>
          Json.obj("not" := Json.obj())
        case AnySchema.Encoding.Boolean => Json.False
      }
  }

  implicit val encoderSchemaLike: Encoder[SchemaLike] = Encoder.instance {
    case s: AnySchema => encoderAnySchema(s)
    case s: Schema    => encoderSchema(s)
  }

  implicit def encodeList[T: Encoder]: Encoder[List[T]] = {
    case Nil        => Json.Null
    case l: List[T] => Json.arr(l.map(i => implicitly[Encoder[T]].apply(i)): _*)
  }

  implicit def encodeListMap[K: KeyEncoder, V: Encoder]: Encoder[ListMap[K, V]] = doEncodeListMap(nullWhenEmpty = true)

  private[apispec] def doEncodeListMap[K: KeyEncoder, V: Encoder](nullWhenEmpty: Boolean): Encoder[ListMap[K, V]] = {
    case m: ListMap[K, V] if m.isEmpty && nullWhenEmpty => Json.Null
    case m: ListMap[K, V] =>
      val properties = m.map { case (k, v) => KeyEncoder[K].apply(k) -> Encoder[V].apply(v) }.toList
      Json.obj(properties: _*)
  }

  /*
      Openapi extensions are arbitrary key-value data that could be added to some of models in specifications, such
      as `OpenAPI` itself, `License`, `Parameter`, etc.

      The key could be any string (that starts with 'x-' by convention) and value is arbitrary Json (string, object,
      array, etc.)

      To be able to encode such arbitrary data and apply it to the final Json it passed through the `extensions` field
      in models and moved (or expanded) to the object level while encoding

      Example:

      ```
      case class License(
         name: String,
         url: Option[String],
         extensions: ListMap[String, ExtensionValue] = ListMap.empty
      )

      val licenseWithExtension = License("hello", None, ListMap("x-foo", ExtensionValue("42"))
      ```

      Applying the transformation below we end up with the following schema in the specification:

      ```
      license:
        name: hello
        x-foo: 42
      ```
   */
  private[apispec] def expandExtensions(jsonObject: JsonObject): JsonObject = {
    val jsonWithoutExt = jsonObject.filterKeys(_ != "extensions")
    jsonObject("extensions")
      .flatMap(_.asObject)
      .map { extObject =>
        val allKeys = (jsonWithoutExt.keys ++ extObject.keys).toSeq.distinct
        allKeys.foldLeft(JsonObject.empty) { case (acc, key) =>
          extObject(key).orElse(jsonWithoutExt(key)) match {
            case Some(value) => acc.add(key, value)
            case None        => acc
          }
        }
      }
      .getOrElse(jsonWithoutExt)
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy