Repository: smithy-lang/smithy
Pull request #2539 (2025-02-26 06:44:11)

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.

---
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.



Patch:
diff --git a/docs/source-2.0/aws/aws-cloudformation.rst b/docs/source-2.0/aws/aws-cloudformation.rst
index 962350ddb..087264422 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 fc72dc1eb..ef8f94c12 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 final class CfnResourceIndex implements KnowledgeIndex {
                                 });
                     }
 
+                    // 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 6d8282673..f841d9efb 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.Set;
 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 final class CfnResourcePropertyValidator extends AbstractValidator {
         List<ValidationEvent> 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 @@ public final class CfnResourcePropertyValidator extends AbstractValidator {
         List<ValidationEvent> events = new ArrayList<>();
         String resourceName = trait.getName().orElse(resource.getId().getName());
 
-        cfnResourceIndex.getResource(resource)
-                .map(CfnResource::getProperties)
-                .ifPresent(properties -> {
-                    for (Map.Entry<String, CfnResourceProperty> 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<CfnResource> cfnResourceOptional = cfnResourceIndex.getResource(resource);
+        if (cfnResourceOptional.isPresent()) {
+            for (Map.Entry<String, CfnResourceProperty> property : cfnResourceOptional.get()
+                    .getProperties()
+                    .entrySet()) {
+                validateResourceProperty(model, resource, resourceName, property).ifPresent(events::add);
+            }
+        }
 
         return events;
     }
 
+    private Optional<ValidationEvent> validateResourcePrimaryIdentifier(
+            Model model,
+            ResourceShape resource,
+            String primaryIdentifier
+    ) {
+        Map<String, ShapeId> 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<ValidationEvent> validateResourceProperty(
             Model model,
             ResourceShape resource,
@@ -72,7 +105,7 @@ public final class CfnResourcePropertyValidator extends AbstractValidator {
 
         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 fb5c4e92a..952396530 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<ShapeId> 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 final class CfnResourceTrait extends AbstractTrait
         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<String> getPrimaryIdentifier() {
+        return Optional.ofNullable(primaryIdentifier);
+    }
+
     public static Builder builder() {
         return new Builder();
     }
@@ -83,6 +94,7 @@ public final class CfnResourceTrait extends AbstractTrait
     public static final class Builder extends AbstractTraitBuilder<CfnResourceTrait, Builder> {
         private String name;
         private final List<ShapeId> additionalSchemas = new ArrayList<>();
+        private String primaryIdentifier;
 
         private Builder() {}
 
@@ -106,5 +118,10 @@ public final class CfnResourceTrait extends AbstractTrait
             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 d27bcf886..e9676fa0c 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


Test patch:
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 527a0296b..3b7a50ff5 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 class CfnResourceIndexTest {
         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 ec8fc4753..1a89b4509 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 @@ 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.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
@@ -36,6 +37,14 @@ public class CfnResourceTraitTest {
         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 63a371355..87d311603 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 000000000..6d37517d2
--- /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 000000000..366946ac8
--- /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 bd25ef59e..1ff2ba203 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 000000000..b3751aed6
--- /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 000000000..4c7187870
--- /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
+}