From a7a5fb5d8916ce8f83f593debd36d6cc8605169e Mon Sep 17 00:00:00 2001 From: Jeff Lewis Date: Mon, 25 Mar 2024 16:52:58 -0600 Subject: [PATCH 1/3] fix issue with openapi error response examples --- .../protocols/AlloyAbstractRestProtocol.scala | 42 ++++++- .../fromsmithy/protocols/ExampleNode.scala | 8 +- modules/openapi/test/resources/foo.json | 106 ++++++++++++++++++ modules/openapi/test/resources/foo.smithy | 47 +++++++- 4 files changed, 196 insertions(+), 7 deletions(-) diff --git a/modules/openapi/src/software/amazon/smithy/openapi/fromsmithy/protocols/AlloyAbstractRestProtocol.scala b/modules/openapi/src/software/amazon/smithy/openapi/fromsmithy/protocols/AlloyAbstractRestProtocol.scala index dd23b49..813390c 100644 --- a/modules/openapi/src/software/amazon/smithy/openapi/fromsmithy/protocols/AlloyAbstractRestProtocol.scala +++ b/modules/openapi/src/software/amazon/smithy/openapi/fromsmithy/protocols/AlloyAbstractRestProtocol.scala @@ -32,6 +32,7 @@ import software.amazon.smithy.openapi.model._ import java.util import java.util.function.Function import scala.jdk.CollectionConverters._ +import software.amazon.smithy.model.traits.ExamplesTrait.Example /* * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. @@ -426,6 +427,43 @@ abstract class AlloyAbstractRestProtocol[T <: Trait] result.asScala } + private def reorganizeExampleTraits( + operation: OperationShape, + shape: StructureShape + ): Shape = { + val isErrorShape = shape.hasTrait(classOf[ErrorTrait]) + val operationOrError = + if (isErrorShape) shape + else operation + + val allExamples: List[Example] = + operation + .getTrait(classOf[ExamplesTrait]) + .asScala + .toList + .flatMap(_.getExamples.asScala) + val allRelevantExamples: List[Example] = + // error response so only include examples that are matching to this error shape + if (isErrorShape) + allExamples + .filter( + _.getError().asScala.map(_.getShapeId).contains(shape.toShapeId) + ) + // not an error response so no error examples should be included + else allExamples.filter(_.getError().isEmpty()) + val newExamplesTraitBuilder = ExamplesTrait.builder() + allRelevantExamples.foreach { ex => newExamplesTraitBuilder.addExample(ex) } + val exTrait: Trait = newExamplesTraitBuilder.build() + val newShape: Shape = + (Shape.shapeToBuilder(operationOrError): AbstractShapeBuilder[_, _]) + .addTrait(exTrait) + .build() + + if (allRelevantExamples.nonEmpty) + newShape + else operationOrError + } + private def updateResponsesMapWithResponseStatusAndObject( context: Context[T], bindingIndex: HttpBindingIndex, @@ -433,9 +471,7 @@ abstract class AlloyAbstractRestProtocol[T <: Trait] shape: StructureShape, responses: util.Map[String, ResponseObject] ) = { - val operationOrError = - if (shape.hasTrait(classOf[ErrorTrait])) shape - else operation + val operationOrError = reorganizeExampleTraits(operation, shape) val statusCode = context.getOpenApiProtocol.getOperationResponseStatusCode( context, operationOrError diff --git a/modules/openapi/src/software/amazon/smithy/openapi/fromsmithy/protocols/ExampleNode.scala b/modules/openapi/src/software/amazon/smithy/openapi/fromsmithy/protocols/ExampleNode.scala index 90ea127..3b3acbd 100644 --- a/modules/openapi/src/software/amazon/smithy/openapi/fromsmithy/protocols/ExampleNode.scala +++ b/modules/openapi/src/software/amazon/smithy/openapi/fromsmithy/protocols/ExampleNode.scala @@ -69,9 +69,11 @@ object ExampleNode { memberNames: List[String] ): ExampleNode = { val members: List[(String, Node)] = memberNames.flatMap(name => - example.getOutput.asScala.flatMap( - _.getMember(name).asScala.map(name -> _) - ) + example.getOutput.asScala + .orElse(example.getError.asScala.map(_.getContent)) + .flatMap( + _.getMember(name).asScala.map(name -> _) + ) ) ExampleNode(example, members) } diff --git a/modules/openapi/test/resources/foo.json b/modules/openapi/test/resources/foo.json index 68ca3d6..4bb3301 100644 --- a/modules/openapi/test/resources/foo.json +++ b/modules/openapi/test/resources/foo.json @@ -129,6 +129,79 @@ } } }, + "/test_errors": { + "post": { + "operationId": "TestErrorsInExamples", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TestErrorsInExamplesRequestContent" + }, + "examples": { + "ONE": { + "value": { + "in": "testinput" + } + }, + "TWO": { + "value": { + "in": "testinputtwo" + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "TestErrorsInExamples200response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TestErrorsInExamplesResponseContent" + }, + "examples": { + "ONE": { + "value": { + "out": "testoutput" + } + } + } + } + } + }, + "404": { + "description": "NotFound404response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundResponseContent" + }, + "examples": { + "TWO": { + "value": { + "message": "Notfoundmessage" + } + } + } + } + } + }, + "500": { + "description": "GeneralServerError500response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GeneralServerErrorResponseContent" + } + } + } + } + } + } + }, "/values": { "get": { "operationId": "GetValues", @@ -340,6 +413,17 @@ } ] }, + "NotFoundResponseContent": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ] + }, "SomeValue": { "oneOf": [ { @@ -352,6 +436,28 @@ "format": "int32" } ] + }, + "TestErrorsInExamplesRequestContent": { + "type": "object", + "properties": { + "in": { + "type": "string" + } + }, + "required": [ + "in" + ] + }, + "TestErrorsInExamplesResponseContent": { + "type": "object", + "properties": { + "out": { + "type": "string" + } + }, + "required": [ + "out" + ] } } }, diff --git a/modules/openapi/test/resources/foo.smithy b/modules/openapi/test/resources/foo.smithy index 79b7b38..bfe2022 100644 --- a/modules/openapi/test/resources/foo.smithy +++ b/modules/openapi/test/resources/foo.smithy @@ -1,3 +1,5 @@ +$version: "2" + namespace foo use alloy#simpleRestJson @@ -14,7 +16,7 @@ use alloy#dataExamples service HelloWorldService { version: "0.0.1", errors: [GeneralServerError], - operations: [Greet, GetUnion, GetValues] + operations: [Greet, GetUnion, GetValues, TestErrorsInExamples] } @externalDocumentation( @@ -28,6 +30,49 @@ operation Greet { output: Greeting } +@error("client") +@httpError(404) +structure NotFound { + @required + message: String +} + +@examples([ + { + title: "ONE" + input: { + in: "test input" + } + output: { + out: "test output" + } + } + { + title: "TWO" + input: { + in: "test input two" + } + error: { + shapeId: NotFound + content: { + message: "Not found message" + } + } + } +]) +@http(method: "POST", uri: "/test_errors") +operation TestErrorsInExamples { + input := { + @required + in: String + } + output := { + @required + out: String + } + errors: [NotFound] +} + @readonly @http(method: "GET", uri: "/default") operation GetUnion { From add4bcd0f1b862c4e380d5b4022d513765c7af24 Mon Sep 17 00:00:00 2001 From: Jeff Lewis Date: Tue, 26 Mar 2024 10:58:53 -0600 Subject: [PATCH 2/3] simplify example trait method --- .../fromsmithy/protocols/AlloyAbstractRestProtocol.scala | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/modules/openapi/src/software/amazon/smithy/openapi/fromsmithy/protocols/AlloyAbstractRestProtocol.scala b/modules/openapi/src/software/amazon/smithy/openapi/fromsmithy/protocols/AlloyAbstractRestProtocol.scala index 813390c..817b972 100644 --- a/modules/openapi/src/software/amazon/smithy/openapi/fromsmithy/protocols/AlloyAbstractRestProtocol.scala +++ b/modules/openapi/src/software/amazon/smithy/openapi/fromsmithy/protocols/AlloyAbstractRestProtocol.scala @@ -459,9 +459,7 @@ abstract class AlloyAbstractRestProtocol[T <: Trait] .addTrait(exTrait) .build() - if (allRelevantExamples.nonEmpty) - newShape - else operationOrError + newShape } private def updateResponsesMapWithResponseStatusAndObject( From fe09c61b21862d56cd3a4c0208b3011719dcf347 Mon Sep 17 00:00:00 2001 From: Jeff Lewis Date: Tue, 26 Mar 2024 10:59:45 -0600 Subject: [PATCH 3/3] fix smithy formatting --- modules/openapi/test/resources/foo.smithy | 32 +++++++++++------------ 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/modules/openapi/test/resources/foo.smithy b/modules/openapi/test/resources/foo.smithy index bfe2022..0752145 100644 --- a/modules/openapi/test/resources/foo.smithy +++ b/modules/openapi/test/resources/foo.smithy @@ -39,26 +39,26 @@ structure NotFound { @examples([ { - title: "ONE" - input: { - in: "test input" - } - output: { - out: "test output" - } + title: "ONE" + input: { + in: "test input" + } + output: { + out: "test output" + } } { - title: "TWO" - input: { - in: "test input two" - } - error: { - shapeId: NotFound - content: { - message: "Not found message" - } + title: "TWO" + input: { + in: "test input two" + } + error: { + shapeId: NotFound + content: { + message: "Not found message" } } + } ]) @http(method: "POST", uri: "/test_errors") operation TestErrorsInExamples {