From bbe1a06e92111565369150d2f51780713c456024 Mon Sep 17 00:00:00 2001 From: Arseniy Zhizhelev Date: Thu, 25 Jan 2024 12:56:35 +0300 Subject: [PATCH 1/2] #136 Decode ListMap[String, MediaType|Encoding|String|Reference[A]] --- .../InternalSttpOpenAPICirceDecoders.scala | 38 ++++++++++--------- .../resources/petstore/basic-petstore.json | 28 +++++++++++++- .../openapi/circe/threeone/EncoderTest.scala | 23 ++++++++++- 3 files changed, 68 insertions(+), 21 deletions(-) diff --git a/openapi-circe/src/main/scala/sttp/apispec/openapi/internal/InternalSttpOpenAPICirceDecoders.scala b/openapi-circe/src/main/scala/sttp/apispec/openapi/internal/InternalSttpOpenAPICirceDecoders.scala index 0e307fb..bc246ef 100644 --- a/openapi-circe/src/main/scala/sttp/apispec/openapi/internal/InternalSttpOpenAPICirceDecoders.scala +++ b/openapi-circe/src/main/scala/sttp/apispec/openapi/internal/InternalSttpOpenAPICirceDecoders.scala @@ -14,18 +14,29 @@ trait InternalSttpOpenAPICirceDecoders extends JsonSchemaCirceDecoders { implicit val referenceDecoder: Decoder[Reference] = deriveDecoder[Reference] implicit def decodeReferenceOr[A: Decoder]: Decoder[ReferenceOr[A]] = referenceDecoder.either(Decoder[A]) + def listMapStringADecoder[A: Decoder]: Decoder[ListMap[String, A]] = + Decoder.decodeOption(Decoder.decodeMapLike[String, A, ListMap]).map(_.getOrElse(ListMap.empty)) + + def listADecoder[A: Decoder]: Decoder[List[A]] = + Decoder.decodeOption(Decoder.decodeList[A]).map(_.getOrElse(Nil)) + + implicit def listMapStringReferenceOrADecoder[A: Decoder]: Decoder[ListMap[String, ReferenceOr[A]]] = + listMapStringADecoder + implicit def listMapStringMediaTypeDecoder: Decoder[ListMap[String, MediaType]] = + listMapStringADecoder + implicit def listMapStringEncodingDecoder: Decoder[ListMap[String, Encoding]] = + listMapStringADecoder + implicit def listMapStringStringDecoder: Decoder[ListMap[String, String]] = + listMapStringADecoder + implicit val externalDocumentationDecoder: Decoder[ExternalDocumentation] = withExtensions( deriveDecoder[ExternalDocumentation] ) implicit val tagDecoder: Decoder[Tag] = withExtensions(deriveDecoder[Tag]) - implicit val oauthFlowDecoder: Decoder[OAuthFlow] = { - // #79: all OAuth flow object MUST include a scopes field, but it MAY be empty. - implicit def listMapDecoder: Decoder[ListMap[String, String]] = - Decoder.decodeOption(Decoder.decodeMapLike[String, String, ListMap]).map(_.getOrElse(ListMap.empty)) + // #79: all OAuth flow object MUST include a scopes field, but it MAY be empty. + implicit val oauthFlowDecoder: Decoder[OAuthFlow] = withExtensions(deriveDecoder[OAuthFlow]) - withExtensions(deriveDecoder[OAuthFlow]) - } implicit val oauthFlowsDecoder: Decoder[OAuthFlows] = withExtensions(deriveDecoder[OAuthFlows]) implicit val securitySchemeDecoder: Decoder[SecurityScheme] = withExtensions(deriveDecoder[SecurityScheme]) @@ -61,15 +72,8 @@ trait InternalSttpOpenAPICirceDecoders extends JsonSchemaCirceDecoders { implicit val mediaTypeDecoder: Decoder[MediaType] = withExtensions(deriveDecoder[MediaType]) implicit val requestBodyDecoder: Decoder[RequestBody] = withExtensions(deriveDecoder[RequestBody]) - implicit val responseDecoder: Decoder[Response] = { - implicit def listMapDecoder[A: Decoder]: Decoder[ListMap[String, ReferenceOr[A]]] = - Decoder.decodeOption(Decoder.decodeMapLike[String, ReferenceOr[A], ListMap]).map(_.getOrElse(ListMap.empty)) - - implicit def listMapMediaTypeDecoder: Decoder[ListMap[String, MediaType]] = - Decoder.decodeOption(Decoder.decodeMapLike[String, MediaType, ListMap]).map(_.getOrElse(ListMap.empty)) + implicit val responseDecoder: Decoder[Response] = withExtensions(deriveDecoder[Response]) - withExtensions(deriveDecoder[Response]) - } implicit val responsesKeyDecoder: KeyDecoder[ResponsesKey] = { val ResponseRange = "(1|2|3|4|5)XX".r val ResponseCode = "([1|2|3|4|5]\\d\\d)".r @@ -92,17 +96,15 @@ trait InternalSttpOpenAPICirceDecoders extends JsonSchemaCirceDecoders { implicit val parameterDecoder: Decoder[Parameter] = withExtensions(deriveDecoder[Parameter]) implicit val callbackDecoder: Decoder[Callback] = deriveDecoder[Callback] implicit val operationDecoder: Decoder[Operation] = { - implicit def listMapDecoder[A: Decoder]: Decoder[ListMap[String, ReferenceOr[A]]] = - Decoder.decodeOption(Decoder.decodeMapLike[String, ReferenceOr[A], ListMap]).map(_.getOrElse(ListMap.empty)) implicit def listReference[A: Decoder]: Decoder[List[A]] = - Decoder.decodeOption(Decoder.decodeList[A]).map(_.getOrElse(Nil)) + listADecoder withExtensions(deriveDecoder[Operation]) } implicit val pathItemDecoder: Decoder[PathItem] = { implicit def listReference[A: Decoder]: Decoder[List[A]] = - Decoder.decodeOption(Decoder.decodeList[A]).map(_.getOrElse(Nil)) + listADecoder withExtensions(deriveDecoder[PathItem]) } diff --git a/openapi-circe/src/test/resources/petstore/basic-petstore.json b/openapi-circe/src/test/resources/petstore/basic-petstore.json index 2d48672..bc440f4 100644 --- a/openapi-circe/src/test/resources/petstore/basic-petstore.json +++ b/openapi-circe/src/test/resources/petstore/basic-petstore.json @@ -23,7 +23,33 @@ "description": "Gets all pets", "responses": { "200": { - "description": "Success" + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" } } } diff --git a/openapi-circe/src/test/scala/sttp/apispec/openapi/circe/threeone/EncoderTest.scala b/openapi-circe/src/test/scala/sttp/apispec/openapi/circe/threeone/EncoderTest.scala index 1f6978f..e12d26a 100644 --- a/openapi-circe/src/test/scala/sttp/apispec/openapi/circe/threeone/EncoderTest.scala +++ b/openapi-circe/src/test/scala/sttp/apispec/openapi/circe/threeone/EncoderTest.scala @@ -37,6 +37,10 @@ class EncoderTest extends AnyFunSuite with ResourcePlatform with SttpOpenAPICirc def refOr[A](a: A): ReferenceOr[A] = Right(a) + def arrayOf(s: SchemaLike) = Schema(`type` = Some(SchemaType.Array), items = Some(s)) + + def ref(s: String): SchemaLike = Schema($ref = Some(s)) + test("petstore serialize") { val withPathItem = petstore.addPathItem( "/pets", @@ -45,12 +49,27 @@ class EncoderTest extends AnyFunSuite with ResourcePlatform with SttpOpenAPICirc Operation( operationId = Some("getPets"), description = Some("Gets all pets") - ).addResponse(200, Response(description = "Success")) + ).addResponse(200, Response( + description = "Success", + content = ListMap("application/json" -> + MediaType(schema = Some(arrayOf(ref("#/components/schemas/Pet")))) + ) + )) ) ) ) + val petSchema = Schema( + `type` = Some(SchemaType.Object), + properties = + ListMap("id" -> Schema(`type` = Some(SchemaType.Integer), format = Some("int32")), + "name" -> Schema(`type` = Some(SchemaType.String)) + ) + ) + val withComponents = withPathItem.components(Components(schemas = ListMap( + "Pet" -> petSchema + ))) - val serialized = withPathItem.asJson + val serialized = withComponents.asJson val Right(json) = readJson("/petstore/basic-petstore.json"): @unchecked assert(serialized === json) From 7e4e02d7133c75c5c4a45f37eb342f48d19256d1 Mon Sep 17 00:00:00 2001 From: Arseniy Zhizhelev Date: Thu, 25 Jan 2024 13:03:55 +0300 Subject: [PATCH 2/2] #136 Fix server.extensions --- .../openapi/internal/InternalSttpOpenAPICirceDecoders.scala | 2 +- .../src/test/resources/petstore/basic-petstore.json | 5 +++++ .../sttp/apispec/openapi/circe/threeone/EncoderTest.scala | 5 +++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/openapi-circe/src/main/scala/sttp/apispec/openapi/internal/InternalSttpOpenAPICirceDecoders.scala b/openapi-circe/src/main/scala/sttp/apispec/openapi/internal/InternalSttpOpenAPICirceDecoders.scala index bc246ef..bffe32d 100644 --- a/openapi-circe/src/main/scala/sttp/apispec/openapi/internal/InternalSttpOpenAPICirceDecoders.scala +++ b/openapi-circe/src/main/scala/sttp/apispec/openapi/internal/InternalSttpOpenAPICirceDecoders.scala @@ -45,7 +45,7 @@ trait InternalSttpOpenAPICirceDecoders extends JsonSchemaCirceDecoders { implicit val infoDecoder: Decoder[Info] = withExtensions(deriveDecoder[Info]) implicit val serverVariableDecoder: Decoder[ServerVariable] = deriveDecoder[ServerVariable] - implicit val serverDecoder: Decoder[Server] = deriveDecoder[Server] + implicit val serverDecoder: Decoder[Server] = withExtensions(deriveDecoder[Server]) implicit val linkDecoder: Decoder[Link] = deriveDecoder[Link] implicit val parameterInDecoder: Decoder[ParameterIn] = Decoder.decodeString.emap { diff --git a/openapi-circe/src/test/resources/petstore/basic-petstore.json b/openapi-circe/src/test/resources/petstore/basic-petstore.json index bc440f4..a7f22e5 100644 --- a/openapi-circe/src/test/resources/petstore/basic-petstore.json +++ b/openapi-circe/src/test/resources/petstore/basic-petstore.json @@ -16,6 +16,11 @@ }, "version": "1.0.1" }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], "paths": { "/pets": { "get": { diff --git a/openapi-circe/src/test/scala/sttp/apispec/openapi/circe/threeone/EncoderTest.scala b/openapi-circe/src/test/scala/sttp/apispec/openapi/circe/threeone/EncoderTest.scala index e12d26a..6ac6f0a 100644 --- a/openapi-circe/src/test/scala/sttp/apispec/openapi/circe/threeone/EncoderTest.scala +++ b/openapi-circe/src/test/scala/sttp/apispec/openapi/circe/threeone/EncoderTest.scala @@ -68,8 +68,9 @@ class EncoderTest extends AnyFunSuite with ResourcePlatform with SttpOpenAPICirc val withComponents = withPathItem.components(Components(schemas = ListMap( "Pet" -> petSchema ))) - - val serialized = withComponents.asJson + val server = Server(url = "http://petstore.swagger.io/v1") + val withServer = withComponents.servers(List(server)) + val serialized = withServer.asJson val Right(json) = readJson("/petstore/basic-petstore.json"): @unchecked assert(serialized === json)