diff --git a/config/spotbugs/filter.xml b/config/spotbugs/filter.xml
index 6188a7e3059..a92ee96eade 100644
--- a/config/spotbugs/filter.xml
+++ b/config/spotbugs/filter.xml
@@ -218,4 +218,10 @@
+
+
+
+
+
+
diff --git a/docs/source-2.0/additional-specs/rules-engine/specification.rst b/docs/source-2.0/additional-specs/rules-engine/specification.rst
index f1861f97689..01ce02bef77 100644
--- a/docs/source-2.0/additional-specs/rules-engine/specification.rst
+++ b/docs/source-2.0/additional-specs/rules-engine/specification.rst
@@ -14,18 +14,27 @@ are composed of a set of *conditions*, which determine if a rule should be
selected, and a result. Conditions act on the defined parameters, and allow for
the modeling of statements.
-When a rule’s conditions are evaluated successfully, the rule provides either a
+When a rule's conditions are evaluated successfully, the rule provides either a
result and its accompanying requirements or an error describing the unsupported
state. Modeled endpoint errors allow for more explicit descriptions to users,
such as providing errors when a service doesn't support a combination of
conditions.
+--------------------
+Rules engine version
+--------------------
+
+The rules engine specification is versioned, with the current version being 1.1. Unless otherwise specified, functions,
+features, and built-ins have been available since version 1.0. Any feature, function, or built-in used in the
+``endpointRuleSet`` or ``endpointBdd`` traits MUST be supported by the declared version of the trait. In other words,
+the feature's introduction version must be less than or equal to the trait version.
.. smithy-trait:: smithy.rules#endpointRuleSet
.. _smithy.rules#endpointRuleSet-trait:
+--------------------------------------
``smithy.rules#endpointRuleSet`` trait
-======================================
+--------------------------------------
Summary
Defines a rule set for deriving service endpoints at runtime.
@@ -45,8 +54,7 @@ The content of the ``endpointRuleSet`` document has the following properties:
- Description
* - version
- ``string``
- - **Required**. The rule set schema version. This specification covers
- version 1.0 of the endpoint rule set.
+ - **Required**. The rules engine version (e.g., 1.0).
* - serviceId
- ``string``
- **Required**. An identifier for the corresponding service.
@@ -74,6 +82,184 @@ or :ref:`error rules, ` with an
empty set of conditions to provide a more meaningful default or error depending
on the scenario.
+.. smithy-trait:: smithy.rules#endpointBdd
+.. _smithy.rules#endpointBdd-trait:
+
+----------------------------------
+``smithy.rules#endpointBdd`` trait
+----------------------------------
+
+.. warning:: Experimental
+
+ This trait is experimental and subject to change.
+
+Summary
+ A Binary `Decision Diagram (BDD) `_ representation of
+ endpoint rules that is more compact and efficient at runtime than the decision-tree-based EndpointRuleSet trait.
+Trait selector
+ ``service``
+Value type
+ ``structure``
+
+The ``endpointBdd`` trait provides a BDD representation of endpoint rules, optimizing runtime evaluation by
+eliminating redundant condition evaluations and reducing the decision tree to a minimal directed acyclic graph.
+This trait is an alternative to ``endpointRuleSet`` that trades compile-time complexity for significantly improved
+runtime performance and reduced artifact sizes.
+
+.. note::
+
+ The ``endpointBdd`` trait can be generated from an ``endpointRuleSet`` trait through compilation. Services may
+ provide either trait, with ``endpointBdd`` preferred for production use due to its performance characteristics.
+
+The ``endpointBdd`` structure has the following properties:
+
+.. list-table::
+ :header-rows: 1
+ :widths: 10 30 60
+
+ * - Property name
+ - Type
+ - Description
+ * - version
+ - ``string``
+ - **Required**. The endpoint rules engine version. Must be at least version 1.1.
+ * - parameters
+ - ``map`` of `Parameter object`_
+ - **Required**. A map of zero or more endpoint parameter names to
+ their parameter configuration. Uses the same parameter structure as
+ ``endpointRuleSet``.
+ * - conditions
+ - ``array`` of `Condition object`_
+ - **Required**. Array of conditions that are evaluated during BDD
+ traversal. Each condition is referenced by its index in this array.
+ * - results
+ - ``array`` of `Endpoint rule object`_ or `Error rule object`_
+ - **Required**. Array of possible endpoint results. The implicit `NoMatchRule` at BDD reference 0 is not included
+ in the array. These rule objects MUST NOT contain conditions.
+ * - root
+ - ``integer``
+ - **Required**. The root reference where BDD evaluation begins.
+ * - nodeCount
+ - ``integer``
+ - **Required**. The total number of nodes in the BDD. Used for validation and exact-sizing arrays during
+ deserialization.
+ * - nodes
+ - ``string``
+ - **Required**. Base64-encoded binary representation of BDD nodes. Each node is encoded as three 4-byte
+ integers: ``[conditionIndex, highRef, lowRef]``.
+
+.. _rules-engine-endpoint-bdd-node-structure:
+
+BDD node structure
+------------------
+
+Each BDD node is encoded as a triple of integers:
+
+* ``conditionIndex``: Zero-based index into the ``conditions`` array
+* ``highRef``: Reference to follow when the condition evaluates to true
+* ``lowRef``: Reference to follow when the condition evaluates to false
+
+The first node, index 0, is always the terminal node ``[-1, 1, -1]`` and MUST NOT be referenced directly. This node
+serves as the canonical base case for BDD reduction algorithms.
+
+.. _rules-engine-endpoint-bdd-reference-encoding:
+
+Reference encoding
+------------------
+
+BDD references use the following encoding scheme:
+
+.. list-table::
+ :header-rows: 1
+ :widths: 20 80
+
+ * - Reference value
+ - Description
+ * - ``0``
+ - Invalid/unused reference (never appears in valid BDDs)
+ * - ``1``
+ - TRUE terminal (no match in endpoint resolution)
+ * - ``-1``
+ - FALSE terminal (no match in endpoint resolution)
+ * - ``2, 3, 4, ...``
+ - Node references (points to ``nodes[ref-1]``)
+ * - ``-2, -3, -4, ...``
+ - Complement edges (logical NOT of the referenced node)
+ * - ``100000000+``
+ - Result terminals (100000000 + resultIndex)
+
+When traversing a complement edge (negative reference), the high and low branches are swapped during evaluation.
+This enables significant node sharing and BDD size reduction.
+
+.. _rules-engine-endpoint-bdd-binary-encoding:
+
+Binary node encoding
+--------------------
+
+Nodes are encoded as a Base64 string using binary encoding for efficiency:
+
+* Each node consists of three 4-byte big-endian integers
+* Nodes are concatenated sequentially: ``[node0, node1, ..., nodeN-1]``
+* The resulting byte array is Base64-encoded
+
+
+.. note:: Why binary?
+
+ This encoding provides:
+
+ * **Size efficiency**: smaller than an array of JSON integers, or an array of arrays of integers
+ * **Performance**: Direct deserialization into the target data structure (e.g., primitive arrays and integers)
+ * **Cleaner diffs**: BDD node changes appear as single-line modifications rather than spread over thousands
+ of numbers.
+
+.. _rules-engine-endpoint-bdd-evaluation:
+
+BDD evaluation
+--------------
+
+BDD evaluation follows these steps:
+
+#. Start at the root reference
+#. While the reference is a node reference (not a terminal or result):
+
+ * Extract the node index: ``nodeIndex = |ref| - 1``
+ * Retrieve the node at that index
+ * Evaluate the condition at ``conditionIndex``
+ * Determine which branch to follow:
+
+ * If the reference is complemented (negative) AND condition is true: follow ``lowRef``
+ * If the reference is complemented (negative) AND condition is false: follow ``highRef``
+ * If the reference is positive AND condition is true: follow ``highRef``
+ * If the reference is positive AND condition is false: follow ``lowRef``
+
+ * Update the reference to the chosen branch and continue
+
+#. When reaching a terminal or result:
+
+ * For result references ≥ 100000000: return ``results[ref - 100000000]``
+ * For terminals (1 or -1): return the ``NoMatchRule``
+
+For example, a reference of 100000003 would return ``results[3]``, while a reference of 1 or -1 indicates no matching
+rule was found.
+
+.. _rules-engine-endpoint-bdd-validation:
+
+Validation requirements
+-----------------------
+
+* **Root reference**: MUST NOT be complemented (negative)
+* **Reference validity**: All references MUST be valid:
+
+ * ``0`` is forbidden
+ * Node references MUST point to existing nodes
+ * Result references MUST point to existing results
+
+* **Node structure**: Each node MUST be a properly formed triple
+* **Condition indices**: Each node's condition index MUST be within ``[0, conditionCount)``
+* **Result structure**: The first result (index 0) implicitly represents ``NoMatchRule`` and is not serialized.
+ All serialized results MUST be either ``EndpointRule`` or ``ErrorRule`` objects without conditions.
+* **Version requirement**: The version MUST be at least 1.1
+
.. _rules-engine-endpoint-rule-set-parameter:
----------------
@@ -119,7 +305,7 @@ allow values to be bound to parameters from other locations in generated
clients.
Parameters MAY be annotated with the ``builtIn`` property, which designates that
-the parameter should be bound to a value determined by the built-in’s name. The
+the parameter should be bound to a value determined by the built-in's name. The
:ref:`rules engine contains built-ins ` and
the set is extensible.
diff --git a/docs/source-2.0/additional-specs/rules-engine/standard-library.rst b/docs/source-2.0/additional-specs/rules-engine/standard-library.rst
index f628d689286..6ce48785ef2 100644
--- a/docs/source-2.0/additional-specs/rules-engine/standard-library.rst
+++ b/docs/source-2.0/additional-specs/rules-engine/standard-library.rst
@@ -38,6 +38,53 @@ parameter is equal to the value ``false``:
}
+.. _rules-engine-standard-library-coalesce:
+
+``coalesce`` function
+=====================
+
+Summary
+ Evaluates arguments in order and returns the first non-empty result, otherwise returns the result of the last
+ argument.
+Argument types
+ * This function is variadic and requires two or more arguments, each of type ``T`` or ``option``
+ * All arguments must have the same inner type ``T``
+Return type
+ * ``coalesce(T, T, ...)`` → ``T``
+ * ``coalesce(option, T, ...)`` → ``T`` (if any argument is non-optional)
+ * ``coalesce(T, option, ...)`` → ``T`` (if any argument is non-optional)
+ * ``coalesce(option, option, ...)`` → ``option`` (if all arguments are optional)
+Since
+ 1.1
+
+The ``coalesce`` function provides null-safe chaining by evaluating arguments in order and returning the first
+non-empty result. If all arguments leading up to the last argument evaluate to empty, it returns the result of the
+last argument. This is particularly useful for providing default values for optional parameters, chaining multiple
+optional values together, and related optimizations.
+
+The function accepts two or more arguments, all of which must have the same inner type after unwrapping any
+optionals. The return type is ``option`` only if all arguments are ``option``; otherwise it returns ``T``.
+
+The following example demonstrates using ``coalesce`` with multiple arguments to try several optional values
+in sequence:
+
+.. code-block:: json
+
+ {
+ "fn": "coalesce",
+ "argv": [
+ {"ref": "customEndpoint"},
+ {"ref": "regionalEndpoint"},
+ {"ref": "defaultEndpoint"}
+ ]
+ }
+
+.. important::
+ All arguments must have the same type after unwrapping any optionals (types are known at compile time and do not
+ need to be validated at runtime). Note that the first non-empty result is returned even if it's ``false``
+ (coalesce is looking for a *non-empty* value, not a truthy value).
+
+
.. _rules-engine-standard-library-getAttr:
``getAttr`` function
diff --git a/smithy-aws-endpoints/src/main/java/software/amazon/smithy/rulesengine/aws/language/functions/AwsArn.java b/smithy-aws-endpoints/src/main/java/software/amazon/smithy/rulesengine/aws/language/functions/AwsArn.java
index c52fe6c727b..f4d05b6503f 100644
--- a/smithy-aws-endpoints/src/main/java/software/amazon/smithy/rulesengine/aws/language/functions/AwsArn.java
+++ b/smithy-aws-endpoints/src/main/java/software/amazon/smithy/rulesengine/aws/language/functions/AwsArn.java
@@ -4,7 +4,7 @@
*/
package software.amazon.smithy.rulesengine.aws.language.functions;
-import java.util.Arrays;
+import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@@ -39,28 +39,67 @@ private AwsArn(Builder builder) {
* @return the optional ARN.
*/
public static Optional parse(String arn) {
- String[] base = arn.split(":", 6);
- if (base.length != 6) {
+ if (arn == null || arn.length() < 8 || !arn.startsWith("arn:")) {
return Optional.empty();
}
- // First section must be "arn".
- if (!base[0].equals("arn")) {
+
+ // find each of the first five ':' positions
+ int p0 = 3; // after "arn"
+ int p1 = arn.indexOf(':', p0 + 1);
+ if (p1 < 0) {
+ return Optional.empty();
+ }
+
+ int p2 = arn.indexOf(':', p1 + 1);
+ if (p2 < 0) {
+ return Optional.empty();
+ }
+
+ int p3 = arn.indexOf(':', p2 + 1);
+ if (p3 < 0) {
return Optional.empty();
}
- // Sections for partition, service, and resource type must not be empty.
- if (base[1].isEmpty() || base[2].isEmpty() || base[5].isEmpty()) {
+
+ int p4 = arn.indexOf(':', p3 + 1);
+ if (p4 < 0) {
+ return Optional.empty();
+ }
+
+ // extract and validate mandatory parts
+ String partition = arn.substring(p0 + 1, p1);
+ String service = arn.substring(p1 + 1, p2);
+ String region = arn.substring(p2 + 1, p3);
+ String accountId = arn.substring(p3 + 1, p4);
+ String resource = arn.substring(p4 + 1);
+
+ if (partition.isEmpty() || service.isEmpty() || resource.isEmpty()) {
return Optional.empty();
}
return Optional.of(builder()
- .partition(base[1])
- .service(base[2])
- .region(base[3])
- .accountId(base[4])
- .resource(Arrays.asList(base[5].split("[:/]", -1)))
+ .partition(partition)
+ .service(service)
+ .region(region)
+ .accountId(accountId)
+ .resource(splitResource(resource))
.build());
}
+ private static List splitResource(String resource) {
+ List result = new ArrayList<>();
+ int start = 0;
+ int length = resource.length();
+ for (int i = 0; i < length; i++) {
+ char c = resource.charAt(i);
+ if (c == ':' || c == '/') {
+ result.add(resource.substring(start, i));
+ start = i + 1;
+ }
+ }
+ result.add(resource.substring(start));
+ return result;
+ }
+
/**
* Builder to create an {@link AwsArn} instance.
*
diff --git a/smithy-aws-endpoints/src/main/java/software/amazon/smithy/rulesengine/aws/validators/RuleSetAwsBuiltInValidator.java b/smithy-aws-endpoints/src/main/java/software/amazon/smithy/rulesengine/aws/validators/RuleSetAwsBuiltInValidator.java
index e0c5cdd0663..f149ab7c52b 100644
--- a/smithy-aws-endpoints/src/main/java/software/amazon/smithy/rulesengine/aws/validators/RuleSetAwsBuiltInValidator.java
+++ b/smithy-aws-endpoints/src/main/java/software/amazon/smithy/rulesengine/aws/validators/RuleSetAwsBuiltInValidator.java
@@ -6,7 +6,6 @@
import java.util.ArrayList;
import java.util.List;
-import java.util.Optional;
import java.util.Set;
import software.amazon.smithy.model.FromSourceLocation;
import software.amazon.smithy.model.Model;
@@ -14,8 +13,8 @@
import software.amazon.smithy.model.validation.AbstractValidator;
import software.amazon.smithy.model.validation.ValidationEvent;
import software.amazon.smithy.rulesengine.aws.language.functions.AwsBuiltIns;
-import software.amazon.smithy.rulesengine.language.EndpointRuleSet;
import software.amazon.smithy.rulesengine.language.syntax.parameters.Parameter;
+import software.amazon.smithy.rulesengine.traits.EndpointBddTrait;
import software.amazon.smithy.rulesengine.traits.EndpointRuleSetTrait;
import software.amazon.smithy.utils.SetUtils;
@@ -33,36 +32,30 @@ public class RuleSetAwsBuiltInValidator extends AbstractValidator {
@Override
public List validate(Model model) {
List events = new ArrayList<>();
- for (ServiceShape serviceShape : model.getServiceShapesWithTrait(EndpointRuleSetTrait.class)) {
- events.addAll(validateRuleSetAwsBuiltIns(serviceShape,
- serviceShape.expectTrait(EndpointRuleSetTrait.class)
- .getEndpointRuleSet()));
+
+ for (ServiceShape s : model.getServiceShapesWithTrait(EndpointRuleSetTrait.class)) {
+ EndpointRuleSetTrait trait = s.expectTrait(EndpointRuleSetTrait.class);
+ validateRuleSetAwsBuiltIns(events, s, trait.getEndpointRuleSet().getParameters());
+ }
+
+ for (ServiceShape s : model.getServiceShapesWithTrait(EndpointBddTrait.class)) {
+ validateRuleSetAwsBuiltIns(events, s, s.expectTrait(EndpointBddTrait.class).getParameters());
}
+
return events;
}
- private List validateRuleSetAwsBuiltIns(ServiceShape serviceShape, EndpointRuleSet ruleSet) {
- List events = new ArrayList<>();
- for (Parameter parameter : ruleSet.getParameters()) {
+ private void validateRuleSetAwsBuiltIns(List events, ServiceShape s, Iterable params) {
+ for (Parameter parameter : params) {
if (parameter.isBuiltIn()) {
- validateBuiltIn(serviceShape, parameter.getBuiltIn().get(), parameter).ifPresent(events::add);
+ validateBuiltIn(events, s, parameter.getBuiltIn().get(), parameter);
}
}
- return events;
}
- private Optional validateBuiltIn(
- ServiceShape serviceShape,
- String builtInName,
- FromSourceLocation source
- ) {
- if (ADDITIONAL_CONSIDERATION_BUILT_INS.contains(builtInName)) {
- return Optional.of(danger(
- serviceShape,
- source,
- String.format(ADDITIONAL_CONSIDERATION_MESSAGE, builtInName),
- builtInName));
+ private void validateBuiltIn(List events, ServiceShape s, String name, FromSourceLocation source) {
+ if (ADDITIONAL_CONSIDERATION_BUILT_INS.contains(name)) {
+ events.add(danger(s, source, String.format(ADDITIONAL_CONSIDERATION_MESSAGE, name), name));
}
- return Optional.empty();
}
}
diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/analysis/BddCoverageChecker.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/analysis/BddCoverageChecker.java
new file mode 100644
index 00000000000..d2b3443163d
--- /dev/null
+++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/analysis/BddCoverageChecker.java
@@ -0,0 +1,160 @@
+/*
+ * 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.BitSet;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import software.amazon.smithy.model.node.Node;
+import software.amazon.smithy.rulesengine.language.evaluation.RuleEvaluator;
+import software.amazon.smithy.rulesengine.language.evaluation.value.Value;
+import software.amazon.smithy.rulesengine.language.syntax.Identifier;
+import software.amazon.smithy.rulesengine.language.syntax.parameters.Parameters;
+import software.amazon.smithy.rulesengine.language.syntax.rule.Condition;
+import software.amazon.smithy.rulesengine.language.syntax.rule.NoMatchRule;
+import software.amazon.smithy.rulesengine.language.syntax.rule.Rule;
+import software.amazon.smithy.rulesengine.logic.ConditionEvaluator;
+import software.amazon.smithy.rulesengine.logic.bdd.Bdd;
+import software.amazon.smithy.rulesengine.traits.EndpointBddTrait;
+import software.amazon.smithy.rulesengine.traits.EndpointTestCase;
+
+/**
+ * Analyzes test coverage for BDD-based endpoint rules.
+ */
+public final class BddCoverageChecker {
+
+ private final Parameters parameters;
+ private final Bdd bdd;
+ private final List conditions;
+ private final List results;
+ private final BitSet visitedConditions;
+ private final BitSet visitedResults;
+
+ public BddCoverageChecker(EndpointBddTrait bddTrait) {
+ this(bddTrait.getParameters(), bddTrait.getBdd(), bddTrait.getResults(), bddTrait.getConditions());
+ }
+
+ BddCoverageChecker(Parameters parameters, Bdd bdd, List results, List conditions) {
+ this.results = results;
+ this.parameters = parameters;
+ this.conditions = conditions;
+ this.bdd = bdd;
+ this.visitedConditions = new BitSet(conditions.size());
+ this.visitedResults = new BitSet(results.size());
+ }
+
+ /**
+ * Evaluates a test case and updates coverage information.
+ *
+ * @param testCase the test case to evaluate
+ */
+ public void evaluateTestCase(EndpointTestCase testCase) {
+ Map input = new LinkedHashMap<>();
+ for (Map.Entry entry : testCase.getParams().getStringMap().entrySet()) {
+ input.put(Identifier.of(entry.getKey()), Value.fromNode(entry.getValue()));
+ }
+ evaluateInput(input);
+ }
+
+ /**
+ * Evaluates with the given inputs and updates coverage.
+ *
+ * @param input the input parameters to evaluate
+ */
+ public void evaluateInput(Map input) {
+ TestEvaluator evaluator = new TestEvaluator(input);
+ int resultIdx = bdd.evaluate(evaluator);
+ if (resultIdx >= 0) {
+ visitedResults.set(resultIdx);
+ }
+ }
+
+ /**
+ * Returns conditions that were never evaluated during testing.
+ *
+ * @return set of unevaluated conditions
+ */
+ public Set getUnevaluatedConditions() {
+ Set unevaluated = new HashSet<>();
+ for (int i = 0; i < conditions.size(); i++) {
+ if (!visitedConditions.get(i)) {
+ unevaluated.add(conditions.get(i));
+ }
+ }
+ return unevaluated;
+ }
+
+ /**
+ * Returns results that were never reached during testing.
+ *
+ * @return set of unreached results
+ */
+ public Set getUnevaluatedResults() {
+ Set unevaluated = new HashSet<>();
+ for (int i = 0; i < results.size(); i++) {
+ if (!visitedResults.get(i)) {
+ Rule result = results.get(i);
+ if (!(result instanceof NoMatchRule)) {
+ unevaluated.add(result);
+ }
+ }
+ }
+ return unevaluated;
+ }
+
+ /**
+ * Returns the percentage of conditions that were evaluated at least once.
+ *
+ * @return condition coverage percentage (0-100)
+ */
+ public double getConditionCoverage() {
+ return conditions.isEmpty() ? 100.0 : (100.0 * visitedConditions.cardinality() / conditions.size());
+ }
+
+ /**
+ * Returns the percentage of results that were reached at least once.
+ *
+ * @return result coverage percentage (0-100)
+ */
+ public double getResultCoverage() {
+ // Count only non-NO_MATCH results
+ int relevantResults = 0;
+ int coveredRelevantResults = 0;
+
+ for (int i = 0; i < results.size(); i++) {
+ if (!(results.get(i) instanceof NoMatchRule)) {
+ relevantResults++;
+ if (visitedResults.get(i)) {
+ coveredRelevantResults++;
+ }
+ }
+ }
+
+ return relevantResults == 0 ? 100.0 : (100.0 * coveredRelevantResults / relevantResults);
+ }
+
+ // Evaluator that tracks what gets visited during BDD evaluation.
+ private final class TestEvaluator implements ConditionEvaluator {
+ private final RuleEvaluator ruleEvaluator;
+
+ TestEvaluator(Map input) {
+ this.ruleEvaluator = new RuleEvaluator(parameters, input);
+ }
+
+ @Override
+ public boolean test(int conditionIndex) {
+ if (conditionIndex < 0 || conditionIndex >= conditions.size()) {
+ return false;
+ } else {
+ visitedConditions.set(conditionIndex);
+ Condition condition = conditions.get(conditionIndex);
+ return ruleEvaluator.evaluateCondition(condition).isTruthy();
+ }
+ }
+ }
+}
diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/CoreExtension.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/CoreExtension.java
index 240422aa4c4..ba7d5113c12 100644
--- a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/CoreExtension.java
+++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/CoreExtension.java
@@ -6,6 +6,7 @@
import java.util.List;
import software.amazon.smithy.rulesengine.language.syntax.expressions.functions.BooleanEquals;
+import software.amazon.smithy.rulesengine.language.syntax.expressions.functions.Coalesce;
import software.amazon.smithy.rulesengine.language.syntax.expressions.functions.FunctionDefinition;
import software.amazon.smithy.rulesengine.language.syntax.expressions.functions.GetAttr;
import software.amazon.smithy.rulesengine.language.syntax.expressions.functions.IsSet;
@@ -36,6 +37,7 @@ public List getLibraryFunctions() {
IsSet.getDefinition(),
IsValidHostLabel.getDefinition(),
Not.getDefinition(),
+ Coalesce.getDefinition(),
ParseUrl.getDefinition(),
StringEquals.getDefinition(),
Substring.getDefinition(),
diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/Endpoint.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/Endpoint.java
index b1b23f7a997..f9b407fb196 100644
--- a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/Endpoint.java
+++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/Endpoint.java
@@ -206,21 +206,21 @@ public int hashCode() {
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
- sb.append("url: ").append(url).append("\n");
+ sb.append("url: ").append(url);
if (!headers.isEmpty()) {
- sb.append("headers:\n");
+ sb.append("\nheaders:");
for (Map.Entry> entry : headers.entrySet()) {
- sb.append(StringUtils.indent(String.format("%s: %s", entry.getKey(), entry.getValue()), 2))
- .append("\n");
+ sb.append("\n");
+ sb.append(StringUtils.indent(String.format("%s: %s", entry.getKey(), entry.getValue()), 2));
}
}
if (!properties.isEmpty()) {
- sb.append("properties:\n");
+ sb.append("\nproperties:");
for (Map.Entry entry : properties.entrySet()) {
- sb.append(StringUtils.indent(String.format("%s: %s", entry.getKey(), entry.getValue()), 2))
- .append("\n");
+ sb.append("\n");
+ sb.append(StringUtils.indent(String.format("%s: %s", entry.getKey(), entry.getValue()), 2));
}
}
diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/EndpointRuleSet.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/EndpointRuleSet.java
index 9f25f9ce426..57c1b423d22 100644
--- a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/EndpointRuleSet.java
+++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/EndpointRuleSet.java
@@ -58,6 +58,7 @@ private static final class LazyEndpointComponentFactoryHolder {
private final List rules;
private final SourceLocation sourceLocation;
private final String version;
+ private final RulesVersion rulesVersion;
private EndpointRuleSet(Builder builder) {
super();
@@ -65,6 +66,7 @@ private EndpointRuleSet(Builder builder) {
rules = builder.rules.copy();
sourceLocation = SmithyBuilder.requiredState("source", builder.getSourceLocation());
version = SmithyBuilder.requiredState(VERSION, builder.version);
+ rulesVersion = RulesVersion.of(version);
}
/**
@@ -130,6 +132,15 @@ public String getVersion() {
return version;
}
+ /**
+ * Get the parsed rules engine version.
+ *
+ * @return parsed version.
+ */
+ public RulesVersion getRulesVersion() {
+ return rulesVersion;
+ }
+
public Type typeCheck() {
return typeCheck(new Scope<>());
}
diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/RulesVersion.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/RulesVersion.java
new file mode 100644
index 00000000000..d0302067dce
--- /dev/null
+++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/RulesVersion.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package software.amazon.smithy.rulesengine.language;
+
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import software.amazon.smithy.utils.SmithyUnstableApi;
+import software.amazon.smithy.utils.StringUtils;
+
+/**
+ * Represents the rules engine version with major and minor components.
+ */
+@SmithyUnstableApi
+public final class RulesVersion implements Comparable {
+
+ private static final ConcurrentHashMap CACHE = new ConcurrentHashMap<>();
+
+ public static final RulesVersion V1_0 = of("1.0");
+ public static final RulesVersion V1_1 = of("1.1");
+
+ private final int major;
+ private final int minor;
+ private final String stringValue;
+ private final int hashCode;
+
+ private RulesVersion(int major, int minor) {
+ if (major < 0 || minor < 0) {
+ throw new IllegalArgumentException("Version components must be non-negative");
+ }
+
+ this.major = major;
+ this.minor = minor;
+ this.stringValue = major + "." + minor;
+ this.hashCode = Objects.hash(major, minor);
+ }
+
+ /**
+ * Creates a RulesVersion from a string representation.
+ *
+ * @param version the version string (e.g., "1.0", "1.2")
+ * @return the RulesVersion instance
+ * @throws IllegalArgumentException if the version string is invalid
+ */
+ public static RulesVersion of(String version) {
+ return CACHE.computeIfAbsent(version, RulesVersion::parse);
+ }
+
+ /**
+ * Creates a RulesVersion from components.
+ *
+ * @param major the major version
+ * @param minor the minor version
+ * @return the RulesVersion instance
+ */
+ public static RulesVersion of(int major, int minor) {
+ String key = major + "." + minor;
+ return CACHE.computeIfAbsent(key, k -> new RulesVersion(major, minor));
+ }
+
+ private static RulesVersion parse(String version) {
+ if (StringUtils.isEmpty(version)) {
+ throw new IllegalArgumentException("Version string cannot be null or empty");
+ }
+
+ String[] parts = version.split("\\.");
+ if (parts.length < 2) {
+ throw new IllegalArgumentException("Invalid version: `" + version + "`. Expected format: major.minor");
+ }
+
+ try {
+ int major = Integer.parseInt(parts[0]);
+ int minor = Integer.parseInt(parts[1]);
+ return new RulesVersion(major, minor);
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException("Invalid version format: " + version, e);
+ }
+ }
+
+ /**
+ * Gets the major version component.
+ *
+ * @return the major version
+ */
+ public int getMajor() {
+ return major;
+ }
+
+ /**
+ * Gets the minor version component.
+ *
+ * @return the minor version
+ */
+ public int getMinor() {
+ return minor;
+ }
+
+ /**
+ * Checks if this version is at least the specified version.
+ *
+ * @param other the version to compare against
+ * @return true if this version >= other
+ */
+ public boolean isAtLeast(RulesVersion other) {
+ return compareTo(other) >= 0;
+ }
+
+ @Override
+ public int compareTo(RulesVersion other) {
+ if (this == other) {
+ return 0;
+ }
+
+ int result = Integer.compare(major, other.major);
+ if (result != 0) {
+ return result;
+ } else {
+ return Integer.compare(minor, other.minor);
+ }
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ } else if (!(obj instanceof RulesVersion)) {
+ return false;
+ }
+ RulesVersion other = (RulesVersion) obj;
+ return major == other.major && minor == other.minor;
+ }
+
+ @Override
+ public int hashCode() {
+ return hashCode;
+ }
+
+ @Override
+ public String toString() {
+ return stringValue;
+ }
+}
diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/RuleEvaluator.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/RuleEvaluator.java
index da81e346228..6e8d70a8771 100644
--- a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/RuleEvaluator.java
+++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/RuleEvaluator.java
@@ -19,9 +19,15 @@
import software.amazon.smithy.rulesengine.language.syntax.expressions.functions.GetAttr;
import software.amazon.smithy.rulesengine.language.syntax.expressions.literal.Literal;
import software.amazon.smithy.rulesengine.language.syntax.parameters.Parameter;
+import software.amazon.smithy.rulesengine.language.syntax.parameters.Parameters;
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.RuleValueVisitor;
+import software.amazon.smithy.rulesengine.logic.RuleBasedConditionEvaluator;
+import software.amazon.smithy.rulesengine.logic.bdd.Bdd;
+import software.amazon.smithy.rulesengine.traits.EndpointBddTrait;
import software.amazon.smithy.utils.SmithyUnstableApi;
/**
@@ -31,6 +37,23 @@
public class RuleEvaluator implements ExpressionVisitor {
private final Scope scope = new Scope<>();
+ public RuleEvaluator() {}
+
+ /**
+ * Create a rule evaluator from parameters and using an initial set of arguments.
+ *
+ *
This is primarily used for manually driven condition evaluation.
+ *
+ * @param parameters Parameters of the evaluator, used to initialize defaults and parameters.
+ * @param parameterArguments Arguments used to initialize evaluation scope state.
+ */
+ public RuleEvaluator(Parameters parameters, Map parameterArguments) {
+ for (Parameter parameter : parameters) {
+ parameter.getDefault().ifPresent(value -> scope.insert(parameter.getName(), value));
+ }
+ parameterArguments.forEach(scope::insert);
+ }
+
/**
* Initializes a new {@link RuleEvaluator} instances, and evaluates
* the provided ruleset and parameter arguments.
@@ -44,6 +67,67 @@ public static Value evaluate(EndpointRuleSet ruleset, Map par
return new RuleEvaluator().evaluateRuleSet(ruleset, parameterArguments);
}
+ /**
+ * Initializes a new {@link RuleEvaluator} instances, and evaluates the provided BDD and parameter arguments.
+ *
+ * @param trait The trait to evaluate.
+ * @param args The rule-set parameter identifiers and values to evaluate the BDD against.
+ * @return The resulting value from the final matched rule.
+ */
+ public static Value evaluate(EndpointBddTrait trait, Map args) {
+ return evaluate(trait.getBdd(), trait.getParameters(), trait.getConditions(), trait.getResults(), args);
+ }
+
+ /**
+ * Initializes a new {@link RuleEvaluator} instances, and evaluates the provided BDD and parameter arguments.
+ *
+ * @param bdd The endpoint bdd.
+ * @param parameterArguments The rule-set parameter identifiers and values to evaluate the BDD against.
+ * @return The resulting value from the final matched rule.
+ */
+ public static Value evaluate(
+ Bdd bdd,
+ Parameters parameters,
+ List conditions,
+ List results,
+ Map parameterArguments
+ ) {
+ return new RuleEvaluator().evaluateBdd(bdd, parameters, conditions, results, parameterArguments);
+ }
+
+ private Value evaluateBdd(
+ Bdd bdd,
+ Parameters parameters,
+ List conditions,
+ List results,
+ Map parameterArguments
+ ) {
+ return scope.inScope(() -> {
+ for (Parameter parameter : parameters) {
+ parameter.getDefault().ifPresent(value -> scope.insert(parameter.getName(), value));
+ }
+
+ parameterArguments.forEach(scope::insert);
+
+ Condition[] conds = conditions.toArray(new Condition[0]);
+ RuleBasedConditionEvaluator conditionEvaluator = new RuleBasedConditionEvaluator(this, conds);
+ int result = bdd.evaluate(conditionEvaluator);
+
+ if (result < 0) {
+ throw new RuntimeException("No BDD result matched");
+ }
+
+ Rule rule = results.get(result);
+ if (rule instanceof EndpointRule) {
+ return resolveEndpoint(this, ((EndpointRule) rule).getEndpoint());
+ } else if (rule instanceof ErrorRule) {
+ return resolveError(this, ((ErrorRule) rule).getError());
+ } else {
+ throw new RuntimeException("Invalid BDD rule result: " + rule);
+ }
+ });
+ }
+
/**
* Evaluate the provided ruleset and parameter arguments.
*
@@ -101,6 +185,17 @@ public Value visitIsSet(Expression fn) {
return Value.booleanValue(!fn.accept(this).isEmpty());
}
+ @Override
+ public Value visitCoalesce(List expressions) {
+ for (Expression exp : expressions) {
+ Value result = exp.accept(this);
+ if (!result.isEmpty()) {
+ return result;
+ }
+ }
+ return Value.emptyValue();
+ }
+
@Override
public Value visitNot(Expression not) {
return Value.booleanValue(!not.accept(this).expectBooleanValue().getValue());
@@ -139,7 +234,7 @@ private Value handleRule(Rule rule) {
return scope.inScope(() -> {
for (Condition condition : rule.getConditions()) {
Value value = evaluateCondition(condition);
- if (value.isEmpty() || value.equals(Value.booleanValue(false))) {
+ if (!value.isTruthy()) {
return Value.emptyValue();
}
}
@@ -159,32 +254,40 @@ public Value visitTreeRule(List rules) {
@Override
public Value visitErrorRule(Expression error) {
- return error.accept(self);
+ return resolveError(self, error);
}
@Override
public Value visitEndpointRule(Endpoint endpoint) {
- EndpointValue.Builder builder = EndpointValue.builder()
- .sourceLocation(endpoint)
- .url(endpoint.getUrl()
- .accept(RuleEvaluator.this)
- .expectStringValue()
- .getValue());
-
- for (Map.Entry entry : endpoint.getProperties().entrySet()) {
- builder.putProperty(entry.getKey().toString(), entry.getValue().accept(RuleEvaluator.this));
- }
-
- for (Map.Entry> entry : endpoint.getHeaders().entrySet()) {
- List values = new ArrayList<>();
- for (Expression expression : entry.getValue()) {
- values.add(expression.accept(RuleEvaluator.this).expectStringValue().getValue());
- }
- builder.putHeader(entry.getKey(), values);
- }
- return builder.build();
+ return resolveEndpoint(self, endpoint);
}
});
});
}
+
+ private static Value resolveEndpoint(RuleEvaluator self, Endpoint endpoint) {
+ EndpointValue.Builder builder = EndpointValue.builder()
+ .sourceLocation(endpoint)
+ .url(endpoint.getUrl()
+ .accept(self)
+ .expectStringValue()
+ .getValue());
+
+ for (Map.Entry entry : endpoint.getProperties().entrySet()) {
+ builder.putProperty(entry.getKey().toString(), entry.getValue().accept(self));
+ }
+
+ for (Map.Entry> entry : endpoint.getHeaders().entrySet()) {
+ List values = new ArrayList<>();
+ for (Expression expression : entry.getValue()) {
+ values.add(expression.accept(self).expectStringValue().getValue());
+ }
+ builder.putHeader(entry.getKey(), values);
+ }
+ return builder.build();
+ }
+
+ private static Value resolveError(RuleEvaluator self, Expression error) {
+ return error.accept(self);
+ }
}
diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/TestEvaluator.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/TestEvaluator.java
index b80efa02c61..2c3fbdda3ce 100644
--- a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/TestEvaluator.java
+++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/TestEvaluator.java
@@ -13,6 +13,7 @@
import software.amazon.smithy.rulesengine.language.evaluation.value.EndpointValue;
import software.amazon.smithy.rulesengine.language.evaluation.value.Value;
import software.amazon.smithy.rulesengine.language.syntax.Identifier;
+import software.amazon.smithy.rulesengine.traits.EndpointBddTrait;
import software.amazon.smithy.rulesengine.traits.EndpointTestCase;
import software.amazon.smithy.rulesengine.traits.EndpointTestExpectation;
import software.amazon.smithy.rulesengine.traits.ExpectedEndpoint;
@@ -34,12 +35,30 @@ private TestEvaluator() {}
* @param testCase The test case.
*/
public static void evaluate(EndpointRuleSet ruleset, EndpointTestCase testCase) {
+ Value result = RuleEvaluator.evaluate(ruleset, createParams(testCase));
+ processResult(result, testCase);
+ }
+
+ /**
+ * Evaluate the given BDD and test case. Throws an exception in the event the test case does not pass.
+ *
+ * @param bdd The BDD trait to be tested.
+ * @param testCase The test case.
+ */
+ public static void evaluate(EndpointBddTrait bdd, EndpointTestCase testCase) {
+ Value result = RuleEvaluator.evaluate(bdd, createParams(testCase));
+ processResult(result, testCase);
+ }
+
+ private static Map createParams(EndpointTestCase testCase) {
Map parameters = new LinkedHashMap<>();
for (Map.Entry entry : testCase.getParams().getMembers().entrySet()) {
parameters.put(Identifier.of(entry.getKey()), Value.fromNode(entry.getValue()));
}
- Value result = RuleEvaluator.evaluate(ruleset, parameters);
+ return parameters;
+ }
+ private static void processResult(Value result, EndpointTestCase testCase) {
StringBuilder messageBuilder = new StringBuilder("while executing test case");
if (testCase.getDocumentation().isPresent()) {
messageBuilder.append(" ").append(testCase.getDocumentation().get());
diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/type/AnyType.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/type/AnyType.java
index d23096dd3d9..799d15a3453 100644
--- a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/type/AnyType.java
+++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/type/AnyType.java
@@ -10,6 +10,9 @@
* The "any" type, which matches all other types.
*/
public final class AnyType extends AbstractType {
+
+ static final AnyType INSTANCE = new AnyType();
+
AnyType() {}
@Override
diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/type/ArrayType.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/type/ArrayType.java
index ba20c757ca9..faeadf6e358 100644
--- a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/type/ArrayType.java
+++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/type/ArrayType.java
@@ -4,12 +4,17 @@
*/
package software.amazon.smithy.rulesengine.language.evaluation.type;
+import java.util.Collections;
import java.util.Objects;
+import java.util.Optional;
+import software.amazon.smithy.rulesengine.language.syntax.expressions.literal.Literal;
/**
* The "array" type, which contains entries of a member type.
*/
public final class ArrayType extends AbstractType {
+
+ private static final Optional ZERO = Optional.of(Literal.tupleLiteral(Collections.emptyList()));
private final Type member;
ArrayType(Type member) {
@@ -51,4 +56,9 @@ public int hashCode() {
public String toString() {
return String.format("ArrayType[%s]", member);
}
+
+ @Override
+ public Optional getZeroValue() {
+ return ZERO;
+ }
}
diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/type/BooleanType.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/type/BooleanType.java
index 8c58f670224..af0cfd62543 100644
--- a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/type/BooleanType.java
+++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/type/BooleanType.java
@@ -4,14 +4,26 @@
*/
package software.amazon.smithy.rulesengine.language.evaluation.type;
+import java.util.Optional;
+import software.amazon.smithy.rulesengine.language.syntax.expressions.literal.Literal;
+
/**
* The "boolean" type.
*/
public final class BooleanType extends AbstractType {
+
+ private static final Optional ZERO = Optional.of(Literal.of(false));
+ static final BooleanType INSTANCE = new BooleanType();
+
BooleanType() {}
@Override
public BooleanType expectBooleanType() {
return this;
}
+
+ @Override
+ public Optional getZeroValue() {
+ return ZERO;
+ }
}
diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/type/EmptyType.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/type/EmptyType.java
index 86ec7490935..7e9fec629de 100644
--- a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/type/EmptyType.java
+++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/type/EmptyType.java
@@ -10,6 +10,9 @@
* The "empty" type.
*/
public final class EmptyType extends AbstractType {
+
+ static final EmptyType INSTANCE = new EmptyType();
+
EmptyType() {}
@Override
diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/type/EndpointType.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/type/EndpointType.java
index b10d57f667d..65a25d4ca97 100644
--- a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/type/EndpointType.java
+++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/type/EndpointType.java
@@ -10,6 +10,9 @@
* The "endpoint" type, representing a valid client endpoint.
*/
public final class EndpointType extends AbstractType {
+
+ static final EndpointType INSTANCE = new EndpointType();
+
EndpointType() {}
@Override
diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/type/IntegerType.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/type/IntegerType.java
index 33a71e2a58c..713e047cac3 100644
--- a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/type/IntegerType.java
+++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/type/IntegerType.java
@@ -4,14 +4,26 @@
*/
package software.amazon.smithy.rulesengine.language.evaluation.type;
+import java.util.Optional;
+import software.amazon.smithy.rulesengine.language.syntax.expressions.literal.Literal;
+
/**
* The "integer" type.
*/
public final class IntegerType extends AbstractType {
+
+ private static final Optional ZERO = Optional.of(Literal.of(0));
+ static final IntegerType INSTANCE = new IntegerType();
+
IntegerType() {}
@Override
public IntegerType expectIntegerType() {
return this;
}
+
+ @Override
+ public Optional getZeroValue() {
+ return ZERO;
+ }
}
diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/type/OptionalType.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/type/OptionalType.java
index d96389ce3b4..fa6150a16c9 100644
--- a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/type/OptionalType.java
+++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/type/OptionalType.java
@@ -5,7 +5,9 @@
package software.amazon.smithy.rulesengine.language.evaluation.type;
import java.util.Objects;
+import java.util.Optional;
import software.amazon.smithy.rulesengine.language.error.InnerParseError;
+import software.amazon.smithy.rulesengine.language.syntax.expressions.literal.Literal;
/**
* The "optional" type, a container for a type that may or may not be present.
@@ -78,4 +80,9 @@ public int hashCode() {
public String toString() {
return String.format("OptionalType[%s]", inner);
}
+
+ @Override
+ public Optional getZeroValue() {
+ return inner.getZeroValue();
+ }
}
diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/type/StringType.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/type/StringType.java
index 9738a474e4f..1546d8113ca 100644
--- a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/type/StringType.java
+++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/type/StringType.java
@@ -4,14 +4,26 @@
*/
package software.amazon.smithy.rulesengine.language.evaluation.type;
+import java.util.Optional;
+import software.amazon.smithy.rulesengine.language.syntax.expressions.literal.Literal;
+
/**
* The "string" type.
*/
public final class StringType extends AbstractType {
+
+ private static final Optional ZERO = Optional.of(Literal.of(""));
+ static final StringType INSTANCE = new StringType();
+
StringType() {}
@Override
public StringType expectStringType() {
return this;
}
+
+ @Override
+ public Optional getZeroValue() {
+ return ZERO;
+ }
}
diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/type/Type.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/type/Type.java
index 15738d40b83..1313819497e 100644
--- a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/type/Type.java
+++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/type/Type.java
@@ -6,8 +6,10 @@
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import software.amazon.smithy.rulesengine.language.error.InnerParseError;
import software.amazon.smithy.rulesengine.language.syntax.Identifier;
+import software.amazon.smithy.rulesengine.language.syntax.expressions.literal.Literal;
import software.amazon.smithy.rulesengine.language.syntax.parameters.ParameterType;
import software.amazon.smithy.utils.SmithyUnstableApi;
@@ -38,20 +40,20 @@ default Type provenTruthy() {
}
static Type fromParameterType(ParameterType parameterType) {
- if (parameterType == ParameterType.STRING) {
- return stringType();
+ switch (parameterType) {
+ case STRING:
+ return stringType();
+ case BOOLEAN:
+ return booleanType();
+ case STRING_ARRAY:
+ return arrayType(stringType());
+ default:
+ throw new IllegalArgumentException("Unexpected parameter type: " + parameterType);
}
- if (parameterType == ParameterType.BOOLEAN) {
- return booleanType();
- }
- if (parameterType == ParameterType.STRING_ARRAY) {
- return arrayType(stringType());
- }
- throw new IllegalArgumentException("Unexpected parameter type: " + parameterType);
}
static AnyType anyType() {
- return new AnyType();
+ return AnyType.INSTANCE;
}
static ArrayType arrayType(Type inner) {
@@ -59,19 +61,19 @@ static ArrayType arrayType(Type inner) {
}
static BooleanType booleanType() {
- return new BooleanType();
+ return BooleanType.INSTANCE;
}
static EmptyType emptyType() {
- return new EmptyType();
+ return EmptyType.INSTANCE;
}
static EndpointType endpointType() {
- return new EndpointType();
+ return EndpointType.INSTANCE;
}
static IntegerType integerType() {
- return new IntegerType();
+ return IntegerType.INSTANCE;
}
static OptionalType optionalType(Type type) {
@@ -83,7 +85,7 @@ static RecordType recordType(Map inner) {
}
static StringType stringType() {
- return new StringType();
+ return StringType.INSTANCE;
}
static TupleType tupleType(List members) {
@@ -129,4 +131,17 @@ default StringType expectStringType() throws InnerParseError {
default TupleType expectTupleType() throws InnerParseError {
throw new InnerParseError("Expected tuple but found " + this);
}
+
+ /**
+ * Gets the default zero-value of the type as a Literal.
+ *
+ *
Strings, booleans, integers, and arrays have zero values. Other types do not. E.g., a map might have
+ * required properties, and the behavior of a tuple _seems_ to imply that each member is required. Optionals
+ * return the zero value of its inner type.
+ *
+ * @return the default zero value.
+ */
+ default Optional getZeroValue() {
+ return Optional.empty();
+ }
}
diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/value/ArrayValue.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/value/ArrayValue.java
index 40eeb6cc48a..64b52bad7ad 100644
--- a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/value/ArrayValue.java
+++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/evaluation/value/ArrayValue.java
@@ -101,4 +101,13 @@ public String toString() {
}
return "[" + String.join(", ", valueStrings) + "]";
}
+
+ @Override
+ public Object toObject() {
+ List