Skip to content

Commit

Permalink
Implement a trait to support adding specification extensions to OpenAPI
Browse files Browse the repository at this point in the history
Additionally add a JsonSchemaMapperV2 interface rather than breaking original.
  • Loading branch information
Xtansia committed Mar 19, 2023
1 parent 15b4374 commit 8e927d3
Show file tree
Hide file tree
Showing 27 changed files with 1,064 additions and 9 deletions.
152 changes: 152 additions & 0 deletions docs/source-2.0/guides/converting-to-openapi.rst
Original file line number Diff line number Diff line change
Expand Up @@ -937,6 +937,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 <json-ast>`.
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 <https://spec.openapis.org/oas/v3.1.0#openapi-object>`_
* - Operation shape
- `Operation object <https://spec.openapis.org/oas/v3.1.0#operation-object>`_
* - Simple & Aggregate shapes
- `Schema object <https://spec.openapis.org/oas/v3.1.0#schema-object>`_

Unsupported use cases can likely be covered by the :ref:`jsonAdd <generate-openapi-setting-jsonAdd>` feature of the ``smithy-openapi`` plugin.

-----------------------------
Amazon API Gateway extensions
Expand Down Expand Up @@ -1501,3 +1652,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
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <T> Type of Smithy {@link Shape} being mapped.
*/
public class JsonSchemaMapperContext<T extends Shape> {
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;
}
}
Original file line number Diff line number Diff line change
@@ -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<? extends Shape> context, Schema.Builder schemaBuilder);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 <T extends Shape> Schema buildSchema(T shape, Schema.Builder builder) {
JsonSchemaConfig config = converter.getConfig();
JsonSchemaMapperContext<T> 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();
Expand Down
15 changes: 15 additions & 0 deletions smithy-openapi-traits/build.gradle
Original file line number Diff line number Diff line change
@@ -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")
}
Original file line number Diff line number Diff line change
@@ -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;

/**
* <code>smithy.openapi#specificationExtension</code> - Indicates a trait shape should be converted into an <a href="https://spec.openapis.org/oas/v3.1.0#specification-extensions">OpenAPI specification extension</a>.
*/
public final class SpecificationExtensionTrait extends AbstractTrait
implements ToSmithyBuilder<SpecificationExtensionTrait> {
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<SpecificationExtensionTrait, SpecificationExtensionTrait.Builder> {
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;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
software.amazon.smithy.openapi.traits.SpecificationExtensionTrait$Provider
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
smithy.openapi.smithy
Loading

0 comments on commit 8e927d3

Please sign in to comment.