diff --git a/modules/core/src/alloy/openapi/OpenApiConfigExtension.java b/modules/core/src/alloy/openapi/OpenApiConfigExtension.java new file mode 100644 index 0000000..20416d0 --- /dev/null +++ b/modules/core/src/alloy/openapi/OpenApiConfigExtension.java @@ -0,0 +1,38 @@ +/* Copyright 2022 Disney Streaming + * + * Licensed under the Tomorrow Open Source Technology License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://disneystreaming.github.io/TOST-1.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package alloy.openapi; + +public final class OpenApiConfigExtension { + + + // OpenAPI 3.1 supports multiple examples, but Swagger UI do not (see https://github.com/swagger-api/swagger-ui/issues/10503), so this feature is hidden by default. + private boolean enableMultipleExamples = false; + + public void setEnableMultipleExamples(boolean enableMultipleExamples) { + this.enableMultipleExamples = enableMultipleExamples; + } + + public boolean getEnableMultipleExamples() { + return this.enableMultipleExamples; + } + + @Override + public String toString() { + return "OpenApiConfigExtension{" + + "enableMultipleExamples=" + enableMultipleExamples + + '}'; + } +} diff --git a/modules/openapi/src/alloy/openapi/DataExamplesMapper.scala b/modules/openapi/src/alloy/openapi/DataExamplesMapper.scala index f65fdd1..cee985b 100644 --- a/modules/openapi/src/alloy/openapi/DataExamplesMapper.scala +++ b/modules/openapi/src/alloy/openapi/DataExamplesMapper.scala @@ -24,6 +24,9 @@ import scala.jdk.CollectionConverters._ import alloy.DataExamplesTrait import software.amazon.smithy.model.node.ObjectNode import software.amazon.smithy.model.node.Node +import software.amazon.smithy.openapi.OpenApiConfig +import software.amazon.smithy.openapi.OpenApiVersion +import scala.math.Ordering.Implicits._ class DataExamplesMapper() extends JsonSchemaMapper { @@ -32,25 +35,52 @@ class DataExamplesMapper() extends JsonSchemaMapper { schemaBuilder: Builder, config: JsonSchemaConfig ): Builder = if (shape.hasTrait(classOf[DataExamplesTrait])) { - shape + val examples = shape .getTrait(classOf[DataExamplesTrait]) .get .getExamples() .asScala - .headOption match { - case Some(example) - if example.getExampleType != DataExamplesTrait.DataExampleType.STRING => - schemaBuilder.putExtension("example", example.getContent()) - case Some(example) - if example.getExampleType == DataExamplesTrait.DataExampleType.STRING => - val maybeStrNode = example.getContent().asStringNode() - val res = if (maybeStrNode.isPresent) { - Node.parse(maybeStrNode.get.getValue) - } else { - ObjectNode.builder().build() - } - schemaBuilder.putExtension("example", res) - case _ => schemaBuilder - } + .toList + implicit val ordering: Ordering[OpenApiVersion] = + Ordering.fromLessThan[OpenApiVersion]((a, b) => a.compareTo(b) < 0) + val configExtension = config.getExtensions(classOf[OpenApiConfigExtension]) + if (examples.isEmpty) + schemaBuilder + else + config match { + case openApiConfig: OpenApiConfig + if openApiConfig.getVersion >= OpenApiVersion.VERSION_3_1_0 && configExtension.getEnableMultipleExamples => + putMultipleExamples(examples, schemaBuilder) + case _ => + putSingleExample(examples.head, schemaBuilder) + } } else schemaBuilder + + private def convertExample(example: DataExamplesTrait.DataExample) = { + if (example.getExampleType == DataExamplesTrait.DataExampleType.STRING) { + val maybeStrNode = example.getContent().asStringNode() + if (maybeStrNode.isPresent) { + Node.parse(maybeStrNode.get.getValue) + } else { + ObjectNode.builder().build() + } + } else { + example.getContent() + } + } + + private def putSingleExample( + example: DataExamplesTrait.DataExample, + schemaBuilder: Builder + ) = + schemaBuilder.putExtension("example", convertExample(example)) + + private def putMultipleExamples( + examples: List[DataExamplesTrait.DataExample], + schemaBuilder: Builder + ) = { + val array = Node.arrayNode(examples.map(convertExample) *) + schemaBuilder.putExtension("examples", array) + } + } diff --git a/modules/openapi/test/resources/bar.json b/modules/openapi/test/resources/bar.json index ea01903..ad17522 100644 --- a/modules/openapi/test/resources/bar.json +++ b/modules/openapi/test/resources/bar.json @@ -10,7 +10,7 @@ "operationId": "BarOp", "responses": { "200": { - "description": "BarOp200response", + "description": "BarOp 200 response", "content": { "application/json": { "schema": { @@ -64,4 +64,4 @@ } } } -} \ No newline at end of file +} diff --git a/modules/openapi/test/resources/foo.json b/modules/openapi/test/resources/foo.json index 45315e6..57e8a31 100644 --- a/modules/openapi/test/resources/foo.json +++ b/modules/openapi/test/resources/foo.json @@ -10,7 +10,7 @@ "operationId": "GetUnion", "responses": { "200": { - "description": "GetUnion200response", + "description": "GetUnion 200 response", "content": { "application/json": { "schema": { @@ -20,7 +20,7 @@ } }, "500": { - "description": "GeneralServerError500response", + "description": "GeneralServerError 500 response", "content": { "application/json": { "schema": { @@ -36,7 +36,7 @@ "get": { "summary": "A simple greeting operation", "externalDocs": { - "description": "APIHomepage2", + "description": "API Homepage 2", "url": "https://www.example2.com/" }, "operationId": "Greet", @@ -108,7 +108,7 @@ ], "responses": { "200": { - "description": "Greet200response", + "description": "Greet 200 response", "content": { "application/json": { "schema": { @@ -118,7 +118,7 @@ } }, "404": { - "description": "404Response", + "description": "404 Response", "headers": { "x-error-one": { "schema": { @@ -129,7 +129,7 @@ } }, "500": { - "description": "GeneralServerError500response", + "description": "GeneralServerError 500 response", "content": { "application/json": { "schema": { @@ -153,17 +153,17 @@ "examples": { "ONE": { "value": { - "in": "testinput" + "in": "test input" } }, "THREE": { "value": { - "in": "testinputthree" + "in": "test input three" } }, "TWO": { "value": { - "in": "testinputtwo" + "in": "test input two" } } } @@ -173,7 +173,7 @@ }, "responses": { "200": { - "description": "TestErrorsInExamples200response", + "description": "TestErrorsInExamples 200 response", "content": { "application/json": { "schema": { @@ -182,7 +182,7 @@ "examples": { "ONE": { "value": { - "out": "testoutput" + "out": "test output" } } } @@ -190,7 +190,7 @@ } }, "404": { - "description": "404Response", + "description": "404 Response", "headers": { "x-one": { "schema": { @@ -223,12 +223,12 @@ "examples": { "THREE": { "value": { - "messageTwo": "Notfoundmessagetwo" + "messageTwo": "Not found message two" } }, "TWO": { "value": { - "message": "Notfoundmessage" + "message": "Not found message" } } } @@ -236,7 +236,7 @@ } }, "500": { - "description": "GeneralServerError500response", + "description": "GeneralServerError 500 response", "content": { "application/json": { "schema": { @@ -253,7 +253,7 @@ "operationId": "GetValues", "responses": { "200": { - "description": "GetValues200response", + "description": "GetValues 200 response", "content": { "application/json": { "schema": { @@ -263,7 +263,7 @@ } }, "500": { - "description": "GeneralServerError500response", + "description": "GeneralServerError 500 response", "content": { "application/json": { "schema": { @@ -694,7 +694,7 @@ } }, "externalDocs": { - "description": "APIHomepage", + "description": "API Homepage", "url": "https://www.example.com/" } -} \ No newline at end of file +} diff --git a/modules/openapi/test/resources/foo.smithy b/modules/openapi/test/resources/foo.smithy index 4d66928..ba2b7d5 100644 --- a/modules/openapi/test/resources/foo.smithy +++ b/modules/openapi/test/resources/foo.smithy @@ -199,6 +199,9 @@ union DoubleOrFloat { @dataExamples([ { smithy: { name: "Meow" } + }, + { + smithy: { name: "Miau" } } ]) structure Cat { @@ -208,6 +211,9 @@ structure Cat { @dataExamples([ { json: { name: "Woof" } + }, + { + json: { name: "Hau hau" } } ]) structure Dog { diff --git a/modules/openapi/test/resources/foo_3.1.0.json b/modules/openapi/test/resources/foo_3.1.0.json new file mode 100644 index 0000000..eeb20fb --- /dev/null +++ b/modules/openapi/test/resources/foo_3.1.0.json @@ -0,0 +1,712 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "HelloWorldService", + "version": "0.0.1" + }, + "paths": { + "/default": { + "get": { + "operationId": "GetUnion", + "responses": { + "200": { + "description": "GetUnion 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetUnionResponseContent" + } + } + } + }, + "500": { + "description": "GeneralServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GeneralServerErrorResponseContent" + } + } + } + } + } + } + }, + "/hello/{name}/{ts}": { + "get": { + "summary": "A simple greeting operation", + "externalDocs": { + "description": "API Homepage 2", + "url": "https://www.example2.com/" + }, + "operationId": "Greet", + "parameters": [ + { + "name": "name", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "ts", + "in": "path", + "schema": { + "type": "string", + "format": "date-time" + }, + "required": true + }, + { + "name": "from", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "whenThree", + "in": "header", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "whenFour", + "in": "header", + "schema": { + "type": "string", + "format": "epoch-seconds" + } + }, + { + "name": "X-Bamtech-Partner", + "in": "header", + "schema": { + "type": "string" + } + }, + { + "name": "when", + "in": "header", + "schema": { + "type": "string", + "format": "http-date" + } + }, + { + "name": "whenAlso", + "in": "header", + "schema": { + "type": "string", + "format": "http-date" + } + } + ], + "responses": { + "200": { + "description": "Greet 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GreetOutputPayload" + } + } + } + }, + "404": { + "description": "404 Response", + "headers": { + "x-error-one": { + "schema": { + "type": "string" + }, + "required": true + } + } + }, + "500": { + "description": "GeneralServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GeneralServerErrorResponseContent" + } + } + } + } + } + } + }, + "/test_errors": { + "post": { + "operationId": "TestErrorsInExamples", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TestErrorsInExamplesRequestContent" + }, + "examples": { + "ONE": { + "value": { + "in": "test input" + } + }, + "THREE": { + "value": { + "in": "test input three" + } + }, + "TWO": { + "value": { + "in": "test input two" + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "TestErrorsInExamples 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TestErrorsInExamplesResponseContent" + }, + "examples": { + "ONE": { + "value": { + "out": "test output" + } + } + } + } + } + }, + "404": { + "description": "404 Response", + "headers": { + "x-one": { + "schema": { + "type": "string" + } + }, + "x-three": { + "schema": { + "type": "string" + } + }, + "x-two": { + "schema": { + "type": "string" + } + } + }, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/NotFoundResponseContent" + }, + { + "$ref": "#/components/schemas/NotFoundTwoResponseContent" + } + ] + }, + "examples": { + "THREE": { + "value": { + "messageTwo": "Not found message two" + } + }, + "TWO": { + "value": { + "message": "Not found message" + } + } + } + } + } + }, + "500": { + "description": "GeneralServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GeneralServerErrorResponseContent" + } + } + } + } + } + } + }, + "/values": { + "get": { + "operationId": "GetValues", + "responses": { + "200": { + "description": "GetValues 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetValuesResponseContent" + } + } + } + }, + "500": { + "description": "GeneralServerError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GeneralServerErrorResponseContent" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Cat": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "examples": [ + { + "name": "Meow" + }, + { + "name": "Miau" + } + ] + }, + "CatOrDog": { + "oneOf": [ + { + "$ref": "#/components/schemas/CatOrDogCat" + }, + { + "$ref": "#/components/schemas/CatOrDogDog" + } + ], + "externalDocs": { + "description": "Homepage", + "url": "https://www.example.com/" + }, + "discriminator": { + "propertyName": "type", + "mapping": { + "cat": "#/components/schemas/CatOrDogCat", + "dog": "#/components/schemas/CatOrDogDog" + } + } + }, + "CatOrDogCat": { + "allOf": [ + { + "$ref": "#/components/schemas/Cat" + }, + { + "$ref": "#/components/schemas/CatOrDogMixin" + } + ] + }, + "CatOrDogDog": { + "allOf": [ + { + "$ref": "#/components/schemas/Dog" + }, + { + "$ref": "#/components/schemas/CatOrDogMixin" + } + ] + }, + "CatOrDogMixin": { + "type": "object", + "properties": { + "type": { + "type": "string" + } + }, + "required": [ + "type" + ] + }, + "CatOrDogOpen": { + "oneOf": [ + { + "type": "object", + "title": "cat", + "properties": { + "cat": { + "$ref": "#/components/schemas/Cat" + } + }, + "required": [ + "cat" + ] + }, + { + "type": "object", + "title": "dog", + "properties": { + "dog": { + "$ref": "#/components/schemas/Dog" + } + }, + "required": [ + "dog" + ] + }, + { + "type": "object", + "additionalProperties": true, + "title": "other" + } + ] + }, + "CatOrDogOpenDiscriminated": { + "oneOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/CatOrDogOpenDiscriminatedCat" + }, + { + "$ref": "#/components/schemas/CatOrDogOpenDiscriminatedDog" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "cat": "#/components/schemas/CatOrDogOpenDiscriminatedCat", + "dog": "#/components/schemas/CatOrDogOpenDiscriminatedDog" + } + } + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/CatOrDogOpenDiscriminatedMixin" + }, + { + "additionalProperties": true + } + ] + } + ] + }, + "CatOrDogOpenDiscriminatedCat": { + "allOf": [ + { + "$ref": "#/components/schemas/Cat" + }, + { + "$ref": "#/components/schemas/CatOrDogOpenDiscriminatedMixin" + } + ] + }, + "CatOrDogOpenDiscriminatedDog": { + "allOf": [ + { + "$ref": "#/components/schemas/Dog" + }, + { + "$ref": "#/components/schemas/CatOrDogOpenDiscriminatedMixin" + } + ] + }, + "CatOrDogOpenDiscriminatedMixin": { + "type": "object", + "properties": { + "type": { + "type": "string" + } + }, + "required": [ + "type" + ] + }, + "Dog": { + "type": "object", + "additionalProperties": true, + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "examples": [ + { + "name": "Woof" + }, + { + "name": "Hau hau" + } + ] + }, + "DoubleOrFloat": { + "oneOf": [ + { + "type": "object", + "title": "float", + "properties": { + "float": { + "type": "number", + "format": "float" + } + }, + "required": [ + "float" + ] + }, + { + "type": "object", + "title": "double", + "properties": { + "double": { + "type": "number", + "format": "double" + } + }, + "required": [ + "double" + ] + } + ] + }, + "GeneralServerErrorResponseContent": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "count": { + "type": "integer", + "format": "int32", + "nullable": true + } + } + }, + "GetUnionResponseContent": { + "type": "object", + "properties": { + "intOrString": { + "$ref": "#/components/schemas/IntOrString" + }, + "doubleOrFloat": { + "$ref": "#/components/schemas/DoubleOrFloat" + }, + "catOrDog": { + "$ref": "#/components/schemas/CatOrDog" + }, + "catOrDogOpen": { + "$ref": "#/components/schemas/CatOrDogOpen" + }, + "catOrDogOpenDiscriminated": { + "$ref": "#/components/schemas/CatOrDogOpenDiscriminated" + }, + "vehicle": { + "$ref": "#/components/schemas/Vehicle" + } + } + }, + "GetValuesResponseContent": { + "type": "object", + "properties": { + "values": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SomeValue" + } + } + }, + "examples": [ + { + "values": [] + } + ] + }, + "GreetOutputPayload": { + "type": "string" + }, + "IntOrString": { + "oneOf": [ + { + "type": "object", + "title": "int", + "properties": { + "int": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "int" + ] + }, + { + "type": "object", + "title": "string", + "properties": { + "string": { + "type": "string" + } + }, + "required": [ + "string" + ] + } + ] + }, + "NotFoundResponseContent": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ] + }, + "NotFoundTwoResponseContent": { + "type": "object", + "properties": { + "messageTwo": { + "type": "string" + } + }, + "required": [ + "messageTwo" + ] + }, + "SomeValue": { + "oneOf": [ + { + "type": "string", + "title": "message" + }, + { + "type": "integer", + "title": "value", + "format": "int32" + } + ] + }, + "TestErrorsInExamplesRequestContent": { + "type": "object", + "properties": { + "in": { + "type": "string" + } + }, + "required": [ + "in" + ] + }, + "TestErrorsInExamplesResponseContent": { + "type": "object", + "properties": { + "out": { + "type": "string" + } + }, + "required": [ + "out" + ] + }, + "Vehicle": { + "oneOf": [ + { + "$ref": "#/components/schemas/VehicleCarCase" + }, + { + "$ref": "#/components/schemas/VehiclePlaneCase" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "car": "#/components/schemas/VehicleCarCase", + "plane": "#/components/schemas/VehiclePlaneCase" + } + } + }, + "VehicleCar": { + "type": "object", + "properties": { + "year": { + "type": "integer", + "format": "int32" + }, + "model": { + "type": "string" + }, + "make": { + "type": "string" + } + } + }, + "VehicleCarCase": { + "allOf": [ + { + "$ref": "#/components/schemas/VehicleCar" + }, + { + "$ref": "#/components/schemas/VehicleMixin" + } + ] + }, + "VehicleMixin": { + "type": "object", + "properties": { + "type": { + "type": "string" + } + }, + "required": [ + "type" + ] + }, + "VehiclePlane": { + "type": "object", + "properties": { + "model": { + "type": "string" + } + } + }, + "VehiclePlaneCase": { + "allOf": [ + { + "$ref": "#/components/schemas/VehiclePlane" + }, + { + "$ref": "#/components/schemas/VehicleMixin" + } + ] + } + } + }, + "externalDocs": { + "description": "API Homepage", + "url": "https://www.example.com/" + } +} diff --git a/modules/openapi/test/src/alloy/openapi/OpenApiConversionSpec.scala b/modules/openapi/test/src/alloy/openapi/OpenApiConversionSpec.scala index 49a705d..bc06efa 100644 --- a/modules/openapi/test/src/alloy/openapi/OpenApiConversionSpec.scala +++ b/modules/openapi/test/src/alloy/openapi/OpenApiConversionSpec.scala @@ -17,13 +17,13 @@ package alloy.openapi import _root_.software.amazon.smithy.model.Model -import scala.io.Source -import scala.util.Using import software.amazon.smithy.model.node.Node import software.amazon.smithy.openapi.OpenApiConfig import software.amazon.smithy.openapi.OpenApiVersion import scala.jdk.CollectionConverters._ +import os.ResourcePath +import munit.diff.Printer final class OpenApiConversionSpec extends munit.FunSuite { @@ -37,23 +37,52 @@ final class OpenApiConversionSpec extends munit.FunSuite { val result = convert(model, None) .map(_.contents) - .mkString - .filterNot(_.isWhitespace) + .map(Node.parse) + .requireOnly - val expected = os.read(os.resource / "foo.json").filterNot(_.isWhitespace) + val expected = readAndParse(os.resource / "foo.json") - if (result != expected) { - val tmp = os.pwd / "actual" / "foo.json" + assertEquals( + result, + expected + ) + } - os.write.over( - tmp, - Node.prettyPrintJson(Node.parse(result)), - createFolders = true - ) - fail( - s"Values are not the same. Wrote current output to $tmp for easier debugging." - ) - } + test( + "OpenAPI conversion from alloy#simpleRestJson protocol (3.1.0 with multiple examples)" + ) { + val model = Model + .assembler() + .addImport(getClass().getClassLoader().getResource("foo.smithy")) + .discoverModels() + .assemble() + .unwrap() + + val result = convertWithConfig( + model, + None, + buildConfig = _ => { + val config = new OpenApiConfig() + config.setVersion(OpenApiVersion.VERSION_3_1_0) + config.putExtensions { + val ext = new OpenApiConfigExtension() + ext.setEnableMultipleExamples(true) + ext + } + config + } + ) + .map(_.contents) + .map(Node.parse) + .requireOnly + + val expected = readAndParse(os.resource / "foo_3.1.0.json") + + assertEquals( + result, + expected, + "Values are not the same" + ) } test( @@ -69,13 +98,10 @@ final class OpenApiConversionSpec extends munit.FunSuite { val result = convert(model, Some(Set("bar"))) .map(_.contents) - .mkString - .filterNot(_.isWhitespace) + .map(Node.parse) + .requireOnly - val expected = Using - .resource(Source.fromResource("bar.json"))( - _.getLines().mkString.filterNot(_.isWhitespace) - ) + val expected = readAndParse(os.resource / "bar.json") assertEquals(result, expected) } @@ -93,13 +119,10 @@ final class OpenApiConversionSpec extends munit.FunSuite { val result = convert(model, Some(Set("baz"))) .map(_.contents) - .mkString - .filterNot(_.isWhitespace) + .map(Node.parse) + .requireOnly - val expected = Using - .resource(Source.fromResource("baz.json"))( - _.getLines().mkString.filterNot(_.isWhitespace) - ) + val expected = readAndParse(os.resource / "baz.json") assertEquals(result, expected) } @@ -114,13 +137,11 @@ final class OpenApiConversionSpec extends munit.FunSuite { val result = convert(model, None) .map(_.contents) - .mkString - .filterNot(_.isWhitespace) + .map(Node.parse) + .requireOnly + + val expected = readAndParse(os.resource / "baz.json") - val expected = Using - .resource(Source.fromResource("baz.json"))( - _.getLines().mkString.filterNot(_.isWhitespace) - ) assertEquals(result, expected) } @@ -142,10 +163,19 @@ final class OpenApiConversionSpec extends munit.FunSuite { } ) .map(_.contents) - .mkString - .filterNot(_.isWhitespace) + .map(Node.parse) + .requireOnly + + val resultOpenApiVersion = + result.expectObjectNode().expectStringMember("openapi").getValue() - assert(result.contains("\"openapi\":\"3.1.0")) + assertEquals(resultOpenApiVersion, "3.1.0") + } + + override def printer: Printer = super.printer.orElse { + Printer(Printer.defaultHeight) { case node: Node => + Node.prettyPrintJson(node) + } } test("OpenAPI conversion with JSON manipulating config") { @@ -159,24 +189,25 @@ final class OpenApiConversionSpec extends munit.FunSuite { val config = new OpenApiConfig() config.setJsonAdd( Map[String, Node]( - "/info/title" -> Node.from("Customtitlegoeshere") + "/info/title" -> Node.from("Custom title goes here") ).asJava ) config.setSubstitutions( Map[String, Node]("X-Bamtech-Partner" -> Node.from("X-Foo")).asJava ) - val result = convertWithConfig( - model, - None, - _ => config - ).map(_.contents) - .mkString - .filterNot(_.isWhitespace) - - assert(result.contains("\"title\":\"Customtitlegoeshere\"")) - assert(!result.contains("X-Bamtech-Partner")) - assert(result.contains("\"name\":\"X-Foo\"")) + val result = + convertWithConfig( + model, + None, + _ => config + ).map(_.contents) + .map(dropWhitespaceInJson) + .requireOnly + + assert(clue(result).contains("\"title\":\"Custom title goes here\"")) + assert(!clue(result).contains("X-Bamtech-Partner")) + assert(clue(result).contains("\"name\":\"X-Foo\"")) } test("OpenAPI conversion of date time types") { @@ -188,7 +219,10 @@ final class OpenApiConversionSpec extends munit.FunSuite { .unwrap() val result = - convert(model, None).map(_.contents).mkString.filterNot(_.isWhitespace) + convert(model, None) + .map(_.contents) + .map(dropWhitespaceInJson) + .requireOnly val expected = List( """"localDate":{"type":"string","x-format":"local-date"}""", @@ -208,4 +242,20 @@ final class OpenApiConversionSpec extends munit.FunSuite { assert(result.contains(expected)) } } + + private def dropWhitespaceInJson(s: String): String = + Node.printJson(Node.parse(s)) + + private def readAndParse(path: ResourcePath) = Node.parse(os.read(path)) + + implicit class CollectionOnlyOps[T](private val coll: List[T]) { + def requireOnly: T = { + assert( + coll.size == 1, + s"Expected exactly 1 element, got ${coll.size}: $coll" + ) + coll.head + } + } + }