Skip to content

Commit

Permalink
Add :topdown selector to match hierarchically
Browse files Browse the repository at this point in the history
The topdown selector is used to match shapes hierarchically based on a
match selector and optional disqualifier selector. This can be used to
match shapes that are marked as controlPlane or dataPlane using
inheritance from resource/service bindings, match shapes that use
httpBasic auth, etc.
  • Loading branch information
mtdowling committed Aug 23, 2020
1 parent 7d79b09 commit 0808e7b
Show file tree
Hide file tree
Showing 6 changed files with 286 additions and 0 deletions.
44 changes: 44 additions & 0 deletions docs/source/1.0/spec/core/selectors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1359,6 +1359,50 @@ trait applied to it:
service :not(-[trait]-> [trait|protocolDefinition])
``:topdown``
------------

The ``:topdown`` function performs a directed traversal of the binding
hierarchy of services, resources, and operations to find shapes that match a
predicate selector or that inherit the match from a binding. Exactly one or
two selectors can be provided to the ``:topdown`` selector:

1. The first selector is used to mark a shape as a match. If the selector
yields any results, then it is considered a match.
2. If provided, the second selector is used to remove the match of the shape
for the current shape before traversing the resource and operation
bindings. If this selector yields any results, then the shape is not
considered a match.

Any shape that is a match is yielded by the selector.

The following selector finds all shapes that are marked with the
``aws.api#dataPlane`` trait or shapes that are bound within the containment
hierarchy of resource and service shapes that are marked as such:

.. code-block:: none
:topdown([trait|aws.api#dataPlane])
The following selector finds all shapes that are marked with the
``aws.api#dataPlane`` trait, but does not match shapes where the
``aws.api#controlPlane`` trait is used to override the ``aws.api#dataPlane``
trait:

.. code-block:: none
:topdown([trait|aws.api#dataPlane], [trait|aws.api#controlPlane])
The following selector matches shapes that utilize HTTP basic auth
by looking for the :ref:`httpBasicAuth-trait` in the :ref:`auth-trait`
applied to service shapes and operation shapes:

.. code-block:: none
:topdown([trait|auth|(values)='smithy.api#httpBasicAuth'],
[trait|auth]:not([trait|auth|(values)='smithy.api#httpBasicAuth']))
.. _selector-variables:

Variables
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,13 @@ private InternalSelector parseSelectorFunction() {
return new TestSelector(selectors);
case "is":
return IsSelector.of(selectors);
case "topdown":
if (selectors.size() > 2) {
throw new SelectorSyntaxException(
"The :topdown function accepts 1 or 2 selectors, but found " + selectors.size(),
expression(), functionPosition, line(), column());
}
return new TopDownSelector(selectors);
case "each":
LOGGER.warning("The `:each` selector function has been renamed to `:is`: " + expression());
return IsSelector.of(selectors);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright 2020 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 java.util.List;
import software.amazon.smithy.model.neighbor.Relationship;
import software.amazon.smithy.model.neighbor.RelationshipType;
import software.amazon.smithy.model.shapes.Shape;

final class TopDownSelector implements InternalSelector {
private final InternalSelector qualifier;
private final InternalSelector disqualifier;

TopDownSelector(List<InternalSelector> selectors) {
this.qualifier = selectors.get(0);
disqualifier = selectors.size() > 1 ? selectors.get(1) : null;
}

@Override
public boolean push(Context context, Shape shape, Receiver next) {
if (shape.isServiceShape() || shape.isResourceShape() || shape.isOperationShape()) {
return pushMatch(false, context, shape, next);
}

return true;
}

private boolean pushMatch(boolean qualified, Context context, Shape shape, Receiver next) {
// If the flag isn't set, then check if this shape sets it to true.
if (!qualified && context.receivedShapes(shape, qualifier)) {
qualified = true;
}

// If the flag is set, then check if any predicates unset it.
if (qualified && disqualifier != null && context.receivedShapes(shape, disqualifier)) {
qualified = false;
}

// 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.
}

// Recursively. check each nested resource/operation.
for (Relationship rel : context.neighborIndex.getProvider().getNeighbors(shape)) {
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)) {
return false;
}
}
}
}

return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package software.amazon.smithy.model.selector;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasKey;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.shapes.ShapeId;

public class TopDownSelectorTest {

private static Model model1;
private static Model model2;

@BeforeAll
public static void before() {
model1 = Model.assembler().addImport(SelectorTest.class.getResource("topdown-auth.smithy"))
.assemble()
.unwrap();

model2 = Model.assembler().addImport(SelectorTest.class.getResource("topdown-exclusive-traits.smithy"))
.assemble()
.unwrap();
}

@Test
public void requiresAtLeastOneSelector() {
Assertions.assertThrows(SelectorSyntaxException.class, () -> Selector.parse(":topdown()"));
}

@Test
public void doesNotAllowMoreThanTwoSelectors() {
Assertions.assertThrows(SelectorSyntaxException.class, () -> Selector.parse(":topdown(*, *, *)"));
}

@Test
public void findsByAuthScheme() {
Set<String> basic = SelectorTest.ids(
model1, ":topdown([trait|auth|(values)='smithy.api#httpBasicAuth'],\n"
+ " [trait|auth]:not([trait|auth|(values)='smithy.api#httpBasicAuth']))");
Set<String> digest = SelectorTest.ids(
model1, ":topdown([trait|auth|(values)='smithy.api#httpDigestAuth'],\n"
+ " [trait|auth]:not([trait|auth|(values)='smithy.api#httpDigestAuth']))");

assertThat(basic, containsInAnyOrder("smithy.example#RA", "smithy.example#ServiceWithAuthTrait",
"smithy.example#OperationWithNoAuthTrait"));
assertThat(digest, containsInAnyOrder("smithy.example#ServiceWithAuthTrait",
"smithy.example#OperationWithNoAuthTrait",
"smithy.example#RA", "smithy.example#OperationWithAuthTrait"));
}

@Test
public void findsExclusiveTraits() {
Set<String> a = SelectorTest.ids(model2, ":topdown([trait|smithy.example#a], [trait|smithy.example#b])");
Set<String> b = SelectorTest.ids(model2, ":topdown([trait|smithy.example#b], [trait|smithy.example#a])");

assertThat(a, containsInAnyOrder("smithy.example#Service1", "smithy.example#R1", "smithy.example#O2"));
assertThat(b, containsInAnyOrder("smithy.example#R2", "smithy.example#O1", "smithy.example#O3",
"smithy.example#O4"));
}

@Test
public void topDownWithNoDisqualifiers() {
Set<String> a = SelectorTest.ids(model2, ":topdown([trait|smithy.example#a])");

assertThat(a, containsInAnyOrder("smithy.example#Service1", "smithy.example#R1",
"smithy.example#O1", "smithy.example#O2", "smithy.example#R2",
"smithy.example#O3", "smithy.example#O4"));
}

@Test
public void topDownWithNoDisqualifiersWithServiceVariableFollowedByFilter() {
Map<ShapeId, ShapeId> matches = new HashMap<>();
Selector.parse("service $service(*) :topdown([trait|smithy.example#a]) resource")
.runner()
.model(model2)
.selectMatches((s, vars) -> matches.put(s.getId(), vars.get("service").iterator().next().getId()));

assertThat(matches, hasKey(ShapeId.from("smithy.example#R1")));
assertThat(matches.get(ShapeId.from("smithy.example#R1")), equalTo(ShapeId.from("smithy.example#Service1")));
assertThat(matches, hasKey(ShapeId.from("smithy.example#R2")));
assertThat(matches.get(ShapeId.from("smithy.example#R2")), equalTo(ShapeId.from("smithy.example#Service1")));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
namespace smithy.example

@httpBasicAuth
@httpDigestAuth
@httpBearerAuth
service ServiceWithNoAuthTrait {
version: "2020-01-29",
operations: [
OperationWithNoAuthTrait,
OperationWithAuthTrait
]
}

@httpBasicAuth
@httpDigestAuth
@httpBearerAuth
@auth([httpBasicAuth, httpDigestAuth])
service ServiceWithAuthTrait {
version: "2020-01-29",
operations: [
OperationWithNoAuthTrait,
OperationWithAuthTrait
],
resources: [
RA
]
}

operation OperationWithNoAuthTrait {}

resource RA {
operations: [OperationWithNoAuthTrait2]
}

@auth([])
operation OperationWithNoAuthTrait2 {}

@auth([httpDigestAuth])
operation OperationWithAuthTrait {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
namespace smithy.example

@trait
structure a {}

@trait
structure b {}

@a
service Service1 {
version: "2020-08-22",
operations: [O1, O2],
resources: [R1]
}

@b
operation O1 {}

operation O2 {}

resource R1 {
resources: [R2],
operations: [O3]
}

@b
operation O3 {}

@b
resource R2 {
operations: [O4]
}

operation O4 {}

0 comments on commit 0808e7b

Please sign in to comment.