Skip to content

Commit

Permalink
Add delimiter and reusable to arn trait
Browse files Browse the repository at this point in the history
This commit adds two fields to the aws.api#arn trait:

* resourceDelimiter - used to define the delimiter for resource segments of
absolute ARN templates.
* reusable - indicates whether an ARN may be reused for different instances
of a resource.
  • Loading branch information
kstich committed Nov 1, 2024
1 parent 4c8403e commit 31f10c5
Show file tree
Hide file tree
Showing 16 changed files with 343 additions and 226 deletions.
1 change: 0 additions & 1 deletion docs/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 16 additions & 1 deletion docs/source-2.0/aws/aws-core.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
================
Expand Down Expand Up @@ -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
======================

Expand Down Expand Up @@ -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
=====================================

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -42,21 +44,27 @@ public final class ArnTrait extends AbstractTrait implements ToSmithyBuilder<Arn
private static final String ABSOLUTE = "absolute";
private static final String NO_REGION = "noRegion";
private static final String NO_ACCOUNT = "noAccount";
private static final String RESOURCE_DELIMITER = "resourceDelimiter";
private static final String REUSABLE = "reusable";
private static final Pattern PATTERN = Pattern.compile("\\{([^}]+)}");

private final boolean noRegion;
private final boolean noAccount;
private final boolean absolute;
private final String template;
private final List<String> 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 '/'. "
Expand All @@ -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;
Expand Down Expand Up @@ -128,12 +138,26 @@ public String getTemplate() {
}

/**
* @return Returns the label place holder variable names.
* @return Returns the label placeholder variable names.
*/
public List<String> getLabels() {
return labels;
}

/**
* @return Returns the resource delimiter for absolute ARNs.
*/
public Optional<ResourceDelimiter> 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()
Expand All @@ -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();
}

Expand All @@ -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)) {
Expand All @@ -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}. */
Expand All @@ -182,6 +243,8 @@ public static final class Builder extends AbstractTraitBuilder<ArnTrait, Builder
private boolean noAccount;
private boolean absolute;
private String template;
private ResourceDelimiter resourceDelimiter;
private boolean reusable;

private Builder() {}

Expand Down Expand Up @@ -209,5 +272,20 @@ public Builder noRegion(boolean noRegion) {
this.noRegion = noRegion;
return this;
}

public Builder resourceDelimiter(ResourceDelimiter resourceDelimiter) {
this.resourceDelimiter = resourceDelimiter;
return this;
}

public Builder resourceDelimiter(String resourceDelimiter) {
this.resourceDelimiter = ResourceDelimiter.from(resourceDelimiter);
return this;
}

public Builder reusable(boolean reusable) {
this.reusable = reusable;
return this;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.aws.traits;

import java.util.ArrayList;
import java.util.List;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.shapes.ResourceShape;
import software.amazon.smithy.model.validation.AbstractValidator;
import software.amazon.smithy.model.validation.ValidationEvent;
import software.amazon.smithy.utils.SmithyInternalApi;

/**
* Validates that the `resourceDelimiter` field of the `aws.api#arn` trait
* is only used in conjunction with absolute ARNs.
*/
@SmithyInternalApi
public final class ArnTraitValidator extends AbstractValidator {
@Override
public List<ValidationEvent> validate(Model model) {
List<ValidationEvent> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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> trait = provider.createTrait(ArnTrait.ID, ShapeId.from("ns.foo#foo"), node);

Expand All @@ -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> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 31f10c5

Please sign in to comment.