Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lower resource identifier collisions to warning #1474

Merged
merged 1 commit into from
Oct 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/source-1.0/spec/core/model.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1950,6 +1950,10 @@ on ``GetHistoricalForecastInput$customHistoricalIdName`` maps that member
to the "historicalId" identifier.


If an operation input supplies both an explicit and an implicit identifier
binding, the explicit identifier binding is utilized.


.. _lifecycle-operations:

Resource lifecycle operations
Expand Down
4 changes: 4 additions & 0 deletions docs/source-2.0/spec/service-types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,10 @@ maps it to the "forecastId" identifier is provided by the
on ``GetHistoricalForecastInput$customHistoricalIdName`` maps that member
to the "historicalId" identifier.

If an operation input supplies both an explicit and an implicit identifier
binding, the explicit identifier binding is utilized.


.. _resource-properties:

Resource Properties
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,10 +169,12 @@ private Map<String, String> computeBindings(ResourceShape resource, StructureSha
if (member.hasTrait(ResourceIdentifierTrait.class)) {
// Mark as a binding if the member has an explicit @resourceIdentifier trait.
String bindingName = member.expectTrait(ResourceIdentifierTrait.class).getValue();
// Override any implicit bindings with explicit trait bindings.
bindings.put(bindingName, member.getMemberName());
} else if (isImplicitIdentifierBinding(member, resource)) {
// Mark as a binding if the member is an implicit identifier binding.
bindings.put(member.getMemberName(), member.getMemberName());
// Only utilize implicit bindings when an explicit binding wasn't found.
bindings.putIfAbsent(member.getMemberName(), member.getMemberName());
}
}
return bindings;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import static java.lang.String.format;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
Expand Down Expand Up @@ -63,14 +62,25 @@ public List<ValidationEvent> validate(Model model) {
// Check if this shape has conflicting resource identifier bindings due to trait bindings.
private void validateResourceIdentifierTraits(Model model, List<ValidationEvent> events) {
for (ShapeId container : findStructuresWithResourceIdentifierTraits(model)) {
Map<String, Set<String>> bindings = computePotentialStructureBindings(model, container);
if (!model.getShape(container).isPresent()) {
continue;
}

Shape structure = model.expectShape(container);
Map<String, Set<String>> bindings = computePotentialStructureBindings(structure);
for (Map.Entry<String, Set<String>> entry : bindings.entrySet()) {
if (entry.getValue().size() > 1) {
events.add(error(model.expectShape(container), String.format(
"Conflicting resource identifier member bindings found for identifier '%s' between "
+ "members %s", entry.getKey(), String.join(", ", entry.getValue()))));
// Only emit this event if the potential bindings contains
// more than the implicit binding.
if (entry.getValue().size() > 1 && entry.getValue().contains(entry.getKey())) {
Set<String> explicitBindings = entry.getValue();
explicitBindings.remove(entry.getKey());
events.add(warning(structure, String.format(
"Implicit resource identifier for '%s' is overridden by `resourceIdentifier` trait on "
+ "members: '%s'", entry.getKey(), String.join("', '", explicitBindings))));
}
}

validateResourceIdentifierTraitConflicts(structure, events);
}
}

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

private Map<String, Set<String>> computePotentialStructureBindings(Model model, ShapeId container) {
return model.getShape(container).map(struct -> {
Map<String, Set<String>> bindings = new HashMap<>();
// Ensure no two members are bound to the same identifier.
for (MemberShape member : struct.members()) {
String bindingName = member.getTrait(ResourceIdentifierTrait.class)
.map(ResourceIdentifierTrait::getValue)
.orElseGet(member::getMemberName);
bindings.computeIfAbsent(bindingName, k -> new HashSet<>()).add(member.getMemberName());
private Map<String, Set<String>> computePotentialStructureBindings(Shape structure) {
Map<String, Set<String>> bindings = new HashMap<>();
// Ensure no two members are bound to the same identifier.
for (MemberShape member : structure.members()) {
String bindingName = member.getTrait(ResourceIdentifierTrait.class)
.map(ResourceIdentifierTrait::getValue)
.orElseGet(member::getMemberName);
bindings.computeIfAbsent(bindingName, k -> new HashSet<>()).add(member.getMemberName());
}
return bindings;
}

private void validateResourceIdentifierTraitConflicts(Shape structure, List<ValidationEvent> events) {
Map<String, Set<String>> explicitBindings = new HashMap<>();
// Ensure no two members use a resourceIdentifier trait to bind to
// the same identifier.
for (MemberShape member : structure.members()) {
if (member.hasTrait(ResourceIdentifierTrait.class)) {
explicitBindings.computeIfAbsent(member.expectTrait(ResourceIdentifierTrait.class).getValue(),
k -> new HashSet<>()).add(member.getMemberName());
}
return bindings;
}).orElse(Collections.emptyMap());
}

for (Map.Entry<String, Set<String>> entry : explicitBindings.entrySet()) {
if (entry.getValue().size() > 1) {
events.add(error(structure, String.format(
"Conflicting resource identifier member bindings found for identifier '%s' between "
+ "members: '%s'", entry.getKey(), String.join("', '", entry.getValue()))));
}
}
}

private void validateOperationBindings(Model model, List<ValidationEvent> events) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import software.amazon.smithy.model.traits.ReadonlyTrait;
import software.amazon.smithy.model.traits.RequiredTrait;
import software.amazon.smithy.model.traits.ResourceIdentifierTrait;
import software.amazon.smithy.utils.MapUtils;

public class IdentifierBindingIndexTest {

Expand Down Expand Up @@ -144,9 +145,19 @@ public void findsExplicitBindings() {
}

@Test
public void doesNotFailWhenLoadingModelWithCollidingMemberBindings() {
public void explicitBindingsPreferred() {
// Ensure that this does not fail to load. This previously failed when using Collectors.toMap due to
// a collision in the keys used to map an identifier to multiple members.
Model.assembler().addImport(getClass().getResource("colliding-resource-identifiers.smithy")).assemble();
Model model = Model.assembler()
.addImport(getClass().getResource("colliding-resource-identifiers.smithy"))
.assemble()
.unwrap();
IdentifierBindingIndex index = IdentifierBindingIndex.of(model);

// Ensure that the explicit trait bindings are preferred over implicit bindings.
assertThat(index.getOperationInputBindings(
ShapeId.from("smithy.example#Foo"),
ShapeId.from("smithy.example#GetFoo")),
equalTo(MapUtils.of("bar", "bam")));
}
}
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
[ERROR] smithy.example#GetFooInput: Conflicting resource identifier member bindings found for identifier 'bar' between members bar, bam | ResourceIdentifierBinding
[ERROR] smithy.example#PutFooInput: Conflicting resource identifier member bindings found for identifier 'bar' between members a, b | ResourceIdentifierBinding
[WARNING] smithy.example#GetFooInput: Implicit resource identifier for 'bar' is overridden by `resourceIdentifier` trait on members: 'bam' | ResourceIdentifierBinding
[ERROR] smithy.example#PutFooInput: Conflicting resource identifier member bindings found for identifier 'bar' between members: 'a', 'b' | ResourceIdentifierBinding
[ERROR] smithy.example#DeleteFooInput: Conflicting resource identifier member bindings found for identifier 'bar' between members: 'a', 'b' | ResourceIdentifierBinding
[WARNING] smithy.example#DeleteFooInput: Implicit resource identifier for 'bar' is overridden by `resourceIdentifier` trait on members: 'a', 'b' | ResourceIdentifierBinding
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ resource Foo {
}
read: GetFoo
put: PutFoo
delete: DeleteFoo
}

@readonly
Expand All @@ -25,7 +26,22 @@ operation GetFoo {
@idempotent
operation PutFoo {
input:= {
@required
@resourceIdentifier("bar")
@required
a: String

@resourceIdentifier("bar")
@required
b: String
}
}

@idempotent
operation DeleteFoo {
input:= {
@required
bar: String

@resourceIdentifier("bar")
@required
a: String
Expand Down