Skip to content

Commit cd780c0

Browse files
committed
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.
1 parent 5891534 commit cd780c0

File tree

13 files changed

+1209
-119
lines changed

13 files changed

+1209
-119
lines changed

docs/source-2.0/aws/aws-cloudformation.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ supports the following members:
5555
Members of these structures with the same names MUST resolve to the
5656
same target. See :ref:`aws-cloudformation-property-deriviation` for
5757
more information.
58+
* - primaryIdentifier
59+
- ``string``
60+
- **Deprecated** An alternative resource property to use as the primary
61+
identifier for the CloudFormation resource. The value MUST be the name
62+
of a property on the resource shape that targets a string shape.
5863

5964
The following example defines a simple resource that is also a CloudFormation
6065
resource:

smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourceIndex.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,14 @@ public CfnResourceIndex(Model model) {
174174
});
175175
}
176176

177+
// If the primary identifier is rerouted, the derived identifiers are
178+
// additional identifiers for the resource.
179+
if (trait.getPrimaryIdentifier().isPresent()) {
180+
CfnResource tempResource = builder.build();
181+
builder.addAdditionalIdentifier(SetUtils.copyOf(tempResource.getPrimaryIdentifiers()));
182+
builder.primaryIdentifiers(SetUtils.of(trait.getPrimaryIdentifier().get()));
183+
}
184+
177185
resourceDefinitions.put(resourceId, builder.build());
178186
});
179187
}

smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourcePropertyValidator.java

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
*/
55
package software.amazon.smithy.aws.cloudformation.traits;
66

7+
import static java.lang.String.format;
8+
79
import java.util.ArrayList;
810
import java.util.List;
911
import java.util.Map;
@@ -12,6 +14,7 @@
1214
import java.util.TreeSet;
1315
import software.amazon.smithy.model.Model;
1416
import software.amazon.smithy.model.shapes.ResourceShape;
17+
import software.amazon.smithy.model.shapes.Shape;
1518
import software.amazon.smithy.model.shapes.ShapeId;
1619
import software.amazon.smithy.model.validation.AbstractValidator;
1720
import software.amazon.smithy.model.validation.ValidationEvent;
@@ -27,10 +30,9 @@ public List<ValidationEvent> validate(Model model) {
2730
List<ValidationEvent> events = new ArrayList<>();
2831

2932
CfnResourceIndex cfnResourceIndex = CfnResourceIndex.of(model);
30-
model.shapes(ResourceShape.class)
31-
.filter(shape -> shape.hasTrait(CfnResourceTrait.ID))
32-
.map(shape -> validateResource(model, cfnResourceIndex, shape))
33-
.forEach(events::addAll);
33+
for (ResourceShape resource : model.getResourceShapesWithTrait(CfnResourceTrait.class)) {
34+
events.addAll(validateResource(model, cfnResourceIndex, resource));
35+
}
3436

3537
return events;
3638
}
@@ -44,17 +46,48 @@ private List<ValidationEvent> validateResource(
4446
List<ValidationEvent> events = new ArrayList<>();
4547
String resourceName = trait.getName().orElse(resource.getId().getName());
4648

47-
cfnResourceIndex.getResource(resource)
48-
.map(CfnResource::getProperties)
49-
.ifPresent(properties -> {
50-
for (Map.Entry<String, CfnResourceProperty> property : properties.entrySet()) {
51-
validateResourceProperty(model, resource, resourceName, property).ifPresent(events::add);
52-
}
53-
});
49+
if (trait.getPrimaryIdentifier().isPresent()) {
50+
validateResourcePrimaryIdentifier(model, resource, trait.getPrimaryIdentifier().get())
51+
.ifPresent(events::add);
52+
}
53+
54+
Optional<CfnResource> cfnResourceOptional = cfnResourceIndex.getResource(resource);
55+
if (cfnResourceOptional.isPresent()) {
56+
for (Map.Entry<String, CfnResourceProperty> property : cfnResourceOptional.get()
57+
.getProperties()
58+
.entrySet()) {
59+
validateResourceProperty(model, resource, resourceName, property).ifPresent(events::add);
60+
}
61+
}
5462

5563
return events;
5664
}
5765

66+
private Optional<ValidationEvent> validateResourcePrimaryIdentifier(
67+
Model model,
68+
ResourceShape resource,
69+
String primaryIdentifier
70+
) {
71+
Map<String, ShapeId> properties = resource.getProperties();
72+
if (!properties.containsKey(primaryIdentifier)) {
73+
return Optional.of(error(resource,
74+
resource.expectTrait(CfnResourceTrait.class),
75+
format("The alternative resource primary identifier, `%s`, must be a property of the resource.",
76+
primaryIdentifier)));
77+
}
78+
79+
Shape propertyTarget = model.expectShape(properties.get(primaryIdentifier));
80+
if (!propertyTarget.isStringShape() && !propertyTarget.isEnumShape()) {
81+
return Optional.of(error(resource,
82+
resource.expectTrait(CfnResourceTrait.class),
83+
format("The alternative resource primary identifier, `%s`, targets a `%s` shape, it must target a `string`.",
84+
primaryIdentifier,
85+
propertyTarget.getType())));
86+
}
87+
88+
return Optional.empty();
89+
}
90+
5891
private Optional<ValidationEvent> validateResourceProperty(
5992
Model model,
6093
ResourceShape resource,
@@ -72,7 +105,7 @@ private Optional<ValidationEvent> validateResourceProperty(
72105

73106
if (propertyTargets.size() > 1) {
74107
return Optional.of(error(resource,
75-
String.format("The `%s` property of the generated `%s` "
108+
format("The `%s` property of the generated `%s` "
76109
+ "CloudFormation resource targets multiple shapes: %s. Reusing member names that "
77110
+ "target different shapes can cause confusion for users of the API. This target "
78111
+ "discrepancy must either be resolved in the model or one of the members must be "

smithy-aws-cloudformation-traits/src/main/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourceTrait.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@ public final class CfnResourceTrait extends AbstractTrait
2525

2626
private final String name;
2727
private final List<ShapeId> additionalSchemas;
28+
private final String primaryIdentifier;
2829

2930
private CfnResourceTrait(Builder builder) {
3031
super(ID, builder.getSourceLocation());
3132
name = builder.name;
3233
additionalSchemas = ListUtils.copyOf(builder.additionalSchemas);
34+
primaryIdentifier = builder.primaryIdentifier;
3335
}
3436

3537
/**
@@ -50,6 +52,15 @@ public List<ShapeId> getAdditionalSchemas() {
5052
return additionalSchemas;
5153
}
5254

55+
/**
56+
* Gets the alternative resource property to use as the primary identifier for the CloudFormation resource.
57+
*
58+
* @return Returns the optional alternative primary identifier.
59+
*/
60+
public Optional<String> getPrimaryIdentifier() {
61+
return Optional.ofNullable(primaryIdentifier);
62+
}
63+
5364
public static Builder builder() {
5465
return new Builder();
5566
}
@@ -83,6 +94,7 @@ public Trait createTrait(ShapeId target, Node value) {
8394
public static final class Builder extends AbstractTraitBuilder<CfnResourceTrait, Builder> {
8495
private String name;
8596
private final List<ShapeId> additionalSchemas = new ArrayList<>();
97+
private String primaryIdentifier;
8698

8799
private Builder() {}
88100

@@ -106,5 +118,10 @@ public Builder additionalSchemas(List<ShapeId> additionalSchemas) {
106118
this.additionalSchemas.addAll(additionalSchemas);
107119
return this;
108120
}
121+
122+
public Builder primaryIdentifier(String primaryIdentifier) {
123+
this.primaryIdentifier = primaryIdentifier;
124+
return this;
125+
}
109126
}
110127
}

smithy-aws-cloudformation-traits/src/main/resources/META-INF/smithy/aws.cloudformation.smithy

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ namespace aws.cloudformation
66
/// additional identifier for the resource.
77
@unstable
88
@trait(
9-
selector: "structure > :test(member > string)",
10-
conflicts: [cfnExcludeProperty],
9+
selector: "structure > :test(member > string)"
10+
conflicts: [cfnExcludeProperty]
1111
breakingChanges: [{change: "remove"}]
1212
)
1313
structure cfnAdditionalIdentifier {}
@@ -16,7 +16,7 @@ structure cfnAdditionalIdentifier {}
1616
/// to differ from a structure member name used in the model.
1717
@unstable
1818
@trait(
19-
selector: "structure > member",
19+
selector: "structure > member"
2020
breakingChanges: [{change: "any"}]
2121
)
2222
string cfnName
@@ -25,12 +25,12 @@ string cfnName
2525
/// CloudFormation resource definitions.
2626
@unstable
2727
@trait(
28-
selector: "structure > member",
28+
selector: "structure > member"
2929
conflicts: [
30-
cfnAdditionalIdentifier,
31-
cfnMutability,
30+
cfnAdditionalIdentifier
31+
cfnMutability
3232
cfnDefaultValue
33-
],
33+
]
3434
breakingChanges: [{change: "add"}]
3535
)
3636
structure cfnExcludeProperty {}
@@ -39,7 +39,7 @@ structure cfnExcludeProperty {}
3939
/// for the property of the CloudFormation resource.
4040
@unstable
4141
@trait(
42-
selector: "resource > operation -[input, output]-> structure > member",
42+
selector: "resource > operation -[input, output]-> structure > member"
4343
conflicts: [cfnExcludeProperty]
4444
)
4545
structure cfnDefaultValue {}
@@ -48,7 +48,7 @@ structure cfnDefaultValue {}
4848
/// when part of a CloudFormation resource.
4949
@unstable
5050
@trait(
51-
selector: "structure > member",
51+
selector: "structure > member"
5252
conflicts: [cfnExcludeProperty]
5353
)
5454
enum cfnMutability {
@@ -86,16 +86,22 @@ enum cfnMutability {
8686
/// Indicates that a Smithy resource is a CloudFormation resource.
8787
@unstable
8888
@trait(
89-
selector: "resource",
89+
selector: "resource"
9090
breakingChanges: [{change: "presence"}]
9191
)
9292
structure cfnResource {
9393
/// Provides a custom CloudFormation resource name.
94-
name: String,
94+
name: String
9595

9696
/// A list of additional shape IDs of structures that will have their
9797
/// properties added to the CloudFormation resource.
98-
additionalSchemas: StructureIdList,
98+
additionalSchemas: StructureIdList
99+
100+
/// An alternative resource property to use as the primary identifier
101+
/// for the CloudFormation resource. The value MUST be the name of a
102+
/// property on the resource shape that targets a string shape.
103+
@deprecated(message: "Prefer the resource's identifiers when generating resource schemas.")
104+
primaryIdentifier: String
99105
}
100106

101107
@private

smithy-aws-cloudformation-traits/src/test/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourceIndexTest.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public class CfnResourceIndexTest {
2828
private static final ShapeId FOO = ShapeId.from("smithy.example#FooResource");
2929
private static final ShapeId BAR = ShapeId.from("smithy.example#BarResource");
3030
private static final ShapeId BAZ = ShapeId.from("smithy.example#BazResource");
31+
private static final ShapeId TAD = ShapeId.from("smithy.example#TadResource");
3132

3233
private static Model model;
3334
private static CfnResourceIndex cfnResourceIndex;
@@ -118,7 +119,20 @@ public static Collection<ResourceData> data() {
118119
bazResource.readOnlyProperties = SetUtils.of("bazId", "bazImplicitReadProperty");
119120
bazResource.writeOnlyProperties = SetUtils.of("bazImplicitWriteProperty");
120121

121-
return ListUtils.of(fooResource, barResource, bazResource);
122+
ResourceData tadResource = new ResourceData();
123+
tadResource.resourceId = TAD;
124+
tadResource.identifiers = SetUtils.of("tadArn");
125+
tadResource.additionalIdentifiers = ListUtils.of(SetUtils.of("tadId"));
126+
tadResource.mutabilities = MapUtils.of(
127+
"tadId",
128+
SetUtils.of(Mutability.READ),
129+
"tadArn",
130+
SetUtils.of(Mutability.READ));
131+
tadResource.createOnlyProperties = SetUtils.of();
132+
tadResource.readOnlyProperties = SetUtils.of("tadId", "tadArn");
133+
tadResource.writeOnlyProperties = SetUtils.of();
134+
135+
return ListUtils.of(fooResource, tadResource, bazResource, tadResource);
122136
}
123137

124138
@ParameterizedTest

smithy-aws-cloudformation-traits/src/test/java/software/amazon/smithy/aws/cloudformation/traits/CfnResourceTraitTest.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import static org.hamcrest.MatcherAssert.assertThat;
88
import static org.hamcrest.Matchers.contains;
99
import static org.hamcrest.Matchers.equalTo;
10+
import static org.junit.jupiter.api.Assertions.assertEquals;
1011
import static org.junit.jupiter.api.Assertions.assertFalse;
1112
import static org.junit.jupiter.api.Assertions.assertTrue;
1213

@@ -36,6 +37,14 @@ public void loadsFromModel() {
3637
assertThat(barTrait.getName().get(), equalTo("CustomResource"));
3738
assertFalse(barTrait.getAdditionalSchemas().isEmpty());
3839
assertThat(barTrait.getAdditionalSchemas(), contains(ShapeId.from("smithy.example#ExtraBarRequest")));
40+
41+
Shape tadResource = result.expectShape(ShapeId.from("smithy.example#TadResource"));
42+
assertTrue(tadResource.hasTrait(CfnResourceTrait.class));
43+
CfnResourceTrait tadTrait = tadResource.expectTrait(CfnResourceTrait.class);
44+
assertFalse(tadTrait.getName().isPresent());
45+
assertTrue(tadTrait.getAdditionalSchemas().isEmpty());
46+
assertTrue(tadTrait.getPrimaryIdentifier().isPresent());
47+
assertEquals("tadArn", tadTrait.getPrimaryIdentifier().get());
3948
}
4049

4150
@Test

smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/cfn-resources.smithy

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,57 @@ resource FooResource {
1212
}
1313

1414
@cfnResource(
15-
name: "CustomResource",
15+
name: "CustomResource"
1616
additionalSchemas: [ExtraBarRequest]
1717
)
1818
resource BarResource {
1919
identifiers: {
2020
barId: BarId
21-
},
22-
operations: [ExtraBarOperation],
21+
}
22+
operations: [ExtraBarOperation]
23+
}
24+
25+
@cfnResource(primaryIdentifier: "tadArn")
26+
resource TadResource {
27+
identifiers: {
28+
tadId: String
29+
}
30+
properties: {
31+
tadArn: String
32+
}
33+
create: CreateTad
34+
read: GetTad
35+
}
36+
37+
operation CreateTad {
38+
input := {}
39+
output := {
40+
@required
41+
tadId: String
42+
tadArn: String
43+
}
44+
}
45+
46+
@readonly
47+
operation GetTad {
48+
input := {
49+
@required
50+
tadId: String
51+
}
52+
output := {
53+
@required
54+
tadId: String
55+
tadArn: String
56+
}
2357
}
2458

2559
operation ExtraBarOperation {
26-
input: ExtraBarRequest,
60+
input: ExtraBarRequest
2761
}
2862

2963
structure ExtraBarRequest {
3064
@required
31-
barId: BarId,
65+
barId: BarId
3266
}
3367

3468
string FooId
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[ERROR] smithy.example#BarResource: The alternative resource primary identifier, `barArn`, must be a property of the resource. | CfnResourceProperty
2+
[ERROR] smithy.example#BazResource: The alternative resource primary identifier, `bazArn`, targets a `integer` shape, it must target a `string`. | CfnResourceProperty
3+
[WARNING] smithy.example#FooResource: This shape applies a trait that is unstable: aws.cloudformation#cfnResource | UnstableTrait.aws.cloudformation#cfnResource
4+
[WARNING] smithy.example#BarResource: This shape applies a trait that is unstable: aws.cloudformation#cfnResource | UnstableTrait.aws.cloudformation#cfnResource
5+
[WARNING] smithy.example#BazResource: This shape applies a trait that is unstable: aws.cloudformation#cfnResource | UnstableTrait.aws.cloudformation#cfnResource

0 commit comments

Comments
 (0)