From fc9397450b40a1c637fd75e6c31ec47f7c788d16 Mon Sep 17 00:00:00 2001 From: Jeff Lewis Date: Tue, 9 Apr 2024 11:16:03 -0600 Subject: [PATCH 1/4] handle 204 NoContent response component --- .../src/internals/OpenApiToIModel.scala | 9 + modules/openapi/tests/src/OperationSpec.scala | 1795 +++++++++-------- 2 files changed, 928 insertions(+), 876 deletions(-) diff --git a/modules/openapi/src/internals/OpenApiToIModel.scala b/modules/openapi/src/internals/OpenApiToIModel.scala index 96f6e50..476e050 100644 --- a/modules/openapi/src/internals/OpenApiToIModel.scala +++ b/modules/openapi/src/internals/OpenApiToIModel.scala @@ -252,6 +252,15 @@ private[openapi] class OpenApiToIModel[F[_]: Parallel: TellShape: TellError]( val code = output.code code >= 200 && code < 300 }.toList match { + case output :: Nil if output.code == 204 => + F.pure( + Some( + (204 -> DefId( + Namespace(List("smithy", "api")), + Name.stdLib("Unit") + )) + ) + ) case output :: Nil => val code = output.code recordRefOrMessage(output.refOrMessage, None) diff --git a/modules/openapi/tests/src/OperationSpec.scala b/modules/openapi/tests/src/OperationSpec.scala index eb5aa85..cde6ed5 100644 --- a/modules/openapi/tests/src/OperationSpec.scala +++ b/modules/openapi/tests/src/OperationSpec.scala @@ -19,649 +19,585 @@ final class OperationSpec extends munit.FunSuite { test("operation - response") { val openapiString = """|openapi: '3.0.' - |info: - | title: test - | version: '1.0' - |paths: - | /test: - | get: - | operationId: testOperationId - | responses: - | '200': - | content: - | application/json: - | schema: - | $ref: '#/components/schemas/Object' - |components: - | schemas: - | Object: - | type: object - | properties: - | s: - | type: string - | required: - | - s - |""".stripMargin + |info: + | title: test + | version: '1.0' + |paths: + | /test: + | get: + | operationId: testOperationId + | responses: + | '200': + | content: + | application/json: + | schema: + | $ref: '#/components/schemas/Object' + |components: + | schemas: + | Object: + | type: object + | properties: + | s: + | type: string + | required: + | - s + |""".stripMargin val expectedString = """|namespace foo - | - |use smithytranslate#contentType - | - |service FooService { - | operations: [ - | TestOperationId - | ] - |} - | - |@http( - | method: "GET", - | uri: "/test", - | code: 200, - |) - |operation TestOperationId { - | input: Unit, - | output: TestOperationId200, - |} - | - |structure Object { - | @required - | s: String, - |} - | - |structure TestOperationId200 { - | @httpPayload - | @required - | @contentType("application/json") - | body: Object, - |} - |""".stripMargin + | + |use smithytranslate#contentType + | + |service FooService { + | operations: [ + | TestOperationId + | ] + |} + | + |@http( + | method: "GET", + | uri: "/test", + | code: 200, + |) + |operation TestOperationId { + | input: Unit, + | output: TestOperationId200, + |} + | + |structure Object { + | @required + | s: String, + |} + | + |structure TestOperationId200 { + | @httpPayload + | @required + | @contentType("application/json") + | body: Object, + |} + |""".stripMargin TestUtils.runConversionTest(openapiString, expectedString) } test("operation - externalDocs") { val openapiString = """|openapi: '3.0.' - |info: - | title: test - | version: '1.0' - |externalDocs: - | description: Example - | url: https://www.example.com - |paths: - | /test: - | get: - | operationId: testOperationId - | externalDocs: - | description: Example 2 - | url: https://www.example.com/2 - | responses: - | '200': - | content: - | application/json: - | schema: - | $ref: '#/components/schemas/Object' - |components: - | schemas: - | Object: - | type: object - | properties: - | s: - | type: string - | required: - | - s - |""".stripMargin + |info: + | title: test + | version: '1.0' + |externalDocs: + | description: Example + | url: https://www.example.com + |paths: + | /test: + | get: + | operationId: testOperationId + | externalDocs: + | description: Example 2 + | url: https://www.example.com/2 + | responses: + | '200': + | content: + | application/json: + | schema: + | $ref: '#/components/schemas/Object' + |components: + | schemas: + | Object: + | type: object + | properties: + | s: + | type: string + | required: + | - s + |""".stripMargin val expectedString = """|namespace foo - | - |use smithytranslate#contentType - | - |@externalDocumentation( - | "Example": "https://www.example.com" - |) - |service FooService { - | operations: [ - | TestOperationId - | ] - |} - | - |@http( - | method: "GET", - | uri: "/test", - | code: 200, - |) - |@externalDocumentation( - | "Example 2": "https://www.example.com/2" - |) - |operation TestOperationId { - | input: Unit, - | output: TestOperationId200, - |} - | - |structure Object { - | @required - | s: String, - |} - | - |structure TestOperationId200 { - | @httpPayload - | @required - | @contentType("application/json") - | body: Object, - |} - |""".stripMargin + | + |use smithytranslate#contentType + | + |@externalDocumentation( + | "Example": "https://www.example.com" + |) + |service FooService { + | operations: [ + | TestOperationId + | ] + |} + | + |@http( + | method: "GET", + | uri: "/test", + | code: 200, + |) + |@externalDocumentation( + | "Example 2": "https://www.example.com/2" + |) + |operation TestOperationId { + | input: Unit, + | output: TestOperationId200, + |} + | + |structure Object { + | @required + | s: String, + |} + | + |structure TestOperationId200 { + | @httpPayload + | @required + | @contentType("application/json") + | body: Object, + |} + |""".stripMargin TestUtils.runConversionTest(openapiString, expectedString) } test("operation - request and response") { - val openapiString = """|openapi: '3.0.' - |info: - | title: test - | version: '1.0' - |paths: - | /test: - | post: - | operationId: testOperationId - | requestBody: - | required: true - | content: - | application/json: - | schema: - | $ref: '#/components/schemas/ObjectIn' - | responses: - | '200': - | content: - | application/json: - | schema: - | $ref: '#/components/schemas/ObjectOut' - |components: - | schemas: - | ObjectIn: - | type: object - | properties: - | s: - | type: string - | required: - | - s - | ObjectOut: - | type: object - | properties: - | sNum: - | type: integer - |""".stripMargin + val openapiString = + """|openapi: '3.0.' + |info: + | title: test + | version: '1.0' + |paths: + | /test: + | post: + | operationId: testOperationId + | requestBody: + | required: true + | content: + | application/json: + | schema: + | $ref: '#/components/schemas/ObjectIn' + | responses: + | '200': + | content: + | application/json: + | schema: + | $ref: '#/components/schemas/ObjectOut' + |components: + | schemas: + | ObjectIn: + | type: object + | properties: + | s: + | type: string + | required: + | - s + | ObjectOut: + | type: object + | properties: + | sNum: + | type: integer + |""".stripMargin val expectedString = """|namespace foo - | - |use smithytranslate#contentType - | - |service FooService { - | operations: [ - | TestOperationId - | ] - |} - | - |@http( - | method: "POST", - | uri: "/test", - | code: 200, - |) - |operation TestOperationId { - | input: TestOperationIdInput, - | output: TestOperationId200, - |} - | - |structure ObjectIn { - | @required - | s: String, - |} - | - |structure ObjectOut { - | sNum: Integer, - |} - | - |structure TestOperationId200 { - | @httpPayload - | @required - | @contentType("application/json") - | body: ObjectOut, - |} - | - |structure TestOperationIdInput { - | @httpPayload - | @required - | @contentType("application/json") - | body: ObjectIn, - |} - |""".stripMargin + | + |use smithytranslate#contentType + | + |service FooService { + | operations: [ + | TestOperationId + | ] + |} + | + |@http( + | method: "POST", + | uri: "/test", + | code: 200, + |) + |operation TestOperationId { + | input: TestOperationIdInput, + | output: TestOperationId200, + |} + | + |structure ObjectIn { + | @required + | s: String, + |} + | + |structure ObjectOut { + | sNum: Integer, + |} + | + |structure TestOperationId200 { + | @httpPayload + | @required + | @contentType("application/json") + | body: ObjectOut, + |} + | + |structure TestOperationIdInput { + | @httpPayload + | @required + | @contentType("application/json") + | body: ObjectIn, + |} + |""".stripMargin TestUtils.runConversionTest(openapiString, expectedString) } test("operation - request and response embedded schemas") { val openapiString = """|openapi: '3.0.' - |info: - | title: test - | version: '1.0' - |paths: - | /test: - | post: - | operationId: testOperationId - | requestBody: - | required: true - | content: - | application/json: - | schema: - | type: object - | properties: - | s: - | type: string - | required: - | - s - | responses: - | '200': - | content: - | application/json: - | schema: - | type: object - | properties: - | sNum: - | type: integer - |""".stripMargin + |info: + | title: test + | version: '1.0' + |paths: + | /test: + | post: + | operationId: testOperationId + | requestBody: + | required: true + | content: + | application/json: + | schema: + | type: object + | properties: + | s: + | type: string + | required: + | - s + | responses: + | '200': + | content: + | application/json: + | schema: + | type: object + | properties: + | sNum: + | type: integer + |""".stripMargin val expectedString = """|namespace foo - | - |use smithytranslate#contentType - | - |service FooService { - | operations: [ - | TestOperationId - | ] - |} - | - |@http( - | method: "POST", - | uri: "/test", - | code: 200, - |) - |operation TestOperationId { - | input: TestOperationIdInput, - | output: TestOperationId200 - |} - | - |structure TestOperationId200 { - | @httpPayload - | @required - | @contentType("application/json") - | body: TestOperationId200Body, - |} - | - |structure TestOperationId200Body { - | sNum: Integer, - |} - | - |structure TestOperationIdInput { - | @httpPayload - | @required - | @contentType("application/json") - | body: TestOperationIdInputBody, - |} - | - |structure TestOperationIdInputBody { - | @required - | s: String - |} - |""".stripMargin + | + |use smithytranslate#contentType + | + |service FooService { + | operations: [ + | TestOperationId + | ] + |} + | + |@http( + | method: "POST", + | uri: "/test", + | code: 200, + |) + |operation TestOperationId { + | input: TestOperationIdInput, + | output: TestOperationId200 + |} + | + |structure TestOperationId200 { + | @httpPayload + | @required + | @contentType("application/json") + | body: TestOperationId200Body, + |} + | + |structure TestOperationId200Body { + | sNum: Integer, + |} + | + |structure TestOperationIdInput { + | @httpPayload + | @required + | @contentType("application/json") + | body: TestOperationIdInputBody, + |} + | + |structure TestOperationIdInputBody { + | @required + | s: String + |} + |""".stripMargin TestUtils.runConversionTest(openapiString, expectedString) } test("operation - path parameter") { val openapiString = """|openapi: '3.0.' - |info: - | title: test - | version: '1.0' - |paths: - | /test/{userId}: - | get: - | operationId: testOperationId - | parameters: - | - in: path - | name: userId - | schema: - | type: integer - | required: true - | responses: - | '200': - | content: - | application/json: - | schema: - | $ref: '#/components/schemas/Object' - |components: - | schemas: - | Object: - | type: object - | properties: - | s: - | type: string - | required: - | - s - |""".stripMargin + |info: + | title: test + | version: '1.0' + |paths: + | /test/{userId}: + | get: + | operationId: testOperationId + | parameters: + | - in: path + | name: userId + | schema: + | type: integer + | required: true + | responses: + | '200': + | content: + | application/json: + | schema: + | $ref: '#/components/schemas/Object' + |components: + | schemas: + | Object: + | type: object + | properties: + | s: + | type: string + | required: + | - s + |""".stripMargin val expectedString = """|namespace foo - | - |use smithytranslate#contentType - | - |service FooService { - | operations: [ - | TestOperationId - | ] - |} - | - |@http( - | method: "GET", - | uri: "/test/{userId}", - | code: 200, - |) - |operation TestOperationId { - | input: TestOperationIdInput, - | output: TestOperationId200, - |} - | - |structure TestOperationIdInput { - | @httpLabel - | @required - | userId: Integer, - |} - | - |structure Object { - | @required - | s: String, - |} - | - |structure TestOperationId200 { - | @httpPayload - | @required - | @contentType("application/json") - | body: Object, - |} - |""".stripMargin + | + |use smithytranslate#contentType + | + |service FooService { + | operations: [ + | TestOperationId + | ] + |} + | + |@http( + | method: "GET", + | uri: "/test/{userId}", + | code: 200, + |) + |operation TestOperationId { + | input: TestOperationIdInput, + | output: TestOperationId200, + |} + | + |structure TestOperationIdInput { + | @httpLabel + | @required + | userId: Integer, + |} + | + |structure Object { + | @required + | s: String, + |} + | + |structure TestOperationId200 { + | @httpPayload + | @required + | @contentType("application/json") + | body: Object, + |} + |""".stripMargin TestUtils.runConversionTest(openapiString, expectedString) } test("operation - query") { val openapiString = """|openapi: '3.0.' - |info: - | title: test - | version: '1.0' - |paths: - | /test: - | get: - | operationId: testOperationId - | parameters: - | - in: query - | name: userId - | schema: - | type: integer - | - in: query - | name: some_id - | schema: - | type: integer - | - in: query - | name: other-id - | schema: - | type: integer - | - in: query - | name: 12-twelve - | schema: - | type: integer - | responses: - | '200': - | content: - | application/json: - | schema: - | $ref: '#/components/schemas/Object' - |components: - | schemas: - | Object: - | type: object - | properties: - | s: - | type: string - | required: - | - s - |""".stripMargin + |info: + | title: test + | version: '1.0' + |paths: + | /test: + | get: + | operationId: testOperationId + | parameters: + | - in: query + | name: userId + | schema: + | type: integer + | - in: query + | name: some_id + | schema: + | type: integer + | - in: query + | name: other-id + | schema: + | type: integer + | - in: query + | name: 12-twelve + | schema: + | type: integer + | responses: + | '200': + | content: + | application/json: + | schema: + | $ref: '#/components/schemas/Object' + |components: + | schemas: + | Object: + | type: object + | properties: + | s: + | type: string + | required: + | - s + |""".stripMargin val expectedString = """|namespace foo - | - |use smithytranslate#contentType - | - |service FooService { - | operations: [ - | TestOperationId - | ] - |} - | - |@http( - | method: "GET", - | uri: "/test", - | code: 200, - |) - |operation TestOperationId { - | input: TestOperationIdInput, - | output: TestOperationId200, - |} - | - |structure TestOperationIdInput { - | @httpQuery("userId") - | userId: Integer, - | @httpQuery("some_id") - | some_id: Integer, - | @httpQuery("other-id") - | other_id: Integer - | @httpQuery("12-twelve") - | n12_twelve: Integer - |} - | - |structure Object { - | @required - | s: String, - |} - | - |structure TestOperationId200 { - | @httpPayload - | @required - | @contentType("application/json") - | body: Object, - |} - |""".stripMargin + | + |use smithytranslate#contentType + | + |service FooService { + | operations: [ + | TestOperationId + | ] + |} + | + |@http( + | method: "GET", + | uri: "/test", + | code: 200, + |) + |operation TestOperationId { + | input: TestOperationIdInput, + | output: TestOperationId200, + |} + | + |structure TestOperationIdInput { + | @httpQuery("userId") + | userId: Integer, + | @httpQuery("some_id") + | some_id: Integer, + | @httpQuery("other-id") + | other_id: Integer + | @httpQuery("12-twelve") + | n12_twelve: Integer + |} + | + |structure Object { + | @required + | s: String, + |} + | + |structure TestOperationId200 { + | @httpPayload + | @required + | @contentType("application/json") + | body: Object, + |} + |""".stripMargin TestUtils.runConversionTest(openapiString, expectedString) } test("operation - response reference") { val openapiString = """|openapi: '3.0.' - |info: - | title: test - | version: '1.0' - |paths: - | /test: - | get: - | operationId: testOperationId - | responses: - | '200': - | $ref: '#/components/responses/okay' - |components: - | responses: - | okay: - | content: - | application/json: - | schema: - | type: object - | properties: - | s: - | type: string - | required: - | - s - |""".stripMargin + |info: + | title: test + | version: '1.0' + |paths: + | /test: + | get: + | operationId: testOperationId + | responses: + | '200': + | $ref: '#/components/responses/okay' + |components: + | responses: + | okay: + | content: + | application/json: + | schema: + | type: object + | properties: + | s: + | type: string + | required: + | - s + |""".stripMargin val expectedString = """|namespace foo - | - |use smithytranslate#contentType - | - |service FooService { - | operations: [ - | TestOperationId - | ] - |} - | - |@http( - | method: "GET", - | uri: "/test", - | code: 200, - |) - |operation TestOperationId { - | input: Unit, - | output: Okay, - |} - | - |structure Okay { - | @httpPayload - | @required - | @contentType("application/json") - | body: Body, - |} - | - |structure Body { - | @required - | s: String, - |} - |""".stripMargin + | + |use smithytranslate#contentType + | + |service FooService { + | operations: [ + | TestOperationId + | ] + |} + | + |@http( + | method: "GET", + | uri: "/test", + | code: 200, + |) + |operation TestOperationId { + | input: Unit, + | output: Okay, + |} + | + |structure Okay { + | @httpPayload + | @required + | @contentType("application/json") + | body: Body, + |} + | + |structure Body { + | @required + | s: String, + |} + |""".stripMargin TestUtils.runConversionTest(openapiString, expectedString) } - test("operation - request reference") { - val openapiString = """|openapi: '3.0.' - |info: - | title: test - | version: '1.0' - |paths: - | /test: - | post: - | operationId: testOperationId - | requestBody: - | $ref: '#/components/requestBodies/generic' - | responses: - | '200': - | $ref: '#/components/responses/okay' - |components: - | requestBodies: - | generic: - | required: true - | content: - | application/json: - | schema: - | type: object - | properties: - | s: - | type: string - | required: - | - s - | responses: - | okay: - | content: - | application/json: - | schema: - | type: object - | properties: - | sNum: - | type: integer - |""".stripMargin + test("operation - response reference 204") { + val openapiString = """|openapi: 3.0.0 + |info: + | title: Sample API + |paths: + | /users: + | post: + | operationId: testOperationId + | responses: + | '204': + | $ref: '#/components/responses/NoContent' + |components: + | responses: + | NoContent: + | description: no content + |""".stripMargin val expectedString = """|namespace foo - | - |use smithytranslate#contentType - | - |service FooService { - | operations: [ - | TestOperationId - | ] - |} - | - |@http( - | method: "POST", - | uri: "/test", - | code: 200, - |) - |operation TestOperationId { - | input: TestOperationIdInput, - | output: Okay, - |} - | - |structure Generic { - | @required - | s: String - |} - | - |structure Okay { - | @httpPayload - | @required - | @contentType("application/json") - | body: Body, - |} - | - |structure Body { - | sNum: Integer, - |} - | - |structure TestOperationIdInput { - | @httpPayload - | @required - | @contentType("application/json") - | body: Generic - |} - |""".stripMargin + | + |service FooService { + | operations: [ + | TestOperationId + | ] + |} + | + |@http( + | method: "POST", + | uri: "/users", + | code: 204, + |) + |operation TestOperationId { + | input: Unit, + | output: Unit, + |} + |""".stripMargin TestUtils.runConversionTest(openapiString, expectedString) } - test("operation - request and operation have the same name") { + test("operation - request reference") { val openapiString = """|openapi: '3.0.' |info: | title: test | version: '1.0' |paths: - | /test2: - | post: - | operationId: testOperation2 - | requestBody: - | $ref: '#/components/requestBodies/TestOperation2' - | responses: - | '200': - | $ref: '#/components/responses/okay' | /test: | post: - | operationId: testOperation + | operationId: testOperationId | requestBody: - | $ref: '#/components/requestBodies/testOperation' + | $ref: '#/components/requestBodies/generic' | responses: | '200': | $ref: '#/components/responses/okay' |components: | requestBodies: - | TestOperation2: - | required: true - | content: - | application/json: - | schema: - | type: object - | properties: - | s: - | type: string - | required: - | - s - | testOperation: + | generic: | required: true | content: | application/json: @@ -683,6 +619,111 @@ final class OperationSpec extends munit.FunSuite { | type: integer |""".stripMargin + val expectedString = """|namespace foo + | + |use smithytranslate#contentType + | + |service FooService { + | operations: [ + | TestOperationId + | ] + |} + | + |@http( + | method: "POST", + | uri: "/test", + | code: 200, + |) + |operation TestOperationId { + | input: TestOperationIdInput, + | output: Okay, + |} + | + |structure Generic { + | @required + | s: String + |} + | + |structure Okay { + | @httpPayload + | @required + | @contentType("application/json") + | body: Body, + |} + | + |structure Body { + | sNum: Integer, + |} + | + |structure TestOperationIdInput { + | @httpPayload + | @required + | @contentType("application/json") + | body: Generic + |} + |""".stripMargin + + TestUtils.runConversionTest(openapiString, expectedString) + } + + test("operation - request and operation have the same name") { + val openapiString = + """|openapi: '3.0.' + |info: + | title: test + | version: '1.0' + |paths: + | /test2: + | post: + | operationId: testOperation2 + | requestBody: + | $ref: '#/components/requestBodies/TestOperation2' + | responses: + | '200': + | $ref: '#/components/responses/okay' + | /test: + | post: + | operationId: testOperation + | requestBody: + | $ref: '#/components/requestBodies/testOperation' + | responses: + | '200': + | $ref: '#/components/responses/okay' + |components: + | requestBodies: + | TestOperation2: + | required: true + | content: + | application/json: + | schema: + | type: object + | properties: + | s: + | type: string + | required: + | - s + | testOperation: + | required: true + | content: + | application/json: + | schema: + | type: object + | properties: + | s: + | type: string + | required: + | - s + | responses: + | okay: + | content: + | application/json: + | schema: + | type: object + | properties: + | sNum: + | type: integer + |""".stripMargin + val expectedString = """|namespace foo | |use smithytranslate#contentType @@ -753,366 +794,368 @@ final class OperationSpec extends munit.FunSuite { } test("operation - uses of a restricted header add suppression") { + val openapiString = + """|openapi: '3.0.' + |info: + | title: test + | version: '1.0' + |paths: + | /test: + | post: + | operationId: testOperation + | parameters: + | - in: header + | name: X-Request-Id + | schema: + | type: string + | - in: header + | name: X-Forwarded-For + | schema: + | type: string + | requestBody: + | $ref: '#/components/requestBodies/testOperation' + | responses: + | '200': + | content: {} + |components: + | requestBodies: + | testOperation: + | required: true + | content: + | application/json: + | schema: + | type: object + | properties: + | s: + | type: string + | required: + | - s + |""".stripMargin + + val expectedString = + """|metadata suppressions = [ + | { + | id: "HttpHeaderTrait", + | namespace: "foo", + | reason: "Restricted headers are in use. See https://awslabs.github.io/smithy/1.0/spec/core/http-traits.html#restricted-http-headers." + | } + |] + | + |namespace foo + | + |use smithytranslate#contentType + | + |service FooService { + | operations: [ + | TestOperation, + | ], + |} + | + |@http( + | method: "POST", + | uri: "/test", + | code: 200, + |) + |operation TestOperation { + | input: TestOperationInput, + | output: Unit, + |} + | + |structure ComponentsRequestBodiesTestOperation { + | @required + | s: String, + |} + | + |structure TestOperationInput { + | @httpHeader("X-Request-Id") + | X_Request_Id: String, + | @httpHeader("X-Forwarded-For") + | X_Forwarded_For: String, + | @httpPayload + | @required + | @contentType("application/json") + | body: ComponentsRequestBodiesTestOperation, + |}""".stripMargin + TestUtils.runConversionTest(openapiString, expectedString) + } + + test("operation - description") { val openapiString = """|openapi: '3.0.' |info: | title: test | version: '1.0' |paths: | /test: - | post: - | operationId: testOperation - | parameters: - | - in: header - | name: X-Request-Id - | schema: - | type: string - | - in: header - | name: X-Forwarded-For - | schema: - | type: string - | requestBody: - | $ref: '#/components/requestBodies/testOperation' + | get: + | operationId: testOperationId + | description: Testing test | responses: | '200': - | content: {} + | content: + | application/json: + | schema: + | $ref: '#/components/schemas/Object' |components: - | requestBodies: - | testOperation: - | required: true - | content: - | application/json: - | schema: - | type: object - | properties: - | s: - | type: string - | required: - | - s + | schemas: + | Object: + | type: object + | properties: + | s: + | type: string + | required: + | - s |""".stripMargin - val expectedString = """|metadata suppressions = [ - | { - | id: "HttpHeaderTrait", - | namespace: "foo", - | reason: "Restricted headers are in use. See https://awslabs.github.io/smithy/1.0/spec/core/http-traits.html#restricted-http-headers." - | } - |] - | - |namespace foo + val expectedString = """|namespace foo | |use smithytranslate#contentType | |service FooService { | operations: [ - | TestOperation, - | ], + | TestOperationId + | ] |} | |@http( - | method: "POST", + | method: "GET", | uri: "/test", | code: 200, |) - |operation TestOperation { - | input: TestOperationInput, - | output: Unit, + |@documentation("Testing test") + |operation TestOperationId { + | input: Unit, + | output: TestOperationId200, |} | - |structure ComponentsRequestBodiesTestOperation { + |structure Object { | @required | s: String, |} | - |structure TestOperationInput { - | @httpHeader("X-Request-Id") - | X_Request_Id: String, - | @httpHeader("X-Forwarded-For") - | X_Forwarded_For: String, + |structure TestOperationId200 { | @httpPayload | @required | @contentType("application/json") - | body: ComponentsRequestBodiesTestOperation, - |}""".stripMargin - TestUtils.runConversionTest(openapiString, expectedString) - } - - test("operation - description") { - val openapiString = """|openapi: '3.0.' - |info: - | title: test - | version: '1.0' - |paths: - | /test: - | get: - | operationId: testOperationId - | description: Testing test - | responses: - | '200': - | content: - | application/json: - | schema: - | $ref: '#/components/schemas/Object' - |components: - | schemas: - | Object: - | type: object - | properties: - | s: - | type: string - | required: - | - s - |""".stripMargin - - val expectedString = """|namespace foo - | - |use smithytranslate#contentType - | - |service FooService { - | operations: [ - | TestOperationId - | ] - |} - | - |@http( - | method: "GET", - | uri: "/test", - | code: 200, - |) - |@documentation("Testing test") - |operation TestOperationId { - | input: Unit, - | output: TestOperationId200, - |} - | - |structure Object { - | @required - | s: String, - |} - | - |structure TestOperationId200 { - | @httpPayload - | @required - | @contentType("application/json") - | body: Object, - |} - |""".stripMargin + | body: Object, + |} + |""".stripMargin TestUtils.runConversionTest(openapiString, expectedString) } test("operation - simplify repeated namespace") { val openapiString = """|openapi: '3.0.' - |info: - | title: test - | version: '1.0' - |paths: - | /test/test: - | get: - | responses: - | '200': - | content: - | application/json: - | schema: - | $ref: '#/components/schemas/Object' - |components: - | schemas: - | Object: - | type: object - | properties: - | s: - | type: string - | required: - | - s - |""".stripMargin + |info: + | title: test + | version: '1.0' + |paths: + | /test/test: + | get: + | responses: + | '200': + | content: + | application/json: + | schema: + | $ref: '#/components/schemas/Object' + |components: + | schemas: + | Object: + | type: object + | properties: + | s: + | type: string + | required: + | - s + |""".stripMargin val expectedString = """|namespace foo - | - |use smithytranslate#contentType - | - |service FooService { - | operations: [ - | TestGET - | ] - |} - | - |@http( - | method: "GET", - | uri: "/test/test", - | code: 200, - |) - |operation TestGET { - | input: Unit, - | output: TestGET200, - |} - | - |structure Object { - | @required - | s: String, - |} - | - |structure TestGET200 { - | @httpPayload - | @required - | @contentType("application/json") - | body: Object, - |} - |""".stripMargin + | + |use smithytranslate#contentType + | + |service FooService { + | operations: [ + | TestGET + | ] + |} + | + |@http( + | method: "GET", + | uri: "/test/test", + | code: 200, + |) + |operation TestGET { + | input: Unit, + | output: TestGET200, + |} + | + |structure Object { + | @required + | s: String, + |} + | + |structure TestGET200 { + | @httpPayload + | @required + | @contentType("application/json") + | body: Object, + |} + |""".stripMargin TestUtils.runConversionTest(openapiString, expectedString) } test("operation - do not simplify repeated namespace") { val openapiString = """|openapi: '3.0.' - |info: - | title: test - | version: '1.0' - |paths: - | /test/test: - | get: - | responses: - | '200': - | content: - | application/json: - | schema: - | $ref: '#/components/schemas/Object' - | /test: - | get: - | responses: - | '200': - | content: - | application/json: - | schema: - | $ref: '#/components/schemas/Object' - |components: - | schemas: - | Object: - | type: object - | properties: - | s: - | type: string - | required: - | - s - |""".stripMargin + |info: + | title: test + | version: '1.0' + |paths: + | /test/test: + | get: + | responses: + | '200': + | content: + | application/json: + | schema: + | $ref: '#/components/schemas/Object' + | /test: + | get: + | responses: + | '200': + | content: + | application/json: + | schema: + | $ref: '#/components/schemas/Object' + |components: + | schemas: + | Object: + | type: object + | properties: + | s: + | type: string + | required: + | - s + |""".stripMargin val expectedString = """|namespace foo - | - |use smithytranslate#contentType - | - |service FooService { - | operations: [ - | TestGET, - | TestTestGET - | ] - |} - | - |@http( - | method: "GET", - | uri: "/test", - | code: 200, - |) - |operation TestGET { - | input: Unit, - | output: TestGET200, - |} - | - |@http( - | method: "GET", - | uri: "/test/test", - | code: 200, - |) - |operation TestTestGET { - | input: Unit, - | output: TestTestGET200, - |} - | - |structure Object { - | @required - | s: String, - |} - | - |structure TestGET200 { - | @httpPayload - | @required - | @contentType("application/json") - | body: Object, - |} - | - |structure TestTestGET200 { - | @httpPayload - | @required - | @contentType("application/json") - | body: Object, - |} - |""".stripMargin + | + |use smithytranslate#contentType + | + |service FooService { + | operations: [ + | TestGET, + | TestTestGET + | ] + |} + | + |@http( + | method: "GET", + | uri: "/test", + | code: 200, + |) + |operation TestGET { + | input: Unit, + | output: TestGET200, + |} + | + |@http( + | method: "GET", + | uri: "/test/test", + | code: 200, + |) + |operation TestTestGET { + | input: Unit, + | output: TestTestGET200, + |} + | + |structure Object { + | @required + | s: String, + |} + | + |structure TestGET200 { + | @httpPayload + | @required + | @contentType("application/json") + | body: Object, + |} + | + |structure TestTestGET200 { + | @httpPayload + | @required + | @contentType("application/json") + | body: Object, + |} + |""".stripMargin TestUtils.runConversionTest(openapiString, expectedString) } test("operation - simplify repeated namespace embedded schema") { val openapiString = """|openapi: '3.0.' - |info: - | title: test - | version: '1.0' - |paths: - | /test/{test}: - | get: - | parameters: - | - in: path - | name: test - | schema: - | type: string - | required: true - | responses: - | '200': - | content: - | application/json: - | schema: - | type: object - | properties: - | s: - | type: string - | required: - | - s - |""".stripMargin + |info: + | title: test + | version: '1.0' + |paths: + | /test/{test}: + | get: + | parameters: + | - in: path + | name: test + | schema: + | type: string + | required: true + | responses: + | '200': + | content: + | application/json: + | schema: + | type: object + | properties: + | s: + | type: string + | required: + | - s + |""".stripMargin val expectedString = """|namespace foo - | - |use smithytranslate#contentType - | - |service FooService { - | operations: [ - | TestGET - | ] - |} - | - |@http( - | method: "GET", - | uri: "/test/{test}", - | code: 200, - |) - |operation TestGET { - | input: TestGETInput, - | output: TestGET200, - |} - | - |structure TestGETInput { - | @httpLabel - | @required - | test: String - |} - | - |structure Body { - | @required - | s: String, - |} - | - |structure TestGET200 { - | @httpPayload - | @required - | @contentType("application/json") - | body: Body, - |} - |""".stripMargin + | + |use smithytranslate#contentType + | + |service FooService { + | operations: [ + | TestGET + | ] + |} + | + |@http( + | method: "GET", + | uri: "/test/{test}", + | code: 200, + |) + |operation TestGET { + | input: TestGETInput, + | output: TestGET200, + |} + | + |structure TestGETInput { + | @httpLabel + | @required + | test: String + |} + | + |structure Body { + | @required + | s: String, + |} + | + |structure TestGET200 { + | @httpPayload + | @required + | @contentType("application/json") + | body: Body, + |} + |""".stripMargin TestUtils.runConversionTest(openapiString, expectedString) } From 523bfcec3b4d7d309ec07ce80c3ae4a7c2373219 Mon Sep 17 00:00:00 2001 From: Jeff Lewis Date: Thu, 11 Apr 2024 16:01:17 -0600 Subject: [PATCH 2/4] use postprocess to handle unit responses --- build.sc | 1 + buildDeps.sc | 1 + .../src/internals/IModelPostProcessor.scala | 3 +- .../EmptyStructureToUnitTransformer.scala | 73 +++++++++ .../src/internals/OpenApiToIModel.scala | 12 +- modules/openapi/tests/src/ModelWrapper.scala | 155 ++++++++++++++++++ modules/openapi/tests/src/TestUtils.scala | 81 --------- 7 files changed, 233 insertions(+), 93 deletions(-) create mode 100644 modules/compiler-core/src/internals/postprocess/EmptyStructureToUnitTransformer.scala create mode 100644 modules/openapi/tests/src/ModelWrapper.scala diff --git a/build.sc b/build.sc index 68b96fb..cdd7e8d 100644 --- a/build.sc +++ b/build.sc @@ -80,6 +80,7 @@ trait OpenApiModule object tests extends this.ScalaTests with BaseMunitTests { def ivyDeps = super.ivyDeps() ++ Agg( buildDeps.smithy.build, + buildDeps.smithy.diff, buildDeps.scalaJavaCompat ) } diff --git a/buildDeps.sc b/buildDeps.sc index 60dab9c..230b05a 100644 --- a/buildDeps.sc +++ b/buildDeps.sc @@ -28,6 +28,7 @@ object smithy { val smithyVersion = "1.41.1" val model = ivy"software.amazon.smithy:smithy-model:$smithyVersion" val build = ivy"software.amazon.smithy:smithy-build:$smithyVersion" + val diff = ivy"software.amazon.smithy:smithy-diff:$smithyVersion" } object cats { val mtl = ivy"org.typelevel::cats-mtl:1.4.0" diff --git a/modules/compiler-core/src/internals/IModelPostProcessor.scala b/modules/compiler-core/src/internals/IModelPostProcessor.scala index 5ea8d2a..4a095d8 100644 --- a/modules/compiler-core/src/internals/IModelPostProcessor.scala +++ b/modules/compiler-core/src/internals/IModelPostProcessor.scala @@ -30,7 +30,8 @@ private[compiler] object IModelPostProcessor { RequirementShiftTransformer, ContentTypeShiftTransformer, ReorientDefaultValueTransformer, - DropRequiredWhenDefaultValue + DropRequiredWhenDefaultValue, + EmptyStructureToUnitTransformer ) private[this] def transform( diff --git a/modules/compiler-core/src/internals/postprocess/EmptyStructureToUnitTransformer.scala b/modules/compiler-core/src/internals/postprocess/EmptyStructureToUnitTransformer.scala new file mode 100644 index 0000000..6e43879 --- /dev/null +++ b/modules/compiler-core/src/internals/postprocess/EmptyStructureToUnitTransformer.scala @@ -0,0 +1,73 @@ +/* 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 smithytranslate.compiler.internals +package postprocess + +import org.typelevel.ci._ + +// Removes empty structures and changes shapes which target +// them to instead target Unit +private[compiler] object EmptyStructureToUnitTransformer + extends IModelPostProcessor { + + def apply(model: IModel): IModel = { + val defs = model.definitions.map(d => d.id -> d).toMap + + val structuresToRemove = model.definitions.flatMap { + case s: Structure if isEmptyStructure(defs, s) => + Some(s.id) + case _ => None + }.toSet + val amendedDefs = model.definitions.flatMap { + case s: Structure if structuresToRemove(s.id) => None + case op: OperationDef => + val changeInput: OperationDef => OperationDef = o => + if (o.input.exists(structuresToRemove)) o.copy(input = Some(unit)) + else o + val changeOutput: OperationDef => OperationDef = o => + if (o.output.exists(structuresToRemove)) o.copy(output = Some(unit)) + else o + Some(changeInput.andThen(changeOutput)(op)) + case other => Some(other) + } + IModel(amendedDefs, model.suppressions) + } + + private val unit = + DefId( + Namespace(List("smithy", "api")), + Name(Segment.StandardLib(ci"Unit")) + ) + + // consider empty if has no fields OR if has one field with Body hint (httpPayload) + private def isEmptyStructure( + defs: Map[DefId, Definition], + d: Definition + ): Boolean = + d match { + case s: Structure => + (s.localFields.isEmpty && s.parents.isEmpty && s.hints.isEmpty) || { + def isHttpPayload = + s.localFields.length == 1 && s.localFields.head.hints + .contains(Hint.Body) + def isHttpPayloadEmpty = defs + .get(s.localFields.head.tpe) + .exists(isEmptyStructure(defs, _)) + isHttpPayload && isHttpPayloadEmpty + } + case _ => false + } +} diff --git a/modules/openapi/src/internals/OpenApiToIModel.scala b/modules/openapi/src/internals/OpenApiToIModel.scala index 476e050..521a2b1 100644 --- a/modules/openapi/src/internals/OpenApiToIModel.scala +++ b/modules/openapi/src/internals/OpenApiToIModel.scala @@ -252,15 +252,6 @@ private[openapi] class OpenApiToIModel[F[_]: Parallel: TellShape: TellError]( val code = output.code code >= 200 && code < 300 }.toList match { - case output :: Nil if output.code == 204 => - F.pure( - Some( - (204 -> DefId( - Namespace(List("smithy", "api")), - Name.stdLib("Unit") - )) - ) - ) case output :: Nil => val code = output.code recordRefOrMessage(output.refOrMessage, None) @@ -340,8 +331,7 @@ private[openapi] class OpenApiToIModel[F[_]: Parallel: TellShape: TellError]( } } .map { fields => - if (fields.isEmpty) None - else Structure(defId, fields, Vector.empty, message.hints).some + Structure(defId, fields, Vector.empty, message.hints).some } .flatMap(_.traverse(recordDef).map(_.as(defId))) } diff --git a/modules/openapi/tests/src/ModelWrapper.scala b/modules/openapi/tests/src/ModelWrapper.scala new file mode 100644 index 0000000..676a504 --- /dev/null +++ b/modules/openapi/tests/src/ModelWrapper.scala @@ -0,0 +1,155 @@ +/* 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 smithytranslate.compiler.openapi + +import software.amazon.smithy.model._ +import software.amazon.smithy.model.node._ +import scala.jdk.CollectionConverters._ +import software.amazon.smithy.build.transforms.FilterSuppressions +import software.amazon.smithy.build.TransformContext +import software.amazon.smithy.model.shapes.SmithyIdlModelSerializer +import software.amazon.smithy.model.transform.ModelTransformer +import software.amazon.smithy.model.traits.Trait +import software.amazon.smithy.model.shapes.Shape +import software.amazon.smithy.model.traits.BoxTrait +import software.amazon.smithy.diff.ModelDiff +import java.util.stream.Collectors + +// In order to have nice comparisons from test reports. +class ModelWrapper(val model: Model) { + + override def equals(obj: Any): Boolean = obj match { + case wrapper: ModelWrapper => + val one = reorderMetadata(reorderFields(model)) + val two = reorderMetadata(reorderFields(wrapper.model)) + val diff = ModelDiff + .builder() + .oldModel(one) + .newModel(two) + .compare() + .getDifferences() + val added = diff.addedShapes().toList + val hasChanges = + diff + .changedShapes() + .toList + .asScala + .exists { changed => + val addedTraits = + changed.addedTraits().toList.asScala + val removedTraits = changed + .removedTraits() + .toList + .asScala + val changedTraits = changed + .changedTraits() + .toList + .asScala + .filterNot { pair => + // compare shapeId and node values to avoid issues with differing java classes + pair.getLeft.toShapeId == pair.getRight.toShapeId && pair.getLeft.toNode == pair.getRight.toNode + } + .filterNot { pair => + // don't consider synthetic traits + pair.getLeft().toShapeId().getNamespace() == "smithy.synthetic" + } + addedTraits.nonEmpty || removedTraits.nonEmpty || changedTraits.nonEmpty + } + val removed = + diff.removedShapes().toList.asScala + added.isEmpty && !hasChanges && removed.isEmpty + case _ => false + } + + private def reorderMetadata(model: Model): Model = { + implicit val nodeOrd: Ordering[Node] = (x: Node, y: Node) => + x.hashCode() - y.hashCode() + + implicit val nodeStringOrd: Ordering[StringNode] = { + val ord = Ordering[String] + (x: StringNode, y: StringNode) => ord.compare(x.getValue(), y.getValue()) + } + def goNode(n: Node): Node = n match { + case array: ArrayNode => + val elements = array.getElements().asScala.toList.sorted + Node.arrayNode(elements: _*) + case obj: ObjectNode => + Node.objectNode( + obj.getMembers().asScala.toSeq.sortBy(_._1).toMap.asJava + ) + case other => other + } + def go(metadata: Map[String, Node]): Map[String, Node] = { + val keys = metadata.keySet.toVector.sorted + keys.map { k => + k -> goNode(metadata(k)) + }.toMap + } + + val builder = model.toBuilder() + val newMeta = go(model.getMetadata().asScala.toMap) + builder.clearMetadata() + builder.metadata(newMeta.asJava) + builder.build() + } + + private val reorderFields: Model => Model = m => { + val structures = m.getStructureShapes().asScala.map { structShape => + val sortedMembers = + structShape.members().asScala.toList.sortBy(_.getMemberName()) + structShape.toBuilder().members(sortedMembers.asJava).build() + } + m.toBuilder().addShapes(structures.asJava).build() + } + + private def update(model: Model): Model = { + val filterSuppressions: Model => Model = m => + new FilterSuppressions().transform( + TransformContext + .builder() + .model(m) + .settings( + ObjectNode.builder().withMember("removeUnused", true).build() + ) + .build() + ) + (filterSuppressions andThen reorderFields)(model) + } + + override def toString() = + SmithyIdlModelSerializer + .builder() + .build() + .serialize(update(model)) + .asScala + .map(in => s"${in._1.toString.toUpperCase}:\n\n${in._2}") + .mkString("\n") +} + +object ModelWrapper { + def apply(model: Model): ModelWrapper = { + // Remove all box traits because they are applied inconsistently depending on if you + // load from Java model or from unparsed string model + @annotation.nowarn("msg=class BoxTrait in package traits is deprecated") + val noBoxModel = ModelTransformer + .create() + .filterTraits( + model, + ((_: Shape, trt: Trait) => trt.toShapeId() != BoxTrait.ID) + ) + new ModelWrapper(noBoxModel) + } +} diff --git a/modules/openapi/tests/src/TestUtils.scala b/modules/openapi/tests/src/TestUtils.scala index 9095271..e1c67a5 100644 --- a/modules/openapi/tests/src/TestUtils.scala +++ b/modules/openapi/tests/src/TestUtils.scala @@ -191,85 +191,4 @@ object TestUtils { ) ) } - - // In order to have nice comparisons from munit reports. - class ModelWrapper(val model: Model) { - override def equals(obj: Any): Boolean = obj match { - case wrapper: ModelWrapper => - reorderMetadata(model) == reorderMetadata(wrapper.model) - case _ => false - } - - private def reorderMetadata(model: Model): Model = { - implicit val nodeOrd: Ordering[Node] = (x: Node, y: Node) => - x.hashCode() - y.hashCode() - - implicit val nodeStringOrd: Ordering[StringNode] = { - val ord = Ordering[String] - (x: StringNode, y: StringNode) => - ord.compare(x.getValue(), y.getValue()) - } - def goNode(n: Node): Node = n match { - case array: ArrayNode => - val elements = array.getElements().asScala.toList.sorted - Node.arrayNode(elements: _*) - case obj: ObjectNode => - Node.objectNode( - obj.getMembers().asScala.toSeq.sortBy(_._1).toMap.asJava - ) - case other => other - } - def go(metadata: Map[String, Node]): Map[String, Node] = { - val keys = metadata.keySet.toVector.sorted - keys.map { k => - k -> goNode(metadata(k)) - }.toMap - } - - val builder = model.toBuilder() - val newMeta = go(model.getMetadata().asScala.toMap) - builder.clearMetadata() - builder.metadata(newMeta.asJava) - builder.build() - } - - private def filter(model: Model): Model = { - val filterSuppressions: Model => Model = m => - new FilterSuppressions().transform( - TransformContext - .builder() - .model(m) - .settings( - ObjectNode.builder().withMember("removeUnused", true).build() - ) - .build() - ) - (filterSuppressions)(model) - } - - override def toString() = { - SmithyIdlModelSerializer - .builder() - .build() - .serialize(filter(model)) - .asScala - .map(in => s"${in._1.toString.toUpperCase}:\n\n${in._2}") - .mkString("\n") - } - } - - object ModelWrapper { - def apply(model: Model): ModelWrapper = { - // Remove all box traits because they are applied inconsistently depending on if you - // load from Java model or from unparsed string model - @annotation.nowarn("msg=class BoxTrait in package traits is deprecated") - val noBoxModel = ModelTransformer - .create() - .filterTraits( - model, - ((_: Shape, trt: Trait) => trt.toShapeId() != BoxTrait.ID).asJava - ) - new ModelWrapper(noBoxModel) - } - } } From 8dfa8ff46fb8c296aca104f3208497f7f60f340b Mon Sep 17 00:00:00 2001 From: Jeff Lewis Date: Thu, 11 Apr 2024 16:16:00 -0600 Subject: [PATCH 3/4] fix compile errors --- modules/openapi/tests/src/ModelWrapper.scala | 21 +++++++++++--------- modules/openapi/tests/src/TestUtils.scala | 10 ---------- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/modules/openapi/tests/src/ModelWrapper.scala b/modules/openapi/tests/src/ModelWrapper.scala index 676a504..fa76673 100644 --- a/modules/openapi/tests/src/ModelWrapper.scala +++ b/modules/openapi/tests/src/ModelWrapper.scala @@ -31,6 +31,12 @@ import java.util.stream.Collectors // In order to have nice comparisons from test reports. class ModelWrapper(val model: Model) { + private final implicit class JavaStreamOps[A]( + stream: java.util.stream.Stream[A] + ) { + def asList: List[A] = stream.collect(Collectors.toList()).asScala.toList + } + override def equals(obj: Any): Boolean = obj match { case wrapper: ModelWrapper => val one = reorderMetadata(reorderFields(model)) @@ -41,23 +47,20 @@ class ModelWrapper(val model: Model) { .newModel(two) .compare() .getDifferences() - val added = diff.addedShapes().toList + val added = diff.addedShapes().asList val hasChanges = diff .changedShapes() - .toList - .asScala + .asList .exists { changed => val addedTraits = - changed.addedTraits().toList.asScala + changed.addedTraits().asList val removedTraits = changed .removedTraits() - .toList - .asScala + .asList val changedTraits = changed .changedTraits() - .toList - .asScala + .asList .filterNot { pair => // compare shapeId and node values to avoid issues with differing java classes pair.getLeft.toShapeId == pair.getRight.toShapeId && pair.getLeft.toNode == pair.getRight.toNode @@ -69,7 +72,7 @@ class ModelWrapper(val model: Model) { addedTraits.nonEmpty || removedTraits.nonEmpty || changedTraits.nonEmpty } val removed = - diff.removedShapes().toList.asScala + diff.removedShapes().asList added.isEmpty && !hasChanges && removed.isEmpty case _ => false } diff --git a/modules/openapi/tests/src/TestUtils.scala b/modules/openapi/tests/src/TestUtils.scala index e1c67a5..feb17b7 100644 --- a/modules/openapi/tests/src/TestUtils.scala +++ b/modules/openapi/tests/src/TestUtils.scala @@ -16,20 +16,10 @@ package smithytranslate.compiler.openapi import software.amazon.smithy.model.Model -import software.amazon.smithy.model.shapes.SmithyIdlModelSerializer -import scala.jdk.CollectionConverters._ import munit.Assertions -import software.amazon.smithy.build.transforms.FilterSuppressions -import software.amazon.smithy.build.TransformContext import cats.syntax.all._ import munit.Location import cats.data.NonEmptyList -import software.amazon.smithy.model.node._ -import software.amazon.smithy.model.transform.ModelTransformer -import software.amazon.smithy.model.shapes.Shape -import software.amazon.smithy.model.traits.Trait -import software.amazon.smithy.model.traits.BoxTrait -import scala.compat.java8.FunctionConverters._ import smithytranslate.compiler.SmithyVersion import smithytranslate.compiler.ToSmithyResult import smithytranslate.compiler.ToSmithyCompilerOptions From e1763821ea4890a947004b54d5583be9cbf819d1 Mon Sep 17 00:00:00 2001 From: Jeff Lewis Date: Fri, 12 Apr 2024 12:43:11 -0600 Subject: [PATCH 4/4] rewrite to use val instead of def --- .../postprocess/EmptyStructureToUnitTransformer.scala | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/modules/compiler-core/src/internals/postprocess/EmptyStructureToUnitTransformer.scala b/modules/compiler-core/src/internals/postprocess/EmptyStructureToUnitTransformer.scala index 6e43879..e74334f 100644 --- a/modules/compiler-core/src/internals/postprocess/EmptyStructureToUnitTransformer.scala +++ b/modules/compiler-core/src/internals/postprocess/EmptyStructureToUnitTransformer.scala @@ -60,11 +60,14 @@ private[compiler] object EmptyStructureToUnitTransformer d match { case s: Structure => (s.localFields.isEmpty && s.parents.isEmpty && s.hints.isEmpty) || { - def isHttpPayload = + val isHttpPayload = s.localFields.length == 1 && s.localFields.head.hints .contains(Hint.Body) - def isHttpPayloadEmpty = defs - .get(s.localFields.head.tpe) + val isHttpPayloadEmpty = s.localFields.headOption + .flatMap(f => + defs + .get(f.tpe) + ) .exists(isEmptyStructure(defs, _)) isHttpPayload && isHttpPayloadEmpty }