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..596586ea767 --- /dev/null +++ b/docs/source/1.0/spec/aws/aws-cloudformation.rst @@ -0,0 +1,780 @@ +========================= +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#resource-trait: + +------------------------------------- +``aws.cloudformation#resource`` trait +------------------------------------- + +Summary + Indicates that a Smithy resource is a CloudFormation resource. +Trait selector + ``resource`` +Value type + ``structure`` + +The ``aws.cloudformation#resource`` 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. This defaults to the + shape name component of the ``resource`` shape's :ref:`shape + ID `. + * - 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#resource + + @resource + resource Foo { + identifiers: { + fooId: String, + }, + } + + +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#resource + + @resource( + 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#resource`` trait applied. + +A resource's properties include the 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 ``@resource`` +trait's ``additionalSchemas`` property. Members of these structures can be +excluded by applying the :ref:`aws.cloudformation#excludeProperty-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#excludeProperty-trait: + +-------------------------------------------- +``aws.cloudformation#excludeProperty`` 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#additionalIdentifier-trait`, + :ref:`aws.cloudformation#mutability-trait` + +.. __: https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-schema.html#schema-properties-properties + +When :ref:`deriving a resource's properties `, +all members of the used structures that have the ``excludeProperty`` trait +applied will not be included. + +The following example defines a CloudFormation resource that excludes the +``responseCode`` property: + +.. tabs:: + + .. code-tab:: smithy + + namespace smithy.example + + use aws.cloudformation#excludeProperty + use aws.cloudformation#resource + + @resource + 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 + @excludeProperty + 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 +mutabilities: + +* **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#resource + + @resource + 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: 50 50 + + * - Name + - Mutability + * - ``fooId`` + - Read only + * - ``createProperty`` + - Create only + * - ``mutableProperty`` + - Full + * - ``readProperty`` + - Read only + * - ``writeProperty`` + - Write only + * - ``createWriteProperty`` + - Create and write only + + +.. _aws.cloudformation#mutability-trait: + +--------------------------------------- +``aws.cloudformation#mutability`` trait +--------------------------------------- + +Summary + Indicates that the CloudFormation property generated from this has the + specified mutability. +Trait selector + ``structure > member`` + + *Any structure member* +Value type + ``string`` that MUST be set to "full", "create", "create-and-read", "read", + or "write" to indicate the property's specific mutability. +Conflicts with + :ref:`aws.cloudformation#excludeProperty-trait` + +Members with this trait applied will have their `derived mutability +`_ overridden. 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 + does not have any mutability restrictions. + * - ``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 a equivalent to create 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#additionalIdentifier-trait`. This is + equivalent to write only CloudFormation mutability. + + +The following example defines a CloudFormation resource that marks the derivable +``tags`` and ``barProperty`` properties as fully mutable: + +.. tabs:: + + .. code-tab:: smithy + + namespace smithy.example + + use aws.cloudformation#mutability + use aws.cloudformation#resource + + @resource(additionalSchemas: [FooProperties]) + resource Foo { + identifiers: { + fooId: String, + }, + create: CreateFoo, + } + + operation CreateFoo { + input: CreateFooRequest, + output: CreateFooResponse, + } + + structure CreateFooRequest { + @mutability("full") + tags: TagList, + } + + structure CreateFooResponse { + fooId: String, + } + + structure FooProperties { + @mutability("full") + barProperty: String, + } + + +The following example defines a CloudFormation resource that marks the derivable +``immutableSetting`` property as create and read only: + +.. tabs:: + + .. code-tab:: smithy + + namespace smithy.example + + use aws.cloudformation#mutability + use aws.cloudformation#resource + + @resource(additionalSchemas: [FooProperties]) + resource Foo { + identifiers: { + fooId: String, + }, + } + + structure FooProperties { + @mutability("create-and-read") + immutableSetting: Boolean, + } + + +The following example defines a CloudFormation resource that marks the derivable +``updatedAt`` and ``createdAt`` properties as read only: + +.. tabs:: + + .. code-tab:: smithy + + namespace smithy.example + + use aws.cloudformation#mutability + use aws.cloudformation#resource + + @resource(additionalSchemas: [FooProperties]) + resource Foo { + identifiers: { + fooId: String, + }, + read: GetFoo, + } + + @readonly + operation GetFoo { + input: GetFooRequest, + output: GetFooResponse, + } + + structure GetFooRequest { + @required + fooId: String + } + + structure GetFooResponse { + @mutability("read") + updatedAt: Timestamp, + } + + structure FooProperties { + @mutability("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#mutability + use aws.cloudformation#resource + + @resource(additionalSchemas: [FooProperties]) + resource Foo { + identifiers: { + fooId: String, + }, + create: CreateFoo, + } + + operation CreateFoo { + input: CreateFooRequest, + output: CreateFooResponse, + } + + structure CreateFooRequest { + @mutability("write") + secret: String, + } + + structure CreateFooResponse { + fooId: String, + } + + structure FooProperties { + @mutability("write") + password: String, + } + +Given the following model with property mutability traits applied, + +.. tabs:: + + .. code-tab:: smithy + + namespace smithy.example + + use aws.cloudformation#additionalIdentifier + use aws.cloudformation#excludeProperty + use aws.cloudformation#mutability + use aws.cloudformation#resource + + @resource(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 { + @mutability("full") + tags: TagList, + + @mutability("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") + @additionalIdentifier + fooAlias: String, + } + + structure GetFooResponse { + fooId: String, + + @httpResponseCode + @excludeProperty + responseCode: Integer, + + @mutability("read") + updatedAt: Timestamp, + + 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, + + @mutability("full") + barProperty: String, + + @mutability("create-and-read") + immutableSetting: Boolean, + + @mutability("read") + createdAt: Timestamp, + + @mutability("write") + password: String, + } + + structure ComplexProperty { + anotherProperty: String, + } + + list TagList { + member: String + } + +The computed resource property mutabilities are: + +.. list-table:: + :header-rows: 1 + :widths: 50 50 + + * - Name + - Mutability + * - ``addedProperty`` + - Full + * - ``barProperty`` + - Full + * - ``createProperty`` + - Create only + * - ``createWriteProperty`` + - Create and write only + * - ``createdAt`` + - Read only + * - ``fooAlias`` + - Full + * - ``fooId`` + - Read only + * - ``immutableSetting`` + - Create only + * - ``mutableProperty`` + - Full + * - ``password`` + - Write only + * - ``readProperty`` + - Read only + * - ``secret`` + - Write only + * - ``tags`` + - Full + * - ``updatedAt`` + - Read only + * - ``writeProperty`` + - Write only + + +.. _aws.cloudformation#propertyName-trait: + +----------------------------------------- +``aws.cloudformation#propertyName`` trait +----------------------------------------- + +Summary + The propertyName trait 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 that is converted to a CloudFormation +resource: + +.. tabs:: + + .. code-tab:: smithy + + namespace smithy.example + + use aws.cloudformation#propertyName + + structure AdditionalFooProperties { + bar: String, + + @propertyName("Tags") + tagList: TagList, + } + +the CloudFormation resource would have the following property names derived +from it: + +:: + + "bar" + "Tags" + +.. _aws.cloudformation#additionalIdentifier-trait: + +------------------------------------------------- +``aws.cloudformation#additionalIdentifier`` 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 + +.. __: https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-schema.html#schema-properties-additionalidentifiers + +``additionalIdentifier`` traits are ignored when applied outside of the input +to an operation bound to the ``read`` lifecycle of a resource. The +``additionalIdentifier`` trait MUST NOT be applied to members with the +:ref:`aws.cloudformation#mutability-trait` set to ``write-only``. + +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#additionalIdentifier + use aws.cloudformation#resource + + @resource + resource Foo { + identifiers: { + fooId: String, + }, + read: GetFoo, + } + + @readonly + operation GetFoo { + input: GetFooRequest, + } + + structure GetFooRequest { + @required + fooId: String, + + @additionalIdentifier + fooAlias: String, + } + + +.. _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 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 aa50eee3e48..a7e9e1fe5c6 100644 --- a/settings.gradle +++ b/settings.gradle @@ -22,3 +22,4 @@ include ":smithy-jsonschema" include ":smithy-openapi" include ":smithy-utils" include ":smithy-protocol-test-traits" +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/AdditionalIdentifierTrait.java b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/AdditionalIdentifierTrait.java new file mode 100644 index 00000000000..83e0588be8b --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/AdditionalIdentifierTrait.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 AdditionalIdentifierTrait extends AnnotationTrait { + public static final ShapeId ID = ShapeId.from("aws.cloudformation#additionalIdentifier"); + + public AdditionalIdentifierTrait(ObjectNode node) { + super(ID, node); + } + + public AdditionalIdentifierTrait() { + this(Node.objectNode()); + } + + public static final class Provider extends AnnotationTrait.Provider { + public Provider() { + super(ID, AdditionalIdentifierTrait::new); + } + } +} diff --git a/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/ExcludePropertyTrait.java b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/ExcludePropertyTrait.java new file mode 100644 index 00000000000..a243b184f3a --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/ExcludePropertyTrait.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 ExcludePropertyTrait extends AnnotationTrait { + public static final ShapeId ID = ShapeId.from("aws.cloudformation#excludeProperty"); + + public ExcludePropertyTrait(ObjectNode node) { + super(ID, node); + } + + public ExcludePropertyTrait() { + this(Node.objectNode()); + } + + public static final class Provider extends AnnotationTrait.Provider { + public Provider() { + super(ID, ExcludePropertyTrait::new); + } + } +} diff --git a/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/MutabilityTrait.java b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/MutabilityTrait.java new file mode 100644 index 00000000000..fb3519b65c4 --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/MutabilityTrait.java @@ -0,0 +1,57 @@ +/* + * 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 the CloudFormation mutability of a structure member. + */ +public final class MutabilityTrait extends StringTrait { + public static final ShapeId ID = ShapeId.from("aws.cloudformation#mutability"); + + public MutabilityTrait(String value, SourceLocation sourceLocation) { + super(ID, value, sourceLocation); + } + + public static final class Provider extends StringTrait.Provider { + public Provider() { + super(ID, MutabilityTrait::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/MutabilityTraitValidator.java b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/MutabilityTraitValidator.java new file mode 100644 index 00000000000..c16fb555320 --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/MutabilityTraitValidator.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 MutabilityTraitValidator extends AbstractValidator { + @Override + public List validate(Model model) { + List events = new ArrayList<>(); + + for (Shape shape : model.getShapesWithTrait(MutabilityTrait.class)) { + MutabilityTrait trait = shape.expectTrait(MutabilityTrait.class); + // Additional identifiers must be able to be read, so write and + // create mutabilities cannot overlap. + if (shape.hasTrait(AdditionalIdentifierTrait.ID) && (trait.isWrite() || trait.isCreate())) { + events.add(error(shape, "Member with the mutability value of \"write-only\" is also marked " + + "as an additional identifier")); + } + } + + return events; + } +} diff --git a/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/PropertyNameTrait.java b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/PropertyNameTrait.java new file mode 100644 index 00000000000..ebdb7da9fb7 --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/PropertyNameTrait.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 PropertyNameTrait extends StringTrait { + public static final ShapeId ID = ShapeId.from("aws.cloudformation#propertyName"); + + public PropertyNameTrait(String value, SourceLocation sourceLocation) { + super(ID, value, sourceLocation); + } + + public PropertyNameTrait(String value) { + this(value, SourceLocation.NONE); + } + + public static final class Provider extends StringTrait.Provider { + public Provider() { + super(ID, PropertyNameTrait::new); + } + } +} diff --git a/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/ResourceIndex.java b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/ResourceIndex.java new file mode 100644 index 00000000000..a50e1e958a6 --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/ResourceIndex.java @@ -0,0 +1,479 @@ +/* + * 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.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.model.traits.Trait; +import software.amazon.smithy.utils.ListUtils; +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 ResourceIndex implements KnowledgeIndex { + + static final Set FULLY_MUTABLE = SetUtils.of( + Mutability.CREATE, Mutability.READ, Mutability.WRITE); + + private final Model model; + private final Map> resourcePropertyMutabilities = new HashMap<>(); + private final Map> resourceExcludedProperties = new HashMap<>(); + private final Map> resourcePrimaryIdentifiers = new HashMap<>(); + private final Map>> resourceAdditionalIdentifiers = new HashMap<>(); + + /** + * CloudFormation-specific property mutability options. + */ + public enum Mutability { + CREATE, + READ, + WRITE + } + + public ResourceIndex(Model model) { + this.model = model; + + OperationIndex operationIndex = OperationIndex.of(model); + model.shapes(ResourceShape.class) + .flatMap(shape -> Trait.flatMapStream(shape, ResourceTrait.class)) + .forEach(pair -> { + ResourceShape resource = pair.getLeft(); + ShapeId resourceId = resource.getId(); + + // Start with the explicit resource identifiers. + resourcePrimaryIdentifiers.put(resourceId, SetUtils.copyOf(resource.getIdentifiers().keySet())); + setIdentifierMutabilities(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(resource, computeResourceAdditionalIdentifiers(input)); + }); + operationIndex.getOutput(operationId).ifPresent(output -> { + updatePropertyMutabilities(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(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(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(resourceId, operationId, input, + SetUtils.of(Mutability.WRITE), this::addWriteMutability); + }); + }); + + // Apply any members found through the trait's additionalSchemas property. + for (ShapeId additionalSchema : pair.getRight().getAdditionalSchemas()) { + StructureShape shape = model.expectShape(additionalSchema, StructureShape.class); + updatePropertyMutabilities(resourceId, null, shape, + SetUtils.of(), Function.identity()); + } + }); + } + + public static ResourceIndex of(Model model) { + return model.getKnowledge(ResourceIndex.class, ResourceIndex::new); + } + + /** + * Get all members of the CloudFormation resource. + * + * @param resource ShapeID of a resource. + * @return Returns all members that map to CloudFormation resource + * properties. + */ + public Map getProperties(ToShapeId resource) { + return resourcePropertyMutabilities.getOrDefault(resource.toShapeId(), MapUtils.of()) + .entrySet().stream() + .filter(entry -> !getExcludedProperties(resource).contains(entry.getValue().getShapeId())) + .collect(MapUtils.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + /** + * Gets the specified member of the CloudFormation resource. + * + * @param resource ShapeID of a resource + * @param propertyName Name of the property to retrieve + * @return The property definition. + */ + public Optional getProperty(ToShapeId resource, String propertyName) { + return Optional.ofNullable(getProperties(resource).get(propertyName)); + } + + /** + * Get create-specifiable-only members of the CloudFormation resource. + * + * These properties can be specified only during resource creation and + * can be returned in a `read` or `list` request. + * + * @param resource ShapeID of a resource. + * @return Returns create-only member names that map to CloudFormation resource + * properties. + */ + public Set getCreateOnlyProperties(ToShapeId resource) { + return getConstrainedProperties(resource, definition -> { + Set mutabilities = definition.getMutabilities(); + return mutabilities.contains(Mutability.CREATE) && !mutabilities.contains(Mutability.WRITE); + }); + } + + /** + * Get read-only members of the CloudFormation resource. + * + * These properties can be returned by a `read` or `list` request, + * but cannot be set by the user. + * + * @param resource ShapeID of a resource. + * @return Returns read-only member names that map to CloudFormation resource + * properties. + */ + public Set getReadOnlyProperties(ToShapeId resource) { + return getConstrainedProperties(resource, definition -> { + Set mutabilities = definition.getMutabilities(); + return mutabilities.size() == 1 && mutabilities.contains(Mutability.READ); + }); + } + + /** + * Get write-only members of the CloudFormation resource. + * + * These properties can be specified by the user, but cannot be + * returned by a `read` or `list` request. + * + * @param resource ShapeID of a resource. + * @return Returns write-only member names that map to CloudFormation resource + * properties. + */ + public Set getWriteOnlyProperties(ToShapeId resource) { + return getConstrainedProperties(resource, definition -> { + Set mutabilities = definition.getMutabilities(); + // Create and non-read properties need to be set as createOnly and writeOnly. + if (mutabilities.size() == 1 && mutabilities.contains(Mutability.CREATE)) { + return true; + } + + // Otherwise, create and update, or update only become writeOnly. + return mutabilities.contains(Mutability.WRITE) && !mutabilities.contains(Mutability.READ); + }); + } + + private Set getConstrainedProperties( + ToShapeId resource, + Predicate constraint + ) { + return getProperties(resource) + .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. + * + * @param resource ShapeID of a resource. + * @return Returns members that have been excluded from a CloudFormation + * resource. + */ + public Set getExcludedProperties(ToShapeId resource) { + return resourceExcludedProperties.getOrDefault(resource.toShapeId(), SetUtils.of()); + } + + /** + * Gets a set of member shape ids that represent the primary way + * to identify a CloudFormation resource. + * + * @param resource ShapeID of a resource. + * @return Returns the identifier set primarily used to access a + * CloudFormation resource. + */ + public Set getPrimaryIdentifiers(ToShapeId resource) { + return resourcePrimaryIdentifiers.get(resource.toShapeId()); + } + + /** + * 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). + * + * @param resource ShapeID of a resource. + * @return Returns identifier sets used to access a CloudFormation resource. + */ + public List> getAdditionalIdentifiers(ToShapeId resource) { + return resourceAdditionalIdentifiers.getOrDefault(resource.toShapeId(), ListUtils.of()); + } + + private void setIdentifierMutabilities(ResourceShape resource) { + Set mutability = getDefaultIdentifierMutabilities(resource); + + ShapeId resourceId = resource.getId(); + + resource.getIdentifiers().forEach((name, shape) -> { + setResourceProperty(resourceId, name, ResourcePropertyDefinition.builder() + .hasExplicitMutability(true) + .mutabilities(mutability) + .shapeId(shape) + .build()); + }); + } + + private void setResourceProperty(ShapeId resourceId, String name, ResourcePropertyDefinition property) { + Map resourceProperties = + resourcePropertyMutabilities.getOrDefault(resourceId, new HashMap<>()); + resourceProperties.put(name, property); + resourcePropertyMutabilities.put(resourceId, resourceProperties); + } + + 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(AdditionalIdentifierTrait.class)) { + continue; + } + + identifiers.add(MapUtils.of(member.getMemberName(), member.getId())); + } + return identifiers; + } + + private void addAdditionalIdentifiers(ResourceShape resource, List> addedIdentifiers) { + if (addedIdentifiers.isEmpty()) { + return; + } + ShapeId resourceId = resource.getId(); + + List> newIdentifierNames = new ArrayList<>(); + // Make sure we have properties entries for the additional identifiers. + for (Map addedIdentifier : addedIdentifiers) { + for (Map.Entry idEntry : addedIdentifier.entrySet()) { + setResourceProperty(resourceId, idEntry.getKey(), ResourcePropertyDefinition.builder() + .mutabilities(SetUtils.of(Mutability.READ)) + .shapeId(idEntry.getValue()) + .build()); + } + newIdentifierNames.add(addedIdentifier.keySet()); + } + + List> currentIdentifiers = + resourceAdditionalIdentifiers.getOrDefault(resourceId, new ArrayList<>()); + currentIdentifiers.addAll(newIdentifierNames); + resourceAdditionalIdentifiers.put(resourceId, currentIdentifiers); + } + + private void updatePropertyMutabilities( + ShapeId resourceId, + ShapeId operationId, + StructureShape propertyContainer, + Set defaultMutabilities, + Function, Set> updater + ) { + addExcludedProperties(resourceId, propertyContainer); + + 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(resourceId, operationId, member)) { + continue; + } + + String memberName = member.getMemberName(); + ResourcePropertyDefinition memberProperty = getProperties(resourceId).get(memberName); + Set explicitMutability = getExplicitMutability(member, memberProperty); + + if (memberProperty != null) { + // Validate that members with the same name target the same shape. + model.getShape(memberProperty.getShapeId()) + .flatMap(Shape::asMemberShape) + .filter(shape -> !member.getTarget().equals(shape.getTarget())) + .ifPresent(shape -> { + throw new RuntimeException(String.format("The derived CloudFormation resource " + + "property for %s is composed of members that target different shapes: %s and %s", + memberName, member.getTarget(), shape.getTarget())); + }); + + // Apply updates to the mutability of the property. + if (!memberProperty.hasExplicitMutability()) { + memberProperty = memberProperty.toBuilder() + .mutabilities(updater.apply(memberProperty.getMutabilities())) + .build(); + } + } else { + // Set the correct mutability for this new property. + Set mutabilities = !explicitMutability.isEmpty() + ? explicitMutability + : defaultMutabilities; + memberProperty = ResourcePropertyDefinition.builder() + .shapeId(member.getId()) + .mutabilities(mutabilities) + .hasExplicitMutability(!explicitMutability.isEmpty()) + .build(); + } + + setResourceProperty(resourceId, memberName, memberProperty); + } + } + + private void addExcludedProperties(ShapeId resourceId, StructureShape propertyContainer) { + Set currentExcludedProperties = + resourceExcludedProperties.getOrDefault(resourceId, new HashSet<>()); + currentExcludedProperties.addAll(propertyContainer.accept(new ExcludedPropertiesVisitor())); + resourceExcludedProperties.put(resourceId, currentExcludedProperties); + } + + private boolean operationMemberIsIdentifier(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( + MemberShape member, + ResourcePropertyDefinition memberProperty + ) { + if (memberProperty != null && memberProperty.hasExplicitMutability()) { + return memberProperty.getMutabilities(); + } + + Optional traitOptional = member.getMemberTrait(model, MutabilityTrait.class); + if (!traitOptional.isPresent()) { + return SetUtils.of(); + } + + MutabilityTrait 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 final class ExcludedPropertiesVisitor extends ShapeVisitor.Default> { + @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(ExcludePropertyTrait.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/ResourcePropertyDefinition.java b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/ResourcePropertyDefinition.java new file mode 100644 index 00000000000..96b895c677c --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/ResourcePropertyDefinition.java @@ -0,0 +1,109 @@ +/* + * 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.Objects; +import java.util.Set; +import software.amazon.smithy.aws.cloudformation.traits.ResourceIndex.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 ResourcePropertyDefinition implements ToSmithyBuilder { + private final ShapeId shapeId; + private final Set mutabilities; + private final boolean hasExplicitMutability; + + private ResourcePropertyDefinition(Builder builder) { + shapeId = Objects.requireNonNull(builder.shapeId); + mutabilities = SetUtils.copyOf(builder.mutabilities); + hasExplicitMutability = builder.hasExplicitMutability; + } + + public static Builder builder() { + return new Builder(); + } + + /** + * Gets the shape ID used to represent this property. + * + * @return Returns the shape ID. + */ + public ShapeId getShapeId() { + return shapeId; + } + + /** + * 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 MutabilityTrait + */ + 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() + .shapeId(shapeId) + .mutabilities(mutabilities); + } + + public static final class Builder implements SmithyBuilder { + private ShapeId shapeId; + private Set mutabilities = new HashSet<>(); + private boolean hasExplicitMutability = false; + + @Override + public ResourcePropertyDefinition build() { + return new ResourcePropertyDefinition(this); + } + + public Builder shapeId(ShapeId shapeId) { + this.shapeId = shapeId; + 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/ResourcePropertyValidator.java b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/ResourcePropertyValidator.java new file mode 100644 index 00000000000..b3c0c4275a6 --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/ResourcePropertyValidator.java @@ -0,0 +1,131 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.aws.cloudformation.traits; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import software.amazon.smithy.model.Model; +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.ShapeId; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.traits.Trait; +import software.amazon.smithy.model.validation.AbstractValidator; +import software.amazon.smithy.model.validation.ValidationEvent; + +/** + * Validates that derived CloudFormation properties all have the same target. + */ +public final class ResourcePropertyValidator extends AbstractValidator { + + @Override + public List validate(Model model) { + List events = new ArrayList<>(); + + OperationIndex operationIndex = OperationIndex.of(model); + model.shapes(ResourceShape.class) + .flatMap(shape -> Trait.flatMapStream(shape, ResourceTrait.class)) + .map(pair -> validateResourceProperties(model, operationIndex, pair.getLeft(), pair.getRight())) + .forEach(events::addAll); + + return events; + } + + private List validateResourceProperties( + Model model, + OperationIndex operationIndex, + ResourceShape resource, + ResourceTrait trait + ) { + List events = new ArrayList<>(); + + Map> resourceProperties = getResourceProperties(model, operationIndex, resource, trait); + for (Map.Entry> property : resourceProperties.entrySet()) { + if (property.getValue().size() > 1) { + events.add(error(resource, String.format("The %s property of the %s CloudFormation resource targets " + + "multiple shapes: %s. This should be resolved in the model or one of the members should be " + + "excluded from the conversion.", trait.getName(), property.getKey(), property.getValue()))); + } + } + + return events; + } + + private Map> getResourceProperties( + Model model, + OperationIndex operationIndex, + ResourceShape resource, + ResourceTrait trait + ) { + Map> resourceProperties = new HashMap<>(); + + // Use the read lifecycle's input to collect the additional identifiers + // and its output to collect readable properties. + resource.getRead().ifPresent(operationId -> { + operationIndex.getOutput(operationId).ifPresent(output -> + computeResourceProperties(resourceProperties, output)); + }); + + // Use the put lifecycle's input to collect put-able properties. + resource.getPut().ifPresent(operationId -> { + operationIndex.getInput(operationId).ifPresent(input -> + computeResourceProperties(resourceProperties, input)); + }); + + // Use the create lifecycle's input to collect creatable properties. + resource.getCreate().ifPresent(operationId -> { + operationIndex.getInput(operationId).ifPresent(input -> + computeResourceProperties(resourceProperties, input)); + }); + + // Use the update lifecycle's input to collect writeable properties. + resource.getUpdate().ifPresent(operationId -> { + operationIndex.getInput(operationId).ifPresent(input -> + computeResourceProperties(resourceProperties, input)); + }); + + // Apply any members found through the trait's additionalSchemas property. + for (ShapeId additionalSchema : trait.getAdditionalSchemas()) { + StructureShape shape = model.expectShape(additionalSchema, StructureShape.class); + computeResourceProperties(resourceProperties, shape); + } + + return resourceProperties; + } + + private void computeResourceProperties(Map> resourceProperties, StructureShape shape) { + for (Map.Entry memberEntry : shape.getAllMembers().entrySet()) { + MemberShape memberShape = memberEntry.getValue(); + + // Skip explicitly excluded property definitions. + if (memberShape.hasTrait(ExcludePropertyTrait.ID)) { + continue; + } + + // Use the correct property name. + String propertyName = memberShape.getTrait(PropertyNameTrait.class) + .map(PropertyNameTrait::getValue) + .orElse(memberEntry.getKey()); + resourceProperties.computeIfAbsent(propertyName, name -> new TreeSet<>()) + .add(memberShape.getTarget()); + } + } +} diff --git a/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/ResourceTrait.java b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/ResourceTrait.java new file mode 100644 index 00000000000..3c2d2584df3 --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/ResourceTrait.java @@ -0,0 +1,148 @@ +/* + * 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.node.ArrayNode; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.AbstractTrait; +import software.amazon.smithy.model.traits.AbstractTraitBuilder; +import software.amazon.smithy.model.traits.Trait; +import software.amazon.smithy.utils.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 ResourceTrait extends AbstractTrait implements ToSmithyBuilder { + public static final ShapeId ID = ShapeId.from("aws.cloudformation#resource"); + private static final String NAME = "name"; + private static final String ADDITIONAL_SCHEMAS = "additionalSchemas"; + private static final List PROPERTIES = ListUtils.of(NAME, ADDITIONAL_SCHEMAS); + + private final String defaultName; + private final String name; + private final List additionalSchemas; + + private ResourceTrait(Builder builder) { + super(ID, builder.getSourceLocation()); + defaultName = builder.defaultName; + name = builder.name; + additionalSchemas = ListUtils.copyOf(builder.additionalSchemas); + } + + /** + * Get the AWS CloudFormation resource name. + * + * @return Returns the name. + */ + public String getName() { + return name == null ? defaultName : 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() { + ObjectNode node = Node.objectNode(); + if (name != null) { + node = node.withMember(NAME, name); + } + if (!additionalSchemas.isEmpty()) { + ArrayNode schemas = additionalSchemas.stream() + .map(ShapeId::toString) + .map(Node::from) + .collect(ArrayNode.collect()); + node = node.withMember("additionalSchemas", schemas); + } + return node; + } + + @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) { + Builder builder = builder().sourceLocation(value); + ObjectNode objectNode = value.expectObjectNode(); + // Use a hidden defaultName property so we don't write out the + // Shape's name when defaulting. + builder.defaultName(target.getName()); + objectNode.getStringMember(NAME).ifPresent(node -> builder.name(node.getValue())); + // Convert this ArrayNode of StringNodes to a List of ShapeId + objectNode.getArrayMember(ADDITIONAL_SCHEMAS) + .map(array -> array.getElementsAs(n -> ShapeId.from(n.expectStringNode().getValue()))) + .ifPresent(builder::additionalSchemas); + return builder.build(); + } + } + + public static final class Builder extends AbstractTraitBuilder { + private String defaultName; + private String name; + private final List additionalSchemas = new ArrayList<>(); + + private Builder() {} + + @Override + public ResourceTrait build() { + return new ResourceTrait(this); + } + + public Builder defaultName(String defaultName) { + this.defaultName = defaultName; + return 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..ce4de46273e --- /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.AdditionalIdentifierTrait$Provider +software.amazon.smithy.aws.cloudformation.traits.ExcludePropertyTrait$Provider +software.amazon.smithy.aws.cloudformation.traits.MutabilityTrait$Provider +software.amazon.smithy.aws.cloudformation.traits.PropertyNameTrait$Provider +software.amazon.smithy.aws.cloudformation.traits.ResourceTrait$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..409035fecb3 --- /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.MutabilityTraitValidator +software.amazon.smithy.aws.cloudformation.traits.ResourcePropertyValidator 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..c80141f088e --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/main/resources/META-INF/smithy/aws.cloudformation.smithy @@ -0,0 +1,99 @@ +$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: ["aws.cloudformation#excludeProperty"] +) +@tags(["diff.error.remove"]) +structure additionalIdentifier {} + +/// The propertyName 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 propertyName + +/// Indicates that structure member should not be included in generated +/// CloudFormation resource definitions. +@trait( + selector: "structure > member", + conflicts: [ + "aws.cloudformation#additionalIdentifier", + "aws.cloudformation#mutability", + ] +) +@tags(["diff.error.add"]) +structure excludeProperty {} + +/// Indicates that the CloudFormation property generated from this has the +/// specified mutability. +@trait( + selector: "structure > member", + conflicts: ["aws.cloudformation#excludeProperty"] +) +@enum([ + { + value: "full", + name: "FULL", + documentation: """ + Indicates that the CloudFormation property generated from this + member does not have any mutability restrictions.""", + }, + { + 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 mutability + +/// Indicates that a Smithy resource is a CloudFormation resource. +@trait(selector: "resource") +@tags(["diff.error.add", "diff.error.remove"]) +structure resource { + /// 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/MutabilityTraitTest.java b/smithy-aws-cloudformation-traits/src/test/java/software/amazon/smithy/aws/cloudformation/traits/MutabilityTraitTest.java new file mode 100644 index 00000000000..c4ca91832ad --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/test/java/software/amazon/smithy/aws/cloudformation/traits/MutabilityTraitTest.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 MutabilityTraitTest { + + @Test + public void loadsTraitWithString() { + Node node = Node.from("full"); + TraitFactory provider = TraitFactory.createServiceFactory(); + Optional trait = provider.createTrait( + ShapeId.from("aws.cloudformation#mutability"), ShapeId.from("ns.qux#Foo"), node); + + assertTrue(trait.isPresent()); + assertThat(trait.get(), instanceOf(MutabilityTrait.class)); + MutabilityTrait mutabilityTrait = (MutabilityTrait) trait.get(); + assertThat(mutabilityTrait.getValue(), equalTo("full")); + assertTrue(mutabilityTrait.isFullyMutable()); + assertThat(mutabilityTrait.toNode(), equalTo(node)); + } + +} diff --git a/smithy-aws-cloudformation-traits/src/test/java/software/amazon/smithy/aws/cloudformation/traits/PropertyNameTraitTest.java b/smithy-aws-cloudformation-traits/src/test/java/software/amazon/smithy/aws/cloudformation/traits/PropertyNameTraitTest.java new file mode 100644 index 00000000000..d25966d7cf4 --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/test/java/software/amazon/smithy/aws/cloudformation/traits/PropertyNameTraitTest.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 PropertyNameTraitTest { + + @Test + public void loadsTraitWithString() { + Node node = Node.from("Text"); + TraitFactory provider = TraitFactory.createServiceFactory(); + Optional trait = provider.createTrait( + ShapeId.from("aws.cloudformation#propertyName"), ShapeId.from("ns.qux#Foo"), node); + + assertTrue(trait.isPresent()); + assertThat(trait.get(), instanceOf(PropertyNameTrait.class)); + PropertyNameTrait propertyNameTrait = (PropertyNameTrait) trait.get(); + assertThat(propertyNameTrait.getValue(), equalTo("Text")); + assertThat(propertyNameTrait.toNode(), equalTo(node)); + } +} diff --git a/smithy-aws-cloudformation-traits/src/test/java/software/amazon/smithy/aws/cloudformation/traits/ResourceIndexTest.java b/smithy-aws-cloudformation-traits/src/test/java/software/amazon/smithy/aws/cloudformation/traits/ResourceIndexTest.java new file mode 100644 index 00000000000..10f2c057e54 --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/test/java/software/amazon/smithy/aws/cloudformation/traits/ResourceIndexTest.java @@ -0,0 +1,194 @@ +/* + * 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.aMapWithSize; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +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.Assertions; +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.ResourceIndex.Mutability; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.node.ExpectationNotMetException; +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 ResourceIndexTest { + 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 final ShapeId MOO = ShapeId.from("smithy.example#MooResource"); + + private static Model model; + private static ResourceIndex resourceIndex; + + @BeforeAll + public static void loadTestModel() { + model = Model.assembler() + .discoverModels(ResourceIndexTest.class.getClassLoader()) + .addImport(ResourceIndexTest.class.getResource("test-service.smithy")) + .assemble() + .unwrap(); + resourceIndex = ResourceIndex.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", ResourceIndex.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", ResourceIndex.FULLY_MUTABLE, + "barValidAdditionalProperty", SetUtils.of(), + "barImplicitReadProperty", SetUtils.of(Mutability.READ), + "barImplicitFullProperty", ResourceIndex.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), + "bazImplicitFullyMutableProperty", ResourceIndex.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), + resourceIndex.getPrimaryIdentifiers(data.resourceId), + containsInAnyOrder(data.identifiers.toArray())); + } + + @ParameterizedTest + @MethodSource("data") + public void detectsAdditionalIdentifiers(ResourceData data) { + assertThat(String.format("Failure for resource %s.", data.resourceId), + resourceIndex.getAdditionalIdentifiers(data.resourceId), + containsInAnyOrder(data.additionalIdentifiers.toArray())); + } + + @ParameterizedTest + @MethodSource("data") + public void findsAllProperties(ResourceData data) { + Map properties = resourceIndex.getProperties(data.resourceId); + + 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 = resourceIndex.getCreateOnlyProperties(data.resourceId); + + assertThat(String.format("Failure for resource %s.", data.resourceId), + properties, containsInAnyOrder(data.createOnlyProperties.toArray())); + } + + @ParameterizedTest + @MethodSource("data") + public void findsReadOnlyProperties(ResourceData data) { + Set properties = resourceIndex.getReadOnlyProperties(data.resourceId); + + assertThat(String.format("Failure for resource %s.", data.resourceId), + properties, containsInAnyOrder(data.readOnlyProperties.toArray())); + } + + @ParameterizedTest + @MethodSource("data") + public void findsWriteOnlyProperties(ResourceData data) { + Set properties = resourceIndex.getWriteOnlyProperties(data.resourceId); + + assertThat(String.format("Failure for resource %s.", data.resourceId), + properties, containsInAnyOrder(data.writeOnlyProperties.toArray())); + } + + @Test + public void setsProperIdentifierMutability() { + Map fooProperties = resourceIndex.getProperties(FOO); + Map barProperties = resourceIndex.getProperties(BAR); + + assertThat(fooProperties.get("fooId").getMutabilities(), containsInAnyOrder(Mutability.READ)); + assertThat(barProperties.get("barId").getMutabilities(), containsInAnyOrder(Mutability.CREATE, Mutability.READ)); + } + + @Test + public void handlesAdditionalSchemaProperty() { + Map barProperties = resourceIndex.getProperties(BAR); + + 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/ResourceTraitTest.java b/smithy-aws-cloudformation-traits/src/test/java/software/amazon/smithy/aws/cloudformation/traits/ResourceTraitTest.java new file mode 100644 index 00000000000..605aab71b88 --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/test/java/software/amazon/smithy/aws/cloudformation/traits/ResourceTraitTest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.aws.cloudformation.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 ResourceTraitTest { + @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(ResourceTrait.class)); + ResourceTrait fooTrait = fooResource.expectTrait(ResourceTrait.class); + assertThat(fooTrait.getName(), equalTo("FooResource")); + assertTrue(fooTrait.getAdditionalSchemas().isEmpty()); + + Shape barResource = result.expectShape(ShapeId.from("smithy.example#BarResource")); + assertTrue(barResource.hasTrait(ResourceTrait.class)); + ResourceTrait barTrait = barResource.expectTrait(ResourceTrait.class); + assertThat(barTrait.getName(), 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(); + + assertThat( + result.expectShape(ShapeId.from("smithy.example#FooResource")) + .expectTrait(ResourceTrait.class).getName(), + equalTo("FooResource")); + assertThat( + result.expectShape(ShapeId.from("smithy.example#BarResource")) + .expectTrait(ResourceTrait.class).getName(), + equalTo("Bar")); + assertThat( + result.expectShape(ShapeId.from("smithy.example#BazResource")) + .expectTrait(ResourceTrait.class).getName(), + 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..1cc6f413cf3 --- /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#resource + +@resource +resource FooResource { + identifiers: { + fooId: FooId + } +} + +@resource( + 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..db0df361026 --- /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 AdditionalSchemasConflictResource property of the bar CloudFormation resource targets multiple shapes: [smithy.api#Boolean, smithy.api#String]. This should be resolved in the model or one of the members should be excluded from the conversion. | ResourceProperty +[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..367cd2f9efb --- /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#resource + +service AdditionalSchemasConflict { + version: "2020-07-02", + resources: [ + AdditionalSchemasConflictResource, + ], +} + +@resource(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/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..ca343b1c73b --- /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#mutability`: String value provided for `aws.cloudformation#mutability` 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..6e8fa1a6394 --- /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#mutability + +structure FooStructure { + @mutability("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..652b54b840c --- /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 LifecycleConflictResource property of the bar CloudFormation resource targets multiple shapes: [smithy.api#Boolean, smithy.api#String]. This should be resolved in the model or one of the members should be excluded from the conversion. | ResourceProperty 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..1bf75a730ba --- /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#resource + +service LifecycleConflict { + version: "2020-07-02", + resources: [ + LifecycleConflictResource, + ], +} + +@resource +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..000627dcdb1 --- /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-only" is also marked as an additional identifier | MutabilityTrait 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..1103220d3f3 --- /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#additionalIdentifier +use aws.cloudformation#mutability + +structure FooStructure { + @additionalIdentifier + @mutability("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..f463c755509 --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/test-service.smithy @@ -0,0 +1,238 @@ +$version: "1.0" + +namespace smithy.example + +use aws.cloudformation#resource +use aws.cloudformation#additionalIdentifier +use aws.cloudformation#excludeProperty +use aws.cloudformation#mutability + +service TestService { + version: "2020-07-02", + resources: [ + FooResource, + BarResource, + ], +} + +/// The Foo resource is cool. +@resource +resource FooResource { + identifiers: { + fooId: FooId, + }, + create: CreateFooOperation, + read: GetFooOperation, + update: UpdateFooOperation, +} + +operation CreateFooOperation { + input: CreateFooRequest, + output: CreateFooResponse, +} + +structure CreateFooRequest { + @mutability("create") + 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, + + @mutability("read") + fooValidReadProperty: String, + + fooValidCreateReadProperty: String, + + fooValidFullyMutableProperty: ComplexProperty, +} + +operation UpdateFooOperation { + input: UpdateFooRequest, + output: UpdateFooResponse, +} + +structure UpdateFooRequest { + @required + fooId: FooId, + + @mutability("write") + fooValidWriteProperty: String, + + fooValidFullyMutableProperty: ComplexProperty, +} + +structure UpdateFooResponse { + fooId: FooId, + + fooValidReadProperty: String, + + fooValidFullyMutableProperty: ComplexProperty, +} + +/// A Bar resource, not that kind of bar though. +@resource(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, + + @additionalIdentifier + arn: String, +} + +structure GetBarResponse { + barId: BarId, + barImplicitReadProperty: String, + barImplicitFullProperty: String, + + @mutability("full") + barExplicitMutableProperty: String, +} + +operation ExtraBarOperation { + input: ExtraBarRequest, +} + +structure ExtraBarRequest { + @required + barId: BarId, + + barValidAdditionalProperty: String, + + @excludeProperty + barValidExcludedProperty: String, +} + +/// This is an herb. +@resource("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, + + 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, + 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, +}