Skip to content

Commit

Permalink
Configure openapi3.0 schema encoder
Browse files Browse the repository at this point in the history
  • Loading branch information
hamnis committed Oct 4, 2022
1 parent 51a80ff commit a94de31
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,16 @@ package sttp.apispec.openapi
import sttp.apispec.AnySchema

package object circe extends SttpOpenAPICirceEncoders with SttpOpenAPICirceDecoders {
val anyObjectEncoding: AnySchema.Encoding = AnySchema.Encoding.Boolean
override val anyObjectEncoding: AnySchema.Encoding = AnySchema.Encoding.Boolean
}

package circe {
trait SttpOpenAPICirceEncoders extends internal.InternalSttpOpenAPICirceEncoders

trait SttpOpenAPI3_0CirceEncoders extends internal.InternalSttpOpenAPICirceEncoders {
override val openApi30: Boolean = true
override val anyObjectEncoding: AnySchema.Encoding = AnySchema.Encoding.Boolean
}

trait SttpOpenAPICirceDecoders extends internal.InternalSttpOpenAPICirceDecoders
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import io.circe.syntax._
import io.circe.generic.semiauto.deriveDecoder
import sttp.apispec.{Reference, ReferenceOr, Schema, SchemaType}

import scala.annotation.nowarn
import scala.collection.immutable.ListMap

trait InternalSttpOpenAPICirceDecoders {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,92 @@ import scala.collection.immutable.ListMap

trait InternalSttpOpenAPICirceEncoders {
def anyObjectEncoding: AnySchema.Encoding

def openApi30: Boolean = false

val encoderSchema31: Encoder[Schema] = Encoder.AsObject
.instance { (s: Schema) =>
val minKey = if (s.exclusiveMinimum.getOrElse(false)) "exclusiveMinimum" else "minimum"
val maxKey = if (s.exclusiveMaximum.getOrElse(false)) "exclusiveMaximum" else "maximum"
JsonObject(
s"$$schema" := s.$schema,
"allOf" := s.allOf,
"title" := s.title,
"required" := s.required,
"type" := (if (s.nullable.getOrElse(false))
s.`type`.map(s => Json.arr(s.asJson, Json.fromString("null"))).asJson
else s.`type`.asJson),
"prefixItems" := s.prefixItems,
"items" := s.items,
"contains" := s.contains,
"properties" := s.properties,
"patternProperties" := s.patternProperties,
"description" := s.description,
"format" := s.format,
"default" := s.default,
"readOnly" := s.readOnly,
"writeOnly" := s.writeOnly,
"examples" := s.example,
"deprecated" := s.deprecated,
"oneOf" := s.oneOf,
"discriminator" := s.discriminator,
"additionalProperties" := s.additionalProperties,
"pattern" := s.pattern,
"minLength" := s.minLength,
"maxLength" := s.maxLength,
minKey := s.minimum,
maxKey := s.maximum,
"minItems" := s.minItems,
"maxItems" := s.maxItems,
"enum" := s.`enum`,
"not" := s.not,
"if" := s.`if`,
"then" := s.`then`,
"else" := s.`else`,
"$defs" := s.$defs,
"extensions" := s.extensions
)
}
.mapJsonObject(expandExtensions)

val encoderSchema30: Encoder[Schema] = Encoder.AsObject
.instance { (s: Schema) =>
JsonObject(
"allOf" := s.allOf,
"title" := s.title,
"required" := s.required,
"nullable" := s.nullable,
"type" := s.`type`,
"prefixItems" := s.prefixItems,
"items" := s.items,
"contains" := s.contains,
"properties" := s.properties,
"patternProperties" := s.patternProperties,
"description" := s.description,
"format" := s.format,
"default" := s.default,
"readOnly" := s.readOnly,
"writeOnly" := s.writeOnly,
"examples" := s.example,
"deprecated" := s.deprecated,
"oneOf" := s.oneOf,
"discriminator" := s.discriminator,
"additionalProperties" := s.additionalProperties,
"pattern" := s.pattern,
"minLength" := s.minLength,
"maxLength" := s.maxLength,
"minimum" := s.minimum,
"maximum" := s.maximum,
"exclusiveMinimum" := s.exclusiveMinimum,
"exclusiveMaximum" := s.exclusiveMaximum,
"minItems" := s.minItems,
"maxItems" := s.maxItems,
"enum" := s.`enum`,
"not" := s.not,
"extensions" := s.extensions
)
}

// note: these are strict val-s, order matters!
implicit def encoderReferenceOr[T: Encoder]: Encoder[ReferenceOr[T]] = {
case Left(Reference(ref, summary, description)) =>
Expand Down Expand Up @@ -45,7 +131,7 @@ trait InternalSttpOpenAPICirceEncoders {
}
implicit val encoderExampleValue: Encoder[ExampleValue] = {
case e: ExampleSingleValue =>
Json.arr(encoderExampleSingleValue(e))
encoderExampleSingleValue(e)
case ExampleMultipleValue(values) =>
Json.arr(values.map(v => encoderExampleSingleValue(ExampleSingleValue(v))): _*)
}
Expand All @@ -60,50 +146,7 @@ trait InternalSttpOpenAPICirceEncoders {

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

implicit val encoderSchema: Encoder[Schema] = Encoder.AsObject
.instance { (s: Schema) =>
val minKey = if (s.exclusiveMinimum.getOrElse(false)) "exclusiveMinimum" else "minimum"
val maxKey = if (s.exclusiveMaximum.getOrElse(false)) "exclusiveMaximum" else "maximum"
JsonObject(
s"$$schema" := s.$schema,
"allOf" := s.allOf,
"title" := s.title,
"required" := s.required,
"type" := (if (s.nullable.getOrElse(false))
s.`type`.map(s => Json.arr(s.asJson, Json.fromString("null"))).asJson
else s.`type`.asJson),
"prefixItems" := s.prefixItems,
"items" := s.items,
"contains" := s.contains,
"properties" := s.properties,
"patternProperties" := s.patternProperties,
"description" := s.description,
"format" := s.format,
"default" := s.default,
"readOnly" := s.readOnly,
"writeOnly" := s.writeOnly,
"examples" := s.example,
"deprecated" := s.deprecated,
"oneOf" := s.oneOf,
"discriminator" := s.discriminator,
"additionalProperties" := s.additionalProperties,
"pattern" := s.pattern,
"minLength" := s.minLength,
"maxLength" := s.maxLength,
minKey := s.minimum,
maxKey := s.maximum,
"minItems" := s.minItems,
"maxItems" := s.maxItems,
"enum" := s.`enum`,
"not" := s.not,
"if" := s.`if`,
"then" := s.`then`,
"else" := s.`else`,
"$defs" := s.$defs,
"extensions" := s.extensions
)
}
.mapJsonObject(expandExtensions)
implicit val encoderSchema: Encoder[Schema] = if (openApi30) encoderSchema30 else encoderSchema31

implicit val encoderAnySchema: Encoder[AnySchema] = Encoder.instance {
case AnySchema.Anything =>
Expand Down Expand Up @@ -159,10 +202,10 @@ trait InternalSttpOpenAPICirceEncoders {
implicit val encoderOperation: Encoder[Operation] = {
// this is needed to override the encoding of `security: List[SecurityRequirement]`. An empty security requirement
// should be represented as an empty object (`{}`), not `null`, which is the default encoding of `ListMap`s.
implicit def encodeListMap[V: Encoder]: Encoder[ListMap[String, V]] = doEncodeListMap(nullWhenEmpty = false) : @nowarn
implicit def encodeListMap[V: Encoder]: Encoder[ListMap[String, V]] = doEncodeListMap(nullWhenEmpty = false)

implicit def encodeListMapForCallbacks: Encoder[ListMap[String, ReferenceOr[Callback]]] =
doEncodeListMap(nullWhenEmpty = true) : @nowarn
doEncodeListMap(nullWhenEmpty = true)

deriveEncoder[Operation].mapJsonObject(expandExtensions)
}
Expand Down Expand Up @@ -195,7 +238,7 @@ trait InternalSttpOpenAPICirceEncoders {
private 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
val properties = m.map { case (k, v) => KeyEncoder[K].apply(k) -> Encoder[V].apply(v) }.toList
Json.obj(properties: _*)
}

Expand Down Expand Up @@ -229,7 +272,7 @@ trait InternalSttpOpenAPICirceEncoders {
x-foo: 42
```
*/
private def expandExtensions(jsonObject: JsonObject): JsonObject = {
private[internal] def expandExtensions(jsonObject: JsonObject): JsonObject = {
val extensions = jsonObject("extensions")
val jsonWithoutExt = jsonObject.filterKeys(_ != "extensions")
extensions.flatMap(_.asObject).map(extObject => extObject.deepMerge(jsonWithoutExt)).getOrElse(jsonWithoutExt)
Expand Down

0 comments on commit a94de31

Please sign in to comment.