diff --git a/docs/Makefile b/docs/Makefile index 48d1df58859..e668e256d31 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -7,7 +7,6 @@ SHELL := /bin/bash install: requirements.txt python3 -m venv build/venv source build/venv/bin/activate && pip3 install -r requirements.txt . && pip3 install -e . - touch install clean: -rm -rf build/* install diff --git a/docs/source-2.0/aws/aws-core.rst b/docs/source-2.0/aws/aws-core.rst index fb961d89023..be11b13dae0 100644 --- a/docs/source-2.0/aws/aws-core.rst +++ b/docs/source-2.0/aws/aws-core.rst @@ -290,14 +290,25 @@ members: - Set to true to specify that the ARN does not contain an account ID. If not set, or if set to false, the resolved ARN will contain a placeholder for the customer account ID. This can only be set to - true if absolute is not set or is false. + true if ``absolute`` is not set or is false. * - absolute - ``boolean`` - Set to true to indicate that the ARN template contains a fully-formed ARN that does not need to be merged with the service. This type of ARN MUST be used when the identifier of a resource is an ARN or is based on the ARN identifier of a parent resource. + * - resourceDelimiter + - ``string`` + - Indicates which character is used to delimit sections of the resource + segment of an ARN. This can only be set if ``absolute`` is set to true. + Valid values are ``/`` (forward slash) and ``:`` (colon). + * - reusable + - ``boolean`` + - Set to true to indicate that an ARN may be reused for different + instances of a resource. + +.. _aws.api#arn-trait_format-of-an-arn: Format of an ARN ================ @@ -354,6 +365,8 @@ Some example ARNs from various services include: arn:aws:s3:::my_corporate_bucket/exampleobject.png +.. _aws.api#arn-trait_relative-arn-templates: + Relative ARN templates ====================== @@ -391,6 +404,8 @@ identifier is to be inserted into the ARN template when resolving it at runtime. +.. _aws.api#arn-trait_using-an-arn-as-a-resource-identifier: + Using an ARN as a resource identifier ===================================== diff --git a/smithy-aws-traits/src/main/java/software/amazon/smithy/aws/traits/ArnTrait.java b/smithy-aws-traits/src/main/java/software/amazon/smithy/aws/traits/ArnTrait.java index c1ab461f61d..3f7131dcbaf 100644 --- a/smithy-aws-traits/src/main/java/software/amazon/smithy/aws/traits/ArnTrait.java +++ b/smithy-aws-traits/src/main/java/software/amazon/smithy/aws/traits/ArnTrait.java @@ -19,11 +19,13 @@ import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; import software.amazon.smithy.model.SourceException; import software.amazon.smithy.model.node.Node; import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.node.ToNode; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.traits.AbstractTrait; import software.amazon.smithy.model.traits.AbstractTraitBuilder; @@ -42,6 +44,8 @@ public final class ArnTrait extends AbstractTrait implements ToSmithyBuilder labels; + private final ResourceDelimiter resourceDelimiter; + private final boolean reusable; private ArnTrait(Builder builder) { super(ID, builder.getSourceLocation()); this.template = SmithyBuilder.requiredState(TEMPLATE, builder.template); + this.labels = Collections.unmodifiableList(parseLabels(template)); this.noRegion = builder.noRegion; this.noAccount = builder.noAccount; this.absolute = builder.absolute; - this.labels = Collections.unmodifiableList(parseLabels(template)); + this.resourceDelimiter = builder.resourceDelimiter; + this.reusable = builder.reusable; if (template.startsWith("/")) { throw new SourceException("Invalid aws.api#arn trait. The template must not start with '/'. " @@ -77,6 +85,8 @@ public Trait createTrait(ShapeId target, Node value) { builder.absolute(objectNode.getBooleanMemberOrDefault(ABSOLUTE)); builder.noRegion(objectNode.getBooleanMemberOrDefault(NO_REGION)); builder.noAccount(objectNode.getBooleanMemberOrDefault(NO_ACCOUNT)); + objectNode.getStringMember(RESOURCE_DELIMITER, builder::resourceDelimiter); + builder.reusable(objectNode.getBooleanMemberOrDefault(REUSABLE)); ArnTrait result = builder.build(); result.setNodeCache(value); return result; @@ -128,12 +138,26 @@ public String getTemplate() { } /** - * @return Returns the label place holder variable names. + * @return Returns the label placeholder variable names. */ public List getLabels() { return labels; } + /** + * @return Returns the resource delimiter for absolute ARNs. + */ + public Optional getResourceDelimiter() { + return Optional.ofNullable(resourceDelimiter); + } + + /** + * @return Returns if the ARN may be reused for different instances of a resource. + */ + public boolean isReusable() { + return reusable; + } + @Override protected Node createNode() { return Node.objectNodeBuilder() @@ -142,6 +166,8 @@ protected Node createNode() { .withMember(ABSOLUTE, Node.from(isAbsolute())) .withMember(NO_ACCOUNT, Node.from(isNoAccount())) .withMember(NO_REGION, Node.from(isNoRegion())) + .withOptionalMember(RESOURCE_DELIMITER, getResourceDelimiter()) + .withMember(REUSABLE, Node.from(isReusable())) .build(); } @@ -151,11 +177,14 @@ public Builder toBuilder() { .sourceLocation(getSourceLocation()) .noRegion(isNoRegion()) .noAccount(isNoAccount()) - .template(getTemplate()); + .absolute(isAbsolute()) + .template(getTemplate()) + .resourceDelimiter(resourceDelimiter) + .reusable(reusable); } // Due to the defaulting of this trait, equals has to be overridden - // so that inconsequential differences in toNode do not effect equality. + // so that inconsequential differences in toNode do not affect equality. @Override public boolean equals(Object other) { if (!(other instanceof ArnTrait)) { @@ -167,13 +196,45 @@ public boolean equals(Object other) { return template.equals(oa.template) && absolute == oa.absolute && noAccount == oa.noAccount - && noRegion == oa.noRegion; + && noRegion == oa.noRegion + && resourceDelimiter == oa.resourceDelimiter + && reusable == oa.reusable; } } @Override public int hashCode() { - return Objects.hash(toShapeId(), template, absolute, noAccount, noRegion); + return Objects.hash(toShapeId(), template, absolute, noAccount, noRegion, resourceDelimiter, reusable); + } + + public enum ResourceDelimiter implements ToNode { + FORWARD_SLASH("/"), + COLON(":"); + + private final String value; + + ResourceDelimiter(String value) { + this.value = value; + } + + static ResourceDelimiter from(String value) { + for (ResourceDelimiter delimiter : values()) { + if (delimiter.value.equals(value)) { + return delimiter; + } + } + throw new IllegalArgumentException("Invalid delimiter: " + value); + } + + @Override + public String toString() { + return value; + } + + @Override + public Node toNode() { + return Node.from(value); + } } /** Builder for {@link ArnTrait}. */ @@ -182,6 +243,8 @@ public static final class Builder extends AbstractTraitBuilder validate(Model model) { + List events = new ArrayList<>(); + for (ResourceShape resource : model.getResourceShapesWithTrait(ArnTrait.class)) { + ArnTrait trait = resource.expectTrait(ArnTrait.class); + if (!trait.isAbsolute() && trait.getResourceDelimiter().isPresent()) { + events.add(error(resource, trait, "A `resourceDelimiter` can only be set for an `absolute` ARN.")); + } + } + return events; + } +} diff --git a/smithy-aws-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator b/smithy-aws-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator index 80a840407e2..133106fc84d 100644 --- a/smithy-aws-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator +++ b/smithy-aws-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator @@ -1,4 +1,5 @@ software.amazon.smithy.aws.traits.ArnTemplateValidator +software.amazon.smithy.aws.traits.ArnTraitValidator software.amazon.smithy.aws.traits.HttpChecksumTraitValidator software.amazon.smithy.aws.traits.SdkServiceIdValidator software.amazon.smithy.aws.traits.clientendpointdiscovery.ClientEndpointDiscoveryValidator diff --git a/smithy-aws-traits/src/main/resources/META-INF/smithy/aws.api.smithy b/smithy-aws-traits/src/main/resources/META-INF/smithy/aws.api.smithy index baf9cb499f9..a079ec69816 100644 --- a/smithy-aws-traits/src/main/resources/META-INF/smithy/aws.api.smithy +++ b/smithy-aws-traits/src/main/resources/META-INF/smithy/aws.api.smithy @@ -38,6 +38,14 @@ structure arn { /// for the customer account ID. This can only be set to true if absolute /// is not set or is false. noAccount: Boolean + + /// Defines which character is used to delimit sections of the resource + /// segment of an ARN. This can only be set if absolute is set to true. + resourceDelimiter: ResourceDelimiter + + /// Set to true to indicate that an ARN may be reused for different + /// instances of a resource. + reusable: Boolean } /// Marks a string as containing an ARN. @@ -262,6 +270,17 @@ structure taggable { disableSystemTags: Boolean } + +/// The possible delimiters for an ARN resource segment. +@private +enum ResourceDelimiter { + /// The `/` character. + FORWARD_SLASH = "/" + + /// The `:` character. + COLON = ":" +} + /// A string representing a service's ARN namespace. @pattern("^[a-z0-9.\\-]{1,63}$") @private diff --git a/smithy-aws-traits/src/test/java/software/amazon/smithy/aws/traits/ArnIndexTest.java b/smithy-aws-traits/src/test/java/software/amazon/smithy/aws/traits/ArnIndexTest.java index 3bb0e340a15..a81f146aeb6 100644 --- a/smithy-aws-traits/src/test/java/software/amazon/smithy/aws/traits/ArnIndexTest.java +++ b/smithy-aws-traits/src/test/java/software/amazon/smithy/aws/traits/ArnIndexTest.java @@ -34,7 +34,7 @@ public class ArnIndexTest { public static void beforeClass() { model = Model.assembler() .discoverModels(ArnIndexTest.class.getClassLoader()) - .addImport(ArnIndexTest.class.getResource("test-model.json")) + .addImport(ArnIndexTest.class.getResource("test-model.smithy")) .assemble() .unwrap(); } @@ -104,7 +104,7 @@ public void returnsDefaultServiceArnNamespaceForAwsService() { public void findsEffectiveArns() { Model m = Model.assembler() .discoverModels(ArnIndexTest.class.getClassLoader()) - .addImport(ArnIndexTest.class.getResource("effective-arns.json")) + .addImport(ArnIndexTest.class.getResource("effective-arns.smithy")) .assemble() .unwrap(); ArnIndex index = ArnIndex.of(m); diff --git a/smithy-aws-traits/src/test/java/software/amazon/smithy/aws/traits/ArnReferenceTraitTest.java b/smithy-aws-traits/src/test/java/software/amazon/smithy/aws/traits/ArnReferenceTraitTest.java index 6ec887db586..9e34455a991 100644 --- a/smithy-aws-traits/src/test/java/software/amazon/smithy/aws/traits/ArnReferenceTraitTest.java +++ b/smithy-aws-traits/src/test/java/software/amazon/smithy/aws/traits/ArnReferenceTraitTest.java @@ -76,7 +76,7 @@ public void loadsTraitWithOptionalValuesAndRelativeShapeIds() { public void loadsFromModel() { Model result = Model.assembler() .discoverModels(getClass().getClassLoader()) - .addImport(getClass().getResource("test-model.json")) + .addImport(getClass().getResource("test-model.smithy")) .assemble() .unwrap(); Shape service = result.expectShape(ShapeId.from("ns.foo#AbsoluteResourceArn")); diff --git a/smithy-aws-traits/src/test/java/software/amazon/smithy/aws/traits/ArnTraitTest.java b/smithy-aws-traits/src/test/java/software/amazon/smithy/aws/traits/ArnTraitTest.java index 2049ca99e4a..435c9a67245 100644 --- a/smithy-aws-traits/src/test/java/software/amazon/smithy/aws/traits/ArnTraitTest.java +++ b/smithy-aws-traits/src/test/java/software/amazon/smithy/aws/traits/ArnTraitTest.java @@ -46,12 +46,15 @@ public void loadsTraitWithFromNode() { assertThat(arnTrait.getTemplate(), equalTo("resourceName")); assertThat(arnTrait.isNoAccount(), is(false)); assertThat(arnTrait.isNoRegion(), is(false)); + assertThat(arnTrait.isAbsolute(), is(false)); assertThat(arnTrait.getLabels(), empty()); + assertThat(arnTrait.getResourceDelimiter().isPresent(), is(false)); + assertThat(arnTrait.isReusable(), is(false)); } @Test - public void canSetRegionAndServiceToNo() { - Node node = Node.parse("{\"noAccount\": true, \"noRegion\": true, \"absolute\": false, \"template\": \"foo\"}"); + public void canSetOtherFields() { + Node node = Node.parse("{\"noAccount\": true, \"noRegion\": true, \"absolute\": false, \"template\": \"foo\", \"reusable\": true}"); TraitFactory provider = TraitFactory.createServiceFactory(); Optional trait = provider.createTrait(ArnTrait.ID, ShapeId.from("ns.foo#foo"), node); @@ -60,8 +63,24 @@ public void canSetRegionAndServiceToNo() { assertThat(arnTrait.getTemplate(), equalTo("foo")); assertThat(arnTrait.isNoAccount(), is(true)); assertThat(arnTrait.isNoRegion(), is(true)); + assertThat(arnTrait.isAbsolute(), is(false)); assertThat(arnTrait.toNode(), equalTo(node)); assertThat(arnTrait.toBuilder().build(), equalTo(arnTrait)); + assertThat(arnTrait.isReusable(), is(true)); + } + + @Test + public void canSetAbsoluteAndDelimiter() { + Node node = Node.parse("{\"absolute\": true, \"template\": \"foo\", \"resourceDelimiter\": \":\"}"); + TraitFactory provider = TraitFactory.createServiceFactory(); + Optional trait = provider.createTrait(ArnTrait.ID, ShapeId.from("ns.foo#foo"), node); + + assertTrue(trait.isPresent()); + ArnTrait arnTrait = (ArnTrait) trait.get(); + assertThat(arnTrait.getTemplate(), equalTo("foo")); + assertThat(arnTrait.toBuilder().build(), equalTo(arnTrait)); + assertThat(arnTrait.isAbsolute(), is(true)); + assertThat(arnTrait.getResourceDelimiter().get(), equalTo(ArnTrait.ResourceDelimiter.COLON)); } @Test diff --git a/smithy-aws-traits/src/test/java/software/amazon/smithy/aws/traits/ServiceTraitTest.java b/smithy-aws-traits/src/test/java/software/amazon/smithy/aws/traits/ServiceTraitTest.java index 9743c4b9674..7de95ce78df 100644 --- a/smithy-aws-traits/src/test/java/software/amazon/smithy/aws/traits/ServiceTraitTest.java +++ b/smithy-aws-traits/src/test/java/software/amazon/smithy/aws/traits/ServiceTraitTest.java @@ -112,7 +112,7 @@ public void requiresProperServiceShapeToResolveDocId() { public void loadsFromModel() { Model result = Model.assembler() .discoverModels(getClass().getClassLoader()) - .addImport(getClass().getResource("test-model.json")) + .addImport(getClass().getResource("test-model.smithy")) .assemble() .unwrap(); ServiceShape service = result diff --git a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/effective-arns.json b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/effective-arns.json deleted file mode 100644 index e426d1477c6..00000000000 --- a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/effective-arns.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "smithy": "2.0", - "shapes": { - "ns.foo#SomeService": { - "type": "service", - "version": "2018-03-17", - "resources": [ - { - "target": "ns.foo#Resource1" - } - ], - "traits": { - "aws.api#service": { - "sdkId": "Some Value" - } - } - }, - "ns.foo#Resource1": { - "type": "resource", - "resources": [ - { - "target": "ns.foo#Resource2" - } - ], - "traits": { - "aws.api#arn": { - "template": "foo" - } - } - }, - "ns.foo#Resource2": { - "type": "resource", - "identifiers": { - "id": { - "target": "smithy.api#String" - } - }, - "operations": [ - { - "target": "ns.foo#InstanceOperation" - } - ], - "collectionOperations": [ - { - "target": "ns.foo#CollectionOperation" - } - ], - "traits": { - "aws.api#arn": { - "template": "foo/{id}" - } - } - }, - "ns.foo#InstanceOperation": { - "type": "operation", - "input": { - "target": "ns.foo#InstanceOperationInput" - } - }, - "ns.foo#InstanceOperationInput": { - "type": "structure", - "members": { - "id": { - "target": "smithy.api#String", - "traits": { - "smithy.api#required": {} - } - } - } - }, - "ns.foo#CollectionOperation": { - "type": "operation", - "input": { - "target": "ns.foo#CollectionOperationInput" - } - }, - "ns.foo#CollectionOperationInput": { - "type": "structure" - } - } -} diff --git a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/effective-arns.smithy b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/effective-arns.smithy new file mode 100644 index 00000000000..467e01245fd --- /dev/null +++ b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/effective-arns.smithy @@ -0,0 +1,49 @@ +$version: "2.0" + +namespace ns.foo + +use aws.api#arn +use aws.api#service + +@service(sdkId: "Some Value") +service SomeService { + version: "2018-03-17" + resources: [ + Resource1 + ] +} + +@arn(template: "foo") +resource Resource1 { + resources: [ + Resource2 + ] +} + +@arn(template: "foo/{id}") +resource Resource2 { + identifiers: { id: String } + operations: [ + InstanceOperation + ] + collectionOperations: [ + CollectionOperation + ] +} + +operation CollectionOperation { + input: CollectionOperationInput + output: Unit +} + +operation InstanceOperation { + input: InstanceOperationInput + output: Unit +} + +structure CollectionOperationInput {} + +structure InstanceOperationInput { + @required + id: String +} diff --git a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/arn-casing.errors b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/arn-casing.errors new file mode 100644 index 00000000000..ab77dbc318b --- /dev/null +++ b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/arn-casing.errors @@ -0,0 +1 @@ +[ERROR] ns.foo#Resource1: A `resourceDelimiter` can only be set for an `absolute` ARN. | ArnTrait diff --git a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/arn-casing.smithy b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/arn-casing.smithy new file mode 100644 index 00000000000..ba6857279d9 --- /dev/null +++ b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/arn-casing.smithy @@ -0,0 +1,49 @@ +$version: "2.0" + +namespace ns.foo + +use aws.api#arn +use aws.api#service + +@service(sdkId: "Some Value") +service SomeService { + version: "2018-03-17" + resources: [ + Resource1 + ] +} + +@arn(template: "foo", resourceDelimiter: ":") +resource Resource1 { + resources: [ + Resource2 + ] +} + +@arn(template: "foo/{id}") +resource Resource2 { + identifiers: { id: String } + operations: [ + InstanceOperation + ] + collectionOperations: [ + CollectionOperation + ] +} + +operation CollectionOperation { + input: CollectionOperationInput + output: Unit +} + +operation InstanceOperation { + input: InstanceOperationInput + output: Unit +} + +structure CollectionOperationInput {} + +structure InstanceOperationInput { + @required + id: String +} diff --git a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/test-model.json b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/test-model.json deleted file mode 100644 index 0af22fe2479..00000000000 --- a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/test-model.json +++ /dev/null @@ -1,131 +0,0 @@ -{ - "smithy": "2.0", - "shapes": { - "ns.foo#NonAwsService": { - "type": "service", - "version": "2018-03-17" - }, - "ns.foo#EmptyAwsService": { - "type": "service", - "version": "2018-03-17", - "traits": { - "aws.api#service": { - "sdkId": "Something Empty" - } - } - }, - "ns.foo#SomeService": { - "type": "service", - "version": "2018-03-17", - "resources": [ - { - "target": "ns.foo#SomeResource" - }, - { - "target": "ns.foo#RootArnResource" - }, - { - "target": "ns.foo#AbsoluteResource" - } - ], - "traits": { - "aws.api#service": { - "sdkId": "Some Value", - "arnNamespace": "service", - "cloudFormationName": "SomeService", - "endpointPrefix": "some-service" - } - } - }, - "ns.foo#RootArnResource": { - "type": "resource", - "traits": { - "aws.api#arn": { - "noRegion": true, - "noAccount": true, - "template": "rootArnResource" - } - } - }, - "ns.foo#SomeResource": { - "type": "resource", - "identifiers": { - "someId": { - "target": "ns.foo#SomeResourceId" - } - }, - "resources": [ - { - "target": "ns.foo#ChildResource" - } - ], - "traits": { - "aws.api#arn": { - "template": "someresource/{someId}" - } - } - }, - "ns.foo#ChildResource": { - "type": "resource", - "identifiers": { - "someId": { - "target": "ns.foo#SomeResourceId" - }, - "childId": { - "target": "ns.foo#ChildResourceId" - } - }, - "resources": [ - { - "target": "ns.foo#AnotherChild" - } - ], - "traits": { - "aws.api#arn": { - "template": "someresource/{someId}/{childId}" - } - } - }, - "ns.foo#AnotherChild": { - "type": "resource", - "identifiers": { - "someId": { - "target": "ns.foo#SomeResourceId" - }, - "childId": { - "target": "ns.foo#ChildResourceId" - } - } - }, - "ns.foo#AbsoluteResource": { - "type": "resource", - "identifiers": { - "arn": { - "target": "ns.foo#AbsoluteResourceArn" - } - }, - "traits": { - "aws.api#arn": { - "template": "{arn}", - "absolute": true - } - } - }, - "ns.foo#AbsoluteResourceArn": { - "type": "string", - "traits": { - "aws.api#arnReference": { - "type": "AWS::SomeService::AbsoluteResource", - "service": "ns.foo#SomeService", - "resource": "ns.foo#AbsoluteResource" - } - } - }, - "ns.foo#SomeResourceId": { - "type": "string" - }, - "ns.foo#ChildResourceId": { - "type": "string" - } - } -} diff --git a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/test-model.smithy b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/test-model.smithy new file mode 100644 index 00000000000..fd2953f2554 --- /dev/null +++ b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/test-model.smithy @@ -0,0 +1,66 @@ +$version: "2.0" + +namespace ns.foo + +use aws.api#arn +use aws.api#arnReference +use aws.api#service + +@service(sdkId: "Something Empty") +service EmptyAwsService { version: "2018-03-17" } + +service NonAwsService { version: "2018-03-17" } + +@service( + sdkId: "Some Value" + arnNamespace: "service" + cloudFormationName: "SomeService" + endpointPrefix: "some-service" +) +service SomeService { + version: "2018-03-17" + resources: [ + AbsoluteResource + RootArnResource + SomeResource + ] +} + +@arn(template: "{arn}", absolute: true) +resource AbsoluteResource { + identifiers: { arn: AbsoluteResourceArn } +} + +resource AnotherChild { + identifiers: { childId: ChildResourceId, someId: SomeResourceId } +} + +@arn(template: "someresource/{someId}/{childId}") +resource ChildResource { + identifiers: { childId: ChildResourceId, someId: SomeResourceId } + resources: [ + AnotherChild + ] +} + +@arn(noRegion: true, noAccount: true, template: "rootArnResource") +resource RootArnResource {} + +@arn(template: "someresource/{someId}") +resource SomeResource { + identifiers: { someId: SomeResourceId } + resources: [ + ChildResource + ] +} + +@arnReference( + type: "AWS::SomeService::AbsoluteResource" + service: "ns.foo#SomeService" + resource: "ns.foo#AbsoluteResource" +) +string AbsoluteResourceArn + +string ChildResourceId + +string SomeResourceId