Skip to content

Commit 2106393

Browse files
committed
Add "trait" relationship to selectors
A "trait" relationship allows selectors to traverse from shapes to the the trait shapes applied to the shape. This can be used to query for things like "services with no protocol traits", "deprecated traits that are being used on shapes", "traits that aren't referenced by any shapes", etc. Trait relationships are not typically needed and add a lot of overhead. As such, they are opt-in only and enabled in selectors by adding a named "trait" directed relationship. They're also opt-in in code. In order to pull this off, I updated the selector API to always require a Model.
1 parent 688c863 commit 2106393

File tree

20 files changed

+226
-39
lines changed

20 files changed

+226
-39
lines changed

docs/source/spec/core/selectors.rst

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,17 @@ an identifier:
258258

259259
resource:test(-[identifier]->)
260260

261+
Relationships from a shape to the traits applied to the shape can be traversed
262+
using a directed relationship named ``trait``. It is atypical to traverse
263+
``trait`` relationships, therefore they are only yielded by selectors when
264+
explicitly requested using a ``trait`` directed relationship. The following
265+
selector finds all service shapes that have a protocol trait applied to it
266+
(that is, a trait that is marked with the :ref:`protocolDefinition-trait`):
267+
268+
::
269+
270+
service:test(-[trait]-> [trait|protocolDefinition])
271+
261272

262273
.. _selector-relationships:
263274

@@ -353,6 +364,12 @@ The table below lists the labeled directed relationships from each shape.
353364
-
354365
- The shape targeted by the member. Note that member targets have no
355366
relationship name.
367+
* - ``*``
368+
- trait
369+
- Each trait applied to a shape. The neighbor shape is the shape that
370+
defines the trait. This kind of relationship is only traversed if the
371+
``trait`` relationship is explicitly stated as a desired directed
372+
neighbor relationship type.
356373

357374
.. important::
358375

@@ -496,6 +513,20 @@ the member does not have the ``length`` trait:
496513
:test(> string:not([trait|length]))
497514
:test(:not([trait|length]))
498515

516+
The following selector finds all service shapes that do not have a
517+
protocol trait applied to it:
518+
519+
::
520+
521+
service:not(:test(-[trait]-> [trait|protocolDefinition]))
522+
523+
The following selector finds all traits that are not attached to any shape
524+
in the model:
525+
526+
::
527+
528+
:not(* -[trait]-> *)[trait|trait]
529+
499530

500531
:of
501532
~~~
@@ -576,6 +607,7 @@ Selectors are defined by the following ABNF_ grammar.
576607
:/ "instanceOperation"
577608
:/ "resource"
578609
:/ "bound"
610+
:/ "trait"
579611
attr :"[" `attr_key` *(`comparator` `attr_value` ["i"]) "]"
580612
attr_key :`id_attribute` / `trait_attribute` / `service_attribute`
581613
id_attribute :"id" ["|" ("namespace" / "name" / "member")]

smithy-model/src/main/java/software/amazon/smithy/model/loader/ValidatorDefinition.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ List<ValidationEvent> map(Model model, List<ValidationEvent> events) {
5757
// If there's a selector, create a list of candidate shape IDs that can be emitted.
5858
if (selector != null) {
5959
NeighborProvider provider = model.getKnowledge(NeighborProviderIndex.class).getProvider();
60-
candidates = selector.select(provider, model).stream()
60+
candidates = selector.select(model, provider).stream()
6161
.map(Shape::getId)
6262
.collect(Collectors.toSet());
6363
}

smithy-model/src/main/java/software/amazon/smithy/model/neighbor/NeighborProvider.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
package software.amazon.smithy.model.neighbor;
1717

18+
import java.util.ArrayList;
1819
import java.util.HashMap;
1920
import java.util.List;
2021
import java.util.Map;
@@ -57,6 +58,36 @@ static NeighborProvider precomputed(Model model) {
5758
return precomputed(model, of(model));
5859
}
5960

61+
/**
62+
* Creates a NeighborProvider that includes {@link RelationshipType#TRAIT}
63+
* relationships.
64+
*
65+
* @param model Model to use to look up trait shapes.
66+
* @param neighborProvider Provider to wrap.
67+
* @return Returns the created neighbor provider.
68+
*/
69+
static NeighborProvider withTraitRelationships(Model model, NeighborProvider neighborProvider) {
70+
return shape -> {
71+
List<Relationship> relationships = neighborProvider.getNeighbors(shape);
72+
73+
// Don't copy the array unless the shape has traits.
74+
if (shape.getAllTraits().isEmpty()) {
75+
return relationships;
76+
}
77+
78+
// The delegate might have returned an immutable list, so copy first.
79+
relationships = new ArrayList<>(relationships);
80+
for (ShapeId trait : shape.getAllTraits().keySet()) {
81+
Relationship traitRel = model.getShape(trait)
82+
.map(target -> Relationship.create(shape, RelationshipType.TRAIT, target))
83+
.orElseGet(() -> Relationship.createInvalid(shape, RelationshipType.TRAIT, trait));
84+
relationships.add(traitRel);
85+
}
86+
87+
return relationships;
88+
};
89+
}
90+
6091
/**
6192
* Creates a NeighborProvider that precomputes the neighbors of a model.
6293
*

smithy-model/src/main/java/software/amazon/smithy/model/neighbor/RelationshipType.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package software.amazon.smithy.model.neighbor;
1717

1818
import java.util.Optional;
19+
import software.amazon.smithy.model.Model;
1920
import software.amazon.smithy.model.selector.Selector;
2021
import software.amazon.smithy.model.shapes.ListShape;
2122
import software.amazon.smithy.model.shapes.MapShape;
@@ -25,6 +26,7 @@
2526
import software.amazon.smithy.model.shapes.SetShape;
2627
import software.amazon.smithy.model.shapes.StructureShape;
2728
import software.amazon.smithy.model.shapes.UnionShape;
29+
import software.amazon.smithy.model.traits.TraitDefinition;
2830

2931
/**
3032
* Defines the relationship types between neighboring shapes.
@@ -187,7 +189,19 @@ public enum RelationshipType {
187189
* shapes. They reference the {@link MemberShape member} shapes that define
188190
* the members of the union.
189191
*/
190-
UNION_MEMBER("member", RelationshipDirection.DIRECTED);
192+
UNION_MEMBER("member", RelationshipDirection.DIRECTED),
193+
194+
/**
195+
* Relationships that exist between a shape and traits bound to the
196+
* shape. They reference shapes marked with the {@link TraitDefinition}
197+
* trait.
198+
*
199+
* <p>This kind of relationship is not returned by default from a
200+
* {@link NeighborProvider}. You must explicitly wrap a {@link NeighborProvider}
201+
* with {@link NeighborProvider#withTraitRelationships(Model, NeighborProvider)}
202+
* in order to yield trait relationships.
203+
*/
204+
TRAIT("trait", RelationshipDirection.DIRECTED);
191205

192206
private String selectorLabel;
193207
private RelationshipDirection direction;

smithy-model/src/main/java/software/amazon/smithy/model/selector/AndSelector.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import java.util.List;
1919
import java.util.Set;
20+
import software.amazon.smithy.model.Model;
2021
import software.amazon.smithy.model.neighbor.NeighborProvider;
2122
import software.amazon.smithy.model.shapes.Shape;
2223
import software.amazon.smithy.utils.SetUtils;
@@ -40,9 +41,9 @@ static Selector of(List<Selector> predicates) {
4041
}
4142

4243
@Override
43-
public Set<Shape> select(NeighborProvider neighborProvider, Set<Shape> shapes) {
44+
public Set<Shape> select(Model model, NeighborProvider neighborProvider, Set<Shape> shapes) {
4445
for (Selector selector : selectors) {
45-
shapes = selector.select(neighborProvider, shapes);
46+
shapes = selector.select(model, neighborProvider, shapes);
4647
if (shapes.isEmpty()) {
4748
return SetUtils.of();
4849
}

smithy-model/src/main/java/software/amazon/smithy/model/selector/AttributeSelector.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.function.BiFunction;
2323
import java.util.function.Function;
2424
import java.util.stream.Collectors;
25+
import software.amazon.smithy.model.Model;
2526
import software.amazon.smithy.model.neighbor.NeighborProvider;
2627
import software.amazon.smithy.model.shapes.ServiceShape;
2728
import software.amazon.smithy.model.shapes.Shape;
@@ -75,7 +76,7 @@ interface Comparator extends BiFunction<String, String, Boolean> {}
7576
}
7677

7778
@Override
78-
public Set<Shape> select(NeighborProvider neighborProvider, Set<Shape> shapes) {
79+
public Set<Shape> select(Model model, NeighborProvider neighborProvider, Set<Shape> shapes) {
7980
return shapes.stream()
8081
.filter(shape -> matchesAttribute(key.apply(shape)))
8182
.collect(Collectors.toSet());

smithy-model/src/main/java/software/amazon/smithy/model/selector/EachSelector.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import java.util.List;
1919
import java.util.Set;
2020
import java.util.stream.Collectors;
21+
import software.amazon.smithy.model.Model;
2122
import software.amazon.smithy.model.neighbor.NeighborProvider;
2223
import software.amazon.smithy.model.shapes.Shape;
2324

@@ -36,9 +37,9 @@ static Selector of(List<Selector> predicates) {
3637
}
3738

3839
@Override
39-
public Set<Shape> select(NeighborProvider neighborProvider, Set<Shape> shapes) {
40+
public Set<Shape> select(Model model, NeighborProvider neighborProvider, Set<Shape> shapes) {
4041
return selectors.stream()
41-
.flatMap(selector -> selector.select(neighborProvider, shapes).stream())
42+
.flatMap(selector -> selector.select(model, neighborProvider, shapes).stream())
4243
.collect(Collectors.toSet());
4344
}
4445
}

smithy-model/src/main/java/software/amazon/smithy/model/selector/NeighborSelector.java

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,47 +15,62 @@
1515

1616
package software.amazon.smithy.model.selector;
1717

18+
import java.util.HashSet;
1819
import java.util.List;
19-
import java.util.Optional;
2020
import java.util.Set;
21-
import java.util.stream.Collectors;
22-
import java.util.stream.Stream;
21+
import software.amazon.smithy.model.Model;
2322
import software.amazon.smithy.model.neighbor.NeighborProvider;
2423
import software.amazon.smithy.model.neighbor.Relationship;
2524
import software.amazon.smithy.model.neighbor.RelationshipType;
2625
import software.amazon.smithy.model.shapes.Shape;
27-
import software.amazon.smithy.utils.OptionalUtils;
2826

2927
/**
3028
* Traverses into the neighbors of shapes with an optional list of
3129
* neighbor rel filters.
3230
*/
3331
final class NeighborSelector implements Selector {
32+
3433
private final List<String> relTypes;
34+
private final boolean includeTraits;
3535

3636
NeighborSelector(List<String> relTypes) {
3737
this.relTypes = relTypes;
38+
includeTraits = relTypes.contains("trait");
3839
}
3940

4041
@Override
41-
public Set<Shape> select(NeighborProvider neighborProvider, Set<Shape> shapes) {
42-
return shapes.stream()
43-
.flatMap(shape -> neighborProvider.getNeighbors(shape).stream().flatMap(this::mapNeighbor))
44-
.collect(Collectors.toSet());
42+
public Set<Shape> select(Model model, NeighborProvider neighborProvider, Set<Shape> shapes) {
43+
NeighborProvider resolvedProvider = createProvider(model, neighborProvider);
44+
45+
Set<Shape> result = new HashSet<>();
46+
for (Shape shape : shapes) {
47+
for (Relationship rel : resolvedProvider.getNeighbors(shape)) {
48+
if (rel.getNeighborShape().isPresent()) {
49+
Shape target = createNeighbor(rel, rel.getNeighborShape().get());
50+
if (target != null) {
51+
result.add(target);
52+
}
53+
}
54+
}
55+
}
56+
57+
return result;
4558
}
4659

47-
private Stream<Shape> mapNeighbor(Relationship rel) {
48-
return OptionalUtils.stream(rel.getNeighborShape()
49-
.flatMap(target -> createNeighbor(rel, target)));
60+
// Enable trait relationships only if explicitly asked for in a selector.
61+
private NeighborProvider createProvider(Model model, NeighborProvider neighborProvider) {
62+
return includeTraits
63+
? NeighborProvider.withTraitRelationships(model, neighborProvider)
64+
: neighborProvider;
5065
}
5166

52-
private Optional<Shape> createNeighbor(Relationship rel, Shape target) {
67+
private Shape createNeighbor(Relationship rel, Shape target) {
5368
if (rel.getRelationshipType() != RelationshipType.MEMBER_CONTAINER
5469
&& (relTypes.isEmpty() || relTypes.contains(getRelType(rel)))) {
55-
return Optional.of(target);
70+
return target;
5671
}
5772

58-
return Optional.empty();
73+
return null;
5974
}
6075

6176
private static String getRelType(Relationship rel) {

smithy-model/src/main/java/software/amazon/smithy/model/selector/NotSelector.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import java.util.HashSet;
1919
import java.util.List;
2020
import java.util.Set;
21+
import software.amazon.smithy.model.Model;
2122
import software.amazon.smithy.model.neighbor.NeighborProvider;
2223
import software.amazon.smithy.model.shapes.Shape;
2324

@@ -32,10 +33,10 @@ final class NotSelector implements Selector {
3233
}
3334

3435
@Override
35-
public Set<Shape> select(NeighborProvider neighborProvider, Set<Shape> shapes) {
36+
public Set<Shape> select(Model model, NeighborProvider neighborProvider, Set<Shape> shapes) {
3637
Set<Shape> result = new HashSet<>(shapes);
3738
for (Selector predicate : selectors) {
38-
result.removeAll(predicate.select(neighborProvider, shapes));
39+
result.removeAll(predicate.select(model, neighborProvider, shapes));
3940
}
4041
return result;
4142
}

smithy-model/src/main/java/software/amazon/smithy/model/selector/OfSelector.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.List;
2020
import java.util.Optional;
2121
import java.util.Set;
22+
import software.amazon.smithy.model.Model;
2223
import software.amazon.smithy.model.neighbor.NeighborProvider;
2324
import software.amazon.smithy.model.neighbor.RelationshipType;
2425
import software.amazon.smithy.model.shapes.Shape;
@@ -44,7 +45,7 @@ final class OfSelector implements Selector {
4445
}
4546

4647
@Override
47-
public Set<Shape> select(NeighborProvider neighborProvider, Set<Shape> shapes) {
48+
public Set<Shape> select(Model model, NeighborProvider neighborProvider, Set<Shape> shapes) {
4849
Set<Shape> result = new HashSet<>();
4950

5051
// Filter out non-member shapes, and member shapes that cannot
@@ -55,7 +56,7 @@ public Set<Shape> select(NeighborProvider neighborProvider, Set<Shape> shapes) {
5556
// If the parent provides a result for the predicate, then the
5657
// Shape is not filtered out.
5758
boolean anyMatch = selectors.stream()
58-
.anyMatch(selector -> !selector.select(neighborProvider, parentSet).isEmpty());
59+
.anyMatch(selector -> !selector.select(model, neighborProvider, parentSet).isEmpty());
5960
if (anyMatch) {
6061
result.add(shape);
6162
}

smithy-model/src/main/java/software/amazon/smithy/model/selector/PathFinder.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ public List<Path> search(ToShapeId startingShape, Selector targetSelector) {
110110
}
111111

112112
// Find all shapes that match the selector then work backwards from there.
113-
Set<Shape> candidates = targetSelector.select(neighborProvider, model);
113+
Set<Shape> candidates = targetSelector.select(model, neighborProvider);
114114
if (candidates.isEmpty()) {
115115
LOGGER.info(() -> "No shapes matched the PathFinder selector of `" + targetSelector + "`");
116116
return ListUtils.of();

smithy-model/src/main/java/software/amazon/smithy/model/selector/Selector.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,26 +28,27 @@
2828
@FunctionalInterface
2929
public interface Selector {
3030
/** A selector that always returns all provided values. */
31-
Selector IDENTITY = (visitor, shapes) -> shapes;
31+
Selector IDENTITY = (model, visitor, shapes) -> shapes;
3232

3333
/**
3434
* Matches a selector to a set of shapes.
3535
*
36+
* @param model Model used to resolve shapes with.
3637
* @param neighborProvider Provides neighbors for shapes.
3738
* @param shapes Matching context of shapes.
3839
* @return Returns the matching shapes.
3940
*/
40-
Set<Shape> select(NeighborProvider neighborProvider, Set<Shape> shapes);
41+
Set<Shape> select(Model model, NeighborProvider neighborProvider, Set<Shape> shapes);
4142

4243
/**
4344
* Matches a selector against a model using a custom neighbor visitor.
4445
*
45-
* @param neighborProvider Provides neighbors for shapes
4646
* @param model Model to query.
47+
* @param neighborProvider Provides neighbors for shapes
4748
* @return Returns the matching shapes.
4849
*/
49-
default Set<Shape> select(NeighborProvider neighborProvider, Model model) {
50-
return select(neighborProvider, model.toSet());
50+
default Set<Shape> select(Model model, NeighborProvider neighborProvider) {
51+
return select(model, neighborProvider, model.toSet());
5152
}
5253

5354
/**
@@ -57,7 +58,7 @@ default Set<Shape> select(NeighborProvider neighborProvider, Model model) {
5758
* @return Returns the matching shapes.
5859
*/
5960
default Set<Shape> select(Model model) {
60-
return select(NeighborProvider.of(model), model);
61+
return select(model, NeighborProvider.of(model));
6162
}
6263

6364
/**

smithy-model/src/main/java/software/amazon/smithy/model/selector/ShapeTypeCategorySelector.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import java.util.Set;
1919
import java.util.stream.Collectors;
20+
import software.amazon.smithy.model.Model;
2021
import software.amazon.smithy.model.neighbor.NeighborProvider;
2122
import software.amazon.smithy.model.shapes.Shape;
2223

@@ -28,7 +29,7 @@ final class ShapeTypeCategorySelector implements Selector {
2829
}
2930

3031
@Override
31-
public Set<Shape> select(NeighborProvider neighborProvider, Set<Shape> shapes) {
32+
public Set<Shape> select(Model model, NeighborProvider neighborProvider, Set<Shape> shapes) {
3233
return shapes.stream().filter(shapeCategory::isInstance).collect(Collectors.toSet());
3334
}
3435
}

0 commit comments

Comments
 (0)