diff --git a/docs/source/1.0/spec/aws/aws-cloudformation.rst b/docs/source/1.0/spec/aws/aws-cloudformation.rst new file mode 100644 index 00000000000..efbdf2061fe --- /dev/null +++ b/docs/source/1.0/spec/aws/aws-cloudformation.rst @@ -0,0 +1,846 @@ +========================= +AWS CloudFormation traits +========================= + +CloudFormation traits are used to describe Smithy resources and their +components so they can be converted to `CloudFormation Resource Schemas`_. + +.. _aws-cloudformation-overview: + +`CloudFormation Resource Schemas`_ are the standard method of `modeling a +resource provider`_ for use within CloudFormation. Smithy's modeled +:ref:`resources `, utilizing the traits below, can generate these +schemas. Automatically generating schemas from a service's API lowers the +effort needed to generate and maintain them, 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`_. + +.. contents:: Table of contents + :depth: 3 + :local: + :backlinks: none + + +.. _aws.cloudformation#cfnResource-trait: + +---------------------------------------- +``aws.cloudformation#cfnResource`` trait +---------------------------------------- + +Summary + Indicates that a Smithy resource is a CloudFormation resource. +Trait selector + ``resource`` +Value type + ``structure`` + +The ``aws.cloudformation#cfnResource`` trait is a structure that +supports the following members: + +.. list-table:: + :header-rows: 1 + :widths: 10 20 70 + + * - Property + - Type + - Description + * - name + - ``string`` + - Provides a custom CloudFormation resource name. Defaults to the + :ref:`shape name of the shape ID ` of the targeted + ``resource`` when generating CloudFormation resource schemas. + * - additionalSchemas + - ``list`` + - A list of additional :ref:`shape IDs ` of structures that + will have their properties added to the CloudFormation resource. + Members of these structures with the same names MUST resolve to the + same target. See :ref:`aws-cloudformation-property-deriviation` for + more information. + +The following example defines a simple resource that is also a CloudFormation +resource: + +.. tabs:: + + .. code-tab:: smithy + + namespace smithy.example + + use aws.cloudformation#cfnResource + + @cfnResource + resource Foo { + identifiers: { + fooId: String, + }, + } + + +Resources that have properties that cannot be :ref:`automatically derived +` can use the ``additionalSchemas`` +trait property to include them. This is useful if interacting with a resource +requires calling non-lifecycle APIs or if some of the resource's properties +cannot be automatically converted to CloudFormation properties. + +The following example provides a ``name`` value and one structure shape in the +``additionalSchemas`` list. + +.. tabs:: + + .. code-tab:: smithy + + namespace smithy.example + + use aws.cloudformation#cfnResource + + @cfnResource( + name: "Foo", + additionalSchemas: [AdditionalFooProperties]) + resource FooResource { + identifiers: { + fooId: String, + }, + } + + structure AdditionalFooProperties { + barProperty: String, + } + + +.. _aws-cloudformation-property-deriviation: + +Resource properties +=================== + +Smithy will automatically derive `property`__ information for resources with +the ``@aws.cloudformation#cfnResource`` trait applied. + +A resource's properties include the :ref:`resource's identifiers ` +as well as the top level members of the resource's ``read`` operation output +structure, ``put`` operation input structure, ``create`` operation input +structure, ``update`` operation input structure, and any structures listed in +the ``@cfnResource`` trait's ``additionalSchemas`` property. Members +of these structures can be excluded by applying the :ref:`aws.cloudformation#cfnExcludeProperty-trait`. + +.. __: https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-schema.html#schema-properties-properties + +.. important:: + + Any members used to derive properties that are defined in more than one of + the above structures MUST resolve to the same target. + +.. seealso:: + + Refer to :ref:`property mutability ` + for more information on how the CloudFormation mutability of a property is + derived. + + +.. _aws.cloudformation#cfnExcludeProperty-trait: + +----------------------------------------------- +``aws.cloudformation#cfnExcludeProperty`` trait +----------------------------------------------- + +Summary + Indicates that structure member should not be included as a `property`__ in + generated CloudFormation resource definitions. +Trait selector + ``structure > member`` + + *Any structure member* +Value type + Annotation trait +Conflicts with + :ref:`aws.cloudformation#cfnAdditionalIdentifier-trait`, + :ref:`aws.cloudformation#cfnMutability-trait` + +.. __: https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-schema.html#schema-properties-properties + +The ``cfnExcludeProperty`` trait omits a member of a Smithy structure from the +:ref:`derived resource properties ` of +a CloudFormation resource. + +The following example defines a CloudFormation resource that excludes the +``responseCode`` property: + +.. tabs:: + + .. code-tab:: smithy + + namespace smithy.example + + use aws.cloudformation#cfnExcludeProperty + use aws.cloudformation#cfnResource + + @cfnResource + resource Foo { + identifiers: { + fooId: String, + }, + read: GetFoo, + } + + @readonly + @http(method: "GET", uri: "/foos/{fooId}", code: 200) + operation GetFoo { + input: GetFooRequest, + output: GetFooResponse, + } + + structure GetFooRequest { + @httpLabel + @required + fooId: String, + } + + structure GetFooResponse { + fooId: String, + + @httpResponseCode + @cfnExcludeProperty + responseCode: Integer, + } + + +.. _aws-cloudformation-mutability-derivation: + +------------------- +Property mutability +------------------- + +Any property derived for a resource will have its mutability automatically +derived as well. CloudFormation resource properties can have the following +mutability settings: + +* **Full** - Properties that can be specified when creating, updating, or + reading a resource. +* **Create Only** - Properties that can be specified only during resource + creation and can be returned in a ``read`` or ``list`` request. +* **Read Only** - Properties that can be returned by a ``read`` or ``list`` + request, but cannot be set by the user. +* **Write Only** - Properties that can be specified by the user, but cannot be + returned by a ``read`` or ``list`` request. +* **Create and Write Only** - Properties that can be specified only during + resource creation and cannot be returned in a ``read`` or ``list`` request. + +Given the following model without mutability traits applied, + +.. tabs:: + + .. code-tab:: smithy + + namespace smithy.example + + use aws.cloudformation#cfnResource + + @cfnResource + resource Foo { + identifiers: { + fooId: String, + }, + create: CreateFoo, + read: GetFoo, + update: UpdateFoo, + } + + operation CreateFoo { + input: CreateFooRequest, + output: CreateFooResponse, + } + + structure CreateFooRequest { + createProperty: ComplexProperty, + mutableProperty: ComplexProperty, + writeProperty: ComplexProperty, + createWriteProperty: ComplexProperty, + } + + structure CreateFooResponse { + fooId: String, + } + + @readonly + operation GetFoo { + input: GetFooRequest, + output: GetFooResponse, + } + + structure GetFooRequest { + @required + fooId: String, + } + + structure GetFooResponse { + fooId: String, + createProperty: ComplexProperty, + mutableProperty: ComplexProperty, + readProperty: ComplexProperty, + } + + @idempotent + operation UpdateFoo { + input: UpdateFooRequest, + } + + structure UpdateFooRequest { + @required + fooId: String, + + mutableProperty: ComplexProperty, + writeProperty: ComplexProperty, + } + + structure ComplexProperty { + anotherProperty: String, + } + +The computed resource property mutabilities are: + +.. list-table:: + :header-rows: 1 + :widths: 20 20 60 + + * - Name + - CloudFormation Mutability + - Reasoning + * - ``fooId`` + - Read only + - + Returned in the ``read`` lifecycle via ``GetFooResponse``. + * - ``createProperty`` + - Create only + - + Specified in the ``create`` lifecycle via ``CreateFooRequest``. + + Returned in the ``read`` lifecycle via ``GetFooResponse``. + * - ``mutableProperty`` + - Full + - + Specified in the ``create`` lifecycle via ``CreateFooRequest``. + + Returned in the ``read`` lifecycle via ``GetFooResponse``. + + Specified in the ``update`` lifecycle via ``UpdateFooRequest``. + * - ``readProperty`` + - Read only + - + Returned in the ``read`` lifecycle via ``GetFooResponse``. + * - ``writeProperty`` + - Write only + - + Specified in the ``update`` lifecycle via ``UpdateFooRequest``. + * - ``createWriteProperty`` + - Create and write only + - + Specified in the ``create`` lifecycle via ``CreateFooRequest``. + + +.. _aws.cloudformation#cfnMutability-trait: + +------------------------------------------ +``aws.cloudformation#cfnMutability`` trait +------------------------------------------ + +Summary + Indicates an explicit CloudFormation mutability of the structure member + when part of a CloudFormation resource. +Trait selector + ``structure > member`` + + *Any structure member* +Value type + ``string`` that MUST be set to "full", "create", "create-and-read", "read", + or "write". +Conflicts with + :ref:`aws.cloudformation#cfnExcludeProperty-trait` + +The ``cfnMutability`` trait overrides any :ref:`derived mutability setting +` on a member. The values of the +mutability trait have the following meanings: + +.. list-table:: + :header-rows: 1 + :widths: 20 80 + + * - Value + - Description + * - ``full`` + - Indicates that the CloudFormation property generated from this member + can be specified by the user on ``create`` and ``update`` and can be + returned in a ``read`` or ``list`` request. + * - ``create`` + - Indicates that the CloudFormation property generated from this member + can be specified only during resource creation and cannot returned in a + ``read`` or ``list`` request. This is equivalent to having both `create + only`_ and `write only`_ CloudFormation mutability. + * - ``create-and-read`` + - Indicates that the CloudFormation property generated from this member + can be specified only during resource creation and can be returned in a + ``read`` or ``list`` request. This is equivalent to `create only`_ + CloudFormation mutability. + * - ``read`` + - Indicates that the CloudFormation property generated from this member + can be returned by a ``read`` or ``list`` request, but cannot be set by + the user. This is equivalent to `read only`_ CloudFormation mutability. + * - ``write`` + - Indicates that the CloudFormation property generated from this member + can be specified by the user, but cannot be returned by a ``read`` or + ``list`` request. MUST NOT be set if the member is also marked with the + :ref:`aws.cloudformation#cfnAdditionalIdentifier-trait`. This is + equivalent to `write only`_ CloudFormation mutability. + +The following example defines a CloudFormation resource that marks the ``tags`` +and ``barProperty`` properties as fully mutable: + +.. tabs:: + + .. code-tab:: smithy + + namespace smithy.example + + use aws.cloudformation#cfnMutability + use aws.cloudformation#cfnResource + + @cfnResource(additionalSchemas: [FooProperties]) + resource Foo { + 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, + } + + +The following example defines a CloudFormation resource that marks the +``immutableSetting`` property as create and read only: + +.. tabs:: + + .. code-tab:: smithy + + namespace smithy.example + + use aws.cloudformation#cfnMutability + use aws.cloudformation#cfnResource + + @cfnResource(additionalSchemas: [FooProperties]) + resource Foo { + identifiers: { + fooId: String, + }, + } + + structure FooProperties { + @cfnMutability("create-and-read") + immutableSetting: Boolean, + } + + +The following example defines a CloudFormation resource that marks the +``updatedAt`` and ``createdAt`` properties as read only: + +.. tabs:: + + .. code-tab:: smithy + + namespace smithy.example + + use aws.cloudformation#cfnMutability + use aws.cloudformation#cfnResource + + @cfnResource(additionalSchemas: [FooProperties]) + resource Foo { + 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, + } + + +The following example defines a CloudFormation resource that marks the +derivable ``secret`` and ``password`` properties as write only: + +.. tabs:: + + .. code-tab:: smithy + + namespace smithy.example + + use aws.cloudformation#cfnMutability + use aws.cloudformation#cfnResource + + @cfnResource(additionalSchemas: [FooProperties]) + resource Foo { + 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, + } + + +.. _aws.cloudformation#cfnName-trait: + +------------------------------------ +``aws.cloudformation#cfnName`` trait +------------------------------------ + +Summary + Allows a CloudFormation `resource property`__ name to differ from a + structure member name used in the model. +Trait selector + ``structure > member`` + + *Any structure member* +Value type + ``string`` + +.. __: https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-schema.html#schema-properties-properties + +Given the following structure definition: + +.. tabs:: + + .. code-tab:: smithy + + namespace smithy.example + + use aws.cloudformation#cfnName + + structure AdditionalFooProperties { + bar: String, + + @cfnName("Tags") + tagList: TagList, + } + +the following property names are derived from it: + +:: + + "bar" + "Tags" + +.. _aws.cloudformation#cfnAdditionalIdentifier-trait: + +---------------------------------------------------- +``aws.cloudformation#cfnAdditionalIdentifier`` trait +---------------------------------------------------- + +Summary + Indicates that the CloudFormation property generated from this member is an + `additional identifier`__ for the resource. +Trait selector + ``structure > :test(member > string)`` + + *Any structure member that targets a string* +Value type + Annotation trait +Validation + The ``cfnAdditionalIdentifier`` trait MUST NOT be applied to members with + the :ref:`aws.cloudformation#cfnMutability-trait` set to ``write`` or + ``create``. + +.. __: https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-schema.html#schema-properties-cfnAdditionalIdentifiers + +Each ``cfnAdditionalIdentifier`` uniquely identifies an instance of the +CloudFormation resource it is a part of. This is useful for resources that +provide identifier aliases (for example, a resource might accept an ARN or +customer provided alias in addition to its unique ID.) + +``cfnAdditionalIdentifier`` traits are ignored when applied outside of the +input to an operation bound to the ``read`` lifecycle of a resource. + +The following example defines a CloudFormation resource that has the +``fooAlias`` property as an additional identifier: + +.. tabs:: + + .. code-tab:: smithy + + namespace smithy.example + + use aws.cloudformation#cfnAdditionalIdentifier + use aws.cloudformation#cfnResource + + @cfnResource + resource Foo { + identifiers: { + fooId: String, + }, + read: GetFoo, + } + + @readonly + operation GetFoo { + input: GetFooRequest, + } + + structure GetFooRequest { + @required + fooId: String, + + @cfnAdditionalIdentifier + fooAlias: String, + } + + +------------- +Example model +------------- + +The above traits and behaviors culminate in the ability to generate +`CloudFormation Resource Schemas`_ from a Smithy model. The following example +model utilizes all of these traits to express how a complex Smithy resource +can be annotated for CloudFormation resource generation. + +Given the following model, + +.. tabs:: + + .. code-tab:: smithy + + namespace smithy.example + + use aws.cloudformation#cfnAdditionalIdentifier + use aws.cloudformation#cfnExcludeProperty + use aws.cloudformation#cfnMutability + use aws.cloudformation#cfnResource + + @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, + + createProperty: ComplexProperty, + mutableProperty: ComplexProperty, + writeProperty: ComplexProperty, + createWriteProperty: ComplexProperty, + } + + 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, + + fooAlias: String, + createProperty: ComplexProperty, + mutableProperty: ComplexProperty, + readProperty: ComplexProperty, + } + + @idempotent + @http(method: "PUT", uri: "/foos/{fooId}", code: 200) + operation UpdateFoo { + input: UpdateFooRequest, + } + + structure UpdateFooRequest { + @httpLabel + @required + fooId: String, + + fooAlias: String, + mutableProperty: ComplexProperty, + writeProperty: ComplexProperty, + } + + structure FooProperties { + addedProperty: String, + + @cfnMutability("full") + barProperty: String, + + @cfnName("Immutable") + @cfnMutability("create-and-read") + immutableSetting: Boolean, + + @cfnMutability("read") + createdAt: Timestamp, + + @cfnMutability("write") + password: String, + } + + structure ComplexProperty { + anotherProperty: String, + } + + list TagList { + member: String + } + +The following CloudFormation resource information is computed: + +.. list-table:: + :header-rows: 1 + :widths: 20 20 60 + + * - Name + - CloudFormation Mutability + - Reasoning + * - ``addedProperty`` + - Full + - + Default mutability in ``FooProperties`` via ``additionalSchemas``. + * - ``barProperty`` + - Full + - + ``@cfnMutability`` trait specified in ``FooProperties`` via + ``additionalSchemas``. + * - ``createProperty`` + - Create only + - + Specified in the ``create`` lifecycle via ``CreateFooRequest``. + + Returned in the ``read`` lifecycle via ``GetFooResponse``.= + * - ``createWriteProperty`` + - Create and write only + - + Specified in the ``create`` lifecycle via ``CreateFooRequest``. + * - ``createdAt`` + - Read only + - + ``@cfnMutability`` trait specified in ``FooProperties`` via + ``additionalSchemas``. + * - ``fooAlias`` + - Full + additional identifier + - + Specified in the ``create`` lifecycle via ``CreateFooRequest``. + + Returned in the ``read`` lifecycle via ``GetFooResponse``. + + Specified in the ``update`` lifecycle via ``UpdateFooRequest``. + + ``@cfnAdditionalIdentifier`` trait specified in ``GetFooRequest``. + * - ``fooId`` + - Read only + primary identifier + - + Returned in the ``read`` lifecycle via ``GetFooResponse``. + * - ``Immutable`` from ``immutableSetting`` + - Create only + - + ``@cfnMutability`` trait specified in ``FooProperties`` via + ``additionalSchemas``. + * - ``mutableProperty`` + - Full + - + Specified in the ``create`` lifecycle via ``CreateFooRequest``. + + Returned in the ``read`` lifecycle via ``GetFooResponse``. + + Specified in the ``update`` lifecycle via ``UpdateFooRequest``. + * - ``password`` + - Write only + - + ``@cfnMutability`` trait specified in ``FooProperties`` via + ``additionalSchemas``. + * - ``readProperty`` + - Read only + - + Returned in the ``read`` lifecycle via ``GetFooResponse``. + * - ``responseCode`` + - None + - + ``@cfnExcludeProperty`` trait specified in ``GetFooResponse``. + * - ``secret`` + - Write only + - + ``@cfnMutability`` trait specified in ``CreateFooRequest``. + * - ``tags`` + - Full + - + ``@cfnMutability`` trait specified in ``CreateFooRequest``. + * - ``updatedAt`` + - Read only + - + ``@cfnMutability`` trait specified in ``GetFooResponse``. + * - ``writeProperty`` + - Write only + - + Specified in the ``create`` lifecycle via ``CreateFooRequest``. + + Specified in the ``update`` lifecycle via ``UpdateFooRequest``. + + +.. _CloudFormation Resource Schemas: https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-schema.html +.. _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 +.. _resource providers: https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-types.html +.. _create only: https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-schema.html#schema-properties-createonlyproperties +.. _write only: https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-schema.html#schema-properties-writeonlyproperties +.. _read only: https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-schema.html#schema-properties-readonlyproperties diff --git a/docs/source/1.0/spec/aws/index.rst b/docs/source/1.0/spec/aws/index.rst index 419d61c4c07..c4c5b9a303c 100644 --- a/docs/source/1.0/spec/aws/index.rst +++ b/docs/source/1.0/spec/aws/index.rst @@ -11,6 +11,7 @@ AWS specifications aws-auth aws-iam amazon-apigateway + aws-cloudformation AWS Protocols diff --git a/settings.gradle b/settings.gradle index 3b36176d0b5..4039eb4034f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -24,3 +24,4 @@ include ":smithy-utils" include ":smithy-protocol-test-traits" include ':smithy-jmespath' include ":smithy-waiters" +include ":smithy-aws-cloudformation-traits" diff --git a/smithy-aws-cloudformation-traits/README.md b/smithy-aws-cloudformation-traits/README.md new file mode 100644 index 00000000000..d3ed7f196fc --- /dev/null +++ b/smithy-aws-cloudformation-traits/README.md @@ -0,0 +1,4 @@ +# Smithy AWS CloudFormation traits + +See the [Smithy specification](https://awslabs.github.io/smithy/spec/) +for details on how these traits are used. diff --git a/smithy-aws-cloudformation-traits/build.gradle b/smithy-aws-cloudformation-traits/build.gradle new file mode 100644 index 00000000000..6c5a68a81a4 --- /dev/null +++ b/smithy-aws-cloudformation-traits/build.gradle @@ -0,0 +1,25 @@ +/* + * 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 provides Smithy traits and validators for CloudFormation." + +ext { + displayName = "Smithy :: AWS :: CloudFormation Traits" + moduleName = "software.amazon.smithy.aws.cloudformation.traits" +} + +dependencies { + api project(":smithy-model") +} diff --git a/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnAdditionalIdentifierTrait.java b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnAdditionalIdentifierTrait.java new file mode 100644 index 00000000000..b002eb047a8 --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnAdditionalIdentifierTrait.java @@ -0,0 +1,43 @@ +/* + * 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.traits; + +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.AnnotationTrait; + +/** + * Indicates that the CloudFormation property generated from this member is an + * additional identifier for the resource. + */ +public final class CfnAdditionalIdentifierTrait extends AnnotationTrait { + public static final ShapeId ID = ShapeId.from("aws.cloudformation#cfnAdditionalIdentifier"); + + public CfnAdditionalIdentifierTrait(ObjectNode node) { + super(ID, node); + } + + public CfnAdditionalIdentifierTrait() { + this(Node.objectNode()); + } + + public static final class Provider extends AnnotationTrait.Provider { + public Provider() { + super(ID, CfnAdditionalIdentifierTrait::new); + } + } +} diff --git a/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnExcludePropertyTrait.java b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnExcludePropertyTrait.java new file mode 100644 index 00000000000..d6c46c227f4 --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnExcludePropertyTrait.java @@ -0,0 +1,43 @@ +/* + * 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.traits; + +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.AnnotationTrait; + +/** + * Indicates that structure member should not be included in generated + * CloudFormation resource definitions. + */ +public final class CfnExcludePropertyTrait extends AnnotationTrait { + public static final ShapeId ID = ShapeId.from("aws.cloudformation#cfnExcludeProperty"); + + public CfnExcludePropertyTrait(ObjectNode node) { + super(ID, node); + } + + public CfnExcludePropertyTrait() { + this(Node.objectNode()); + } + + public static final class Provider extends AnnotationTrait.Provider { + public Provider() { + super(ID, CfnExcludePropertyTrait::new); + } + } +} diff --git a/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnMutabilityTrait.java b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnMutabilityTrait.java new file mode 100644 index 00000000000..501ef0fe6f7 --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnMutabilityTrait.java @@ -0,0 +1,58 @@ +/* + * 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.traits; + +import software.amazon.smithy.model.SourceLocation; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.StringTrait; + +/** + * Indicates an explicit CloudFormation mutability of the structure member + * when part of a CloudFormation resource. + */ +public final class CfnMutabilityTrait extends StringTrait { + public static final ShapeId ID = ShapeId.from("aws.cloudformation#cfnMutability"); + + public CfnMutabilityTrait(String value, SourceLocation sourceLocation) { + super(ID, value, sourceLocation); + } + + public static final class Provider extends StringTrait.Provider { + public Provider() { + super(ID, CfnMutabilityTrait::new); + } + } + + public boolean isFullyMutable() { + return getValue().equals("full"); + } + + public boolean isCreate() { + return getValue().equals("create"); + } + + public boolean isCreateAndRead() { + return getValue().equals("create-and-read"); + } + + public boolean isRead() { + return getValue().equals("read"); + } + + public boolean isWrite() { + return getValue().equals("write"); + } +} diff --git a/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnMutabilityTraitValidator.java b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnMutabilityTraitValidator.java new file mode 100644 index 00000000000..905cc6b7c22 --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnMutabilityTraitValidator.java @@ -0,0 +1,46 @@ +/* + * 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.traits; + +import java.util.ArrayList; +import java.util.List; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.validation.AbstractValidator; +import software.amazon.smithy.model.validation.ValidationEvent; + +/** + * Validates that members marked as having write-only mutability are not also + * marked as additional identifiers for their CloudFormation resource. + */ +public final class CfnMutabilityTraitValidator extends AbstractValidator { + @Override + public List validate(Model model) { + List events = new ArrayList<>(); + + for (Shape shape : model.getShapesWithTrait(CfnMutabilityTrait.class)) { + CfnMutabilityTrait trait = shape.expectTrait(CfnMutabilityTrait.class); + // Additional identifiers must be able to be read, so write and + // create mutabilities cannot overlap. + if (shape.hasTrait(CfnAdditionalIdentifierTrait.ID) && (trait.isWrite() || trait.isCreate())) { + events.add(error(shape, trait, String.format("Member with the mutability value of \"%s\" " + + "is also marked as an additional identifier", trait.getValue()))); + } + } + + return events; + } +} diff --git a/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnNameTrait.java b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnNameTrait.java new file mode 100644 index 00000000000..79ac477a288 --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnNameTrait.java @@ -0,0 +1,38 @@ +/* + * 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.traits; + +import software.amazon.smithy.model.SourceLocation; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.StringTrait; + +public final class CfnNameTrait extends StringTrait { + public static final ShapeId ID = ShapeId.from("aws.cloudformation#cfnName"); + + public CfnNameTrait(String value, SourceLocation sourceLocation) { + super(ID, value, sourceLocation); + } + + public CfnNameTrait(String value) { + this(value, SourceLocation.NONE); + } + + public static final class Provider extends StringTrait.Provider { + public Provider() { + super(ID, CfnNameTrait::new); + } + } +} diff --git a/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnResource.java b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnResource.java new file mode 100644 index 00000000000..8810b52d075 --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnResource.java @@ -0,0 +1,285 @@ +/* + * 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.traits; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.utils.SmithyBuilder; +import software.amazon.smithy.utils.ToSmithyBuilder; + +/** + * Contains extracted resource information. + */ +public final class CfnResource implements ToSmithyBuilder { + private final Map propertyDefinitions = new HashMap<>(); + private final Map availableProperties = new HashMap<>(); + private final Set excludedProperties = new HashSet<>(); + private final Set primaryIdentifiers = new HashSet<>(); + private final List> additionalIdentifiers = new ArrayList<>(); + + private CfnResource(Builder builder) { + propertyDefinitions.putAll(builder.propertyDefinitions); + excludedProperties.addAll(builder.excludedProperties); + primaryIdentifiers.addAll(builder.primaryIdentifiers); + additionalIdentifiers.addAll(builder.additionalIdentifiers); + + // Pre-compute the properties available, cleaning up any exclusions. + for (Map.Entry propertyDefinition : propertyDefinitions.entrySet()) { + for (ShapeId shapeId : propertyDefinition.getValue().getShapeIds()) { + if (excludedProperties.contains(shapeId)) { + // Remove an excluded ShapeId for validation. + CfnResourceProperty updatedDefinition = propertyDefinition.getValue().toBuilder() + .removeShapeId(shapeId) + .build(); + propertyDefinitions.put(propertyDefinition.getKey(), updatedDefinition); + + // Update any definition in available properties to also + // remove the excluded ShapeId. + if (availableProperties.containsKey(propertyDefinition.getKey())) { + availableProperties.put(propertyDefinition.getKey(), updatedDefinition); + } + } else { + availableProperties.put(propertyDefinition.getKey(), propertyDefinition.getValue()); + } + } + } + } + + public static Builder builder() { + return new Builder(); + } + + /** + * Get all property definitions of the CloudFormation resource. + * + *

Properties excluded by the {@code cfnExcludedProperty} trait are not + * returned. + * + * @see CfnResource#getExcludedProperties() + * + * @return Returns all members that map to CloudFormation resource + * properties. + */ + public Map getProperties() { + return availableProperties; + } + + /** + * Gets the definition of the specified property of the CloudFormation resource. + * + *

An empty {@code Optional} will be returned if the requested property + * has been excluded by the {@code cfnExcludedProperty} trait. + * + * @see CfnResource#getExcludedProperties() + * + * @param propertyName Name of the property to retrieve + * @return The property definition. + */ + public Optional getProperty(String propertyName) { + return Optional.ofNullable(getProperties().get(propertyName)); + } + + /** + * Get create-specifiable-only property definitions of the CloudFormation resource. + * + * These properties can be specified only during resource creation and + * can be returned in a {@code read} or {@code list} request. + * + * @return Returns create-only member names that map to CloudFormation resource + * properties. + */ + public Set getCreateOnlyProperties() { + return getConstrainedProperties(definition -> { + Set mutabilities = definition.getMutabilities(); + return mutabilities.contains(CfnResourceIndex.Mutability.CREATE) + && !mutabilities.contains(CfnResourceIndex.Mutability.WRITE); + }); + } + + /** + * Get read-only property definitions of the CloudFormation resource. + * + * These properties can be returned by a {@code read} or {@code list} request, + * but cannot be set by the user. + * + * @return Returns read-only member names that map to CloudFormation resource + * properties. + */ + public Set getReadOnlyProperties() { + return getConstrainedProperties(definition -> { + Set mutabilities = definition.getMutabilities(); + return mutabilities.size() == 1 && mutabilities.contains(CfnResourceIndex.Mutability.READ); + }); + } + + /** + * Get write-only property definitions of the CloudFormation resource. + * + * These properties can be specified by the user, but cannot be + * returned by a {@code read} or {@code list} request. + * + * @return Returns write-only member names that map to CloudFormation resource + * properties. + */ + public Set getWriteOnlyProperties() { + return getConstrainedProperties(definition -> { + Set mutabilities = definition.getMutabilities(); + // Create and non-read properties need to be set as createOnly and writeOnly. + if (mutabilities.size() == 1 && mutabilities.contains(CfnResourceIndex.Mutability.CREATE)) { + return true; + } + + // Otherwise, create and update, or update only become writeOnly. + return mutabilities.contains(CfnResourceIndex.Mutability.WRITE) + && !mutabilities.contains(CfnResourceIndex.Mutability.READ); + }); + } + + private Set getConstrainedProperties(Predicate constraint) { + return getProperties().entrySet().stream() + .filter(property -> constraint.test(property.getValue())) + .map(Map.Entry::getKey) + .collect(Collectors.toSet()); + } + + /** + * Get members that have been explicitly excluded from the CloudFormation + * resource. + * + * @return Returns members that have been excluded from a CloudFormation + * resource. + */ + public Set getExcludedProperties() { + return excludedProperties; + } + + /** + * Gets a set of identifier names that represent the primary way to identify + * a CloudFormation resource. This uniquely identifies an individual instance + * of the resource type, and can be one or more properties to represent + * composite-key identifiers. + * + * @return Returns the identifier set primarily used to access a + * CloudFormation resource. + */ + public Set getPrimaryIdentifiers() { + return primaryIdentifiers; + } + + /** + * Get a list of sets of member shape ids, each set can be used to identify + * the CloudFormation resource in addition to its primary identifier(s). + * + * @return Returns identifier sets used to access a CloudFormation resource. + */ + public List> getAdditionalIdentifiers() { + return additionalIdentifiers; + } + + @Override + public Builder toBuilder() { + return builder() + .propertyDefinitions(propertyDefinitions) + .excludedProperties(excludedProperties) + .primaryIdentifiers(primaryIdentifiers) + .additionalIdentifiers(additionalIdentifiers); + } + + public static final class Builder implements SmithyBuilder { + private final Map propertyDefinitions = new HashMap<>(); + private final Set excludedProperties = new HashSet<>(); + private final Set primaryIdentifiers = new HashSet<>(); + private final List> additionalIdentifiers = new ArrayList<>(); + + private Builder() {} + + public boolean hasPropertyDefinition(String propertyName) { + return propertyDefinitions.containsKey(propertyName); + } + + public Builder putPropertyDefinition(String propertyName, CfnResourceProperty definition) { + propertyDefinitions.put(propertyName, definition); + return this; + } + + public Builder updatePropertyDefinition( + String propertyName, + Function updater + ) { + CfnResourceProperty definition = propertyDefinitions.get(propertyName); + + // Don't update if we don't have a property or it's already locked. + if (definition == null || definition.hasExplicitMutability()) { + return this; + } + + return putPropertyDefinition(propertyName, updater.apply(definition)); + } + + public Builder propertyDefinitions(Map propertyDefinitions) { + this.propertyDefinitions.clear(); + this.propertyDefinitions.putAll(propertyDefinitions); + return this; + } + + public Builder addExcludedProperty(ShapeId excludedProperty) { + this.excludedProperties.add(excludedProperty); + return this; + } + + public Builder excludedProperties(Set excludedProperties) { + this.excludedProperties.clear(); + this.excludedProperties.addAll(excludedProperties); + return this; + } + + public Builder addPrimaryIdentifier(String primaryIdentifier) { + this.primaryIdentifiers.add(primaryIdentifier); + return this; + } + + public Builder primaryIdentifiers(Set primaryIdentifiers) { + this.primaryIdentifiers.clear(); + this.primaryIdentifiers.addAll(primaryIdentifiers); + return this; + } + + public Builder addAdditionalIdentifier(Set additionalIdentifier) { + this.additionalIdentifiers.add(additionalIdentifier); + return this; + } + + public Builder additionalIdentifiers(List> additionalIdentifiers) { + this.additionalIdentifiers.clear(); + this.additionalIdentifiers.addAll(additionalIdentifiers); + return this; + } + + @Override + public CfnResource build() { + return new CfnResource(this); + } + } +} diff --git a/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourceIndex.java b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourceIndex.java new file mode 100644 index 00000000000..c7b725aa349 --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourceIndex.java @@ -0,0 +1,380 @@ +/* + * 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.traits; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.knowledge.IdentifierBindingIndex; +import software.amazon.smithy.model.knowledge.KnowledgeIndex; +import software.amazon.smithy.model.knowledge.OperationIndex; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.ResourceShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.ShapeVisitor; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.shapes.ToShapeId; +import software.amazon.smithy.utils.MapUtils; +import software.amazon.smithy.utils.SetUtils; + +/** + * Index of resources to their CloudFormation identifiers and properties. + * + *

This index performs no validation that the identifiers and reference + * valid shapes. + */ +public final class CfnResourceIndex implements KnowledgeIndex { + + static final Set FULLY_MUTABLE = SetUtils.of( + Mutability.CREATE, Mutability.READ, Mutability.WRITE); + + private final Map resourceDefinitions = new HashMap<>(); + + /** + * Mutability options derived through lifecycle operations or traits. + * These mutability options are used to derive the CloudFormation + * mutabilities of the properties they are associated with. + * + * @see CfnResource#getCreateOnlyProperties + * @see CfnResource#getReadOnlyProperties + * @see CfnResource#getWriteOnlyProperties + */ + public enum Mutability { + /** + * Applied when a property was derived from a lifecycle operation that + * creates a resource or the {@link CfnMutabilityTrait} specifying {@code full}, + * {@code create}, or {@code create-and-read} mutability. + */ + CREATE, + + /** + * Applied when a property was derived from a lifecycle operation that + * retrieves a resource or the {@link CfnMutabilityTrait} specifying {@code full}, + * {@code read}, or {@code create-and-read} mutability. + */ + READ, + + /** + * Applied when a property was derived from a lifecycle operation that + * updates a resource or the {@link CfnMutabilityTrait} specifying {@code full} + * or {@code write} mutability. + */ + WRITE + } + + public CfnResourceIndex(Model model) { + + OperationIndex operationIndex = OperationIndex.of(model); + model.shapes(ResourceShape.class) + .filter(shape -> shape.hasTrait(CfnResourceTrait.ID)) + .forEach(resource -> { + CfnResource.Builder builder = CfnResource.builder(); + ShapeId resourceId = resource.getId(); + + // Start with the explicit resource identifiers. + builder.primaryIdentifiers(resource.getIdentifiers().keySet()); + setIdentifierMutabilities(builder, resource); + + // Use the read lifecycle's input to collect the additional identifiers + // and its output to collect readable properties. + resource.getRead().ifPresent(operationId -> { + operationIndex.getInput(operationId).ifPresent(input -> { + addAdditionalIdentifiers(builder, computeResourceAdditionalIdentifiers(input)); + }); + operationIndex.getOutput(operationId).ifPresent(output -> { + updatePropertyMutabilities(builder, model, resourceId, operationId, output, + SetUtils.of(Mutability.READ), this::addReadMutability); + }); + }); + + // Use the put lifecycle's input to collect put-able properties. + resource.getPut().ifPresent(operationId -> { + operationIndex.getInput(operationId).ifPresent(input -> { + updatePropertyMutabilities(builder, model, resourceId, operationId, input, + SetUtils.of(Mutability.CREATE, Mutability.WRITE), this::addPutMutability); + }); + }); + + // Use the create lifecycle's input to collect creatable properties. + resource.getCreate().ifPresent(operationId -> { + operationIndex.getInput(operationId).ifPresent(input -> { + updatePropertyMutabilities(builder, model, resourceId, operationId, input, + SetUtils.of(Mutability.CREATE), this::addCreateMutability); + }); + }); + + // Use the update lifecycle's input to collect writeable properties. + resource.getUpdate().ifPresent(operationId -> { + operationIndex.getInput(operationId).ifPresent(input -> { + updatePropertyMutabilities(builder, model, resourceId, operationId, input, + SetUtils.of(Mutability.WRITE), this::addWriteMutability); + }); + }); + + // Apply any members found through the trait's additionalSchemas property. + CfnResourceTrait trait = resource.expectTrait(CfnResourceTrait.class); + for (ShapeId additionalSchema : trait.getAdditionalSchemas()) { + // These shapes should be present given the @idRef failWhenMissing + // setting, but gracefully handle if they're not. + model.getShape(additionalSchema) + .map(Shape::asStructureShape) + .map(Optional::get) + .ifPresent(shape -> { + addAdditionalIdentifiers(builder, computeResourceAdditionalIdentifiers(shape)); + updatePropertyMutabilities(builder, model, resourceId, null, shape, + SetUtils.of(), Function.identity()); + }); + } + + resourceDefinitions.put(resourceId, builder.build()); + }); + } + + public static CfnResourceIndex of(Model model) { + return model.getKnowledge(CfnResourceIndex.class, CfnResourceIndex::new); + } + + /** + * Gets the definition of the specified CloudFormation resource. + * + * @param resource ShapeID of a resource + * @return The resource definition. + */ + public Optional getResource(ToShapeId resource) { + return Optional.ofNullable(resourceDefinitions.get(resource.toShapeId())); + } + + private void setIdentifierMutabilities(CfnResource.Builder builder, ResourceShape resource) { + Set mutability = getDefaultIdentifierMutabilities(resource); + + resource.getIdentifiers().forEach((name, shape) -> { + builder.putPropertyDefinition(name, CfnResourceProperty.builder() + .hasExplicitMutability(true) + .mutabilities(mutability) + .addShapeId(shape) + .build()); + }); + } + + private Set getDefaultIdentifierMutabilities(ResourceShape resource) { + // If we have a put operation, the identifier will be specified + // on creation. Otherwise, it's read only. + if (resource.getPut().isPresent()) { + return SetUtils.of(Mutability.CREATE, Mutability.READ); + } + + return SetUtils.of(Mutability.READ); + } + + private List> computeResourceAdditionalIdentifiers(StructureShape readInput) { + List> identifiers = new ArrayList<>(); + for (MemberShape member : readInput.members()) { + if (!member.hasTrait(CfnAdditionalIdentifierTrait.class)) { + continue; + } + + identifiers.add(MapUtils.of(member.getMemberName(), member.getId())); + } + return identifiers; + } + + private void addAdditionalIdentifiers( + CfnResource.Builder builder, + List> addedIdentifiers + ) { + if (addedIdentifiers.isEmpty()) { + return; + } + + // Make sure we have properties entries for the additional identifiers. + for (Map addedIdentifier : addedIdentifiers) { + for (Map.Entry idEntry : addedIdentifier.entrySet()) { + builder.putPropertyDefinition(idEntry.getKey(), CfnResourceProperty.builder() + .mutabilities(SetUtils.of(Mutability.READ)) + .addShapeId(idEntry.getValue()) + .build()); + } + builder.addAdditionalIdentifier(addedIdentifier.keySet()); + } + } + + private void updatePropertyMutabilities( + CfnResource.Builder builder, + Model model, + ShapeId resourceId, + ShapeId operationId, + StructureShape propertyContainer, + Set defaultMutabilities, + Function, Set> updater + ) { + // Handle the @excludeProperty trait. + propertyContainer.accept(new ExcludedPropertiesVisitor(model)) + .forEach(builder::addExcludedProperty); + + for (MemberShape member : propertyContainer.members()) { + // We've explicitly set identifier mutability based on how the + // resource instance comes about, so only handle non-identifiers. + if (operationMemberIsIdentifier(model, resourceId, operationId, member)) { + continue; + } + + String memberName = member.getMemberName(); + Set explicitMutability = getExplicitMutability(model, member); + + // Set the correct mutability for if this is a new property. + Set mutabilities = !explicitMutability.isEmpty() + ? explicitMutability + : defaultMutabilities; + + if (builder.hasPropertyDefinition(memberName)) { + builder.updatePropertyDefinition(memberName, + getCfnResourcePropertyUpdater(member, explicitMutability, updater)); + } else { + builder.putPropertyDefinition(memberName, + CfnResourceProperty.builder() + .addShapeId(member.getId()) + .mutabilities(mutabilities) + .hasExplicitMutability(!explicitMutability.isEmpty()) + .build()); + } + } + } + + private Function getCfnResourcePropertyUpdater( + MemberShape member, + Set explicitMutability, + Function, Set> updater + ) { + return definition -> { + CfnResourceProperty.Builder builder = definition.toBuilder().addShapeId(member.getId()); + + if (explicitMutability.isEmpty()) { + // Update the existing mutabilities. + builder.mutabilities(updater.apply(definition.getMutabilities())); + } else { + // Handle explicit mutability from any trait location. + builder.hasExplicitMutability(true) + .mutabilities(explicitMutability); + } + + return builder.build(); + }; + } + + private boolean operationMemberIsIdentifier( + Model model, + ShapeId resourceId, + ShapeId operationId, + MemberShape member + ) { + // The operationId will be null in the case of additionalSchemas, so + // we shouldn't worry if these are bound to operation identifiers. + if (operationId == null) { + return false; + } + + IdentifierBindingIndex index = IdentifierBindingIndex.of(model); + Map bindings = index.getOperationBindings(resourceId, operationId); + String memberName = member.getMemberName(); + // Check for literal identifier bindings. + for (String bindingMemberName : bindings.values()) { + if (memberName.equals(bindingMemberName)) { + return true; + } + } + + return false; + } + + private Set getExplicitMutability( + Model model, + MemberShape member + ) { + Optional traitOptional = member.getMemberTrait(model, CfnMutabilityTrait.class); + if (!traitOptional.isPresent()) { + return SetUtils.of(); + } + + CfnMutabilityTrait trait = traitOptional.get(); + if (trait.isFullyMutable()) { + return FULLY_MUTABLE; + } else if (trait.isCreateAndRead()) { + return SetUtils.of(Mutability.CREATE, Mutability.READ); + } else if (trait.isCreate()) { + return SetUtils.of(Mutability.CREATE); + } else if (trait.isRead()) { + return SetUtils.of(Mutability.READ); + } else if (trait.isWrite()) { + return SetUtils.of(Mutability.WRITE); + } + return SetUtils.of(); + } + + private Set addReadMutability(Set mutabilities) { + Set newMutabilities = new HashSet<>(mutabilities); + newMutabilities.add(Mutability.READ); + return SetUtils.copyOf(newMutabilities); + } + + private Set addCreateMutability(Set mutabilities) { + Set newMutabilities = new HashSet<>(mutabilities); + newMutabilities.add(Mutability.CREATE); + return SetUtils.copyOf(newMutabilities); + } + + private Set addWriteMutability(Set mutabilities) { + Set newMutabilities = new HashSet<>(mutabilities); + newMutabilities.add(Mutability.WRITE); + return SetUtils.copyOf(newMutabilities); + } + + private Set addPutMutability(Set mutabilities) { + return addWriteMutability(addCreateMutability(mutabilities)); + } + + private static final class ExcludedPropertiesVisitor extends ShapeVisitor.Default> { + private final Model model; + + private ExcludedPropertiesVisitor(Model model) { + this.model = model; + } + + @Override + protected Set getDefault(Shape shape) { + return SetUtils.of(); + } + + @Override + public Set structureShape(StructureShape shape) { + Set excludedShapes = new HashSet<>(); + for (MemberShape member : shape.members()) { + if (member.hasTrait(CfnExcludePropertyTrait.ID)) { + excludedShapes.add(member.getId()); + } else { + excludedShapes.addAll(model.expectShape(member.getTarget()).accept(this)); + } + } + return excludedShapes; + } + } +} diff --git a/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourceProperty.java b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourceProperty.java new file mode 100644 index 00000000000..385e0669101 --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourceProperty.java @@ -0,0 +1,130 @@ +/* + * 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.traits; + +import java.util.HashSet; +import java.util.Set; +import java.util.TreeSet; +import software.amazon.smithy.aws.cloudformation.traits.CfnResourceIndex.Mutability; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.utils.SetUtils; +import software.amazon.smithy.utils.SmithyBuilder; +import software.amazon.smithy.utils.ToSmithyBuilder; + +/** + * Contains extracted resource property information. + */ +public final class CfnResourceProperty implements ToSmithyBuilder { + private final Set shapeIds = new TreeSet<>(); + private final Set mutabilities; + private final boolean hasExplicitMutability; + + private CfnResourceProperty(Builder builder) { + shapeIds.addAll(builder.shapeIds); + mutabilities = SetUtils.copyOf(builder.mutabilities); + hasExplicitMutability = builder.hasExplicitMutability; + } + + public static Builder builder() { + return new Builder(); + } + + /* + * Gets all shape IDs used for this property. + * + *

The list of potential shape IDs is used only for validation, + * as having only one shape ID is required. + */ + Set getShapeIds() { + return shapeIds; + } + + /** + * Gets the shape ID used to represent this property. + * + * @return Returns the shape ID. + */ + public ShapeId getShapeId() { + return shapeIds.iterator().next(); + } + + /** + * Returns true if the property's mutability was configured explicitly + * by the use of a trait instead of derived through its lifecycle + * bindings within a resource. + * + * @return Returns true if the mutability is explicitly defined by a trait. + * + * @see CfnMutabilityTrait + */ + public boolean hasExplicitMutability() { + return hasExplicitMutability; + } + + /** + * Gets all of the CloudFormation-specific property mutability options + * associated with this resource property. + * + * @return Returns the mutabilities. + */ + public Set getMutabilities() { + return mutabilities; + } + + @Override + public Builder toBuilder() { + return builder() + .shapeIds(shapeIds) + .mutabilities(mutabilities); + } + + public static final class Builder implements SmithyBuilder { + private final Set shapeIds = new TreeSet<>(); + private Set mutabilities = new HashSet<>(); + private boolean hasExplicitMutability = false; + + @Override + public CfnResourceProperty build() { + return new CfnResourceProperty(this); + } + + public Builder addShapeId(ShapeId shapeId) { + this.shapeIds.add(shapeId); + return this; + } + + public Builder removeShapeId(ShapeId shapeId) { + this.shapeIds.remove(shapeId); + return this; + } + + public Builder shapeIds(Set shapeIds) { + this.shapeIds.clear(); + this.shapeIds.addAll(shapeIds); + return this; + } + + public Builder mutabilities(Set mutabilities) { + this.mutabilities = mutabilities; + return this; + } + + public Builder hasExplicitMutability(boolean hasExplicitMutability) { + this.hasExplicitMutability = hasExplicitMutability; + return this; + } + } +} diff --git a/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourcePropertyValidator.java b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourcePropertyValidator.java new file mode 100644 index 00000000000..d855291bc89 --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourcePropertyValidator.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.traits; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.ResourceShape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.validation.AbstractValidator; +import software.amazon.smithy.model.validation.ValidationEvent; +import software.amazon.smithy.utils.OptionalUtils; + +/** + * Validates that derived CloudFormation properties all have the same target. + */ +public final class CfnResourcePropertyValidator extends AbstractValidator { + + @Override + public List validate(Model model) { + List events = new ArrayList<>(); + + CfnResourceIndex cfnResourceIndex = CfnResourceIndex.of(model); + model.shapes(ResourceShape.class) + .filter(shape -> shape.hasTrait(CfnResourceTrait.ID)) + .map(shape -> validateResource(model, cfnResourceIndex, shape)) + .forEach(events::addAll); + + return events; + } + + private List validateResource( + Model model, + CfnResourceIndex cfnResourceIndex, + ResourceShape resource + ) { + CfnResourceTrait trait = resource.expectTrait(CfnResourceTrait.class); + List events = new ArrayList<>(); + String resourceName = trait.getName().orElse(resource.getId().getName()); + + cfnResourceIndex.getResource(resource) + .map(CfnResource::getProperties) + .ifPresent(properties -> { + for (Map.Entry property : properties.entrySet()) { + validateResourceProperty(model, resource, resourceName, property).ifPresent(events::add); + } + }); + + return events; + } + + private Optional validateResourceProperty( + Model model, + ResourceShape resource, + String resourceName, + Map.Entry property + ) { + Set propertyTargets = new TreeSet<>(); + for (ShapeId shapeId : property.getValue().getShapeIds()) { + model.getShape(shapeId).ifPresent(shape -> + // Use the member target or identifier definition shape. + OptionalUtils.ifPresentOrElse(shape.asMemberShape(), + memberShape -> propertyTargets.add(memberShape.getTarget()), + () -> propertyTargets.add(shapeId))); + } + + if (propertyTargets.size() > 1) { + return Optional.of(error(resource, String.format("The `%s` property of the generated `%s` " + + "CloudFormation resource targets multiple shapes: %s. Reusing member names that " + + "target different shapes can cause confusion for users of the API. This target " + + "discrepancy must either be resolved in the model or one of the members must be " + + "excluded from the conversion.", + property.getKey(), resourceName, propertyTargets))); + } + + return Optional.empty(); + } +} diff --git a/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourceTrait.java b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourceTrait.java new file mode 100644 index 00000000000..9508930599d --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourceTrait.java @@ -0,0 +1,120 @@ +/* + * 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.traits; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.NodeMapper; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.AbstractTrait; +import software.amazon.smithy.model.traits.AbstractTraitBuilder; +import software.amazon.smithy.model.traits.Trait; +import software.amazon.smithy.utils.ListUtils; +import software.amazon.smithy.utils.SmithyBuilder; +import software.amazon.smithy.utils.ToSmithyBuilder; + +/** + * Indicates that a Smithy resource is a CloudFormation resource. + */ +public final class CfnResourceTrait extends AbstractTrait + implements ToSmithyBuilder { + public static final ShapeId ID = ShapeId.from("aws.cloudformation#cfnResource"); + + private final String name; + private final List additionalSchemas; + + private CfnResourceTrait(Builder builder) { + super(ID, builder.getSourceLocation()); + name = builder.name; + additionalSchemas = ListUtils.copyOf(builder.additionalSchemas); + } + + /** + * Get the AWS CloudFormation resource name. + * + * @return Returns the name. + */ + public Optional getName() { + return Optional.ofNullable(name); + } + + /** + * Get the Smithy structure shape Ids for additional schema properties. + * + * @return Returns the additional schema shape Ids. + */ + public List getAdditionalSchemas() { + return additionalSchemas; + } + + public static Builder builder() { + return new Builder(); + } + + @Override + protected Node createNode() { + NodeMapper mapper = new NodeMapper(); + mapper.disableToNodeForClass(CfnResourceTrait.class); + mapper.setOmitEmptyValues(true); + return mapper.serialize(this).expectObjectNode(); + } + + @Override + public SmithyBuilder toBuilder() { + return builder().name(name).additionalSchemas(additionalSchemas); + } + + public static final class Provider extends AbstractTrait.Provider { + public Provider() { + super(ID); + } + + @Override + public Trait createTrait(ShapeId target, Node value) { + return new NodeMapper().deserialize(value, CfnResourceTrait.class); + } + } + + public static final class Builder extends AbstractTraitBuilder { + private String name; + private final List additionalSchemas = new ArrayList<>(); + + private Builder() {} + + @Override + public CfnResourceTrait build() { + return new CfnResourceTrait(this); + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder addAdditionalSchema(ShapeId additionalSchema) { + this.additionalSchemas.add(additionalSchema); + return this; + } + + public Builder additionalSchemas(List additionalSchemas) { + this.additionalSchemas.clear(); + this.additionalSchemas.addAll(additionalSchemas); + return this; + } + } +} diff --git a/smithy-aws-cloudformation-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService b/smithy-aws-cloudformation-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService new file mode 100644 index 00000000000..f005150aec2 --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService @@ -0,0 +1,5 @@ +software.amazon.smithy.aws.cloudformation.traits.CfnAdditionalIdentifierTrait$Provider +software.amazon.smithy.aws.cloudformation.traits.CfnExcludePropertyTrait$Provider +software.amazon.smithy.aws.cloudformation.traits.CfnMutabilityTrait$Provider +software.amazon.smithy.aws.cloudformation.traits.CfnNameTrait$Provider +software.amazon.smithy.aws.cloudformation.traits.CfnResourceTrait$Provider diff --git a/smithy-aws-cloudformation-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator b/smithy-aws-cloudformation-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator new file mode 100644 index 00000000000..55d19d484d5 --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator @@ -0,0 +1,2 @@ +software.amazon.smithy.aws.cloudformation.traits.CfnMutabilityTraitValidator +software.amazon.smithy.aws.cloudformation.traits.CfnResourcePropertyValidator diff --git a/smithy-aws-cloudformation-traits/src/main/resources/META-INF/smithy/aws.cloudformation.smithy b/smithy-aws-cloudformation-traits/src/main/resources/META-INF/smithy/aws.cloudformation.smithy new file mode 100644 index 00000000000..11c04dd2ba2 --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/main/resources/META-INF/smithy/aws.cloudformation.smithy @@ -0,0 +1,101 @@ +$version: "1.0" + +namespace aws.cloudformation + +/// Indicates that the CloudFormation property generated from this member is an +/// additional identifier for the resource. +@trait( + selector: "structure > :test(member > string)", + conflicts: [cfnExcludeProperty] +) +@tags(["diff.error.remove"]) +structure cfnAdditionalIdentifier {} + +/// The cloudFormationName trait allows a CloudFormation resource property name +/// to differ from a structure member name used in the model. +@trait(selector: "structure > member") +@tags(["diff.error.const"]) +string cfnName + +/// Indicates that a structure member should not be included in generated +/// CloudFormation resource definitions. +@trait( + selector: "structure > member", + conflicts: [ + cfnAdditionalIdentifier, + cfnMutability, + ] +) +@tags(["diff.error.add"]) +structure cfnExcludeProperty {} + +/// Indicates an explicit CloudFormation mutability of the structure member +/// when part of a CloudFormation resource. +@trait( + selector: "structure > member", + conflicts: [cfnExcludeProperty] +) +@enum([ + { + value: "full", + name: "FULL", + documentation: """ + Indicates that the CloudFormation property generated from this + member does not have any mutability restrictions, meaning that it + can be specified by the user and returned in a `read` or `list` + request.""", + }, + { + value: "create-and-read", + name: "CREATE_AND_READ", + documentation: """ + Indicates that the CloudFormation property generated from this + member can be specified only during resource creation and can be + returned in a `read` or `list` request.""", + }, + { + value: "create", + name: "CREATE", + documentation: """ + Indicates that the CloudFormation property generated from this + member can be specified only during resource creation and cannot + be returned in a `read` or `list` request. MUST NOT be set if the + member is also marked with the `@additionalIdentifier` trait.""", + }, + { + value: "read", + name: "READ", + documentation: """ + Indicates that the CloudFormation property generated from this + member can be returned by a `read` or `list` request, but + cannot be set by the user.""", + }, + { + value: "write", + name: "WRITE", + documentation: """ + Indicates that the CloudFormation property generated from this + member can be specified by the user, but cannot be returned by a + `read` or `list` request. MUST NOT be set if the member is also + marked with the `@additionalIdentifier` trait.""", + } +]) +string cfnMutability + +/// Indicates that a Smithy resource is a CloudFormation resource. +@trait(selector: "resource") +@tags(["diff.error.add", "diff.error.remove"]) +structure cfnResource { + /// Provides a custom CloudFormation resource name. + name: String, + + /// A list of additional shape IDs of structures that will have their + /// properties added to the CloudFormation resource. + additionalSchemas: StructureIdList, +} + +@private +list StructureIdList { + @idRef(failWhenMissing: true, selector: "structure") + member: String +} diff --git a/smithy-aws-cloudformation-traits/src/main/resources/META-INF/smithy/manifest b/smithy-aws-cloudformation-traits/src/main/resources/META-INF/smithy/manifest new file mode 100644 index 00000000000..10ff5cb7ed8 --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/main/resources/META-INF/smithy/manifest @@ -0,0 +1 @@ +aws.cloudformation.smithy diff --git a/smithy-aws-cloudformation-traits/src/test/java/software/amazon/smithy/aws/cloudformation/traits/CfnMutabilityTraitTest.java b/smithy-aws-cloudformation-traits/src/test/java/software/amazon/smithy/aws/cloudformation/traits/CfnMutabilityTraitTest.java new file mode 100644 index 00000000000..0767f69066f --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/test/java/software/amazon/smithy/aws/cloudformation/traits/CfnMutabilityTraitTest.java @@ -0,0 +1,47 @@ +/* + * 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.traits; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Optional; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.Trait; +import software.amazon.smithy.model.traits.TraitFactory; + +public class CfnMutabilityTraitTest { + + @Test + public void loadsTraitWithString() { + Node node = Node.from("full"); + TraitFactory provider = TraitFactory.createServiceFactory(); + Optional trait = provider.createTrait( + ShapeId.from("aws.cloudformation#cfnMutability"), ShapeId.from("ns.qux#Foo"), node); + + assertTrue(trait.isPresent()); + assertThat(trait.get(), instanceOf(CfnMutabilityTrait.class)); + CfnMutabilityTrait cfnMutabilityTrait = (CfnMutabilityTrait) trait.get(); + assertThat(cfnMutabilityTrait.getValue(), equalTo("full")); + assertTrue(cfnMutabilityTrait.isFullyMutable()); + assertThat(cfnMutabilityTrait.toNode(), equalTo(node)); + } + +} diff --git a/smithy-aws-cloudformation-traits/src/test/java/software/amazon/smithy/aws/cloudformation/traits/CfnNameTraitTest.java b/smithy-aws-cloudformation-traits/src/test/java/software/amazon/smithy/aws/cloudformation/traits/CfnNameTraitTest.java new file mode 100644 index 00000000000..d734c93c478 --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/test/java/software/amazon/smithy/aws/cloudformation/traits/CfnNameTraitTest.java @@ -0,0 +1,45 @@ +/* + * 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.traits; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Optional; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.Trait; +import software.amazon.smithy.model.traits.TraitFactory; + +public final class CfnNameTraitTest { + + @Test + public void loadsTraitWithString() { + Node node = Node.from("Text"); + TraitFactory provider = TraitFactory.createServiceFactory(); + Optional trait = provider.createTrait( + ShapeId.from("aws.cloudformation#cfnName"), ShapeId.from("ns.qux#Foo"), node); + + assertTrue(trait.isPresent()); + assertThat(trait.get(), instanceOf(CfnNameTrait.class)); + CfnNameTrait cfnNameTrait = (CfnNameTrait) trait.get(); + assertThat(cfnNameTrait.getValue(), equalTo("Text")); + assertThat(cfnNameTrait.toNode(), equalTo(node)); + } +} diff --git a/smithy-aws-cloudformation-traits/src/test/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourceIndexTest.java b/smithy-aws-cloudformation-traits/src/test/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourceIndexTest.java new file mode 100644 index 00000000000..2aaba836ca9 --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/test/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourceIndexTest.java @@ -0,0 +1,192 @@ +/* + * 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.traits; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.smithy.aws.cloudformation.traits.CfnResourceIndex.Mutability; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.utils.ListUtils; +import software.amazon.smithy.utils.MapUtils; +import software.amazon.smithy.utils.SetUtils; + +public class CfnResourceIndexTest { + private static final ShapeId FOO = ShapeId.from("smithy.example#FooResource"); + private static final ShapeId BAR = ShapeId.from("smithy.example#BarResource"); + private static final ShapeId BAZ = ShapeId.from("smithy.example#BazResource"); + + private static Model model; + private static CfnResourceIndex cfnResourceIndex; + + @BeforeAll + public static void loadTestModel() { + model = Model.assembler() + .discoverModels(CfnResourceIndexTest.class.getClassLoader()) + .addImport(CfnResourceIndexTest.class.getResource("test-service.smithy")) + .assemble() + .unwrap(); + cfnResourceIndex = CfnResourceIndex.of(model); + } + + private static class ResourceData { + ShapeId resourceId; + Collection identifiers; + List> additionalIdentifiers; + Map> mutabilities; + Set createOnlyProperties; + Set readOnlyProperties; + Set writeOnlyProperties; + } + + public static Collection data() { + ResourceData fooResource = new ResourceData(); + fooResource.resourceId = FOO; + fooResource.identifiers = SetUtils.of("fooId"); + fooResource.additionalIdentifiers = ListUtils.of(); + fooResource.mutabilities = MapUtils.of( + "fooId", SetUtils.of(Mutability.READ), + "fooValidFullyMutableProperty", CfnResourceIndex.FULLY_MUTABLE, + "fooValidCreateProperty", SetUtils.of(Mutability.CREATE), + "fooValidCreateReadProperty", SetUtils.of(Mutability.CREATE, Mutability.READ), + "fooValidReadProperty", SetUtils.of(Mutability.READ), + "fooValidWriteProperty", SetUtils.of(Mutability.WRITE)); + fooResource.createOnlyProperties = SetUtils.of("fooValidCreateProperty", "fooValidCreateReadProperty"); + fooResource.readOnlyProperties = SetUtils.of("fooId", "fooValidReadProperty"); + fooResource.writeOnlyProperties = SetUtils.of("fooValidWriteProperty", "fooValidCreateProperty"); + + ResourceData barResource = new ResourceData(); + barResource.resourceId = BAR; + barResource.identifiers = SetUtils.of("barId"); + barResource.additionalIdentifiers = ListUtils.of(SetUtils.of("arn")); + barResource.mutabilities = MapUtils.of( + "barId", SetUtils.of(Mutability.CREATE, Mutability.READ), + "arn", SetUtils.of(Mutability.READ), + "barExplicitMutableProperty", CfnResourceIndex.FULLY_MUTABLE, + "barValidAdditionalProperty", SetUtils.of(), + "barImplicitReadProperty", SetUtils.of(Mutability.READ), + "barImplicitFullProperty", CfnResourceIndex.FULLY_MUTABLE); + barResource.createOnlyProperties = SetUtils.of("barId"); + barResource.readOnlyProperties = SetUtils.of("arn", "barImplicitReadProperty"); + barResource.writeOnlyProperties = SetUtils.of(); + + ResourceData bazResource = new ResourceData(); + bazResource.resourceId = BAZ; + bazResource.identifiers = SetUtils.of("barId", "bazId"); + bazResource.additionalIdentifiers = ListUtils.of(); + bazResource.mutabilities = MapUtils.of( + "barId", SetUtils.of(Mutability.READ), + "bazId", SetUtils.of(Mutability.READ), + "bazExplicitMutableProperty", CfnResourceIndex.FULLY_MUTABLE, + "bazImplicitFullyMutableProperty", CfnResourceIndex.FULLY_MUTABLE, + "bazImplicitCreateProperty", SetUtils.of(Mutability.CREATE, Mutability.READ), + "bazImplicitReadProperty", SetUtils.of(Mutability.READ), + "bazImplicitWriteProperty", SetUtils.of(Mutability.CREATE, Mutability.WRITE)); + bazResource.createOnlyProperties = SetUtils.of("bazImplicitCreateProperty"); + bazResource.readOnlyProperties = SetUtils.of("barId", "bazId", "bazImplicitReadProperty"); + bazResource.writeOnlyProperties = SetUtils.of("bazImplicitWriteProperty"); + + return ListUtils.of(fooResource, barResource, bazResource); + } + + @ParameterizedTest + @MethodSource("data") + public void detectsPrimaryIdentifiers(ResourceData data) { + assertThat(String.format("Failure for resource %s.", data.resourceId), + cfnResourceIndex.getResource(data.resourceId).get().getPrimaryIdentifiers(), + containsInAnyOrder(data.identifiers.toArray())); + } + + @ParameterizedTest + @MethodSource("data") + public void detectsAdditionalIdentifiers(ResourceData data) { + assertThat(String.format("Failure for resource %s.", data.resourceId), + cfnResourceIndex.getResource(data.resourceId).get().getAdditionalIdentifiers(), + containsInAnyOrder(data.additionalIdentifiers.toArray())); + } + + @ParameterizedTest + @MethodSource("data") + public void findsAllProperties(ResourceData data) { + Map properties = cfnResourceIndex.getResource(data.resourceId) + .get().getProperties(); + + assertThat(properties.keySet(), containsInAnyOrder(data.mutabilities.keySet().toArray())); + properties.forEach((name, definition) -> { + assertThat(String.format("Mismatch on property %s for %s.", name, data.resourceId), + definition.getMutabilities(), containsInAnyOrder(data.mutabilities.get(name).toArray())); + }); + } + + @ParameterizedTest + @MethodSource("data") + public void findsCreateOnlyProperties(ResourceData data) { + Set properties = cfnResourceIndex.getResource(data.resourceId).get().getCreateOnlyProperties(); + + assertThat(String.format("Failure for resource %s.", data.resourceId), + properties, containsInAnyOrder(data.createOnlyProperties.toArray())); + } + + @ParameterizedTest + @MethodSource("data") + public void findsReadOnlyProperties(ResourceData data) { + Set properties = cfnResourceIndex.getResource(data.resourceId).get().getReadOnlyProperties(); + + assertThat(String.format("Failure for resource %s.", data.resourceId), + properties, containsInAnyOrder(data.readOnlyProperties.toArray())); + } + + @ParameterizedTest + @MethodSource("data") + public void findsWriteOnlyProperties(ResourceData data) { + Set properties = cfnResourceIndex.getResource(data.resourceId).get().getWriteOnlyProperties(); + + assertThat(String.format("Failure for resource %s.", data.resourceId), + properties, containsInAnyOrder(data.writeOnlyProperties.toArray())); + } + + @Test + public void setsProperIdentifierMutability() { + Map fooProperties = cfnResourceIndex.getResource(FOO) + .get().getProperties(); + Map barProperties = cfnResourceIndex.getResource(BAR) + .get().getProperties(); + + assertThat(fooProperties.get("fooId").getMutabilities(), containsInAnyOrder(Mutability.READ)); + assertThat(barProperties.get("barId").getMutabilities(), containsInAnyOrder(Mutability.CREATE, Mutability.READ)); + } + + @Test + public void handlesAdditionalSchemaProperty() { + Map barProperties = cfnResourceIndex.getResource(BAR) + .get().getProperties(); + + assertTrue(barProperties.containsKey("barValidAdditionalProperty")); + assertTrue(barProperties.get("barValidAdditionalProperty").getMutabilities().isEmpty()); + assertFalse(barProperties.containsKey("barValidExcludedProperty")); + } +} diff --git a/smithy-aws-cloudformation-traits/src/test/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourceTraitTest.java b/smithy-aws-cloudformation-traits/src/test/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourceTraitTest.java new file mode 100644 index 00000000000..8469edc8cf6 --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/test/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourceTraitTest.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.traits; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; + +public class CfnResourceTraitTest { + @Test + public void loadsFromModel() { + Model result = Model.assembler() + .discoverModels(getClass().getClassLoader()) + .addImport(getClass().getResource("cfn-resources.smithy")) + .assemble() + .unwrap(); + + Shape fooResource = result.expectShape(ShapeId.from("smithy.example#FooResource")); + assertTrue(fooResource.hasTrait(CfnResourceTrait.class)); + CfnResourceTrait fooTrait = fooResource.expectTrait(CfnResourceTrait.class); + assertFalse(fooTrait.getName().isPresent()); + assertTrue(fooTrait.getAdditionalSchemas().isEmpty()); + + Shape barResource = result.expectShape(ShapeId.from("smithy.example#BarResource")); + assertTrue(barResource.hasTrait(CfnResourceTrait.class)); + CfnResourceTrait barTrait = barResource.expectTrait(CfnResourceTrait.class); + assertThat(barTrait.getName().get(), equalTo("CustomResource")); + assertFalse(barTrait.getAdditionalSchemas().isEmpty()); + assertThat(barTrait.getAdditionalSchemas(), contains(ShapeId.from("smithy.example#ExtraBarRequest"))); + } + + @Test + public void handlesNameProperty() { + Model result = Model.assembler() + .discoverModels(getClass().getClassLoader()) + .addImport(getClass().getResource("test-service.smithy")) + .assemble() + .unwrap(); + + assertFalse( + result.expectShape(ShapeId.from("smithy.example#FooResource")) + .expectTrait(CfnResourceTrait.class).getName().isPresent()); + assertThat( + result.expectShape(ShapeId.from("smithy.example#BarResource")) + .expectTrait(CfnResourceTrait.class).getName().get(), + equalTo("Bar")); + assertThat( + result.expectShape(ShapeId.from("smithy.example#BazResource")) + .expectTrait(CfnResourceTrait.class).getName().get(), + equalTo("Basil")); + } +} diff --git a/smithy-aws-cloudformation-traits/src/test/java/software/amazon/smithy/aws/cloudformation/traits/TestRunnerTest.java b/smithy-aws-cloudformation-traits/src/test/java/software/amazon/smithy/aws/cloudformation/traits/TestRunnerTest.java new file mode 100644 index 00000000000..3dd845a679d --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/test/java/software/amazon/smithy/aws/cloudformation/traits/TestRunnerTest.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.traits; + +import java.util.concurrent.Callable; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.smithy.model.validation.testrunner.SmithyTestCase; +import software.amazon.smithy.model.validation.testrunner.SmithyTestSuite; + +public class TestRunnerTest { + @ParameterizedTest(name = "{0}") + @MethodSource("source") + public void testRunner(String filename, Callable callable) throws Exception { + callable.call(); + } + + public static Stream source() { + return SmithyTestSuite.defaultParameterizedTestSource(TestRunnerTest.class); + } +} + diff --git a/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/cfn-resources.smithy b/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/cfn-resources.smithy new file mode 100644 index 00000000000..3b1a85b24b7 --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/cfn-resources.smithy @@ -0,0 +1,36 @@ +$version: "1.0" + +namespace smithy.example + +use aws.cloudformation#cfnResource + +@cfnResource +resource FooResource { + identifiers: { + fooId: FooId + } +} + +@cfnResource( + name: "CustomResource", + additionalSchemas: [ExtraBarRequest] +) +resource BarResource { + identifiers: { + barId: BarId + }, + operations: [ExtraBarOperation], +} + +operation ExtraBarOperation { + input: ExtraBarRequest, +} + +structure ExtraBarRequest { + @required + barId: BarId, +} + +string FooId + +string BarId diff --git a/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/additionalschemas-conflict.errors b/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/additionalschemas-conflict.errors new file mode 100644 index 00000000000..38d7af23580 --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/additionalschemas-conflict.errors @@ -0,0 +1,2 @@ +[ERROR] smithy.example#AdditionalSchemasConflictResource: The `bar` property of the generated `AdditionalSchemasConflictResource` CloudFormation resource targets multiple shapes: [smithy.api#Boolean, smithy.api#String]. Reusing member names that target different shapes can cause confusion for users of the API. This target discrepancy must either be resolved in the model or one of the members must be excluded from the conversion. | CfnResourceProperty +[NOTE] smithy.example#AdditionalSchemasConflictProperties: The structure shape is not connected to from any service shape. | UnreferencedShape diff --git a/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/additionalschemas-conflict.smithy b/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/additionalschemas-conflict.smithy new file mode 100644 index 00000000000..c598d8cc7f4 --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/additionalschemas-conflict.smithy @@ -0,0 +1,32 @@ +$version: "1.0" + +namespace smithy.example + +use aws.cloudformation#cfnResource + +service AdditionalSchemasConflict { + version: "2020-07-02", + resources: [ + AdditionalSchemasConflictResource, + ], +} + +@cfnResource(additionalSchemas: [AdditionalSchemasConflictProperties]) +resource AdditionalSchemasConflictResource { + identifiers: { + fooId: String, + }, + create: CreateAdditionalSchemasConflictResource, +} + +operation CreateAdditionalSchemasConflictResource { + input: CreateAdditionalSchemasConflictResourceRequest, +} + +structure CreateAdditionalSchemasConflictResourceRequest { + bar: String, +} + +structure AdditionalSchemasConflictProperties { + bar: Boolean, +} diff --git a/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/deconflict-by-excluding.errors b/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/deconflict-by-excluding.errors new file mode 100644 index 00000000000..e91cb99431f --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/deconflict-by-excluding.errors @@ -0,0 +1 @@ +[NOTE] smithy.example#AdditionalSchemasDeconflictedProperties: The structure shape is not connected to from any service shape. | UnreferencedShape diff --git a/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/deconflict-by-excluding.smithy b/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/deconflict-by-excluding.smithy new file mode 100644 index 00000000000..98c1c9b7d57 --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/deconflict-by-excluding.smithy @@ -0,0 +1,34 @@ +$version: "1.0" + +namespace smithy.example + +use aws.cloudformation#cfnResource +use aws.cloudformation#cfnExcludeProperty + +service AdditionalSchemasDeconflicted { + version: "2020-07-02", + resources: [ + AdditionalSchemasDeconflictedResource, + ], +} + +@cfnResource(additionalSchemas: [AdditionalSchemasDeconflictedProperties]) +resource AdditionalSchemasDeconflictedResource { + identifiers: { + fooId: String, + }, + create: CreateAdditionalSchemasDeconflictedResource, +} + +operation CreateAdditionalSchemasDeconflictedResource { + input: CreateAdditionalSchemasDeconflictedResourceRequest, +} + +structure CreateAdditionalSchemasDeconflictedResourceRequest { + @cfnExcludeProperty + bar: String, +} + +structure AdditionalSchemasDeconflictedProperties { + bar: Boolean, +} diff --git a/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/invalid-mutability.errors b/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/invalid-mutability.errors new file mode 100644 index 00000000000..af0c8afc5fc --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/invalid-mutability.errors @@ -0,0 +1 @@ +[ERROR] smithy.example#FooStructure$member: Error validating trait `aws.cloudformation#cfnMutability`: String value provided for `aws.cloudformation#cfnMutability` must be one of the following values: `create`, `create-and-read`, `full`, `read`, `write` | TraitValue diff --git a/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/invalid-mutability.smithy b/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/invalid-mutability.smithy new file mode 100644 index 00000000000..16e34aed193 --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/invalid-mutability.smithy @@ -0,0 +1,10 @@ +$version: "1.0" + +namespace smithy.example + +use aws.cloudformation#cfnMutability + +structure FooStructure { + @cfnMutability("undefined") + member: String +} diff --git a/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/lifecycle-conflict.errors b/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/lifecycle-conflict.errors new file mode 100644 index 00000000000..a25fe36484a --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/lifecycle-conflict.errors @@ -0,0 +1 @@ +[ERROR] smithy.example#LifecycleConflictResource: The `bar` property of the generated `LifecycleConflictResource` CloudFormation resource targets multiple shapes: [smithy.api#Boolean, smithy.api#String]. Reusing member names that target different shapes can cause confusion for users of the API. This target discrepancy must either be resolved in the model or one of the members must be excluded from the conversion. | CfnResourceProperty diff --git a/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/lifecycle-conflict.smithy b/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/lifecycle-conflict.smithy new file mode 100644 index 00000000000..f1f1d8d54e4 --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/lifecycle-conflict.smithy @@ -0,0 +1,45 @@ +$version: "1.0" + +namespace smithy.example + +use aws.cloudformation#cfnResource + +service LifecycleConflict { + version: "2020-07-02", + resources: [ + LifecycleConflictResource, + ], +} + +@cfnResource +resource LifecycleConflictResource { + identifiers: { + fooId: String, + }, + create: CreateLifecycleConflictResource, + read: GetLifecycleConflictResource, +} + +operation CreateLifecycleConflictResource { + input: CreateLifecycleConflictResourceRequest, +} + +structure CreateLifecycleConflictResourceRequest { + bar: String, +} + +@readonly +operation GetLifecycleConflictResource { + input: GetLifecycleConflictResourceRequest, + output: GetLifecycleConflictResourceResponse, +} + +structure GetLifecycleConflictResourceRequest { + @required + fooId: String, +} + +structure GetLifecycleConflictResourceResponse { + bar: Boolean, +} + diff --git a/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/write-only-additional-identifier.errors b/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/write-only-additional-identifier.errors new file mode 100644 index 00000000000..45295afd91c --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/write-only-additional-identifier.errors @@ -0,0 +1 @@ +[ERROR] smithy.example#FooStructure$member: Member with the mutability value of "write" is also marked as an additional identifier | CfnMutabilityTrait diff --git a/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/write-only-additional-identifier.smithy b/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/write-only-additional-identifier.smithy new file mode 100644 index 00000000000..1332bdd69c1 --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/write-only-additional-identifier.smithy @@ -0,0 +1,12 @@ +$version: "1.0" + +namespace smithy.example + +use aws.cloudformation#cfnAdditionalIdentifier +use aws.cloudformation#cfnMutability + +structure FooStructure { + @cfnAdditionalIdentifier + @cfnMutability("write") + member: String +} diff --git a/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/test-service.smithy b/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/test-service.smithy new file mode 100644 index 00000000000..79dc87f321e --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/test-service.smithy @@ -0,0 +1,240 @@ +$version: "1.0" + +namespace smithy.example + +use aws.cloudformation#cfnResource +use aws.cloudformation#cfnAdditionalIdentifier +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: String, + + 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, +}