-
Notifications
You must be signed in to change notification settings - Fork 215
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Closes #2228
- Loading branch information
Showing
4 changed files
with
264 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
67 changes: 67 additions & 0 deletions
67
smithy-model/src/main/java/software/amazon/smithy/model/selector/RecursiveSelector.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
/* | ||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package software.amazon.smithy.model.selector; | ||
|
||
import java.util.ArrayDeque; | ||
import java.util.Deque; | ||
import java.util.HashSet; | ||
import java.util.Set; | ||
import software.amazon.smithy.model.shapes.Shape; | ||
import software.amazon.smithy.model.shapes.ShapeId; | ||
|
||
final class RecursiveSelector implements InternalSelector { | ||
|
||
private final InternalSelector selector; | ||
|
||
RecursiveSelector(InternalSelector selector) { | ||
this.selector = selector; | ||
} | ||
|
||
@Override | ||
public Response push(Context context, Shape shape, Receiver next) { | ||
// This queue contains the shapes that have yet to have the selector applied to them. | ||
QueueReceiver queueReceiver = new QueueReceiver(next); | ||
queueReceiver.queue.add(shape); | ||
|
||
while (!queueReceiver.queue.isEmpty()) { | ||
Shape match = queueReceiver.queue.pop(); | ||
// Apply the selector to the queue, it will send results downstream immediately, and can ask to stop early. | ||
if (selector.push(context, match, queueReceiver) == Response.STOP) { | ||
return Response.STOP; | ||
} | ||
} | ||
|
||
return Response.CONTINUE; | ||
} | ||
|
||
private static final class QueueReceiver implements Receiver { | ||
|
||
final Deque<Shape> queue = new ArrayDeque<>(); | ||
private final Set<ShapeId> visited = new HashSet<>(); | ||
private final Receiver next; | ||
|
||
QueueReceiver(Receiver next) { | ||
this.next = next; | ||
} | ||
|
||
@Override | ||
public Response apply(Context context, Shape matchedShapeFromSelector) { | ||
// This method receives each shape matched by the selector of RecursiveSelector. | ||
if (visited.add(matchedShapeFromSelector.getId())) { | ||
// Send the match downstream right away to do as little work as possible. | ||
// For example, in `:recursive(-[mixin]->) :test([id=foo#Bar])`, when a match is found, the recursive | ||
// function can stop finding more mixins. | ||
if (next.apply(context, matchedShapeFromSelector) == Response.STOP) { | ||
return Response.STOP; | ||
} | ||
// Enqueue the shape so that the outer loop can send this match back into the selector. | ||
queue.add(matchedShapeFromSelector); | ||
} | ||
|
||
return Response.CONTINUE; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
160 changes: 160 additions & 0 deletions
160
.../src/test/resources/software/amazon/smithy/model/selector/cases/recursive-function.smithy
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
$version: "2.0" | ||
|
||
metadata selectorTests = [ | ||
{ | ||
// Get the resource hierarchy of a shape. | ||
selector: "[id=smithy.example#C] :recursive(<-[resource]-)" | ||
matches: [ | ||
smithy.example#A | ||
smithy.example#B | ||
] | ||
} | ||
|
||
{ | ||
// Check if a shape is in a specific heirarchy. | ||
selector: ":test(:recursive(<-[resource]-) [id=smithy.example#A])" | ||
matches: [ | ||
smithy.example#B | ||
smithy.example#C | ||
] | ||
} | ||
|
||
{ | ||
// Get all mixins of a shape. | ||
selector: "[id=smithy.example#Indirect2] :recursive(-[mixin]->)" | ||
matches: [ | ||
smithy.example#Indirect | ||
smithy.example#Direct | ||
smithy.example#MyMixin | ||
] | ||
} | ||
|
||
{ | ||
// Get all shapes that have a specific mixin. | ||
// This will also short-circuit the recursive function once the fist match is found by the attribute. | ||
selector: ":test(:recursive(-[mixin]->) [id=smithy.example#MyMixin])" | ||
matches: [ | ||
smithy.example#Direct | ||
smithy.example#Indirect | ||
smithy.example#Indirect2 | ||
] | ||
} | ||
{ | ||
// This is the same as the previous selector, but uses and unnecessary :test in the :test. | ||
// This will also short-circuit the recursive function once the fist match is found by the test. | ||
selector: ":test(:recursive(-[mixin]->) :test([id=smithy.example#MyMixin]))" | ||
matches: [ | ||
smithy.example#Direct | ||
smithy.example#Indirect | ||
smithy.example#Indirect2 | ||
] | ||
} | ||
|
||
{ | ||
// An inefficient way to check if a mixin is applied to any shape as a mixin. | ||
// The more efficient approach is: [trait|mixin] :test(<-[mixin]-) | ||
selector: ":recursive(-[mixin]->) [trait|mixin]" | ||
matches: [ | ||
smithy.example#MyMixin | ||
smithy.example#Direct | ||
smithy.example#Indirect | ||
] | ||
} | ||
{ | ||
// Proof of the more efficient way to check if a mixin is applied to any shape as a mixin. | ||
selector: "[trait|mixin] :test(<-[mixin]-)" | ||
matches: [ | ||
smithy.example#MyMixin | ||
smithy.example#Direct | ||
smithy.example#Indirect | ||
] | ||
} | ||
|
||
|
||
{ | ||
// A slightly less efficient form of "~>". | ||
selector: "[id=smithy.example#Direct] :recursive(>)" | ||
matches: [ | ||
smithy.example#MyMixin | ||
smithy.api#String | ||
smithy.example#Direct$foo | ||
] | ||
} | ||
{ | ||
// This is the same result, but slightly more efficient. | ||
selector: "[id=smithy.example#Direct] ~>" | ||
matches: [ | ||
smithy.example#MyMixin | ||
smithy.api#String | ||
smithy.example#Direct$foo | ||
] | ||
} | ||
|
||
{ | ||
// Find the closure of shapes that ultimately target a specific shape. | ||
selector: "[id=smithy.example#Direct] :recursive(<)" | ||
matches: [ | ||
smithy.example#Indirect | ||
smithy.example#Indirect2 | ||
] | ||
} | ||
|
||
{ | ||
// Make a pathological selector to ensure we don't inifinitely recurse. | ||
// This just matches shapes in the smithy.example namespace that are targeted by another shape. | ||
// Note: This isn't a useful selector. | ||
selector: ":recursive(:recursive(:recursive(:recursive(:recursive(~>))))) [id|namespace=smithy.example]" | ||
matches: [ | ||
smithy.example#C | ||
smithy.example#Direct | ||
smithy.example#MyMixin | ||
smithy.example#Indirect | ||
smithy.example#B | ||
smithy.example#Direct$foo | ||
smithy.example#Indirect$foo | ||
smithy.example#Indirect2$foo | ||
] | ||
} | ||
{ | ||
// Make another pathological selector to ensure we don't inifinitely recurse. | ||
// This matches shapes in the smithy.example namespace that are targeted by another shape that are targeted | ||
// by another shape. Note: This isn't a useful selector. | ||
selector: "~> :recursive(:recursive(:recursive(:recursive(:recursive(~>))))) [id|namespace=smithy.example]" | ||
matches: [ | ||
smithy.example#C | ||
smithy.example#Direct | ||
smithy.example#MyMixin | ||
smithy.example#Direct$foo | ||
smithy.example#Indirect$foo | ||
] | ||
} | ||
] | ||
|
||
namespace smithy.example | ||
|
||
resource A { | ||
resources: [B] | ||
} | ||
|
||
@internal | ||
resource B { | ||
resources: [C] | ||
} | ||
|
||
resource C {} | ||
|
||
@mixin | ||
structure UnusedMixin {} | ||
|
||
@mixin | ||
structure MyMixin {} | ||
|
||
@mixin | ||
structure Direct with [MyMixin] { | ||
foo: String | ||
} | ||
|
||
@mixin | ||
structure Indirect with [Direct] {} | ||
|
||
structure Indirect2 with [Indirect] {} |