From f42c5169d96fab60b07584c02d3ba70a91c792da Mon Sep 17 00:00:00 2001 From: kstich Date: Mon, 28 Sep 2020 16:17:07 -0700 Subject: [PATCH] Add CloudFormation resource schema generation This commit introduces a new "cloudformation" build plugin that, given a model decorated with the aws.cloudformation traits, will generate CloudFormation Resource Schemas. This includes support for specifying the mutability of properties, their documentation, and more. --- .../generating-cloudformation-resources.rst | 478 ++++++++++++++++++ docs/source/1.0/guides/index.rst | 1 + .../1.0/spec/aws/aws-cloudformation.rst | 2 + settings.gradle | 1 + smithy-aws-cloudformation/build.gradle | 37 ++ .../aws/cloudformation/schema/CfnConfig.java | 315 ++++++++++++ .../cloudformation/schema/CfnException.java | 31 ++ .../schema/fromsmithy/CfnConverter.java | 369 ++++++++++++++ .../schema/fromsmithy/CfnMapper.java | 75 +++ .../schema/fromsmithy/Context.java | 135 +++++ .../schema/fromsmithy/Smithy2Cfn.java | 50 ++ .../fromsmithy/Smithy2CfnExtension.java | 49 ++ .../fromsmithy/mappers/CoreExtension.java | 36 ++ .../fromsmithy/mappers/DeprecatedMapper.java | 52 ++ .../mappers/DocumentationMapper.java | 93 ++++ .../fromsmithy/mappers/IdentifierMapper.java | 60 +++ .../fromsmithy/mappers/JsonAddMapper.java | 73 +++ .../fromsmithy/mappers/MutabilityMapper.java | 54 ++ .../cloudformation/schema/model/Handler.java | 106 ++++ .../cloudformation/schema/model/Property.java | 126 +++++ .../cloudformation/schema/model/Remote.java | 131 +++++ .../schema/model/ResourceSchema.java | 430 ++++++++++++++++ ...tion.schema.fromsmithy.Smithy2CfnExtension | 1 + ...ware.amazon.smithy.build.SmithyBuildPlugin | 1 + .../schema/fromsmithy/CfnConfigTest.java | 60 +++ .../schema/fromsmithy/CfnConverterTest.java | 131 +++++ .../schema/fromsmithy/CfnSchemasTest.java | 81 +++ .../schema/fromsmithy/Smithy2CfnTest.java | 95 ++++ .../schema/fromsmithy/TestRunnerTest.java | 74 +++ .../mappers/DeprecatedMapperTest.java | 72 +++ .../mappers/DocumentationMapperTest.java | 81 +++ .../fromsmithy/mappers/JsonAddTest.java | 62 +++ .../disable-caps-fooresource.cfn.json | 61 +++ .../integ/complex-resource.cfn.json | 100 ++++ .../fromsmithy/integ/complex-resource.smithy | 126 +++++ .../integ/create-and-read-mutability.cfn.json | 22 + .../integ/create-and-read-mutability.smithy | 21 + .../integ/create-write-mutability.cfn.json | 25 + .../integ/create-write-mutability.smithy | 34 ++ .../fromsmithy/integ/full-mutability.cfn.json | 25 + .../fromsmithy/integ/full-mutability.smithy | 40 ++ .../fromsmithy/integ/put-lifecycle.cfn.json | 25 + .../fromsmithy/integ/put-lifecycle.smithy | 44 ++ .../fromsmithy/integ/queue-example.cfn.json | 101 ++++ .../fromsmithy/integ/queue-example.smithy | 210 ++++++++ .../fromsmithy/integ/read-mutability.cfn.json | 26 + .../fromsmithy/integ/read-mutability.smithy | 38 ++ .../integ/write-mutability.cfn.json | 26 + .../fromsmithy/integ/write-mutability.smithy | 36 ++ .../schema/fromsmithy/mappers/simple.smithy | 107 ++++ .../provider.definition.schema.v1.json | 425 ++++++++++++++++ .../fromsmithy/simple-service-aws.cfn.json | 57 +++ .../fromsmithy/simple-service-aws.smithy | 109 ++++ .../smithy-testservice-bar.cfn.json | 40 ++ .../smithy-testservice-basil.cfn.json | 43 ++ .../smithy-testservice-fooresource.cfn.json | 61 +++ .../schema/fromsmithy/test-service.smithy | 245 +++++++++ .../model/node/DefaultNodeSerializers.java | 6 +- 58 files changed, 5412 insertions(+), 3 deletions(-) create mode 100644 docs/source/1.0/guides/generating-cloudformation-resources.rst create mode 100644 smithy-aws-cloudformation/build.gradle create mode 100644 smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/CfnConfig.java create mode 100644 smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/CfnException.java create mode 100644 smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/CfnConverter.java create mode 100644 smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/CfnMapper.java create mode 100644 smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/Context.java create mode 100644 smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/Smithy2Cfn.java create mode 100644 smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/Smithy2CfnExtension.java create mode 100644 smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/CoreExtension.java create mode 100644 smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/DeprecatedMapper.java create mode 100644 smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/DocumentationMapper.java create mode 100644 smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/IdentifierMapper.java create mode 100644 smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/JsonAddMapper.java create mode 100644 smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/MutabilityMapper.java create mode 100644 smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/Handler.java create mode 100644 smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/Property.java create mode 100644 smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/Remote.java create mode 100644 smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/ResourceSchema.java create mode 100644 smithy-aws-cloudformation/src/main/resources/META-INF/services/software.amazon.smithy.aws.cloudformation.schema.fromsmithy.Smithy2CfnExtension create mode 100644 smithy-aws-cloudformation/src/main/resources/META-INF/services/software.amazon.smithy.build.SmithyBuildPlugin create mode 100644 smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/CfnConfigTest.java create mode 100644 smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/CfnConverterTest.java create mode 100644 smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/CfnSchemasTest.java create mode 100644 smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/Smithy2CfnTest.java create mode 100644 smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/TestRunnerTest.java create mode 100644 smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/DeprecatedMapperTest.java create mode 100644 smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/DocumentationMapperTest.java create mode 100644 smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/JsonAddTest.java create mode 100644 smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/disable-caps-fooresource.cfn.json create mode 100644 smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/complex-resource.cfn.json create mode 100644 smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/complex-resource.smithy create mode 100644 smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/create-and-read-mutability.cfn.json create mode 100644 smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/create-and-read-mutability.smithy create mode 100644 smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/create-write-mutability.cfn.json create mode 100644 smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/create-write-mutability.smithy create mode 100644 smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/full-mutability.cfn.json create mode 100644 smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/full-mutability.smithy create mode 100644 smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/put-lifecycle.cfn.json create mode 100644 smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/put-lifecycle.smithy create mode 100644 smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/queue-example.cfn.json create mode 100644 smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/queue-example.smithy create mode 100644 smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/read-mutability.cfn.json create mode 100644 smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/read-mutability.smithy create mode 100644 smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/write-mutability.cfn.json create mode 100644 smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/write-mutability.smithy create mode 100644 smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/simple.smithy create mode 100644 smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/provider.definition.schema.v1.json create mode 100644 smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/simple-service-aws.cfn.json create mode 100644 smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/simple-service-aws.smithy create mode 100644 smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/smithy-testservice-bar.cfn.json create mode 100644 smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/smithy-testservice-basil.cfn.json create mode 100644 smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/smithy-testservice-fooresource.cfn.json create mode 100644 smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/test-service.smithy diff --git a/docs/source/1.0/guides/generating-cloudformation-resources.rst b/docs/source/1.0/guides/generating-cloudformation-resources.rst new file mode 100644 index 00000000000..034ca0c3830 --- /dev/null +++ b/docs/source/1.0/guides/generating-cloudformation-resources.rst @@ -0,0 +1,478 @@ +====================================================== +Generating CloudFormation Resource Schemas from Smithy +====================================================== + +This guide describes how Smithy models can generate `CloudFormation Resource +Schemas`_. + +.. contents:: Table of contents + :depth: 2 + :local: + :backlinks: none + +------------ +Introduction +------------ + +CloudFormation Resource Schemas are the standard method of `modeling a resource +provider`_ for use within `CloudFormation`_. These schemas can then be used +to `develop the resource provider`_ for support in CloudFormation. Generating +Resource Schemas automatically from Smithy resources removes the duplicate +effort of specifying them. + +:ref:`AWS CloudFormation traits ` define how +CloudFormation Resource Schemas should be generated from Smithy resources. +Automatically generating schemas from a service's API lowers the effort needed +to generate and maintain them, keeps the schemas in sync with any changes to +the Smithy model, reduces the potential for errors in the translation, and +provides a more complete depiction of a resource in its schema. These schemas +can be utilized by the `CloudFormation Command Line Interface`_ to build, +register, and deploy resource providers. + +:ref:`Other traits may also influence CloudFormation Resource Schema +generation. ` + +Differences between Smithy resources and CloudFormation Resource Schemas +------------------------------------------------------------------------ + +Smithy and CloudFormation have different approaches to modeling resources. In +Smithy, a :ref:`resource ` is an entity with an identity that has a +set of operations. CloudFormation resources are defined as a collection of +properties and their attributes, along with additional information on which +properties are identifiers or have restrictions on their mutability. + + +------------------------------------ +Generating Schemas with smithy-build +------------------------------------ + +The ``cloudformation`` plugin contained in the ``software.amazon.smithy:smithy-aws-cloudformation`` +package can be used with smithy-build and the `Smithy Gradle plugin`_ to +generate CloudFormation Resource Schemas from Smithy models. + +The following example shows how to configure Gradle to generate CloudFormation +Resource Schemas from a Smithy model :ref:`using a buildscript dependency +`: + +.. code-block:: kotlin + :caption: build.gradle.kts + :name: smithy-build-gradle + + plugins { + java + id("software.amazon.smithy").version("0.5.1") + } + + buildscript { + dependencies { + classpath("software.amazon.smithy:smithy-aws-cloudformation:__smithy_version__") + } + } + +The Smithy Gradle plugin relies on a ``smithy-build.json`` file found at the +root of a project to define the actual process of generating the CloudFormation +Resource Schemas. The following example defines a ``smithy-build.json`` file +that generates a CloudFormation Resource Schemas for the specified resource +shapes bound to the ``smithy.example#Queues`` service using the ``Smithy`` +organization. + +.. code-block:: json + :caption: smithy-build.json + :name: cfn-smithy-build-json + + { + "version": "1.0", + "plugins": { + "cloudformation": { + "service": "smithy.example#Queues", + "organizationName": "Smithy" + } + } + } + +AWS Service teams SHOULD NOT set the ``organizationName`` property, and instead +use the :ref:`cloudFormationName property of the aws.api#service trait +`. The following configuration and model would +generate one Resource Schema with the ``typeName`` of ``AWS:Queues:Queue``. + +.. code-block:: json + :caption: smithy-build.json + + { + "version": "1.0", + "plugins": { + "cloudformation": { + "service": "smithy.example#QueueService", + } + } + } + +.. code-block:: smithy + :caption: model.smithy + + namespace smithy.example + + use aws.api#service + + @service(sdkId: "Queues", cloudFormationName: "Queues") + service QueueService { + version: "2020-07-02", + resources: [Queue], + } + +.. important:: + + A buildscript dependency on "software.amazon.smithy:smithy-aws-cloudformation:__smithy_version__" is + required in order for smithy-build to map the "cloudformation" plugin name + to the correct Java library implementation. + + +------------------------------------- +CloudFormation configuration settings +------------------------------------- + +The ``cloudformation`` plugin provides configuration options to influence the +Resource Schemas that it generates. + +.. tip:: + + You typically only need to configure the ``service`` and + ``organizationName`` settings to generate Resource Schemas. + +The following settings are supported: + +.. _generate-cloudformation-setting-service: + +service (``string``) + **Required**. The Smithy service :ref:`shape ID ` to convert. + For example, ``smithy.example#Queues``. + + .. code-block:: json + + { + "version": "1.0", + "plugins": { + "cloudformation": { + "service": "smithy.example#Queues", + "organizationName": "Smithy" + } + } + } + +.. _generate-cloudformation-setting-organizationName: + +organizationName (``string``) + The ``Organization`` component of the resource's `type name`_. Defaults to + "AWS" if the :ref:`aws.api#service-trait` is present, otherwise is + **required**. + + .. code-block:: json + + { + "version": "1.0", + "plugins": { + "cloudformation": { + "service": "smithy.example#Queues", + "organizationName": "Smithy" + } + } + } + +.. _generate-cloudformation-setting-serviceName: + +serviceName (``string``) + Allows overriding the ``Service`` component of the resource's `type name`_. + This value defaults to the :ref:`cloudFormationName property of the + aws.api#service trait ` if present, or the + shape name of the specified service shape otherwise. + + .. code-block:: json + + { + "version": "1.0", + "plugins": { + "cloudformation": { + "service": "smithy.example#QueueService", + "organizationName": "Smithy", + "serviceName": "Queues" + } + } + } + +.. _generate-cloudformation-setting-externalDocs: + +externalDocs (``[string]``) + Limits the source of generated `"documentationUrl" fields`__ to the + specified priority ordered list of names in an :ref:`externaldocumentation-trait`. + This list is case insensitive. By default, this is a list of the following + values: "Documentation Url", "DocumentationUrl", "API Reference", "User + Guide", "Developer Guide", "Reference", and "Guide". + + .. code-block:: json + + { + "version": "1.0", + "plugins": { + "cloudformation": { + "service": "smithy.example#Queues", + "organizationName": "Smithy", + "externalDocs": [ + "Documentation Url", + "Custom" + ] + } + } + } + +.. __: https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-schema.html#schema-properties-documentationUrl + +.. _generate-cloudformation-setting-sourceDocs: + +sourceDocs (``[string]``) + Limits the source of generated `"sourceUrl" fields`__ to the specified + priority ordered list of names in an :ref:`externaldocumentation-trait`. + This list is case insensitive. By default, this is a list of the following + values: "Source Url", "SourceUrl", "Source", and "Source Code". + + .. code-block:: json + + { + "version": "1.0", + "plugins": { + "cloudformation": { + "service": "smithy.example#Queues", + "organizationName": "Smithy", + "sourceDocs": [ + "Source Url", + "Custom" + ] + } + } + } + +.. __: https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-schema.html#schema-properties-sourceUrl + +.. _generate-cloudformation-setting-jsonAdd: + +jsonAdd (``Map>``) + Adds or replaces the JSON value in the generated Resource Schemas at the + given JSON pointer locations with a different JSON value. The value must be + a map where each key is a resource shape ID. The value is a map where each + key is a valid JSON pointer string as defined in :rfc:`6901`. Each value in + the nested map is the JSON value to add or replace at the given target. + + Values are added using similar semantics of the "add" operation of + JSON Patch, as specified in :rfc:`6902`, with the exception that adding + properties to an undefined object will create nested objects in the + result as needed. + + .. code-block:: json + + { + "version": "1.0", + "plugins": { + "cloudformation": { + "service": "smithy.example#Queues", + "organizationName": "Smithy", + "jsonAdd": { + "/info/title": "Replaced title value", + "/info/nested/foo": { + "hi": "Adding this object created intermediate objects too!" + }, + "/info/nested/foo/baz": true + } + } + } + } + +.. _generate-cloudformation-setting-disableDeprecatedPropertyGeneration: + +disableDeprecatedPropertyGeneration (``boolean``) + Sets whether to disable generating ``deprecatedProperties`` for Resource + Schemas. By default, deprecated members are automatically added to the + ``deprecatedProperties`` schema property. + + .. code-block:: json + + { + "version": "1.0", + "plugins": { + "cloudformation": { + "service": "smithy.example#Queues", + "organizationName": "Smithy", + "disableDeprecatedPropertyGeneration": true + } + } + } + +.. _generate-cloudformation-setting-disableCapitalizedProperties: + +disableCapitalizedProperties (``boolean``) + Sets whether to disable automatically capitalizing names of properties of + Resource Schemas. By default, property names of resource schemas are + capitalized if no :ref:`cfnName ` trait + is applied. + + .. code-block:: json + + { + "version": "1.0", + "plugins": { + "cloudformation": { + "service": "smithy.example#Queues", + "organizationName": "Smithy", + "disableCapitalizedProperties": true + } + } + } + +---------------------------------- +JSON schema configuration settings +---------------------------------- + +.. _generate-cloudformation-jsonschema-setting-defaultTimestampFormat: + +defaultTimestampFormat (``string``) + Sets the assumed :ref:`timestampFormat-trait` value for timestamps with + no explicit timestampFormat trait. The provided value is expected to be + a string. Defaults to "date-time" if not set. Can be set to "date-time", + "epoch-seconds", or "http-date". + + .. code-block:: json + + { + "version": "1.0", + "plugins": { + "cloudformation": { + "service": "smithy.example#Queues", + "organizationName": "Smithy", + "defaultTimestampFormat": "epoch-seconds" + } + } + } + +.. _generate-cloudformation-jsonschema-setting-schemaDocumentExtensions: + +schemaDocumentExtensions (``Map``) + Adds custom top-level key-value pairs to all of the generated + CloudFormation Resource Schemas. Any existing value is overwritten. + + .. code-block:: json + + { + "version": "1.0", + "plugins": { + "cloudformation": { + "service": "smithy.example#Queues", + "organizationName": "Smithy", + "schemaDocumentExtensions": { + "x-my-custom-top-level-property": "Hello!", + "x-another-custom-top-level-property": { + "can be": ["complex", "value", "too!"] + } + } + } + } + } + +.. _generate-cloudformation-jsonschema-setting-disableFeatures: + +disableFeatures (``[string]``) + Disables JSON schema and CloudFormation schema property names from + appearing in the generated CloudFormation Resource Schemas. + + .. code-block:: json + + { + "version": "1.0", + "plugins": { + "cloudformation": { + "service": "smithy.example#Queues", + "organizationName": "Smithy", + "disableFeatures": ["propertyNames"] + } + } + } + +.. _generate-cloudformation-other-traits: + +-------------------------------------- +Other traits that influence generation +-------------------------------------- + +In addition to the :ref:`AWS CloudFormation traits `, +the following traits affect the generation of CloudFormation Resource Schemas. + +``documentation`` + When applied to a :ref:`resource` shape, the contents will be converted + into the ``description`` property of the generated Resource Schema. + +``externalDocumentation`` + When applied to a :ref:`resource ` shape, the contents will be + converted according to the :ref:`externalDocs ` + and :ref:`sourceDocs ` + settings. + +.. note:: + + :ref:`Custom traits ` defined in a Smithy model are not + converted and added to CloudFormation Resource Schemas. Doing so requires + the creation of a custom ``software.amazon.smithy.aws.cloudformation.schema.fromsmithy.Smithy2CfnExtension``. + + +---------------------------- +Generating Schemas with code +---------------------------- + +Developers that need more advanced control over the generation of +CloudFormation resources from Smithy can use the +``software.amazon.smithy:smithy-aws-cloudformation`` Java library to perform +the generation. + +First, you'll need to get a copy of the library. The following example shows +how to install ``software.amazon.smithy:smithy-aws-cloudformation`` through +Gradle: + +.. code-block:: kotlin + :caption: build.gradle.kts + :name: code-build-gradle + + buildscript { + dependencies { + classpath("software.amazon.smithy:smithy-aws-cloudformation:__smithy_version__") + } + } + +Next, you need to create and configure a ``CloudFormationConverter``: + +.. code-block:: java + + import java.util.List; + import software.amazon.smithy.model.shapes.ShapeId; + import software.amazon.smithy.aws.cloudformation.schema.CfnConfig; + import software.amazon.smithy.aws.cloudformation.schema.fromsmithy.CfnConverter; + import software.amazon.smithy.aws.cloudformation.schema.model.ResourceSchema; + + CfnConverter converter = CfnConverter.create(); + + // Add any necessary configuration settings. + CfnConfig config = new CfnConfig(); + config.setService(ShapeId.from("smithy.example#Queues")); + config.setOrganizationName("Smithy"); + + // Generate the schemas. + List schemas = converter.convert(myModel); + +The conversion process is highly extensible through +``software.amazon.smithy.aws.cloudformation.schema.fromsmithy.Smithy2CfnExtension`` +service providers. See the `Javadocs`_ for more information. + +.. _CloudFormation Resource Schemas: https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-schema.html +.. _CloudFormation: https://aws.amazon.com/cloudformation/ +.. _modeling a resource provider: https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-types.html +.. _develop the resource provider: https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-develop.html +.. _CloudFormation Command Line Interface: https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/what-is-cloudformation-cli.html +.. _Smithy Resource: https://awslabs.github.io/smithy/1.0/spec/core/model.html#resource +.. _Smithy Gradle plugin: https://github.com/awslabs/smithy-gradle-plugin +.. _type name: https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-schema.html#schema-properties-typeName +.. _Javadocs: https://awslabs.github.io/smithy/javadoc/__smithy_version__/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/Smithy2CfnExtension.html diff --git a/docs/source/1.0/guides/index.rst b/docs/source/1.0/guides/index.rst index 95b7c9b1598..5e655e22fcd 100644 --- a/docs/source/1.0/guides/index.rst +++ b/docs/source/1.0/guides/index.rst @@ -12,3 +12,4 @@ Smithy Guides evolving-models style-guide converting-to-openapi + generating-cloudformation-resources diff --git a/docs/source/1.0/spec/aws/aws-cloudformation.rst b/docs/source/1.0/spec/aws/aws-cloudformation.rst index efbdf2061fe..f64eed1e438 100644 --- a/docs/source/1.0/spec/aws/aws-cloudformation.rst +++ b/docs/source/1.0/spec/aws/aws-cloudformation.rst @@ -1,3 +1,5 @@ +.. _aws-cloudformation-traits: + ========================= AWS CloudFormation traits ========================= diff --git a/settings.gradle b/settings.gradle index 4039eb4034f..5bc32dbe6ad 100644 --- a/settings.gradle +++ b/settings.gradle @@ -25,3 +25,4 @@ include ":smithy-protocol-test-traits" include ':smithy-jmespath' include ":smithy-waiters" include ":smithy-aws-cloudformation-traits" +include ":smithy-aws-cloudformation" diff --git a/smithy-aws-cloudformation/build.gradle b/smithy-aws-cloudformation/build.gradle new file mode 100644 index 00000000000..299ae87a0db --- /dev/null +++ b/smithy-aws-cloudformation/build.gradle @@ -0,0 +1,37 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +description = "This module contains support for converting Smithy resources to CloudFormation Resource Schemas." + +ext { + displayName = "Smithy :: Cloudformation Conversion" + moduleName = "software.amazon.smithy.cloudformation.converter" +} + +// Necessary to load the everit JSON Schema validator. +repositories { + maven { url "https://jitpack.io" } +} + +dependencies { + api project(":smithy-build") + api project(":smithy-jsonschema") + api project(":smithy-aws-cloudformation-traits") + api project(":smithy-aws-traits") + + // For use in validating schemas used in tests against the supplied + // CloudFormation definition schema. + testCompile("com.github.everit-org.json-schema:org.everit.json.schema:1.12.1") +} diff --git a/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/CfnConfig.java b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/CfnConfig.java new file mode 100644 index 00000000000..b65fb61c36c --- /dev/null +++ b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/CfnConfig.java @@ -0,0 +1,315 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.aws.cloudformation.schema; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.logging.Logger; +import software.amazon.smithy.jsonschema.JsonSchemaConfig; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.NodeMapper; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.utils.ListUtils; + +/** + * "cloudformation" smithy-build plugin configuration settings. + */ +public final class CfnConfig extends JsonSchemaConfig { + + /** The JSON pointer to where CloudFormation schema shared resource properties should be written. */ + public static final String SCHEMA_COMPONENTS_POINTER = "#/definitions"; + + private static final Logger LOGGER = Logger.getLogger(CfnConfig.class.getName()); + + private boolean disableDeprecatedPropertyGeneration = false; + private boolean disableCapitalizedProperties = false; + private List externalDocs = ListUtils.of( + "Documentation Url", "DocumentationUrl", "API Reference", "User Guide", + "Developer Guide", "Reference", "Guide"); + private Map> jsonAdd = Collections.emptyMap(); + private String organizationName; + private String serviceName; + private ShapeId service; + private List sourceDocs = ListUtils.of( + "Source Url", "SourceUrl", "Source", "Source Code"); + + public CfnConfig() { + super(); + + // CloudFormation Resource Schemas MUST use alphanumeric only references. + // Invoke the parent class's method directly since we override it to lock + // this functionality. + // + // https://github.com/aws-cloudformation/cloudformation-cli/blob/master/src/rpdk/core/data/schema/provider.definition.schema.v1.json#L303 + super.setAlphanumericOnlyRefs(true); + + setDefinitionPointer(SCHEMA_COMPONENTS_POINTER); + + // CloudFormation Resource Schemas MUST use the patternProperties schema + // property for maps. Invoke the parent class's method directly since + // we override it to lock this functionality. + // + // https://github.com/aws-cloudformation/cloudformation-cli/blob/master/src/rpdk/core/data/schema/provider.definition.schema.v1.json#L166-L177 + super.setMapStrategy(MapStrategy.PATTERN_PROPERTIES); + + // + // CloudFormation Resource Schemas MUST use the oneOf schema property for + // unions. Invoke the parent class's method directly since we override it + // to lock this functionality. + // + // https://github.com/aws-cloudformation/cloudformation-cli/blob/master/src/rpdk/core/data/schema/provider.definition.schema.v1.json#L210 + // https://github.com/aws-cloudformation/cloudformation-cli/blob/master/src/rpdk/core/data/schema/provider.definition.schema.v1.json#L166 + super.setUnionStrategy(UnionStrategy.ONE_OF); + } + + @Override + public void setAlphanumericOnlyRefs(boolean alphanumericOnlyRefs) { + // CloudFormation Resource Schemas MUST use alphanumeric only references. + // Throw if customers tried to set it to false. + // + // https://github.com/aws-cloudformation/cloudformation-cli/blob/master/src/rpdk/core/data/schema/provider.definition.schema.v1.json#L303 + if (!alphanumericOnlyRefs) { + throw new CfnException("CloudFormation Resource Schemas MUST use alphanumeric only " + + "references. `alphanumericOnlyRefs` value of `false` was provided."); + } + } + + public boolean getDisableDeprecatedPropertyGeneration() { + return disableDeprecatedPropertyGeneration; + } + + /** + * Set to true to disable generating {@code deprecatedProperties} for Resource Schemas. + * + *

By default, deprecated members are automatically added to the + * {@code deprecatedProperties} schema property. + * + * @param disableDeprecatedPropertyGeneration True to disable {@code deprecatedProperties} + * generation, false otherwise. + */ + public void setDisableDeprecatedPropertyGeneration(boolean disableDeprecatedPropertyGeneration) { + this.disableDeprecatedPropertyGeneration = disableDeprecatedPropertyGeneration; + } + + public boolean getDisableCapitalizedProperties() { + return disableCapitalizedProperties; + } + + /** + * Set to true to disable automatically capitalizing names of properties + * of Resource Schemas. + * + *

By default, property names of Resource Schemas are capitalized if + * no {@code cfnName} trait is applied. + * + * @param disableCapitalizedProperties True to disable capitalizing property names, + * false otherwise. + */ + public void setDisableCapitalizedProperties(boolean disableCapitalizedProperties) { + this.disableCapitalizedProperties = disableCapitalizedProperties; + } + + public List getExternalDocs() { + return externalDocs; + } + + /** + * Limits the source of converted "externalDocs" fields to the specified + * priority ordered list of names in an externalDocumentation trait. + * + *

This list is case insensitive. By default, this is a list of the + * following values: "Documentation Url", "DocumentationUrl", "API Reference", + * "User Guide", "Developer Guide", "Reference", and "Guide". + * + * @param externalDocs External docs to look for and convert, in order. + */ + public void setExternalDocs(List externalDocs) { + this.externalDocs = externalDocs; + } + + public Map> getJsonAdd() { + return jsonAdd; + } + + /** + * Adds or replaces the JSON value in the generated resource schema + * document at the given JSON pointer locations with a different JSON + * value. + * + *

The value must be a map where each key is a resource shape ID. The + * value is a map where each key is a valid JSON pointer string as defined + * in RFC 6901. Each value in the nested map is the JSON value to add or + * replace at the given target. + * + *

Values are added using similar semantics of the "add" operation + * of JSON Patch, as specified in RFC 6902, with the exception that + * adding properties to an undefined object will create nested + * objects in the result as needed. + * + * @param jsonAdd Map of JSON path to values to patch in. + */ + public void setJsonAdd(Map> jsonAdd) { + this.jsonAdd = Objects.requireNonNull(jsonAdd); + } + + @Override + public void setUseJsonName(boolean useJsonName) { + // CloudFormation Resource Schemas use a separate strategy, via @cfnName, + // for naming JSON Schema properties for structures and unions. Throw if + // customers tried to set it at all. + // + // See CfnConverter::getPropertyNamingStrategy + throw new CfnException(String.format("CloudFormation Resource Schemas use the `@cfnName` trait for " + + "naming JSON Schema properties for structures and unions. `useJsonName` value of `%b` was provided.", + useJsonName)); + } + + @Override + public void setMapStrategy(MapStrategy mapStrategy) { + // CloudFormation Resource Schemas MUST use the patternProperties schema + // property for maps, which was already set in the constructor. Throw if + // customers tried to set it to another MapStrategy. + // + // https://github.com/aws-cloudformation/cloudformation-cli/blob/master/src/rpdk/core/data/schema/provider.definition.schema.v1.json#L166-L177 + if (mapStrategy != MapStrategy.PATTERN_PROPERTIES) { + throw new CfnException(String.format("CloudFormation Resource Schemas require the use of " + + "`patternProperties` for defining maps in JSON Schema. `mapStrategy` value of `%s` was provided.", + mapStrategy)); + } + } + + public String getOrganizationName() { + return organizationName; + } + + /** + * Sets the "Organization" component for each of the generated resource's + * type name. + * + *

This value defaults to "AWS" if the {@code aws.api#service} trait is + * present. Otherwise, the value is required configuration. + * + * @see Type Name + * + * @param organizationName Name to use for the "Organization" component of resource type names. + */ + public void setOrganizationName(String organizationName) { + this.organizationName = organizationName; + } + + public String getServiceName() { + return serviceName; + } + + /** + * Sets the "Service" component for each of the generated resource's + * type name. + * + *

This value defaults to the value of the {@code aws.api#service/cloudFormationName} + * if the trait is present. Otherwise, the value defaults to the shape name of the + * specified service shape. + * + * @see Type Name + * + * @param serviceName Name to use for the "Service" component of resource type names. + */ + public void setServiceName(String serviceName) { + this.serviceName = serviceName; + } + + public ShapeId getService() { + return service; + } + + /** + * Sets the service shape ID to convert the resources of. + * + *

For example, smithy.example#Weather. + * + * @param service the Smithy service shape ID to convert the resources of. + */ + public void setService(ShapeId service) { + this.service = service; + } + + public List getSourceDocs() { + return sourceDocs; + } + + /** + * Limits the source of converted "sourceDocs" fields to the specified + * priority ordered list of names in an externalDocumentation trait. + * + *

This list is case insensitive. By default, this is a list of the + * following values: "Source Url", "SourceUrl", "Source", and "Source Code". + * + * @param sourceDocs Source docs to look for and convert, in order. + */ + public void setSourceDocs(List sourceDocs) { + this.sourceDocs = sourceDocs; + } + + @Override + public void setUnionStrategy(UnionStrategy unionStrategy) { + // CloudFormation Resource Schemas MUST use the oneOf schema property + // for unions, which was already set in the constructor. Schemas are + // not allowed to define additionalProperties as true, and modeling + // as a structure is incorrect when oneOf is supported. Throw if + // customers tried to set it to another UnionStrategy. + // + // https://github.com/aws-cloudformation/cloudformation-cli/blob/master/src/rpdk/core/data/schema/provider.definition.schema.v1.json#L210 + // https://github.com/aws-cloudformation/cloudformation-cli/blob/master/src/rpdk/core/data/schema/provider.definition.schema.v1.json#L166 + if (unionStrategy != UnionStrategy.ONE_OF) { + throw new CfnException(String.format("CloudFormation Resource Schemas require the use of `oneOf` " + + "for defining unions in JSON Schema. `unionStrategy` value of `%s` was provided.", + unionStrategy)); + } + } + + /** + * Creates a CfnConfig from a Node value. + * + *

This method uses the {@link NodeMapper} on the converted input object. + * Note that this class can be deserialized using a NodeMapper too since the + * NodeMapper will look for a static, public, fromNode method. + * + *

This method also serializes unknown properties into the + * "extensions" map so that they are accessible to CfnMapper implementations. + * + * @param settings Input to deserialize. + * @return Returns the deserialized + */ + public static CfnConfig fromNode(Node settings) { + NodeMapper mapper = new NodeMapper(); + + mapper.setWhenMissingSetter(NodeMapper.WhenMissing.INGORE); + + ObjectNode node = settings.expectObjectNode(); + CfnConfig config = new CfnConfig(); + mapper.deserializeInto(node, config); + + // Add all properties to "extensions" to make them accessible + // in plugins. + for (Map.Entry entry : node.getStringMap().entrySet()) { + config.putExtension(entry.getKey(), entry.getValue()); + } + + return config; + } +} diff --git a/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/CfnException.java b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/CfnException.java new file mode 100644 index 00000000000..86481b0d841 --- /dev/null +++ b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/CfnException.java @@ -0,0 +1,31 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.aws.cloudformation.schema; + +public class CfnException extends RuntimeException { + + public CfnException(RuntimeException e) { + super(e); + } + + public CfnException(String message) { + super(message); + } + + public CfnException(String message, Throwable previous) { + super(message, previous); + } +} diff --git a/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/CfnConverter.java b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/CfnConverter.java new file mode 100644 index 00000000000..774ffd01767 --- /dev/null +++ b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/CfnConverter.java @@ -0,0 +1,369 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.aws.cloudformation.schema.fromsmithy; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.ServiceLoader; +import java.util.Set; +import software.amazon.smithy.aws.cloudformation.schema.CfnConfig; +import software.amazon.smithy.aws.cloudformation.schema.CfnException; +import software.amazon.smithy.aws.cloudformation.schema.model.Property; +import software.amazon.smithy.aws.cloudformation.schema.model.ResourceSchema; +import software.amazon.smithy.aws.cloudformation.traits.CfnNameTrait; +import software.amazon.smithy.aws.cloudformation.traits.CfnResource; +import software.amazon.smithy.aws.cloudformation.traits.CfnResourceIndex; +import software.amazon.smithy.aws.cloudformation.traits.CfnResourceTrait; +import software.amazon.smithy.aws.traits.ServiceTrait; +import software.amazon.smithy.jsonschema.JsonSchemaConverter; +import software.amazon.smithy.jsonschema.JsonSchemaMapper; +import software.amazon.smithy.jsonschema.PropertyNamingStrategy; +import software.amazon.smithy.jsonschema.Schema; +import software.amazon.smithy.jsonschema.SchemaDocument; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.knowledge.TopDownIndex; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.ResourceShape; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.traits.DocumentationTrait; +import software.amazon.smithy.model.traits.StringTrait; +import software.amazon.smithy.utils.ListUtils; +import software.amazon.smithy.utils.StringUtils; + +public final class CfnConverter { + + private ClassLoader classLoader = CfnConverter.class.getClassLoader(); + private CfnConfig config = new CfnConfig(); + private final List extensions = new ArrayList<>(); + + private CfnConverter() {} + + public static CfnConverter create() { + return new CfnConverter(); + } + + /** + * Get the CloudFormation configuration settings. + * + * @return Returns the config object. + */ + public CfnConfig getConfig() { + return config; + } + + /** + * Set the CloudFormation configuration settings. + * + * @param config Config object to set. + * @return Returns the converter. + */ + public CfnConverter config(CfnConfig config) { + this.config = config; + return this; + } + + /** + * Sets a {@link ClassLoader} to use to discover {@link Smithy2CfnExtension} + * service providers through SPI. + * + *

The {@code CfnConverter} will use its own ClassLoader by default. + * + * @param classLoader ClassLoader to use. + * @return Returns the converter. + */ + public CfnConverter classLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + return this; + } + + /** + * Converts resource shapes annotated with the {@code @cfnResource} trait to + * JSON/Node representations of CloudFormation Resource Schemas using the + * given Smithy model. + * + *

The result of this method may differ from the result of calling + * {@link ResourceSchema#toNode()} because this method will pass the Node + * representation of the ResourceSchema through the {@link CfnMapper#updateNode} + * method of each registered {@link CfnMapper}. + * + * @param model Smithy model to convert. + * @return A map of CloudFormation resource type names to their converted schema nodes. + */ + public Map convertToNodes(Model model) { + List environments = createConversionEnvironments(model); + Map resources = convertWithEnvironments(environments); + + Map convertedNodes = new HashMap<>(); + for (ConversionEnvironment environment : environments) { + ResourceSchema resourceSchema = resources.get(environment.context.getResource().getId()); + ObjectNode node = resourceSchema.toNode().expectObjectNode(); + + // Apply all the mappers' updateNode methods. + for (CfnMapper mapper : environment.mappers) { + node = mapper.updateNode(environment.context, resourceSchema, node); + } + + // CloudFormation resource schemas require the presence of a top-level + // additionalProperties setting with the value of false to be validated. + node = node.withMember("additionalProperties", false); + + convertedNodes.put(resourceSchema.getTypeName(), node); + } + return convertedNodes; + } + + /** + * Converts the annotated resources in the Smithy model to CloudFormation + * Resource Schemas. + * + * @param model Smithy model containing resources to convert. + * @return Returns the converted resources. + */ + public List convert(Model model) { + return ListUtils.copyOf(convertWithEnvironments(createConversionEnvironments(model)).values()); + } + + private Map convertWithEnvironments(List environments) { + Map resourceSchemas = new HashMap<>(); + for (ConversionEnvironment environment : environments) { + ResourceShape resourceShape = environment.context.getResource(); + ResourceSchema resourceSchema = convertResource(environment, resourceShape); + resourceSchemas.put(resourceShape.getId(), resourceSchema); + } + return resourceSchemas; + } + + private List createConversionEnvironments(Model model) { + ShapeId serviceShapeId = config.getService(); + + if (serviceShapeId == null) { + throw new CfnException("cloudformation is missing required property, `service`"); + } + + // Load the Smithy2Cfn extensions. + ServiceLoader.load(Smithy2CfnExtension.class, classLoader).forEach(extensions::add); + + // Find the service shape. + ServiceShape serviceShape = model.expectShape(serviceShapeId, ServiceShape.class); + + TopDownIndex topDownIndex = TopDownIndex.of(model); + Set resourceShapes = topDownIndex.getContainedResources(serviceShape); + + // Create an environment for each of the resources to be converted with. + List environments = new ArrayList<>(); + for (ResourceShape resourceShape : resourceShapes) { + if (resourceShape.getTrait(CfnResourceTrait.class).isPresent()) { + ConversionEnvironment environment = createConversionEnvironment(model, serviceShape, resourceShape); + environments.add(environment); + } + } + + return environments; + } + + private ConversionEnvironment createConversionEnvironment( + Model model, + ServiceShape serviceShape, + ResourceShape resourceShape + ) { + // Prepare the JSON Schema Converter. + JsonSchemaConverter.Builder jsonSchemaConverterBuilder = JsonSchemaConverter.builder() + .config(config) + .propertyNamingStrategy(getPropertyNamingStrategy()); + + List mappers = new ArrayList<>(); + for (Smithy2CfnExtension extension : extensions) { + mappers.addAll(extension.getCfnMappers()); + // Add JSON schema mappers from found extensions. + for (JsonSchemaMapper mapper : extension.getJsonSchemaMappers()) { + jsonSchemaConverterBuilder.addMapper(mapper); + } + } + mappers.sort(Comparator.comparingInt(CfnMapper::getOrder)); + + CfnResourceIndex resourceIndex = CfnResourceIndex.of(model); + CfnResource cfnResource = resourceIndex.getResource(resourceShape) + .orElseThrow(() -> new CfnException("Attempted to generate a CloudFormation resource schema " + + "not found to have resource data.")); + + // Prepare a structure representing the CFN resource to be created and + // add that structure to a temporary model that's used for conversion. + // JSON Schema conversion requires that the shape being converted is + // present in the model. See the docs for getCfnResourceStructure for + // more information. + StructureShape pseudoResource = getCfnResourceStructure(model, resourceShape, cfnResource); + Model updatedModel = model.toBuilder().addShape(pseudoResource).build(); + jsonSchemaConverterBuilder.model(updatedModel); + + Context context = new Context(updatedModel, serviceShape, resourceShape, cfnResource, + pseudoResource, config, jsonSchemaConverterBuilder.build()); + + return new ConversionEnvironment(context, mappers); + } + + private PropertyNamingStrategy getPropertyNamingStrategy() { + return (containingShape, member, config) -> { + // The cfnName trait's value takes precedence, even over any settings. + Optional trait = member.getTrait(CfnNameTrait.class); + if (trait.isPresent()) { + return trait.get().getValue(); + } + + // Otherwise, respect the property capitalization setting. + String name = PropertyNamingStrategy.createMemberNameStrategy() + .toPropertyName(containingShape, member, config); + + return this.config.getDisableCapitalizedProperties() + ? name + : StringUtils.capitalize(name); + }; + } + + private static final class ConversionEnvironment { + private final Context context; + private final List mappers; + + private ConversionEnvironment( + Context context, + List mappers + ) { + this.context = context; + this.mappers = mappers; + } + } + + private ResourceSchema convertResource(ConversionEnvironment environment, ResourceShape resourceShape) { + Context context = environment.context; + JsonSchemaConverter jsonSchemaConverter = context.getJsonSchemaConverter().toBuilder() + .rootShape(context.getResourceStructure()) + .build(); + SchemaDocument document = jsonSchemaConverter.convert(); + + // Prepare the initial contents + CfnResourceTrait resourceTrait = resourceShape.expectTrait(CfnResourceTrait.class); + ResourceSchema.Builder builder = ResourceSchema.builder(); + String typeName = resolveResourceTypeName(environment, resourceTrait); + builder.typeName(typeName); + + // Apply the resource's documentation if present, or default. + builder.description(resourceShape.getTrait(DocumentationTrait.class) + .map(StringTrait::getValue) + .orElse("Definition of " + typeName + " Resource Type")); + + // Apply all the mappers' before methods. + for (CfnMapper mapper : environment.mappers) { + mapper.before(context, builder); + } + + // Add the properties from the converted shape. + document.getRootSchema().getProperties().forEach((name, schema) -> { + Property property = Property.builder() + .schema(schema) + .build(); + builder.addProperty(name, property); + }); + + // Supply all the definitions that were created. + for (Map.Entry definition : document.getDefinitions().entrySet()) { + String definitionName = definition.getKey() + .replace(CfnConfig.SCHEMA_COMPONENTS_POINTER, "") + .substring(1); + builder.addDefinition(definitionName, definition.getValue()); + } + + // Apply all the mappers' after methods. + ResourceSchema resourceSchema = builder.build(); + for (CfnMapper mapper : environment.mappers) { + resourceSchema = mapper.after(context, resourceSchema); + } + + return resourceSchema; + } + + private String resolveResourceTypeName( + ConversionEnvironment environment, + CfnResourceTrait resourceTrait + ) { + CfnConfig config = environment.context.getConfig(); + ServiceShape serviceShape = environment.context.getModel().expectShape(config.getService(), ServiceShape.class); + Optional serviceTrait = serviceShape.getTrait(ServiceTrait.class); + + String organizationName = config.getOrganizationName(); + if (organizationName == null) { + // Services utilizing the AWS service trait default to being in the + // "AWS" organization instead of requiring the configuration value. + organizationName = serviceTrait + .map(t -> "AWS") + .orElseThrow(() -> + new CfnException("cloudformation is missing required property, `organizationName`")); + } + + String serviceName = config.getServiceName(); + if (serviceName == null) { + // Services utilizing the AWS service trait have the `cloudFormationName` + // member, so use that if present. Otherwise, default to the service + // shape's name. + serviceName = serviceTrait + .map(ServiceTrait::getCloudFormationName) + .orElse(serviceShape.getId().getName()); + } + + // Use the trait's name if present, or default to the resource shape's name. + String resourceName = resourceTrait.getName() + .orElse(environment.context.getResource().getId().getName()); + + return String.format("%s::%s::%s", organizationName, serviceName, resourceName); + } + + /* + * JSON Schema conversion requires that the shape being converted is present + * in the model. Since the properties of a CloudFormation resource are derived + * from multiple locations, these properties need to be added to a single + * StructureShape that can be added to a model for converting. + * + * To do so, the derived properties of a CloudFormation resource are added + * to a synthetic structure. Members are reparented and identifiers are + * added as new members. + */ + private StructureShape getCfnResourceStructure(Model model, ResourceShape resource, CfnResource cfnResource) { + StructureShape.Builder builder = StructureShape.builder(); + ShapeId resourceId = resource.getId(); + builder.id(ShapeId.fromParts(resourceId.getNamespace(), resourceId.getName() + "__SYNTHETIC__")); + + cfnResource.getProperties().forEach((name, definition) -> { + Shape definitionShape = model.expectShape(definition.getShapeId()); + // We got a member that's pulled in, so reparent it. + if (definitionShape.isMemberShape()) { + MemberShape member = definitionShape.asMemberShape().get(); + // Adjust the ID of the member. + member = member.toBuilder().id(builder.getId().withMember(name)).build(); + builder.addMember(member); + } else { + // This is an identifier, create a new member. + builder.addMember(name, definition.getShapeId()); + } + }); + return builder.build(); + } +} diff --git a/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/CfnMapper.java b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/CfnMapper.java new file mode 100644 index 00000000000..c874f17e460 --- /dev/null +++ b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/CfnMapper.java @@ -0,0 +1,75 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.aws.cloudformation.schema.fromsmithy; + +import software.amazon.smithy.aws.cloudformation.schema.model.ResourceSchema; +import software.amazon.smithy.model.node.ObjectNode; + +/** + * Provides a plugin infrastructure to hook into the Smithy CloudFormation + * Resource Schema generation process and map over the result. + * + *

The methods of a plugin are invoked by {@link CfnConverter} during + * Resource Schema generation. There is no need to invoke these manually. + * Implementations may choose to leverage configuration options of the + * provided context to determine whether or not to enact the plugin. + */ +public interface CfnMapper { + /** + * Gets the sort order of the plugin from -128 to 127. + * + *

Plugins are applied according to this sort order. Lower values + * are executed before higher values (for example, -128 comes before 0, + * comes before 127). Plugins default to 0, which is the middle point + * between the minimum and maximum order values. + * + * @return Returns the sort order, defaulting to 0. + */ + default byte getOrder() { + return 0; + } + + /** + * Updates an ResourceSchema.Builder before converting the model. + * + * @param context Conversion context. + * @param builder ResourceSchema builder to modify. + */ + default void before(Context context, ResourceSchema.Builder builder) {} + + /** + * Updates an ResourceSchema.Builder after converting the model. + * + * @param context Conversion context. + * @param resourceSchema ResourceSchema to modify. + * @return Returns the updated ResourceSchema object. + */ + default ResourceSchema after(Context context, ResourceSchema resourceSchema) { + return resourceSchema; + } + + /** + * Modifies the Node/JSON representation of a ResourceSchema object. + * + * @param context Conversion context. + * @param resourceSchema ResourceSchema being converted to a node. + * @param node ResourceSchema object node. + * @return Returns the updated ObjectNode. + */ + default ObjectNode updateNode(Context context, ResourceSchema resourceSchema, ObjectNode node) { + return node; + } +} diff --git a/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/Context.java b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/Context.java new file mode 100644 index 00000000000..395cfb8422c --- /dev/null +++ b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/Context.java @@ -0,0 +1,135 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.aws.cloudformation.schema.fromsmithy; + +import software.amazon.smithy.aws.cloudformation.schema.CfnConfig; +import software.amazon.smithy.aws.cloudformation.traits.CfnResource; +import software.amazon.smithy.jsonschema.JsonSchemaConverter; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.ResourceShape; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.StructureShape; + +/** + * Smithy to CloudFormation conversion context object. + * + *

One context is used per CloudFormation resource generated. + */ +public final class Context { + + private final Model model; + private final ServiceShape service; + private final ResourceShape resource; + private final CfnResource cfnResource; + private final StructureShape resourceStructure; + private final JsonSchemaConverter jsonSchemaConverter; + private final CfnConfig config; + + Context( + Model model, + ServiceShape service, + ResourceShape resource, + CfnResource cfnResource, + StructureShape resourceStructure, + CfnConfig config, + JsonSchemaConverter jsonSchemaConverter + ) { + this.model = model; + this.service = service; + this.resource = resource; + this.cfnResource = cfnResource; + this.resourceStructure = resourceStructure; + this.config = config; + this.jsonSchemaConverter = jsonSchemaConverter; + } + + /** + * Gets the Smithy model being converted. + * + * @return Returns the Smithy model. + */ + public Model getModel() { + return model; + } + + /** + * Gets the service shape containing the resource being converted. + * + * @return Returns the service shape. + */ + public ServiceShape getService() { + return service; + } + + /** + * Gets the resource shape being converted. + * + * @return Returns the resource shape. + */ + public ResourceShape getResource() { + return resource; + } + + /** + * Gets the {@link CfnResource} index data for this resource. + * + * @return Returns the CfnResource index data. + */ + public CfnResource getCfnResource() { + return cfnResource; + } + + /** + * Gets the structure shape that represents the consolidated properties of the resource. + * + * @return Returns the structure shape. + */ + public StructureShape getResourceStructure() { + return resourceStructure; + } + + /** + * Gets the configuration object used for the conversion. + * + *

Plugins can query this object for configuration values. + * + * @return Returns the configuration object. + */ + public CfnConfig getConfig() { + return config; + } + + /** + * Gets the JSON schema converter. + * + * @return Returns the JSON Schema converter. + */ + public JsonSchemaConverter getJsonSchemaConverter() { + return jsonSchemaConverter; + } + + /** + * Gets the JSON pointer string to a specific property. + * + * @param propertyName Property name to build a JSON pointer to. + * @return Returns the JSON pointer to the property. + */ + public String getPropertyPointer(String propertyName) { + MemberShape member = resourceStructure.getMember(propertyName).get(); + return "/properties/" + getJsonSchemaConverter().toPropertyName(member); + } +} diff --git a/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/Smithy2Cfn.java b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/Smithy2Cfn.java new file mode 100644 index 00000000000..a2088a7977d --- /dev/null +++ b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/Smithy2Cfn.java @@ -0,0 +1,50 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.aws.cloudformation.schema.fromsmithy; + +import java.util.Locale; +import java.util.Map; +import software.amazon.smithy.aws.cloudformation.schema.CfnConfig; +import software.amazon.smithy.build.PluginContext; +import software.amazon.smithy.build.SmithyBuildPlugin; +import software.amazon.smithy.model.node.ObjectNode; + +public final class Smithy2Cfn implements SmithyBuildPlugin { + @Override + public String getName() { + return "cloudformation"; + } + + @Override + public void execute(PluginContext context) { + CfnConverter converter = CfnConverter.create(); + context.getPluginClassLoader().ifPresent(converter::classLoader); + CfnConfig config = CfnConfig.fromNode(context.getSettings()); + converter.config(config); + + Map resourceNodes = converter.convertToNodes(context.getModel()); + for (Map.Entry resourceNode : resourceNodes.entrySet()) { + String filename = getFileNameFromResourceType(resourceNode.getKey()); + context.getFileManifest().writeJson( + filename, + resourceNode.getValue()); + } + } + + static String getFileNameFromResourceType(String resourceType) { + return resourceType.toLowerCase(Locale.US).replace("::", "-") + ".json"; + } +} diff --git a/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/Smithy2CfnExtension.java b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/Smithy2CfnExtension.java new file mode 100644 index 00000000000..9ab054013e4 --- /dev/null +++ b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/Smithy2CfnExtension.java @@ -0,0 +1,49 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.aws.cloudformation.schema.fromsmithy; + +import java.util.List; +import software.amazon.smithy.jsonschema.JsonSchemaMapper; +import software.amazon.smithy.utils.ListUtils; + +/** + * An extension mechanism used to influence how CloudFormation resource schemas + * are generated from Smithy models. + * + *

Implementations of this interface are discovered through Java SPI. + */ +public interface Smithy2CfnExtension { + + /** + * Registers CloudFormation mappers, classes used to modify and extend the + * process of converting a Smithy model to CloudFormation resource schemas. + * + * @return Returns the mappers to register. + */ + default List getCfnMappers() { + return ListUtils.of(); + } + + /** + * Registers JsonSchema mappers that are used to modify JsonSchema + * definitions created from a Smithy model. + * + * @return Returns the mappers to register. + */ + default List getJsonSchemaMappers() { + return ListUtils.of(); + } +} diff --git a/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/CoreExtension.java b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/CoreExtension.java new file mode 100644 index 00000000000..4bc437ff22f --- /dev/null +++ b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/CoreExtension.java @@ -0,0 +1,36 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.aws.cloudformation.schema.fromsmithy.mappers; + +import java.util.List; +import software.amazon.smithy.aws.cloudformation.schema.fromsmithy.CfnMapper; +import software.amazon.smithy.aws.cloudformation.schema.fromsmithy.Smithy2CfnExtension; +import software.amazon.smithy.utils.ListUtils; + +/** + * Registers the core Smithy2CloudFormation functionality. + */ +public final class CoreExtension implements Smithy2CfnExtension { + @Override + public List getCfnMappers() { + return ListUtils.of( + new DeprecatedMapper(), + new DocumentationMapper(), + new IdentifierMapper(), + new JsonAddMapper(), + new MutabilityMapper()); + } +} diff --git a/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/DeprecatedMapper.java b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/DeprecatedMapper.java new file mode 100644 index 00000000000..c7a393f5965 --- /dev/null +++ b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/DeprecatedMapper.java @@ -0,0 +1,52 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.aws.cloudformation.schema.fromsmithy.mappers; + +import software.amazon.smithy.aws.cloudformation.schema.fromsmithy.CfnMapper; +import software.amazon.smithy.aws.cloudformation.schema.fromsmithy.Context; +import software.amazon.smithy.aws.cloudformation.schema.model.ResourceSchema.Builder; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.traits.DeprecatedTrait; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Generates the resource's deprecated properties list based on the + * deprecated operation members that are part of the derived resource. + * + * @see deprecatedProperties Docs + */ +@SmithyInternalApi +final class DeprecatedMapper implements CfnMapper { + + @Override + public void before(Context context, Builder resourceSchema) { + if (context.getConfig().getDisableDeprecatedPropertyGeneration()) { + return; + } + + // If any of the pseudo-resource structure's members are deprecated, + // then deprecate the CFN property as well. + Model model = context.getModel(); + StructureShape resourceStructure = context.getResourceStructure(); + for (MemberShape member : resourceStructure.members()) { + if (member.getMemberTrait(model, DeprecatedTrait.class).isPresent()) { + resourceSchema.addDeprecatedProperty(context.getPropertyPointer(member.getMemberName())); + } + } + } +} diff --git a/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/DocumentationMapper.java b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/DocumentationMapper.java new file mode 100644 index 00000000000..f031a730a7f --- /dev/null +++ b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/DocumentationMapper.java @@ -0,0 +1,93 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.aws.cloudformation.schema.fromsmithy.mappers; + +import static java.util.function.Function.identity; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import software.amazon.smithy.aws.cloudformation.schema.CfnConfig; +import software.amazon.smithy.aws.cloudformation.schema.fromsmithy.CfnMapper; +import software.amazon.smithy.aws.cloudformation.schema.fromsmithy.Context; +import software.amazon.smithy.aws.cloudformation.schema.model.ResourceSchema; +import software.amazon.smithy.model.shapes.ResourceShape; +import software.amazon.smithy.model.traits.ExternalDocumentationTrait; +import software.amazon.smithy.utils.MapUtils; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Generates the schema doc urls based on the resource's {@code @externalDocumentation} + * trait. This is configurable based on the {@code "sourceDocKeys"} and + * {@code "externalDocKeys"} plugin properties. + * + * @see sourceUrl Docs + * @see documentationUrl Docs + */ +@SmithyInternalApi +final class DocumentationMapper implements CfnMapper { + + @Override + public void before(Context context, ResourceSchema.Builder builder) { + ResourceShape resource = context.getResource(); + ExternalDocumentationTrait trait = resource.getTrait(ExternalDocumentationTrait.class).orElse(null); + + if (trait == null) { + return; + } + + CfnConfig config = context.getConfig(); + + getResolvedExternalDocs(trait, config.getSourceDocs()).ifPresent(builder::sourceUrl); + getResolvedExternalDocs(trait, config.getExternalDocs()).ifPresent(builder::documentationUrl); + } + + private Optional getResolvedExternalDocs(ExternalDocumentationTrait trait, List enabledKeys) { + // Get the valid list of lower case names to look for when converting. + List externalDocKeys = listToLowerCase(enabledKeys); + + // Get lower case keys to check for when converting. + Map traitUrls = trait.getUrls(); + Map lowercaseKeyMap = traitUrls.keySet().stream() + .collect(MapUtils.toUnmodifiableMap(this::toLowerCase, identity())); + + for (String externalDocKey : externalDocKeys) { + // Compare the lower case name, but use the specified name. + if (lowercaseKeyMap.containsKey(externalDocKey)) { + String traitKey = lowercaseKeyMap.get(externalDocKey); + // Return the url from the trait. + return Optional.of(traitUrls.get(traitKey)); + } + } + + // We didn't find any external docs with the a name in the specified set. + return Optional.empty(); + } + + private List listToLowerCase(List inputs) { + List outputs = new ArrayList<>(inputs.size()); + for (String input : inputs) { + outputs.add(toLowerCase(input)); + } + return outputs; + } + + private String toLowerCase(String input) { + return input.toLowerCase(Locale.US); + } +} diff --git a/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/IdentifierMapper.java b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/IdentifierMapper.java new file mode 100644 index 00000000000..01229000d2f --- /dev/null +++ b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/IdentifierMapper.java @@ -0,0 +1,60 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.aws.cloudformation.schema.fromsmithy.mappers; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import software.amazon.smithy.aws.cloudformation.schema.fromsmithy.CfnMapper; +import software.amazon.smithy.aws.cloudformation.schema.fromsmithy.Context; +import software.amazon.smithy.aws.cloudformation.schema.model.ResourceSchema.Builder; +import software.amazon.smithy.aws.cloudformation.traits.CfnResource; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Applies the resource's identifier and annotated additional identifiers + * to the resulting resource schema. + * + * @see primaryIdentifier Docs + * @see additionalIdentifiers Docs + */ +@SmithyInternalApi +final class IdentifierMapper implements CfnMapper { + + @Override + public void before(Context context, Builder builder) { + CfnResource cfnResource = context.getCfnResource(); + + // Add the primary identifier. + Set primaryIdentifier = cfnResource.getPrimaryIdentifiers(); + builder.primaryIdentifier(primaryIdentifier.stream() + .map(context::getPropertyPointer) + .collect(Collectors.toList())); + + // Add any additional identifiers. + List> additionalIdentifiers = cfnResource.getAdditionalIdentifiers(); + for (Set additionalIdentifier : additionalIdentifiers) { + // Convert the names into their property pointer. + List additionalIdentifierPointers = new ArrayList<>(); + for (String additionalIdentifierName : additionalIdentifier) { + additionalIdentifierPointers.add(context.getPropertyPointer(additionalIdentifierName)); + } + + builder.addAdditionalIdentifier(additionalIdentifierPointers); + } + } +} diff --git a/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/JsonAddMapper.java b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/JsonAddMapper.java new file mode 100644 index 00000000000..5c2e1bf184a --- /dev/null +++ b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/JsonAddMapper.java @@ -0,0 +1,73 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.aws.cloudformation.schema.fromsmithy.mappers; + +import java.util.Map; +import java.util.logging.Logger; +import software.amazon.smithy.aws.cloudformation.schema.CfnException; +import software.amazon.smithy.aws.cloudformation.schema.fromsmithy.CfnMapper; +import software.amazon.smithy.aws.cloudformation.schema.fromsmithy.Context; +import software.amazon.smithy.aws.cloudformation.schema.model.ResourceSchema; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.NodePointer; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Adds JSON values into the generated CloudFormation resource schemas using + * a JSON Patch like "add" operation that also generated intermediate objects + * as needed. Any existing property is overwritten. + */ +@SmithyInternalApi +final class JsonAddMapper implements CfnMapper { + + private static final Logger LOGGER = Logger.getLogger(JsonAddMapper.class.getName()); + + @Override + public byte getOrder() { + // After DisableMapper in the JSON Schema conversion, with a + // small buffer to allow for intermediate mappers. + return 124; + } + + @Override + public ObjectNode updateNode(Context context, ResourceSchema resourceSchema, ObjectNode node) { + ShapeId resourceShapeId = context.getResource().getId(); + Map> add = context.getConfig().getJsonAdd(); + + // Short circuit if we don't have anything to add for this resource. + if (add.isEmpty() || !add.containsKey(resourceShapeId)) { + return node; + } + + // Apply the set of pointers for this resource. + ObjectNode result = node; + for (Map.Entry entry : add.get(resourceShapeId).entrySet()) { + try { + LOGGER.info(() -> String.format("CloudFormation `jsonAdd` for `%s`: adding `%s`", + resourceShapeId, entry.getKey())); + result = NodePointer.parse(entry.getKey()) + .addWithIntermediateValues(result, entry.getValue().toNode()) + .expectObjectNode(); + } catch (IllegalArgumentException e) { + throw new CfnException(e.getMessage(), e); + } + } + + return result; + } +} diff --git a/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/MutabilityMapper.java b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/MutabilityMapper.java new file mode 100644 index 00000000000..765b91191fc --- /dev/null +++ b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/MutabilityMapper.java @@ -0,0 +1,54 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.aws.cloudformation.schema.fromsmithy.mappers; + +import software.amazon.smithy.aws.cloudformation.schema.fromsmithy.CfnMapper; +import software.amazon.smithy.aws.cloudformation.schema.fromsmithy.Context; +import software.amazon.smithy.aws.cloudformation.schema.model.ResourceSchema; +import software.amazon.smithy.aws.cloudformation.traits.CfnResource; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Applies property mutability restrictions to their proper location + * in the resulting resource schema. + * + * @see createOnlyProperties Docs + * @see readOnlyProperties Docs + * @see writeOnlyProperties Docs + */ +@SmithyInternalApi +final class MutabilityMapper implements CfnMapper { + + @Override + public void before(Context context, ResourceSchema.Builder builder) { + CfnResource cfnResource = context.getCfnResource(); + + // Add any createOnlyProperty entries, if present. + cfnResource.getCreateOnlyProperties().stream() + .map(context::getPropertyPointer) + .forEach(builder::addCreateOnlyProperty); + + // Add any readOnlyProperty entries, if present. + cfnResource.getReadOnlyProperties().stream() + .map(context::getPropertyPointer) + .forEach(builder::addReadOnlyProperty); + + // Add any writeOnlyProperty entries, if present. + cfnResource.getWriteOnlyProperties().stream() + .map(context::getPropertyPointer) + .forEach(builder::addWriteOnlyProperty); + } +} diff --git a/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/Handler.java b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/Handler.java new file mode 100644 index 00000000000..53bf516ead6 --- /dev/null +++ b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/Handler.java @@ -0,0 +1,106 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.aws.cloudformation.schema.model; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.NodeMapper; +import software.amazon.smithy.model.node.ToNode; +import software.amazon.smithy.utils.ListUtils; +import software.amazon.smithy.utils.MapUtils; +import software.amazon.smithy.utils.SmithyBuilder; +import software.amazon.smithy.utils.ToSmithyBuilder; + +/** + * Data class representing a CloudFormation Resource Schema's handler definition. + * + * @see Resource Handler Definition + * @see Resource Type Handler Definition JSON Schema + */ +public final class Handler implements ToNode, ToSmithyBuilder { + public static final String CREATE = "create"; + public static final String READ = "read"; + public static final String UPDATE = "update"; + public static final String DELETE = "delete"; + public static final String LIST = "list"; + private static final Map HANDLER_NAME_ORDERS = MapUtils.of( + CREATE, 0, + READ, 1, + UPDATE, 2, + DELETE, 3, + LIST, 4); + + private final List permissions; + + private Handler(Builder builder) { + this.permissions = ListUtils.copyOf(builder.permissions); + } + + @Override + public Node toNode() { + NodeMapper mapper = new NodeMapper(); + mapper.disableToNodeForClass(Handler.class); + mapper.setOmitEmptyValues(true); + return mapper.serialize(this).expectObjectNode(); + } + + @Override + public SmithyBuilder toBuilder() { + return builder() + .permissions(permissions); + } + + public static Builder builder() { + return new Builder(); + } + + public List getPermissions() { + return permissions; + } + + public static Integer getHandlerNameOrder(String name) { + return HANDLER_NAME_ORDERS.getOrDefault(name, Integer.MAX_VALUE); + } + + public static final class Builder implements SmithyBuilder { + private final List permissions = new ArrayList<>(); + + private Builder() {} + + @Override + public Handler build() { + return new Handler(this); + } + + public Builder permissions(List permissions) { + this.permissions.clear(); + this.permissions.addAll(permissions); + return this; + } + + public Builder addPermission(String permission) { + this.permissions.add(permission); + return this; + } + + public Builder clearPermissions() { + this.permissions.clear(); + return this; + } + } +} diff --git a/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/Property.java b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/Property.java new file mode 100644 index 00000000000..0fe013bd9db --- /dev/null +++ b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/Property.java @@ -0,0 +1,126 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.aws.cloudformation.schema.model; + +import java.util.ArrayList; +import java.util.List; +import software.amazon.smithy.jsonschema.Schema; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.node.ToNode; +import software.amazon.smithy.utils.ListUtils; +import software.amazon.smithy.utils.SmithyBuilder; +import software.amazon.smithy.utils.ToSmithyBuilder; + +/** + * Data class representing a CloudFormation Resource Schema's property. + * + * @see Resource Properties Definition + * @see Resource Type Properties JSON Schema + */ +public final class Property implements ToNode, ToSmithyBuilder { + private final boolean insertionOrder; + private final List dependencies; + private final Schema schema; + // Other reserved property names in definition but not in the validation + // JSON Schema, so not defined in code: + // * readOnly + // * writeOnly + + private Property(Builder builder) { + this.insertionOrder = builder.insertionOrder; + this.dependencies = ListUtils.copyOf(builder.dependencies); + this.schema = builder.schema; + } + + @Override + public Node toNode() { + ObjectNode.Builder builder = schema.toNode().expectObjectNode().toBuilder(); + + // Only serialize these properties if set to non-defaults. + if (insertionOrder) { + builder.withMember("insertionOrder", Node.from(insertionOrder)); + } + if (!dependencies.isEmpty()) { + builder.withMember("dependencies", Node.fromStrings(dependencies)); + } + + return builder.build(); + } + + @Override + public SmithyBuilder toBuilder() { + return builder() + .insertionOrder(insertionOrder) + .dependencies(dependencies) + .schema(schema); + } + + public static Builder builder() { + return new Builder(); + } + + public boolean isInsertionOrder() { + return insertionOrder; + } + + public List getDependencies() { + return dependencies; + } + + public Schema getSchema() { + return schema; + } + + public static final class Builder implements SmithyBuilder { + private boolean insertionOrder = false; + private final List dependencies = new ArrayList<>(); + private Schema schema; + + private Builder() {} + + @Override + public Property build() { + return new Property(this); + } + + public Builder insertionOrder(boolean insertionOrder) { + this.insertionOrder = insertionOrder; + return this; + } + + public Builder dependencies(List dependencies) { + this.dependencies.clear(); + this.dependencies.addAll(dependencies); + return this; + } + + public Builder addDependency(String dependency) { + this.dependencies.add(dependency); + return this; + } + + public Builder clearDependencies() { + this.dependencies.clear(); + return this; + } + + public Builder schema(Schema schema) { + this.schema = schema; + return this; + } + } +} diff --git a/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/Remote.java b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/Remote.java new file mode 100644 index 00000000000..5c8c00a5c57 --- /dev/null +++ b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/Remote.java @@ -0,0 +1,131 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.aws.cloudformation.schema.model; + +import java.util.Map; +import java.util.TreeMap; +import software.amazon.smithy.jsonschema.Schema; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.NodeMapper; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.node.ToNode; +import software.amazon.smithy.utils.SmithyBuilder; +import software.amazon.smithy.utils.ToSmithyBuilder; + +/** + * Data class representing a CloudFormation Resource Schema's remote definition. + * + * @see Resource Handler Definition + * @see Resource Type Remote JSON Schema + */ +public final class Remote implements ToNode, ToSmithyBuilder { + private final Map definitions = new TreeMap<>(); + private final Map properties = new TreeMap<>(); + + private Remote(Builder builder) { + properties.putAll(builder.properties); + definitions.putAll(builder.definitions); + } + + @Override + public Node toNode() { + NodeMapper mapper = new NodeMapper(); + ObjectNode.Builder builder = Node.objectNodeBuilder(); + + if (!definitions.isEmpty()) { + builder.withMember("definitions", mapper.serialize(definitions)); + } + + if (!properties.isEmpty()) { + builder.withMember("properties", mapper.serialize(properties)); + } + + return builder.build(); + } + + @Override + public Builder toBuilder() { + return builder() + .definitions(definitions) + .properties(properties); + } + + public static Builder builder() { + return new Builder(); + } + + public Map getDefinitions() { + return definitions; + } + + public Map getProperties() { + return properties; + } + + public static final class Builder implements SmithyBuilder { + private final Map definitions = new TreeMap<>(); + private final Map properties = new TreeMap<>(); + + private Builder() {} + + @Override + public Remote build() { + return new Remote(this); + } + + public Builder definitions(Map definitions) { + this.definitions.clear(); + this.definitions.putAll(definitions); + return this; + } + + public Builder addDefinition(String name, Schema definition) { + this.definitions.put(name, definition); + return this; + } + + public Builder removeDefinition(String name) { + this.definitions.remove(name); + return this; + } + + public Builder clearDefinitions() { + this.definitions.clear(); + return this; + } + + public Builder properties(Map properties) { + this.properties.clear(); + this.properties.putAll(properties); + return this; + } + + public Builder addProperty(String name, Property property) { + this.properties.put(name, property); + return this; + } + + public Builder removeProperty(String name) { + this.properties.remove(name); + return this; + } + + public Builder clearProperties() { + this.properties.clear(); + return this; + } + } +} diff --git a/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/ResourceSchema.java b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/ResourceSchema.java new file mode 100644 index 00000000000..4bc1d9e19f0 --- /dev/null +++ b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/ResourceSchema.java @@ -0,0 +1,430 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.aws.cloudformation.schema.model; + +import static java.lang.String.format; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import software.amazon.smithy.aws.cloudformation.schema.CfnException; +import software.amazon.smithy.jsonschema.Schema; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.NodeMapper; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.node.ToNode; +import software.amazon.smithy.utils.ListUtils; +import software.amazon.smithy.utils.SmithyBuilder; +import software.amazon.smithy.utils.ToSmithyBuilder; + +/** + * Data class representing a CloudFormation Resource Schema. + * + * @see Resource Type Schema + * @see Resource Type JSON Schema + */ +public final class ResourceSchema implements ToNode, ToSmithyBuilder { + private final String typeName; + private final String description; + private final String sourceUrl; + private final String documentationUrl; + private final Map definitions = new TreeMap<>(); + private final Map properties = new TreeMap<>(); + private final Set readOnlyProperties = new TreeSet<>(); + private final Set writeOnlyProperties = new TreeSet<>(); + private final Set primaryIdentifier = new TreeSet<>(); + private final Set createOnlyProperties = new TreeSet<>(); + private final Set deprecatedProperties = new TreeSet<>(); + private final List> additionalIdentifiers; + // Use a custom comparator to keep the Handler outputs in CRUDL order. + private final Map handlers = new TreeMap<>(Comparator.comparing(Handler::getHandlerNameOrder)); + private final Map remotes = new TreeMap<>(); + + private ResourceSchema(Builder builder) { + typeName = SmithyBuilder.requiredState("typeName", builder.typeName); + description = SmithyBuilder.requiredState("description", builder.description); + + if (builder.properties.isEmpty()) { + throw new CfnException(format("Expected CloudFormation resource %s to have properties, " + + "found none", typeName)); + } + properties.putAll(builder.properties); + + sourceUrl = builder.sourceUrl; + documentationUrl = builder.documentationUrl; + definitions.putAll(builder.definitions); + readOnlyProperties.addAll(builder.readOnlyProperties); + writeOnlyProperties.addAll(builder.writeOnlyProperties); + primaryIdentifier.addAll(builder.primaryIdentifier); + createOnlyProperties.addAll(builder.createOnlyProperties); + deprecatedProperties.addAll(builder.deprecatedProperties); + additionalIdentifiers = ListUtils.copyOf(builder.additionalIdentifiers); + handlers.putAll(builder.handlers); + remotes.putAll(builder.remotes); + } + + @Override + public Node toNode() { + NodeMapper mapper = new NodeMapper(); + ObjectNode.Builder builder = Node.objectNodeBuilder(); + + // This ordering is hand maintained to produce a similar output + // to those of the resource schemas in CloudFormation documentation, + // as the NodeMapper does not have a mechanism to order members. + builder.withMember("typeName", typeName); + builder.withMember("description", description); + + getSourceUrl().ifPresent(sourceUrl -> builder.withMember("sourceUrl", sourceUrl)); + getDocumentationUrl().ifPresent(documentationUrl -> builder.withMember("documentationUrl", documentationUrl)); + + if (!definitions.isEmpty()) { + builder.withMember("definitions", mapper.serialize(definitions)); + } + + builder.withMember("properties", mapper.serialize(properties)); + + if (!readOnlyProperties.isEmpty()) { + builder.withMember("readOnlyProperties", mapper.serialize(readOnlyProperties)); + } + if (!writeOnlyProperties.isEmpty()) { + builder.withMember("writeOnlyProperties", mapper.serialize(writeOnlyProperties)); + } + if (!createOnlyProperties.isEmpty()) { + builder.withMember("createOnlyProperties", mapper.serialize(createOnlyProperties)); + } + if (!deprecatedProperties.isEmpty()) { + builder.withMember("deprecatedProperties", mapper.serialize(deprecatedProperties)); + } + if (!primaryIdentifier.isEmpty()) { + builder.withMember("primaryIdentifier", mapper.serialize(primaryIdentifier)); + } + if (!additionalIdentifiers.isEmpty()) { + builder.withMember("additionalIdentifiers", mapper.serialize(additionalIdentifiers)); + } + if (!handlers.isEmpty()) { + builder.withMember("handlers", mapper.serialize(handlers)); + } + if (!remotes.isEmpty()) { + builder.withMember("remotes", mapper.serialize(remotes)); + } + + return builder.build(); + } + + @Override + public Builder toBuilder() { + return builder() + .typeName(typeName) + .description(description) + .sourceUrl(sourceUrl) + .documentationUrl(documentationUrl) + .definitions(definitions) + .properties(properties) + .readOnlyProperties(readOnlyProperties) + .writeOnlyProperties(writeOnlyProperties) + .primaryIdentifier(primaryIdentifier) + .createOnlyProperties(createOnlyProperties) + .deprecatedProperties(deprecatedProperties) + .additionalIdentifiers(additionalIdentifiers) + .handlers(handlers) + .remotes(remotes); + } + + public static Builder builder() { + return new Builder(); + } + + public String getTypeName() { + return typeName; + } + + public String getDescription() { + return description; + } + + public Optional getSourceUrl() { + return Optional.ofNullable(sourceUrl); + } + + public Optional getDocumentationUrl() { + return Optional.ofNullable(documentationUrl); + } + + public Map getDefinitions() { + return definitions; + } + + public Map getProperties() { + return properties; + } + + public Set getReadOnlyProperties() { + return readOnlyProperties; + } + + public Set getWriteOnlyProperties() { + return writeOnlyProperties; + } + + public Set getPrimaryIdentifier() { + return primaryIdentifier; + } + + public Set getCreateOnlyProperties() { + return createOnlyProperties; + } + + public Set getDeprecatedProperties() { + return deprecatedProperties; + } + + public List> getAdditionalIdentifiers() { + return additionalIdentifiers; + } + + public Map getHandlers() { + return handlers; + } + + public Map getRemotes() { + return remotes; + } + + public static final class Builder implements SmithyBuilder { + private String typeName; + private String description; + private String sourceUrl; + private String documentationUrl; + private final Map definitions = new TreeMap<>(); + private final Map properties = new TreeMap<>(); + private final Set readOnlyProperties = new TreeSet<>(); + private final Set writeOnlyProperties = new TreeSet<>(); + private final Set primaryIdentifier = new TreeSet<>(); + private final Set createOnlyProperties = new TreeSet<>(); + private final Set deprecatedProperties = new TreeSet<>(); + private final List> additionalIdentifiers = new ArrayList<>(); + private final Map handlers = new TreeMap<>(); + private final Map remotes = new TreeMap<>(); + + private Builder() {} + + @Override + public ResourceSchema build() { + return new ResourceSchema(this); + } + + public Builder typeName(String typeName) { + this.typeName = typeName; + return this; + } + + public Builder description(String description) { + this.description = description; + return this; + } + + public Builder sourceUrl(String sourceUrl) { + this.sourceUrl = sourceUrl; + return this; + } + + public Builder documentationUrl(String documentationUrl) { + this.documentationUrl = documentationUrl; + return this; + } + + public Builder definitions(Map definitions) { + this.definitions.clear(); + this.definitions.putAll(definitions); + return this; + } + + public Builder addDefinition(String name, Schema definition) { + this.definitions.put(name, definition); + return this; + } + + public Builder removeDefinition(String name) { + this.definitions.remove(name); + return this; + } + + public Builder clearDefinitions() { + this.definitions.clear(); + return this; + } + + public Builder properties(Map properties) { + this.properties.clear(); + this.properties.putAll(properties); + return this; + } + + public Builder addProperty(String name, Property property) { + this.properties.put(name, property); + return this; + } + + public Builder removeProperty(String name) { + this.properties.remove(name); + return this; + } + + public Builder clearProperties() { + this.properties.clear(); + return this; + } + + public Builder addReadOnlyProperty(String propertyRef) { + this.readOnlyProperties.add(propertyRef); + return this; + } + + public Builder readOnlyProperties(Collection readOnlyProperties) { + this.readOnlyProperties.clear(); + this.readOnlyProperties.addAll(readOnlyProperties); + return this; + } + + public Builder clearReadOnlyProperties() { + this.readOnlyProperties.clear(); + return this; + } + + public Builder addWriteOnlyProperty(String propertyRef) { + this.writeOnlyProperties.add(propertyRef); + return this; + } + + public Builder writeOnlyProperties(Collection writeOnlyProperties) { + this.writeOnlyProperties.clear(); + this.writeOnlyProperties.addAll(writeOnlyProperties); + return this; + } + + public Builder clearWriteOnlyProperties() { + this.writeOnlyProperties.clear(); + return this; + } + + public Builder primaryIdentifier(Collection primaryIdentifier) { + this.primaryIdentifier.clear(); + this.primaryIdentifier.addAll(primaryIdentifier); + return this; + } + + public Builder clearPrimaryIdentifier() { + this.primaryIdentifier.clear(); + return this; + } + + public Builder addCreateOnlyProperty(String propertyRef) { + this.createOnlyProperties.add(propertyRef); + return this; + } + + public Builder createOnlyProperties(Collection createOnlyProperties) { + this.createOnlyProperties.clear(); + this.createOnlyProperties.addAll(createOnlyProperties); + return this; + } + + public Builder clearCreateOnlyProperties() { + this.createOnlyProperties.clear(); + return this; + } + + public Builder addDeprecatedProperty(String propertyRef) { + this.deprecatedProperties.add(propertyRef); + return this; + } + + public Builder deprecatedProperties(Collection deprecatedProperties) { + this.deprecatedProperties.clear(); + this.deprecatedProperties.addAll(deprecatedProperties); + return this; + } + + public Builder clearDeprecatedProperties() { + this.deprecatedProperties.clear(); + return this; + } + + public Builder addAdditionalIdentifier(List additionalIdentifier) { + this.additionalIdentifiers.add(additionalIdentifier); + return this; + } + + public Builder additionalIdentifiers(List> additionalIdentifiers) { + this.additionalIdentifiers.clear(); + this.additionalIdentifiers.addAll(additionalIdentifiers); + return this; + } + + public Builder clearAdditionalIdentifiers() { + this.additionalIdentifiers.clear(); + return this; + } + + public Builder handlers(Map handlers) { + this.handlers.clear(); + this.handlers.putAll(handlers); + return this; + } + + public Builder addHandler(String name, Handler handler) { + this.handlers.put(name, handler); + return this; + } + + public Builder removeHandler(String name) { + this.handlers.remove(name); + return this; + } + + public Builder clearHandlers() { + this.handlers.clear(); + return this; + } + + public Builder remotes(Map remotes) { + this.remotes.clear(); + this.remotes.putAll(remotes); + return this; + } + + public Builder addRemote(String name, Remote remote) { + this.remotes.put(name, remote); + return this; + } + + public Builder removeRemote(String name) { + this.remotes.remove(name); + return this; + } + + public Builder clearRemotes() { + this.remotes.clear(); + return this; + } + } +} diff --git a/smithy-aws-cloudformation/src/main/resources/META-INF/services/software.amazon.smithy.aws.cloudformation.schema.fromsmithy.Smithy2CfnExtension b/smithy-aws-cloudformation/src/main/resources/META-INF/services/software.amazon.smithy.aws.cloudformation.schema.fromsmithy.Smithy2CfnExtension new file mode 100644 index 00000000000..5560d46182d --- /dev/null +++ b/smithy-aws-cloudformation/src/main/resources/META-INF/services/software.amazon.smithy.aws.cloudformation.schema.fromsmithy.Smithy2CfnExtension @@ -0,0 +1 @@ +software.amazon.smithy.aws.cloudformation.schema.fromsmithy.mappers.CoreExtension diff --git a/smithy-aws-cloudformation/src/main/resources/META-INF/services/software.amazon.smithy.build.SmithyBuildPlugin b/smithy-aws-cloudformation/src/main/resources/META-INF/services/software.amazon.smithy.build.SmithyBuildPlugin new file mode 100644 index 00000000000..594507fefaa --- /dev/null +++ b/smithy-aws-cloudformation/src/main/resources/META-INF/services/software.amazon.smithy.build.SmithyBuildPlugin @@ -0,0 +1 @@ +software.amazon.smithy.aws.cloudformation.schema.fromsmithy.Smithy2Cfn diff --git a/smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/CfnConfigTest.java b/smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/CfnConfigTest.java new file mode 100644 index 00000000000..06bcdec4af8 --- /dev/null +++ b/smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/CfnConfigTest.java @@ -0,0 +1,60 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.aws.cloudformation.schema.fromsmithy; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import software.amazon.smithy.aws.cloudformation.schema.CfnConfig; +import software.amazon.smithy.aws.cloudformation.schema.CfnException; +import software.amazon.smithy.jsonschema.JsonSchemaConfig; + +public class CfnConfigTest { + @Test + public void throwsOnDifferentAlphanumericRefs() { + CfnConfig config = new CfnConfig(); + + assertTrue(config.getAlphanumericOnlyRefs()); + assertThrows(CfnException.class, () -> config.setAlphanumericOnlyRefs(false)); + } + + @Test + public void throwsOnJsonName() { + CfnConfig config = new CfnConfig(); + + assertThrows(CfnException.class, () -> config.setUseJsonName(true)); + assertThrows(CfnException.class, () -> config.setUseJsonName(false)); + } + + @Test + public void throwsOnDifferentMapStrategy() { + CfnConfig config = new CfnConfig(); + + assertEquals(config.getMapStrategy(), JsonSchemaConfig.MapStrategy.PATTERN_PROPERTIES); + assertThrows(CfnException.class, () -> config.setMapStrategy(JsonSchemaConfig.MapStrategy.PROPERTY_NAMES)); + } + + @Test + public void throwsOnDifferentUnionStrategy() { + CfnConfig config = new CfnConfig(); + + assertEquals(config.getUnionStrategy(), JsonSchemaConfig.UnionStrategy.ONE_OF); + assertThrows(CfnException.class, () -> config.setUnionStrategy(JsonSchemaConfig.UnionStrategy.OBJECT)); + assertThrows(CfnException.class, () -> config.setUnionStrategy(JsonSchemaConfig.UnionStrategy.STRUCTURE)); + } +} diff --git a/smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/CfnConverterTest.java b/smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/CfnConverterTest.java new file mode 100644 index 00000000000..70d37bbfadd --- /dev/null +++ b/smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/CfnConverterTest.java @@ -0,0 +1,131 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.aws.cloudformation.schema.fromsmithy; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Map; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.aws.cloudformation.schema.CfnConfig; +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.shapes.ShapeId; +import software.amazon.smithy.utils.IoUtils; +import software.amazon.smithy.utils.ListUtils; + +public class CfnConverterTest { + + private static Model testService; + + @BeforeAll + private static void setup() { + testService = Model.assembler() + .addImport(CfnConverterTest.class.getResource("test-service.smithy")) + .discoverModels() + .assemble() + .unwrap(); + } + + @Test + public void convertsResourcesToCloudFormation() { + CfnConfig config = new CfnConfig(); + config.setOrganizationName("Smithy"); + config.setService(ShapeId.from("smithy.example#TestService")); + Map result = CfnConverter.create().config(config) + .convertToNodes(testService); + + assertEquals(result.keySet().size(), 3); + assertThat(result.keySet(), containsInAnyOrder(ListUtils.of( + "Smithy::TestService::Bar", + "Smithy::TestService::Basil", + "Smithy::TestService::FooResource").toArray())); + for (String resourceTypeName : result.keySet()) { + String filename = Smithy2Cfn.getFileNameFromResourceType(resourceTypeName); + // Handle our convention of using ".cfn.json" for schema validation. + filename = filename.replace(".json", ".cfn.json"); + Node expectedNode = Node.parse(IoUtils.toUtf8String( + getClass().getResourceAsStream(filename))); + + ObjectNode generatedResource = result.get(resourceTypeName); + Node.assertEquals(generatedResource, expectedNode); + + // Assert that the additionalProperties property is set to false, + // so that this behavior is enforced regardless of other changes. + assertEquals(generatedResource.expectMember("additionalProperties"), Node.from(false)); + } + } + + @Test + public void handlesAwsServiceTraitDefaulting() { + Model model = Model.assembler() + .addImport(CfnConverterTest.class.getResource("simple-service-aws.smithy")) + .discoverModels() + .assemble() + .unwrap(); + + CfnConfig config = new CfnConfig(); + config.setService(ShapeId.from("smithy.example#TestService")); + Map result = CfnConverter.create().config(config) + .convertToNodes(model); + + assertEquals(result.keySet().size(), 1); + assertThat(result.keySet(), containsInAnyOrder(ListUtils.of("AWS::SomeThing::FooResource").toArray())); + Node expectedNode = Node.parse(IoUtils.toUtf8String( + getClass().getResourceAsStream("simple-service-aws.cfn.json"))); + + Node.assertEquals(result.get("AWS::SomeThing::FooResource"), expectedNode); + } + + @Test + public void usesConfiguredServiceName() { + CfnConfig config = new CfnConfig(); + config.setOrganizationName("Smithy"); + config.setService(ShapeId.from("smithy.example#TestService")); + config.setServiceName("ExampleService"); + Map result = CfnConverter.create().config(config) + .convertToNodes(testService); + + assertEquals(result.keySet().size(), 3); + assertThat(result.keySet(), containsInAnyOrder(ListUtils.of( + "Smithy::ExampleService::Bar", + "Smithy::ExampleService::Basil", + "Smithy::ExampleService::FooResource").toArray())); + } + + @Test + public void handlesDisabledPropertyCaps() { + CfnConfig config = new CfnConfig(); + config.setOrganizationName("Smithy"); + config.setService(ShapeId.from("smithy.example#TestService")); + config.setDisableCapitalizedProperties(true); + Map result = CfnConverter.create().config(config) + .convertToNodes(testService); + + assertEquals(result.keySet().size(), 3); + assertThat(result.keySet(), containsInAnyOrder(ListUtils.of( + "Smithy::TestService::Bar", + "Smithy::TestService::Basil", + "Smithy::TestService::FooResource").toArray())); + Node expectedNode = Node.parse(IoUtils.toUtf8String( + getClass().getResourceAsStream("disable-caps-fooresource.cfn.json"))); + + Node.assertEquals(result.get("Smithy::TestService::FooResource"), expectedNode); + } +} diff --git a/smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/CfnSchemasTest.java b/smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/CfnSchemasTest.java new file mode 100644 index 00000000000..321852ecdc9 --- /dev/null +++ b/smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/CfnSchemasTest.java @@ -0,0 +1,81 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.aws.cloudformation.schema.fromsmithy; + +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Collectors; +import org.everit.json.schema.Schema; +import org.everit.json.schema.ValidationException; +import org.everit.json.schema.loader.SchemaLoader; +import org.json.JSONObject; +import org.json.JSONTokener; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.smithy.utils.IoUtils; + +public class CfnSchemasTest { + private static final String DEFINITION = "provider.definition.schema.v1.json"; + + private static Schema validationSchema; + + @BeforeAll + public static void loadSchema() { + try (InputStream schemaStream = CfnSchemasTest.class.getResourceAsStream(DEFINITION)) { + validationSchema = SchemaLoader.load(new JSONObject(new JSONTokener(schemaStream))); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @ParameterizedTest + @MethodSource("resourceSchemaFiles") + public void validateTestResourceSchema(String resourceSchemaFile) { + // Validate that all of our ".cfn.json" schemas used for testing + // pass the definition schema from CloudFormation. + JSONObject resourceSchema = new JSONObject(IoUtils.readUtf8File(resourceSchemaFile)); + try { + validationSchema.validate(resourceSchema); + } catch (ValidationException e) { + String fileName = resourceSchemaFile.substring(resourceSchemaFile.lastIndexOf("/") + 1); + fail("Got validation errors for " + fileName + ": " + e.getErrorMessage()); + } + } + + public static List resourceSchemaFiles() { + try { + Path definitionPath = Paths.get(CfnSchemasTest.class.getResource(DEFINITION).toURI()); + + // Check for any ".cfn.json" files at or deeper than the + // validation schema definition. + return Files.walk(Paths.get(definitionPath.getParent().toUri())) + .filter(Files::isRegularFile) + .filter(file -> file.toString().endsWith(".cfn.json")) + .map(Object::toString) + .collect(Collectors.toList()); + } catch (IOException | URISyntaxException e) { + throw new RuntimeException(e); + } + } +} diff --git a/smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/Smithy2CfnTest.java b/smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/Smithy2CfnTest.java new file mode 100644 index 00000000000..6ec7e2dcd07 --- /dev/null +++ b/smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/Smithy2CfnTest.java @@ -0,0 +1,95 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.aws.cloudformation.schema.fromsmithy; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import software.amazon.smithy.aws.cloudformation.schema.CfnException; +import software.amazon.smithy.build.MockManifest; +import software.amazon.smithy.build.PluginContext; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.node.Node; + +public class Smithy2CfnTest { + @Test + public void pluginConvertsModel() { + Model model = Model.assembler() + .addImport(Smithy2CfnTest.class.getResource("test-service.smithy")) + .discoverModels() + .assemble() + .unwrap(); + MockManifest manifest = new MockManifest(); + PluginContext context = PluginContext.builder() + .settings(Node.objectNode() + .withMember("organizationName", "Smithy") + .withMember("service", "smithy.example#TestService")) + .fileManifest(manifest) + .model(model) + .originalModel(model) + .build(); + new Smithy2Cfn().execute(context); + + assertEquals(manifest.getFiles().size(), 3); + assertTrue(manifest.hasFile("/smithy-testservice-fooresource.json")); + assertTrue(manifest.hasFile("/smithy-testservice-bar.json")); + assertTrue(manifest.hasFile("/smithy-testservice-basil.json")); + } + + @Test + public void throwsWhenServiceNotConfigured() { + Model model = Model.assembler() + .addImport(Smithy2CfnTest.class.getResource("test-service.smithy")) + .discoverModels() + .assemble() + .unwrap(); + MockManifest manifest = new MockManifest(); + PluginContext context = PluginContext.builder() + .settings(Node.objectNode() + .withMember("organizationName", "Smithy")) + .fileManifest(manifest) + .model(model) + .originalModel(model) + .build(); + + assertThrows(CfnException.class, () -> { + new Smithy2Cfn().execute(context); + }); + } + + @Test + public void throwsWhenOrganizationNameNotConfigured() { + Model model = Model.assembler() + .addImport(Smithy2CfnTest.class.getResource("test-service.smithy")) + .discoverModels() + .assemble() + .unwrap(); + MockManifest manifest = new MockManifest(); + PluginContext context = PluginContext.builder() + .settings(Node.objectNode() + .withMember("service", "smithy.example#TestService")) + .fileManifest(manifest) + .model(model) + .originalModel(model) + .build(); + + assertThrows(CfnException.class, () -> { + new Smithy2Cfn().execute(context); + }); + } +} diff --git a/smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/TestRunnerTest.java b/smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/TestRunnerTest.java new file mode 100644 index 00000000000..052ea391246 --- /dev/null +++ b/smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/TestRunnerTest.java @@ -0,0 +1,74 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.aws.cloudformation.schema.fromsmithy; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.smithy.aws.cloudformation.schema.CfnConfig; +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.shapes.ShapeId; +import software.amazon.smithy.utils.IoUtils; + +public class TestRunnerTest { + + @ParameterizedTest + @MethodSource("integFiles") + public void generatesResources(String modelFile) { + Model model = Model.assembler() + .discoverModels(getClass().getClassLoader()) + .addImport(modelFile) + .assemble() + .unwrap(); + + CfnConfig config = new CfnConfig(); + config.setService(ShapeId.from("smithy.example#TestService")); + + // Handle @service trait defaulting setup. + if (!modelFile.endsWith("-aws.smithy")) { + config.setOrganizationName("Smithy"); + } + + Map result = CfnConverter.create().config(config) + .convertToNodes(model); + Node expectedNode = Node.parse(IoUtils.readUtf8File(modelFile.replace(".smithy", ".cfn.json"))); + + // Assert that we got one resource and that it matches + assertEquals(result.keySet().size(), 1); + Node.assertEquals(result.get(result.keySet().iterator().next()), expectedNode); + } + + public static List integFiles() { + try { + return Files.walk(Paths.get(TestRunnerTest.class.getResource("integ").getPath())) + .filter(Files::isRegularFile) + .filter(file -> file.toString().endsWith(".smithy")) + .map(Object::toString) + .collect(Collectors.toList()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/DeprecatedMapperTest.java b/smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/DeprecatedMapperTest.java new file mode 100644 index 00000000000..a37047bdc44 --- /dev/null +++ b/smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/DeprecatedMapperTest.java @@ -0,0 +1,72 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.aws.cloudformation.schema.fromsmithy.mappers; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.aws.cloudformation.schema.CfnConfig; +import software.amazon.smithy.aws.cloudformation.schema.fromsmithy.CfnConverter; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.node.StringNode; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.utils.ListUtils; + +public class DeprecatedMapperTest { + @Test + public void addsDeprecatedPropertiesByDefault() { + Model model = Model.assembler() + .addImport(JsonAddTest.class.getResource("simple.smithy")) + .discoverModels() + .assemble() + .unwrap(); + + CfnConfig config = new CfnConfig(); + config.setOrganizationName("Smithy"); + config.setService(ShapeId.from("smithy.example#TestService")); + + ObjectNode resourceNode = CfnConverter.create() + .config(config) + .convertToNodes(model) + .get("Smithy::TestService::FooResource"); + + Assertions.assertEquals(ListUtils.of("/properties/FooDeprecatedMutableProperty"), + resourceNode.expectArrayMember("deprecatedProperties") + .getElementsAs(StringNode::getValue)); + } + @Test + public void canDisableDeprecatedPropertyGeneration() { + Model model = Model.assembler() + .addImport(JsonAddTest.class.getResource("simple.smithy")) + .discoverModels() + .assemble() + .unwrap(); + + CfnConfig config = new CfnConfig(); + config.setOrganizationName("Smithy"); + config.setService(ShapeId.from("smithy.example#TestService")); + config.setDisableDeprecatedPropertyGeneration(true); + + ObjectNode resourceNode = CfnConverter.create() + .config(config) + .convertToNodes(model) + .get("Smithy::TestService::FooResource"); + + assertFalse(resourceNode.getMember("deprecatedProperties").isPresent()); + } +} diff --git a/smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/DocumentationMapperTest.java b/smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/DocumentationMapperTest.java new file mode 100644 index 00000000000..6742af456fd --- /dev/null +++ b/smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/DocumentationMapperTest.java @@ -0,0 +1,81 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.aws.cloudformation.schema.fromsmithy.mappers; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.aws.cloudformation.schema.CfnConfig; +import software.amazon.smithy.aws.cloudformation.schema.fromsmithy.CfnConverter; +import software.amazon.smithy.aws.cloudformation.schema.model.ResourceSchema; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.utils.ListUtils; + +public class DocumentationMapperTest { + + private static Model model; + + @BeforeAll + public static void loadModel() { + model = Model.assembler() + .addImport(JsonAddTest.class.getResource("simple.smithy")) + .discoverModels() + .assemble() + .unwrap(); + } + + @Test + public void supportsExternalDocumentationUrls() { + CfnConfig config = new CfnConfig(); + config.setOrganizationName("Smithy"); + config.setService(ShapeId.from("smithy.example#TestService")); + + List schemas = CfnConverter.create() + .config(config) + .convert(model); + + assertEquals(1, schemas.size()); + ResourceSchema schema = schemas.get(0); + assertTrue(schema.getDocumentationUrl().isPresent()); + assertEquals("https://docs.example.com", schema.getDocumentationUrl().get()); + assertTrue(schema.getSourceUrl().isPresent()); + assertEquals("https://source.example.com", schema.getSourceUrl().get()); + } + + @Test + public void supportsCustomExternalDocNames() { + CfnConfig config = new CfnConfig(); + config.setOrganizationName("Smithy"); + config.setService(ShapeId.from("smithy.example#TestService")); + config.setExternalDocs(ListUtils.of("main")); + config.setSourceDocs(ListUtils.of("code")); + + List schemas = CfnConverter.create() + .config(config) + .convert(model); + + assertEquals(1, schemas.size()); + ResourceSchema schema = schemas.get(0); + assertTrue(schema.getDocumentationUrl().isPresent()); + assertEquals("https://docs2.example.com", schema.getDocumentationUrl().get()); + assertTrue(schema.getSourceUrl().isPresent()); + assertEquals("https://source2.example.com", schema.getSourceUrl().get()); + } +} diff --git a/smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/JsonAddTest.java b/smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/JsonAddTest.java new file mode 100644 index 00000000000..895ebcbda66 --- /dev/null +++ b/smithy-aws-cloudformation/src/test/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/JsonAddTest.java @@ -0,0 +1,62 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.aws.cloudformation.schema.fromsmithy.mappers; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.aws.cloudformation.schema.CfnConfig; +import software.amazon.smithy.aws.cloudformation.schema.fromsmithy.CfnConverter; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.NodePointer; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.utils.MapUtils; + +public class JsonAddTest { + @Test + public void addsWithPointers() { + Model model = Model.assembler() + .addImport(JsonAddTest.class.getResource("simple.smithy")) + .discoverModels() + .assemble() + .unwrap(); + + ObjectNode addNode = Node.objectNodeBuilder() + .withMember("/arbitrary/foo", "whoa") + .withMember("/arbitrary/bar/baz", "nested") + .withMember("/documentationUrl", "https://example.com") + .build(); + + CfnConfig config = new CfnConfig(); + config.setOrganizationName("Smithy"); + config.setService(ShapeId.from("smithy.example#TestService")); + config.setJsonAdd(MapUtils.of(ShapeId.from("smithy.example#FooResource"), addNode.getStringMap())); + + ObjectNode resourceNode = CfnConverter.create() + .config(config) + .convertToNodes(model) + .get("Smithy::TestService::FooResource"); + + String arbitraryFoo = NodePointer.parse("/arbitrary/foo").getValue(resourceNode).expectStringNode().getValue(); + String arbitraryBarBaz = NodePointer.parse("/arbitrary/bar/baz").getValue(resourceNode).expectStringNode().getValue(); + String documentationUrl = NodePointer.parse("/documentationUrl").getValue(resourceNode).expectStringNode().getValue(); + + Assertions.assertEquals("whoa", arbitraryFoo); + Assertions.assertEquals("nested", arbitraryBarBaz); + Assertions.assertEquals("https://example.com", documentationUrl); + } +} diff --git a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/disable-caps-fooresource.cfn.json b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/disable-caps-fooresource.cfn.json new file mode 100644 index 00000000000..3d661348e75 --- /dev/null +++ b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/disable-caps-fooresource.cfn.json @@ -0,0 +1,61 @@ +{ + "typeName": "Smithy::TestService::FooResource", + "description": "The Foo resource is cool.", + "definitions": { + "ComplexProperty": { + "type": "object", + "properties": { + "another": { + "type": "string" + }, + "property": { + "type": "string" + } + } + }, + "FooMap": { + "type": "object", + "patternProperties": { + ".+": { + "type": "string" + } + } + } + }, + "properties": { + "fooId": { + "type": "string" + }, + "fooValidCreateProperty": { + "$ref": "#/definitions/FooMap" + }, + "fooValidCreateReadProperty": { + "type": "string" + }, + "fooValidFullyMutableProperty": { + "$ref": "#/definitions/ComplexProperty" + }, + "fooValidReadProperty": { + "type": "string" + }, + "fooValidWriteProperty": { + "type": "string" + } + }, + "createOnlyProperties": [ + "/properties/fooValidCreateProperty", + "/properties/fooValidCreateReadProperty" + ], + "readOnlyProperties": [ + "/properties/fooId", + "/properties/fooValidReadProperty" + ], + "writeOnlyProperties": [ + "/properties/fooValidCreateProperty", + "/properties/fooValidWriteProperty" + ], + "primaryIdentifier": [ + "/properties/fooId" + ], + "additionalProperties": false +} diff --git a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/complex-resource.cfn.json b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/complex-resource.cfn.json new file mode 100644 index 00000000000..39defd2512b --- /dev/null +++ b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/complex-resource.cfn.json @@ -0,0 +1,100 @@ +{ + "typeName": "Smithy::TestService::Foo", + "description": "Definition of Example::TestService::Foo Resource Type", + "definitions": { + "ArbitraryMap": { + "type": "object", + "patternProperties": { + ".+": { + "type": "string" + } + } + }, + "ComplexProperty": { + "type": "object", + "properties": { + "AnotherProperty": { + "type": "string" + } + } + } + }, + "properties": { + "AddedProperty": { + "type": "string" + }, + "BarProperty": { + "type": "string" + }, + "CreateProperty": { + "$ref": "#/definitions/ComplexProperty" + }, + "CreateWriteProperty": { + "$ref": "#/definitions/ArbitraryMap" + }, + "CreatedAt": { + "type": "string", + "format": "date-time" + }, + "FooAlias": { + "type": "string" + }, + "FooId": { + "type": "string" + }, + "ImmutableSetting": { + "type": "boolean" + }, + "MutableProperty": { + "$ref": "#/definitions/ComplexProperty" + }, + "Password": { + "type": "string" + }, + "ReadProperty": { + "$ref": "#/definitions/ComplexProperty" + }, + "Secret": { + "type": "string" + }, + "Tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "UpdatedAt": { + "type": "string", + "format": "date-time" + }, + "WriteProperty": { + "$ref": "#/definitions/ComplexProperty" + } + }, + "readOnlyProperties": [ + "/properties/CreatedAt", + "/properties/FooId", + "/properties/ReadProperty", + "/properties/UpdatedAt" + ], + "writeOnlyProperties": [ + "/properties/CreateWriteProperty", + "/properties/Password", + "/properties/Secret", + "/properties/WriteProperty" + ], + "createOnlyProperties": [ + "/properties/CreateProperty", + "/properties/CreateWriteProperty", + "/properties/ImmutableSetting" + ], + "primaryIdentifier": [ + "/properties/FooId" + ], + "additionalIdentifiers": [ + [ + "/properties/FooAlias" + ] + ], + "additionalProperties": false +} diff --git a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/complex-resource.smithy b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/complex-resource.smithy new file mode 100644 index 00000000000..dc044ddf7b0 --- /dev/null +++ b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/complex-resource.smithy @@ -0,0 +1,126 @@ +namespace smithy.example + +use aws.cloudformation#cfnAdditionalIdentifier +use aws.cloudformation#cfnResource +use aws.cloudformation#cfnExcludeProperty +use aws.cloudformation#cfnMutability + +service TestService { + version: "2020-07-02", + resources: [ + Foo, + ], +} + +/// Definition of Example::TestService::Foo Resource Type +@cfnResource(additionalSchemas: [FooProperties]) +resource Foo { + identifiers: { + fooId: String, + }, + create: CreateFoo, + read: GetFoo, + update: UpdateFoo, +} + +@http(method: "POST", uri: "/foos", code: 200) +operation CreateFoo { + input: CreateFooRequest, + output: CreateFooResponse, +} + +structure CreateFooRequest { + @cfnMutability("full") + tags: TagList, + + @cfnMutability("write") + secret: String, + + fooAlias: String, + + mutableProperty: ComplexProperty, + createProperty: ComplexProperty, + writeProperty: ComplexProperty, + createWriteProperty: ArbitraryMap, +} + +structure CreateFooResponse { + fooId: String, +} + +@readonly +@http(method: "GET", uri: "/foos/{fooId}", code: 200) +operation GetFoo { + input: GetFooRequest, + output: GetFooResponse, +} + +structure GetFooRequest { + @httpLabel + @required + fooId: String, + + @httpQuery("fooAlias") + @cfnAdditionalIdentifier + fooAlias: String, +} + +structure GetFooResponse { + fooId: String, + + @httpResponseCode + @cfnExcludeProperty + responseCode: Integer, + + @cfnMutability("read") + updatedAt: Timestamp, + + mutableProperty: ComplexProperty, + createProperty: ComplexProperty, + readProperty: ComplexProperty, +} + +@idempotent +@http(method: "PUT", uri: "/foos/{fooId}", code: 200) +operation UpdateFoo { + input: UpdateFooRequest, +} + +structure UpdateFooRequest { + @httpLabel + @required + fooId: String, + + fooAlias: String, + writeProperty: ComplexProperty, + mutableProperty: ComplexProperty, +} + +structure FooProperties { + addedProperty: String, + + @cfnMutability("full") + barProperty: String, + + @cfnMutability("create-and-read") + immutableSetting: Boolean, + + @cfnMutability("read") + createdAt: Timestamp, + + @cfnMutability("write") + password: String, +} + +structure ComplexProperty { + anotherProperty: String, +} + +list TagList { + member: String +} + +map ArbitraryMap { + key: String, + value: String +} diff --git a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/create-and-read-mutability.cfn.json b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/create-and-read-mutability.cfn.json new file mode 100644 index 00000000000..522f8d10350 --- /dev/null +++ b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/create-and-read-mutability.cfn.json @@ -0,0 +1,22 @@ +{ + "typeName": "Smithy::TestService::CreateAndRead", + "description": "Definition of Smithy::TestService::CreateAndRead Resource Type", + "properties": { + "FooId": { + "type": "string" + }, + "ImmutableSetting": { + "type": "boolean" + } + }, + "readOnlyProperties": [ + "/properties/FooId" + ], + "createOnlyProperties": [ + "/properties/ImmutableSetting" + ], + "primaryIdentifier": [ + "/properties/FooId" + ], + "additionalProperties": false +} diff --git a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/create-and-read-mutability.smithy b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/create-and-read-mutability.smithy new file mode 100644 index 00000000000..0fc58e2d258 --- /dev/null +++ b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/create-and-read-mutability.smithy @@ -0,0 +1,21 @@ +namespace smithy.example + +use aws.cloudformation#cfnMutability +use aws.cloudformation#cfnResource + +service TestService { + version: "2020-07-02", + resources: [CreateAndRead] +} + +@cfnResource(additionalSchemas: [FooProperties]) +resource CreateAndRead { + identifiers: { + fooId: String, + }, +} + +structure FooProperties { + @cfnMutability("create-and-read") + immutableSetting: Boolean, +} diff --git a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/create-write-mutability.cfn.json b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/create-write-mutability.cfn.json new file mode 100644 index 00000000000..5532e4b926a --- /dev/null +++ b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/create-write-mutability.cfn.json @@ -0,0 +1,25 @@ +{ + "typeName": "Smithy::TestService::CreateWrite", + "description": "Definition of Smithy::TestService::CreateWrite Resource Type", + "properties": { + "CreateWriteProperty": { + "type": "string" + }, + "FooId": { + "type": "string" + } + }, + "readOnlyProperties": [ + "/properties/FooId" + ], + "writeOnlyProperties": [ + "/properties/CreateWriteProperty" + ], + "createOnlyProperties": [ + "/properties/CreateWriteProperty" + ], + "primaryIdentifier": [ + "/properties/FooId" + ], + "additionalProperties": false +} diff --git a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/create-write-mutability.smithy b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/create-write-mutability.smithy new file mode 100644 index 00000000000..b188bc94185 --- /dev/null +++ b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/create-write-mutability.smithy @@ -0,0 +1,34 @@ +namespace smithy.example + +use aws.cloudformation#cfnAdditionalIdentifier +use aws.cloudformation#cfnResource +use aws.cloudformation#cfnExcludeProperty +use aws.cloudformation#cfnMutability + +service TestService { + version: "2020-07-02", + resources: [ + CreateWrite, + ], +} + +@cfnResource +resource CreateWrite { + identifiers: { + fooId: String, + }, + create: CreateFoo, +} + +operation CreateFoo { + input: CreateFooRequest, + output: CreateFooResponse, +} + +structure CreateFooRequest { + createWriteProperty: String, +} + +structure CreateFooResponse { + fooId: String, +} diff --git a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/full-mutability.cfn.json b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/full-mutability.cfn.json new file mode 100644 index 00000000000..628e29f8c7f --- /dev/null +++ b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/full-mutability.cfn.json @@ -0,0 +1,25 @@ +{ + "typeName": "Smithy::TestService::Full", + "description": "Definition of Smithy::TestService::Full Resource Type", + "properties": { + "BarProperty": { + "type": "string" + }, + "FooId": { + "type": "string" + }, + "Tags": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "readOnlyProperties": [ + "/properties/FooId" + ], + "primaryIdentifier": [ + "/properties/FooId" + ], + "additionalProperties": false +} diff --git a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/full-mutability.smithy b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/full-mutability.smithy new file mode 100644 index 00000000000..9c2b2fecfc2 --- /dev/null +++ b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/full-mutability.smithy @@ -0,0 +1,40 @@ +namespace smithy.example + +use aws.cloudformation#cfnMutability +use aws.cloudformation#cfnResource + +service TestService { + version: "2020-07-02", + resources: [Full] +} + +@cfnResource(additionalSchemas: [FooProperties]) +resource Full { + identifiers: { + fooId: String, + }, + create: CreateFoo, +} + +operation CreateFoo { + input: CreateFooRequest, + output: CreateFooResponse, +} + +structure CreateFooRequest { + @cfnMutability("full") + tags: TagList, +} + +structure CreateFooResponse { + fooId: String, +} + +structure FooProperties { + @cfnMutability("full") + barProperty: String, +} + +list TagList { + member: String +} diff --git a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/put-lifecycle.cfn.json b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/put-lifecycle.cfn.json new file mode 100644 index 00000000000..6486dfb0bdc --- /dev/null +++ b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/put-lifecycle.cfn.json @@ -0,0 +1,25 @@ +{ + "typeName": "Smithy::TestService::PutResource", + "description": "Definition of Smithy::TestService::PutResource Resource Type", + "properties": { + "BarProperty": { + "type": "string" + }, + "FooId": { + "type": "string" + }, + "Tags": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "createOnlyProperties": [ + "/properties/FooId" + ], + "primaryIdentifier": [ + "/properties/FooId" + ], + "additionalProperties": false +} diff --git a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/put-lifecycle.smithy b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/put-lifecycle.smithy new file mode 100644 index 00000000000..e2d77d5549c --- /dev/null +++ b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/put-lifecycle.smithy @@ -0,0 +1,44 @@ +namespace smithy.example + +use aws.cloudformation#cfnMutability +use aws.cloudformation#cfnResource + +service TestService { + version: "2020-07-02", + resources: [PutResource] +} + +@cfnResource(additionalSchemas: [FooProperties]) +resource PutResource { + identifiers: { + fooId: String, + }, + put: PutFoo, +} + +@idempotent +operation PutFoo { + input: PutFooRequest, + output: PutFooResponse, +} + +structure PutFooRequest { + @required + fooId: String, + + @cfnMutability("full") + tags: TagList, +} + +structure PutFooResponse { + fooId: String, +} + +structure FooProperties { + @cfnMutability("full") + barProperty: String, +} + +list TagList { + member: String +} diff --git a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/queue-example.cfn.json b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/queue-example.cfn.json new file mode 100644 index 00000000000..ec253208d93 --- /dev/null +++ b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/queue-example.cfn.json @@ -0,0 +1,101 @@ +{ + "typeName": "Smithy::TestService::Queue", + "description": "Definition of Smithy::TestService::Queue Resource Type", + "definitions": { + "RedrivePolicy": { + "type": "object", + "properties": { + "MaxReceiveCount": { + "type": "number" + }, + "DeadLetterTargetArn": { + "type": "string" + } + } + }, + "TagMap": { + "type": "object", + "patternProperties": { + ".+": { + "type": "string" + } + } + } + }, + "properties": { + "Arn": { + "type": "string" + }, + "ContentBasedDeduplication": { + "type": "boolean" + }, + "DelaySeconds": { + "type": "number", + "maximum": 900, + "minimum": 0 + }, + "FifoQueue": { + "type": "boolean" + }, + "KmsDataKeyReusePeriodSeconds": { + "type": "number", + "maximum": 86400, + "minimum": 60 + }, + "KmsMasterKeyId": { + "type": "string" + }, + "MaximumMessageSize": { + "type": "number", + "maximum": 262144, + "minimum": 1024 + }, + "MessageRetentionPeriod": { + "type": "number", + "maximum": 1209600, + "minimum": 60 + }, + "QueueName": { + "type": "string" + }, + "ReceiveMessageWaitTimeSeconds": { + "type": "number", + "maximum": 20, + "minimum": 0 + }, + "RedrivePolicy": { + "$ref": "#/definitions/RedrivePolicy" + }, + "Tags": { + "$ref": "#/definitions/TagMap" + }, + "URL": { + "type": "string" + }, + "VisibilityTimeout": { + "type": "number", + "maximum": 43200, + "minimum": 0 + } + }, + "readOnlyProperties": [ + "/properties/Arn", + "/properties/URL" + ], + "createOnlyProperties": [ + "/properties/FifoQueue", + "/properties/QueueName" + ], + "primaryIdentifier": [ + "/properties/QueueName" + ], + "additionalIdentifiers": [ + [ + "/properties/URL" + ], + [ + "/properties/Arn" + ] + ], + "additionalProperties": false +} diff --git a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/queue-example.smithy b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/queue-example.smithy new file mode 100644 index 00000000000..12c4399e32a --- /dev/null +++ b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/queue-example.smithy @@ -0,0 +1,210 @@ +$version: "1.0" + +namespace smithy.example + +use aws.cloudformation#cfnAdditionalIdentifier +use aws.cloudformation#cfnName +use aws.cloudformation#cfnResource +use aws.cloudformation#cfnExcludeProperty +use aws.cloudformation#cfnMutability + +service TestService { + version: "2012-11-05", + resources: [ + Queue, + ] +} + +/// Definition of Smithy::TestService::Queue Resource Type +@cfnResource( + additionalSchemas: [ + GetQueueUrlResult, + AttributeStructure, + ]) +resource Queue { + // The QueueName is the literal identifier, but access + // in other operations is handled through the QueueUrl. + identifiers: { + QueueName: String, + }, + put: CreateQueue, + operations: [ + GetQueueUrl, + ], +} + +// This structure is necessary to handle the way these queue +// attributes are handled as a map with an enum of allowed +// attributes. +@internal +structure AttributeStructure { + @cfnMutability("read") + @cfnAdditionalIdentifier + Arn: String, + ContentBasedDeduplication: Boolean, + + @range(min: 0, max: 900) + DelaySeconds: Integer, + + @cfnMutability("create-and-read") + FifoQueue: Boolean, + KmsMasterKeyId: String, + + @range(min: 60, max: 86400) + KmsDataKeyReusePeriodSeconds: Integer, + + @range(min: 1024, max: 262144) + MaximumMessageSize: Integer, + + @range(min: 60, max: 1209600) + MessageRetentionPeriod: Integer, + + @range(min: 0, max: 20) + ReceiveMessageWaitTimeSeconds: Integer, + RedrivePolicy: RedrivePolicy, + + @range(min: 0, max: 43200) + VisibilityTimeout: Integer, +} + +@internal +structure RedrivePolicy { + deadLetterTargetArn: String, + maxReceiveCount: Integer, +} + +@idempotent +operation CreateQueue { + input: CreateQueueRequest, + output: CreateQueueResult, + errors: [ + QueueDeletedRecently, + QueueNameExists, + ], +} + +operation GetQueueUrl { + input: GetQueueUrlRequest, + output: GetQueueUrlResult, + errors: [ + QueueDoesNotExist, + ], +} + +structure CreateQueueRequest { + @cfnName("Tags") + @cfnMutability("full") + tags: TagMap, + + @required + QueueName: String, + + // Exclude this property because we've modeled explicitly + // as the AttributeStructure structure. + @cfnExcludeProperty + Attributes: QueueAttributeMap, +} + +structure CreateQueueResult { + QueueUrl: String, +} + +structure GetQueueUrlRequest { + QueueOwnerAWSAccountId: String, + + @required + QueueName: String, +} + +structure GetQueueUrlResult { + @cfnAdditionalIdentifier + @cfnMutability("read") + @cfnName("URL") + QueueUrl: String, +} + +@error("client") +@httpError(400) +structure QueueDeletedRecently {} + +@error("client") +@httpError(400) +structure QueueDoesNotExist {} + +@error("client") +@httpError(400) +structure QueueNameExists {} + +map QueueAttributeMap { + key: QueueAttributeName, + + value: String, +} + +map TagMap { + key: TagKey, + + value: TagValue, +} + +@enum([ + { + value: "All", + }, + { + value: "Policy", + }, + { + value: "VisibilityTimeout", + }, + { + value: "MaximumMessageSize", + }, + { + value: "MessageRetentionPeriod", + }, + { + value: "ApproximateNumberOfMessages", + }, + { + value: "ApproximateNumberOfMessagesNotVisible", + }, + { + value: "CreatedTimestamp", + }, + { + value: "LastModifiedTimestamp", + }, + { + value: "QueueArn", + }, + { + value: "ApproximateNumberOfMessagesDelayed", + }, + { + value: "DelaySeconds", + }, + { + value: "ReceiveMessageWaitTimeSeconds", + }, + { + value: "RedrivePolicy", + }, + { + value: "FifoQueue", + }, + { + value: "ContentBasedDeduplication", + }, + { + value: "KmsMasterKeyId", + }, + { + value: "KmsDataKeyReusePeriodSeconds", + }, +]) +string QueueAttributeName + +string TagKey + +string TagValue diff --git a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/read-mutability.cfn.json b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/read-mutability.cfn.json new file mode 100644 index 00000000000..3f9b660568a --- /dev/null +++ b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/read-mutability.cfn.json @@ -0,0 +1,26 @@ +{ + "typeName": "Smithy::TestService::Read", + "description": "Definition of Smithy::TestService::Read Resource Type", + "properties": { + "CreatedAt": { + "type": "string", + "format": "date-time" + }, + "FooId": { + "type": "string" + }, + "UpdatedAt": { + "type": "string", + "format": "date-time" + } + }, + "readOnlyProperties": [ + "/properties/CreatedAt", + "/properties/FooId", + "/properties/UpdatedAt" + ], + "primaryIdentifier": [ + "/properties/FooId" + ], + "additionalProperties": false +} diff --git a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/read-mutability.smithy b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/read-mutability.smithy new file mode 100644 index 00000000000..f044d17d142 --- /dev/null +++ b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/read-mutability.smithy @@ -0,0 +1,38 @@ +namespace smithy.example + +use aws.cloudformation#cfnMutability +use aws.cloudformation#cfnResource + +service TestService { + version: "2020-07-02", + resources: [Read] +} + +@cfnResource(additionalSchemas: [FooProperties]) +resource Read { + identifiers: { + fooId: String, + }, + read: GetFoo, +} + +@readonly +operation GetFoo { + input: GetFooRequest, + output: GetFooResponse, +} + +structure GetFooRequest { + @required + fooId: String +} + +structure GetFooResponse { + @cfnMutability("read") + updatedAt: Timestamp, +} + +structure FooProperties { + @cfnMutability("read") + createdAt: Timestamp, +} diff --git a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/write-mutability.cfn.json b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/write-mutability.cfn.json new file mode 100644 index 00000000000..14cbd0be71c --- /dev/null +++ b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/write-mutability.cfn.json @@ -0,0 +1,26 @@ +{ + "typeName": "Smithy::TestService::Write", + "description": "Definition of Smithy::TestService::Write Resource Type", + "properties": { + "FooId": { + "type": "string" + }, + "Password": { + "type": "string" + }, + "Secret": { + "type": "string" + } + }, + "readOnlyProperties": [ + "/properties/FooId" + ], + "writeOnlyProperties": [ + "/properties/Password", + "/properties/Secret" + ], + "primaryIdentifier": [ + "/properties/FooId" + ], + "additionalProperties": false +} diff --git a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/write-mutability.smithy b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/write-mutability.smithy new file mode 100644 index 00000000000..88edf0d285c --- /dev/null +++ b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/write-mutability.smithy @@ -0,0 +1,36 @@ +namespace smithy.example + +use aws.cloudformation#cfnMutability +use aws.cloudformation#cfnResource + +service TestService { + version: "2020-07-02", + resources: [Write] +} + +@cfnResource(additionalSchemas: [FooProperties]) +resource Write { + identifiers: { + fooId: String, + }, + create: CreateFoo, +} + +operation CreateFoo { + input: CreateFooRequest, + output: CreateFooResponse, +} + +structure CreateFooRequest { + @cfnMutability("write") + secret: String, +} + +structure CreateFooResponse { + fooId: String, +} + +structure FooProperties { + @cfnMutability("write") + password: String, +} diff --git a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/simple.smithy b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/simple.smithy new file mode 100644 index 00000000000..1ef039e7b40 --- /dev/null +++ b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/simple.smithy @@ -0,0 +1,107 @@ +$version: "1.0" + +namespace smithy.example + +use aws.cloudformation#cfnResource + +service TestService { + version: "2020-07-02", + resources: [ + FooResource, + ], +} + +/// The Foo resource is cool. +@externalDocumentation( + "Documentation Url": "https://docs.example.com", + "Source Url": "https://source.example.com", + "Main": "https://docs2.example.com", + "Code": "https://source2.example.com", +) +@cfnResource +resource FooResource { + identifiers: { + fooId: FooId, + }, + create: CreateFooOperation, + read: GetFooOperation, + update: UpdateFooOperation, +} + +operation CreateFooOperation { + input: CreateFooRequest, + output: CreateFooResponse, +} + +structure CreateFooRequest { + fooValidCreateProperty: String, + + @deprecated(message: "Use the `fooValidFullyMutableProperty` property.") + fooDeprecatedMutableProperty: String, + fooValidFullyMutableProperty: ComplexProperty, +} + +structure CreateFooResponse { + fooId: FooId, + + @deprecated(message: "Use the `fooValidFullyMutableProperty` property.") + fooDeprecatedMutableProperty: String, + fooValidFullyMutableProperty: ComplexProperty, +} + +@readonly +operation GetFooOperation { + input: GetFooRequest, + output: GetFooResponse, +} + +structure GetFooRequest { + @required + fooId: FooId, +} + +structure GetFooResponse { + fooId: FooId, + + fooValidReadProperty: String, + + @deprecated(message: "Use the `fooValidFullyMutableProperty` property.") + fooDeprecatedMutableProperty: String, + + fooValidFullyMutableProperty: ComplexProperty, +} + +operation UpdateFooOperation { + input: UpdateFooRequest, + output: UpdateFooResponse, +} + +structure UpdateFooRequest { + @required + fooId: FooId, + + fooValidWriteProperty: String, + + @deprecated(message: "Use the `fooValidFullyMutableProperty` property.") + fooDeprecatedMutableProperty: String, + + fooValidFullyMutableProperty: ComplexProperty, +} + +structure UpdateFooResponse { + fooId: FooId, + + fooValidReadProperty: String, + + @deprecated(message: "Use the `fooValidFullyMutableProperty` property.") + fooDeprecatedMutableProperty: String, + + fooValidFullyMutableProperty: ComplexProperty, +} + +string FooId + +structure ComplexProperty { + property: String, + another: String, +} diff --git a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/provider.definition.schema.v1.json b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/provider.definition.schema.v1.json new file mode 100644 index 00000000000..ebb6cfb9bde --- /dev/null +++ b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/provider.definition.schema.v1.json @@ -0,0 +1,425 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema#", + "$id": "https://schema.cloudformation.us-east-1.amazonaws.com/provider.definition.schema.v1.json", + "title": "CloudFormation Resource Provider Definition MetaSchema", + "description": "This schema validates a CloudFormation resource provider definition.", + "definitions": { + "httpsUrl": { + "type": "string", + "pattern": "^https://[0-9a-zA-Z]([-.\\w]*[0-9a-zA-Z])(:[0-9]*)*([?/#].*)?$", + "maxLength": 4096 + }, + "handlerDefinition": { + "description": "Defines any execution operations which can be performed on this resource provider", + "type": "object", + "properties": { + "permissions": { + "type": "array", + "items": { + "type": "string" + }, + "additionalItems": false + }, + "timeoutInMinutes": { + "description": "Defines the timeout for the entire operation to be interpreted by the invoker of the handler. The default is 120 (2 hours).", + "type": "integer", + "minimum": 2, + "maximum": 2160, + "default": 120 + } + }, + "additionalProperties": false, + "required": [ + "permissions" + ] + }, + "jsonPointerArray": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "format": "json-pointer" + } + }, + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/properties" + } + }, + "validations": { + "dependencies": { + "enum": { + "$comment": "Enforce that properties are strongly typed when enum, or const is specified.", + "required": [ + "type" + ] + }, + "const": { + "required": [ + "type" + ] + }, + "properties": { + "$comment": "An object cannot have both defined and undefined properties; therefore, patternProperties is not allowed when properties is specified.", + "not": { + "required": [ + "patternProperties" + ] + } + } + } + }, + "properties": { + "allOf": [ + { + "$ref": "#/definitions/validations" + }, + { + "$comment": "The following subset of draft-07 property references is supported for resource definitions. Nested properties are disallowed and should be specified as a $ref to a definitions block.", + "type": "object", + "properties": { + "insertionOrder": { + "description": "When set to true, this flag indicates that the order of insertion of the array will be honored, and that changing the order of the array would indicate a diff", + "type": "boolean", + "default": true + }, + "$ref": { + "$ref": "https://json-schema.org/draft-07/schema#/properties/$ref" + }, + "$comment": { + "$ref": "https://json-schema.org/draft-07/schema#/properties/$comment" + }, + "title": { + "$ref": "https://json-schema.org/draft-07/schema#/properties/title" + }, + "description": { + "$ref": "https://json-schema.org/draft-07/schema#/properties/description" + }, + "examples": { + "$ref": "https://json-schema.org/draft-07/schema#/properties/examples" + }, + "default": { + "$ref": "https://json-schema.org/draft-07/schema#/properties/default" + }, + "multipleOf": { + "$ref": "https://json-schema.org/draft-07/schema#/properties/multipleOf" + }, + "maximum": { + "$ref": "https://json-schema.org/draft-07/schema#/properties/maximum" + }, + "exclusiveMaximum": { + "$ref": "https://json-schema.org/draft-07/schema#/properties/exclusiveMaximum" + }, + "minimum": { + "$ref": "https://json-schema.org/draft-07/schema#/properties/minimum" + }, + "exclusiveMinimum": { + "$ref": "https://json-schema.org/draft-07/schema#/properties/exclusiveMinimum" + }, + "maxLength": { + "$ref": "https://json-schema.org/draft-07/schema#/properties/maxLength" + }, + "minLength": { + "$ref": "https://json-schema.org/draft-07/schema#/properties/minLength" + }, + "pattern": { + "$ref": "https://json-schema.org/draft-07/schema#/properties/pattern" + }, + "items": { + "$comment": "Redefined as just a schema. A list of schemas is not allowed", + "$ref": "#/definitions/properties", + "default": {} + }, + "maxItems": { + "$ref": "https://json-schema.org/draft-07/schema#/properties/maxItems" + }, + "minItems": { + "$ref": "https://json-schema.org/draft-07/schema#/properties/minItems" + }, + "uniqueItems": { + "$ref": "https://json-schema.org/draft-07/schema#/properties/uniqueItems" + }, + "contains": { + "$ref": "https://json-schema.org/draft-07/schema#/properties/contains" + }, + "maxProperties": { + "$ref": "https://json-schema.org/draft-07/schema#/properties/maxProperties" + }, + "minProperties": { + "$ref": "https://json-schema.org/draft-07/schema#/properties/minProperties" + }, + "required": { + "$ref": "https://json-schema.org/draft-07/schema#/properties/required" + }, + "properties": { + "type": "object", + "patternProperties": { + "^[A-Za-z0-9]{1,64}$": { + "$ref": "#/definitions/properties" + } + }, + "additionalProperties": false, + "minProperties": 1 + }, + "additionalProperties": { + "$comment": "All properties of a resource must be expressed in the schema - arbitrary inputs are not allowed", + "type": "boolean", + "const": false + }, + "patternProperties": { + "$comment": "patternProperties allow providers to introduce a specification for key-value pairs, or Map inputs.", + "type": "object", + "propertyNames": { + "format": "regex" + } + }, + "dependencies": { + "$comment": "Redefined to capture our properties override.", + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/definitions/properties" + }, + { + "$ref": "https://json-schema.org/draft-07/schema#/definitions/stringArray" + } + ] + } + }, + "const": { + "$ref": "https://json-schema.org/draft-07/schema#/properties/const" + }, + "enum": { + "$ref": "https://json-schema.org/draft-07/schema#/properties/enum" + }, + "type": { + "$ref": "https://json-schema.org/draft-07/schema#/properties/type" + }, + "format": { + "$ref": "https://json-schema.org/draft-07/schema#/properties/format" + }, + "allOf": { + "$ref": "#/definitions/schemaArray" + }, + "anyOf": { + "$ref": "#/definitions/schemaArray" + }, + "oneOf": { + "$ref": "#/definitions/schemaArray" + } + }, + "additionalProperties": false + } + ] + }, + "resourceLink": { + "type": "object", + "properties": { + "$comment": { + "$ref": "https://json-schema.org/draft-07/schema#/properties/$comment" + }, + "templateUri": { + "type": "string", + "pattern": "^(/|https:)" + }, + "mappings": { + "type": "object", + "patternProperties": { + "^[A-Za-z0-9]{1,64}$": { + "type": "string", + "format": "json-pointer" + } + }, + "additionalProperties": false + } + }, + "required": [ + "templateUri", + "mappings" + ], + "additionalProperties": false + } + }, + "type": "object", + "patternProperties": { + "^\\$id$": { + "$ref": "https://json-schema.org/draft-07/schema#/properties/$id" + } + }, + "properties": { + "$schema": { + "$ref": "https://json-schema.org/draft-07/schema#/properties/$schema" + }, + "type": { + "$comment": "Resource Type", + "type": "string", + "const": "RESOURCE" + }, + "typeName": { + "$comment": "Resource Type Identifier", + "examples": [ + "Organization::Service::Resource", + "AWS::EC2::Instance", + "Initech::TPS::Report" + ], + "type": "string", + "pattern": "^[a-zA-Z0-9]{2,64}::[a-zA-Z0-9]{2,64}::[a-zA-Z0-9]{2,64}$" + }, + "$comment": { + "$ref": "https://json-schema.org/draft-07/schema#/properties/$comment" + }, + "title": { + "$ref": "https://json-schema.org/draft-07/schema#/properties/title" + }, + "description": { + "$comment": "A short description of the resource provider. This will be shown in the AWS CloudFormation console.", + "$ref": "https://json-schema.org/draft-07/schema#/properties/description" + }, + "sourceUrl": { + "$comment": "The location of the source code for this resource provider, to help interested parties submit issues or improvements.", + "examples": [ + "https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-s3" + ], + "$ref": "#/definitions/httpsUrl" + }, + "documentationUrl": { + "$comment": "A page with supplemental documentation. The property documentation in schemas should be able to stand alone, but this is an opportunity for e.g. rich examples or more guided documents.", + "examples": [ + "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/CHAP_Using.html" + ], + "$ref": "#/definitions/httpsUrl" + }, + "additionalProperties": { + "$comment": "All properties of a resource must be expressed in the schema - arbitrary inputs are not allowed", + "type": "boolean", + "const": false + }, + "properties": { + "type": "object", + "patternProperties": { + "^[A-Za-z0-9]{1,64}$": { + "$ref": "#/definitions/properties" + } + }, + "additionalProperties": false, + "minProperties": 1 + }, + "definitions": { + "type": "object", + "patternProperties": { + "^[A-Za-z0-9]{1,64}$": { + "$ref": "#/definitions/properties" + } + }, + "additionalProperties": false + }, + "propertyTransform": { + "type": "object", + "patternProperties": { + "^[A-Za-z0-9]{1,64}$": { + "type": "string" + } + } + }, + "handlers": { + "description": "Defines the provisioning operations which can be performed on this resource type", + "type": "object", + "properties": { + "create": { + "$ref": "#/definitions/handlerDefinition" + }, + "read": { + "$ref": "#/definitions/handlerDefinition" + }, + "update": { + "$ref": "#/definitions/handlerDefinition" + }, + "delete": { + "$ref": "#/definitions/handlerDefinition" + }, + "list": { + "$ref": "#/definitions/handlerDefinition" + } + }, + "additionalProperties": false + }, + "remote": { + "description": "Reserved for CloudFormation use. A namespace to inline remote schemas.", + "type": "object", + "patternProperties": { + "^schema[0-9]+$": { + "description": "Reserved for CloudFormation use. A inlined remote schema.", + "type": "object", + "properties": { + "$comment": { + "$ref": "https://json-schema.org/draft-07/schema#/properties/$comment" + }, + "properties": { + "$ref": "#/properties/properties" + }, + "definitions": { + "$ref": "#/properties/definitions" + } + }, + "additionalProperties": true + } + }, + "additionalProperties": false + }, + "readOnlyProperties": { + "description": "A list of JSON pointers to properties that are able to be found in a Read request but unable to be specified by the customer", + "$ref": "#/definitions/jsonPointerArray" + }, + "writeOnlyProperties": { + "description": "A list of JSON pointers to properties (typically sensitive) that are able to be specified by the customer but unable to be returned in a Read request", + "$ref": "#/definitions/jsonPointerArray" + }, + "createOnlyProperties": { + "description": "A list of JSON pointers to properties that are only able to be specified by the customer when creating a resource. Conversely, any property *not* in this list can be applied to an Update request.", + "$ref": "#/definitions/jsonPointerArray" + }, + "deprecatedProperties": { + "description": "A list of JSON pointers to properties that have been deprecated by the underlying service provider. These properties are still accepted in create & update operations, however they may be ignored, or converted to a consistent model on application. Deprecated properties are not guaranteed to be present in read paths.", + "$ref": "#/definitions/jsonPointerArray" + }, + "primaryIdentifier": { + "description": "A required identifier which uniquely identifies an instance of this resource type. An identifier is a non-zero-length list of JSON pointers to properties that form a single key. An identifier can be a single or multiple properties to support composite-key identifiers.", + "$ref": "#/definitions/jsonPointerArray" + }, + "additionalIdentifiers": { + "description": "An optional list of supplementary identifiers, each of which uniquely identifies an instance of this resource type. An identifier is a non-zero-length list of JSON pointers to properties that form a single key. An identifier can be a single or multiple properties to support composite-key identifiers.", + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/jsonPointerArray" + } + }, + "required": { + "$ref": "https://json-schema.org/draft-07/schema#/properties/required" + }, + "allOf": { + "$ref": "#/definitions/schemaArray" + }, + "anyOf": { + "$ref": "#/definitions/schemaArray" + }, + "oneOf": { + "$ref": "#/definitions/schemaArray" + }, + "resourceLink": { + "description": "A template-able link to a resource instance. AWS-internal service links must be relative to the AWS console domain. External service links must be absolute, HTTPS URIs.", + "$ref": "#/definitions/resourceLink" + } + }, + "required": [ + "typeName", + "properties", + "description", + "primaryIdentifier", + "additionalProperties" + ], + "additionalProperties": false +} diff --git a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/simple-service-aws.cfn.json b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/simple-service-aws.cfn.json new file mode 100644 index 00000000000..26a9ac545fb --- /dev/null +++ b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/simple-service-aws.cfn.json @@ -0,0 +1,57 @@ +{ + "typeName": "AWS::SomeThing::FooResource", + "description": "The Foo resource is cool.", + "sourceUrl": "https://source.example.com", + "documentationUrl": "https://docs.example.com", + "definitions": { + "ComplexProperty": { + "type": "object", + "properties": { + "Property": { + "type": "string" + }, + "Another": { + "type": "string" + } + } + } + }, + "properties": { + "FooDeprecatedMutableProperty": { + "type": "string" + }, + "FooId": { + "type": "string" + }, + "FooValidCreateProperty": { + "type": "string" + }, + "FooValidFullyMutableProperty": { + "$ref": "#/definitions/ComplexProperty" + }, + "FooValidReadProperty": { + "type": "string" + }, + "FooValidWriteProperty": { + "type": "string" + } + }, + "readOnlyProperties": [ + "/properties/FooId", + "/properties/FooValidReadProperty" + ], + "writeOnlyProperties": [ + "/properties/FooValidCreateProperty", + "/properties/FooValidWriteProperty" + ], + "createOnlyProperties": [ + "/properties/FooValidCreateProperty" + ], + "deprecatedProperties": [ + "/properties/FooDeprecatedMutableProperty" + ], + "primaryIdentifier": [ + "/properties/FooId" + ], + "additionalProperties": false +} diff --git a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/simple-service-aws.smithy b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/simple-service-aws.smithy new file mode 100644 index 00000000000..d758c384e87 --- /dev/null +++ b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/simple-service-aws.smithy @@ -0,0 +1,109 @@ +$version: "1.0" + +namespace smithy.example + +use aws.api#service +use aws.cloudformation#cfnResource + +@service(sdkId: "Some Thing", cloudFormationName: "SomeThing") +service TestService { + version: "2020-07-02", + resources: [ + FooResource, + ], +} + +/// The Foo resource is cool. +@externalDocumentation( + "Documentation Url": "https://docs.example.com", + "Source Url": "https://source.example.com", + "Main": "https://docs2.example.com", + "Code": "https://source2.example.com", +) +@cfnResource +resource FooResource { + identifiers: { + fooId: FooId, + }, + create: CreateFooOperation, + read: GetFooOperation, + update: UpdateFooOperation, +} + +operation CreateFooOperation { + input: CreateFooRequest, + output: CreateFooResponse, +} + +structure CreateFooRequest { + fooValidCreateProperty: String, + + @deprecated(message: "Use the `fooValidFullyMutableProperty` property.") + fooDeprecatedMutableProperty: String, + fooValidFullyMutableProperty: ComplexProperty, +} + +structure CreateFooResponse { + fooId: FooId, + + @deprecated(message: "Use the `fooValidFullyMutableProperty` property.") + fooDeprecatedMutableProperty: String, + fooValidFullyMutableProperty: ComplexProperty, +} + +@readonly +operation GetFooOperation { + input: GetFooRequest, + output: GetFooResponse, +} + +structure GetFooRequest { + @required + fooId: FooId, +} + +structure GetFooResponse { + fooId: FooId, + + fooValidReadProperty: String, + + @deprecated(message: "Use the `fooValidFullyMutableProperty` property.") + fooDeprecatedMutableProperty: String, + + fooValidFullyMutableProperty: ComplexProperty, +} + +operation UpdateFooOperation { + input: UpdateFooRequest, + output: UpdateFooResponse, +} + +structure UpdateFooRequest { + @required + fooId: FooId, + + fooValidWriteProperty: String, + + @deprecated(message: "Use the `fooValidFullyMutableProperty` property.") + fooDeprecatedMutableProperty: String, + + fooValidFullyMutableProperty: ComplexProperty, +} + +structure UpdateFooResponse { + fooId: FooId, + + fooValidReadProperty: String, + + @deprecated(message: "Use the `fooValidFullyMutableProperty` property.") + fooDeprecatedMutableProperty: String, + + fooValidFullyMutableProperty: ComplexProperty, +} + +string FooId + +structure ComplexProperty { + property: String, + another: String, +} diff --git a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/smithy-testservice-bar.cfn.json b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/smithy-testservice-bar.cfn.json new file mode 100644 index 00000000000..109d6a2beb6 --- /dev/null +++ b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/smithy-testservice-bar.cfn.json @@ -0,0 +1,40 @@ +{ + "typeName": "Smithy::TestService::Bar", + "description": "A Bar resource, not that kind of bar though.", + "properties": { + "Arn": { + "type": "string" + }, + "BarExplicitMutableProperty": { + "type": "string" + }, + "BarId": { + "type": "string" + }, + "BarImplicitFullProperty": { + "type": "string" + }, + "BarImplicitReadProperty": { + "type": "string" + }, + "BarValidAdditionalProperty": { + "type": "string" + } + }, + "readOnlyProperties": [ + "/properties/Arn", + "/properties/BarImplicitReadProperty" + ], + "createOnlyProperties": [ + "/properties/BarId" + ], + "primaryIdentifier": [ + "/properties/BarId" + ], + "additionalIdentifiers": [ + [ + "/properties/Arn" + ] + ], + "additionalProperties": false +} diff --git a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/smithy-testservice-basil.cfn.json b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/smithy-testservice-basil.cfn.json new file mode 100644 index 00000000000..3cd4aa91f06 --- /dev/null +++ b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/smithy-testservice-basil.cfn.json @@ -0,0 +1,43 @@ +{ + "typeName": "Smithy::TestService::Basil", + "description": "This is an herb.", + "properties": { + "BarId": { + "type": "string" + }, + "BazExplicitMutableProperty": { + "type": "string" + }, + "BazId": { + "type": "string" + }, + "BazImplicitCreateProperty": { + "type": "string" + }, + "BazImplicitFullyMutableProperty": { + "type": "string" + }, + "BazImplicitReadProperty": { + "type": "string" + }, + "BazImplicitWriteProperty": { + "type": "string" + } + }, + "readOnlyProperties": [ + "/properties/BarId", + "/properties/BazId", + "/properties/BazImplicitReadProperty" + ], + "writeOnlyProperties": [ + "/properties/BazImplicitWriteProperty" + ], + "createOnlyProperties": [ + "/properties/BazImplicitCreateProperty" + ], + "primaryIdentifier": [ + "/properties/BarId", + "/properties/BazId" + ], + "additionalProperties": false +} diff --git a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/smithy-testservice-fooresource.cfn.json b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/smithy-testservice-fooresource.cfn.json new file mode 100644 index 00000000000..41d6d0a0a75 --- /dev/null +++ b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/smithy-testservice-fooresource.cfn.json @@ -0,0 +1,61 @@ +{ + "typeName": "Smithy::TestService::FooResource", + "description": "The Foo resource is cool.", + "definitions": { + "ComplexProperty": { + "type": "object", + "properties": { + "Property": { + "type": "string" + }, + "Another": { + "type": "string" + } + } + }, + "FooMap": { + "type": "object", + "patternProperties": { + ".+": { + "type": "string" + } + } + } + }, + "properties": { + "FooId": { + "type": "string" + }, + "FooValidCreateProperty": { + "$ref": "#/definitions/FooMap" + }, + "FooValidCreateReadProperty": { + "type": "string" + }, + "FooValidFullyMutableProperty": { + "$ref": "#/definitions/ComplexProperty" + }, + "FooValidReadProperty": { + "type": "string" + }, + "FooValidWriteProperty": { + "type": "string" + } + }, + "readOnlyProperties": [ + "/properties/FooId", + "/properties/FooValidReadProperty" + ], + "writeOnlyProperties": [ + "/properties/FooValidCreateProperty", + "/properties/FooValidWriteProperty" + ], + "createOnlyProperties": [ + "/properties/FooValidCreateProperty", + "/properties/FooValidCreateReadProperty" + ], + "primaryIdentifier": [ + "/properties/FooId" + ], + "additionalProperties": false +} diff --git a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/test-service.smithy b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/test-service.smithy new file mode 100644 index 00000000000..d0a17c4319e --- /dev/null +++ b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/test-service.smithy @@ -0,0 +1,245 @@ +$version: "1.0" + +namespace smithy.example + +use aws.cloudformation#cfnAdditionalIdentifier +use aws.cloudformation#cfnResource +use aws.cloudformation#cfnExcludeProperty +use aws.cloudformation#cfnMutability + +service TestService { + version: "2020-07-02", + resources: [ + FooResource, + BarResource, + ], +} + +/// The Foo resource is cool. +@cfnResource +resource FooResource { + identifiers: { + fooId: FooId, + }, + create: CreateFooOperation, + read: GetFooOperation, + update: UpdateFooOperation, +} + +operation CreateFooOperation { + input: CreateFooRequest, + output: CreateFooResponse, +} + +structure CreateFooRequest { + fooValidCreateProperty: FooMap, + + fooValidCreateReadProperty: String, + + fooValidFullyMutableProperty: ComplexProperty, +} + +structure CreateFooResponse { + fooId: FooId, +} + +@readonly +operation GetFooOperation { + input: GetFooRequest, + output: GetFooResponse, +} + +structure GetFooRequest { + @required + fooId: FooId, +} + +structure GetFooResponse { + fooId: FooId, + + fooValidReadProperty: String, + + fooValidCreateReadProperty: String, + + fooValidFullyMutableProperty: ComplexProperty, +} + +operation UpdateFooOperation { + input: UpdateFooRequest, + output: UpdateFooResponse, +} + +structure UpdateFooRequest { + @required + fooId: FooId, + + @cfnMutability("write") + fooValidWriteProperty: String, + + fooValidFullyMutableProperty: ComplexProperty, +} + +structure UpdateFooResponse { + fooId: FooId, + + fooValidReadProperty: String, + + fooValidFullyMutableProperty: ComplexProperty, +} + +/// A Bar resource, not that kind of bar though. +@cfnResource(name: "Bar", additionalSchemas: [ExtraBarRequest]) +resource BarResource { + identifiers: { + barId: BarId, + }, + put: PutBarOperation, + read: GetBarOperation, + operations: [ExtraBarOperation], + resources: [BazResource], +} + +@idempotent +operation PutBarOperation { + input: PutBarRequest, +} + +structure PutBarRequest { + @required + barId: BarId, + + barImplicitFullProperty: String, +} + +@readonly +operation GetBarOperation { + input: GetBarRequest, + output: GetBarResponse, +} + +structure GetBarRequest { + @required + barId: BarId, + + @cfnAdditionalIdentifier + arn: String, +} + +structure GetBarResponse { + barId: BarId, + barImplicitReadProperty: String, + barImplicitFullProperty: String, + + @cfnMutability("full") + barExplicitMutableProperty: String, +} + +operation ExtraBarOperation { + input: ExtraBarRequest, +} + +structure ExtraBarRequest { + @required + barId: BarId, + + barValidAdditionalProperty: String, + + @cfnExcludeProperty + barValidExcludedProperty: String, +} + +/// This is an herb. +@cfnResource("name": "Basil") +resource BazResource { + identifiers: { + barId: BarId, + bazId: BazId, + }, + create: CreateBazOperation, + read: GetBazOperation, + update: UpdateBazOperation, +} + +operation CreateBazOperation { + input: CreateBazRequest, + output: CreateBazResponse, +} + +structure CreateBazRequest { + @required + barId: BarId, + + bazExplicitMutableProperty: String, + bazImplicitCreateProperty: String, + bazImplicitFullyMutableProperty: String, + bazImplicitWriteProperty: String, +} + +structure CreateBazResponse { + barId: BarId, + bazId: BazId, +} + +@readonly +operation GetBazOperation { + input: GetBazRequest, + output: GetBazResponse, +} + +structure GetBazRequest { + @required + barId: BarId, + + @required + bazId: BazId, +} + +structure GetBazResponse { + barId: BarId, + bazId: BazId, + + @cfnMutability("full") + bazExplicitMutableProperty: String, + bazImplicitCreateProperty: String, + bazImplicitReadProperty: String, + bazImplicitFullyMutableProperty: String, +} + +operation UpdateBazOperation { + input: UpdateBazRequest, + output: UpdateBazResponse, +} + +structure UpdateBazRequest { + @required + barId: BarId, + + @required + bazId: BazId, + + bazImplicitWriteProperty: String, + bazImplicitFullyMutableProperty: String, +} + +structure UpdateBazResponse { + barId: BarId, + bazId: BazId, + bazImplicitWriteProperty: String, + bazImplicitFullyMutableProperty: String, +} + +string FooId + +string BarId + +string BazId + +structure ComplexProperty { + property: String, + another: String, +} + +map FooMap { + key: String, + value: String +} diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/node/DefaultNodeSerializers.java b/smithy-model/src/main/java/software/amazon/smithy/model/node/DefaultNodeSerializers.java index 2b956394e88..f2b61875b5e 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/node/DefaultNodeSerializers.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/node/DefaultNodeSerializers.java @@ -26,8 +26,8 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Comparator; -import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -188,7 +188,7 @@ public Class getType() { @Override @SuppressWarnings("unchecked") public Node serialize(Map value, Set serializedObjects, NodeMapper mapper) { - Map mappings = new HashMap<>(); + Map mappings = new LinkedHashMap<>(); Set> entries = (Set>) value.entrySet(); // Iterate over the map entries and populate map entries for an ObjectNode. @@ -254,7 +254,7 @@ private static final class ClassInfo { private static final ConcurrentMap CACHE = new ConcurrentHashMap<>(); // Methods aren't returned normally in any particular order, so give them an order. - final Map getters = new HashMap<>(); + final Map getters = new TreeMap<>(); static ClassInfo fromClass(Class type) { return CACHE.computeIfAbsent(type, klass -> {