Skip to content

Commit d8185ac

Browse files
committed
Lower resource identifier collisions to warning
This commit updates the validation for resource identifier collisions to warn instead of error. When an explicit and implicit binding for the same resource identifier are found, the explicit binding is preferred.
1 parent c6ac196 commit d8185ac

File tree

7 files changed

+90
-23
lines changed

7 files changed

+90
-23
lines changed

docs/source-1.0/spec/core/model.rst

+4
Original file line numberDiff line numberDiff line change
@@ -1950,6 +1950,10 @@ on ``GetHistoricalForecastInput$customHistoricalIdName`` maps that member
19501950
to the "historicalId" identifier.
19511951

19521952

1953+
If an operation input supplies both an explicit and an implicit identifier
1954+
binding, the explicit identifier binding is utilized.
1955+
1956+
19531957
.. _lifecycle-operations:
19541958

19551959
Resource lifecycle operations

docs/source-2.0/spec/service-types.rst

+4
Original file line numberDiff line numberDiff line change
@@ -678,6 +678,10 @@ maps it to the "forecastId" identifier is provided by the
678678
on ``GetHistoricalForecastInput$customHistoricalIdName`` maps that member
679679
to the "historicalId" identifier.
680680

681+
If an operation input supplies both an explicit and an implicit identifier
682+
binding, the explicit identifier binding is utilized.
683+
684+
681685
.. _resource-properties:
682686

683687
Resource Properties

smithy-model/src/main/java/software/amazon/smithy/model/knowledge/IdentifierBindingIndex.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -169,10 +169,12 @@ private Map<String, String> computeBindings(ResourceShape resource, StructureSha
169169
if (member.hasTrait(ResourceIdentifierTrait.class)) {
170170
// Mark as a binding if the member has an explicit @resourceIdentifier trait.
171171
String bindingName = member.expectTrait(ResourceIdentifierTrait.class).getValue();
172+
// Override any implicit bindings with explicit trait bindings.
172173
bindings.put(bindingName, member.getMemberName());
173174
} else if (isImplicitIdentifierBinding(member, resource)) {
174175
// Mark as a binding if the member is an implicit identifier binding.
175-
bindings.put(member.getMemberName(), member.getMemberName());
176+
// Only utilize implicit bindings when an explicit binding wasn't found.
177+
bindings.putIfAbsent(member.getMemberName(), member.getMemberName());
176178
}
177179
}
178180
return bindings;

smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/ResourceIdentifierBindingValidator.java

+45-17
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import static java.lang.String.format;
1919

2020
import java.util.ArrayList;
21-
import java.util.Collections;
2221
import java.util.HashMap;
2322
import java.util.HashSet;
2423
import java.util.List;
@@ -63,14 +62,25 @@ public List<ValidationEvent> validate(Model model) {
6362
// Check if this shape has conflicting resource identifier bindings due to trait bindings.
6463
private void validateResourceIdentifierTraits(Model model, List<ValidationEvent> events) {
6564
for (ShapeId container : findStructuresWithResourceIdentifierTraits(model)) {
66-
Map<String, Set<String>> bindings = computePotentialStructureBindings(model, container);
65+
if (!model.getShape(container).isPresent()) {
66+
continue;
67+
}
68+
69+
Shape structure = model.expectShape(container);
70+
Map<String, Set<String>> bindings = computePotentialStructureBindings(structure);
6771
for (Map.Entry<String, Set<String>> entry : bindings.entrySet()) {
68-
if (entry.getValue().size() > 1) {
69-
events.add(error(model.expectShape(container), String.format(
70-
"Conflicting resource identifier member bindings found for identifier '%s' between "
71-
+ "members %s", entry.getKey(), String.join(", ", entry.getValue()))));
72+
// Only emit this event if the potential bindings contains
73+
// more than the implicit binding.
74+
if (entry.getValue().size() > 1 && entry.getValue().contains(entry.getKey())) {
75+
Set<String> explicitBindings = entry.getValue();
76+
explicitBindings.remove(entry.getKey());
77+
events.add(warning(structure, String.format(
78+
"Implicit resource identifier for '%s' is overridden by `resourceIdentifier` trait on "
79+
+ "members: '%s'", entry.getKey(), String.join("', '", explicitBindings))));
7280
}
7381
}
82+
83+
validateResourceIdentifierTraitConflicts(structure, events);
7484
}
7585
}
7686

@@ -82,18 +92,36 @@ private Set<ShapeId> findStructuresWithResourceIdentifierTraits(Model model) {
8292
return containers;
8393
}
8494

85-
private Map<String, Set<String>> computePotentialStructureBindings(Model model, ShapeId container) {
86-
return model.getShape(container).map(struct -> {
87-
Map<String, Set<String>> bindings = new HashMap<>();
88-
// Ensure no two members are bound to the same identifier.
89-
for (MemberShape member : struct.members()) {
90-
String bindingName = member.getTrait(ResourceIdentifierTrait.class)
91-
.map(ResourceIdentifierTrait::getValue)
92-
.orElseGet(member::getMemberName);
93-
bindings.computeIfAbsent(bindingName, k -> new HashSet<>()).add(member.getMemberName());
95+
private Map<String, Set<String>> computePotentialStructureBindings(Shape structure) {
96+
Map<String, Set<String>> bindings = new HashMap<>();
97+
// Ensure no two members are bound to the same identifier.
98+
for (MemberShape member : structure.members()) {
99+
String bindingName = member.getTrait(ResourceIdentifierTrait.class)
100+
.map(ResourceIdentifierTrait::getValue)
101+
.orElseGet(member::getMemberName);
102+
bindings.computeIfAbsent(bindingName, k -> new HashSet<>()).add(member.getMemberName());
103+
}
104+
return bindings;
105+
}
106+
107+
private void validateResourceIdentifierTraitConflicts(Shape structure, List<ValidationEvent> events) {
108+
Map<String, Set<String>> explicitBindings = new HashMap<>();
109+
// Ensure no two members use a resourceIdentifier trait to bind to
110+
// the same identifier.
111+
for (MemberShape member : structure.members()) {
112+
if (member.hasTrait(ResourceIdentifierTrait.class)) {
113+
explicitBindings.computeIfAbsent(member.expectTrait(ResourceIdentifierTrait.class).getValue(),
114+
k -> new HashSet<>()).add(member.getMemberName());
94115
}
95-
return bindings;
96-
}).orElse(Collections.emptyMap());
116+
}
117+
118+
for (Map.Entry<String, Set<String>> entry : explicitBindings.entrySet()) {
119+
if (entry.getValue().size() > 1) {
120+
events.add(error(structure, String.format(
121+
"Conflicting resource identifier member bindings found for identifier '%s' between "
122+
+ "members: '%s'", entry.getKey(), String.join("', '", entry.getValue()))));
123+
}
124+
}
97125
}
98126

99127
private void validateOperationBindings(Model model, List<ValidationEvent> events) {

smithy-model/src/test/java/software/amazon/smithy/model/knowledge/IdentifierBindingIndexTest.java

+13-2
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import software.amazon.smithy.model.traits.ReadonlyTrait;
3434
import software.amazon.smithy.model.traits.RequiredTrait;
3535
import software.amazon.smithy.model.traits.ResourceIdentifierTrait;
36+
import software.amazon.smithy.utils.MapUtils;
3637

3738
public class IdentifierBindingIndexTest {
3839

@@ -144,9 +145,19 @@ public void findsExplicitBindings() {
144145
}
145146

146147
@Test
147-
public void doesNotFailWhenLoadingModelWithCollidingMemberBindings() {
148+
public void explicitBindingsPreferred() {
148149
// Ensure that this does not fail to load. This previously failed when using Collectors.toMap due to
149150
// a collision in the keys used to map an identifier to multiple members.
150-
Model.assembler().addImport(getClass().getResource("colliding-resource-identifiers.smithy")).assemble();
151+
Model model = Model.assembler()
152+
.addImport(getClass().getResource("colliding-resource-identifiers.smithy"))
153+
.assemble()
154+
.unwrap();
155+
IdentifierBindingIndex index = IdentifierBindingIndex.of(model);
156+
157+
// Ensure that the explicit trait bindings are preferred over implicit bindings.
158+
assertThat(index.getOperationInputBindings(
159+
ShapeId.from("smithy.example#Foo"),
160+
ShapeId.from("smithy.example#GetFoo")),
161+
equalTo(MapUtils.of("bar", "bam")));
151162
}
152163
}
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
1-
[ERROR] smithy.example#GetFooInput: Conflicting resource identifier member bindings found for identifier 'bar' between members bar, bam | ResourceIdentifierBinding
2-
[ERROR] smithy.example#PutFooInput: Conflicting resource identifier member bindings found for identifier 'bar' between members a, b | ResourceIdentifierBinding
1+
[WARNING] smithy.example#GetFooInput: Implicit resource identifier for 'bar' is overridden by `resourceIdentifier` trait on members: 'bam' | ResourceIdentifierBinding
2+
[ERROR] smithy.example#PutFooInput: Conflicting resource identifier member bindings found for identifier 'bar' between members: 'a', 'b' | ResourceIdentifierBinding
3+
[ERROR] smithy.example#DeleteFooInput: Conflicting resource identifier member bindings found for identifier 'bar' between members: 'a', 'b' | ResourceIdentifierBinding
4+
[WARNING] smithy.example#DeleteFooInput: Implicit resource identifier for 'bar' is overridden by `resourceIdentifier` trait on members: 'a', 'b' | ResourceIdentifierBinding

smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/resource-identifiers/detects-conflicting-bindings.smithy

+17-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ resource Foo {
88
}
99
read: GetFoo
1010
put: PutFoo
11+
delete: DeleteFoo
1112
}
1213

1314
@readonly
@@ -25,7 +26,22 @@ operation GetFoo {
2526
@idempotent
2627
operation PutFoo {
2728
input:= {
28-
@required
29+
@resourceIdentifier("bar")
30+
@required
31+
a: String
32+
33+
@resourceIdentifier("bar")
34+
@required
35+
b: String
36+
}
37+
}
38+
39+
@idempotent
40+
operation DeleteFoo {
41+
input:= {
42+
@required
43+
bar: String
44+
2945
@resourceIdentifier("bar")
3046
@required
3147
a: String

0 commit comments

Comments
 (0)