Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions smithy-rules-engine/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -15,4 +15,6 @@ dependencies {
api(project(":smithy-model"))
api(project(":smithy-utils"))
api(project(":smithy-jmespath"))

testImplementation(project(":smithy-aws-endpoints"))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.rulesengine.analysis;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import software.amazon.smithy.rulesengine.language.EndpointRuleSet;
import software.amazon.smithy.rulesengine.language.syntax.bdd.RulesBdd;
import software.amazon.smithy.rulesengine.language.syntax.bdd.RulesBddCondition;
import software.amazon.smithy.rulesengine.language.syntax.rule.Condition;
import software.amazon.smithy.rulesengine.language.syntax.rule.EndpointRule;
import software.amazon.smithy.rulesengine.language.syntax.rule.ErrorRule;
import software.amazon.smithy.rulesengine.language.syntax.rule.Rule;
import software.amazon.smithy.rulesengine.language.syntax.rule.TreeRule;

/**
* Converts a {@link EndpointRuleSet} into a list of unique paths, a tree of conditions and leaves, and a BDD.
*/
public final class HashConsGraph {

// Endpoint ruleset to optimize.
private final EndpointRuleSet ruleSet;

// Provides a hash of endpoints/errors to their index.
private final Map<Rule, Integer> resultHashCons = new HashMap<>();

// Provides a hash of conditions to their index.
private final Map<Condition, Integer> conditionHashCons = new HashMap<>();

// Provides a mapping of originally defined conditions to their canonicalized conditions.
// (e.g., moving variables before literals in commutative functions).
private final Map<Condition, Condition> canonicalizedConditions = new HashMap<>();

// A flattened list of unique leaves.
private final List<Rule> results = new ArrayList<>();

// A flattened list of unique conditions
private final List<RulesBddCondition> conditions = new ArrayList<>();

// A flattened set of unique condition paths to leaves, sorted based on desired complexity order.
private final Set<BddPath> paths = new LinkedHashSet<>();

public HashConsGraph(EndpointRuleSet ruleSet) {
this.ruleSet = ruleSet;
hashConsConditions();

// Now build up paths and refer to the hash-consed conditions.
for (Rule rule : ruleSet.getRules()) {
crawlRules(rule, new LinkedHashSet<>());
}
}

// First create a global ordering of conditions. The ordering of conditions is the primary way to influence
// the resulting node tables of a BDD.
// 1. Simplest conditions come first (e.g., isset, booleanEquals, etc.). We build this up by gathering all
// the stateless conditions and sorting them by complexity order so that simplest checks happen earlier.
// 2. Stateful conditions come after, and they must appear in a dependency ordering (i.e., if a condition
// depends on a previous condition to bind a variable, then it must come after its dependency). This is
// done by iterating over paths and add stateful conditions, in path order, to a LinkedHashSet of
// conditions, giving us a hash-consed but ordered set of all stateful conditions across all paths.
private void hashConsConditions() {
Set<RulesBddCondition> statelessCondition = new LinkedHashSet<>();
Set<RulesBddCondition> statefulConditions = new LinkedHashSet<>();
for (Rule rule : ruleSet.getRules()) {
crawlConditions(rule, statelessCondition, statefulConditions);
}

// Sort the stateless conditions by complexity order, maintaining insertion order when equal.
List<RulesBddCondition> sortedStatelessConditions = new ArrayList<>(statelessCondition);
sortedStatelessConditions.sort(Comparator.comparingInt(RulesBddCondition::getComplexity));

// Now build up the hash-consed map of conditions to their integer position in a sorted array of RuleCondition.
hashConsCollectedConditions(sortedStatelessConditions);
hashConsCollectedConditions(statefulConditions);
}

private void hashConsCollectedConditions(Collection<RulesBddCondition> ruleConditions) {
for (RulesBddCondition ruleCondition : ruleConditions) {
conditionHashCons.put(ruleCondition.getCondition(), conditions.size());
conditions.add(ruleCondition);
}
}

public List<BddPath> getPaths() {
return new ArrayList<>(paths);
}

public List<RulesBddCondition> getConditions() {
return new ArrayList<>(conditions);
}

public List<Rule> getResults() {
return new ArrayList<>(results);
}

public EndpointRuleSet getRuleSet() {
return ruleSet;
}

public RulesBdd getBdd() {
return RulesBdd.from(this);
}

// Crawl rules to build up the global total ordering of variables.
private void crawlConditions(
Rule rule,
Set<RulesBddCondition> statelessConditions,
Set<RulesBddCondition> statefulConditions
) {
for (Condition condition : rule.getConditions()) {
if (!canonicalizedConditions.containsKey(condition)) {
// Create the RuleCondition and also canonicalize the underlying condition.
RulesBddCondition ruleCondition = RulesBddCondition.from(condition, ruleSet);
// Add a mapping between the original condition and the canonicalized condition.
canonicalizedConditions.put(condition, ruleCondition.getCondition());
if (ruleCondition.isStateful()) {
statefulConditions.add(ruleCondition);
} else {
statelessConditions.add(ruleCondition);
}
}
}

if (rule instanceof TreeRule) {
TreeRule treeRule = (TreeRule) rule;
for (Rule subRule : treeRule.getRules()) {
crawlConditions(subRule, statelessConditions, statefulConditions);
}
}
}

private void crawlRules(Rule rule, Set<Integer> conditionIndices) {
for (Condition condition : rule.getConditions()) {
Condition c = Objects.requireNonNull(canonicalizedConditions.get(condition), "Condition not found");
Integer idx = Objects.requireNonNull(conditionHashCons.get(c), "Condition not hashed");
conditionIndices.add(idx);
}

Rule leaf = null;
if (rule instanceof TreeRule) {
TreeRule treeRule = (TreeRule) rule;
for (Rule subRule : treeRule.getRules()) {
crawlRules(subRule, new LinkedHashSet<>(conditionIndices));
}
} else if (!rule.getConditions().isEmpty()) {
leaf = createStandaloneResult(rule);
} else {
leaf = rule;
}

if (leaf != null) {
int position = resultHashCons.computeIfAbsent(leaf, l -> {
results.add(l);
return results.size() - 1;
});
paths.add(createPath(position, conditionIndices));
}
}

// Create a rule that strips off conditions and is just left with docs + the error or endpoint.
private static Rule createStandaloneResult(Rule rule) {
if (rule instanceof ErrorRule) {
ErrorRule e = (ErrorRule) rule;
return new ErrorRule(
ErrorRule.builder().description(e.getDocumentation().orElse(null)),
e.getError());
} else if (rule instanceof EndpointRule) {
EndpointRule e = (EndpointRule) rule;
return new EndpointRule(
EndpointRule.builder().description(e.getDocumentation().orElse(null)),
e.getEndpoint());
} else {
throw new UnsupportedOperationException("Unsupported result node: " + rule);
}
}

private BddPath createPath(int leafIdx, Set<Integer> conditionIndices) {
Set<Integer> statefulConditions = new LinkedHashSet<>();
Set<Integer> statelessConditions = new TreeSet<>((a, b) -> {
int conditionComparison = ruleComparator(conditions.get(a), conditions.get(b));
// fall back to index comparison to ensure uniqueness
return conditionComparison != 0 ? conditionComparison : Integer.compare(a, b);
});

for (Integer conditionIdx : conditionIndices) {
RulesBddCondition node = conditions.get(conditionIdx);
if (!node.isStateful()) {
statelessConditions.add(conditionIdx);
} else {
statefulConditions.add(conditionIdx);
}
}

return new BddPath(leafIdx, statelessConditions, statefulConditions);
}

private int ruleComparator(RulesBddCondition a, RulesBddCondition b) {
return Integer.compare(a.getComplexity(), b.getComplexity());
}

/**
* Represents a path through rule conditions to reach a specific result.
*
* <p>Contains both stateless conditions (sorted by complexity) and stateful conditions (ordered by dependency)
* that must be evaluated to reach the target leaf (endpoint or error).
*/
public static final class BddPath {

// The endpoint or error index.
private final int leafIndex;

// Conditions that create or use stateful bound variables and must be maintained in order.
private final Set<Integer> statefulConditions;

// Sort conditions based on complexity scores.
private final Set<Integer> statelessConditions;

private int hash;

BddPath(int leafIndex, Set<Integer> statelessConditions, Set<Integer> statefulConditions) {
this.leafIndex = leafIndex;
this.statelessConditions = Collections.unmodifiableSet(statelessConditions);
this.statefulConditions = Collections.unmodifiableSet(statefulConditions);
}

public Set<Integer> getStatefulConditions() {
return statefulConditions;
}

public Set<Integer> getStatelessConditions() {
return statelessConditions;
}

public int getLeafIndex() {
return leafIndex;
}

@Override
public boolean equals(Object object) {
if (this == object) {
return true;
} else if (object == null || getClass() != object.getClass()) {
return false;
}
BddPath path = (BddPath) object;
return leafIndex == path.leafIndex
&& statefulConditions.equals(path.statefulConditions)
&& statelessConditions.equals(path.statelessConditions);
}

@Override
public int hashCode() {
int result = hash;
if (result == 0) {
result = Objects.hash(leafIndex, statefulConditions, statelessConditions);
hash = result;
}
return result;
}

@Override
public String toString() {
return "Path{statelessConditions=" + statelessConditions + ", statefulConditions=" + statefulConditions
+ ", leafIndex=" + leafIndex + '}';
}
}
}
Original file line number Diff line number Diff line change
@@ -21,10 +21,12 @@
import software.amazon.smithy.model.node.ObjectNode;
import software.amazon.smithy.model.node.StringNode;
import software.amazon.smithy.model.node.ToNode;
import software.amazon.smithy.rulesengine.analysis.HashConsGraph;
import software.amazon.smithy.rulesengine.language.error.RuleError;
import software.amazon.smithy.rulesengine.language.evaluation.Scope;
import software.amazon.smithy.rulesengine.language.evaluation.TypeCheck;
import software.amazon.smithy.rulesengine.language.evaluation.type.Type;
import software.amazon.smithy.rulesengine.language.syntax.bdd.RulesBdd;
import software.amazon.smithy.rulesengine.language.syntax.expressions.functions.FunctionNode;
import software.amazon.smithy.rulesengine.language.syntax.expressions.functions.LibraryFunction;
import software.amazon.smithy.rulesengine.language.syntax.parameters.Parameters;
@@ -48,6 +50,7 @@ public final class EndpointRuleSet implements FromSourceLocation, ToNode, ToSmit
private static final String VERSION = "version";
private static final String PARAMETERS = "parameters";
private static final String RULES = "rules";
private static final String BDD = "bdd";

private static final class LazyEndpointComponentFactoryHolder {
static final EndpointComponentFactory INSTANCE = EndpointComponentFactory.createServiceFactory(
@@ -58,13 +61,15 @@ private static final class LazyEndpointComponentFactoryHolder {
private final List<Rule> rules;
private final SourceLocation sourceLocation;
private final String version;
private final RulesBdd bdd;

private EndpointRuleSet(Builder builder) {
super();
parameters = SmithyBuilder.requiredState(PARAMETERS, builder.parameters);
rules = builder.rules.copy();
sourceLocation = SmithyBuilder.requiredState("source", builder.getSourceLocation());
version = SmithyBuilder.requiredState(VERSION, builder.version);
bdd = builder.bdd;
}

/**
@@ -90,14 +95,38 @@ public static EndpointRuleSet fromNode(Node node) throws RuleError {
builder.parameters(Parameters.fromNode(objectNode.expectObjectMember(PARAMETERS)));
objectNode.expectStringMember(VERSION, builder::version);

for (Node element : objectNode.expectArrayMember(RULES).getElements()) {
builder.addRule(context("while parsing rule", element, () -> EndpointRule.fromNode(element)));
}
objectNode.getArrayMember(RULES).ifPresent(rules -> {
for (Node element : rules) {
builder.addRule(context("while parsing rule", element, () -> EndpointRule.fromNode(element)));
}
});

objectNode.getObjectMember(BDD).ifPresent(o -> {
builder.bdd(RulesBdd.fromNode(o));
});

return builder.build();
});
}

/**
* Convert the endpoint ruleset to BDD form if it isn't already.
*
* @return the current ruleset if in BDD form, otherwise a new ruleset instance in BDD form.
*/
public EndpointRuleSet toBddForm() {
if (bdd != null) {
return this;
}

return builder()
.version(version)
.parameters(parameters)
.bdd(new HashConsGraph(this).getBdd())
.sourceLocation(sourceLocation)
.build();
}

@Override
public SourceLocation getSourceLocation() {
return sourceLocation;
@@ -130,6 +159,15 @@ public String getVersion() {
return version;
}

/**
* Get the BDD for this rule-set, if it exists.
*
* @return the optionally present BDD definitions.
*/
public Optional<RulesBdd> getBdd() {
return Optional.ofNullable(bdd);
}

public Type typeCheck() {
return typeCheck(new Scope<>());
}
@@ -151,19 +189,27 @@ public Builder toBuilder() {
.sourceLocation(getSourceLocation())
.parameters(parameters)
.rules(rules)
.version(version);
.version(version)
.bdd(bdd);
}

@Override
public Node toNode() {
ArrayNode.Builder rulesBuilder = ArrayNode.builder();
rules.forEach(rulesBuilder::withValue);

return ObjectNode.builder()
ObjectNode.Builder builder = ObjectNode.builder()
.withMember(VERSION, version)
.withMember(PARAMETERS, parameters)
.withMember(RULES, rulesBuilder.build())
.build();
.withMember(PARAMETERS, parameters);

if (!rules.isEmpty()) {
ArrayNode.Builder rulesBuilder = ArrayNode.builder();
rules.forEach(rulesBuilder::withValue);
builder.withMember(RULES, rulesBuilder.build());
}

if (bdd != null) {
builder.withMember(BDD, bdd.toNode());
}

return builder.build();
}

@Override
@@ -175,21 +221,29 @@ public boolean equals(Object o) {
return false;
}
EndpointRuleSet that = (EndpointRuleSet) o;
return rules.equals(that.rules) && parameters.equals(that.parameters) && version.equals(that.version);
return rules.equals(that.rules)
&& parameters.equals(that.parameters)
&& version.equals(that.version)
&& Objects.equals(bdd, that.bdd);
}

@Override
public int hashCode() {
return Objects.hash(rules, parameters, version);
return Objects.hash(rules, parameters, version, bdd);
}

@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append(String.format("version: %s%n", version));
builder.append("params: \n").append(StringUtils.indent(parameters.toString(), 2));
builder.append("rules: \n");
rules.forEach(rule -> builder.append(StringUtils.indent(rule.toString(), 2)));
if (!rules.isEmpty()) {
builder.append("rules: \n");
rules.forEach(rule -> builder.append(StringUtils.indent(rule.toString(), 2)));
}
if (bdd != null) {
builder.append("bdd: \n").append(bdd);
}
return builder.toString();
}

@@ -242,6 +296,7 @@ public static class Builder extends RulesComponentBuilder<Builder, EndpointRuleS
private Parameters parameters;
// Default the version to the latest.
private String version = LATEST_VERSION;
private RulesBdd bdd;

/**
* Construct a builder from a {@link SourceLocation}.
@@ -310,6 +365,17 @@ public Builder parameters(Parameters parameters) {
return this;
}

/**
* Set the BDD program of the ruleset.
*
* @param bdd BDD program.
* @return the {@link Builder}.
*/
public Builder bdd(RulesBdd bdd) {
this.bdd = bdd;
return this;
}

@Override
public EndpointRuleSet build() {
EndpointRuleSet ruleSet = new EndpointRuleSet(this);

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.rulesengine.language.syntax.bdd;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import software.amazon.smithy.rulesengine.language.EndpointRuleSet;
import software.amazon.smithy.rulesengine.language.syntax.expressions.Expression;
import software.amazon.smithy.rulesengine.language.syntax.expressions.Reference;
import software.amazon.smithy.rulesengine.language.syntax.expressions.Template;
import software.amazon.smithy.rulesengine.language.syntax.expressions.functions.BooleanEquals;
import software.amazon.smithy.rulesengine.language.syntax.expressions.functions.LibraryFunction;
import software.amazon.smithy.rulesengine.language.syntax.expressions.functions.StringEquals;
import software.amazon.smithy.rulesengine.language.syntax.expressions.literal.Literal;
import software.amazon.smithy.rulesengine.language.syntax.expressions.literal.StringLiteral;
import software.amazon.smithy.rulesengine.language.syntax.rule.Condition;

public final class RulesBddCondition {

// Heuristic based complexity score for each method.
private static final int UNKNOWN_FUNCTION_COMPLEXITY = 4;

private static final Map<String, Integer> COMPLEXITY_ASSIGNMENTS = new HashMap<>();
static {
COMPLEXITY_ASSIGNMENTS.put("isSet", 1);
COMPLEXITY_ASSIGNMENTS.put("not", 2);
COMPLEXITY_ASSIGNMENTS.put("booleanEquals", 3);
COMPLEXITY_ASSIGNMENTS.put("stringEquals", 4);
COMPLEXITY_ASSIGNMENTS.put("substring", 5);
COMPLEXITY_ASSIGNMENTS.put("aws.partition", 6);
COMPLEXITY_ASSIGNMENTS.put("getAttr", 7);
COMPLEXITY_ASSIGNMENTS.put("uriEncode", 8);
COMPLEXITY_ASSIGNMENTS.put("aws.parseArn", 9);
COMPLEXITY_ASSIGNMENTS.put("isValidHostLabel", 10);
COMPLEXITY_ASSIGNMENTS.put("parseURL", 11);
}

private final Condition condition;
private boolean isStateful;
private int complexity = 0;
private int hash = 0;

private RulesBddCondition(Condition condition, EndpointRuleSet ruleSet) {
this.condition = condition;
// Conditions that assign a value are always considered stateful.
isStateful = condition.getResult().isPresent();
crawlCondition(ruleSet, 0, condition.getFunction());
}

public static RulesBddCondition from(Condition condition, EndpointRuleSet ruleSet) {
return new RulesBddCondition(canonicalizeCondition(condition), ruleSet);
}

// Canonicalize conditions such that variable references for booleanEquals and stringEquals come before
// a literal. This ensures that these commutative functions count as a single variable and don't needlessly
// bloat the BDD table.
private static Condition canonicalizeCondition(Condition condition) {
Expression func = condition.getFunction();
if (func instanceof BooleanEquals) {
BooleanEquals f = (BooleanEquals) func;
if (f.getArguments().get(0) instanceof Literal && !(f.getArguments().get(1) instanceof Literal)) {
// Flip the order to move the literal last.
return condition.toBuilder().fn(BooleanEquals.ofExpressions(
f.getArguments().get(1),
f.getArguments().get(0)
)).build();
}
} else if (func instanceof StringEquals) {
StringEquals f = (StringEquals) func;
if (f.getArguments().get(0) instanceof Literal && !(f.getArguments().get(1) instanceof Literal)) {
// Flip the order to move the literal last.
return condition.toBuilder().fn(StringEquals.ofExpressions(
f.getArguments().get(1),
f.getArguments().get(0)
)).build();
}
}

return condition;
}

public Condition getCondition() {
return condition;
}

public int getComplexity() {
return complexity;
}

public boolean isStateful() {
return isStateful;
}

private void crawlCondition(EndpointRuleSet ruleSet, int depth, Expression e) {
// Every level of nesting is an automatic complexity++.
complexity++;
if (e instanceof Literal) {
walkLiteral(ruleSet, (Literal) e, depth);
} else if (e instanceof Reference) {
walkReference(ruleSet, (Reference) e);
} else if (e instanceof LibraryFunction) {
walkLibraryFunction(ruleSet, (LibraryFunction) e, depth);
}
}

private void walkLiteral(EndpointRuleSet ruleSet, Literal l, int depth) {
if (l instanceof StringLiteral) {
StringLiteral s = (StringLiteral) l;
Template template = s.value();
if (!template.isStatic()) {
complexity += 8;
for (Template.Part part : template.getParts()) {
if (part instanceof Template.Dynamic) {
// Need to check for dynamic variables that reference non-global params.
// Also add to the score for each parameter.
Template.Dynamic dynamic = (Template.Dynamic) part;
crawlCondition(ruleSet, depth + 1, dynamic.toExpression());
}
}
}
}
}

private void walkReference(EndpointRuleSet ruleSet, Reference r) {
// It's stateful if the name referenced here is not an input parameter name.
if (!ruleSet.getParameters().get(r.getName()).isPresent()) {
isStateful = true;
}
}

private void walkLibraryFunction(EndpointRuleSet ruleSet, LibraryFunction l, int depth) {
// Track function complexity.
Integer functionComplexity = COMPLEXITY_ASSIGNMENTS.get(l.getName());
complexity += functionComplexity != null ? functionComplexity : UNKNOWN_FUNCTION_COMPLEXITY;
// Crawl the arguments.
for (Expression arg : l.getArguments()) {
crawlCondition(ruleSet, depth + 1, arg);
}
}

@Override
public boolean equals(Object object) {
if (this == object) {
return true;
} else if (object == null || getClass() != object.getClass()) {
return false;
} else {
RulesBddCondition that = (RulesBddCondition) object;
return isStateful == that.isStateful
&& complexity == that.complexity
&& Objects.equals(condition, that.condition);
}
}

@Override
public int hashCode() {
int result = hash;
if (hash == 0) {
result = Objects.hash(condition, isStateful, complexity);
hash = result;
}
return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.rulesengine.language.syntax.bdd;

import java.util.ArrayList;
import java.util.List;
import software.amazon.smithy.model.node.ArrayNode;
import software.amazon.smithy.model.node.ExpectationNotMetException;
import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.node.ObjectNode;
import software.amazon.smithy.rulesengine.language.syntax.rule.Condition;
import software.amazon.smithy.rulesengine.language.syntax.rule.Rule;

/**
* Handles converting BDD to and from Nodes.
*/
final class RulesBddNode {

private RulesBddNode() {}

static Node toNode(RulesBdd bdd) {
ObjectNode.Builder builder = Node.objectNodeBuilder();
builder.withMember("root", bdd.getRootNode());

ArrayNode.Builder conditionBuilder = ArrayNode.builder();
for (Condition condition : bdd.getConditions()) {
conditionBuilder.withValue(condition.toNode());
}
builder.withMember("conditions", conditionBuilder.build());

ArrayNode.Builder resultBuilder = ArrayNode.builder();
for (Rule result : bdd.getResults()) {
resultBuilder.withValue(result.toNode());
}
builder.withMember("results", resultBuilder.build());

if (bdd.getNodes().length > 0) {
ArrayNode.Builder nodeBuilder = ArrayNode.builder();
builder.withMember("nodes", nodeBuilder.build());
}

return Node.objectNode();
}

static RulesBdd fromNode(Node node) {
ObjectNode o = node.expectObjectNode();
int root = o.expectNumberMember("root").getValue().intValue();

ArrayNode conditionsArray = o.expectArrayMember("conditions").expectArrayNode();
List<Condition> conditions = new ArrayList<>(conditionsArray.size());
for (Node value : conditionsArray.getElements()) {
conditions.add(Condition.fromNode(value));
}

ArrayNode resultsArray = o.expectArrayMember("results").expectArrayNode();
List<Rule> results = new ArrayList<>(resultsArray.size());
for (Node value : resultsArray.getElements()) {
results.add(Rule.fromNode(value));
}

ArrayNode nodesArray = o.expectArrayMember("nodes").expectArrayNode();
int[][] nodes = new int[nodesArray.size()][];
int row = 0;
for (Node value : nodesArray.getElements()) {
ArrayNode nodeArray = value.expectArrayNode();
if (nodeArray.size() != 3) {
throw new ExpectationNotMetException("Each node array must have three numbers", nodeArray);
}
int[] nodeRow = new int[3];
for (int i = 0; i < 3; i++) {
nodeRow[i] = nodeArray.get(i).get().expectNumberNode().getValue().intValue();
}
nodes[row++] = nodeRow;
}

return new RulesBdd(conditions, results, nodes, root);
}
}
Original file line number Diff line number Diff line change
@@ -21,7 +21,7 @@
public final class EndpointRule extends Rule {
private final Endpoint endpoint;

EndpointRule(Rule.Builder builder, Endpoint endpoint) {
public EndpointRule(Rule.Builder builder, Endpoint endpoint) {
super(builder);
this.endpoint = endpoint;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.rulesengine.analysis;

import java.nio.file.Files;
import java.nio.file.Paths;
import org.junit.jupiter.api.Test;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.loader.ModelAssembler;
import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.shapes.ModelSerializer;
import software.amazon.smithy.model.shapes.ServiceShape;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.rulesengine.language.EndpointRuleSet;
import software.amazon.smithy.rulesengine.traits.EndpointRuleSetTrait;

public class HashConsGraphTest {
@Test
public void test() throws Exception {
String[] regional = {
"/Users/dowling/projects/aws-sdk-js-v3/codegen/sdk-codegen/aws-models/connect.json",
"com.amazonaws.connect#AmazonConnectService"
};
String[] s3 = {
"/Users/dowling/projects/smithy-java/aws/client/aws-client-rulesengine/src/shared-resources/software/amazon/smithy/java/aws/client/rulesengine/s3.json",
"com.amazonaws.s3#AmazonS3"
};
String[] inputs = s3;

Model model = Model.assembler()
.addImport(Paths.get(inputs[0]))
.discoverModels()
.putProperty(ModelAssembler.ALLOW_UNKNOWN_TRAITS, true)
.assemble()
.unwrap();

ServiceShape service = model.expectShape(ShapeId.from(inputs[1]), ServiceShape.class);
EndpointRuleSet ruleSet = service.expectTrait(EndpointRuleSetTrait.class).getEndpointRuleSet();
HashConsGraph graph = new HashConsGraph(ruleSet);

double paths = graph.getPaths().size();
double totalConditions = 0;
int maxDepth = 0;
for (HashConsGraph.BddPath path : graph.getPaths()) {
totalConditions += path.getStatefulConditions().size() + path.getStatelessConditions().size();
maxDepth = Math.max(maxDepth, path.getStatefulConditions().size() + path.getStatelessConditions().size());
System.out.println(path);
}

System.out.println("Max depth: " + maxDepth);
System.out.println("Average path conditions: " + (totalConditions / paths));
System.out.println("BDD:");
System.out.println(graph.getBdd());


EndpointRuleSet updated = ruleSet.toBddForm();
EndpointRuleSetTrait updatedTrait = service
.expectTrait(EndpointRuleSetTrait.class)
.toBuilder()
.ruleSet(updated.toNode())
.build();
ServiceShape updatedService = service.toBuilder().addTrait(updatedTrait).build();
Model updatedModel = model.toBuilder().addShape(updatedService).build();

Files.write(Paths.get("/tmp/s3.json"), Node.prettyPrintJson(ModelSerializer.builder().build().serialize(updatedModel)).getBytes());
}
}