From 0ce4187cb12de8259310cfeaa15b87ffd7fd6a1b Mon Sep 17 00:00:00 2001 From: Thomas Farr Date: Thu, 17 Aug 2023 07:11:20 +1200 Subject: [PATCH] Support serializing traits into specification extensions in OpenAPI (#1609) ## `smithy.openapi#specificationExtension` Adds a meta trait `smithy.openapi#specificationExtension` that can be used to annotate traits to indicate they should be serialized into specification extension (`x-*`) properties when converting to OpenAPI. This is supported on shapes, operations, and services. By default the extension will be named by the shape ID replacing `#` & `.` with `-` prefixed with `x-`, otherwise the extension can be specified using the `as` property. A new package `smithy-openapi-traits` is introduced to contain the `smithy.openapi#specificationExtension` trait. ## `JsonSchemaMapper` and `JsonSchemaShapeVisitor` BREAKING CHANGE: Technically, `JsonSchemaMapper` has a breaking change from a functional interface to a normal interface, but we are anticipating customers are not using `JsonSchemaMapper` as a functional interface since it was not annotated with `@FunctionalInterface`. `JsonSchemaMapper` is updated to use `updateSchema(JsonSchemaMapperContext, Schema.Builder)` in `JsonSchemaShapeVisitor`, which will call the existing `updateSchema(Shape, Schema.Builder, JsonSchemaConfig)` by default when not implemented for backwards compatibility. ## `smithy-openapi` Support is added for `smithy.openapi#specificationExtension` by implementing `SpecificationExtensionsMapper` for operations and services and updating `OpenApiJsonSchemaMapper` for shapes. --------- Co-authored-by: Steven Yuan --- .../guides/converting-to-openapi.rst | 152 ++++++++++++++++++ settings.gradle | 1 + .../smithy/jsonschema/JsonSchemaMapper.java | 30 +++- .../jsonschema/JsonSchemaMapperContext.java | 55 +++++++ .../jsonschema/JsonSchemaShapeVisitor.java | 3 +- .../jsonschema/JsonSchemaConverterTest.java | 25 ++- smithy-openapi-traits/build.gradle | 15 ++ .../traits/SpecificationExtensionTrait.java | 96 +++++++++++ ...re.amazon.smithy.model.traits.TraitService | 1 + .../main/resources/META-INF/smithy/manifest | 1 + .../META-INF/smithy/smithy.openapi.smithy | 21 +++ smithy-openapi/build.gradle | 1 + .../amazon/smithy/openapi/OpenApiUtils.java | 58 +++++++ .../openapi/fromsmithy/CoreExtension.java | 4 +- .../openapi/fromsmithy/OpenApiConverter.java | 2 +- .../fromsmithy/OpenApiJsonSchemaMapper.java | 9 +- .../SpecificationExtensionsMapper.java | 45 ++++++ .../amazon/smithy/openapi/model/OpenApi.java | 2 +- .../OpenApiJsonSchemaMapperTest.java | 34 +++- .../SpecificationExtensionsMapperTest.java | 55 +++++++ .../inlined-type-target.openapi.json | 73 +++++++++ .../inlined-type-target.smithy | 45 ++++++ .../operation-target.openapi.json | 49 ++++++ .../operation-target.smithy | 37 +++++ .../service-target.openapi.json | 49 ++++++ .../service-target.smithy | 37 +++++ .../specification-extension-traits.smithy | 100 ++++++++++++ .../structure-target.openapi.json | 73 +++++++++ .../structure-target.smithy | 43 +++++ 29 files changed, 1107 insertions(+), 9 deletions(-) create mode 100644 smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaMapperContext.java create mode 100644 smithy-openapi-traits/build.gradle create mode 100644 smithy-openapi-traits/src/main/java/software/amazon/smithy/openapi/traits/SpecificationExtensionTrait.java create mode 100644 smithy-openapi-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService create mode 100644 smithy-openapi-traits/src/main/resources/META-INF/smithy/manifest create mode 100644 smithy-openapi-traits/src/main/resources/META-INF/smithy/smithy.openapi.smithy create mode 100644 smithy-openapi/src/main/java/software/amazon/smithy/openapi/OpenApiUtils.java create mode 100644 smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/mappers/SpecificationExtensionsMapper.java create mode 100644 smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/mappers/SpecificationExtensionsMapperTest.java create mode 100644 smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/mappers/specificationextensions/inlined-type-target.openapi.json create mode 100644 smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/mappers/specificationextensions/inlined-type-target.smithy create mode 100644 smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/mappers/specificationextensions/operation-target.openapi.json create mode 100644 smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/mappers/specificationextensions/operation-target.smithy create mode 100644 smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/mappers/specificationextensions/service-target.openapi.json create mode 100644 smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/mappers/specificationextensions/service-target.smithy create mode 100644 smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/mappers/specificationextensions/specification-extension-traits.smithy create mode 100644 smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/mappers/specificationextensions/structure-target.openapi.json create mode 100644 smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/mappers/specificationextensions/structure-target.smithy diff --git a/docs/source-2.0/guides/converting-to-openapi.rst b/docs/source-2.0/guides/converting-to-openapi.rst index 2095436f324..49448b23a19 100644 --- a/docs/source-2.0/guides/converting-to-openapi.rst +++ b/docs/source-2.0/guides/converting-to-openapi.rst @@ -1151,6 +1151,157 @@ The following is an example OpenAPI model for the above Smithy example value. } } +------------------------- +OpenAPI conversion traits +------------------------- + +The ``software.amazon.smithy:smithy-openapi-traits`` package defines traits used to augment the conversion +of a Smithy model into an OpenAPI specification. + +The following example shows how to add it to your Gradle build alongside the ``smithy-openapi`` plugin: + +.. code-block:: kotlin + :caption: build.gradle.kts + + plugins { + java + id("software.amazon.smithy").version("0.6.0") + } + + buildscript { + dependencies { + classpath("software.amazon.smithy:smithy-openapi:__smithy_version__") + classpath("software.amazon.smithy:smithy-openapi-traits:__smithy_version__") + } + } + + dependencies { + implementation("software.amazon.smithy:smithy-openapi-traits:__smithy_version__") + } + +Refer to `Converting to OpenAPI with smithy-build`_ for more detailed information about using the plugin and Gradle. + +.. smithy-trait:: smithy.openapi#specificationExtension +.. _specification-extension-trait: + +``specificationExtension`` trait +================================ + +Summary + Indicates a trait shape should be converted into an `OpenAPI specification extension`_. + Any custom trait that has been annotated with this trait will be serialized into the OpenAPI specification using + its :ref:`Smithy JSON AST representation `. +Trait selector + ``[trait|trait]`` +Value type + ``structure`` + +The ``specificationExtension`` trait is a structure that supports the following members: + +.. list-table:: + :header-rows: 1 + :widths: 10 25 65 + + * - Property + - Type + - Description + * - as + - ``string`` + - Explicitly name the specification extension. + If set, it must begin with ``"x-"``. + Otherwise, it defaults to the target trait's shape ID normalized with hyphens and prepended with ``"x-"``. + +The following example defines a specification extension representing a custom metadata structure using the ``specificationExtension`` trait: + +.. code-block:: smithy + + $version: "2" + namespace smithy.example + + use smithy.openapi#specificationExtension + + @trait + @specificationExtension(as: "x-meta") + structure metadata { + owner: String + } + + @output + @metadata(owner: "greetings-team-b") + structure GreetResponse { + greeting: String + } + + @readonly + @http(method: "GET", uri: "/greet") + @metadata(owner: "greetings-team-a") + operation Greet { + output: GreetResponse + } + +This results in an ``x-meta`` property being added to the respective objects in the OpenAPI output: + +.. code-block:: json + + { + "...": "...", + "paths": { + "/greet": { + "get": { + "operationId": "Greet", + "responses": { + "200": { + "description": "Greet 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GreetResponseContent" + } + } + } + } + }, + "x-meta": { + "owner": "greetings-team-a" + } + } + } + }, + "components": { + "schemas": { + "GreetResponseContent": { + "type": "object", + "properties": { + "greeting": { + "type": "string" + } + }, + "x-meta": { + "owner": "greetings-team-b" + } + } + } + } + } + +.. rubric:: Supported trait locations: + +Only a subset of OpenAPI locations are supported in the conversion: + +.. list-table:: + :header-rows: 1 + :widths: 50 50 + + * - Smithy Location + - OpenAPI Location + * - Service shape + - `Root OpenAPI schema `_ + * - Operation shape + - `Operation object `_ + * - Simple & Aggregate shapes + - `Schema object `_ + +Unsupported use cases can likely be covered by the :ref:`jsonAdd ` feature of the ``smithy-openapi`` plugin. ----------------------------- Amazon API Gateway extensions @@ -1754,3 +1905,4 @@ The conversion process is highly extensible through .. _x-amazon-apigateway-authorizer: https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-swagger-extensions-authorizer.html .. _Lambda authorizers: https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-swagger-extensions-authorizer.html .. _API Gateway's API key usage plans: https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-api-usage-plans.html +.. _OpenAPI specification extension: https://spec.openapis.org/oas/v3.1.0#specification-extensions diff --git a/settings.gradle b/settings.gradle index f127d43b230..3ce6a7f338a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -21,6 +21,7 @@ include ":smithy-linters" include ":smithy-mqtt-traits" include ":smithy-jsonschema" include ":smithy-openapi" +include ":smithy-openapi-traits" include ":smithy-utils" include ":smithy-protocol-test-traits" include ':smithy-jmespath' diff --git a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaMapper.java b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaMapper.java index fbcd186b68e..a1ff07bcd8c 100644 --- a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaMapper.java +++ b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaMapper.java @@ -19,6 +19,11 @@ /** * Updates a schema builder before converting a shape to a schema. + * + * {@link JsonSchemaMapper#updateSchema(JsonSchemaMapperContext, Schema.Builder)} is the entry point during JSON Schema + * conversion, and is the recommended method to implement. If this method is implemented, + * {@link JsonSchemaMapper#updateSchema(Shape, Schema.Builder, JsonSchemaConfig)} will NOT be called unless written in + * the implementation. */ public interface JsonSchemaMapper { /** @@ -36,12 +41,33 @@ default byte getOrder() { } /** - * Updates a schema builder. + * Updates a schema builder using information in {@link JsonSchemaMapperContext}. + * + * If not implemented, will default to + * {@link JsonSchemaMapper#updateSchema(Shape, Schema.Builder, JsonSchemaConfig)} for backwards-compatibility. + * + * @param context Context with information needed to update the schema. + * @param schemaBuilder Schema builder to update. + * @return Returns an updated schema builder. + */ + default Schema.Builder updateSchema(JsonSchemaMapperContext context, Schema.Builder schemaBuilder) { + return updateSchema(context.getShape(), schemaBuilder, context.getConfig()); + } + + /** + * Updates a schema builder, and is not recommended. Use + * {@link JsonSchemaMapper#updateSchema(JsonSchemaMapperContext, Schema.Builder)} instead. + * + * If not implemented, this method will default to a no-op. + * + * This method is not deprecated for backwards-compatibility. * * @param shape Shape used for the conversion. * @param schemaBuilder Schema builder to update. * @param config JSON Schema config. * @return Returns an updated schema builder. */ - Schema.Builder updateSchema(Shape shape, Schema.Builder schemaBuilder, JsonSchemaConfig config); + default Schema.Builder updateSchema(Shape shape, Schema.Builder schemaBuilder, JsonSchemaConfig config) { + return schemaBuilder; + } } diff --git a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaMapperContext.java b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaMapperContext.java new file mode 100644 index 00000000000..49f47e4ef31 --- /dev/null +++ b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaMapperContext.java @@ -0,0 +1,55 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.jsonschema; + +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.Shape; + +/** + * Context for a JSON schema mapping. + */ +public class JsonSchemaMapperContext { + private final Model model; + private final Shape shape; + private final JsonSchemaConfig config; + + JsonSchemaMapperContext( + Model model, + Shape shape, + JsonSchemaConfig config + ) { + this.model = model; + this.shape = shape; + this.config = config; + } + + /** + * Gets the Smithy model being converted. + * + * @return Returns the Smithy model. + */ + public Model getModel() { + return model; + } + + /** + * Gets the Smithy shape being mapped. + * + * @return Returns the Smithy shape. + */ + public Shape getShape() { + return shape; + } + + /** + * Gets the JSON schema configuration object. + * + * @return Returns the JSON schema config object. + */ + public JsonSchemaConfig getConfig() { + return config; + } +} diff --git a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaShapeVisitor.java b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaShapeVisitor.java index 477bf152643..46550dfff93 100644 --- a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaShapeVisitor.java +++ b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaShapeVisitor.java @@ -344,8 +344,9 @@ private Schema.Builder updateBuilder(Shape shape, Schema.Builder builder) { * @return Returns the built schema. */ private Schema buildSchema(Shape shape, Schema.Builder builder) { + JsonSchemaMapperContext context = new JsonSchemaMapperContext(model, shape, converter.getConfig()); for (JsonSchemaMapper mapper : mappers) { - mapper.updateSchema(shape, builder, converter.getConfig()); + builder = mapper.updateSchema(context, builder); } return builder.build(); diff --git a/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/JsonSchemaConverterTest.java b/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/JsonSchemaConverterTest.java index f7bea5ec398..c65c8b9b353 100644 --- a/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/JsonSchemaConverterTest.java +++ b/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/JsonSchemaConverterTest.java @@ -211,7 +211,30 @@ public void canUseCustomPropertyNamingStrategy() { public void canAddCustomSchemaMapper() { Shape struct = StructureShape.builder().id("smithy.example#Foo").build(); Model model = Model.builder().addShape(struct).build(); - JsonSchemaMapper mapper = (shape, builder, conf) -> builder.putExtension("Hi", Node.from("There")); + class CustomMapper implements JsonSchemaMapper { + @Override + public Schema.Builder updateSchema(Shape shape, Schema.Builder builder, JsonSchemaConfig conf) { + return builder.putExtension("Hi", Node.from("There")); + } + } + JsonSchemaMapper mapper = new CustomMapper(); + SchemaDocument doc = JsonSchemaConverter.builder().addMapper(mapper).model(model).build().convert(); + + assertTrue(doc.getDefinition("#/definitions/Foo").isPresent()); + assertTrue(doc.getDefinition("#/definitions/Foo").get().getExtension("Hi").isPresent()); + } + + @Test + public void canAddCustomSchemaMapperContextMethod() { + Shape struct = StructureShape.builder().id("smithy.example#Foo").build(); + Model model = Model.builder().addShape(struct).build(); + class CustomMapper implements JsonSchemaMapper { + @Override + public Schema.Builder updateSchema(JsonSchemaMapperContext context, Schema.Builder builder) { + return builder.putExtension("Hi", Node.from("There")); + } + } + JsonSchemaMapper mapper = new CustomMapper(); SchemaDocument doc = JsonSchemaConverter.builder().addMapper(mapper).model(model).build().convert(); assertTrue(doc.getDefinition("#/definitions/Foo").isPresent()); diff --git a/smithy-openapi-traits/build.gradle b/smithy-openapi-traits/build.gradle new file mode 100644 index 00000000000..b671893fd13 --- /dev/null +++ b/smithy-openapi-traits/build.gradle @@ -0,0 +1,15 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +description = "This module provides Smithy traits that are used in converting a Smithy model to OpenAPI." + +ext { + displayName = "Smithy :: OpenAPI Traits" + moduleName = "software.amazon.smithy.openapi.traits" +} + +dependencies { + api project(":smithy-model") +} diff --git a/smithy-openapi-traits/src/main/java/software/amazon/smithy/openapi/traits/SpecificationExtensionTrait.java b/smithy-openapi-traits/src/main/java/software/amazon/smithy/openapi/traits/SpecificationExtensionTrait.java new file mode 100644 index 00000000000..1d472dbad9e --- /dev/null +++ b/smithy-openapi-traits/src/main/java/software/amazon/smithy/openapi/traits/SpecificationExtensionTrait.java @@ -0,0 +1,96 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.openapi.traits; + +import java.util.Optional; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.NodeMapper; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.AbstractTrait; +import software.amazon.smithy.model.traits.AbstractTraitBuilder; +import software.amazon.smithy.model.traits.Trait; +import software.amazon.smithy.utils.ToSmithyBuilder; + +/** + * smithy.openapi#specificationExtension - Indicates a trait shape should be converted into an OpenAPI specification extension. + */ +public final class SpecificationExtensionTrait extends AbstractTrait + implements ToSmithyBuilder { + public static final ShapeId ID = ShapeId.from("smithy.openapi#specificationExtension"); + + private final String as; + + private SpecificationExtensionTrait(Builder builder) { + super(ID, builder.getSourceLocation()); + this.as = builder.as; + } + + public static final class Provider extends AbstractTrait.Provider { + public Provider() { + super(ID); + } + + @Override + public Trait createTrait(ShapeId target, Node value) { + SpecificationExtensionTrait trait = new NodeMapper().deserialize(value, SpecificationExtensionTrait.class); + trait.setNodeCache(value); + return trait; + } + } + + /** + * Gets the specification extension header value in "as". + * + * @return Returns the optionally present "as". + */ + public Optional getAs() { + return Optional.ofNullable(as); + } + + public static Builder builder() { + return new Builder(); + } + + @Override + protected Node createNode() { + NodeMapper mapper = new NodeMapper(); + mapper.disableToNodeForClass(SpecificationExtensionTrait.class); + mapper.setOmitEmptyValues(true); + return mapper.serialize(this).expectObjectNode(); + } + + @Override + public Builder toBuilder() { + return builder() + .sourceLocation(getSourceLocation()) + .as(this.as); + } + + /** + * Builds a {@link SpecificationExtensionTrait} trait. + */ + public static final class Builder extends AbstractTraitBuilder { + private String as; + + private Builder() {} + + @Override + public SpecificationExtensionTrait build() { + return new SpecificationExtensionTrait(this); + } + + /** + * Set the explicit name for the target specification extension. + * + * @param as Explicit name for the target specification extension, or null. + * @return This builder instance. + */ + public Builder as(String as) { + this.as = as; + return this; + } + } +} diff --git a/smithy-openapi-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService b/smithy-openapi-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService new file mode 100644 index 00000000000..82fbaa1fe9f --- /dev/null +++ b/smithy-openapi-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService @@ -0,0 +1 @@ +software.amazon.smithy.openapi.traits.SpecificationExtensionTrait$Provider diff --git a/smithy-openapi-traits/src/main/resources/META-INF/smithy/manifest b/smithy-openapi-traits/src/main/resources/META-INF/smithy/manifest new file mode 100644 index 00000000000..7357478d92e --- /dev/null +++ b/smithy-openapi-traits/src/main/resources/META-INF/smithy/manifest @@ -0,0 +1 @@ +smithy.openapi.smithy diff --git a/smithy-openapi-traits/src/main/resources/META-INF/smithy/smithy.openapi.smithy b/smithy-openapi-traits/src/main/resources/META-INF/smithy/smithy.openapi.smithy new file mode 100644 index 00000000000..0af3d334bd0 --- /dev/null +++ b/smithy-openapi-traits/src/main/resources/META-INF/smithy/smithy.openapi.smithy @@ -0,0 +1,21 @@ +$version: "2" + +namespace smithy.openapi + +/// Indicates a trait shape should be converted into an [OpenAPI specification extension](https://spec.openapis.org/oas/v3.1.0#specification-extensions). +@trait( + selector: "[trait|trait]", + breakingChanges: [ + {change: "presence"}, + {path: "/as", change: "any"} + ] +) +structure specificationExtension { + /// Explicitly name the specification extension. + /// If set must begin with `x-`, otherwise defaults to the target trait shape's ID normalized with hyphens and prepended with `x-`. + as: SpecificationExtensionKey +} + +@private +@pattern("^x-.+$") +string SpecificationExtensionKey diff --git a/smithy-openapi/build.gradle b/smithy-openapi/build.gradle index 4f5d6be28da..5cc5ba831dc 100644 --- a/smithy-openapi/build.gradle +++ b/smithy-openapi/build.gradle @@ -25,4 +25,5 @@ dependencies { api project(":smithy-build") api project(":smithy-jsonschema") api project(":smithy-aws-traits") + api project(":smithy-openapi-traits") } diff --git a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/OpenApiUtils.java b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/OpenApiUtils.java new file mode 100644 index 00000000000..fb91434541d --- /dev/null +++ b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/OpenApiUtils.java @@ -0,0 +1,58 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.openapi; + +import java.util.LinkedHashMap; +import java.util.Map; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.openapi.traits.SpecificationExtensionTrait; +import software.amazon.smithy.utils.SmithyInternalApi; + +@SmithyInternalApi +public final class OpenApiUtils { + private OpenApiUtils() {} + + /** + * Gets the specification extension name for a given meta trait. + * + * Either an explicitly configured extension name in specificationExtensionTrait, or a normalization of the shape + * ID. The normalization replaces all "." and "#" in a shapeId to "-". + * + * @param metaTraitId Trait shape to get the extension name. + * @return Extension name for the given trait shape. + */ + public static String getSpecificationExtensionName( + ShapeId metaTraitId, + SpecificationExtensionTrait specificationExtensionTrait + ) { + return specificationExtensionTrait.getAs() + .orElse("x-" + metaTraitId.toString().replaceAll("[.#]", "-")); + } + + /** + * Return specification extensions attached to a given shape. + * + * @param shape Shape to get extensions for. + * @param model Model the shape belongs to. + * @return map of specification extension names to node values + */ + public static Map getSpecificationExtensionsMap(Model model, Shape shape) { + Map specificationExtensions = new LinkedHashMap(); + shape.getAllTraits().forEach((traitId, trait) -> + // Get Applied Trait + model.getShape(traitId) + // Get SpecificationExtensionTrait on the Applied Trait + .flatMap(traitShape -> traitShape.getTrait(SpecificationExtensionTrait.class)) + // Get specification extension name from the Applied Trait and SpecificationExtensionTrait + .map(specificationExtension -> getSpecificationExtensionName(traitId, specificationExtension)) + // Put the specification extension name and Applied Meta trait into the map. + .ifPresent(name -> specificationExtensions.put(name, trait.toNode()))); + return specificationExtensions; + } +} diff --git a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/CoreExtension.java b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/CoreExtension.java index a3ddc8a2830..4907b2b8be4 100644 --- a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/CoreExtension.java +++ b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/CoreExtension.java @@ -24,6 +24,7 @@ import software.amazon.smithy.openapi.fromsmithy.mappers.OpenApiJsonSubstitutions; import software.amazon.smithy.openapi.fromsmithy.mappers.RemoveEmptyComponents; import software.amazon.smithy.openapi.fromsmithy.mappers.RemoveUnusedComponents; +import software.amazon.smithy.openapi.fromsmithy.mappers.SpecificationExtensionsMapper; import software.amazon.smithy.openapi.fromsmithy.mappers.UnsupportedTraits; import software.amazon.smithy.openapi.fromsmithy.protocols.AwsRestJson1Protocol; import software.amazon.smithy.openapi.fromsmithy.security.AwsV4Converter; @@ -62,7 +63,8 @@ public List getOpenApiMappers() { new OpenApiJsonAdd(), new RemoveUnusedComponents(), new UnsupportedTraits(), - new RemoveEmptyComponents() + new RemoveEmptyComponents(), + new SpecificationExtensionsMapper() ); } diff --git a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/OpenApiConverter.java b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/OpenApiConverter.java index 4424a0480db..fa7855f681a 100644 --- a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/OpenApiConverter.java +++ b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/OpenApiConverter.java @@ -330,7 +330,7 @@ private OpenApi convertWithEnvironment(ConversionEnvironment builder.putExtension(entry.getKey(), entry.getValue())); + + JsonSchemaConfig config = context.getConfig(); getResolvedExternalDocs(shape, config) .map(ExternalDocumentation::toNode) .ifPresent(docs -> builder.putExtension("externalDocs", docs)); diff --git a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/mappers/SpecificationExtensionsMapper.java b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/mappers/SpecificationExtensionsMapper.java new file mode 100644 index 00000000000..9e42255710f --- /dev/null +++ b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/mappers/SpecificationExtensionsMapper.java @@ -0,0 +1,45 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.openapi.fromsmithy.mappers; + +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.traits.Trait; +import software.amazon.smithy.openapi.OpenApiUtils; +import software.amazon.smithy.openapi.fromsmithy.Context; +import software.amazon.smithy.openapi.fromsmithy.OpenApiMapper; +import software.amazon.smithy.openapi.model.OpenApi; +import software.amazon.smithy.openapi.model.OperationObject; +import software.amazon.smithy.openapi.traits.SpecificationExtensionTrait; + +/** + * Maps trait shapes tagged with {@link SpecificationExtensionTrait} into OpenAPI specification extensions. + */ +public class SpecificationExtensionsMapper implements OpenApiMapper { + /** + * Attach Specification Extensions to Service. + */ + @Override + public OpenApi after(Context context, OpenApi openapi) { + openapi.getExtensions().putAll( + OpenApiUtils.getSpecificationExtensionsMap(context.getModel(), context.getService())); + return openapi; + } + + /** + * Attach Specification Extensions to Operation. + */ + @Override + public OperationObject updateOperation( + Context context, + OperationShape shape, + OperationObject operation, + String httpMethodName, + String path + ) { + operation.getExtensions().putAll(OpenApiUtils.getSpecificationExtensionsMap(context.getModel(), shape)); + return operation; + } +} diff --git a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/model/OpenApi.java b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/model/OpenApi.java index 1573542ca67..69d59581426 100644 --- a/smithy-openapi/src/main/java/software/amazon/smithy/openapi/model/OpenApi.java +++ b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/model/OpenApi.java @@ -101,7 +101,7 @@ public Builder toBuilder() { security.forEach(builder::addSecurity); servers.forEach(builder::addServer); tags.forEach(builder::addTag); - return builder.extensions(getExtensions()); + return builder; } @Override diff --git a/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/OpenApiJsonSchemaMapperTest.java b/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/OpenApiJsonSchemaMapperTest.java index ceeee93edfa..eb87b08b69d 100644 --- a/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/OpenApiJsonSchemaMapperTest.java +++ b/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/OpenApiJsonSchemaMapperTest.java @@ -16,7 +16,6 @@ package software.amazon.smithy.openapi.fromsmithy; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.equalTo; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -31,6 +30,7 @@ import software.amazon.smithy.model.Model; import software.amazon.smithy.model.node.Node; import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.node.StringNode; import software.amazon.smithy.model.shapes.BlobShape; import software.amazon.smithy.model.shapes.ByteShape; import software.amazon.smithy.model.shapes.DoubleShape; @@ -43,9 +43,12 @@ import software.amazon.smithy.model.shapes.StringShape; import software.amazon.smithy.model.shapes.StructureShape; import software.amazon.smithy.model.traits.DeprecatedTrait; +import software.amazon.smithy.model.traits.DynamicTrait; import software.amazon.smithy.model.traits.ExternalDocumentationTrait; import software.amazon.smithy.model.traits.SensitiveTrait; +import software.amazon.smithy.model.traits.TraitDefinition; import software.amazon.smithy.openapi.OpenApiConfig; +import software.amazon.smithy.openapi.traits.SpecificationExtensionTrait; import software.amazon.smithy.utils.ListUtils; public class OpenApiJsonSchemaMapperTest { @@ -371,4 +374,33 @@ public void supportsSensitiveTrait() { assertThat(document.getRootSchema().getFormat().get(), equalTo("password")); } + + @Test + public void supportsSpecificationExtensionTrait() { + StringShape extensionTraitShape = StringShape.builder() + .id("a.b#extensionTrait") + .addTrait(TraitDefinition.builder().build()) + .addTrait(SpecificationExtensionTrait.builder().as("x-important-metadata").build()) + .build(); + DynamicTrait extensionTraitInstance = new DynamicTrait(extensionTraitShape.getId(), StringNode.from("string content")); + IntegerShape integerShape = IntegerShape.builder().id("a.b#Integer").build(); + StructureShape structure = StructureShape.builder() + .id("a.b#Struct") + .addTrait(extensionTraitInstance) + .addMember("c", integerShape.getId()) + .build(); + + Model model = Model.builder().addShapes(extensionTraitShape, integerShape, structure).build(); + + SchemaDocument document = JsonSchemaConverter.builder() + .addMapper(new OpenApiJsonSchemaMapper()) + .model(model) + .build() + .convertShape(structure); + + assertThat( + document.getRootSchema().getExtension("x-important-metadata").get().toNode().expectStringNode().getValue(), + equalTo("string content") + ); + } } diff --git a/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/mappers/SpecificationExtensionsMapperTest.java b/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/mappers/SpecificationExtensionsMapperTest.java new file mode 100644 index 00000000000..e51a2eaed5c --- /dev/null +++ b/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/mappers/SpecificationExtensionsMapperTest.java @@ -0,0 +1,55 @@ +package software.amazon.smithy.openapi.fromsmithy.mappers; + +import java.io.InputStream; +import java.net.URL; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.openapi.OpenApiConfig; +import software.amazon.smithy.openapi.fromsmithy.OpenApiConverter; +import software.amazon.smithy.utils.IoUtils; + +public class SpecificationExtensionsMapperTest { + @ParameterizedTest + @ValueSource(strings = { + "inlined-type-target", + "structure-target", + "operation-target", + "service-target" + }) + public void checkMapping(String name) { + OpenApiConfig config = new OpenApiConfig(); + config.setService(ShapeId.from("smithy.example#Service")); + + Node actual = OpenApiConverter + .create() + .config(config) + .convertToNode(getSpecificationExtensionTraits(name)); + Node expected = getExpectedOpenAPI(name); + + Node.assertEquals(actual, expected); + } + + private static Model getSpecificationExtensionTraits(String name) { + return Model.assembler() + .addImport(getResource(name + ".smithy")) + .addImport(getResource("specification-extension-traits.smithy")) + .discoverModels() + .assemble() + .unwrap(); + } + + private static Node getExpectedOpenAPI(String name) { + return Node.parse(IoUtils.toUtf8String(getResourceAsStream(name + ".openapi.json"))); + } + + private static URL getResource(String name) { + return SpecificationExtensionsMapperTest.class.getResource("specificationextensions/" + name); + } + + private static InputStream getResourceAsStream(String name) { + return SpecificationExtensionsMapperTest.class.getResourceAsStream("specificationextensions/" + name); + } +} diff --git a/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/mappers/specificationextensions/inlined-type-target.openapi.json b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/mappers/specificationextensions/inlined-type-target.openapi.json new file mode 100644 index 00000000000..22075a3e4cd --- /dev/null +++ b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/mappers/specificationextensions/inlined-type-target.openapi.json @@ -0,0 +1,73 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "Service", + "version": "" + }, + "paths": { + "/": { + "put": { + "operationId": "Operation", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OperationRequestContent" + } + } + } + }, + "responses": { + "200": { + "description": "Operation 200 response" + } + } + } + } + }, + "components": { + "schemas": { + "OperationRequestContent": { + "type": "object", + "properties": { + "name": { + "type": "string", + "x-blob": "blob content", + "x-boolean": true, + "x-string": "string content", + "x-byte": 64, + "x-short": 16384, + "x-integer": 1073741824, + "x-long": 4611686018427387904, + "x-float": 1.07846, + "x-double": 57.64123, + "x-big-integer": 46116860184273879045678, + "x-big-decimal": 0.1234567890123456789, + "x-timestamp": "2023-02-27T13:01:57Z", + "x-document": { + "a": "b", + "c": ["d"] + }, + "x-enum": "first", + "x-int-enum": 3, + "x-list": ["a", "b", "c"], + "x-map": { + "a": 15, + "b": 18 + }, + "x-smithy-example-structureExt": { + "stringMember": "first field", + "integerMember": 17 + }, + "x-smithy-example-unionExt": { + "string": "string variant" + } + }, + "language": { + "type": "string" + } + } + } + } + } +} diff --git a/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/mappers/specificationextensions/inlined-type-target.smithy b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/mappers/specificationextensions/inlined-type-target.smithy new file mode 100644 index 00000000000..092b9fcf669 --- /dev/null +++ b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/mappers/specificationextensions/inlined-type-target.smithy @@ -0,0 +1,45 @@ +$version: "2.0" + +namespace smithy.example + +@blobExt("blob content") +@booleanExt(true) +@stringExt("string content") +@byteExt(64) +@shortExt(16384) +@integerExt(1073741824) +@longExt(4611686018427387904) +@floatExt(1.07846) +@doubleExt(57.64123) +@bigIntegerExt(46116860184273879045678) +@bigDecimalExt(0.1234567890123456789) +@timestampExt("2023-02-27T13:01:57Z") +@documentExt({ + "a": "b", + "c": ["d"] +}) +@enumExt("first") +@intEnumExt(3) +@listExt(["a", "b", "c"]) +@mapExt("a": 15, "b": 18) +@structureExt( + stringMember: "first field" + integerMember: 17 +) +@unionExt(string: "string variant") +string Name + +structure Input { + name: Name + language: String +} + +@http(method: "PUT", uri: "/") +operation Operation { + input: Input +} + +@aws.protocols#restJson1 +service Service { + operations: [Operation] +} diff --git a/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/mappers/specificationextensions/operation-target.openapi.json b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/mappers/specificationextensions/operation-target.openapi.json new file mode 100644 index 00000000000..da9d355dfb0 --- /dev/null +++ b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/mappers/specificationextensions/operation-target.openapi.json @@ -0,0 +1,49 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "Service", + "version": "" + }, + "paths": { + "/": { + "put": { + "operationId": "Operation", + "responses": { + "200": { + "description": "Operation 200 response" + } + }, + "x-blob": "blob content", + "x-boolean": true, + "x-string": "string content", + "x-byte": 64, + "x-short": 16384, + "x-integer": 1073741824, + "x-long": 4611686018427387904, + "x-float": 1.07846, + "x-double": 57.64123, + "x-big-integer": 46116860184273879045678, + "x-big-decimal": 0.1234567890123456789, + "x-timestamp": "2023-02-27T13:01:57Z", + "x-document": { + "a": "b", + "c": ["d"] + }, + "x-enum": "first", + "x-int-enum": 3, + "x-list": ["a", "b", "c"], + "x-map": { + "a": 15, + "b": 18 + }, + "x-smithy-example-structureExt": { + "stringMember": "first field", + "integerMember": 17 + }, + "x-smithy-example-unionExt": { + "string": "string variant" + } + } + } + } +} diff --git a/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/mappers/specificationextensions/operation-target.smithy b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/mappers/specificationextensions/operation-target.smithy new file mode 100644 index 00000000000..63c616baf3b --- /dev/null +++ b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/mappers/specificationextensions/operation-target.smithy @@ -0,0 +1,37 @@ +$version: "2.0" + +namespace smithy.example + +@blobExt("blob content") +@booleanExt(true) +@stringExt("string content") +@byteExt(64) +@shortExt(16384) +@integerExt(1073741824) +@longExt(4611686018427387904) +@floatExt(1.07846) +@doubleExt(57.64123) +@bigIntegerExt(46116860184273879045678) +@bigDecimalExt(0.1234567890123456789) +@timestampExt("2023-02-27T13:01:57Z") +@documentExt({ + "a": "b", + "c": ["d"] +}) +@enumExt("first") +@intEnumExt(3) +@listExt(["a", "b", "c"]) +@mapExt("a": 15, "b": 18) +@structureExt( + stringMember: "first field" + integerMember: 17 +) +@unionExt(string: "string variant") +@http(method: "PUT", uri: "/") +operation Operation { +} + +@aws.protocols#restJson1 +service Service { + operations: [Operation] +} diff --git a/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/mappers/specificationextensions/service-target.openapi.json b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/mappers/specificationextensions/service-target.openapi.json new file mode 100644 index 00000000000..7a612271971 --- /dev/null +++ b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/mappers/specificationextensions/service-target.openapi.json @@ -0,0 +1,49 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "Service", + "version": "" + }, + "paths": { + "/": { + "put": { + "operationId": "Operation", + "responses": { + "200": { + "description": "Operation 200 response" + } + } + } + } + }, + "x-blob": "blob content", + "x-boolean": true, + "x-string": "string content", + "x-byte": 64, + "x-short": 16384, + "x-integer": 1073741824, + "x-long": 4611686018427387904, + "x-float": 1.07846, + "x-double": 57.64123, + "x-big-integer": 46116860184273879045678, + "x-big-decimal": 0.1234567890123456789, + "x-timestamp": "2023-02-27T13:01:57Z", + "x-document": { + "a": "b", + "c": ["d"] + }, + "x-enum": "first", + "x-int-enum": 3, + "x-list": ["a", "b", "c"], + "x-map": { + "a": 15, + "b": 18 + }, + "x-smithy-example-structureExt": { + "stringMember": "first field", + "integerMember": 17 + }, + "x-smithy-example-unionExt": { + "string": "string variant" + } +} diff --git a/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/mappers/specificationextensions/service-target.smithy b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/mappers/specificationextensions/service-target.smithy new file mode 100644 index 00000000000..9064c6971de --- /dev/null +++ b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/mappers/specificationextensions/service-target.smithy @@ -0,0 +1,37 @@ +$version: "2.0" + +namespace smithy.example + +@http(method: "PUT", uri: "/") +operation Operation { +} + +@blobExt("blob content") +@booleanExt(true) +@stringExt("string content") +@byteExt(64) +@shortExt(16384) +@integerExt(1073741824) +@longExt(4611686018427387904) +@floatExt(1.07846) +@doubleExt(57.64123) +@bigIntegerExt(46116860184273879045678) +@bigDecimalExt(0.1234567890123456789) +@timestampExt("2023-02-27T13:01:57Z") +@documentExt({ + "a": "b", + "c": ["d"] +}) +@enumExt("first") +@intEnumExt(3) +@listExt(["a", "b", "c"]) +@mapExt("a": 15, "b": 18) +@structureExt( + stringMember: "first field" + integerMember: 17 +) +@unionExt(string: "string variant") +@aws.protocols#restJson1 +service Service { + operations: [Operation] +} diff --git a/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/mappers/specificationextensions/specification-extension-traits.smithy b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/mappers/specificationextensions/specification-extension-traits.smithy new file mode 100644 index 00000000000..2eff3fde8f7 --- /dev/null +++ b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/mappers/specificationextensions/specification-extension-traits.smithy @@ -0,0 +1,100 @@ +$version: "2.0" + +namespace smithy.example + +use smithy.openapi#specificationExtension + +@trait +@specificationExtension(as: "x-blob") +blob blobExt + +@trait +@specificationExtension(as: "x-boolean") +boolean booleanExt + +@trait +@specificationExtension(as: "x-string") +string stringExt + +@trait +@specificationExtension(as: "x-byte") +byte byteExt + +@trait +@specificationExtension(as: "x-short") +short shortExt + +@trait +@specificationExtension(as: "x-integer") +integer integerExt + +@trait +@specificationExtension(as: "x-long") +long longExt + +@trait +@specificationExtension(as: "x-float") +float floatExt + +@trait +@specificationExtension(as: "x-double") +double doubleExt + +@trait +@specificationExtension(as: "x-big-integer") +bigInteger bigIntegerExt + +@trait +@specificationExtension(as: "x-big-decimal") +bigDecimal bigDecimalExt + +@trait +@specificationExtension(as: "x-timestamp") +timestamp timestampExt + +@trait +@specificationExtension(as: "x-document") +document documentExt + +@trait +@specificationExtension(as: "x-enum") +enum enumExt { + FIRST = "first" + SECOND = "second" + THIRD = "third" +} + +@trait +@specificationExtension(as: "x-int-enum") +intEnum intEnumExt { + ONE = 1 + TWO = 2 + THREE = 3 +} + +@trait +@specificationExtension(as: "x-list") +list listExt { + member: String +} + +@trait +@specificationExtension(as: "x-map") +map mapExt { + key: String + value: Integer +} + +@trait +@specificationExtension +structure structureExt { + stringMember: String + integerMember: Integer +} + +@trait +@specificationExtension +union unionExt { + i32: Integer + string: String +} diff --git a/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/mappers/specificationextensions/structure-target.openapi.json b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/mappers/specificationextensions/structure-target.openapi.json new file mode 100644 index 00000000000..10c9fc72829 --- /dev/null +++ b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/mappers/specificationextensions/structure-target.openapi.json @@ -0,0 +1,73 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "Service", + "version": "" + }, + "paths": { + "/": { + "put": { + "operationId": "Operation", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OperationRequestContent" + } + } + } + }, + "responses": { + "200": { + "description": "Operation 200 response" + } + } + } + } + }, + "components": { + "schemas": { + "OperationRequestContent": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "language": { + "type": "string" + } + }, + "x-blob": "blob content", + "x-boolean": true, + "x-string": "string content", + "x-byte": 64, + "x-short": 16384, + "x-integer": 1073741824, + "x-long": 4611686018427387904, + "x-float": 1.07846, + "x-double": 57.64123, + "x-big-integer": 46116860184273879045678, + "x-big-decimal": 0.1234567890123456789, + "x-timestamp": "2023-02-27T13:01:57Z", + "x-document": { + "a": "b", + "c": ["d"] + }, + "x-enum": "first", + "x-int-enum": 3, + "x-list": ["a", "b", "c"], + "x-map": { + "a": 15, + "b": 18 + }, + "x-smithy-example-structureExt": { + "stringMember": "first field", + "integerMember": 17 + }, + "x-smithy-example-unionExt": { + "string": "string variant" + } + } + } + } +} diff --git a/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/mappers/specificationextensions/structure-target.smithy b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/mappers/specificationextensions/structure-target.smithy new file mode 100644 index 00000000000..9058f48249d --- /dev/null +++ b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/mappers/specificationextensions/structure-target.smithy @@ -0,0 +1,43 @@ +$version: "2.0" + +namespace smithy.example + +@blobExt("blob content") +@booleanExt(true) +@stringExt("string content") +@byteExt(64) +@shortExt(16384) +@integerExt(1073741824) +@longExt(4611686018427387904) +@floatExt(1.07846) +@doubleExt(57.64123) +@bigIntegerExt(46116860184273879045678) +@bigDecimalExt(0.1234567890123456789) +@timestampExt("2023-02-27T13:01:57Z") +@documentExt({ + "a": "b", + "c": ["d"] +}) +@enumExt("first") +@intEnumExt(3) +@listExt(["a", "b", "c"]) +@mapExt("a": 15, "b": 18) +@structureExt( + stringMember: "first field" + integerMember: 17 +) +@unionExt(string: "string variant") +structure Input { + name: String + language: String +} + +@http(method: "PUT", uri: "/") +operation Operation { + input: Input +} + +@aws.protocols#restJson1 +service Service { + operations: [Operation] +}