diff --git a/docs/source-2.0/guides/converting-to-openapi.rst b/docs/source-2.0/guides/converting-to-openapi.rst index 721fe396e3f..a7d55181a9f 100644 --- a/docs/source-2.0/guides/converting-to-openapi.rst +++ b/docs/source-2.0/guides/converting-to-openapi.rst @@ -942,6 +942,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 @@ -1506,3 +1657,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 \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 8fd75a22602..19d557619d6 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/JsonSchemaMapperContext.java b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaMapperContext.java new file mode 100644 index 00000000000..628e502a66b --- /dev/null +++ b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaMapperContext.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.jsonschema; + +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.Shape; + +/** + * Context for a JSON schema mapping. + * + * @param Type of Smithy {@link Shape} being mapped. + */ +public class JsonSchemaMapperContext { + private final Model model; + private final T shape; + private final JsonSchemaConfig config; + + JsonSchemaMapperContext( + Model model, + T 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 T 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/JsonSchemaMapperV2.java b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaMapperV2.java new file mode 100644 index 00000000000..53fe6935be5 --- /dev/null +++ b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaMapperV2.java @@ -0,0 +1,23 @@ +/* + * 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.shapes.Shape; + +public interface JsonSchemaMapperV2 extends JsonSchemaMapper { + default Schema.Builder updateSchema(Shape shape, Schema.Builder schemaBuilder, JsonSchemaConfig config) { + return schemaBuilder; + } + + /** + * Updates a schema builder. + * + * @param context Context of this schema mapping. + * @param schemaBuilder Schema builder to update. + * @return Returns an updated schema builder. + */ + Schema.Builder updateSchema(JsonSchemaMapperContext context, Schema.Builder schemaBuilder); +} 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 ac11846b910..77754970544 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 @@ -334,9 +334,16 @@ private Schema.Builder updateBuilder(Shape shape, Schema.Builder builder) { * @param builder Schema being built. * @return Returns the built schema. */ - private Schema buildSchema(Shape shape, Schema.Builder builder) { + private Schema buildSchema(T shape, Schema.Builder builder) { + JsonSchemaConfig config = converter.getConfig(); + JsonSchemaMapperContext context = new JsonSchemaMapperContext<>(model, shape, config); + for (JsonSchemaMapper mapper : mappers) { - mapper.updateSchema(shape, builder, converter.getConfig()); + builder = mapper.updateSchema(shape, builder, config); + + if (mapper instanceof JsonSchemaMapperV2) { + builder = ((JsonSchemaMapperV2) mapper).updateSchema(context, builder); + } } return builder.build(); 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..eb4e24eb670 --- /dev/null +++ b/smithy-openapi-traits/src/main/java/software/amazon/smithy/openapi/traits/SpecificationExtensionTrait.java @@ -0,0 +1,103 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.openapi.traits; + +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; +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 static final String AS_MEMBER_NAME = "as"; + + private final String as; + + private SpecificationExtensionTrait(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) { + ObjectNode node = value.expectObjectNode(); + SpecificationExtensionTrait.Builder builder = builder().sourceLocation(value); + node.getStringMember(AS_MEMBER_NAME, builder::as); + SpecificationExtensionTrait trait = builder.build(); + trait.setNodeCache(value); + return trait; + } + } + + /** + * Gets the extension name for a given trait shape. + * Either an explicitly configured extension name, or a default transformation of the shape ID. + * + * @param traitShapeId Trait shape to get the extension name. + * @return Extension name for the given trait shape. + */ + public String extensionNameFor(ShapeId traitShapeId) { + return as != null + ? as + : "x-" + traitShapeId.toString().replaceAll("[.#]", "-"); + } + + public static SpecificationExtensionTrait.Builder builder() { + return new SpecificationExtensionTrait.Builder(); + } + + @Override + protected Node createNode() { + return Node.objectNodeBuilder() + .sourceLocation(getSourceLocation()) + .withMember(AS_MEMBER_NAME, this.as) + .build(); + } + + @Override + public SpecificationExtensionTrait.Builder toBuilder() { + return builder() + .sourceLocation(getSourceLocation()) + .as(this.as); + } + + 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 SpecificationExtensionTrait.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..bda299297e4 --- /dev/null +++ b/smithy-openapi-traits/src/main/resources/META-INF/smithy/manifest @@ -0,0 +1 @@ +smithy.openapi.smithy \ No newline at end of file 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..786fe28fb9c --- /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 \ No newline at end of file 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/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 60571d13574..7d3ce57d8b9 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,10 @@ private OpenApi convertWithEnvironment(ConversionEnvironmentNote: the properties and features added by this mapper can be removed using * {@link OpenApiConfig#setDisableFeatures}. */ -public final class OpenApiJsonSchemaMapper implements JsonSchemaMapper { +public final class OpenApiJsonSchemaMapper implements JsonSchemaMapperV2 { /** See https://swagger.io/docs/specification/data-models/keywords/. */ private static final Set UNSUPPORTED_KEYWORD_DIRECTIVES = SetUtils.of( @@ -97,6 +99,14 @@ public Schema.Builder updateSchema(Shape shape, Schema.Builder builder, JsonSche return builder; } + @Override + public Schema.Builder updateSchema(JsonSchemaMapperContext context, Schema.Builder builder) { + SpecificationExtensionsMapper.findSpecificationExtensions( + context.getModel(), context.getShape(), builder::putExtension); + + return builder; + } + private void handleFormatKeyword(Schema.Builder builder, OpenApiConfig config) { String blobFormat = config.getDefaultBlobFormat(); if (config.getVersion().supportsContentEncodingKeyword()) { 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..3fb7f3afe8d --- /dev/null +++ b/smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/mappers/SpecificationExtensionsMapper.java @@ -0,0 +1,62 @@ +/* + * 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 java.util.Map; +import java.util.function.BiConsumer; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.traits.Trait; +import software.amazon.smithy.openapi.fromsmithy.Context; +import software.amazon.smithy.openapi.fromsmithy.OpenApiMapper; +import software.amazon.smithy.openapi.model.Component; +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 { + @Override + public OpenApi after(Context context, OpenApi openapi) { + return attachAllExtensionsFromShape(openapi, context.getModel(), context.getService()); + } + + @Override + public OperationObject updateOperation( + Context context, + OperationShape shape, + OperationObject operation, + String httpMethodName, + String path + ) { + return attachAllExtensionsFromShape(operation, context.getModel(), shape); + } + + private static T attachAllExtensionsFromShape(T component, Model model, Shape shape) { + Map extensions = component.getExtensions(); + findSpecificationExtensions(model, shape, extensions::put); + return component; + } + + /** + * Find all specification extension trait values attached to the given shape. + * + * @param model Model the shape belongs to. + * @param shape Shape to get extensions for. + * @param consumer Consumer called for each specification extension key-value pair found. + */ + public static void findSpecificationExtensions(Model model, Shape shape, BiConsumer consumer) { + shape.getAllTraits().forEach((traitShapeId, trait) -> + model.getShape(traitShapeId) + .flatMap(traitShape -> traitShape.getTrait(SpecificationExtensionTrait.class)) + .map(specificationExtensionTrait -> specificationExtensionTrait.extensionNameFor(traitShapeId)) + .ifPresent(name -> consumer.accept(name, trait.toNode()))); + } +} 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 41fded1df9b..6e7558965b1 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; @@ -42,11 +42,13 @@ import software.amazon.smithy.model.shapes.ShortShape; import software.amazon.smithy.model.shapes.StringShape; import software.amazon.smithy.model.shapes.StructureShape; -import software.amazon.smithy.model.traits.BoxTrait; 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 { @@ -366,4 +368,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..a4da2d55f53 --- /dev/null +++ b/smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/mappers/SpecificationExtensionsMapperTest.java @@ -0,0 +1,58 @@ +package software.amazon.smithy.openapi.fromsmithy.mappers; + +import java.io.InputStream; +import java.net.URL; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +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.assertEquals( + OpenApiConverter + .create() + .config(config) + .convertToNode(getModel(name)), + + getExpectedOpenAPI(name) + ); + } + + private static Model getModel(String name) { + return Model.assembler() + .addImport(getResource(name + ".smithy")) + .addImport(getResource("trait-shapes.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..4809b7d185b --- /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" + } + } + } + } + } +} \ No newline at end of file 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..83bcfe76203 --- /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] +} \ No newline at end of file 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..d9426e4bb73 --- /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" + } + } + } + } +} \ No newline at end of file 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..402a0482044 --- /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] +} \ No newline at end of file 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..c22fef4647c --- /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" + } +} \ No newline at end of file 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..7d69385b93f --- /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] +} \ No newline at end of file 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..b9c6feaf7be --- /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" + } + } + } + } +} \ No newline at end of file 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..ef5b2f8184c --- /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] +} \ No newline at end of file diff --git a/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/mappers/specificationextensions/trait-shapes.smithy b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/mappers/specificationextensions/trait-shapes.smithy new file mode 100644 index 00000000000..2eff3fde8f7 --- /dev/null +++ b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/mappers/specificationextensions/trait-shapes.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 +}