diff --git a/docs/source-2.0/aws/aws-cloudformation.rst b/docs/source-2.0/aws/aws-cloudformation.rst index 962350ddb28..08726442207 100644 --- a/docs/source-2.0/aws/aws-cloudformation.rst +++ b/docs/source-2.0/aws/aws-cloudformation.rst @@ -55,6 +55,11 @@ supports the following members: Members of these structures with the same names MUST resolve to the same target. See :ref:`aws-cloudformation-property-deriviation` for more information. + * - primaryIdentifier + - ``string`` + - **Deprecated** An alternative resource property to use as the primary + identifier for the CloudFormation resource. The value MUST be the name + of a property on the resource shape that targets a string shape. The following example defines a simple resource that is also a CloudFormation resource: diff --git a/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourceIndex.java b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourceIndex.java index fc72dc1eb14..ef8f94c1241 100644 --- a/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourceIndex.java +++ b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourceIndex.java @@ -174,6 +174,14 @@ public CfnResourceIndex(Model model) { }); } + // If the primary identifier is rerouted, the derived identifiers are + // additional identifiers for the resource. + if (trait.getPrimaryIdentifier().isPresent()) { + CfnResource tempResource = builder.build(); + builder.addAdditionalIdentifier(SetUtils.copyOf(tempResource.getPrimaryIdentifiers())); + builder.primaryIdentifiers(SetUtils.of(trait.getPrimaryIdentifier().get())); + } + resourceDefinitions.put(resourceId, builder.build()); }); } diff --git a/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourcePropertyValidator.java b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourcePropertyValidator.java index 6d828267347..f841d9efbca 100644 --- a/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourcePropertyValidator.java +++ b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourcePropertyValidator.java @@ -4,6 +4,8 @@ */ package software.amazon.smithy.aws.cloudformation.traits; +import static java.lang.String.format; + import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -12,6 +14,7 @@ import java.util.TreeSet; import software.amazon.smithy.model.Model; 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.validation.AbstractValidator; import software.amazon.smithy.model.validation.ValidationEvent; @@ -27,10 +30,9 @@ public List validate(Model model) { List events = new ArrayList<>(); CfnResourceIndex cfnResourceIndex = CfnResourceIndex.of(model); - model.shapes(ResourceShape.class) - .filter(shape -> shape.hasTrait(CfnResourceTrait.ID)) - .map(shape -> validateResource(model, cfnResourceIndex, shape)) - .forEach(events::addAll); + for (ResourceShape resource : model.getResourceShapesWithTrait(CfnResourceTrait.class)) { + events.addAll(validateResource(model, cfnResourceIndex, resource)); + } return events; } @@ -44,17 +46,48 @@ private List validateResource( List events = new ArrayList<>(); String resourceName = trait.getName().orElse(resource.getId().getName()); - cfnResourceIndex.getResource(resource) - .map(CfnResource::getProperties) - .ifPresent(properties -> { - for (Map.Entry property : properties.entrySet()) { - validateResourceProperty(model, resource, resourceName, property).ifPresent(events::add); - } - }); + if (trait.getPrimaryIdentifier().isPresent()) { + validateResourcePrimaryIdentifier(model, resource, trait.getPrimaryIdentifier().get()) + .ifPresent(events::add); + } + + Optional cfnResourceOptional = cfnResourceIndex.getResource(resource); + if (cfnResourceOptional.isPresent()) { + for (Map.Entry property : cfnResourceOptional.get() + .getProperties() + .entrySet()) { + validateResourceProperty(model, resource, resourceName, property).ifPresent(events::add); + } + } return events; } + private Optional validateResourcePrimaryIdentifier( + Model model, + ResourceShape resource, + String primaryIdentifier + ) { + Map properties = resource.getProperties(); + if (!properties.containsKey(primaryIdentifier)) { + return Optional.of(error(resource, + resource.expectTrait(CfnResourceTrait.class), + format("The alternative resource primary identifier, `%s`, must be a property of the resource.", + primaryIdentifier))); + } + + Shape propertyTarget = model.expectShape(properties.get(primaryIdentifier)); + if (!propertyTarget.isStringShape() && !propertyTarget.isEnumShape()) { + return Optional.of(error(resource, + resource.expectTrait(CfnResourceTrait.class), + format("The alternative resource primary identifier, `%s`, targets a `%s` shape, it must target a `string`.", + primaryIdentifier, + propertyTarget.getType()))); + } + + return Optional.empty(); + } + private Optional validateResourceProperty( Model model, ResourceShape resource, @@ -72,7 +105,7 @@ private Optional validateResourceProperty( if (propertyTargets.size() > 1) { return Optional.of(error(resource, - String.format("The `%s` property of the generated `%s` " + format("The `%s` property of the generated `%s` " + "CloudFormation resource targets multiple shapes: %s. Reusing member names that " + "target different shapes can cause confusion for users of the API. This target " + "discrepancy must either be resolved in the model or one of the members must be " diff --git a/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourceTrait.java b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourceTrait.java index fb5c4e92abb..952396530d4 100644 --- a/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourceTrait.java +++ b/smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourceTrait.java @@ -25,11 +25,13 @@ public final class CfnResourceTrait extends AbstractTrait private final String name; private final List additionalSchemas; + private final String primaryIdentifier; private CfnResourceTrait(Builder builder) { super(ID, builder.getSourceLocation()); name = builder.name; additionalSchemas = ListUtils.copyOf(builder.additionalSchemas); + primaryIdentifier = builder.primaryIdentifier; } /** @@ -50,6 +52,15 @@ public List getAdditionalSchemas() { return additionalSchemas; } + /** + * Gets the alternative resource property to use as the primary identifier for the CloudFormation resource. + * + * @return Returns the optional alternative primary identifier. + */ + public Optional getPrimaryIdentifier() { + return Optional.ofNullable(primaryIdentifier); + } + public static Builder builder() { return new Builder(); } @@ -83,6 +94,7 @@ public Trait createTrait(ShapeId target, Node value) { public static final class Builder extends AbstractTraitBuilder { private String name; private final List additionalSchemas = new ArrayList<>(); + private String primaryIdentifier; private Builder() {} @@ -106,5 +118,10 @@ public Builder additionalSchemas(List additionalSchemas) { this.additionalSchemas.addAll(additionalSchemas); return this; } + + public Builder primaryIdentifier(String primaryIdentifier) { + this.primaryIdentifier = primaryIdentifier; + return this; + } } } 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 index d27bcf886e4..e9676fa0cf1 100644 --- 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 @@ -6,8 +6,8 @@ namespace aws.cloudformation /// additional identifier for the resource. @unstable @trait( - selector: "structure > :test(member > string)", - conflicts: [cfnExcludeProperty], + selector: "structure > :test(member > string)" + conflicts: [cfnExcludeProperty] breakingChanges: [{change: "remove"}] ) structure cfnAdditionalIdentifier {} @@ -16,7 +16,7 @@ structure cfnAdditionalIdentifier {} /// to differ from a structure member name used in the model. @unstable @trait( - selector: "structure > member", + selector: "structure > member" breakingChanges: [{change: "any"}] ) string cfnName @@ -25,12 +25,12 @@ string cfnName /// CloudFormation resource definitions. @unstable @trait( - selector: "structure > member", + selector: "structure > member" conflicts: [ - cfnAdditionalIdentifier, - cfnMutability, + cfnAdditionalIdentifier + cfnMutability cfnDefaultValue - ], + ] breakingChanges: [{change: "add"}] ) structure cfnExcludeProperty {} @@ -39,7 +39,7 @@ structure cfnExcludeProperty {} /// for the property of the CloudFormation resource. @unstable @trait( - selector: "resource > operation -[input, output]-> structure > member", + selector: "resource > operation -[input, output]-> structure > member" conflicts: [cfnExcludeProperty] ) structure cfnDefaultValue {} @@ -48,7 +48,7 @@ structure cfnDefaultValue {} /// when part of a CloudFormation resource. @unstable @trait( - selector: "structure > member", + selector: "structure > member" conflicts: [cfnExcludeProperty] ) enum cfnMutability { @@ -86,16 +86,22 @@ enum cfnMutability { /// Indicates that a Smithy resource is a CloudFormation resource. @unstable @trait( - selector: "resource", + selector: "resource" breakingChanges: [{change: "presence"}] ) structure cfnResource { /// Provides a custom CloudFormation resource name. - name: String, + name: String /// A list of additional shape IDs of structures that will have their /// properties added to the CloudFormation resource. - additionalSchemas: StructureIdList, + additionalSchemas: StructureIdList + + /// An alternative resource property to use as the primary identifier + /// for the CloudFormation resource. The value MUST be the name of a + /// property on the resource shape that targets a string shape. + @deprecated(message: "Prefer the resource's identifiers when generating resource schemas.") + primaryIdentifier: String } @private diff --git a/smithy-aws-cloudformation-traits/src/test/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourceIndexTest.java b/smithy-aws-cloudformation-traits/src/test/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourceIndexTest.java index 527a0296be7..3b7a50ff562 100644 --- a/smithy-aws-cloudformation-traits/src/test/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourceIndexTest.java +++ b/smithy-aws-cloudformation-traits/src/test/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourceIndexTest.java @@ -28,6 +28,7 @@ public class CfnResourceIndexTest { private static final ShapeId FOO = ShapeId.from("smithy.example#FooResource"); private static final ShapeId BAR = ShapeId.from("smithy.example#BarResource"); private static final ShapeId BAZ = ShapeId.from("smithy.example#BazResource"); + private static final ShapeId TAD = ShapeId.from("smithy.example#TadResource"); private static Model model; private static CfnResourceIndex cfnResourceIndex; @@ -118,7 +119,20 @@ public static Collection data() { bazResource.readOnlyProperties = SetUtils.of("bazId", "bazImplicitReadProperty"); bazResource.writeOnlyProperties = SetUtils.of("bazImplicitWriteProperty"); - return ListUtils.of(fooResource, barResource, bazResource); + ResourceData tadResource = new ResourceData(); + tadResource.resourceId = TAD; + tadResource.identifiers = SetUtils.of("tadArn"); + tadResource.additionalIdentifiers = ListUtils.of(SetUtils.of("tadId")); + tadResource.mutabilities = MapUtils.of( + "tadId", + SetUtils.of(Mutability.READ), + "tadArn", + SetUtils.of(Mutability.READ)); + tadResource.createOnlyProperties = SetUtils.of(); + tadResource.readOnlyProperties = SetUtils.of("tadId", "tadArn"); + tadResource.writeOnlyProperties = SetUtils.of(); + + return ListUtils.of(fooResource, tadResource, bazResource, tadResource); } @ParameterizedTest diff --git a/smithy-aws-cloudformation-traits/src/test/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourceTraitTest.java b/smithy-aws-cloudformation-traits/src/test/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourceTraitTest.java index ec8fc475321..1a89b450960 100644 --- a/smithy-aws-cloudformation-traits/src/test/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourceTraitTest.java +++ b/smithy-aws-cloudformation-traits/src/test/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourceTraitTest.java @@ -7,6 +7,7 @@ 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.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -36,6 +37,14 @@ public void loadsFromModel() { assertThat(barTrait.getName().get(), equalTo("CustomResource")); assertFalse(barTrait.getAdditionalSchemas().isEmpty()); assertThat(barTrait.getAdditionalSchemas(), contains(ShapeId.from("smithy.example#ExtraBarRequest"))); + + Shape tadResource = result.expectShape(ShapeId.from("smithy.example#TadResource")); + assertTrue(tadResource.hasTrait(CfnResourceTrait.class)); + CfnResourceTrait tadTrait = tadResource.expectTrait(CfnResourceTrait.class); + assertFalse(tadTrait.getName().isPresent()); + assertTrue(tadTrait.getAdditionalSchemas().isEmpty()); + assertTrue(tadTrait.getPrimaryIdentifier().isPresent()); + assertEquals("tadArn", tadTrait.getPrimaryIdentifier().get()); } @Test 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 index 63a37135587..87d3116036e 100644 --- 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 @@ -12,23 +12,57 @@ resource FooResource { } @cfnResource( - name: "CustomResource", + name: "CustomResource" additionalSchemas: [ExtraBarRequest] ) resource BarResource { identifiers: { barId: BarId - }, - operations: [ExtraBarOperation], + } + operations: [ExtraBarOperation] +} + +@cfnResource(primaryIdentifier: "tadArn") +resource TadResource { + identifiers: { + tadId: String + } + properties: { + tadArn: String + } + create: CreateTad + read: GetTad +} + +operation CreateTad { + input := {} + output := { + @required + tadId: String + tadArn: String + } +} + +@readonly +operation GetTad { + input := { + @required + tadId: String + } + output := { + @required + tadId: String + tadArn: String + } } operation ExtraBarOperation { - input: ExtraBarRequest, + input: ExtraBarRequest } structure ExtraBarRequest { @required - barId: BarId, + barId: BarId } string FooId diff --git a/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/primary-identifier-swap.errors b/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/primary-identifier-swap.errors new file mode 100644 index 00000000000..6d37517d2ae --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/primary-identifier-swap.errors @@ -0,0 +1,5 @@ +[ERROR] smithy.example#BarResource: The alternative resource primary identifier, `barArn`, must be a property of the resource. | CfnResourceProperty +[ERROR] smithy.example#BazResource: The alternative resource primary identifier, `bazArn`, targets a `integer` shape, it must target a `string`. | CfnResourceProperty +[WARNING] smithy.example#FooResource: This shape applies a trait that is unstable: aws.cloudformation#cfnResource | UnstableTrait.aws.cloudformation#cfnResource +[WARNING] smithy.example#BarResource: This shape applies a trait that is unstable: aws.cloudformation#cfnResource | UnstableTrait.aws.cloudformation#cfnResource +[WARNING] smithy.example#BazResource: This shape applies a trait that is unstable: aws.cloudformation#cfnResource | UnstableTrait.aws.cloudformation#cfnResource diff --git a/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/primary-identifier-swap.smithy b/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/primary-identifier-swap.smithy new file mode 100644 index 00000000000..366946ac8bb --- /dev/null +++ b/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/primary-identifier-swap.smithy @@ -0,0 +1,104 @@ +$version: "2.0" + +namespace smithy.example + +use aws.cloudformation#cfnResource + +@cfnResource(primaryIdentifier: "fooArn") +resource FooResource { + identifiers: { + fooId: String + } + properties: { + fooArn: String + } + create: CreateFoo + read: GetFoo +} + +operation CreateFoo { + input := {} + output := { + @required + fooId: String + fooArn: String + } +} + +@readonly +operation GetFoo { + input := { + @required + fooId: String + } + output := { + @required + fooId: String + fooArn: String + } +} + +@cfnResource(primaryIdentifier: "barArn") +resource BarResource { + identifiers: { + barId: String + } + create: CreateBar + read: GetBar +} + +operation CreateBar { + input := {} + output := { + @required + barId: String + barArn: String + } +} + +@readonly +operation GetBar { + input := { + @required + barId: String + } + output := { + @required + barId: String + barArn: String + } +} + +@cfnResource(primaryIdentifier: "bazArn") +resource BazResource { + identifiers: { + bazId: String + } + properties: { + bazArn: Integer + } + create: CreateBaz + read: GetBaz +} + +operation CreateBaz { + input := {} + output := { + @required + bazId: String + bazArn: Integer + } +} + +@readonly +operation GetBaz { + input := { + @required + bazId: String + } + output := { + @required + bazId: String + bazArn: Integer + } +} 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 index bd25ef59eca..1ff2ba203d6 100644 --- 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 @@ -9,225 +9,260 @@ use aws.cloudformation#cfnMutability use aws.cloudformation#cfnDefaultValue service TestService { - version: "2020-07-02", + version: "2020-07-02" resources: [ - FooResource, - BarResource, - ], + FooResource + BarResource + TadResource + ] } /// The Foo resource is cool. @cfnResource resource FooResource { identifiers: { - fooId: FooId, - }, - create: CreateFooOperation, - read: GetFooOperation, - update: UpdateFooOperation, + fooId: FooId + } + create: CreateFooOperation + read: GetFooOperation + update: UpdateFooOperation } operation CreateFooOperation { - input: CreateFooRequest, - output: CreateFooResponse, + input: CreateFooRequest + output: CreateFooResponse } structure CreateFooRequest { - fooValidCreateProperty: String, + fooValidCreateProperty: String - fooValidCreateReadProperty: String, + fooValidCreateReadProperty: String - fooValidFullyMutableProperty: ComplexProperty, + fooValidFullyMutableProperty: ComplexProperty } structure CreateFooResponse { - fooId: FooId, + fooId: FooId } @readonly operation GetFooOperation { - input: GetFooRequest, - output: GetFooResponse, + input: GetFooRequest + output: GetFooResponse } structure GetFooRequest { @required - fooId: FooId, + fooId: FooId } structure GetFooResponse { @cfnDefaultValue - fooId: FooId, + fooId: FooId - fooValidReadProperty: String, + fooValidReadProperty: String - fooValidCreateReadProperty: String, + fooValidCreateReadProperty: String - fooValidFullyMutableProperty: ComplexProperty, + fooValidFullyMutableProperty: ComplexProperty } operation UpdateFooOperation { - input: UpdateFooRequest, - output: UpdateFooResponse, + input: UpdateFooRequest + output: UpdateFooResponse } structure UpdateFooRequest { @required - fooId: FooId, + fooId: FooId @cfnMutability("write") - fooValidWriteProperty: String, + fooValidWriteProperty: String - fooValidFullyMutableProperty: ComplexProperty, + fooValidFullyMutableProperty: ComplexProperty } structure UpdateFooResponse { - fooId: FooId, + fooId: FooId - fooValidReadProperty: String, + fooValidReadProperty: String - fooValidFullyMutableProperty: ComplexProperty, + fooValidFullyMutableProperty: ComplexProperty } -/// A Bar resource, not that kind of bar though. +/// A Bar resource not that kind of bar though. @cfnResource(name: "Bar", additionalSchemas: [ExtraBarRequest]) resource BarResource { identifiers: { - barId: BarId, - }, - put: PutBarOperation, - read: GetBarOperation, - operations: [ExtraBarOperation], - resources: [BazResource], + barId: BarId + } + put: PutBarOperation + read: GetBarOperation + operations: [ExtraBarOperation] + resources: [BazResource] } @idempotent operation PutBarOperation { - input: PutBarRequest, + input: PutBarRequest } structure PutBarRequest { @required - barId: BarId, + barId: BarId - barImplicitFullProperty: String, + barImplicitFullProperty: String } @readonly operation GetBarOperation { - input: GetBarRequest, - output: GetBarResponse, + input: GetBarRequest + output: GetBarResponse } structure GetBarRequest { @required - barId: BarId, + barId: BarId @cfnAdditionalIdentifier - arn: String, + arn: String } structure GetBarResponse { - barId: BarId, - barImplicitReadProperty: String, - barImplicitFullProperty: String, + barId: BarId + barImplicitReadProperty: String + barImplicitFullProperty: String @cfnMutability("full") - barExplicitMutableProperty: String, + barExplicitMutableProperty: String } operation ExtraBarOperation { - input: ExtraBarRequest, + input: ExtraBarRequest } structure ExtraBarRequest { @required - barId: BarId, + barId: BarId - barValidAdditionalProperty: String, + barValidAdditionalProperty: String @cfnExcludeProperty - barValidExcludedProperty: String, + barValidExcludedProperty: String } /// This is an herb. @cfnResource("name": "Basil") resource BazResource { identifiers: { - barId: BarId, - bazId: BazId, - }, - create: CreateBazOperation, - read: GetBazOperation, - update: UpdateBazOperation, + barId: BarId + bazId: BazId + } + create: CreateBazOperation + read: GetBazOperation + update: UpdateBazOperation } operation CreateBazOperation { - input: CreateBazRequest, - output: CreateBazResponse, + input: CreateBazRequest + output: CreateBazResponse } structure CreateBazRequest { @required - barId: BarId, + barId: BarId - bazExplicitMutableProperty: String, - bazImplicitCreateProperty: String, - bazImplicitFullyMutableProperty: String, - bazImplicitWriteProperty: String, + bazExplicitMutableProperty: String + bazImplicitCreateProperty: String + bazImplicitFullyMutableProperty: String + bazImplicitWriteProperty: String } structure CreateBazResponse { - barId: BarId, - bazId: BazId, + barId: BarId + bazId: BazId } @readonly operation GetBazOperation { - input: GetBazRequest, - output: GetBazResponse, + input: GetBazRequest + output: GetBazResponse } structure GetBazRequest { @required - barId: BarId, + barId: BarId @required - bazId: BazId, + bazId: BazId } structure GetBazResponse { - barId: BarId, - bazId: BazId, + barId: BarId + bazId: BazId @cfnMutability("full") - bazExplicitMutableProperty: String, - bazImplicitCreateProperty: String, - bazImplicitReadProperty: String, - bazImplicitFullyMutableProperty: String, + bazExplicitMutableProperty: String + bazImplicitCreateProperty: String + bazImplicitReadProperty: String + bazImplicitFullyMutableProperty: String } operation UpdateBazOperation { - input: UpdateBazRequest, - output: UpdateBazResponse, + input: UpdateBazRequest + output: UpdateBazResponse } structure UpdateBazRequest { @required - barId: BarId, + barId: BarId @required - bazId: BazId, + bazId: BazId - bazImplicitWriteProperty: String, - bazImplicitFullyMutableProperty: String, + bazImplicitWriteProperty: String + bazImplicitFullyMutableProperty: String } structure UpdateBazResponse { - barId: BarId, - bazId: BazId, - bazImplicitWriteProperty: String, - bazImplicitFullyMutableProperty: String, + barId: BarId + bazId: BazId + bazImplicitWriteProperty: String + bazImplicitFullyMutableProperty: String +} + +@cfnResource(primaryIdentifier: "tadArn") +resource TadResource { + identifiers: { + tadId: String + } + properties: { + tadArn: String + } + create: CreateTad + read: GetTad +} + +operation CreateTad { + input := {} + output := { + @required + tadId: String + tadArn: String + } +} + +@readonly +operation GetTad { + input := { + @required + tadId: String + } + output := { + @required + tadId: String + tadArn: String + } } string FooId @@ -237,6 +272,6 @@ string BarId string BazId structure ComplexProperty { - property: String, - another: String, + property: String + another: String } diff --git a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/rule-example.cfn.json b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/rule-example.cfn.json new file mode 100644 index 00000000000..b3751aed6b8 --- /dev/null +++ b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/rule-example.cfn.json @@ -0,0 +1,247 @@ +{ + "typeName": "Smithy::TestService::Rule", + "description": "Represents a Recycle Bin retention rule that governs the retention of specified resources", + "definitions": { + "LockConfiguration": { + "type": "object", + "properties": { + "UnlockDelay": { + "$ref": "#/definitions/UnlockDelay" + } + }, + "required": [ + "UnlockDelay" + ], + "additionalProperties": false + }, + "LockState": { + "type": "string", + "enum": [ + "locked", + "pending_unlock", + "unlocked" + ] + }, + "ResourceTag": { + "type": "object", + "properties": { + "ResourceTagKey": { + "type": "string", + "pattern": "^[\\S\\s]{1,128}$" + }, + "ResourceTagValue": { + "type": "string", + "pattern": "^[\\S\\s]{0,256}$" + } + }, + "required": [ + "ResourceTagKey" + ], + "additionalProperties": false + }, + "ResourceType": { + "type": "string", + "enum": [ + "EBS_SNAPSHOT", + "EC2_IMAGE" + ] + }, + "RetentionPeriod": { + "type": "object", + "properties": { + "RetentionPeriodValue": { + "type": "number", + "maximum": 3650, + "minimum": 1 + }, + "RetentionPeriodUnit": { + "$ref": "#/definitions/RetentionPeriodUnit" + } + }, + "required": [ + "RetentionPeriodUnit", + "RetentionPeriodValue" + ], + "additionalProperties": false + }, + "RetentionPeriodUnit": { + "type": "string", + "enum": [ + "DAYS" + ] + }, + "RuleStatus": { + "type": "string", + "enum": [ + "pending", + "available" + ] + }, + "Tag": { + "type": "object", + "properties": { + "Key": { + "type": "string", + "maxLength": 128, + "minLength": 1, + "pattern": "^([\\p{L}\\p{Z}\\p{N}_.:/=+\\-@]*)$" + }, + "Value": { + "type": "string", + "maxLength": 256, + "minLength": 0, + "pattern": "^([\\p{L}\\p{Z}\\p{N}_.:/=+\\-@]*)$" + } + }, + "required": [ + "Key", + "Value" + ], + "additionalProperties": false + }, + "UnlockDelay": { + "type": "object", + "properties": { + "UnlockDelayValue": { + "type": "number", + "maximum": 30, + "minimum": 7 + }, + "UnlockDelayUnit": { + "$ref": "#/definitions/UnlockDelayUnit" + } + }, + "required": [ + "UnlockDelayUnit", + "UnlockDelayValue" + ], + "additionalProperties": false + }, + "UnlockDelayUnit": { + "type": "string", + "enum": [ + "DAYS" + ] + } + }, + "properties": { + "Description": { + "type": "string", + "pattern": "^[\\S ]{0,255}$" + }, + "ExcludeResourceTags": { + "type": "array", + "items": { + "$ref": "#/definitions/ResourceTag" + }, + "maxItems": 5, + "minItems": 0 + }, + "Identifier": { + "type": "string", + "pattern": "^[0-9a-zA-Z]{11}$" + }, + "LockConfiguration": { + "$ref": "#/definitions/LockConfiguration" + }, + "LockState": { + "$ref": "#/definitions/LockState" + }, + "ResourceTags": { + "type": "array", + "items": { + "$ref": "#/definitions/ResourceTag" + }, + "maxItems": 50, + "minItems": 0 + }, + "ResourceType": { + "$ref": "#/definitions/ResourceType" + }, + "RetentionPeriod": { + "$ref": "#/definitions/RetentionPeriod" + }, + "Arn": { + "type": "string", + "maxLength": 1011, + "minLength": 0, + "pattern": "^arn:aws(-[a-z]{1,3}){0,2}:ruler:[a-z\\-0-9]{0,63}:[0-9]{12}:rule/[0-9a-zA-Z]{11}{0,1011}$" + }, + "Status": { + "$ref": "#/definitions/RuleStatus" + }, + "Tags": { + "type": "array", + "items": { + "$ref": "#/definitions/Tag" + }, + "maxItems": 200, + "minItems": 0 + } + }, + "required": [ + "ResourceType", + "RetentionPeriod" + ], + "readOnlyProperties": [ + "/properties/Arn", + "/properties/Identifier", + "/properties/LockState", + "/properties/Status" + ], + "writeOnlyProperties": [ + "/properties/Tags" + ], + "createOnlyProperties": [ + "/properties/LockConfiguration", + "/properties/Tags" + ], + "primaryIdentifier": [ + "/properties/Arn" + ], + "additionalIdentifiers": [ + [ + "/properties/Identifier" + ] + ], + "handlers": { + "create": { + "permissions": [ + "testservice:CreateRule" + ] + }, + "read": { + "permissions": [ + "testservice:GetRule" + ] + }, + "update": { + "permissions": [ + "testservice:UpdateRule" + ] + }, + "delete": { + "permissions": [ + "testservice:DeleteRule" + ] + }, + "list": { + "permissions": [ + "testservice:ListRules" + ] + } + }, + "tagging": { + "cloudFormationSystemTags": true, + "permissions": [ + "testservice:ListTagsForResource", + "testservice:TagResource", + "testservice:UntagResource" + ], + "tagOnCreate": true, + "tagProperty": "/properties/Tags", + "tagUpdatable": true, + "taggable": true + }, + "additionalProperties": false +} diff --git a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/rule-example.smithy b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/rule-example.smithy new file mode 100644 index 00000000000..4c71878706f --- /dev/null +++ b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/rule-example.smithy @@ -0,0 +1,573 @@ +$version: "2.0" + +namespace smithy.example + +use aws.api#arn +use aws.api#tagEnabled +use aws.api#taggable +use aws.cloudformation#cfnName +use aws.cloudformation#cfnResource + +@tagEnabled +service TestService { + version: "2024-10-29" + operations: [ + ListTagsForResource + TagResource + UntagResource + ] + resources: [ + Rule + ] +} + +/// Represents a Recycle Bin retention rule that governs the retention of specified resources +@arn(template: "rule/{ResourceName}") +@taggable(property: "Tags") +@cfnResource(primaryIdentifier: "RuleArn") +resource Rule { + identifiers: { + Identifier: RuleIdentifier + } + properties: { + Status: RuleStatus + Description: Description + ResourceTags: ResourceTags + LockConfiguration: LockConfiguration + ExcludeResourceTags: ExcludeResourceTags + ResourceType: ResourceType + LockState: LockState + RetentionPeriod: RetentionPeriod + Tags: TagList + RuleArn: RuleArn + } + create: CreateRule + read: GetRule + update: UpdateRule + delete: DeleteRule + list: ListRules + operations: [ + LockRule + UnlockRule + ] +} + +operation CreateRule { + input: CreateRuleRequest + output: CreateRuleResponse + errors: [ + InternalServerException + ServiceQuotaExceededException + ValidationException + ] +} + +@idempotent +operation DeleteRule { + input: DeleteRuleRequest + output: DeleteRuleResponse + errors: [ + ConflictException + InternalServerException + ResourceNotFoundException + ValidationException + ] +} + +@readonly +operation GetRule { + input: GetRuleRequest + output: GetRuleResponse + errors: [ + InternalServerException + ResourceNotFoundException + ValidationException + ] +} + +@paginated(inputToken: "NextToken", outputToken: "NextToken", items: "Rules", pageSize: "MaxResults") +@readonly +operation ListRules { + input: ListRulesRequest + output: ListRulesResponse + errors: [ + InternalServerException + ValidationException + ] +} + +@readonly +operation ListTagsForResource { + input: ListTagsForResourceRequest + output: ListTagsForResourceResponse + errors: [ + InternalServerException + ResourceNotFoundException + ValidationException + ] +} + +operation LockRule { + input: LockRuleRequest + output: LockRuleResponse + errors: [ + ConflictException + InternalServerException + ResourceNotFoundException + ValidationException + ] +} + +operation TagResource { + input: TagResourceRequest + output: TagResourceResponse + errors: [ + InternalServerException + ResourceNotFoundException + ServiceQuotaExceededException + ValidationException + ] +} + +operation UnlockRule { + input: UnlockRuleRequest + output: UnlockRuleResponse + errors: [ + ConflictException + InternalServerException + ResourceNotFoundException + ValidationException + ] +} + +operation UntagResource { + input: UntagResourceRequest + output: UntagResourceResponse + errors: [ + InternalServerException + ResourceNotFoundException + ValidationException + ] +} + +operation UpdateRule { + input: UpdateRuleRequest + output: UpdateRuleResponse + errors: [ + ConflictException + InternalServerException + ResourceNotFoundException + ServiceQuotaExceededException + ValidationException + ] +} + +@error("client") +structure ConflictException { + Message: ErrorMessage + Reason: ConflictExceptionReason +} + +structure CreateRuleRequest { + @required + RetentionPeriod: RetentionPeriod + + Description: Description + + Tags: TagList + + @required + ResourceType: ResourceType + + ResourceTags: ResourceTags + + LockConfiguration: LockConfiguration + + ExcludeResourceTags: ExcludeResourceTags +} + +structure CreateRuleResponse { + @required + Identifier: RuleIdentifier + + RetentionPeriod: RetentionPeriod + + Description: Description + + Tags: TagList + + ResourceType: ResourceType + + ResourceTags: ResourceTags + + Status: RuleStatus + + LockConfiguration: LockConfiguration + + LockState: LockState + + RuleArn: RuleArn + + ExcludeResourceTags: ExcludeResourceTags +} + +structure DeleteRuleRequest { + @required + Identifier: RuleIdentifier +} + +structure DeleteRuleResponse {} + +structure GetRuleRequest { + @required + Identifier: RuleIdentifier +} + +structure GetRuleResponse { + @required + Identifier: RuleIdentifier + + Description: Description + + ResourceType: ResourceType + + RetentionPeriod: RetentionPeriod + + ResourceTags: ResourceTags + + Status: RuleStatus + + LockConfiguration: LockConfiguration + + LockState: LockState + + @notProperty + LockEndTime: TimeStamp + + @cfnName("Arn") + RuleArn: RuleArn + + ExcludeResourceTags: ExcludeResourceTags +} + +@error("server") +structure InternalServerException { + Message: ErrorMessage +} + +structure ListRulesRequest { + MaxResults: MaxResults + + NextToken: NextToken + + @required + ResourceType: ResourceType + + ResourceTags: ResourceTags + + LockState: LockState + + ExcludeResourceTags: ExcludeResourceTags +} + +structure ListRulesResponse { + Rules: RuleSummaryList + NextToken: NextToken +} + +structure ListTagsForResourceRequest { + @required + ResourceArn: RuleArn +} + +structure ListTagsForResourceResponse { + Tags: TagList +} + +structure LockConfiguration { + @required + UnlockDelay: UnlockDelay +} + +structure LockRuleRequest { + @required + Identifier: RuleIdentifier + + @required + LockConfiguration: LockConfiguration +} + +structure LockRuleResponse { + @required + Identifier: RuleIdentifier + + Description: Description + + ResourceType: ResourceType + + RetentionPeriod: RetentionPeriod + + ResourceTags: ResourceTags + + Status: RuleStatus + + LockConfiguration: LockConfiguration + + LockState: LockState + + RuleArn: RuleArn + + ExcludeResourceTags: ExcludeResourceTags +} + +@error("client") +structure ResourceNotFoundException { + Message: ErrorMessage + Reason: ResourceNotFoundExceptionReason +} + +structure ResourceTag { + @required + ResourceTagKey: ResourceTagKey + + ResourceTagValue: ResourceTagValue +} + +structure RetentionPeriod { + @required + RetentionPeriodValue: RetentionPeriodValue + + @required + RetentionPeriodUnit: RetentionPeriodUnit +} + +structure RuleSummary { + Identifier: RuleIdentifier + Description: Description + RetentionPeriod: RetentionPeriod + LockState: LockState + RuleArn: RuleArn +} + +@error("client") +structure ServiceQuotaExceededException { + Message: ErrorMessage + Reason: ServiceQuotaExceededExceptionReason +} + +structure Tag { + @required + Key: TagKey + + @required + Value: TagValue +} + +structure TagResourceRequest { + @required + ResourceArn: RuleArn + + @required + Tags: TagList +} + +structure TagResourceResponse {} + +structure UnlockDelay { + @required + UnlockDelayValue: UnlockDelayValue + + @required + UnlockDelayUnit: UnlockDelayUnit +} + +structure UnlockRuleRequest { + @required + Identifier: RuleIdentifier +} + +structure UnlockRuleResponse { + @required + Identifier: RuleIdentifier + + Description: Description + + ResourceType: ResourceType + + RetentionPeriod: RetentionPeriod + + ResourceTags: ResourceTags + + Status: RuleStatus + + LockConfiguration: LockConfiguration + + LockState: LockState + + @notProperty + LockEndTime: TimeStamp + + RuleArn: RuleArn + + ExcludeResourceTags: ExcludeResourceTags +} + +structure UntagResourceRequest { + @required + ResourceArn: RuleArn + + @required + TagKeys: TagKeyList +} + +structure UntagResourceResponse {} + +structure UpdateRuleRequest { + @required + Identifier: RuleIdentifier + + RetentionPeriod: RetentionPeriod + + Description: Description + + ResourceType: ResourceType + + ResourceTags: ResourceTags + + ExcludeResourceTags: ExcludeResourceTags +} + +structure UpdateRuleResponse { + @required + Identifier: RuleIdentifier + + RetentionPeriod: RetentionPeriod + + Description: Description + + ResourceType: ResourceType + + ResourceTags: ResourceTags + + Status: RuleStatus + + LockState: LockState + + @notProperty + LockEndTime: TimeStamp + + RuleArn: RuleArn + + ExcludeResourceTags: ExcludeResourceTags +} + +@error("client") +structure ValidationException { + Message: ErrorMessage + Reason: ValidationExceptionReason +} + +@length(min: 0, max: 5) +list ExcludeResourceTags { + member: ResourceTag +} + +@length(min: 0, max: 50) +list ResourceTags { + member: ResourceTag +} + +list RuleSummaryList { + member: RuleSummary +} + +@length(min: 0, max: 200) +list TagKeyList { + member: TagKey +} + +@length(min: 0, max: 200) +list TagList { + member: Tag +} + +enum ConflictExceptionReason { + INVALID_RULE_STATE +} + +@pattern("^[\\S ]{0,255}$") +string Description + +string ErrorMessage + +enum LockState { + LOCKED = "locked" + PENDING_UNLOCK = "pending_unlock" + UNLOCKED = "unlocked" +} + +@range(min: 1, max: 1000) +integer MaxResults + +@pattern("^[A-Za-z0-9+/=]{1,2048}$") +string NextToken + +enum ResourceNotFoundExceptionReason { + RULE_NOT_FOUND +} + +@pattern("^[\\S\\s]{1,128}$") +string ResourceTagKey + +@pattern("^[\\S\\s]{0,256}$") +string ResourceTagValue + +enum ResourceType { + EBS_SNAPSHOT + EC2_IMAGE +} + +enum RetentionPeriodUnit { + DAYS +} + +@range(min: 1, max: 3650) +integer RetentionPeriodValue + +@length(min: 0, max: 1011) +@pattern("^arn:aws(-[a-z]{1,3}){0,2}:ruler:[a-z\\-0-9]{0,63}:[0-9]{12}:rule/[0-9a-zA-Z]{11}{0,1011}$") +string RuleArn + +@pattern("^[0-9a-zA-Z]{11}$") +string RuleIdentifier + +enum RuleStatus { + PENDING = "pending" + AVAILABLE = "available" +} + +enum ServiceQuotaExceededExceptionReason { + SERVICE_QUOTA_EXCEEDED +} + +@length(min: 1, max: 128) +@pattern("^([\\p{L}\\p{Z}\\p{N}_.:/=+\\-@]*)$") +string TagKey + +@length(min: 0, max: 256) +@pattern("^([\\p{L}\\p{Z}\\p{N}_.:/=+\\-@]*)$") +string TagValue + +timestamp TimeStamp + +enum UnlockDelayUnit { + DAYS +} + +@range(min: 7, max: 30) +integer UnlockDelayValue + +enum ValidationExceptionReason { + INVALID_PAGE_TOKEN + INVALID_PARAMETER_VALUE +}