From 030a17047c6e679345493e1e7ad728dacd93967c Mon Sep 17 00:00:00 2001 From: Kevin Stich Date: Tue, 25 Feb 2025 21:42:41 -0800 Subject: [PATCH] Add primaryIdentifier to cfnResource trait This commit adds the primaryIdentifier field to the cfnResource trait. This supports service teams that have CFN support that deviates from their APIs in their primary identifier, normally from a human readable value to an ARN. This field is deprecated from the start, as it is not the preferred path for supporting resource identifiers. The value of the primaryIdentifier field, when set, MUST be the name of a property defined on the resource shape and that property MUST be defined as a string or enum shape. --- docs/source-2.0/aws/aws-cloudformation.rst | 5 + .../traits/CfnResourceIndex.java | 8 + .../traits/CfnResourcePropertyValidator.java | 57 +- .../traits/CfnResourceTrait.java | 17 + .../META-INF/smithy/aws.cloudformation.smithy | 30 +- .../traits/CfnResourceIndexTest.java | 16 +- .../traits/CfnResourceTraitTest.java | 9 + .../traits/cfn-resources.smithy | 44 +- .../errorfiles/primary-identifier-swap.errors | 5 + .../errorfiles/primary-identifier-swap.smithy | 104 ++++ .../cloudformation/traits/test-service.smithy | 213 ++++--- .../fromsmithy/integ/rule-example.cfn.json | 247 ++++++++ .../fromsmithy/integ/rule-example.smithy | 573 ++++++++++++++++++ 13 files changed, 1209 insertions(+), 119 deletions(-) create mode 100644 smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/primary-identifier-swap.errors create mode 100644 smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/primary-identifier-swap.smithy create mode 100644 smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/rule-example.cfn.json create mode 100644 smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/integ/rule-example.smithy 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 +}