Skip to content

Commit

Permalink
Somewhat better encoding of async api
Browse files Browse the repository at this point in the history
  • Loading branch information
hamnis committed Oct 4, 2022
1 parent 967aac3 commit 51a80ff
Show file tree
Hide file tree
Showing 8 changed files with 106 additions and 71 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ package circe {
case Left(Reference(ref, summary, description)) =>
Json
.obj(
"$ref" := ref,
s"$$ref" := ref,
"summary" := summary,
"description" := description
)
Expand All @@ -37,16 +37,16 @@ package circe {
implicit val encoderSecurityScheme: Encoder[SecurityScheme] =
deriveEncoder[SecurityScheme].mapJsonObject(expandExtensions)
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: 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)
case ExampleSingleValue(value: BigInt) => Json.fromBigInt(value)
case ExampleSingleValue(null) => Json.Null
case ExampleSingleValue(value) => Json.fromString(value.toString)
}
implicit val encoderExampleValue: Encoder[ExampleValue] = {
case e: ExampleSingleValue => encoderExampleSingleValue(e)
Expand All @@ -65,7 +65,7 @@ package circe {
implicit val encoderAnySchema: Encoder[AnySchema] = Encoder.instance {
case AnySchema.Anything =>
anyObjectEncoding match {
case AnySchema.Encoding.Object => Json.obj()
case AnySchema.Encoding.Object => Json.obj()
case AnySchema.Encoding.Boolean => Json.True
}
case AnySchema.Nothing =>
Expand Down Expand Up @@ -105,9 +105,9 @@ package circe {
nullIfEmpty(a)(
Json.obj(
a.map {
case v: HttpMessageBinding => "http" -> v.asJson
case v: HttpMessageBinding => "http" -> v.asJson
case v: WebSocketMessageBinding => "ws" -> v.asJson
case v: KafkaMessageBinding => "kafka" -> v.asJson
case v: KafkaMessageBinding => "kafka" -> v.asJson
}: _*
)
)
Expand All @@ -122,9 +122,9 @@ package circe {
nullIfEmpty(a)(
Json.obj(
a.map {
case v: HttpOperationBinding => "http" -> v.asJson
case v: HttpOperationBinding => "http" -> v.asJson
case v: WebSocketOperationBinding => "ws" -> v.asJson
case v: KafkaOperationBinding => "kafka" -> v.asJson
case v: KafkaOperationBinding => "kafka" -> v.asJson
}: _*
)
)
Expand All @@ -139,9 +139,9 @@ package circe {
nullIfEmpty(a)(
Json.obj(
a.map {
case v: HttpChannelBinding => "http" -> v.asJson
case v: HttpChannelBinding => "http" -> v.asJson
case v: WebSocketChannelBinding => "ws" -> v.asJson
case v: KafkaChannelBinding => "kafka" -> v.asJson
case v: KafkaChannelBinding => "kafka" -> v.asJson
}: _*
)
)
Expand All @@ -156,9 +156,9 @@ package circe {
nullIfEmpty(a)(
Json.obj(
a.map {
case v: HttpServerBinding => "http" -> v.asJson
case v: HttpServerBinding => "http" -> v.asJson
case v: WebSocketServerBinding => "ws" -> v.asJson
case v: KafkaServerBinding => "kafka" -> v.asJson
case v: KafkaServerBinding => "kafka" -> v.asJson
}: _*
)
)
Expand All @@ -167,7 +167,7 @@ package circe {
private def nullIfEmpty[T](a: List[T])(otherwise: => Json): Json = if (a.isEmpty) Json.Null else otherwise

implicit val encoderMessagePayload: Encoder[Option[Either[AnyValue, ReferenceOr[Schema]]]] = {
case None => Json.Null
case None => Json.Null
case Some(Left(av)) => encoderAnyValue.apply(av)
case Some(Right(s)) => encoderReferenceOr[Schema].apply(s)
}
Expand All @@ -179,7 +179,7 @@ package circe {
implicit val encoderOneOfMessage: Encoder[OneOfMessage] = deriveEncoder[OneOfMessage]
implicit val encoderMessage: Encoder[Message] = {
case s: SingleMessage => encoderSingleMessage.apply(s)
case o: OneOfMessage => encoderOneOfMessage.apply(o)
case o: OneOfMessage => encoderOneOfMessage.apply(o)
}

implicit val encoderOperationTrait: Encoder[OperationTrait] =
Expand All @@ -196,16 +196,16 @@ package circe {
implicit val encoderAsyncAPI: Encoder[AsyncAPI] = deriveEncoder[AsyncAPI].mapJsonObject(expandExtensions)

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

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

private def doEncodeListMap[V: Encoder](nullWhenEmpty: Boolean): Encoder[ListMap[String, V]] = {
case m: ListMap[String, V] if m.isEmpty && nullWhenEmpty => Json.Null
case m: ListMap[String, V] =>
val properties = m.mapValues(v => implicitly[Encoder[V]].apply(v)).toList
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
Json.obj(properties: _*)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package sttp.apispec
package asyncapi
package circe

import io.circe.Json
import org.scalatest.funsuite.AnyFunSuite

import scala.collection.immutable.ListMap
import io.circe.syntax._

class EncoderTest extends AnyFunSuite {
test("encode as expected") {
val expected =
parse(
"""{
| "messages" : {
| "string" : {
| "payload" : {
| "type" : "string"
| },
| "contentType" : "text/plain"
| }
| }
|}""".stripMargin)

val comp = Components(messages = ListMap("string" -> Right(SingleMessage(payload = Some(Right(Right(Schema(SchemaType.String)))), contentType = Some("text/plain")))))

assert(expected === comp.asJson.deepDropNullValues)
}

def parse(s: String): Json = io.circe.parser.parse(s).fold(throw _, identity)
}
52 changes: 26 additions & 26 deletions asyncapi-model/src/main/scala/sttp/apispec/asyncapi/AsyncAPI.scala
Original file line number Diff line number Diff line change
Expand Up @@ -141,36 +141,36 @@ case class KafkaMessageBinding(key: Option[Schema], bindingVersion: Option[Strin
sealed trait Message
case class OneOfMessage(oneOf: List[SingleMessage]) extends Message
case class SingleMessage(
headers: Option[ReferenceOr[Schema]],
payload: Option[Either[AnyValue, ReferenceOr[Schema]]],
correlationId: Option[ReferenceOr[Schema]],
schemaFormat: Option[String],
contentType: Option[String],
name: Option[String],
title: Option[String],
summary: Option[String],
description: Option[String],
tags: List[Tag],
externalDocs: Option[ExternalDocumentation],
bindings: List[MessageBinding],
examples: List[Map[String, List[ExampleValue]]],
traits: List[ReferenceOr[MessageTrait]],
headers: Option[ReferenceOr[Schema]] = None,
payload: Option[Either[AnyValue, ReferenceOr[Schema]]] = None,
correlationId: Option[ReferenceOr[Schema]] = None,
schemaFormat: Option[String] = None,
contentType: Option[String] = None,
name: Option[String] = None,
title: Option[String] = None,
summary: Option[String] = None,
description: Option[String] = None,
tags: List[Tag] = Nil,
externalDocs: Option[ExternalDocumentation] = None,
bindings: List[MessageBinding] = Nil,
examples: List[Map[String, List[ExampleValue]]] = Nil,
traits: List[ReferenceOr[MessageTrait]] = Nil,
extensions: ListMap[String, ExtensionValue] = ListMap.empty
) extends Message

case class MessageTrait(
headers: Option[ReferenceOr[Schema]],
correlationId: Option[ReferenceOr[Schema]],
schemaFormat: Option[String],
contentType: Option[String],
name: Option[String],
title: Option[String],
summary: Option[String],
description: Option[String],
tags: List[Tag],
externalDocs: Option[ExternalDocumentation],
bindings: List[MessageBinding],
examples: ListMap[String, ExampleValue],
headers: Option[ReferenceOr[Schema]] = None,
correlationId: Option[ReferenceOr[Schema]] = None,
schemaFormat: Option[String] = None,
contentType: Option[String] = None,
name: Option[String] = None,
title: Option[String] = None,
summary: Option[String] = None,
description: Option[String] = None,
tags: List[Tag] = Nil,
externalDocs: Option[ExternalDocumentation] = None,
bindings: List[MessageBinding] = Nil,
examples: ListMap[String, ExampleValue] = ListMap.empty,
extensions: ListMap[String, ExtensionValue] = ListMap.empty
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ package openapi
package internal

import cats.syntax.all._
import io.circe.{Decoder, KeyDecoder, DecodingFailure, Json, JsonObject}
import io.circe.{Decoder, DecodingFailure, Json, JsonObject, KeyDecoder}
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 @@ -5,8 +5,9 @@ package internal
import io.circe.generic.semiauto._
import io.circe.parser._
import io.circe.syntax._
import io.circe.{Encoder, KeyEncoder, Json, JsonObject}
import io.circe.{Encoder, Json, JsonObject, KeyEncoder}

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

trait InternalSttpOpenAPICirceEncoders {
Expand All @@ -16,7 +17,7 @@ trait InternalSttpOpenAPICirceEncoders {
case Left(Reference(ref, summary, description)) =>
Json
.obj(
"$ref" := ref,
s"$$ref" := ref,
"summary" := summary,
"description" := description
)
Expand Down Expand Up @@ -64,7 +65,7 @@ trait InternalSttpOpenAPICirceEncoders {
val minKey = if (s.exclusiveMinimum.getOrElse(false)) "exclusiveMinimum" else "minimum"
val maxKey = if (s.exclusiveMaximum.getOrElse(false)) "exclusiveMaximum" else "maximum"
JsonObject(
"$schema" := s.$schema,
s"$$schema" := s.$schema,
"allOf" := s.allOf,
"title" := s.title,
"required" := s.required,
Expand Down Expand Up @@ -154,13 +155,14 @@ trait InternalSttpOpenAPICirceEncoders {
val respJson = resp.responses.asJson
respJson.asObject.map(_.deepMerge(extensions).asJson).getOrElse(respJson)
}

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)
implicit def encodeListMap[V: Encoder]: Encoder[ListMap[String, V]] = doEncodeListMap(nullWhenEmpty = false) : @nowarn

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

deriveEncoder[Operation].mapJsonObject(expandExtensions)
}
Expand Down Expand Up @@ -191,9 +193,9 @@ trait InternalSttpOpenAPICirceEncoders {
implicit def encodeListMap[K: KeyEncoder, V: Encoder]: Encoder[ListMap[K, V]] = doEncodeListMap(nullWhenEmpty = true)

private def doEncodeListMap[K: KeyEncoder, V: Encoder](nullWhenEmpty: Boolean): Encoder[ListMap[K, V]] = {
case m: ListMap[String, V] if m.isEmpty && nullWhenEmpty => Json.Null
case m: ListMap[String, V] =>
val properties = m.mapValues(v => implicitly[Encoder[V]].apply(v)).toList
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: _*)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import org.scalatest.funsuite.AnyFunSuite

class DecoderTest extends AnyFunSuite with ResourcePlatform {
test("petstore deserialize") {
val Right(openapi) = readJson("/petstore/basic-petstore.json").flatMap(_.as[OpenAPI])
val Right(openapi) = readJson("/petstore/basic-petstore.json").flatMap(_.as[OpenAPI]): @unchecked

assert(openapi.info.description === Some("This is a sample server for a pet store."))
}

test("spec any nothing schema boolean") {
val Right(openapi) = readJson("/spec/3.1/any_and_nothing1.json").flatMap(_.as[OpenAPI])
val Right(openapi) = readJson("/spec/3.1/any_and_nothing1.json").flatMap(_.as[OpenAPI]) : @unchecked

assert(openapi.info.title === "API")
val schemas = openapi.components.getOrElse(Components.Empty).schemas
Expand All @@ -22,7 +22,7 @@ class DecoderTest extends AnyFunSuite with ResourcePlatform {
}

test("spec any nothing schema object") {
val Right(openapi) = readJson("/spec/3.1/any_and_nothing2.json").flatMap(_.as[OpenAPI])
val Right(openapi) = readJson("/spec/3.1/any_and_nothing2.json").flatMap(_.as[OpenAPI]) : @unchecked

assert(openapi.info.title === "API")
val schemas = openapi.components.getOrElse(Components.Empty).schemas
Expand All @@ -32,11 +32,11 @@ class DecoderTest extends AnyFunSuite with ResourcePlatform {
}

test("all schemas types") {
val Right(openapi) = readJson("/spec/3.1/schema.json").flatMap(_.as[OpenAPI])
val Right(openapi) = readJson("/spec/3.1/schema.json").flatMap(_.as[OpenAPI]) : @unchecked
assert(openapi.info.title === "API")
val schemas = openapi.components.getOrElse(Components.Empty).schemas
assert(schemas.nonEmpty)
val Right(model) = schemas("model")
val Right(model) = schemas("model"): @unchecked
assert(model.asInstanceOf[Schema].properties.size === 12)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class EncoderTest extends AnyFunSuite with ResourcePlatform {
)

val serialized = withPathItem.asJson
val Right(json) = readJson("/petstore/basic-petstore.json")
val Right(json) = readJson("/petstore/basic-petstore.json"): @unchecked

assert(serialized === json)
}
Expand Down Expand Up @@ -112,7 +112,7 @@ class EncoderTest extends AnyFunSuite with ResourcePlatform {
)

val openApiJson = openapi.asJson
val Right(json) = readJson("/spec/3.1/schema.json")
val Right(json) = readJson("/spec/3.1/schema.json"): @unchecked

assert(openApiJson.spaces2SortKeys == json.spaces2SortKeys)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class EncoderTest extends AnyFunSuite with ResourcePlatform {
)

val openApiJson = openapi.asJson
val Right(json) = readJson("/spec/3.1/any_and_nothing1.json")
val Right(json) = readJson("/spec/3.1/any_and_nothing1.json"): @unchecked

assert(openApiJson.spaces2SortKeys == json.spaces2SortKeys)
}
Expand All @@ -51,7 +51,7 @@ class EncoderTest extends AnyFunSuite with ResourcePlatform {
)

val openApiJson = openapi.asJson
val Right(json) = readJson("/spec/3.1/any_and_nothing2.json")
val Right(json) = readJson("/spec/3.1/any_and_nothing2.json"): @unchecked

assert(openApiJson.spaces2SortKeys == json.spaces2SortKeys)
}
Expand Down

0 comments on commit 51a80ff

Please sign in to comment.