-
Notifications
You must be signed in to change notification settings - Fork 221
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Smithy Rules engine #1356
Smithy Rules engine #1356
Changes from all commits
5f52c06
b5172cf
f5160cc
0194b09
97838df
fbfcfdc
521a9e8
c8a63f7
3136498
0ef91c0
da17266
a87b3ae
c4d5585
33f6470
cc31c46
e220145
73df2b6
a0efefb
ebad2ef
7e884b5
c0a84e9
fe5a70e
635104c
68dc7be
5768c7f
cd15998
afcb970
f29c8d5
3fe3e38
560f96b
220b251
1cff45a
91a14a3
4d6d778
65e8e9b
0f2b91c
1910262
5180a03
ff7a596
034bd7b
957379a
d58ac06
238a1b8
4f791a4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
/* | ||
* Copyright 2022 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.rulesengine; | ||
|
||
import software.amazon.smithy.utils.SmithyUnstableApi; | ||
|
||
/** | ||
* An interface that describe a type that can be transformed to type T. | ||
* @param <T> the type. | ||
*/ | ||
@SmithyUnstableApi | ||
public interface Into<T> { | ||
T into(); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
/* | ||
* Copyright 2022 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.rulesengine; | ||
|
||
import software.amazon.smithy.utils.SmithyUnstableApi; | ||
|
||
/** | ||
* An interface that describe a type that can convert itself into itself. | ||
* @param <T> the type. | ||
*/ | ||
@SmithyUnstableApi | ||
public interface IntoSelf<T extends IntoSelf<T>> extends Into<T> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is only used once, on |
||
@Override | ||
default T into() { | ||
return (T) this; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,247 @@ | ||
/* | ||
* Copyright 2022 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.rulesengine.analysis; | ||
|
||
import java.util.ArrayList; | ||
import java.util.Collections; | ||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.NoSuchElementException; | ||
import java.util.Objects; | ||
import java.util.stream.Collectors; | ||
import java.util.stream.Stream; | ||
import software.amazon.smithy.rulesengine.language.EndpointRuleSet; | ||
import software.amazon.smithy.rulesengine.language.eval.RuleEvaluator; | ||
import software.amazon.smithy.rulesengine.language.eval.Value; | ||
import software.amazon.smithy.rulesengine.language.syntax.Identifier; | ||
import software.amazon.smithy.rulesengine.language.syntax.rule.Condition; | ||
import software.amazon.smithy.rulesengine.language.syntax.rule.Rule; | ||
import software.amazon.smithy.rulesengine.language.util.PathFinder; | ||
import software.amazon.smithy.rulesengine.language.util.StringUtils; | ||
import software.amazon.smithy.rulesengine.language.visit.TraversingVisitor; | ||
import software.amazon.smithy.rulesengine.traits.EndpointTestCase; | ||
import software.amazon.smithy.utils.SmithyUnstableApi; | ||
|
||
/** | ||
* Analyzer for determining coverage of a rule-set. | ||
*/ | ||
@SmithyUnstableApi | ||
public final class CoverageChecker { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How is this class supposed to be used? Is it for rule-set writers? Should it be integrated into a validation? |
||
private final CoverageCheckerCore checkerCore; | ||
private final EndpointRuleSet ruleSet; | ||
|
||
public CoverageChecker(EndpointRuleSet ruleSet) { | ||
this.ruleSet = ruleSet; | ||
this.checkerCore = new CoverageCheckerCore(); | ||
} | ||
|
||
/** | ||
* Evaluates the rule-set with the given inputs to determine rule coverage. | ||
* | ||
* @param input the map parameters and inputs to test coverage. | ||
*/ | ||
public void evaluateInput(Map<Identifier, Value> input) { | ||
this.checkerCore.evaluateRuleSet(ruleSet, input); | ||
} | ||
|
||
/** | ||
* Evaluate the rule-set using the given test case to determine rule coverage. | ||
* | ||
* @param testCase the test case to evaluate. | ||
*/ | ||
public void evaluateTestCase(EndpointTestCase testCase) { | ||
HashMap<Identifier, Value> map = new HashMap<>(); | ||
kstich marked this conversation as resolved.
Show resolved
Hide resolved
|
||
testCase.getParams().getStringMap().forEach((s, node) -> map.put(Identifier.of(s), Value.fromNode(node))); | ||
this.checkerCore.evaluateRuleSet(ruleSet, map); | ||
} | ||
|
||
/** | ||
* Analyze and provides the coverage results for the rule-set. | ||
* | ||
* @return stream of {@link CoverageResult}. | ||
*/ | ||
public Stream<CoverageResult> checkCoverage() { | ||
Stream<Condition> conditions = new CollectConditions().visitRuleset(ruleSet); | ||
return coverageForConditions(conditions); | ||
} | ||
|
||
/** | ||
* Analyze and provides the coverage results for a specific rule. | ||
* | ||
* @return stream of {@link CoverageResult}. | ||
*/ | ||
public Stream<CoverageResult> checkCoverageFromRule(Rule rule) { | ||
kstich marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Stream<Condition> conditions = rule.accept(new CollectConditions()); | ||
return coverageForConditions(conditions); | ||
|
||
kstich marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
private Stream<CoverageResult> coverageForConditions(Stream<Condition> conditions) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This method could do with some documentation internally. I'm not sure why it's doing what it's doing around branch results. Are |
||
return conditions.distinct().flatMap(condition -> { | ||
Wrapper<Condition> w = new Wrapper<>(condition); | ||
ArrayList<BranchResult> conditionResults = checkerCore.conditionResults.getOrDefault(w, new ArrayList<>()); | ||
kstich marked this conversation as resolved.
Show resolved
Hide resolved
|
||
List<Boolean> branches = conditionResults.stream() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this need to be boxed or can it be a |
||
.map(c -> c.result) | ||
.distinct() | ||
.collect(Collectors.toList()); | ||
if (branches.size() == 1) { | ||
return Stream.of(new CoverageResult(condition, !branches.get(0), conditionResults.stream() | ||
.map(c -> c.context.input) | ||
.collect(Collectors.toList()))); | ||
} else if (branches.size() == 0) { | ||
return Stream.of(new CoverageResult(condition, false, Collections.emptyList()), | ||
new CoverageResult(condition, true, Collections.emptyList())); | ||
} else { | ||
return Stream.empty(); | ||
} | ||
}); | ||
} | ||
|
||
private static class Wrapper<T> { | ||
kstich marked this conversation as resolved.
Show resolved
Hide resolved
|
||
private final T inner; | ||
|
||
Wrapper(T inner) { | ||
this.inner = inner; | ||
} | ||
|
||
@Override | ||
public int hashCode() { | ||
return Objects.hash(inner); | ||
} | ||
|
||
@Override | ||
public boolean equals(Object o) { | ||
if (this == o) { | ||
return true; | ||
} | ||
if (o == null || getClass() != o.getClass()) { | ||
return false; | ||
} | ||
Wrapper<?> wrapper = (Wrapper<?>) o; | ||
return inner.equals(wrapper.inner); | ||
} | ||
} | ||
|
||
public static class BranchResult { | ||
kstich marked this conversation as resolved.
Show resolved
Hide resolved
|
||
private final boolean result; | ||
private final CoverageCheckerCore.Context context; | ||
|
||
public BranchResult(boolean result, CoverageCheckerCore.Context context) { | ||
this.result = result; | ||
this.context = context; | ||
} | ||
} | ||
|
||
static class CoverageCheckerCore extends RuleEvaluator { | ||
HashMap<Wrapper<Condition>, ArrayList<BranchResult>> conditionResults = new HashMap<>(); | ||
kstich marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Context context = null; | ||
|
||
@Override | ||
public Value evaluateRuleSet(EndpointRuleSet ruleset, Map<Identifier, Value> parameterArguments) { | ||
try { | ||
context = new Context(parameterArguments); | ||
return super.evaluateRuleSet(ruleset, parameterArguments); | ||
} finally { | ||
context = null; | ||
} | ||
} | ||
|
||
@Override | ||
public Value evaluateCondition(Condition condition) { | ||
assert context != null; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We usually avoid |
||
Value result = super.evaluateCondition(condition); | ||
Wrapper<Condition> cond = new Wrapper<>(condition); | ||
ArrayList<BranchResult> list = conditionResults.getOrDefault(cond, new ArrayList<>()); | ||
kstich marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (result.isNone() || result.equals(Value.bool(false))) { | ||
list.add(new BranchResult(false, context)); | ||
} else { | ||
list.add(new BranchResult(true, context)); | ||
} | ||
conditionResults.put(cond, list); | ||
return result; | ||
} | ||
|
||
static class Context { | ||
private final Map<Identifier, Value> input; | ||
|
||
Context(Map<Identifier, Value> input) { | ||
this.input = input; | ||
} | ||
} | ||
} | ||
|
||
static class CollectConditions extends TraversingVisitor<Condition> { | ||
@Override | ||
public Stream<Condition> visitConditions(List<Condition> conditions) { | ||
return conditions.stream(); | ||
} | ||
} | ||
|
||
public static class CoverageResult { | ||
kstich marked this conversation as resolved.
Show resolved
Hide resolved
|
||
private final Condition condition; | ||
private final boolean result; | ||
private final List<Map<Identifier, Value>> otherUsages; | ||
|
||
public CoverageResult(Condition condition, boolean result, List<Map<Identifier, Value>> otherUsages) { | ||
this.condition = condition; | ||
this.result = result; | ||
this.otherUsages = otherUsages; | ||
} | ||
|
||
public Condition condition() { | ||
return condition; | ||
} | ||
|
||
public boolean result() { | ||
return result; | ||
} | ||
|
||
public List<Map<Identifier, Value>> otherUsages() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the purpose of this method? It's not used anywhere. |
||
return otherUsages; | ||
} | ||
|
||
public String pretty() { | ||
StringBuilder sb = new StringBuilder(); | ||
sb.append("leaf: ").append(pretty(condition)); | ||
return sb.toString(); | ||
} | ||
|
||
private String pretty(Condition condition) { | ||
return new StringBuilder() | ||
.append(condition) | ||
.append("(") | ||
.append(condition.getSourceLocation().getFilename()) | ||
.append(":") | ||
.append(condition.getSourceLocation().getLine()) | ||
.append(")") | ||
.toString(); | ||
} | ||
|
||
public String prettyWithPath(EndpointRuleSet ruleset) { | ||
kstich marked this conversation as resolved.
Show resolved
Hide resolved
|
||
PathFinder.Path path = PathFinder.findPath(ruleset, condition).orElseThrow(NoSuchElementException::new); | ||
StringBuilder sb = new StringBuilder(); | ||
sb.append(pretty()).append("\n"); | ||
for (List<Condition> cond : path.negated()) { | ||
kstich marked this conversation as resolved.
Show resolved
Hide resolved
|
||
sb.append(StringUtils.indent(String.format("!%s", cond.toString()), 2)); | ||
} | ||
for (Condition cond : path.positive()) { | ||
sb.append(StringUtils.indent(cond.toString(), 2)); | ||
} | ||
return sb.toString(); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to only resolve into
T
being aCondition
and it's only present onFunction
. Can it be removed to reduce the surface area?