Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support serializing traits into specification extensions in OpenAPI #1609

Merged
merged 2 commits into from
Aug 16, 2023

Conversation

Xtansia
Copy link
Contributor

@Xtansia Xtansia commented Feb 9, 2023

Description of changes

Adds a "meta-trait" smithy.openapi#specificationExtension that can be used to annotate other traits to indicate they should be serialized into specification extension (x-*) properties when converting to OpenAPI. This is supported on both shapes/"schemas" as well as operations. By default the extension will be named by the shape ID replacing # & . with - prefixed with x-, otherwise there is an as: member on the trait that allows renaming.

I believe this could be useful in bridging the gap where custom traits are currently unsupported in OpenAPI conversion, by allowing some opt in.

Adds a JsonSchemaMapperV2 interface that extends the original, and adds an overload of updateSchema that accepts a context object that includes the model being converted.

Example

Some operation:

$version: "2.0"

namespace smithy.example

list StringList {
    member: String
}

@trait
@smithy.openapi#specificationExtension
structure structuredExtension {
    stringMember: String
    integerMember: Integer
    listMember: StringList
}

@trait
@smithy.openapi#specificationExtension(as: "x-metadata")
string stringExtension

@stringExtension("hello world")
structure SpecificationExtensionsOperationInput {
    name: String
    language: String
}

@structuredExtension(
    stringMember: "first field"
    integerMember: 17
    listMember: ["item1", "item2", "item3"]
)
@http(method: "PUT", uri: "/")
operation SpecificationExtensionsOperation {
    input: SpecificationExtensionsOperationInput
}

@aws.protocols#restJson1
service SpecificationExtensions {
    operations: [SpecificationExtensionsOperation]
}

Becomes:

{
    "openapi": "3.0.2",
    "info": {
        "title": "SpecificationExtensions",
        "version": ""
    },
    "paths": {
        "/": {
            "put": {
                "operationId": "SpecificationExtensionsOperation",
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/SpecificationExtensionsOperationRequestContent"
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "SpecificationExtensionsOperation 200 response"
                    }
                },
                "x-smithy-example-structuredExtension": {
                    "stringMember": "first field",
                    "integerMember": 17,
                    "listMember": [
                        "item1",
                        "item2",
                        "item3"
                    ]
                }
            }
        }
    },
    "components": {
        "schemas": {
            "SpecificationExtensionsOperationRequestContent": {
                "type": "object",
                "properties": {
                    "name": {
                        "type": "string"
                    },
                    "language": {
                        "type": "string"
                    }
                },
               "x-metadata": "hello world"
            }
        }
    }
}

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@Xtansia Xtansia requested a review from a team as a code owner February 9, 2023 05:24
@cmoher cmoher requested a review from syall February 22, 2023 16:12
Copy link
Contributor

@syall syall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @Xtansia, thanks for the contribution! This PR adds a lot of new functionality to OpenAPI conversions which is powerful and exciting.

Overall feedback for the changes besides the comments:

  • For new files, use the following copyright header:
    /*
     * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
     * SPDX-License-Identifier: Apache-2.0
     */
    
  • Some classes / methods are missing updated javadoc comments.
  • The tests should be expanded to cover:
    • Custom extension traits for all applicable shape types, or constrained via selector the shape types used to define custom extension traits.
    • Applying these custom extension traits to different parts of the smithy model, e.g. operations, input / output, members of structures and unions, etc.
  • This feature and trait should be renamed to "Specification Extensions" rather than "Vendor Extensions" to match the official OpenAPI specification.

The trait also needs a dedicated section in the "Converting Smithy to OpenAPI" guide at docs/source-2.0/guides/converting-to-openapi.rst similar to other trait definitions (arbitrary example: httpApiKeyAuth). This would include:

  • How to use it in a smithy model,
  • Where to get the trait (the new smithy-openapi-traits package!),
  • The expected OpenAPI conversion (similar to the @examples trait conversion section), and
  • What subset of OpenAPI conversions are supported by this trait compared to the OpenAPI Specification (behavior captured in the expanded tests).

For unsupported features, I believe the jsonAdd configuration setting documented in the "Converting Smithy to OpenAPI" guide should cover most cases.

@Xtansia
Copy link
Contributor Author

Xtansia commented Feb 26, 2023

@syall Thank you for the super thorough feedback, I'll get to work addressing those 👍

@Xtansia Xtansia force-pushed the feat/openapi-vendor-extensions branch 2 times, most recently from 8e63791 to 847fff2 Compare February 27, 2023 21:52
@Xtansia
Copy link
Contributor Author

Xtansia commented Feb 28, 2023

@syall I've had a first go at fixing all the changes you requested and drafting some documentation, would get your thoughts on anything that needs further improving.

@Xtansia Xtansia requested a review from syall February 28, 2023 04:17
Copy link
Contributor

@syall syall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @Xtansia, all of my comments are on the documentation section added.

One item I missed from last review is that SpecificationExtensionsMapper is missing a corresponding SpecificationExtensionsMapperTest. Besides this, the code changes look good!

docs/source-2.0/guides/converting-to-openapi.rst Outdated Show resolved Hide resolved
docs/source-2.0/guides/converting-to-openapi.rst Outdated Show resolved Hide resolved
docs/source-2.0/guides/converting-to-openapi.rst Outdated Show resolved Hide resolved
docs/source-2.0/guides/converting-to-openapi.rst Outdated Show resolved Hide resolved
docs/source-2.0/guides/converting-to-openapi.rst Outdated Show resolved Hide resolved
docs/source-2.0/guides/converting-to-openapi.rst Outdated Show resolved Hide resolved
docs/source-2.0/guides/converting-to-openapi.rst Outdated Show resolved Hide resolved
docs/source-2.0/guides/converting-to-openapi.rst Outdated Show resolved Hide resolved
@syall syall changed the title Support serializing traits into vendor extensions in OpenAPI Support serializing traits into specification extensions in OpenAPI Mar 6, 2023
@Xtansia
Copy link
Contributor Author

Xtansia commented Mar 6, 2023

Hi @Xtansia, all of my comments are on the documentation section added.

One item I missed from last review is that SpecificationExtensionsMapper is missing a corresponding SpecificationExtensionsMapperTest. Besides this, the code changes look good!

@syall Is there something specific you'd want to be covered by a SpecificationExtensionsMapperTest or just moving the current test I'd added out of OpenApiConverterTest?

@syall
Copy link
Contributor

syall commented Mar 6, 2023

@Xtansia Good question. I think moving out the test to SpecificationExtensionsMapperTest makes sense.

The specification-extensions.smithy test model could also be split into separate "unit test" models that cover all of the specification extension smithy types at the different OpenAPI specification extension locations documented:

  • Service shape
  • Operation shape
  • Simple & Aggregate shapes

@Xtansia Xtansia force-pushed the feat/openapi-vendor-extensions branch from 78afd1a to 344625d Compare March 7, 2023 04:17
@Xtansia
Copy link
Contributor Author

Xtansia commented Mar 7, 2023

@syall I've refactored those tests and added cases covering all trait shapes on services, operation, structure and string (as an "inlined" schema). Do you think that's sufficient or would you like coverage of applying the extensions to all simple & aggregate types?

@syall
Copy link
Contributor

syall commented Mar 7, 2023

@Xtansia The current tests are sufficient, I believe that the Simple & Aggregate shapes fall under the same testing category for "inlined" schema.

syall
syall previously approved these changes Mar 7, 2023
@syall syall dismissed their stale review March 13, 2023 16:06

The JsonSchemaMapper::updateSchema breaking change has too much risk, looking into alternatives.

@Xtansia
Copy link
Contributor Author

Xtansia commented Mar 14, 2023

@syall Other possibility I can think of for not breaking current implementations of JsonSchemaMapper is:

Add overload with default implementation to interface:

public interface JsonSchemaMapper {
    default byte getOrder() { return 0; }

    default Schema.Builder updateSchema(Shape shape, Schema.Builder schemaBuilder, JsonSchemaConfig config) { return schemaBuilder; }

    default Schema.Builder updateSchema(Shape shape, Schema.Builder schemaBuilder, JsonSchemaConfig config, Model model) { return schemaBuilder; }
}

And change JsonSchemaShapeVisitor::buildSchema to:

    private Schema buildSchema(Shape shape, Schema.Builder builder) {
        JsonSchemaConfig config = converter.getConfig();
        
        for (JsonSchemaMapper mapper : mappers) {
            builder = mapper.updateSchema(shape, builder, config);
            builder = mapper.updateSchema(shape, builder, config, model);
        }

        return builder.build();
    }

Though it's a tad gross in duplication, and I'm not sure if adding a default implementation to the original method def is class compatible with code compiled with the previous interface version. Alternatively could do similar thing but with a new interface (ie. JsonSchemaMapperWithContext) that extends old JsonSchemaMapper and then do instanceof differentiation in buildSchema

@Xtansia
Copy link
Contributor Author

Xtansia commented Mar 14, 2023

@syall Have pushed up an a version of what I suggested, also changed to passing in a "context" object to the new interface to minimise breaking changes in future.

@Xtansia Xtansia force-pushed the feat/openapi-vendor-extensions branch 2 times, most recently from 7c92736 to 8e927d3 Compare March 19, 2023 19:50
@Xtansia Xtansia requested a review from syall March 19, 2023 19:51
@Xtansia Xtansia force-pushed the feat/openapi-vendor-extensions branch from 8e927d3 to 22eb49a Compare April 2, 2023 23:06
@Xtansia
Copy link
Contributor Author

Xtansia commented Apr 10, 2023

@syall Have you been able to review my alternative approach?

Copy link
Contributor

@syall syall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed the previous approach to use an overloaded JsonSchemaMapper::updateSchema() that uses the new JsonSchemaMapperContext.

JsonSchemaMapperContext is a class that contains all the info needed to update schemas, and is additive by adding fields to the class (e.g. in this PR Model). This avoids adding overloaded methods with parameters.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed JsonSchemaMapper tests to class implementation rather than functional interface.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a breaking change in JsonSchemaMapper from a functional interface to a normal interface.

Although this is a breaking change, I think this is a reasonable compromise to enable this feature.

Xtansia and others added 2 commits August 15, 2023 17:11
@syall syall force-pushed the feat/openapi-vendor-extensions branch from 1c437c1 to 3635eb0 Compare August 16, 2023 00:11
@syall syall merged commit 0ce4187 into smithy-lang:main Aug 16, 2023
@Xtansia Xtansia deleted the feat/openapi-vendor-extensions branch August 16, 2023 20:29
alextwoods pushed a commit to alextwoods/smithy that referenced this pull request Sep 15, 2023
…mithy-lang#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 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants