diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index cf6d5f7f5..1570322a8 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -108,6 +108,28 @@ If you are retrospectively adding the API, Then you can refer to the following r - [OpenSearch Documentation](https://opensearch.org/docs/latest) - [Go through the serialisation logic in OpenSearch](https://github.com/opensearch-project/OpenSearch) +### OpenAPI Vendor Extensions + +This repository includes a custom Smithy trait `@vendorExtensions` and accompanying build extension to enable adding custom OpenAPI specification extensions to operations in the converted OpenAPI output. It is used to add additional metadata about the operations to track the "namespaced" concept from the OpenSearch server and clients, and to account for when a single API operation being represented by multiple REST operations. + +```smithy +use opensearch.openapi#vendorExtensions + +@externalDocumentation( + "API Reference": "https://opensearch.org/docs/latest/api-reference/cat/cat-indices/" +) +@vendorExtensions( + "x-operation-group": "cat.indices", + "x-version-added": "1.0" +) +@http(method: "GET", uri: "/_cat/indices") +@documentation("Returns information about indices: number of primaries and replicas, document counts, disk size, ...") +operation CatIndices { + input: CatIndices_Input, + output: CatIndices_Output +} +``` + ## Adding a test-case for API definition Once you've finished with the model API, follow the steps below to create a test-case. diff --git a/build.gradle.kts b/build.gradle.kts index 03fc68863..334bf5462 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -21,6 +21,7 @@ buildscript { classpath("software.amazon.smithy:smithy-openapi:$smithyVersion") classpath("software.amazon.smithy:smithy-aws-traits:$smithyVersion") classpath("software.amazon.smithy:smithy-cli:$smithyVersion") + classpath("org.opensearch.smithy:openapi-traits") // Can't have a buildscript classpath dependency on a project, so need to use composite build & substitution } } @@ -28,6 +29,7 @@ dependencies { implementation("software.amazon.smithy:smithy-model:$smithyVersion") implementation("software.amazon.smithy:smithy-linters:$smithyVersion") implementation("software.amazon.smithy:smithy-aws-traits:$smithyVersion") + implementation("org.opensearch.smithy:openapi-traits") } spotless { diff --git a/model/cat/indices/cat_indicies/operations.smithy b/model/cat/indices/cat_indicies/operations.smithy index 539d85b39..2d4191b4a 100644 --- a/model/cat/indices/cat_indicies/operations.smithy +++ b/model/cat/indices/cat_indicies/operations.smithy @@ -7,6 +7,8 @@ $version: "2" namespace OpenSearch +use opensearch.openapi#vendorExtensions + @externalDocumentation( "OpenSearch Documentation": "https://opensearch.org/docs/latest/api-reference/cat/cat-indices/" ) @@ -15,6 +17,10 @@ namespace OpenSearch @http(method: "GET", uri: "/_cat/indices") @suppress(["HttpUriConflict"]) @documentation("Returns information about indices: number of primaries and replicas, document counts, disk size, etc.") +@vendorExtensions( + "x-namespace": "cat" + "x-operation": "indices" +) operation GetCatIndices { input: GetCatIndicesInput, output: GetCatIndicesOutput @@ -25,6 +31,10 @@ operation GetCatIndices { @http(method: "GET", uri: "/_cat/indices/{index}") @suppress(["HttpUriConflict"]) @documentation("Returns information about indices: number of primaries and replicas, document counts, disk size, etc.") +@vendorExtensions( + "x-namespace": "cat" + "x-operation": "indices" +) operation GetCatIndicesWithIndex { input: GetCatIndicesWithIndexInput, output: GetCatIndicesWithIndexOutput diff --git a/openapi-traits/build.gradle.kts b/openapi-traits/build.gradle.kts new file mode 100644 index 000000000..b6928316c --- /dev/null +++ b/openapi-traits/build.gradle.kts @@ -0,0 +1,25 @@ +plugins { + java + `java-library` + id("com.diffplug.spotless").version("6.11.0") +} + +group = "org.opensearch.smithy" + +repositories { + mavenCentral() +} + +dependencies { + implementation("software.amazon.smithy:smithy-openapi:1.26.0") +} + +spotless { + kotlinGradle { + target("**/*.kts", "**/*.java", "**/*.smithy") + + indentWithSpaces() + endWithNewline() + trimTrailingWhitespace() + } +} diff --git a/openapi-traits/settings.gradle.kts b/openapi-traits/settings.gradle.kts new file mode 100644 index 000000000..cc2384f8f --- /dev/null +++ b/openapi-traits/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "openapi-traits" diff --git a/openapi-traits/src/main/java/org/opensearch/smithy/openapi/extensions/VendorExtensionsExtension.java b/openapi-traits/src/main/java/org/opensearch/smithy/openapi/extensions/VendorExtensionsExtension.java new file mode 100644 index 000000000..dc7721d70 --- /dev/null +++ b/openapi-traits/src/main/java/org/opensearch/smithy/openapi/extensions/VendorExtensionsExtension.java @@ -0,0 +1,21 @@ +package org.opensearch.smithy.openapi.extensions; + +import org.opensearch.smithy.openapi.extensions.mappers.VendorExtensionsJsonSchemaMapper; +import org.opensearch.smithy.openapi.extensions.mappers.VendorExtensionsOpenApiMapper; +import software.amazon.smithy.jsonschema.JsonSchemaMapper; +import software.amazon.smithy.openapi.fromsmithy.OpenApiMapper; +import software.amazon.smithy.openapi.fromsmithy.Smithy2OpenApiExtension; + +import java.util.List; + +public class VendorExtensionsExtension implements Smithy2OpenApiExtension { + @Override + public List getJsonSchemaMappers() { + return List.of(new VendorExtensionsJsonSchemaMapper()); + } + + @Override + public List getOpenApiMappers() { + return List.of(new VendorExtensionsOpenApiMapper()); + } +} diff --git a/openapi-traits/src/main/java/org/opensearch/smithy/openapi/extensions/mappers/VendorExtensionsJsonSchemaMapper.java b/openapi-traits/src/main/java/org/opensearch/smithy/openapi/extensions/mappers/VendorExtensionsJsonSchemaMapper.java new file mode 100644 index 000000000..93ee0b094 --- /dev/null +++ b/openapi-traits/src/main/java/org/opensearch/smithy/openapi/extensions/mappers/VendorExtensionsJsonSchemaMapper.java @@ -0,0 +1,19 @@ +package org.opensearch.smithy.openapi.extensions.mappers; + +import org.opensearch.smithy.openapi.traits.VendorExtensionsTrait; +import software.amazon.smithy.jsonschema.JsonSchemaConfig; +import software.amazon.smithy.jsonschema.JsonSchemaMapper; +import software.amazon.smithy.jsonschema.Schema; +import software.amazon.smithy.model.shapes.Shape; + +public class VendorExtensionsJsonSchemaMapper implements JsonSchemaMapper { + @Override + public Schema.Builder updateSchema(Shape shape, Schema.Builder schemaBuilder, JsonSchemaConfig config) { + shape.getTrait(VendorExtensionsTrait.class) + .ifPresent(trait -> trait.getNode() + .getMembers() + .forEach((k, v) -> schemaBuilder.putExtension(k.getValue(), v))); + + return schemaBuilder; + } +} diff --git a/openapi-traits/src/main/java/org/opensearch/smithy/openapi/extensions/mappers/VendorExtensionsOpenApiMapper.java b/openapi-traits/src/main/java/org/opensearch/smithy/openapi/extensions/mappers/VendorExtensionsOpenApiMapper.java new file mode 100644 index 000000000..40be05617 --- /dev/null +++ b/openapi-traits/src/main/java/org/opensearch/smithy/openapi/extensions/mappers/VendorExtensionsOpenApiMapper.java @@ -0,0 +1,25 @@ +package org.opensearch.smithy.openapi.extensions.mappers; + +import org.opensearch.smithy.openapi.traits.VendorExtensionsTrait; +import software.amazon.smithy.model.shapes.OperationShape; +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.OperationObject; + +public class VendorExtensionsOpenApiMapper implements OpenApiMapper { + @Override + public OperationObject updateOperation(Context context, OperationShape shape, OperationObject operation, String httpMethodName, String path) { + return shape.getTrait(VendorExtensionsTrait.class) + .map(trait -> { + OperationObject.Builder builder = operation.toBuilder(); + + trait.getNode() + .getMembers() + .forEach((k, v) -> builder.putExtension(k.getValue(), v)); + + return builder.build(); + }) + .orElse(operation); + } +} diff --git a/openapi-traits/src/main/java/org/opensearch/smithy/openapi/traits/VendorExtensionsTrait.java b/openapi-traits/src/main/java/org/opensearch/smithy/openapi/traits/VendorExtensionsTrait.java new file mode 100644 index 000000000..3308183a3 --- /dev/null +++ b/openapi-traits/src/main/java/org/opensearch/smithy/openapi/traits/VendorExtensionsTrait.java @@ -0,0 +1,75 @@ +package org.opensearch.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.SmithyBuilder; +import software.amazon.smithy.utils.ToSmithyBuilder; + +public final class VendorExtensionsTrait extends AbstractTrait implements ToSmithyBuilder { + public static final ShapeId ID = ShapeId.from("opensearch.openapi#vendorExtensions"); + + private final ObjectNode node; + + private VendorExtensionsTrait(Builder builder) { + super(ID, builder.getSourceLocation()); + this.node = SmithyBuilder.requiredState("node", builder.node); + } + + public static final class Provider extends AbstractTrait.Provider { + public Provider() { + super(ID); + } + + @Override + public Trait createTrait(ShapeId target, Node value) { + ObjectNode node = value.expectObjectNode(); + VendorExtensionsTrait trait = builder().sourceLocation(value).node(node).build(); + trait.setNodeCache(value); + return trait; + } + } + + public ObjectNode getNode() { + return this.node; + } + + public static Builder builder() { + return new Builder(); + } + + @Override + protected Node createNode() { + return Node.objectNodeBuilder() + .sourceLocation(getSourceLocation()) + .merge(node) + .build(); + } + + @Override + public Builder toBuilder() { + return builder() + .sourceLocation(getSourceLocation()) + .node(node); + } + + public static final class Builder extends AbstractTraitBuilder { + private ObjectNode node; + + private Builder() { + } + + @Override + public VendorExtensionsTrait build() { + return new VendorExtensionsTrait(this); + } + + public Builder node(ObjectNode node) { + this.node = node; + return this; + } + } +} diff --git a/openapi-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService b/openapi-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService new file mode 100644 index 000000000..99e4e093b --- /dev/null +++ b/openapi-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService @@ -0,0 +1 @@ +org.opensearch.smithy.openapi.traits.VendorExtensionsTrait$Provider \ No newline at end of file diff --git a/openapi-traits/src/main/resources/META-INF/services/software.amazon.smithy.openapi.fromsmithy.Smithy2OpenApiExtension b/openapi-traits/src/main/resources/META-INF/services/software.amazon.smithy.openapi.fromsmithy.Smithy2OpenApiExtension new file mode 100644 index 000000000..8297ce5b1 --- /dev/null +++ b/openapi-traits/src/main/resources/META-INF/services/software.amazon.smithy.openapi.fromsmithy.Smithy2OpenApiExtension @@ -0,0 +1 @@ +org.opensearch.smithy.openapi.extensions.VendorExtensionsExtension \ No newline at end of file diff --git a/openapi-traits/src/main/resources/META-INF/smithy/manifest b/openapi-traits/src/main/resources/META-INF/smithy/manifest new file mode 100644 index 000000000..c49b3fa14 --- /dev/null +++ b/openapi-traits/src/main/resources/META-INF/smithy/manifest @@ -0,0 +1 @@ +opensearch.openapi.smithy \ No newline at end of file diff --git a/openapi-traits/src/main/resources/META-INF/smithy/opensearch.openapi.smithy b/openapi-traits/src/main/resources/META-INF/smithy/opensearch.openapi.smithy new file mode 100644 index 000000000..d033060c1 --- /dev/null +++ b/openapi-traits/src/main/resources/META-INF/smithy/opensearch.openapi.smithy @@ -0,0 +1,12 @@ +$version: "2" + +namespace opensearch.openapi + +@trait( + selector: ":is(simpleType, list, map, structure, union, operation, member)" +) +map vendorExtensions { + @pattern("^x-.+$") + key: String + value: Document +} diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 000000000..97c187fc9 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,3 @@ +rootProject.name = "opensearch-api-specification" + +includeBuild("openapi-traits") diff --git a/smithy-build.json b/smithy-build.json index 5cdc1c451..34abf16a4 100644 --- a/smithy-build.json +++ b/smithy-build.json @@ -1,9 +1,15 @@ { "version": "1.0", - "plugins": { - "openapi": { - "service": "OpenSearch#OpenSearch", - "protocol": "aws.protocols#restJson1" + "projections": { + "full": { + "plugins": { + "openapi": { + "service": "OpenSearch#OpenSearch", + "protocol": "aws.protocols#restJson1", + "tags": true, + "useIntegerType": true + } + } } } }