From edd987a1fa006a7fe0b028f2d115bb94efe3f4e2 Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Sat, 18 Mar 2023 23:07:49 -0700 Subject: [PATCH] Add :root and :in selectors, fix variable bug This commit adds support for the `:in` and `:root` functions. The `:in` function is a simpler way to check if a shape is in an expression, typically used to test if a variable contains a shape or if a root expression contains a shape. `:root` is used to create rooted common subexpressions that are evaluated once against every shape in the model. `:root` expressions are evaluate in an isolated context, so any variables used or stored by them are not accessible outside the root selector. `:root` selectors allows selection to be broken into multiple steps and evaluate globally. Let's say you want all number shapes that are used in operation inputs, but not used in operation outputs. This can be done today using the following expression: ``` service $outputs(~> operation -[output]-> ~> number) ~> operation -[input]-> ~> number :not([@: @{id} = @{var|outputs|id}]) ``` With the addition of the ``:in` selector, this gets easier because we can avoid using a scoped attribute selector: ``` service $outputs(~> operation -[output]-> ~> number) ~> operation -[input]-> ~> number :not(:in(${outputs})) ``` (Note: to make this work, I had to uncover and fix a bug in the implementation of how we store variables. We previously used `Collection#add` as a `Receiver`, but that method will return false if it's already seen a shape, which is wrong.) With the addition of `:root`, you can use a much simpler expression: ``` number :in(:root(service ~> operation -[input]-> ~> number)) :not(:in(:root(service ~> operation -[output]-> ~> number))) ``` (Note: the result of root expressions are run once and cached. No need to store them in a variable) These expressions _seem_ to be exactly the same, however, the `:root` expression gives a different result when working with models that contain multiple services. In the first two expressions, if any service uses shape X in input and not output, then X is a result. However, in the `:root` expression, X is only part of the result if no service uses it in their output shape closures. --- docs/source-2.0/spec/selectors.rst | 82 ++++++++++++ .../smithy/cli/commands/SelectCommand.java | 6 + .../software/amazon/smithy/model/Model.java | 10 ++ .../selector/AbstractNeighborSelector.java | 10 +- .../smithy/model/selector/AndSelector.java | 120 +++--------------- .../model/selector/AttributeSelector.java | 25 ++-- .../amazon/smithy/model/selector/Context.java | 46 +++---- .../selector/ForwardNeighborSelector.java | 2 +- .../smithy/model/selector/InSelector.java | 71 +++++++++++ .../model/selector/InternalSelector.java | 71 +++++++++-- .../smithy/model/selector/IsSelector.java | 8 +- .../smithy/model/selector/NotSelector.java | 4 +- .../selector/RecursiveNeighborSelector.java | 8 +- .../selector/ReverseNeighborSelector.java | 2 +- .../smithy/model/selector/RootSelector.java | 53 ++++++++ .../selector/ScopedAttributeSelector.java | 4 +- .../smithy/model/selector/SelectorParser.java | 23 +++- .../selector/ShapeTypeCategorySelector.java | 16 ++- .../model/selector/ShapeTypeSelector.java | 16 ++- .../smithy/model/selector/TestSelector.java | 7 +- .../model/selector/TopDownSelector.java | 18 +-- .../model/selector/VariableGetSelector.java | 22 +++- .../model/selector/VariableStoreSelector.java | 7 +- .../model/selector/WrappedSelector.java | 84 +++++++----- .../smithy/model/selector/SelectorTest.java | 49 ++++++- .../ShapeTypeCategorySelectorTest.java | 46 +++++++ .../model/selector/ShapeTypeSelectorTest.java | 7 + .../model/selector/cases/in-function.smithy | 75 +++++++++++ .../model/selector/cases/root-function.smithy | 61 +++++++++ 29 files changed, 719 insertions(+), 234 deletions(-) create mode 100644 smithy-model/src/main/java/software/amazon/smithy/model/selector/InSelector.java create mode 100644 smithy-model/src/main/java/software/amazon/smithy/model/selector/RootSelector.java create mode 100644 smithy-model/src/test/java/software/amazon/smithy/model/selector/ShapeTypeCategorySelectorTest.java create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/selector/cases/in-function.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/selector/cases/root-function.smithy diff --git a/docs/source-2.0/spec/selectors.rst b/docs/source-2.0/spec/selectors.rst index 86913164a66..fe090d606b1 100644 --- a/docs/source-2.0/spec/selectors.rst +++ b/docs/source-2.0/spec/selectors.rst @@ -1401,6 +1401,88 @@ trait applied to it: service :not(-[trait]-> [trait|protocolDefinition]) +.. _selector-in-function: + +``:in`` +------- + +The ``:in`` function is used to test if a shape is contained within the +result of an expression. This function is most useful when testing if a +:ref:`variable ` or the result of a +:ref:`root ` function contains a shape. The ``:in`` +function requires exactly one selector. If a shape is contained in the +result of evaluating the selector, the shape is yielded from the function. + +The following example finds all numbers that are used in service operation +inputs and not used in service operation outputs: + +.. code-block:: none + :caption: :in example using variables + :name: in-variable-input-output-example + + service + $outputs(~> operation -[output]-> ~> number) + ~> operation -[input]-> ~> number + :not(:in(${outputs})) + +.. note:: + + The above example returns the aggregate results of applying the selector + to every shape: if a model contains multiple services, and one of the + services uses a number 'X' in input and not output, but another service + uses 'X' in both input and output, 'X' is part of the matched shapes. + Use the :ref:`:root function ` to match shapes + globally. + + +.. _selector-root-function: + +``:root`` +--------- + +The ``:root`` function evaluates a subexpression against *all* shapes in the +model and yields all matches. The ``:root`` function is useful for breaking +a selector down into smaller operations, and it works best when used with +:ref:`variables ` or the :ref:`:in function `. +The ``:root`` function requires exactly one selector. + +The following example finds all numbers that are used in any operation inputs +and not used in any operation outputs: + +.. code-block:: none + + number + :in(:root(service ~> operation -[input]-> ~> number)) + :not(:in(:root(service ~> operation -[output]-> ~> number))) + +.. note:: + + The above example is similar to ":ref:`in-variable-input-output-example`" + but works independent of services. That is, if a model contains multiple + services, and one of the services uses a number 'X' in input and not + output, but another service uses 'X' in both input and output, 'X' + *is not* part of the matched shapes. + + +:root functions are isolated subexpressions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The expression evaluated by a ``:root`` expression is evaluated in an isolated +context from the rest of the expression. The selector provided to a ``:root`` +function cannot access variables defined outside the function, and variables +defined in the selector do not persist outside the selector. + + +:root functions are evaluated at most once +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +There is no need to store the result of a ``:root`` function in a variable +because ``:root`` selector functions are considered global common +subexpressions and are evaluated at most once during the selection process. +Implementations MAY choose to evaluate ``:root`` expressions eagerly or +lazily, though they MUST evaluate ``:root`` expressions no more than once. + + ``:topdown`` ------------ diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/SelectCommand.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/SelectCommand.java index 44630058b38..caeaa02e120 100644 --- a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/SelectCommand.java +++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/SelectCommand.java @@ -21,6 +21,7 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import java.util.logging.Logger; import java.util.stream.Stream; import software.amazon.smithy.build.model.SmithyBuildConfig; import software.amazon.smithy.cli.ArgumentReceiver; @@ -41,6 +42,8 @@ final class SelectCommand extends ClasspathCommand { + private static final Logger LOGGER = Logger.getLogger(SelectCommand.class.getName()); + SelectCommand(String parentCommandName, DependencyResolver.Factory dependencyResolverFactory) { super(parentCommandName, dependencyResolverFactory); } @@ -117,6 +120,7 @@ int runWithClassLoader(SmithyBuildConfig config, Arguments arguments, Env env, L Model model = CommandUtils.buildModel(arguments, models, env, env.stderr(), true, config); Selector selector = options.selector(); + long startTime = System.nanoTime(); if (!options.vars()) { sortShapeIds(selector.select(model)).forEach(stdout::println); } else { @@ -130,6 +134,8 @@ int runWithClassLoader(SmithyBuildConfig config, Arguments arguments, Env env, L }); stdout.println(Node.prettyPrintJson(new ArrayNode(result, SourceLocation.NONE))); } + long endTime = System.nanoTime(); + LOGGER.fine(() -> "Select time: " + ((endTime - startTime) / 1000000) + "ms"); return 0; } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/Model.java b/smithy-model/src/main/java/software/amazon/smithy/model/Model.java index 112b33fa8ff..c317a67b346 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/Model.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/Model.java @@ -802,6 +802,16 @@ public boolean contains(Object o) { public Iterator iterator() { return shapeMap.values().iterator(); } + + @Override + public Stream stream() { + return shapes(); + } + + @Override + public Stream parallelStream() { + return shapes().parallel(); + } }; } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/selector/AbstractNeighborSelector.java b/smithy-model/src/main/java/software/amazon/smithy/model/selector/AbstractNeighborSelector.java index 70aa5f2a70e..ed2cb134f3c 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/selector/AbstractNeighborSelector.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/selector/AbstractNeighborSelector.java @@ -32,23 +32,23 @@ abstract class AbstractNeighborSelector implements InternalSelector { } @Override - public final boolean push(Context context, Shape shape, Receiver next) { + public final Response push(Context context, Shape shape, Receiver next) { NeighborProvider resolvedProvider = getNeighborProvider(context, includeTraits); for (Relationship rel : resolvedProvider.getNeighbors(shape)) { if (matches(rel)) { - if (!emitMatchingRel(context, rel, next)) { + if (emitMatchingRel(context, rel, next) == Response.STOP) { // Stop pushing shapes upstream and propagate the signal to stop. - return false; + return Response.STOP; } } } - return true; + return Response.CONTINUE; } abstract NeighborProvider getNeighborProvider(Context context, boolean includeTraits); - abstract boolean emitMatchingRel(Context context, Relationship rel, Receiver next); + abstract Response emitMatchingRel(Context context, Relationship rel, Receiver next); private boolean matches(Relationship rel) { return rel.getNeighborShape().isPresent() diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/selector/AndSelector.java b/smithy-model/src/main/java/software/amazon/smithy/model/selector/AndSelector.java index fd7cda1c459..fa4ee23cdc5 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/selector/AndSelector.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/selector/AndSelector.java @@ -15,7 +15,9 @@ package software.amazon.smithy.model.selector; +import java.util.Collection; import java.util.List; +import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.Shape; /** @@ -33,122 +35,38 @@ private AndSelector() {} static InternalSelector of(List selectors) { switch (selectors.size()) { case 0: - // This happens when selectors are optimized (i.e., the first internal - // selector is a shape type and it gets applied in Model.shape() before - // pushing shapes through the selector. return InternalSelector.IDENTITY; case 1: // If there's only a single selector, then no need to wrap. return selectors.get(0); case 2: - // Cases 2-7 are optimizations that make selectors about - // 40% faster based on JMH benchmarks (at least on my machine, - // JDK 11.0.5, Java HotSpot(TM) 64-Bit Server VM, 11.0.5+10-LTS). - // I stopped at 7 because, it needs to stop somewhere, and it's lucky. - return (c, s, n) -> { - return selectors.get(0).push(c, s, (c2, s2) -> { - return selectors.get(1).push(c2, s2, n); - }); - }; - case 3: - return (c, s, n) -> { - return selectors.get(0).push(c, s, (c2, s2) -> { - return selectors.get(1).push(c2, s2, (c3, s3) -> { - return selectors.get(2).push(c3, s3, n); - }); - }); - }; - case 4: - return (c, s, n) -> { - return selectors.get(0).push(c, s, (c2, s2) -> { - return selectors.get(1).push(c2, s2, (c3, s3) -> { - return selectors.get(2).push(c3, s3, (c4, s4) -> { - return selectors.get(3).push(c4, s4, n); - }); - }); - }); - }; - case 5: - return (c, s, n) -> { - return selectors.get(0).push(c, s, (c2, s2) -> { - return selectors.get(1).push(c2, s2, (c3, s3) -> { - return selectors.get(2).push(c3, s3, (c4, s4) -> { - return selectors.get(3).push(c4, s4, (c5, s5) -> { - return selectors.get(4).push(c5, s5, n); - }); - }); - }); - }); - }; - case 6: - return (c, s, n) -> { - return selectors.get(0).push(c, s, (c2, s2) -> { - return selectors.get(1).push(c2, s2, (c3, s3) -> { - return selectors.get(2).push(c3, s3, (c4, s4) -> { - return selectors.get(3).push(c4, s4, (c5, s5) -> { - return selectors.get(4).push(c5, s5, (c6, s6) -> { - return selectors.get(5).push(c6, s6, n); - }); - }); - }); - }); - }); - }; - case 7: - return (c, s, n) -> { - return selectors.get(0).push(c, s, (c2, s2) -> { - return selectors.get(1).push(c2, s2, (c3, s3) -> { - return selectors.get(2).push(c3, s3, (c4, s4) -> { - return selectors.get(3).push(c4, s4, (c5, s5) -> { - return selectors.get(4).push(c5, s5, (c6, s6) -> { - return selectors.get(5).push(c6, s6, (c7, s7) -> { - return selectors.get(6).push(c7, s7, n); - }); - }); - }); - }); - }); - }); - }; + return new IntermediateAndSelector(selectors.get(0), selectors.get(1)); default: - return new RecursiveAndSelector(selectors); + InternalSelector result = selectors.get(selectors.size() - 1); + for (int i = selectors.size() - 2; i >= 0; i--) { + result = new IntermediateAndSelector(selectors.get(i), result); + } + return result; } } - static final class RecursiveAndSelector implements InternalSelector { - - private final List selectors; - private final int terminalSelectorIndex; + static final class IntermediateAndSelector implements InternalSelector { + private final InternalSelector leftSelector; + private final InternalSelector rightSelector; - private RecursiveAndSelector(List selectors) { - this.selectors = selectors; - this.terminalSelectorIndex = this.selectors.size() - 1; + IntermediateAndSelector(InternalSelector leftSelector, InternalSelector rightSelector) { + this.leftSelector = leftSelector; + this.rightSelector = rightSelector; } @Override - public boolean push(Context context, Shape shape, Receiver next) { - // This is safe since the number of selectors is always >= 2. - return selectors.get(0).push(context, shape, new State(1, next)); + public Response push(Context ctx, Shape shape, Receiver next) { + return leftSelector.push(ctx, shape, (c, s) -> rightSelector.push(c, s, next)); } - private final class State implements Receiver { - - private final int position; - private final Receiver downstream; - - private State(int position, Receiver downstream) { - this.position = position; - this.downstream = downstream; - } - - @Override - public boolean apply(Context context, Shape shape) { - if (position == terminalSelectorIndex) { - return selectors.get(position).push(context, shape, downstream); - } else { - return selectors.get(position).push(context, shape, new State(position + 1, downstream)); - } - } + @Override + public Collection getStartingShapes(Model model) { + return leftSelector.getStartingShapes(model); } } } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/selector/AttributeSelector.java b/smithy-model/src/main/java/software/amazon/smithy/model/selector/AttributeSelector.java index ba8248ea3cf..80198ef7fb2 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/selector/AttributeSelector.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/selector/AttributeSelector.java @@ -34,6 +34,7 @@ final class AttributeSelector implements InternalSelector { private final List expected; private final AttributeComparator comparator; private final boolean caseInsensitive; + private final Function> optimizer; AttributeSelector( List path, @@ -54,14 +55,7 @@ final class AttributeSelector implements InternalSelector { this.expected.add(AttributeValue.literal(validValue)); } } - } - static AttributeSelector existence(List path) { - return new AttributeSelector(path, null, null, false); - } - - @Override - public Function> optimize() { // Optimization for loading shapes with a specific trait. // This optimization can only be applied when there's no comparator, // and it doesn't matter how deep into the trait the selector descends. @@ -69,23 +63,32 @@ public Function> optimize() { && path.size() >= 2 && path.get(0).equals("trait") // only match on traits && !path.get(1).startsWith("(")) { // don't match projections - return model -> { + optimizer = model -> { // The trait name might be relative to the prelude, so ensure it's absolute. String absoluteShapeId = Trait.makeAbsoluteName(path.get(1)); ShapeId trait = ShapeId.from(absoluteShapeId); return model.getShapesWithTrait(trait); }; } else { - return null; + optimizer = Model::toSet; } } + static AttributeSelector existence(List path) { + return new AttributeSelector(path, null, null, false); + } + + @Override + public Collection getStartingShapes(Model model) { + return optimizer.apply(model); + } + @Override - public boolean push(Context context, Shape shape, Receiver next) { + public Response push(Context context, Shape shape, Receiver next) { if (matchesAttribute(shape, context)) { return next.apply(context, shape); } else { - return true; + return Response.CONTINUE; } } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/selector/Context.java b/smithy-model/src/main/java/software/amazon/smithy/model/selector/Context.java index c1eddd8abb1..d0de7a43b9a 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/selector/Context.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/selector/Context.java @@ -16,8 +16,10 @@ package software.amazon.smithy.model.selector; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Set; +import software.amazon.smithy.model.Model; import software.amazon.smithy.model.knowledge.NeighborProviderIndex; import software.amazon.smithy.model.shapes.Shape; @@ -27,43 +29,31 @@ final class Context { NeighborProviderIndex neighborIndex; - private final Map> variables; + private final Model model; + private final Map> variables = new HashMap<>(); + private final List> roots; - Context(NeighborProviderIndex neighborIndex) { + Context(Model model, NeighborProviderIndex neighborIndex, List> roots) { + this.model = model; this.neighborIndex = neighborIndex; - this.variables = new HashMap<>(); + this.roots = roots; } /** - * Clears the variables stored in the context. + * Gets the mutable map of captured variables. * - * @return Returns the current context. - */ - Context clearVars() { - variables.clear(); - return this; - } - - /** - * Gets the currently set variables. - * - *

Note that this is a mutable array and needs to be copied to - * get a persistent snapshot of the variables. - * - * @return Returns the currently set variables. + * @return Returns the captured variables. */ Map> getVars() { return variables; } - /** - * Puts a variable into the context using a variable name. - * - * @param variable Variable to set. - * @param shapes Shapes to associate with the variable. - */ - void putVar(String variable, Set shapes) { - variables.put(variable, shapes); + Set getRootResult(int index) { + return roots.get(index); + } + + Model getModel() { + return model; } /** @@ -73,10 +63,10 @@ private static final class Holder implements InternalSelector.Receiver { boolean set; @Override - public boolean apply(Context context, Shape shape) { + public InternalSelector.Response apply(Context context, Shape shape) { set = true; // Stop receiving shapes once the first value is seen. - return false; + return InternalSelector.Response.STOP; } } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/selector/ForwardNeighborSelector.java b/smithy-model/src/main/java/software/amazon/smithy/model/selector/ForwardNeighborSelector.java index c8fa57882fa..fd67bf8223b 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/selector/ForwardNeighborSelector.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/selector/ForwardNeighborSelector.java @@ -37,7 +37,7 @@ NeighborProvider getNeighborProvider(Context context, boolean includeTraits) { } @Override - boolean emitMatchingRel(Context context, Relationship rel, Receiver next) { + Response emitMatchingRel(Context context, Relationship rel, Receiver next) { return next.apply(context, rel.getNeighborShape().get()); } } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/selector/InSelector.java b/smithy-model/src/main/java/software/amazon/smithy/model/selector/InSelector.java new file mode 100644 index 00000000000..59bcc0363e1 --- /dev/null +++ b/smithy-model/src/main/java/software/amazon/smithy/model/selector/InSelector.java @@ -0,0 +1,71 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.model.selector; + +import software.amazon.smithy.model.shapes.Shape; + +/** + * Checks if the given value is in the result of a selector. + */ +final class InSelector implements InternalSelector { + + private final InternalSelector selector; + + InSelector(InternalSelector selector) { + this.selector = selector; + } + + @Override + public Response push(Context context, Shape shape, Receiver next) { + // Some internal selectors provide optimizations for quickly checking if they contain a shape. + switch (selector.containsShapeOptimization(context, shape)) { + case YES: + return next.apply(context, shape); + case NO: + return Response.CONTINUE; + case MAYBE: + default: + // Unable to use the optimization, so emit each shape until a match is found. + FilteredHolder holder = new FilteredHolder(shape); + selector.push(context, shape, holder); + + if (holder.matched) { + return next.apply(context, shape); + } + + return Response.CONTINUE; + } + } + + private static final class FilteredHolder implements InternalSelector.Receiver { + private final Shape shapeToMatch; + private boolean matched; + + FilteredHolder(Shape shapeToMatch) { + this.shapeToMatch = shapeToMatch; + } + + @Override + public Response apply(Context context, Shape shape) { + if (shape.equals(shapeToMatch)) { + matched = true; + return Response.STOP; + } + + return Response.CONTINUE; + } + } +} diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/selector/InternalSelector.java b/smithy-model/src/main/java/software/amazon/smithy/model/selector/InternalSelector.java index a0f12b9bf01..97c26592096 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/selector/InternalSelector.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/selector/InternalSelector.java @@ -16,7 +16,6 @@ package software.amazon.smithy.model.selector; import java.util.Collection; -import java.util.function.Function; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.Shape; @@ -53,21 +52,73 @@ interface InternalSelector { * @param next Receiver to call 0 or more times. * @return Returns true to continue sending shapes to the selector. */ - boolean push(Context ctx, Shape shape, Receiver next); + Response push(Context ctx, Shape shape, Receiver next); + + /** Tells shape emitters whether to continue to send shapes to an InternalSelector or Receiver. */ + enum Response { + CONTINUE, + STOP + } + + /** + * Pushes {@code shape} through the selector and adds all results to {@code captures}. + * + *

This method exists because we've messed this up multiple times. When buffering values sent to a receiver, + * you have to return true to keep getting results. It's easy to make a closure that just uses + * {@link Collection#add(Object)}, but that will return false if the shape was already in the collection, which + * isn't the desired behavior. + * + * @param context Context being evaluated. + * @param shape Shape being pushed through the selector. + * @param captures Where to buffer all results. + * @param Collection type that is given and returned. + * @return Returns the given {@code captures} collection. + */ + default > C pushResultsToCollection(Context context, Shape shape, C captures) { + push(context, shape, (c, s) -> { + captures.add(s); + return Response.CONTINUE; + }); + return captures; + } /** - * Returns a function that is used to optimize which shapes in a model - * need to be evaluated. + * Returns the set of shapes to pump through the selector. * - *

For example, when selecting "structure", it is far less work + *

This method returns all shapes in the model by default. Some selectors can return a subset of shapes if + * the selector can filter shapes more efficiently. For example, when selecting "structure", it is far less work * to leverage {@link Model#toSet(Class)} than it is to send every shape * through every selector. * - * @return Returns a function that returns null if no optimization can - * be made, or a Collection of Shapes if an optimization was made. + * @return Returns the starting shapes to push through the selector. + */ + default Collection getStartingShapes(Model model) { + return model.toSet(); + } + + /** + * The result of determining if a presence optimization can be made to find a shape. + */ + enum ContainsShape { + /** The shape is definitely in the selector. */ + YES, + + /** The shape is definitely not in the selector. */ + NO, + + /** No optimization could be made, so send every shape through to determine if the shape is present. */ + MAYBE + } + + /** + * Checks if the internal selector can quickly detect if it contains the given shape. + * + * @param context Evaluation context. + * @param shape Shape to check. + * @return Returns YES if the selector knows the shape is in the selector, NO if it isn't, and MAYBE if unknown. */ - default Function> optimize() { - return null; + default ContainsShape containsShapeOptimization(Context context, Shape shape) { + return ContainsShape.MAYBE; } /** @@ -82,6 +133,6 @@ interface Receiver { * @param shape Shape that is received. * @return Returns true to continue receiving shapes. */ - boolean apply(Context context, Shape shape); + Response apply(Context context, Shape shape); } } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/selector/IsSelector.java b/smithy-model/src/main/java/software/amazon/smithy/model/selector/IsSelector.java index 4798512f22d..4e5f5588ac0 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/selector/IsSelector.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/selector/IsSelector.java @@ -33,13 +33,13 @@ static InternalSelector of(List predicates) { } @Override - public boolean push(Context context, Shape shape, Receiver next) { + public Response push(Context context, Shape shape, Receiver next) { for (InternalSelector selector : selectors) { - if (!selector.push(context, shape, next)) { - return false; + if (selector.push(context, shape, next) == Response.STOP) { + return Response.STOP; } } - return true; + return Response.CONTINUE; } } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/selector/NotSelector.java b/smithy-model/src/main/java/software/amazon/smithy/model/selector/NotSelector.java index e7d348b0018..3c9703588f5 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/selector/NotSelector.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/selector/NotSelector.java @@ -29,11 +29,11 @@ final class NotSelector implements InternalSelector { } @Override - public boolean push(Context context, Shape shape, Receiver next) { + public Response push(Context context, Shape shape, Receiver next) { if (!context.receivedShapes(shape, selector)) { return next.apply(context, shape); } else { - return true; + return Response.CONTINUE; } } } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/selector/RecursiveNeighborSelector.java b/smithy-model/src/main/java/software/amazon/smithy/model/selector/RecursiveNeighborSelector.java index 23bc2ca391c..49b005f903a 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/selector/RecursiveNeighborSelector.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/selector/RecursiveNeighborSelector.java @@ -25,7 +25,7 @@ */ final class RecursiveNeighborSelector implements InternalSelector { @Override - public boolean push(Context context, Shape shape, Receiver next) { + public Response push(Context context, Shape shape, Receiver next) { Walker walker = new Walker(context.neighborIndex.getProvider()); Iterator shapeIterator = walker.iterateShapes(shape); @@ -33,13 +33,13 @@ public boolean push(Context context, Shape shape, Receiver next) { Shape nextShape = shapeIterator.next(); // Don't include the shape being visited. if (!nextShape.equals(shape)) { - if (!next.apply(context, nextShape)) { + if (next.apply(context, nextShape) == Response.STOP) { // Stop sending recursive neighbors when told to stop and propagate. - return false; + return Response.STOP; } } } - return true; + return Response.CONTINUE; } } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/selector/ReverseNeighborSelector.java b/smithy-model/src/main/java/software/amazon/smithy/model/selector/ReverseNeighborSelector.java index e31916a483b..0b8068d958f 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/selector/ReverseNeighborSelector.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/selector/ReverseNeighborSelector.java @@ -37,7 +37,7 @@ NeighborProvider getNeighborProvider(Context context, boolean includeTraits) { } @Override - boolean emitMatchingRel(Context context, Relationship rel, Receiver next) { + Response emitMatchingRel(Context context, Relationship rel, Receiver next) { return next.apply(context, rel.getShape()); } } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/selector/RootSelector.java b/smithy-model/src/main/java/software/amazon/smithy/model/selector/RootSelector.java new file mode 100644 index 00000000000..eac206171e3 --- /dev/null +++ b/smithy-model/src/main/java/software/amazon/smithy/model/selector/RootSelector.java @@ -0,0 +1,53 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.model.selector; + +import software.amazon.smithy.model.shapes.Shape; + +/** + * Root expressions are rooted common subexpressions. + * + *

Roots are evaluated eagerly and then the result is retrieved by ID. This prevents needing to evaluate a root + * expression over and over for each shape given the result does not vary based on the current shape. + * Roots are evaluated in an isolated context, meaning it can't use variables defined outside the root, nor can it + * set variables that can be used outside the root. + */ +final class RootSelector implements InternalSelector { + + private final InternalSelector selector; + private final int id; + + RootSelector(InternalSelector selector, int id) { + this.selector = selector; + this.id = id; + } + + @Override + public Response push(Context context, Shape shape, Receiver next) { + for (Shape v : context.getRootResult(id)) { + if (next.apply(context, v) == Response.STOP) { + return Response.STOP; + } + } + + return Response.CONTINUE; + } + + @Override + public ContainsShape containsShapeOptimization(Context context, Shape shape) { + return context.getRootResult(id).contains(shape) ? ContainsShape.YES : ContainsShape.NO; + } +} diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/selector/ScopedAttributeSelector.java b/smithy-model/src/main/java/software/amazon/smithy/model/selector/ScopedAttributeSelector.java index 88dc6867aba..cc576258607 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/selector/ScopedAttributeSelector.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/selector/ScopedAttributeSelector.java @@ -64,11 +64,11 @@ interface ScopedFactory { } @Override - public boolean push(Context context, Shape shape, Receiver next) { + public Response push(Context context, Shape shape, Receiver next) { if (matchesAssertions(shape, context.getVars())) { return next.apply(context, shape); } else { - return true; + return Response.CONTINUE; } } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/selector/SelectorParser.java b/smithy-model/src/main/java/software/amazon/smithy/model/selector/SelectorParser.java index 174b5e1ff21..564b367092e 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/selector/SelectorParser.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/selector/SelectorParser.java @@ -39,6 +39,7 @@ final class SelectorParser extends SimpleParser { private static final Logger LOGGER = Logger.getLogger(SelectorParser.class.getName()); private static final Set BREAK_TOKENS = SetUtils.of(',', ']', ')'); private static final Set REL_TYPES = new HashSet<>(); + private final List roots = new ArrayList<>(); static { // Adds selector relationship labels for warnings when unknown relationship names are used. @@ -52,7 +53,9 @@ private SelectorParser(String selector) { } static Selector parse(String selector) { - return new WrappedSelector(selector, new SelectorParser(selector).parse()); + SelectorParser parser = new SelectorParser(selector); + List result = parser.parse(); + return new WrappedSelector(selector, result, parser.roots); } List parse() { @@ -227,6 +230,22 @@ private InternalSelector parseSelectorFunction() { return new TestSelector(selectors); case "is": return IsSelector.of(selectors); + case "in": + if (selectors.size() != 1) { + throw new SelectorSyntaxException( + "The :in function requires a single selector argument", + expression(), functionPosition, line(), column()); + } + return new InSelector(selectors.get(0)); + case "root": + if (selectors.size() != 1) { + throw new SelectorSyntaxException( + "The :root function requires a single selector argument", + expression(), functionPosition, line(), column()); + } + InternalSelector root = new RootSelector(selectors.get(0), roots.size()); + roots.add(selectors.get(0)); + return root; case "topdown": if (selectors.size() > 2) { throw new SelectorSyntaxException( @@ -240,7 +259,7 @@ private InternalSelector parseSelectorFunction() { default: LOGGER.warning(String.format("Unknown function name `%s` found in selector: %s", name, expression())); - return (context, shape, next) -> true; + return (context, shape, next) -> InternalSelector.Response.CONTINUE; } } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/selector/ShapeTypeCategorySelector.java b/smithy-model/src/main/java/software/amazon/smithy/model/selector/ShapeTypeCategorySelector.java index 0b252fef60b..d3ba6ada030 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/selector/ShapeTypeCategorySelector.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/selector/ShapeTypeCategorySelector.java @@ -15,6 +15,8 @@ package software.amazon.smithy.model.selector; +import java.util.Collection; +import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.Shape; final class ShapeTypeCategorySelector implements InternalSelector { @@ -25,11 +27,21 @@ final class ShapeTypeCategorySelector implements InternalSelector { } @Override - public boolean push(Context ctx, Shape shape, Receiver next) { + public Response push(Context ctx, Shape shape, Receiver next) { if (shapeCategory.isInstance(shape)) { return next.apply(ctx, shape); } - return true; + return Response.CONTINUE; + } + + @Override + public Collection getStartingShapes(Model model) { + return model.toSet(shapeCategory); + } + + @Override + public ContainsShape containsShapeOptimization(Context context, Shape shape) { + return getStartingShapes(context.getModel()).contains(shape) ? ContainsShape.YES : ContainsShape.NO; } } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/selector/ShapeTypeSelector.java b/smithy-model/src/main/java/software/amazon/smithy/model/selector/ShapeTypeSelector.java index 7f6b34e8c96..38357cb8df6 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/selector/ShapeTypeSelector.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/selector/ShapeTypeSelector.java @@ -16,7 +16,6 @@ package software.amazon.smithy.model.selector; import java.util.Collection; -import java.util.function.Function; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.ShapeType; @@ -30,16 +29,23 @@ final class ShapeTypeSelector implements InternalSelector { } @Override - public boolean push(Context ctx, Shape shape, Receiver next) { + public Response push(Context ctx, Shape shape, Receiver next) { if (shape.getType().isShapeType(shapeType)) { return next.apply(ctx, shape); } - return true; + return Response.CONTINUE; } @Override - public Function> optimize() { - return model -> model.toSet(shapeType.getShapeClass()); + public Collection getStartingShapes(Model model) { + return model.toSet(shapeType.getShapeClass()); + } + + @Override + public ContainsShape containsShapeOptimization(Context context, Shape shape) { + return context.getModel().toSet(shapeType.getShapeClass()).contains(shape) + ? ContainsShape.YES + : ContainsShape.NO; } } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/selector/TestSelector.java b/smithy-model/src/main/java/software/amazon/smithy/model/selector/TestSelector.java index b1c850a48aa..c3286ced8de 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/selector/TestSelector.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/selector/TestSelector.java @@ -32,7 +32,7 @@ final class TestSelector implements InternalSelector { } @Override - public boolean push(Context context, Shape shape, Receiver next) { + public Response push(Context context, Shape shape, Receiver next) { for (InternalSelector predicate : selectors) { if (context.receivedShapes(shape, predicate)) { // The instant something matches, stop testing selectors. @@ -40,8 +40,7 @@ public boolean push(Context context, Shape shape, Receiver next) { } } - // Note that this does not return false when there is not a match, - // since it should to continue to receive shapes to test. - return true; + // Continue to receive shapes because other shapes could match. + return Response.CONTINUE; } } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/selector/TopDownSelector.java b/smithy-model/src/main/java/software/amazon/smithy/model/selector/TopDownSelector.java index a192540b7c8..9f31d215c4d 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/selector/TopDownSelector.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/selector/TopDownSelector.java @@ -33,12 +33,12 @@ final class TopDownSelector implements InternalSelector { } @Override - public boolean push(Context context, Shape shape, Receiver next) { + public Response push(Context context, Shape shape, Receiver next) { if (shape.isServiceShape() || shape.isResourceShape() || shape.isOperationShape()) { return pushMatch(false, context, shape, next, new HashSet<>()); } - return true; + return Response.CONTINUE; } // While a model can't contain recursive resource references, a custom @@ -46,9 +46,9 @@ public boolean push(Context context, Shape shape, Receiver next) { // recursive references. Custom validators are applied before resource // cycles are detected, meaning this function needs to protect against // recursion. - private boolean pushMatch(boolean qualified, Context context, Shape shape, Receiver next, Set visited) { + private Response pushMatch(boolean qualified, Context context, Shape shape, Receiver next, Set visited) { if (visited.contains(shape.getId())) { - return true; + return Response.CONTINUE; } visited.add(shape.getId()); @@ -64,8 +64,8 @@ private boolean pushMatch(boolean qualified, Context context, Shape shape, Recei } // If the shape is matched, then it's sent to the next receiver. - if (qualified && !next.apply(context, shape)) { - return false; // fast-fail if the receiver fast-fails. + if (qualified && next.apply(context, shape) == Response.STOP) { + return Response.STOP; // fast-fail if the receiver fast-fails. } // Recursively check each nested resource/operation. @@ -73,13 +73,13 @@ private boolean pushMatch(boolean qualified, Context context, Shape shape, Recei if (rel.getNeighborShape().isPresent() && !rel.getNeighborShapeId().equals(shape.getId())) { if (rel.getRelationshipType() == RelationshipType.RESOURCE || rel.getRelationshipType() == RelationshipType.OPERATION) { - if (!pushMatch(qualified, context, rel.getNeighborShape().get(), next, visited)) { - return false; + if (pushMatch(qualified, context, rel.getNeighborShape().get(), next, visited) == Response.STOP) { + return Response.STOP; } } } } - return true; + return Response.CONTINUE; } } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/selector/VariableGetSelector.java b/smithy-model/src/main/java/software/amazon/smithy/model/selector/VariableGetSelector.java index e8f466f7d88..02ddfbe428d 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/selector/VariableGetSelector.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/selector/VariableGetSelector.java @@ -16,6 +16,7 @@ package software.amazon.smithy.model.selector; import java.util.Collections; +import java.util.Set; import software.amazon.smithy.model.shapes.Shape; /** @@ -29,15 +30,24 @@ final class VariableGetSelector implements InternalSelector { } @Override - public boolean push(Context context, Shape shape, Receiver next) { - // Do not fail on an invalid variable access. - for (Shape v : context.getVars().getOrDefault(variableName, Collections.emptySet())) { - if (!next.apply(context, v)) { + public Response push(Context context, Shape shape, Receiver next) { + // Do not fail on invalid variable access. + for (Shape v : getShapes(context)) { + if (next.apply(context, v) == Response.STOP) { // Propagate the signal to stop upstream. - return false; + return Response.STOP; } } - return true; + return Response.CONTINUE; + } + + private Set getShapes(Context context) { + return context.getVars().getOrDefault(variableName, Collections.emptySet()); + } + + @Override + public ContainsShape containsShapeOptimization(Context context, Shape shape) { + return getShapes(context).contains(shape) ? ContainsShape.YES : ContainsShape.NO; } } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/selector/VariableStoreSelector.java b/smithy-model/src/main/java/software/amazon/smithy/model/selector/VariableStoreSelector.java index e29906e691f..3a568afaa20 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/selector/VariableStoreSelector.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/selector/VariableStoreSelector.java @@ -38,12 +38,11 @@ final class VariableStoreSelector implements InternalSelector { } @Override - public boolean push(Context context, Shape shape, Receiver next) { + public Response push(Context context, Shape shape, Receiver next) { // Buffer the result of piping the shape through the selector // so that it can be retrieved through context vars. - Set captures = new HashSet<>(); - selector.push(context, shape, (c, s) -> captures.add(s)); - context.putVar(variableName, captures); + Set captures = selector.pushResultsToCollection(context, shape, new HashSet<>()); + context.getVars().put(variableName, captures); // Now send the received shape to the next receiver. return next.apply(context, shape); diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/selector/WrappedSelector.java b/smithy-model/src/main/java/software/amazon/smithy/model/selector/WrappedSelector.java index f6579952b64..c2fc51c5f47 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/selector/WrappedSelector.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/selector/WrappedSelector.java @@ -21,7 +21,6 @@ import java.util.List; import java.util.Set; import java.util.function.Consumer; -import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; import software.amazon.smithy.model.Model; @@ -38,12 +37,12 @@ final class WrappedSelector implements Selector { private final String expression; private final InternalSelector delegate; - private final Function> optimizer; + private final List roots; - WrappedSelector(String expression, List selectors) { + WrappedSelector(String expression, List selectors, List roots) { this.expression = expression; - delegate = AndSelector.of(selectors); - optimizer = selectors.get(0).optimize(); + this.roots = roots; + this.delegate = AndSelector.of(selectors); } @Override @@ -71,7 +70,7 @@ public Set select(Model model) { // that aren't parallelized. pushShapes(model, (ctx, s) -> { result.add(s); - return true; + return InternalSelector.Response.CONTINUE; }); return result; } @@ -84,59 +83,80 @@ public void consumeMatches(Model model, Consumer shapeMatchConsumer) // pushing each shape into internal selectors. pushShapes(model, (ctx, s) -> { shapeMatchConsumer.accept(new ShapeMatch(s, ctx.getVars())); - return true; + return InternalSelector.Response.CONTINUE; }); } @Override public Stream shapes(Model model) { + NeighborProviderIndex index = NeighborProviderIndex.of(model); + List> computedRoots = computeRoots(model); return streamStartingShape(model).flatMap(shape -> { - List result = new ArrayList<>(); - delegate.push(createContext(model), shape, (ctx, s) -> { - result.add(s); - return true; - }); - return result.stream(); + Context context = new Context(model, index, computedRoots); + return delegate.pushResultsToCollection(context, shape, new ArrayList<>()).stream(); }); } @Override public Stream matches(Model model) { + NeighborProviderIndex index = NeighborProviderIndex.of(model); + List> computedRoots = computeRoots(model); return streamStartingShape(model).flatMap(shape -> { List result = new ArrayList<>(); - delegate.push(createContext(model), shape, (ctx, s) -> { + delegate.push(new Context(model, index, computedRoots), shape, (ctx, s) -> { result.add(new ShapeMatch(s, ctx.getVars())); - return true; + return InternalSelector.Response.CONTINUE; }); return result.stream(); }); } - private Context createContext(Model model) { - return new Context(NeighborProviderIndex.of(model)); + // Eagerly compute roots over all model shapes before evaluating shapes one at a time. + private List> computeRoots(Model model) { + NeighborProviderIndex index = NeighborProviderIndex.of(model); + List> rootResults = new ArrayList<>(roots.size()); + for (InternalSelector selector : roots) { + Set result = evalRoot(model, index, selector, rootResults); + rootResults.add(result); + } + return rootResults; + } + + // Eagerly compute a root subexpression. + private Set evalRoot( + Model model, + NeighborProviderIndex index, + InternalSelector selector, + List> results + ) { + Collection shapesToEmit = selector.getStartingShapes(model); + Context isolatedContext = new Context(model, index, results); + Set captures = new HashSet<>(); + for (Shape rootShape : shapesToEmit) { + isolatedContext.getVars().clear(); + selector.push(isolatedContext, rootShape, (c, s) -> { + captures.add(s); + return InternalSelector.Response.CONTINUE; + }); + } + + return captures; } private void pushShapes(Model model, InternalSelector.Receiver acceptor) { - Context context = createContext(model); - Collection shapes = optimizer == null - ? model.toSet() - : optimizer.apply(model); + Context context = new Context(model, NeighborProviderIndex.of(model), computeRoots(model)); + Collection shapes = delegate.getStartingShapes(model); for (Shape shape : shapes) { - delegate.push(context.clearVars(), shape, acceptor); + context.getVars().clear(); + delegate.push(context, shape, acceptor); } } private Stream streamStartingShape(Model model) { - Stream stream = optimizer != null - ? optimizer.apply(model).stream() - : model.shapes(); - - // Use a parallel stream for larger models. - if (isParallel(model)) { - stream = stream.parallel(); - } - - return stream; + Collection startingShapes = delegate.getStartingShapes(model); + return startingShapes.size() > PARALLEL_THRESHOLD + ? startingShapes.parallelStream() + : startingShapes.stream(); } private boolean isParallel(Model model) { diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/selector/SelectorTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/selector/SelectorTest.java index ecd999bdb51..130114fe7ff 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/selector/SelectorTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/selector/SelectorTest.java @@ -45,7 +45,6 @@ import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.ResourceShape; import software.amazon.smithy.model.shapes.ServiceShape; -import software.amazon.smithy.model.shapes.SetShape; import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.shapes.StringShape; @@ -1116,4 +1115,52 @@ public void supportsResourceProperties() { assertThat(shapesTargettedByCityOnly.size(), equalTo(2)); assertThat(shapesTargettedByCityOnly, containsInAnyOrder(coordinatesShape, stringShape)); } + + @Test + public void rootFunctionReturnsAllShapes() { + Selector selector = Selector.parse("string" + + ":in(:root(-[input]-> ~> *))" + + ":not(:in(:root(-[output]-> ~> *)))"); + Set result = selector.select(resourceModel); + + // This is the only string used in input but not output. + assertThat(result, contains(resourceModel.expectShape(ShapeId.from("example.weather#CityId")))); + } + + @Test + public void inefficientIfNotCached() { + Selector selector = Selector.parse(":in(:root(service ~> number))"); + Set result = selector.select(resourceModel); + + // This is the only number used in any service. + assertThat(result, contains(resourceModel.expectShape(ShapeId.from("smithy.api#Float")))); + } + + @Test + public void allowsNestedRoots() { + Selector selector = Selector.parse(":root(:root(:root(*)))"); + Set result = selector.select(resourceModel); + + assertThat(result, equalTo(resourceModel.toSet())); + } + + @Test + public void inDoesNotSupportMoreThanOneSelector() { + Assertions.assertThrows(SelectorSyntaxException.class, () -> Selector.parse(":in(*, *)")); + } + + @Test + public void inRequiresOneSelector() { + Assertions.assertThrows(SelectorSyntaxException.class, () -> Selector.parse(":in()")); + } + + @Test + public void rootDoesNotSupportMoreThanOneSelector() { + Assertions.assertThrows(SelectorSyntaxException.class, () -> Selector.parse(":root(*, *)")); + } + + @Test + public void rootRequiresOneSelector() { + Assertions.assertThrows(SelectorSyntaxException.class, () -> Selector.parse(":root()")); + } } diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/selector/ShapeTypeCategorySelectorTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/selector/ShapeTypeCategorySelectorTest.java new file mode 100644 index 00000000000..dfebd6adacf --- /dev/null +++ b/smithy-model/src/test/java/software/amazon/smithy/model/selector/ShapeTypeCategorySelectorTest.java @@ -0,0 +1,46 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.model.selector; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.in; + +import java.util.Set; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.model.Model; + +public class ShapeTypeCategorySelectorTest { + + private static Model model; + + @BeforeAll + public static void before() { + model = Model.assembler() + .addImport(SelectorTest.class.getResource("shape-type-test.smithy")) + .assemble() + .unwrap(); + } + + @Test + public void hasContainsOptimization() { + // "number" is a category. intEnum is considered a number, so it is returned. This example triggers the + // :in function optimization of the selector. + Set ids = SelectorTest.ids(model, ":in(number) [id|namespace = smithy.example]"); + + assertThat("smithy.example#IntEnum", in(ids)); + } +} diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/selector/ShapeTypeSelectorTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/selector/ShapeTypeSelectorTest.java index 01637972259..42888a161ba 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/selector/ShapeTypeSelectorTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/selector/ShapeTypeSelectorTest.java @@ -50,4 +50,11 @@ public void integerSelectsIntEnum() { assertThat("smithy.example#Integer", in(ids)); assertThat("smithy.example#IntEnum", in(ids)); } + + @Test + public void hasContainsOptimization() { + Set ids = SelectorTest.ids(model, ":in(enum) [id|namespace = smithy.example]"); + + assertThat("smithy.example#Enum", in(ids)); + } } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/selector/cases/in-function.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/selector/cases/in-function.smithy new file mode 100644 index 00000000000..38ede8fb258 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/selector/cases/in-function.smithy @@ -0,0 +1,75 @@ +$version: "2.0" + +metadata selectorTests = [ + // Find numbers that are used in input but are not used in output. + { + selector: """ + service + $output(~> operation -[output]-> ~> number) + ~> operation -[input]-> ~> number + :not(:in(${output}))""" + matches: [ + smithy.api#Integer + smithy.api#Double + ] + } + // This is not how you should write this expression, but it does test that :in can be used with variables. + // This should instead be written as: + // operation ~> number + { + selector: """ + $usedNumbers(operation ~> number) + operation ~> * + :in(${usedNumbers})""" + matches: [ + smithy.api#Integer + smithy.api#Float + smithy.api#Double + smithy.api#Short + smithy.api#Long + smithy.api#Byte + ] + } + // This is also not how you'd write this, but it is valid. + // This should be written more directly as: + // member [id|namespace = smithy.example] > number + { + selector: ":in(number :test(< member [id|namespace = smithy.example]))" + matches: [ + smithy.api#Integer + smithy.api#Float + smithy.api#Double + smithy.api#Short + smithy.api#Long + smithy.api#Byte + ] + } +] + +namespace smithy.example + +service MyService { + operations: [A, B] +} + +operation A { + input:= { + a: Integer + b: Float + c: Double + } + output:= { + a: Short + b: Long + c: Float + } +} + +operation B { + input:= { + a: Byte + } + output:= { + b: Byte + } +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/selector/cases/root-function.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/selector/cases/root-function.smithy new file mode 100644 index 00000000000..23ba70acf89 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/selector/cases/root-function.smithy @@ -0,0 +1,61 @@ +$version: "2.0" + +metadata selectorTests = [ + // Find shapes used in service operation inputs but not operation outputs. + { + selector: """ + number + :in(:root(service ~> operation -[input]-> * ~> number)) + :not(:in(:root(service ~> operation -[output]-> * ~> number)))""" + matches: [ + smithy.api#Integer + ] + } + // This is similar to the above :root example, but also returns smithy.api#Double because each capture + // of service operations is isolated to a single service and not global across all services. + // * When MyService1 is evaluated, Double is only used in input and not output. + // * The usage of Double in the output of MyService2 is not taken into account when evaluating MyService1. + { + selector: """ + service + $outputs(~> operation -[output]-> ~> number) + ~> operation -[input]-> ~> number + :not(:in(${outputs}))""" + matches: [ + smithy.api#Integer + smithy.api#Double + ] + } +] + +namespace smithy.example + +service MyService1 { + operations: [A] +} + +operation A { + input:= { + a: Integer + b: Float + c: Double + } + output:= { + d: Float + e: Short + f: Long + } +} + +service MyService2 { + operations: [B] +} + +operation B { + input:= { + a: Double + } + output:= { + a: Double + } +}