diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/Into.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/Into.java new file mode 100644 index 00000000000..9f60f913877 --- /dev/null +++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/Into.java @@ -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 the type. + */ +@SmithyUnstableApi +public interface Into { + T into(); +} diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/IntoSelf.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/IntoSelf.java new file mode 100644 index 00000000000..b537dcb100a --- /dev/null +++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/IntoSelf.java @@ -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 the type. + */ +@SmithyUnstableApi +public interface IntoSelf> extends Into { + @Override + default T into() { + return (T) this; + } +} diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/analysis/CoverageChecker.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/analysis/CoverageChecker.java new file mode 100644 index 00000000000..de8099da1de --- /dev/null +++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/analysis/CoverageChecker.java @@ -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 { + 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 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 map = new HashMap<>(); + 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 checkCoverage() { + Stream 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 checkCoverageFromRule(Rule rule) { + Stream conditions = rule.accept(new CollectConditions()); + return coverageForConditions(conditions); + + } + + private Stream coverageForConditions(Stream conditions) { + return conditions.distinct().flatMap(condition -> { + Wrapper w = new Wrapper<>(condition); + ArrayList conditionResults = checkerCore.conditionResults.getOrDefault(w, new ArrayList<>()); + List branches = conditionResults.stream() + .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 { + 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 { + 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, ArrayList> conditionResults = new HashMap<>(); + Context context = null; + + @Override + public Value evaluateRuleSet(EndpointRuleSet ruleset, Map parameterArguments) { + try { + context = new Context(parameterArguments); + return super.evaluateRuleSet(ruleset, parameterArguments); + } finally { + context = null; + } + } + + @Override + public Value evaluateCondition(Condition condition) { + assert context != null; + Value result = super.evaluateCondition(condition); + Wrapper cond = new Wrapper<>(condition); + ArrayList list = conditionResults.getOrDefault(cond, new ArrayList<>()); + 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 input; + + Context(Map input) { + this.input = input; + } + } + } + + static class CollectConditions extends TraversingVisitor { + @Override + public Stream visitConditions(List conditions) { + return conditions.stream(); + } + } + + public static class CoverageResult { + private final Condition condition; + private final boolean result; + private final List> otherUsages; + + public CoverageResult(Condition condition, boolean result, List> otherUsages) { + this.condition = condition; + this.result = result; + this.otherUsages = otherUsages; + } + + public Condition condition() { + return condition; + } + + public boolean result() { + return result; + } + + public List> otherUsages() { + 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) { + PathFinder.Path path = PathFinder.findPath(ruleset, condition).orElseThrow(NoSuchElementException::new); + StringBuilder sb = new StringBuilder(); + sb.append(pretty()).append("\n"); + for (List cond : path.negated()) { + 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(); + } + } +} 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 new file mode 100644 index 00000000000..669da1b3ea2 --- /dev/null +++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/Endpoint.java @@ -0,0 +1,328 @@ +/* + * 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.language; + +import static software.amazon.smithy.rulesengine.language.error.RuleError.context; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import software.amazon.smithy.model.FromSourceLocation; +import software.amazon.smithy.model.SourceLocation; +import software.amazon.smithy.model.node.ArrayNode; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.node.ToNode; +import software.amazon.smithy.rulesengine.language.error.RuleError; +import software.amazon.smithy.rulesengine.language.eval.Scope; +import software.amazon.smithy.rulesengine.language.eval.Type; +import software.amazon.smithy.rulesengine.language.eval.TypeCheck; +import software.amazon.smithy.rulesengine.language.syntax.Identifier; +import software.amazon.smithy.rulesengine.language.syntax.expr.Expression; +import software.amazon.smithy.rulesengine.language.syntax.expr.Literal; +import software.amazon.smithy.rulesengine.language.util.MandatorySourceLocation; +import software.amazon.smithy.rulesengine.language.util.SourceLocationTrackingBuilder; +import software.amazon.smithy.rulesengine.language.util.StringUtils; +import software.amazon.smithy.utils.BuilderRef; +import software.amazon.smithy.utils.MapUtils; +import software.amazon.smithy.utils.Pair; +import software.amazon.smithy.utils.SmithyBuilder; +import software.amazon.smithy.utils.SmithyUnstableApi; +import software.amazon.smithy.utils.ToSmithyBuilder; + +/** + * An Endpoint as returned by EndpointRules. + */ +@SmithyUnstableApi +public final class Endpoint extends MandatorySourceLocation implements ToSmithyBuilder, TypeCheck, ToNode { + private static final String URL = "url"; + private static final String PROPERTIES = "properties"; + private static final String HEADERS = "headers"; + private static final String SIGV_4 = "sigv4"; + private static final String SIG_V4A = "sigv4a"; + private static final String SIGNING_REGION = "signingRegion"; + + private final Expression url; + private final Map> headers; + private final Map properties; + + private Endpoint(Builder builder) { + super(builder.getSourceLocation()); + this.url = SmithyBuilder.requiredState("url", builder.url); + Map properties = new LinkedHashMap<>(builder.properties.copy()); + List authSchemes = + builder.authSchemes.copy().stream() + .map( + authScheme -> { + Map base = new LinkedHashMap<>(); + base.put(Identifier.of("name"), Literal.of(authScheme.left.asString())); + base.putAll(authScheme.right); + return Literal.record(base); + }) + .collect(Collectors.toList()); + if (!authSchemes.isEmpty()) { + properties.put(Identifier.of("authSchemes"), Literal.tuple(authSchemes)); + } + + this.properties = properties; + this.headers = builder.headers.copy(); + } + + /** + * Constructs an {@link Endpoint} from a {@link Node}. Node must be an {@link ObjectNode}. + * + * @param node the object node. + * @return the node as an {@link Endpoint}. + */ + public static Endpoint fromNode(Node node) { + ObjectNode on = node.expectObjectNode(); + + Builder builder = builder() + .sourceLocation(node); + + builder.url(Expression.fromNode(on.expectMember(URL, "URL must be included in endpoint"))); + on.expectNoAdditionalProperties(Arrays.asList(PROPERTIES, HEADERS, URL)); + + on.getObjectMember(PROPERTIES) + .ifPresent( + props -> { + Map members = new LinkedHashMap<>(); + props.getMembers() + .forEach((k, v) -> members.put(Identifier.of(k), Literal.fromNode(v))); + builder.properties(members); + }); + + on.getObjectMember(HEADERS).ifPresent(objectNode -> { + objectNode.getMembers().forEach((headerName, headerValues) -> { + builder.addHeader(headerName.getValue(), + headerValues.expectArrayNode("header values should be an array") + .getElements().stream().map(Expression::fromNode).collect(Collectors.toList())); + }); + }); + + return builder.build(); + } + + /** + * Create a new Endpoint builder. + * + * @return Endpoint builder + */ + public static Builder builder() { + return new Builder(SourceLocation.none()); + } + + /** + * Returns the Endpoint URL as an expression. + * + * @return the endpoint URL expression. + */ + public Expression getUrl() { + return url; + } + + @Override + public Builder toBuilder() { + return builder() + .sourceLocation(this.getSourceLocation()) + .url(url).properties(properties); + } + + @Override + public int hashCode() { + return Objects.hash(url, properties, headers); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Endpoint endpoint = (Endpoint) o; + return url.equals(endpoint.url) && properties.equals(endpoint.properties) && headers.equals(endpoint.headers); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("url: ").append(url).append("\n"); + if (!headers.isEmpty()) { + headers.forEach( + (key, value) -> { + sb.append(StringUtils.indent(String.format("%s:%s", key, value), 2)); + }); + } + if (!properties.isEmpty()) { + sb.append("properties:\n"); + properties.forEach((k, v) -> sb.append(StringUtils.indent(String.format("%s: %s", k, v), 2))); + } + return sb.toString(); + } + + @Override + public Type typeCheck(Scope scope) { + context("while checking the URL", url, () -> url.typeCheck(scope).expectString()); + RuleError.context("while checking properties", () -> { + properties.forEach((k, lit) -> { + lit.typeCheck(scope); + }); + return null; + }); + RuleError.context("while checking headers", () -> { + for (List headerList : headers.values()) { + for (Expression header : headerList) { + header.typeCheck(scope).expectString(); + } + } + return null; + }); + return Type.endpoint(); + } + + @Override + public Node toNode() { + return ObjectNode.builder() + .withMember(URL, url) + .withMember(PROPERTIES, propertiesNode()) + .withMember(HEADERS, headersNode()) + .build(); + } + + private Node propertiesNode() { + ObjectNode.Builder on = ObjectNode.builder(); + properties.forEach((k, v) -> + on.withMember(k.toString(), v.toNode()) + ); + return on.build(); + } + + private Node headersNode() { + return exprMapNode(headers); + } + + private Node exprMapNode(Map> m) { + ObjectNode.Builder mapNode = ObjectNode.builder(); + m.forEach((k, v) -> mapNode.withMember(k, ArrayNode.fromNodes(v.stream() + .map(Expression::toNode) + .collect(Collectors.toList())))); + return mapNode.build(); + } + + /** + * Get the endpoint properties as a map of {@link Identifier} to {@link Literal} values. + * + * @return the endpoint properties. + */ + public Map getProperties() { + return properties; + } + + /** + * Get the endpoint headers as a map of {@link String} to list of {@link Expression} values. + * + * @return the endpoint headers. + */ + public Map> getHeaders() { + return headers; + } + + /** + * Builder for {@link Endpoint}. + */ + public static class Builder extends SourceLocationTrackingBuilder { + private static final String SIGNING_NAME = "signingName"; + private static final String SIGNING_REGION_SET = "signingRegionSet"; + + private final BuilderRef>> headers = BuilderRef.forOrderedMap(); + private final BuilderRef> properties = BuilderRef.forOrderedMap(); + private final BuilderRef>>> authSchemes = BuilderRef.forList(); + private Expression url; + + public Builder(FromSourceLocation sourceLocation) { + super(sourceLocation); + } + + public Builder url(Expression url) { + this.url = url; + return this; + } + + public Builder properties(Map properties) { + this.properties.clear(); + this.properties.get().putAll(properties); + return this; + } + + public Builder authSchemes(List schemes, Map> params) { + this.authSchemes.clear(); + schemes.forEach(scheme -> addAuthScheme(scheme, params.get(scheme))); + return this; + } + + public Builder addAuthScheme(Identifier scheme, Map parameters) { + this.authSchemes.get().add(Pair.of(scheme, parameters)); + return this; + } + + public Builder addAuthScheme(String scheme, Map parameters) { + this.authSchemes.get().add(Pair.of(Identifier.of(scheme), + parameters.entrySet().stream() + .collect(Collectors.toMap(k -> Identifier.of(k.getKey()), Map.Entry::getValue)))); + return this; + } + + public Builder sigv4(Literal signingRegion, Literal signingService) { + return addAuthScheme(SIGV_4, MapUtils.of(SIGNING_REGION, signingRegion, SIGNING_NAME, signingService)); + } + + public Builder sigv4a(List signingRegionSet, Literal signingService) { + return addAuthScheme(SIG_V4A, MapUtils.of(SIGNING_REGION_SET, Literal.tuple(signingRegionSet), + SIGNING_NAME, signingService)); + } + + public Builder headers(Map> headers) { + this.headers.clear(); + this.headers.get().putAll(headers); + return this; + } + + public Builder addHeader(String name, List value) { + this.headers.get().put(name, value); + return this; + } + + public Builder addHeader(String name, Literal value) { + // Note: if we want to add multi-header support in the future we'll need to tackle that separately + if (this.headers.get().containsKey(name)) { + throw new RuntimeException(String.format("A header already exists for %s", name)); + } + this.headers.get().put(name, Collections.singletonList(value)); + return this; + } + + @Override + public Endpoint build() { + return new Endpoint(this); + } + } +} 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 new file mode 100644 index 00000000000..036ede8d31e --- /dev/null +++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/EndpointRuleSet.java @@ -0,0 +1,235 @@ +/* + * 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.language; + +import static software.amazon.smithy.rulesengine.language.error.RuleError.context; +import static software.amazon.smithy.rulesengine.language.util.StringUtils.indent; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import software.amazon.smithy.model.FromSourceLocation; +import software.amazon.smithy.model.SourceLocation; +import software.amazon.smithy.model.node.ArrayNode; +import software.amazon.smithy.model.node.Node; +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.language.error.RuleError; +import software.amazon.smithy.rulesengine.language.eval.Scope; +import software.amazon.smithy.rulesengine.language.eval.Type; +import software.amazon.smithy.rulesengine.language.eval.TypeCheck; +import software.amazon.smithy.rulesengine.language.syntax.parameters.Parameters; +import software.amazon.smithy.rulesengine.language.syntax.rule.EndpointRule; +import software.amazon.smithy.rulesengine.language.syntax.rule.Rule; +import software.amazon.smithy.rulesengine.language.util.MandatorySourceLocation; +import software.amazon.smithy.rulesengine.language.util.SourceLocationTrackingBuilder; +import software.amazon.smithy.utils.BuilderRef; +import software.amazon.smithy.utils.SmithyBuilder; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * A set of EndpointRules. Endpoint Rules describe the endpoint resolution behavior for a service. + */ +@SmithyUnstableApi +public final class EndpointRuleSet extends MandatorySourceLocation implements TypeCheck, ToNode { + private static final String LATEST_VERSION = "1.3"; + private static final String VERSION = "version"; + private static final String PARAMETERS = "parameters"; + private static final String RULES = "rules"; + + private final List rules; + private final Parameters parameters; + private final String version; + + private EndpointRuleSet(Builder builder) { + super(builder.getSourceLocation()); + rules = builder.rules.copy(); + parameters = SmithyBuilder.requiredState("parameters", builder.parameters); + version = SmithyBuilder.requiredState("version", builder.version); + } + + public static EndpointRuleSet fromNode(Node node) throws RuleError { + return RuleError.context("when parsing endpoint ruleset", () -> EndpointRuleSet.newFromNode(node)); + } + + private static EndpointRuleSet newFromNode(Node node) throws RuleError { + ObjectNode on = node.expectObjectNode("The root of a ruleset must be an object"); + EndpointRuleSet.Builder builder = new Builder(node); + Parameters parameters = Parameters.fromNode(on.expectObjectMember(PARAMETERS)); + StringNode version = on.expectStringMember(VERSION); + + on.expectArrayMember(RULES) + .getElements().forEach(n -> { + builder.addRule(context("while parsing rule", n, () -> EndpointRule.fromNode(n))); + }); + return builder.version(version.getValue()).parameters(parameters).build(); + } + + public static Builder builder() { + return new Builder(SourceLocation.none()); + } + + public Parameters getParameters() { + return parameters; + } + + public List getRules() { + return rules; + } + + @Override + public Type typeCheck(Scope scope) { + return scope.inScope(() -> { + parameters.writeToScope(scope); + for (Rule rule : rules) { + rule.typeCheck(scope); + } + return Type.endpoint(); + }); + } + + public void typecheck() { + typeCheck(new Scope<>()); + } + + @Override + public Node toNode() { + return ObjectNode.builder() + .withMember(VERSION, version) + .withMember(PARAMETERS, parameters) + .withMember(RULES, rulesNode()) + .build(); + } + + public Builder toBuilder() { + return builder() + .sourceLocation(getSourceLocation()) + .parameters(parameters) + .rules(getRules()); + } + + private Node rulesNode() { + ArrayNode.Builder node = ArrayNode.builder(); + rules.forEach(node::withValue); + return node.build(); + } + + @Override + public int hashCode() { + return Objects.hash(rules, parameters, version); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + EndpointRuleSet that = (EndpointRuleSet) o; + return rules.equals(that.rules) && parameters.equals(that.parameters) && version.equals(that.version); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append(String.format("version: %s%n", version)); + builder.append("params: \n").append(indent(parameters.toString(), 2)); + builder.append("rules: \n"); + rules.forEach(rule -> builder.append(indent(rule.toString(), 2))); + return builder.toString(); + } + + public static class Builder extends SourceLocationTrackingBuilder { + private final BuilderRef> rules = BuilderRef.forList(); + private Parameters parameters; + // default the version to the latest. + private String version = LATEST_VERSION; + + /** + * Construct a builder from a {@link SourceLocation}. + * + * @param sourceLocation The source location + */ + public Builder(FromSourceLocation sourceLocation) { + super(sourceLocation); + } + + /** + * Sets the version for the {@link EndpointRuleSet}. + * If not set, the version will default to the latest version. + * + * @param version The version to set + * @return the {@link Builder} + */ + public Builder version(String version) { + this.version = version; + return this; + } + + /** + * Adds a rule to this ruleset. The rule be evaluated if all previous rules do not match. + * + * @param rule The {@link Rule} to add + * @return the {@link Builder} + */ + public Builder addRule(Rule rule) { + this.rules.get().add(rule); + return this; + } + + /** + * Inserts a rule into the ruleset. + * + * @param index the position to add the rule at. + * @param rule The {@link Rule} to add + * @return the {@link Builder} + */ + public Builder addRule(int index, Rule rule) { + this.rules.get().add(index, rule); + return this; + } + + /** + * Add rules to this ruleset. The rules be evaluated if all previous rules do not match. + * + * @param rules The Collection of {@link Rule} to add + * @return the {@link Builder} + */ + public Builder rules(Collection rules) { + this.rules.get().addAll(rules); + return this; + } + + /** + * Set the parameters for this {@link EndpointRuleSet}. + * + * @param parameters {@link Parameters} to set + * @return the {@link Builder} + */ + public Builder parameters(Parameters parameters) { + this.parameters = parameters; + return this; + } + + @Override + public EndpointRuleSet build() { + return new EndpointRuleSet(this); + } + } +} diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/error/InnerParseError.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/error/InnerParseError.java new file mode 100644 index 00000000000..626af683782 --- /dev/null +++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/error/InnerParseError.java @@ -0,0 +1,28 @@ +/* + * 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.language.error; + +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * Represents an error encountered when parsing a rule-set expression. + */ +@SmithyUnstableApi +public final class InnerParseError extends Exception { + public InnerParseError(String message) { + super(message); + } +} diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/error/InvalidRulesException.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/error/InvalidRulesException.java new file mode 100644 index 00000000000..dfb8c3e7dd4 --- /dev/null +++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/error/InvalidRulesException.java @@ -0,0 +1,48 @@ +/* + * 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.language.error; + +import software.amazon.smithy.model.FromSourceLocation; +import software.amazon.smithy.model.SourceLocation; +import software.amazon.smithy.rulesengine.language.util.SourceLocationUtils; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * Exception thrown when a rule-set is invalid. + */ +@SmithyUnstableApi +public final class InvalidRulesException extends RuntimeException implements FromSourceLocation { + private final transient SourceLocation sourceLocation; + + public InvalidRulesException(String message, FromSourceLocation location) { + super(createMessage(message, location.getSourceLocation())); + sourceLocation = location.getSourceLocation(); + } + + @Override + public SourceLocation getSourceLocation() { + return sourceLocation; + } + + private static String createMessage(String message, SourceLocation sourceLocation) { + if (sourceLocation == SourceLocation.NONE) { + return message; + } else { + String prettyLocation = SourceLocationUtils.stackTraceForm(sourceLocation); + return message.contains(prettyLocation) ? message : message + " (" + prettyLocation + ")"; + } + } +} diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/error/RuleError.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/error/RuleError.java new file mode 100644 index 00000000000..fa3454a8b70 --- /dev/null +++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/error/RuleError.java @@ -0,0 +1,111 @@ +/* + * 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.language.error; + +import java.util.ArrayList; +import java.util.List; +import software.amazon.smithy.model.FromSourceLocation; +import software.amazon.smithy.model.SourceException; +import software.amazon.smithy.model.SourceLocation; +import software.amazon.smithy.utils.Pair; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * An exception that can be thrown when rule-set is invalid. Used for providing meaningful contextual + * information when an invalid rule-set is encountered. + */ +@SmithyUnstableApi +public final class RuleError extends RuntimeException { + private final List> contexts = new ArrayList<>(); + private final SourceException root; + + public RuleError(SourceException root) { + super(root); + this.root = root; + } + + public static T context(String message, Runnable f) throws RuleError { + return RuleError.context(message, SourceLocation.none(), () -> { + f.run(); + return null; + }); + } + + public static T context(String message, Evaluator f) throws RuleError { + return RuleError.context(message, SourceLocation.none(), f); + } + + public static void context(String message, FromSourceLocation sourceLocation, Runnable f) { + context(message, sourceLocation, () -> { + f.run(); + return null; + }); + } + + public static T context(String message, FromSourceLocation sourceLocation, Evaluator f) throws RuleError { + try { + return f.call(); + } catch (SourceException ex) { + throw new RuleError(ex).withContext(message, sourceLocation.getSourceLocation()); + } catch (RuleError ex) { + throw ex.withContext(message, sourceLocation.getSourceLocation()); + } catch (Exception | Error ex) { + if (ex.getMessage() == null) { + throw new RuntimeException(ex); + } + throw new RuleError(new SourceException(ex.getMessage(), sourceLocation.getSourceLocation(), ex)) + .withContext(message, sourceLocation.getSourceLocation()); + } + } + + public RuleError withContext(String context, SourceLocation loc) { + this.contexts.add(Pair.of(context, loc)); + return this; + } + + @Override + public String toString() { + StringBuilder message = new StringBuilder(); + SourceLocation lastLoc = SourceLocation.none(); + for (int i = contexts.size() - 1; i > 0; i--) { + Pair context = contexts.get(i); + message.append(context.left); + message.append("\n"); + if (context.right != SourceLocation.NONE && context.right != lastLoc) { + message.append(" at ") + .append(context.right.getSourceLocation().getFilename()) + .append(":") + .append(context.right.getSourceLocation().getLine()) + .append("\n"); + lastLoc = context.right; + } + } + + if (root.getSourceLocation() != SourceLocation.none() && root.getSourceLocation() != lastLoc) { + message.append(" at ") + .append(root.getSourceLocation().getFilename()) + .append(":").append(root.getSourceLocation().getLine()) + .append("\n"); + } + message.append(root.getMessageWithoutLocation()); + return message.toString(); + } + + @FunctionalInterface + public interface Evaluator { + T call() throws InnerParseError; + } +} diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/eval/RuleEvaluator.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/eval/RuleEvaluator.java new file mode 100644 index 00000000000..4ea596e83d5 --- /dev/null +++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/eval/RuleEvaluator.java @@ -0,0 +1,175 @@ +/* + * 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.language.eval; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import software.amazon.smithy.rulesengine.language.Endpoint; +import software.amazon.smithy.rulesengine.language.EndpointRuleSet; +import software.amazon.smithy.rulesengine.language.syntax.Identifier; +import software.amazon.smithy.rulesengine.language.syntax.expr.Expression; +import software.amazon.smithy.rulesengine.language.syntax.expr.Literal; +import software.amazon.smithy.rulesengine.language.syntax.expr.Reference; +import software.amazon.smithy.rulesengine.language.syntax.fn.FunctionDefinition; +import software.amazon.smithy.rulesengine.language.syntax.fn.GetAttr; +import software.amazon.smithy.rulesengine.language.syntax.rule.Condition; +import software.amazon.smithy.rulesengine.language.syntax.rule.Rule; +import software.amazon.smithy.rulesengine.language.visit.ExpressionVisitor; +import software.amazon.smithy.rulesengine.language.visit.RuleValueVisitor; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * A runtime implementation of a rule-set evaluation engine. + */ +@SmithyUnstableApi +public class RuleEvaluator implements ExpressionVisitor { + private final Scope scope = new Scope<>(); + + /** + * Initializes a new {@link RuleEvaluator} instances, and evaluates the provided ruleset and parameter arguments. + * + * @param ruleset The endpoint ruleset. + * @param parameterArguments The rule-set parameter identifiers and values to evaluate the rule-set against. + * @return The resulting value from the final matched rule. + */ + public static Value evaluate(EndpointRuleSet ruleset, Map parameterArguments) { + return new RuleEvaluator().evaluateRuleSet(ruleset, parameterArguments); + } + + /** + * Evaluate the provided ruleset and parameter arguments. + * + * @param ruleset The endpoint ruleset. + * @param parameterArguments The rule-set parameter identifiers and values to evaluate the rule-set against. + * @return The resulting value from the final matched rule. + */ + public Value evaluateRuleSet(EndpointRuleSet ruleset, Map parameterArguments) { + return scope.inScope( + () -> { + ruleset + .getParameters() + .toList() + .forEach( + param -> { + param.getDefault().ifPresent(value -> scope.insert(param.getName(), value)); + }); + parameterArguments.forEach(scope::insert); + for (Rule rule : ruleset.getRules()) { + Value result = handleRule(rule); + if (!result.isNone()) { + return result; + } + } + throw new RuntimeException("No rules in ruleset matched"); + }); + } + + @Override + public Value visitLiteral(Literal literal) { + return literal.evaluate(this); + } + + @Override + public Value visitRef(Reference reference) { + return scope + .getValue(reference.getName()) + .orElse(Value.none()); + } + + @Override + public Value visitIsSet(Expression fn) { + return Value.bool(!fn.accept(this).isNone()); + } + + @Override + public Value visitNot(Expression not) { + return Value.bool(!not.accept(this).expectBool()); + } + + @Override + public Value visitBoolEquals(Expression left, Expression right) { + return Value.bool(left.accept(this).expectBool() == right.accept(this).expectBool()); + } + + @Override + public Value visitStringEquals(Expression left, Expression right) { + return Value.bool(left.accept(this).expectString().equals(right.accept(this).expectString())); + } + + public Value visitGetAttr(GetAttr getAttr) { + return getAttr.evaluate(getAttr.getTarget().accept(this)); + } + + @Override + public Value visitLibraryFunction(FunctionDefinition definition, List arguments) { + return definition.evaluate(arguments.stream().map(arg -> arg.accept(this)).collect(Collectors.toList())); + } + + private Value handleRule(Rule rule) { + RuleEvaluator self = this; + return scope.inScope(() -> { + for (Condition condition : rule.getConditions()) { + Value value = evaluateCondition(condition); + if (value.isNone() || value.equals(Value.bool(false))) { + return Value.none(); + } + } + return rule.accept(new RuleValueVisitor() { + @Override + public Value visitTreeRule(List rules) { + for (Rule subRule : rules) { + Value result = handleRule(subRule); + if (!result.isNone()) { + return result; + } + } + throw new RuntimeException( + String.format("no rules inside of tree rule matched—invalid rules (%s)", rule)); + } + + @Override + public Value visitErrorRule(Expression error) { + return error.accept(self); + } + + @Override + public Value visitEndpointRule(Endpoint endpoint) { + Value.Endpoint.Builder builder = Value.Endpoint.builder() + .sourceLocation(endpoint) + .url(endpoint.getUrl() + .accept(RuleEvaluator.this) + .expectString()); + endpoint.getProperties() + .forEach((key, value) -> builder.addProperty(key.toString(), + value.accept(RuleEvaluator.this))); + endpoint.getHeaders() + .forEach((name, expressions) -> expressions.forEach(expr -> builder.addHeader(name, + expr.accept(RuleEvaluator.this).expectString()))); + return builder.build(); + } + }); + }); + } + + public Value evaluateCondition(Condition condition) { + Value value = condition.getFn().accept(this); + if (!value.isNone()) { + condition.getResult().ifPresent(res -> scope.insert(res, value)); + } + return value; + } +} diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/eval/Scope.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/eval/Scope.java new file mode 100644 index 00000000000..42241416a53 --- /dev/null +++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/eval/Scope.java @@ -0,0 +1,124 @@ +/* + * 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.language.eval; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.rulesengine.language.error.InnerParseError; +import software.amazon.smithy.rulesengine.language.syntax.Identifier; +import software.amazon.smithy.rulesengine.language.syntax.expr.Reference; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * Scope is a stack for tracking facts for named values of type T. + * + * @param The type of values in scope. + */ +@SmithyUnstableApi +public final class Scope { + private final Deque> scope; + + public Scope() { + this.scope = new ArrayDeque<>(); + this.scope.push(new ScopeLayer<>()); + } + + public static Scope fromNode(Node node) { + ObjectNode objectNode = node.expectObjectNode("scope must be loaded from object node"); + Scope scope = new Scope<>(); + objectNode.getMembers().forEach((member, value) -> scope.insert(member.getValue(), Value.fromNode(value))); + return scope; + } + + public void push() { + scope.push(new ScopeLayer<>()); + } + + public void pop() { + scope.pop(); + } + + public void insert(String name, T value) { + this.insert(Identifier.of(name), value); + } + + public void insert(Identifier name, T value) { + this.scope.getFirst().getTypes().put(name, value); + } + + public void setNonNull(Reference name) { + this.scope.getFirst().getNonNullRefs().add(name); + } + + public U inScope(Supplier func) { + this.push(); + try { + return func.get(); + } finally { + this.pop(); + } + } + + @Override + public String toString() { + HashMap toPrint = new HashMap<>(); + for (ScopeLayer layer : scope) { + toPrint.putAll(layer.getTypes()); + } + return toPrint.toString(); + } + + public boolean isNonNull(Reference reference) { + return scope.stream().anyMatch(s -> s.getNonNullRefs().contains(reference)); + } + + public T expectValue(Identifier name) throws InnerParseError { + for (ScopeLayer layer : scope) { + if (layer.getTypes().containsKey(name)) { + return layer.getTypes().get(name); + } + } + throw new InnerParseError(String.format("No field named %s", name)); + } + + public Optional> getDeclaration(Identifier name) { + for (ScopeLayer layer : scope) { + if (layer.getTypes().containsKey(name)) { + return Optional.of(layer.getTypes().entrySet().stream() + .filter(e -> e.getKey().equals(name)) + .collect(Collectors.toList()).get(0)); + } + } + return Optional.empty(); + + } + + public Optional getValue(Identifier name) { + for (ScopeLayer layer : scope) { + if (layer.getTypes().containsKey(name)) { + return Optional.of(layer.getTypes().get(name)); + } + } + return Optional.empty(); + } +} diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/eval/ScopeLayer.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/eval/ScopeLayer.java new file mode 100644 index 00000000000..dadd9c62911 --- /dev/null +++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/eval/ScopeLayer.java @@ -0,0 +1,73 @@ +/* + * 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.language.eval; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import software.amazon.smithy.rulesengine.language.syntax.Identifier; +import software.amazon.smithy.rulesengine.language.syntax.expr.Reference; +import software.amazon.smithy.utils.SmithyUnstableApi; + +@SmithyUnstableApi +final class ScopeLayer { + private final Map types; + private final Set nonNullReferences; + + ScopeLayer(HashMap types, Set nonNullReferences) { + this.types = types; + this.nonNullReferences = nonNullReferences; + } + + ScopeLayer() { + this(new HashMap<>(), new HashSet<>()); + } + + public Map getTypes() { + return types; + } + + public Set getNonNullRefs() { + return nonNullReferences; + } + + @Override + public int hashCode() { + return Objects.hash(types, nonNullReferences); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ScopeLayer scopeLayer = (ScopeLayer) o; + return types.equals(scopeLayer.types) && nonNullReferences.equals(scopeLayer.nonNullReferences); + } + + @Override + public String toString() { + return "ScopeLayer[" + + "types=" + types + ", " + + "facts=" + nonNullReferences + ']'; + } + +} diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/eval/TestEvaluator.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/eval/TestEvaluator.java new file mode 100644 index 00000000000..34078fbb05a --- /dev/null +++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/eval/TestEvaluator.java @@ -0,0 +1,95 @@ +/* + * 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.language.eval; + +import static software.amazon.smithy.rulesengine.language.util.StringUtils.indent; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; +import software.amazon.smithy.rulesengine.language.EndpointRuleSet; +import software.amazon.smithy.rulesengine.language.error.RuleError; +import software.amazon.smithy.rulesengine.language.syntax.Identifier; +import software.amazon.smithy.rulesengine.traits.EndpointTestCase; +import software.amazon.smithy.rulesengine.traits.EndpointTestExpectation; +import software.amazon.smithy.rulesengine.traits.ExpectedEndpoint; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * Provides facilities for evaluating an endpoint rule-set and tests. + */ +@SmithyUnstableApi +public final class TestEvaluator { + private TestEvaluator() { + } + + /** + * Evaluate the given rule-set and test case. Throws an exception in the event + * the test case does not pass. + * + * @param ruleset The rule-set to be tested. + * @param testCase The test case. + */ + public static void evaluate(EndpointRuleSet ruleset, EndpointTestCase testCase) { + Map map = new LinkedHashMap<>(); + testCase.getParams().getStringMap().forEach((s, node) -> { + map.put(Identifier.of(s), Value.fromNode(node)); + }); + Value got = RuleEvaluator.evaluate(ruleset, map); + RuleError.context( + String.format("while executing test case%s", Optional + .ofNullable(testCase.getDocumentation()) + .map(d -> " " + d) + .orElse("")), + testCase, + () -> evaluateExpectation(testCase.getExpect(), got) + ); + } + + private static void evaluateExpectation(EndpointTestExpectation want, Value got) { + if (!(want.getEndpoint().isPresent() || want.getError().isPresent())) { + throw new RuntimeException("Unhandled endpoint test case want."); + } + if (want.getEndpoint().isPresent()) { + ExpectedEndpoint wantEndpoint = want.getEndpoint().get(); + + Value.Endpoint.Builder builder = Value.Endpoint.builder() + .url(wantEndpoint.getUrl()) + .headers(wantEndpoint.getHeaders()); + + wantEndpoint.getProperties().forEach((s, node) -> { + builder.addProperty(s, Value.fromNode(node)); + }); + + Value.Endpoint wantValue = builder.build(); + + if (!got.expectEndpoint().equals(wantValue)) { + throw new AssertionError( + String.format("Expected endpoint:%n%s but got:%n%s (generated by %s)", + indent(wantEndpoint.toString(), 2), + indent(got.toString(), 2), + wantEndpoint.getSourceLocation())); + } + } else { + String wantError = want.getError().get(); + RuleError.context("While checking endpoint test (expecting an error)", () -> { + if (!got.expectString().equals(wantError)) { + throw new AssertionError(String.format("Expected error `%s` but got `%s`", wantError, got)); + } + }); + } + } +} diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/eval/Type.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/eval/Type.java new file mode 100644 index 00000000000..ee4542f7f04 --- /dev/null +++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/eval/Type.java @@ -0,0 +1,435 @@ +/* + * 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.language.eval; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import software.amazon.smithy.rulesengine.language.error.InnerParseError; +import software.amazon.smithy.rulesengine.language.syntax.Identifier; +import software.amazon.smithy.utils.SmithyUnstableApi; + +@SmithyUnstableApi +public interface Type { + static String string() { + return new String(); + } + + static Endpoint endpoint() { + return new Endpoint(); + } + + static Type empty() { + return new Type.Empty(); + } + + static Array array(Type inner) { + return new Type.Array(inner); + } + + static Record record(Map inner) { + return new Record(inner); + } + + static Type integer() { + return new Integer(); + } + + static Option optional(Type t) { + return new Option(t); + } + + static Bool bool() { + return new Bool(); + } + + default String expectString() throws InnerParseError { + throw new InnerParseError("Expected string but found " + this); + } + + default Record expectObject(java.lang.String message) throws InnerParseError { + throw new InnerParseError(java.lang.String.format("Expected record but found %s%n == hint: %s", this, message)); + } + + default Bool expectBool() throws InnerParseError { + throw new InnerParseError("Expected boolean but found " + this); + } + + default Integer expectInt() throws InnerParseError { + throw new InnerParseError("Expected int but found " + this); + } + + default Option expectOptional() throws InnerParseError { + throw new InnerParseError("Expected optional but found " + this); + } + + default Array expectArray() throws InnerParseError { + throw new InnerParseError("Expected array but found " + this); + } + + default boolean isA(Type t) { + if (t.equals(new Type.Any())) { + return true; + } + return t.equals(this); + } + + /** + * When used in the context of a condition, the condition can only match if the value was truthful. This means + * that a certain expression can be a different type, for example, {@code Option} will become {@code T}. + * + * @return The type, given that it has been proven truthy + */ + default Type provenTruthy() { + return this; + } + + final class Integer implements Type { + @Override + public int hashCode() { + return 2; + } + + @Override + public boolean equals(Object obj) { + return obj == this || obj != null && obj.getClass() == this.getClass(); + } + + @Override + public java.lang.String toString() { + return "Int"; + } + + @Override + public Integer expectInt() { + return this; + } + } + + final class Any implements Type { + public Any() { + } + + @Override + public boolean isA(Type t) { + return true; + } + + @Override + public int hashCode() { + return 1; + } + + @Override + public boolean equals(Object obj) { + return obj == this || obj != null && obj.getClass() == this.getClass(); + } + + @Override + public java.lang.String toString() { + return "Any[]"; + } + + } + + final class Empty implements Type { + public Empty() { + } + + @Override + public int hashCode() { + return 1; + } + + @Override + public boolean equals(Object obj) { + return obj == this || obj != null && obj.getClass() == this.getClass(); + } + + @Override + public java.lang.String toString() { + return "Empty[]"; + } + + } + + final class Endpoint implements Type { + public Endpoint() { + } + + @Override + public int hashCode() { + return 1; + } + + @Override + public boolean equals(Object obj) { + return obj == this || obj != null && obj.getClass() == this.getClass(); + } + + @Override + public java.lang.String toString() { + return "Endpoint[]"; + } + + } + + final class String implements Type { + public String() { + } + + @Override + public String expectString() { + return this; + } + + @Override + public int hashCode() { + return 1; + } + + @Override + public boolean equals(Object obj) { + return obj == this || obj != null && obj.getClass() == this.getClass(); + } + + @Override + public java.lang.String toString() { + return "String"; + } + + } + + final class Bool implements Type { + public Bool() { + } + + public Bool expectBool() { + return this; + } + + @Override + public int hashCode() { + return 1; + } + + @Override + public boolean equals(Object obj) { + return obj == this || obj != null && obj.getClass() == this.getClass(); + } + + @Override + public java.lang.String toString() { + return "Bool"; + } + + } + + final class Option implements Type { + private final Type inner; + + public Option(Type inner) { + this.inner = inner; + } + + @Override + public String expectString() throws InnerParseError { + throw new InnerParseError(java.lang.String + .format("Expected string but found %s. hint: use `assign` in a condition " + + "or `isSet` to prove that this value is non-null", this)); + } + + @Override + public Bool expectBool() throws InnerParseError { + throw new InnerParseError(java.lang.String + .format("Expected boolean but found %s. hint: use `isSet` to convert " + + "Option to bool", this)); + } + + @Override + public Option expectOptional() throws InnerParseError { + return this; + } + + @Override + public boolean isA(Type t) { + if (!(t instanceof Option)) { + return false; + } + return ((Option) t).inner.isA(inner); + } + + @Override + public Type provenTruthy() { + return inner; + } + + public Type inner() { + return inner; + } + + @Override + public int hashCode() { + return Objects.hash(inner); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj == null || obj.getClass() != this.getClass()) { + return false; + } + Option that = (Option) obj; + return Objects.equals(this.inner, that.inner); + } + + @Override + public java.lang.String toString() { + return java.lang.String.format("Option<%s>", inner); + } + + } + + final class Tuple implements Type { + private final List members; + + public Tuple(List members) { + this.members = members; + } + + public List members() { + return members; + } + + @Override + public int hashCode() { + return Objects.hash(members); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj == null || obj.getClass() != this.getClass()) { + return false; + } + Tuple that = (Tuple) obj; + return Objects.equals(this.members, that.members); + } + + @Override + public java.lang.String toString() { + return this.members.toString(); + } + + } + + final class Array implements Type { + private final Type member; + + public Array(Type member) { + this.member = member; + } + + public Type getMember() { + return member; + } + + @Override + public Array expectArray() throws InnerParseError { + return this; + } + + public Type member() { + return member; + } + + @Override + public int hashCode() { + return Objects.hash(member); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj == null || obj.getClass() != this.getClass()) { + return false; + } + Array that = (Array) obj; + return Objects.equals(this.member, that.member); + } + + @Override + public java.lang.String toString() { + return java.lang.String.format("[%s]", this.member); + } + + } + + final class Record implements Type { + private final Map shape; + + public Record(Map shape) { + this.shape = shape; + } + + @Override + public Record expectObject(java.lang.String message) { + return this; + } + + public Optional get(Identifier name) { + if (shape.containsKey(name)) { + return Optional.of(shape.get(name)); + } else { + return Optional.empty(); + } + } + + public Map shape() { + return shape; + } + + @Override + public int hashCode() { + return Objects.hash(shape); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj == null || obj.getClass() != this.getClass()) { + return false; + } + Record that = (Record) obj; + return Objects.equals(this.shape, that.shape); + } + + @Override + public java.lang.String toString() { + return shape.toString(); + } + + } +} diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/eval/TypeCheck.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/eval/TypeCheck.java new file mode 100644 index 00000000000..b044c0ecf94 --- /dev/null +++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/eval/TypeCheck.java @@ -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.language.eval; + +import software.amazon.smithy.utils.SmithyUnstableApi; + +/* + * TypeCheck provides an interface for determining whether the given types within scope + * satisfy the associated constraints. + */ +@SmithyUnstableApi +public interface TypeCheck { + Type typeCheck(Scope scope); +} diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/eval/Value.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/eval/Value.java new file mode 100644 index 00000000000..6617c00e8c0 --- /dev/null +++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/eval/Value.java @@ -0,0 +1,618 @@ +/* + * 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.language.eval; + +import static software.amazon.smithy.rulesengine.language.util.StringUtils.indent; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.stream.Collectors; +import software.amazon.smithy.model.FromSourceLocation; +import software.amazon.smithy.model.SourceException; +import software.amazon.smithy.model.SourceLocation; +import software.amazon.smithy.model.node.ArrayNode; +import software.amazon.smithy.model.node.BooleanNode; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.NodeVisitor; +import software.amazon.smithy.model.node.NullNode; +import software.amazon.smithy.model.node.NumberNode; +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.language.syntax.Identifier; +import software.amazon.smithy.rulesengine.language.util.SourceLocationTrackingBuilder; +import software.amazon.smithy.rulesengine.language.util.SourceLocationUtils; +import software.amazon.smithy.utils.BuilderRef; +import software.amazon.smithy.utils.SmithyBuilder; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * An abstract representing a typed value. + */ +@SmithyUnstableApi +public abstract class Value implements FromSourceLocation, ToNode { + private SourceLocation sourceLocation; + + public Value(SourceLocation sourceLocation) { + this.sourceLocation = sourceLocation; + } + + public static Value fromNode(Node source) { + Value value = source.accept(new NodeVisitor() { + @Override + public Value arrayNode(ArrayNode node) { + return new Array(node.getElements().stream().map(Value::fromNode).collect(Collectors.toList())); + } + + @Override + public Value booleanNode(BooleanNode node) { + return bool(node.getValue()); + } + + @Override + public Value nullNode(NullNode node) { + throw new RuntimeException("null cannot be used as literal"); + } + + @Override + public Value numberNode(NumberNode node) { + if (!node.isNaturalNumber()) { + throw new RuntimeException("only integers >=0 are supported"); + } + return Value.integer(node.getValue().intValue()); + } + + @Override + public Value objectNode(ObjectNode node) { + HashMap out = new HashMap<>(); + node.getMembers().forEach((name, member) -> out.put(Identifier.of(name), Value.fromNode(member))); + return Value.record(out); + } + + @Override + public Value stringNode(StringNode node) { + return Value.string(node.getValue()); + } + }); + value.sourceLocation = source.getSourceLocation(); + return value; + } + + public static Endpoint endpointFromNode(Node source) { + Endpoint ep = Endpoint.fromNode(source); + ((Value) ep).sourceLocation = source.getSourceLocation(); + return ep; + } + + public static Value none() { + return new None(); + } + + public static String string(java.lang.String value) { + return new String(value); + } + + public static Record record(Map value) { + return new Record(value); + } + + public static Bool bool(boolean value) { + return new Bool(value); + } + + public static Array array(List value) { + return new Array(value); + } + + public static Integer integer(int value) { + return new Integer(value); + } + + public abstract Type type(); + + public java.lang.String expectString() { + throw new RuntimeException("Expected string but was: " + this); + } + + public boolean expectBool() { + throw new RuntimeException("Expected bool but was: " + this); + } + + @Override + public SourceLocation getSourceLocation() { + return Optional.ofNullable(sourceLocation).orElse(SourceLocation.none()); + } + + public Record expectRecord() { + throw new RuntimeException("Expected object but was: " + this); + } + + public boolean isNone() { + return false; + } + + public Endpoint expectEndpoint() { + throw new RuntimeException("Expected endpoint, found " + this); + } + + public Array expectArray() { + throw new RuntimeException("Expected array, found " + this); + } + + public int expectInteger() { + throw new RuntimeException("Expected int, found " + this); + } + + public static final class Integer extends Value { + private final int value; + + private Integer(int value) { + super(SourceLocation.none()); + this.value = value; + } + + @Override + public Type type() { + return Type.integer(); + } + + @Override + public int expectInteger() { + return value; + } + + @Override + public Node toNode() { + return Node.from(value); + } + } + + public static final class String extends Value { + private final java.lang.String value; + + private String(java.lang.String value) { + super(SourceLocation.none()); + this.value = value; + } + + @Override + public Type type() { + return Type.string(); + } + + @Override + public java.lang.String expectString() { + return value(); + } + + public java.lang.String value() { + return value; + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + String string = (String) o; + return value.equals(string.value); + } + + @Override + public java.lang.String toString() { + return value; + } + + @Override + public Node toNode() { + return StringNode.from(value); + } + } + + public static final class Bool extends Value { + + private final boolean value; + + private Bool(boolean value) { + super(SourceLocation.none()); + this.value = value; + } + + @Override + public Type type() { + return Type.bool(); + } + + @Override + public boolean expectBool() { + return value(); + } + + private boolean value() { + return this.value; + } + + @Override + public Node toNode() { + return BooleanNode.from(value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Bool bool = (Bool) o; + + return value == bool.value; + } + + @Override + public java.lang.String toString() { + return java.lang.String.valueOf(value); + } + } + + public static final class Record extends Value { + private final Map value; + + private Record(Map value) { + super(SourceLocation.none()); + this.value = value; + } + + @Override + public Type type() { + Map type = value.entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, value -> value.getValue().type())); + return new Type.Record(type); + } + + @Override + public Record expectRecord() { + return this; + } + + public Value get(java.lang.String key) { + return get(Identifier.of(key)); + } + + public Value get(Identifier key) { + return this.value.get(key); + } + + public void forEach(BiConsumer fn) { + value.forEach(fn); + } + + @Override + public Node toNode() { + ObjectNode.Builder builder = ObjectNode.builder(); + value.forEach((k, v) -> builder.withMember(k.getName(), v)); + return builder.build(); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Record record = (Record) o; + return Objects.equals(value, record.value); + } + + @Override + public java.lang.String toString() { + return value.toString(); + } + + public Map getValue() { + return this.value; + } + } + + public static final class Array extends Value { + private final List inner; + + private Array(List values) { + super(SourceLocation.none()); + this.inner = values; + } + + public List getValues() { + return inner; + } + + @Override + public Type type() { + if (inner.isEmpty()) { + return Type.array(Type.empty()); + } else { + Type first = inner.get(0).type(); + if (inner.stream().allMatch(item -> item.type() == first)) { + return Type.array(first); + } else { + throw new SourceException("An array cannot contain different types", this); + } + } + } + + @Override + public Array expectArray() { + return this; + } + + public Value get(int idx) { + if (this.inner.size() > idx) { + return this.inner.get(idx); + } else { + return new Value.None(); + } + } + + @Override + public Node toNode() { + return inner.stream() + .map(ToNode::toNode) + .collect(ArrayNode.collect()); + } + + @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; + } + Array array = (Array) o; + return inner.equals(array.inner); + } + + @Override + public java.lang.String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("["); + sb.append(inner.stream().map(Object::toString).collect(Collectors.joining(", "))); + sb.append("]"); + return sb.toString(); + } + } + + public static final class None extends Value { + + public None() { + super(SourceLocation.none()); + } + + @Override + public Type type() { + return Type.empty(); + } + + @Override + public boolean isNone() { + return true; + } + + @Override + public Node toNode() { + return Node.nullNode(); + } + } + + public static final class Endpoint extends Value { + private final java.lang.String url; + private final Map properties; + private final Map> headers; + + private Endpoint(Builder builder) { + super(builder.getSourceLocation()); + this.url = SmithyBuilder.requiredState("url", builder.url); + this.properties = builder.properties.copy(); + this.headers = builder.headers.copy(); + } + + public static Endpoint fromNode(Node node) { + Builder builder = new Builder(node); + ObjectNode on = node.expectObjectNode("endpoints are object nodes"); + on.expectNoAdditionalProperties(Arrays.asList("properties", "url", "headers")); + builder.url(on.expectStringMember("url").getValue()); + on.getObjectMember("properties").ifPresent(props -> { + props.getMembers().forEach((k, v) -> { + builder.addProperty(k.getValue(), Value.fromNode(v)); + }); + + }); + + on.getObjectMember("headers").ifPresent(headers -> headers.getMembers().forEach(((key, value) -> { + java.lang.String name = key.getValue(); + value.expectArrayNode("Header values must be an array").getElements() + .forEach(e -> builder.addHeader(name, e.expectStringNode().getValue())); + }))); + return builder.build(); + } + + public static Builder builder() { + return new Builder(SourceLocationUtils.javaLocation()); + } + + @Override + public Node toNode() { + return ObjectNode.builder() + .withMember("url", url) + .withMember("properties", propertiesNode()) + .withMember("headers", headersNode()) + .build(); + } + + private Node propertiesNode() { + ObjectNode.Builder b = ObjectNode.builder(); + properties.forEach(b::withMember); + return b.build(); + } + + private Node headersNode() { + ObjectNode.Builder builder = ObjectNode.builder(); + + headers.forEach((k, v) -> { + ArrayNode valuesNode = v.stream().map(StringNode::from).collect(ArrayNode.collect()); + builder.withMember(k, valuesNode); + }); + + return builder.build(); + } + + public Map getProperties() { + return properties; + } + + public java.lang.String getUrl() { + return url; + } + + public Map> getHeaders() { + return headers; + } + + @Override + public Type type() { + return Type.endpoint(); + } + + @Override + public Endpoint expectEndpoint() { + return this; + } + + @Override + public int hashCode() { + return Objects.hash(url, properties, headers); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Endpoint endpoint = (Endpoint) o; + return url.equals(endpoint.url) + && properties.equals(endpoint.properties) + && headers.equals(endpoint.headers); + } + + @Override + public java.lang.String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("url: ").append(url).append("\n"); + sb.append("properties:\n"); + sb.append(indent(properties.toString(), 2)); + // todo(rcoh) + if (!headers.isEmpty()) { + headers.forEach((key, value) -> { + sb.append(indent(java.lang.String.format("%s:%s", key, value), 2)); + }); + } + return sb.toString(); + } + + public static final class Builder extends SourceLocationTrackingBuilder { + private final BuilderRef> properties = BuilderRef.forOrderedMap(); + private final BuilderRef>> headers = + BuilderRef.forOrderedMap(); + private java.lang.String url; + + public Builder(FromSourceLocation sourceLocation) { + super(sourceLocation); + } + + public Builder url(java.lang.String url) { + this.url = url; + return this; + } + + public Builder headers(Map> headers) { + this.headers.clear(); + this.headers.get().putAll(headers); + return this; + } + + public Builder addHeader(java.lang.String name, java.lang.String value) { + List values = this.headers.get().computeIfAbsent(name, (k) -> new ArrayList<>()); + values.add(value); + return this; + } + + public Builder properties(Map properties) { + this.properties.clear(); + this.properties.get().putAll(properties); + return this; + } + + public Builder addProperty(java.lang.String value, Value fromNode) { + this.properties.get().put(value, fromNode); + return this; + } + + @Override + public Endpoint build() { + return new Endpoint(this); + } + + } + + } +} diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/impl/AwsArn.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/impl/AwsArn.java new file mode 100644 index 00000000000..3d1cbb347d9 --- /dev/null +++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/impl/AwsArn.java @@ -0,0 +1,189 @@ +/* + * 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.language.impl; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; +import software.amazon.smithy.utils.BuilderRef; +import software.amazon.smithy.utils.SmithyBuilder; +import software.amazon.smithy.utils.SmithyUnstableApi; +import software.amazon.smithy.utils.ToSmithyBuilder; + +/** + * An AWS ARN. + */ +@SmithyUnstableApi +public final class AwsArn implements ToSmithyBuilder { + private final String partition; + private final String service; + private final String region; + private final String accountId; + private final List resource; + + private AwsArn(Builder builder) { + this.partition = SmithyBuilder.requiredState("partition", builder.partition); + this.service = SmithyBuilder.requiredState("service", builder.service); + this.region = SmithyBuilder.requiredState("region", builder.region); + this.accountId = SmithyBuilder.requiredState("accountId", builder.accountId); + this.resource = builder.resource.copy(); + } + + /** + * Parses and returns the ARN components if the provided value is a valid AWS ARN. + * + * @param arn the value to parse. + * @return the optional ARN. + */ + public static Optional parse(String arn) { + String[] base = arn.split(":", 6); + if (base.length != 6) { + return Optional.empty(); + } + // service, resource and `arn` may not be null + if (!base[0].equals("arn")) { + return Optional.empty(); + } + if (base[1].isEmpty() || base[2].isEmpty()) { + return Optional.empty(); + } + if (base[5].isEmpty()) { + return Optional.empty(); + } + return Optional.of(builder() + .partition(base[1]) + .service(base[2]) + .region(base[3]) + .accountId(base[4]) + .resource(Arrays.stream(base[5].split("[:/]", -1)) + .collect(Collectors.toList())) + .build()); + } + + public static Builder builder() { + return new Builder(); + } + + public String partition() { + return partition; + } + + public String service() { + return service; + } + + public String region() { + return region; + } + + public String accountId() { + return accountId; + } + + public List resource() { + return resource; + } + + @Override + public int hashCode() { + return Objects.hash(partition, service, region, accountId, resource); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + AwsArn awsArn = (AwsArn) o; + return partition.equals(awsArn.partition) && service.equals(awsArn.service) && region.equals(awsArn.region) + && accountId.equals(awsArn.accountId) && resource.equals(awsArn.resource); + } + + @Override + public String toString() { + StringBuilder resource = new StringBuilder(); + this.resource().forEach(resource::append); + + return "Arn[" + + "partition=" + + partition + ", " + + "service=" + + service + ", " + + "region=" + + region + ", " + + "accountId=" + + accountId + ", " + + "resource=" + + resource + ']'; + } + + @Override + public Builder toBuilder() { + return builder() + .partition(partition) + .service(service) + .region(region) + .accountId(accountId) + .resource(resource); + } + + public static final class Builder implements SmithyBuilder { + private final BuilderRef> resource = BuilderRef.forList(); + private String partition; + private String service; + private String region; + private String accountId; + + private Builder() { + } + + public Builder partition(String partition) { + this.partition = partition; + return this; + } + + public Builder service(String service) { + this.service = service; + return this; + } + + public Builder region(String region) { + this.region = region; + return this; + } + + public Builder accountId(String accountId) { + this.accountId = accountId; + return this; + } + + public Builder resource(List resource) { + this.resource.clear(); + this.resource.get().addAll(resource); + return this; + } + + @Override + public AwsArn build() { + return new AwsArn(this); + } + } +} diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/model/Partition.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/model/Partition.java new file mode 100644 index 00000000000..6648961c9e1 --- /dev/null +++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/model/Partition.java @@ -0,0 +1,167 @@ +/* + * 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.language.model; + +import java.util.Arrays; +import java.util.Map; +import java.util.Objects; +import software.amazon.smithy.model.FromSourceLocation; +import software.amazon.smithy.model.SourceLocation; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.rulesengine.language.util.SourceLocationTrackingBuilder; +import software.amazon.smithy.utils.BuilderRef; +import software.amazon.smithy.utils.SmithyBuilder; +import software.amazon.smithy.utils.SmithyUnstableApi; +import software.amazon.smithy.utils.ToSmithyBuilder; + +/** + * Describes an AWS partition, it's regions, and the outputs to be provided by the rule-set aws.partition function. + */ +@SmithyUnstableApi +public final class Partition implements ToSmithyBuilder, FromSourceLocation { + private static final String ID = "id"; + private static final String REGION_REGEX = "regionRegex"; + private static final String REGIONS = "regions"; + private static final String OUTPUTS = "outputs"; + + private final String id; + private final String regionRegex; + private final Map regions; + private final PartitionOutputs partitionOutputs; + + private final SourceLocation sourceLocation; + + private Partition(Builder builder) { + this.id = builder.id; + this.regionRegex = builder.regionRegex; + this.regions = builder.regions.copy(); + this.partitionOutputs = builder.partitionOutputs; + this.sourceLocation = builder.getSourceLocation(); + } + + public static Builder builder() { + return new Builder(SourceLocation.none()); + } + + public static Partition fromNode(Node node) { + Builder b = new Builder(node); + + ObjectNode objNode = node.expectObjectNode(); + + objNode.expectObjectNode() + .expectNoAdditionalProperties(Arrays.asList(ID, REGION_REGEX, REGIONS, OUTPUTS)); + + objNode.getStringMember(ID).ifPresent(n -> b.id(n.toString())); + objNode.getStringMember(REGION_REGEX).ifPresent(n -> b.regionRegex(n.toString())); + + objNode.getObjectMember(REGIONS).ifPresent(regionsNode -> + regionsNode.getMembers().forEach((k, v) -> b.putRegion(k.toString(), RegionOverride.fromNode(v)))); + + objNode.getObjectMember(OUTPUTS).ifPresent(outputsNode -> b.outputs(PartitionOutputs.fromNode(outputsNode))); + + return b.build(); + } + + public String id() { + return id; + } + + public String regionRegex() { + return regionRegex; + } + + public Map regions() { + return regions; + } + + public PartitionOutputs getOutputs() { + return partitionOutputs; + } + + @Override + public SourceLocation getSourceLocation() { + return sourceLocation; + } + + @Override + public SmithyBuilder toBuilder() { + return new Builder(getSourceLocation()) + .id(id) + .regionRegex(regionRegex); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Partition partition = (Partition) o; + return Objects.equals(id, partition.id) && Objects.equals(regionRegex, partition.regionRegex) + && Objects.equals(regions, partition.regions) + && Objects.equals(partitionOutputs, partition.partitionOutputs); + } + + @Override + public int hashCode() { + return Objects.hash(id, regionRegex, regions, partitionOutputs); + } + + public static class Builder extends SourceLocationTrackingBuilder { + private String id; + private String regionRegex; + private final BuilderRef> regions = BuilderRef.forOrderedMap(); + private PartitionOutputs partitionOutputs; + + public Builder(FromSourceLocation sourceLocation) { + super(sourceLocation); + } + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder regionRegex(String regionRegex) { + this.regionRegex = regionRegex; + return this; + } + + public Builder regions(Map regions) { + this.regions.clear(); + this.regions.get().putAll(regions); + return this; + } + + public Builder putRegion(String name, RegionOverride regionOverride) { + this.regions.get().put(name, regionOverride); + return this; + } + + public Builder outputs(PartitionOutputs partitionOutputs) { + this.partitionOutputs = partitionOutputs; + return this; + } + + @Override + public Partition build() { + return new Partition(this); + } + } +} diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/model/PartitionOutputs.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/model/PartitionOutputs.java new file mode 100644 index 00000000000..e9ddf5387e8 --- /dev/null +++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/model/PartitionOutputs.java @@ -0,0 +1,159 @@ +/* + * 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.language.model; + +import java.util.Arrays; +import java.util.Objects; +import software.amazon.smithy.model.FromSourceLocation; +import software.amazon.smithy.model.SourceLocation; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.rulesengine.language.util.SourceLocationTrackingBuilder; +import software.amazon.smithy.utils.SmithyBuilder; +import software.amazon.smithy.utils.SmithyUnstableApi; +import software.amazon.smithy.utils.ToSmithyBuilder; + +/** + * The outputs to be provided by the rule-set aws.partition function. + */ +@SmithyUnstableApi +public final class PartitionOutputs implements ToSmithyBuilder, FromSourceLocation { + private static final String DNS_SUFFIX = "dnsSuffix"; + private static final String DUAL_STACK_DNS_SUFFIX = "dualStackDnsSuffix"; + private static final String SUPPORTS_FIPS = "supportsFIPS"; + private static final String SUPPORTS_DUAL_STACK = "supportsDualStack"; + private static final String NAME = "name"; + + private final String dnsSuffix; + private final String dualStackDnsSuffix; + private final boolean supportsFips; + private final boolean supportsDualStack; + + private final SourceLocation sourceLocation; + + private PartitionOutputs(Builder builder) { + this.dnsSuffix = builder.dnsSuffix; + this.dualStackDnsSuffix = builder.dualStackDnsSuffix; + this.supportsFips = builder.supportsFips; + this.supportsDualStack = builder.supportsDualStack; + this.sourceLocation = builder.getSourceLocation(); + } + + public static Builder builder() { + return new Builder(SourceLocation.none()); + } + + public static PartitionOutputs fromNode(Node node) { + ObjectNode objNode = node.expectObjectNode(); + + objNode.expectNoAdditionalProperties(Arrays.asList( + NAME, DNS_SUFFIX, DUAL_STACK_DNS_SUFFIX, SUPPORTS_FIPS, SUPPORTS_DUAL_STACK)); + + Builder b = new Builder(node); + + objNode.getStringMember(DNS_SUFFIX).ifPresent(n -> b.dnsSuffix(n.getValue())); + objNode.getStringMember(DUAL_STACK_DNS_SUFFIX).ifPresent(n -> b.dualStackDnsSuffix(n.getValue())); + objNode.getBooleanMember(SUPPORTS_FIPS).ifPresent(n -> b.supportsFips(n.getValue())); + objNode.getBooleanMember(SUPPORTS_DUAL_STACK).ifPresent(n -> b.supportsDualStack(n.getValue())); + + return b.build(); + } + + public String dnsSuffix() { + return dnsSuffix; + } + + public String dualStackDnsSuffix() { + return dualStackDnsSuffix; + } + + public boolean supportsFips() { + return supportsFips; + } + + public boolean supportsDualStack() { + return supportsDualStack; + } + + @Override + public SourceLocation getSourceLocation() { + return sourceLocation; + } + + @Override + public SmithyBuilder toBuilder() { + return new Builder(getSourceLocation()) + .dnsSuffix(dnsSuffix) + .dualStackDnsSuffix(dualStackDnsSuffix) + .supportsFips(supportsFips) + .supportsDualStack(supportsDualStack); + } + + @Override + public int hashCode() { + return Objects.hash(dnsSuffix, dualStackDnsSuffix, supportsFips, supportsDualStack); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PartitionOutputs partitionOutputs = (PartitionOutputs) o; + return supportsFips == partitionOutputs.supportsFips && supportsDualStack == partitionOutputs.supportsDualStack + && Objects.equals(dnsSuffix, partitionOutputs.dnsSuffix) + && Objects.equals(dualStackDnsSuffix, partitionOutputs.dualStackDnsSuffix); + } + + public static class Builder extends SourceLocationTrackingBuilder { + private String dnsSuffix; + private String dualStackDnsSuffix; + private boolean supportsFips; + private boolean supportsDualStack; + + public Builder(FromSourceLocation sourceLocation) { + super(sourceLocation); + } + + public Builder dnsSuffix(String dnsSuffix) { + this.dnsSuffix = dnsSuffix; + return this; + } + + public Builder dualStackDnsSuffix(String dualStackDnsSuffix) { + this.dualStackDnsSuffix = dualStackDnsSuffix; + return this; + } + + public Builder supportsFips(boolean supportsFips) { + this.supportsFips = supportsFips; + return this; + } + + public Builder supportsDualStack(boolean supportsDualStack) { + this.supportsDualStack = supportsDualStack; + return this; + } + + @Override + public PartitionOutputs build() { + return new PartitionOutputs(this); + } + } +} diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/model/Partitions.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/model/Partitions.java new file mode 100644 index 00000000000..e7e073cfa54 --- /dev/null +++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/model/Partitions.java @@ -0,0 +1,142 @@ +/* + * 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.language.model; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import software.amazon.smithy.model.FromSourceLocation; +import software.amazon.smithy.model.SourceLocation; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.rulesengine.language.util.SourceLocationTrackingBuilder; +import software.amazon.smithy.utils.BuilderRef; +import software.amazon.smithy.utils.SmithyBuilder; +import software.amazon.smithy.utils.SmithyUnstableApi; +import software.amazon.smithy.utils.ToSmithyBuilder; + +/** + * A model for defining the set of partitions that are used by the rule-set aws.partition function. + */ +@SmithyUnstableApi +public final class Partitions implements ToSmithyBuilder, FromSourceLocation { + private static final String VERSION = "version"; + private static final String PARTITIONS = "partitions"; + private final String version; + private final List partitions; + private final SourceLocation sourceLocation; + + private Partitions(Builder builder) { + this.version = builder.version; + this.partitions = builder.partitions.copy(); + this.sourceLocation = builder.getSourceLocation(); + } + + public static Partitions fromNode(Node node) { + ObjectNode objNode = node.expectObjectNode(); + + Builder b = new Builder(node); + + objNode.expectNoAdditionalProperties(Arrays.asList(VERSION, PARTITIONS)); + + objNode.getStringMember(VERSION).ifPresent(v -> b.version(v.toString())); + objNode.getArrayMember(PARTITIONS).ifPresent(partitionsNode -> + partitionsNode.forEach(partNode -> + b.addPartition(Partition.fromNode(partNode)))); + + return b.build(); + } + + public static Builder builder() { + return new Builder(SourceLocation.none()); + } + + @Override + public int hashCode() { + return Objects.hash(version, partitions, sourceLocation); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Partitions that = (Partitions) o; + return version.equals(that.version) && partitions.equals(that.partitions); + } + + @Override + public String toString() { + return "Partitions{" + + "version='" + version + '\'' + + ", partitions=" + partitions + + ", sourceLocation=" + sourceLocation + + '}'; + } + + public String version() { + return version; + } + + public List partitions() { + return partitions; + } + + @Override + public SourceLocation getSourceLocation() { + return sourceLocation; + } + + @Override + public SmithyBuilder toBuilder() { + return new Builder(getSourceLocation()) + .version(version) + .partitions(partitions); + } + + public static class Builder extends SourceLocationTrackingBuilder { + private String version; + private final BuilderRef> partitions = BuilderRef.forList(); + + public Builder(FromSourceLocation sourceLocation) { + super(sourceLocation); + } + + public Builder version(String version) { + this.version = version; + return this; + } + + public Builder partitions(List partitions) { + this.partitions.clear(); + this.partitions.get().addAll(partitions); + return this; + } + + public Builder addPartition(Partition p) { + this.partitions.get().add(p); + return this; + } + + @Override + public Partitions build() { + return new Partitions(this); + } + } +} diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/model/RegionOverride.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/model/RegionOverride.java new file mode 100644 index 00000000000..60fac63e1db --- /dev/null +++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/model/RegionOverride.java @@ -0,0 +1,77 @@ +/* + * 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.language.model; + +import software.amazon.smithy.model.FromSourceLocation; +import software.amazon.smithy.model.SourceLocation; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.rulesengine.language.util.SourceLocationTrackingBuilder; +import software.amazon.smithy.utils.SmithyBuilder; +import software.amazon.smithy.utils.SmithyUnstableApi; +import software.amazon.smithy.utils.ToSmithyBuilder; + +/** + * Provides a facility for overriding a partition's regions. + */ +@SmithyUnstableApi +public final class RegionOverride implements ToSmithyBuilder, FromSourceLocation { + private final SourceLocation sourceLocation; + + private RegionOverride(Builder builder) { + this.sourceLocation = builder.getSourceLocation(); + } + + public static Builder builder() { + return new Builder(SourceLocation.none()); + } + + public static RegionOverride fromNode(Node node) { + Builder b = new Builder(node); + return b.build(); + } + + @Override + public SmithyBuilder toBuilder() { + return new Builder(getSourceLocation()); + } + + @Override + public SourceLocation getSourceLocation() { + return sourceLocation; + } + + @Override + public int hashCode() { + return 7; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof RegionOverride; + } + + public static class Builder extends SourceLocationTrackingBuilder { + + public Builder(FromSourceLocation sourceLocation) { + super(sourceLocation); + } + + @Override + public RegionOverride build() { + return new RegionOverride(this); + } + } +} diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/stdlib/AwsIsVirtualHostableS3Bucket.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/stdlib/AwsIsVirtualHostableS3Bucket.java new file mode 100644 index 00000000000..8150893b00a --- /dev/null +++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/stdlib/AwsIsVirtualHostableS3Bucket.java @@ -0,0 +1,68 @@ +/* + * 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.language.stdlib; + +import java.util.Arrays; +import java.util.List; +import software.amazon.smithy.rulesengine.language.eval.Type; +import software.amazon.smithy.rulesengine.language.eval.Value; +import software.amazon.smithy.rulesengine.language.syntax.expr.Expression; +import software.amazon.smithy.rulesengine.language.syntax.fn.Function; +import software.amazon.smithy.rulesengine.language.syntax.fn.FunctionDefinition; +import software.amazon.smithy.rulesengine.language.syntax.fn.LibraryFunction; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * An AWS rule-set function for determining whether a given string can be promoted to an S3 virtual bucket host label. + */ +@SmithyUnstableApi +public class AwsIsVirtualHostableS3Bucket extends FunctionDefinition { + public static final String ID = "aws.isVirtualHostableS3Bucket"; + + @Override + public String getId() { + return ID; + } + + @Override + public List getArguments() { + return Arrays.asList(Type.string(), Type.bool()); + } + + @Override + public Type getReturnType() { + return Type.bool(); + } + + @Override + public Value evaluate(List arguments) { + String hostLabel = arguments.get(0).expectString(); + boolean allowDots = arguments.get(1).expectBool(); + if (allowDots) { + return Value.bool( + hostLabel.matches("[a-z\\d][a-z\\d\\-.]{1,61}[a-z\\d]") + && !hostLabel.matches("(\\d+\\.){3}\\d+") // don't allow ip address + && !hostLabel.matches(".*[.-]{2}.*") // don't allow names like bucket-.name or bucket.-name + ); + } else { + return Value.bool(hostLabel.matches("[a-z\\d][a-z\\d\\-]{1,61}[a-z\\d]")); + } + } + + public static Function ofExpression(Expression input, boolean allowDots) { + return LibraryFunction.ofExpressions(new AwsIsVirtualHostableS3Bucket(), input, Expression.of(allowDots)); + } +} diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/stdlib/AwsPartition.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/stdlib/AwsPartition.java new file mode 100644 index 00000000000..ade1bc27892 --- /dev/null +++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/stdlib/AwsPartition.java @@ -0,0 +1,155 @@ +/* + * 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.language.stdlib; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.regex.Pattern; +import software.amazon.smithy.rulesengine.language.eval.Type; +import software.amazon.smithy.rulesengine.language.eval.Value; +import software.amazon.smithy.rulesengine.language.model.PartitionOutputs; +import software.amazon.smithy.rulesengine.language.model.Partitions; +import software.amazon.smithy.rulesengine.language.stdlib.partition.PartitionDataProvider; +import software.amazon.smithy.rulesengine.language.syntax.Identifier; +import software.amazon.smithy.rulesengine.language.syntax.expr.Expression; +import software.amazon.smithy.rulesengine.language.syntax.fn.Function; +import software.amazon.smithy.rulesengine.language.syntax.fn.FunctionDefinition; +import software.amazon.smithy.rulesengine.language.syntax.fn.FunctionNode; +import software.amazon.smithy.rulesengine.language.syntax.fn.LibraryFunction; +import software.amazon.smithy.rulesengine.language.util.LazyValue; +import software.amazon.smithy.utils.MapUtils; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * An AWS rule-set function for mapping a region string to a partition. + */ +@SmithyUnstableApi +public final class AwsPartition extends FunctionDefinition { + public static final String ID = "aws.partition"; + + public static final Identifier NAME = Identifier.of("name"); + public static final Identifier DNS_SUFFIX = Identifier.of("dnsSuffix"); + public static final Identifier DUAL_STACK_DNS_SUFFIX = Identifier.of("dualStackDnsSuffix"); + public static final Identifier SUPPORTS_FIPS = Identifier.of("supportsFIPS"); + public static final Identifier SUPPORTS_DUAL_STACK = Identifier.of("supportsDualStack"); + public static final Identifier INFERRED = Identifier.of("inferred"); + + private final LazyValue partitionData = LazyValue.builder() + .initializer(this::loadPartitionData) + .build(); + + @Override + public String getId() { + return ID; + } + + @Override + public List getArguments() { + return Collections.singletonList(Type.string()); + } + + @Override + public Type getReturnType() { + LinkedHashMap type = new LinkedHashMap<>(); + type.put(NAME, Type.string()); + type.put(DNS_SUFFIX, Type.string()); + type.put(DUAL_STACK_DNS_SUFFIX, Type.string()); + type.put(SUPPORTS_DUAL_STACK, Type.bool()); + type.put(SUPPORTS_FIPS, Type.bool()); + return Type.optional(new Type.Record(type)); + } + + @Override + public Value evaluate(List arguments) { + String regionName = arguments.get(0).expectString(); + + final PartitionData data = partitionData.value(); + + software.amazon.smithy.rulesengine.language.model.Partition matchedPartition; + boolean inferred = false; + + // Known region + matchedPartition = data.regionMap.get(regionName); + if (matchedPartition == null) { + // try matching on region name pattern + for (software.amazon.smithy.rulesengine.language.model.Partition p : data.partitions) { + Pattern regex = Pattern.compile(p.regionRegex()); + if (regex.matcher(regionName).matches()) { + matchedPartition = p; + inferred = true; + break; + } + } + } + + if (matchedPartition == null) { + matchedPartition = data.partitions.stream().filter(p -> p.id().equals("aws")).findFirst().get(); + } + + PartitionOutputs matchedPartitionOutputs = matchedPartition.getOutputs(); + return Value.record(MapUtils.of( + NAME, Value.string(matchedPartition.id()), + DNS_SUFFIX, Value.string(matchedPartitionOutputs.dnsSuffix()), + DUAL_STACK_DNS_SUFFIX, Value.string(matchedPartitionOutputs.dualStackDnsSuffix()), + SUPPORTS_FIPS, Value.bool(matchedPartitionOutputs.supportsFips()), + SUPPORTS_DUAL_STACK, Value.bool(matchedPartitionOutputs.supportsDualStack()), + INFERRED, Value.bool(inferred))); + } + + /** + * Constructs a function definition for resolving a string expression to a partition. + * + * @param expression expression to evaluate to a partition. + * @return the function representing the partition lookup. + */ + public static Function ofExpression(Expression expression) { + return new LibraryFunction(new AwsPartition(), FunctionNode.ofExpressions(ID, expression)); + } + + private PartitionData loadPartitionData() { + Iterator iter = ServiceLoader.load(PartitionDataProvider.class).iterator(); + if (!iter.hasNext()) { + throw new RuntimeException("Unable to locate partition data"); + } + + PartitionDataProvider provider = iter.next(); + + Partitions partitions = provider.loadPartitions(); + + PartitionData partitionData = new PartitionData(); + + partitions.partitions().forEach(part -> { + partitionData.partitions.add(part); + part.regions().forEach((name, override) -> { + partitionData.regionMap.put(name, part); + }); + }); + + return partitionData; + } + + private static class PartitionData { + private final List partitions = new ArrayList<>(); + private final Map regionMap = + new HashMap<>(); + } +} diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/stdlib/BooleanEquals.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/stdlib/BooleanEquals.java new file mode 100644 index 00000000000..bf5b8c944cf --- /dev/null +++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/stdlib/BooleanEquals.java @@ -0,0 +1,89 @@ +/* + * 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.language.stdlib; + +import java.util.Arrays; +import java.util.List; +import software.amazon.smithy.rulesengine.language.error.InnerParseError; +import software.amazon.smithy.rulesengine.language.eval.Scope; +import software.amazon.smithy.rulesengine.language.eval.Type; +import software.amazon.smithy.rulesengine.language.eval.Value; +import software.amazon.smithy.rulesengine.language.syntax.expr.Expression; +import software.amazon.smithy.rulesengine.language.syntax.fn.Function; +import software.amazon.smithy.rulesengine.language.syntax.fn.FunctionDefinition; +import software.amazon.smithy.rulesengine.language.syntax.fn.FunctionNode; +import software.amazon.smithy.rulesengine.language.syntax.fn.LibraryFunction; +import software.amazon.smithy.rulesengine.language.visit.ExpressionVisitor; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * Represents a two argument function that compares two expression for boolean equality. + */ +@SmithyUnstableApi +public final class BooleanEquals extends Function { + public static final String ID = "booleanEquals"; + private static final Definition DEFINITION = new Definition(); + + public BooleanEquals(FunctionNode functionNode) { + super(functionNode); + } + + /** + * Returns a BooleanEquals {@link Function} comparing the left and right expressions for equality. + * + * @param left the left hand-side expression. + * @param right the right hand-side expression. + * @return the function defining the BooleanEquals of the left and right expressions. + */ + public static Function ofExpressions(Expression left, Expression right) { + return LibraryFunction.ofExpressions(DEFINITION, left, right); + } + + @Override + public R accept(ExpressionVisitor visitor) { + return visitor.visitBoolEquals(functionNode.getArguments().get(0), functionNode.getArguments().get(1)); + } + + @Override + protected Type typeCheckLocal(Scope scope) throws InnerParseError { + LibraryFunction.checkTypeSignature(DEFINITION.getArguments(), functionNode.getArguments(), scope); + return DEFINITION.getReturnType(); + } + + static class Definition extends FunctionDefinition { + public static final String ID = BooleanEquals.ID; + + @Override + public String getId() { + return ID; + } + + @Override + public List getArguments() { + return Arrays.asList(Type.bool(), Type.bool()); + } + + @Override + public Type getReturnType() { + return Type.bool(); + } + + @Override + public Value evaluate(List arguments) { + return Value.bool(arguments.get(0).expectBool() == arguments.get(1).expectBool()); + } + } +} diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/stdlib/IsValidHostLabel.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/stdlib/IsValidHostLabel.java new file mode 100644 index 00000000000..21b3b291667 --- /dev/null +++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/stdlib/IsValidHostLabel.java @@ -0,0 +1,64 @@ +/* + * 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.language.stdlib; + +import java.util.Arrays; +import java.util.List; +import software.amazon.smithy.rulesengine.language.eval.Type; +import software.amazon.smithy.rulesengine.language.eval.Value; +import software.amazon.smithy.rulesengine.language.syntax.expr.Expression; +import software.amazon.smithy.rulesengine.language.syntax.fn.Function; +import software.amazon.smithy.rulesengine.language.syntax.fn.FunctionDefinition; +import software.amazon.smithy.rulesengine.language.syntax.fn.LibraryFunction; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * A rule-engine function for checking whether a string is a valid DNS host label. + */ +@SmithyUnstableApi +public final class IsValidHostLabel extends FunctionDefinition { + public static final String ID = "isValidHostLabel"; + + @Override + public String getId() { + return ID; + } + + @Override + public List getArguments() { + return Arrays.asList(Type.string(), Type.bool()); + } + + @Override + public Type getReturnType() { + return Type.bool(); + } + + @Override + public Value evaluate(List arguments) { + String hostLabel = arguments.get(0).expectString(); + boolean allowDots = arguments.get(1).expectBool(); + if (allowDots) { + return Value.bool(hostLabel.matches("[a-zA-Z\\d][a-zA-Z\\d\\-.]{0,62}")); + } else { + return Value.bool(hostLabel.matches("[a-zA-Z\\d][a-zA-Z\\d\\-]{0,62}")); + } + } + + public static Function ofExpression(Expression input, boolean allowDots) { + return LibraryFunction.ofExpressions(new IsValidHostLabel(), input, Expression.of(allowDots)); + } +} diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/stdlib/ParseArn.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/stdlib/ParseArn.java new file mode 100644 index 00000000000..209cdd21e37 --- /dev/null +++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/stdlib/ParseArn.java @@ -0,0 +1,87 @@ +/* + * 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.language.stdlib; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import software.amazon.smithy.rulesengine.language.eval.Type; +import software.amazon.smithy.rulesengine.language.eval.Value; +import software.amazon.smithy.rulesengine.language.impl.AwsArn; +import software.amazon.smithy.rulesengine.language.syntax.Identifier; +import software.amazon.smithy.rulesengine.language.syntax.expr.Expression; +import software.amazon.smithy.rulesengine.language.syntax.fn.Function; +import software.amazon.smithy.rulesengine.language.syntax.fn.FunctionDefinition; +import software.amazon.smithy.rulesengine.language.syntax.fn.LibraryFunction; +import software.amazon.smithy.utils.MapUtils; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * An aws rule-set function for parsing an AWS ARN into it's componenet parts. + */ +@SmithyUnstableApi +public final class ParseArn extends FunctionDefinition { + public static final String ID = "aws.parseArn"; + public static final Identifier PARTITION = Identifier.of("partition"); + public static final Identifier SERVICE = Identifier.of("service"); + public static final Identifier REGION = Identifier.of("region"); + public static final Identifier ACCOUNT_ID = Identifier.of("accountId"); + private static final Identifier RESOURCE_ID = Identifier.of("resourceId"); + + + @Override + public String getId() { + return ID; + } + + @Override + public List getArguments() { + return Collections.singletonList(Type.string()); + } + + @Override + public Type getReturnType() { + return Type.optional(new Type.Record(MapUtils.of( + PARTITION, Type.string(), + SERVICE, Type.string(), + REGION, Type.string(), + ACCOUNT_ID, Type.string(), + RESOURCE_ID, Type.array(Type.string()) + ))); + } + + @Override + public Value evaluate(List arguments) { + String value = arguments.get(0).expectString(); + Optional arnOpt = AwsArn.parse(value); + return arnOpt.map(awsArn -> + (Value) Value.record(MapUtils.of( + PARTITION, Value.string(awsArn.partition()), + SERVICE, Value.string(awsArn.service()), + REGION, Value.string(awsArn.region()), + ACCOUNT_ID, Value.string(awsArn.accountId()), + RESOURCE_ID, Value.array(awsArn.resource().stream() + .map(v -> (Value) Value.string(v)) + .collect(Collectors.toList())) + )) + ).orElse(new Value.None()); + } + + public static Function ofExpression(Expression expression) { + return LibraryFunction.ofExpressions(new ParseArn(), expression); + } +} diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/stdlib/ParseUrl.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/stdlib/ParseUrl.java new file mode 100644 index 00000000000..b4f39dce4a5 --- /dev/null +++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/stdlib/ParseUrl.java @@ -0,0 +1,129 @@ +/* + * 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.language.stdlib; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import software.amazon.smithy.rulesengine.language.eval.Type; +import software.amazon.smithy.rulesengine.language.eval.Value; +import software.amazon.smithy.rulesengine.language.syntax.Identifier; +import software.amazon.smithy.rulesengine.language.syntax.expr.Expression; +import software.amazon.smithy.rulesengine.language.syntax.fn.Function; +import software.amazon.smithy.rulesengine.language.syntax.fn.FunctionDefinition; +import software.amazon.smithy.rulesengine.language.syntax.fn.LibraryFunction; +import software.amazon.smithy.utils.MapUtils; +import software.amazon.smithy.utils.SmithyUnstableApi; +import software.amazon.smithy.utils.StringUtils; + +/** + * A rule-set function to parse a URI from a string. + */ +@SmithyUnstableApi +public class ParseUrl extends FunctionDefinition { + public static final String ID = "parseURL"; + public static final Identifier SCHEME = Identifier.of("scheme"); + public static final Identifier AUTHORITY = Identifier.of("authority"); + public static final Identifier PATH = Identifier.of("path"); + public static final Identifier NORMALIZED_PATH = Identifier.of("normalizedPath"); + public static final Identifier IS_IP = Identifier.of("isIp"); + + + @Override + public String getId() { + return "parseURL"; + } + + @Override + public List getArguments() { + return Collections.singletonList(Type.string()); + } + + public static Function ofExpression(Expression expression) { + return LibraryFunction.ofExpressions(new ParseUrl(), expression); + } + + @Override + public Type getReturnType() { + return Type.optional(Type.record( + MapUtils.of( + SCHEME, Type.string(), + AUTHORITY, Type.string(), + PATH, Type.string(), + NORMALIZED_PATH, Type.string(), + IS_IP, Type.bool() + ) + )); + } + + @Override + public Value evaluate(List arguments) { + String url = arguments.get(0).expectString(); + try { + URL parsed = new URL(url); + String path = parsed.getPath(); + if (parsed.getQuery() != null) { + System.out.println("empty query not supported"); + return Value.none(); + + } + boolean isIpAddr = false; + String host = parsed.getHost(); + if (host.startsWith("[") && host.endsWith("]")) { + isIpAddr = true; + } + String[] dottedParts = host.split("\\."); + if (dottedParts.length == 4) { + if (Arrays.stream(dottedParts).allMatch(part -> { + try { + int value = Integer.parseInt(part); + return value >= 0 && value <= 255; + } catch (NumberFormatException ex) { + return false; + } + })) { + isIpAddr = true; + } + } + String normalizedPath; + if (StringUtils.isBlank(path)) { + normalizedPath = "/"; + } else { + StringBuilder builder = new StringBuilder(); + if (!path.startsWith("/")) { + builder.append("/"); + } + builder.append(path); + if (!path.endsWith("/")) { + builder.append("/"); + } + normalizedPath = builder.toString(); + } + return Value.record(MapUtils.of( + SCHEME, Value.string(parsed.getProtocol()), + AUTHORITY, Value.string(parsed.getAuthority()), + PATH, Value.string(path), + NORMALIZED_PATH, Value.string(normalizedPath.toString()), + IS_IP, Value.bool(isIpAddr) + )); + } catch (MalformedURLException e) { + System.out.printf("invalid URL: %s%n", e); + return Value.none(); + } + } +} diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/stdlib/StringEquals.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/stdlib/StringEquals.java new file mode 100644 index 00000000000..4460f5cbeaf --- /dev/null +++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/stdlib/StringEquals.java @@ -0,0 +1,89 @@ +/* + * 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.language.stdlib; + +import java.util.Arrays; +import java.util.List; +import software.amazon.smithy.rulesengine.language.error.InnerParseError; +import software.amazon.smithy.rulesengine.language.eval.Scope; +import software.amazon.smithy.rulesengine.language.eval.Type; +import software.amazon.smithy.rulesengine.language.eval.Value; +import software.amazon.smithy.rulesengine.language.syntax.expr.Expression; +import software.amazon.smithy.rulesengine.language.syntax.fn.Function; +import software.amazon.smithy.rulesengine.language.syntax.fn.FunctionDefinition; +import software.amazon.smithy.rulesengine.language.syntax.fn.FunctionNode; +import software.amazon.smithy.rulesengine.language.syntax.fn.LibraryFunction; +import software.amazon.smithy.rulesengine.language.visit.ExpressionVisitor; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * A rule-set function for comparing strings for equality. + */ +@SmithyUnstableApi +public final class StringEquals extends Function { + public static final String ID = "stringEquals"; + private static final Definition DEFINITION = new Definition(); + + public StringEquals(FunctionNode functionNode) { + super(functionNode); + } + + /** + * Constructs a function that compare the left and right expressions for string equality. + * + * @param left the left expression. + * @param right the right expression. + * @return a function instance representing the StringEquals comparison. + */ + public static Function ofExpressions(Expression left, Expression right) { + return LibraryFunction.ofExpressions(DEFINITION, left, right); + } + + @Override + public R accept(ExpressionVisitor visitor) { + return visitor.visitStringEquals(functionNode.getArguments().get(0), functionNode.getArguments().get(1)); + } + + @Override + protected Type typeCheckLocal(Scope scope) throws InnerParseError { + LibraryFunction.checkTypeSignature(DEFINITION.getArguments(), functionNode.getArguments(), scope); + return DEFINITION.getReturnType(); + } + + private static class Definition extends FunctionDefinition { + public static final String ID = StringEquals.ID; + + @Override + public String getId() { + return ID; + } + + @Override + public List getArguments() { + return Arrays.asList(Type.string(), Type.string()); + } + + @Override + public Type getReturnType() { + return Type.bool(); + } + + @Override + public Value evaluate(List arguments) { + return Value.bool(arguments.get(0).expectString().equals(arguments.get(1).expectString())); + } + } +} diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/stdlib/Substring.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/stdlib/Substring.java new file mode 100644 index 00000000000..95f756162f1 --- /dev/null +++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/stdlib/Substring.java @@ -0,0 +1,83 @@ +/* + * 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.language.stdlib; + +import java.util.Arrays; +import java.util.List; +import software.amazon.smithy.rulesengine.language.eval.Type; +import software.amazon.smithy.rulesengine.language.eval.Value; +import software.amazon.smithy.rulesengine.language.syntax.expr.Expression; +import software.amazon.smithy.rulesengine.language.syntax.fn.Function; +import software.amazon.smithy.rulesengine.language.syntax.fn.FunctionDefinition; +import software.amazon.smithy.rulesengine.language.syntax.fn.LibraryFunction; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * A rule-set function for getting the substring of a string value. + */ +@SmithyUnstableApi +public final class Substring extends FunctionDefinition { + public static final String ID = "substring"; + + @Override + public String getId() { + return ID; + } + + @Override + public List getArguments() { + return Arrays.asList(Type.string(), Type.integer(), Type.integer(), Type.bool()); + } + + @Override + public Type getReturnType() { + return Type.string(); + } + + @Override + public Value evaluate(List arguments) { + String str = arguments.get(0).expectString(); + int startIndex = arguments.get(1).expectInteger(); + int stopIndex = arguments.get(2).expectInteger(); + boolean reverse = arguments.get(3).expectBool(); + + for (int i = 0; i < str.length(); i++) { + char ch = str.charAt(i); + if (!(ch <= 127)) { + return Value.none(); + } + } + + if (startIndex >= stopIndex || str.length() < stopIndex) { + return new Value.None(); + } + + if (!reverse) { + return Value.string(str.substring(startIndex, stopIndex)); + } else { + int revStart = str.length() - stopIndex; + int revStop = str.length() - startIndex; + return Value.string(str.substring(revStart, revStop)); + } + } + + public static Function ofExpression(Expression str, int startIndex, int stopIndex, Boolean reverse) { + return LibraryFunction.ofExpressions( + new Substring(), + str, Expression.of(startIndex), Expression.of(stopIndex), Expression.of(reverse) + ); + } +} diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/stdlib/UriEncode.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/stdlib/UriEncode.java new file mode 100644 index 00000000000..1df44868e42 --- /dev/null +++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/stdlib/UriEncode.java @@ -0,0 +1,73 @@ +/* + * 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.language.stdlib; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.Collections; +import java.util.List; +import software.amazon.smithy.rulesengine.language.eval.Type; +import software.amazon.smithy.rulesengine.language.eval.Value; +import software.amazon.smithy.rulesengine.language.syntax.expr.Expression; +import software.amazon.smithy.rulesengine.language.syntax.fn.Function; +import software.amazon.smithy.rulesengine.language.syntax.fn.FunctionDefinition; +import software.amazon.smithy.rulesengine.language.syntax.fn.LibraryFunction; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * A rule-set function for URI encoding a string. + */ +@SmithyUnstableApi +public final class UriEncode extends FunctionDefinition { + + public static final String ID = "uriEncode"; + private static final String[] ENCODED_CHARACTERS = new String[]{"+", "*", "%7E"}; + private static final String[] ENCODED_CHARACTERS_REPLACEMENTS = new String[]{"%20", "%2A", "~"}; + + + @Override + public String getId() { + return ID; + } + + @Override + public List getArguments() { + return Collections.singletonList(Type.string()); + } + + @Override + public Type getReturnType() { + return Type.string(); + } + + @Override + public Value evaluate(List arguments) { + String url = arguments.get(0).expectString(); + try { + String encoded = URLEncoder.encode(url, "UTF-8"); + for (int i = 0; i < ENCODED_CHARACTERS.length; i++) { + encoded = encoded.replace(ENCODED_CHARACTERS[i], ENCODED_CHARACTERS_REPLACEMENTS[i]); + } + return Value.string(encoded); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + public static Function ofExpression(Expression expression) { + return LibraryFunction.ofExpressions(new UriEncode(), expression); + } +} diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/stdlib/partition/DefaultPartitionDataProvider.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/stdlib/partition/DefaultPartitionDataProvider.java new file mode 100644 index 00000000000..f1945987753 --- /dev/null +++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/stdlib/partition/DefaultPartitionDataProvider.java @@ -0,0 +1,33 @@ +/* + * 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.language.stdlib.partition; + +import java.io.InputStream; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.rulesengine.language.model.Partitions; +import software.amazon.smithy.utils.SmithyUnstableApi; + +@SmithyUnstableApi +public final class DefaultPartitionDataProvider implements PartitionDataProvider { + private static final String DEFAULT_PARTITIONS_DATA = + "/software/amazon/smithy/rulesengine/language/partitions.json"; + + @Override + public Partitions loadPartitions() { + InputStream json = DefaultPartitionDataProvider.class.getResourceAsStream(DEFAULT_PARTITIONS_DATA); + return Partitions.fromNode(Node.parse(json)); + } +} diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/stdlib/partition/PartitionDataProvider.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/stdlib/partition/PartitionDataProvider.java new file mode 100644 index 00000000000..bcee9bdd0e4 --- /dev/null +++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/stdlib/partition/PartitionDataProvider.java @@ -0,0 +1,24 @@ +/* + * 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.language.stdlib.partition; + +import software.amazon.smithy.rulesengine.language.model.Partitions; +import software.amazon.smithy.utils.SmithyUnstableApi; + +@SmithyUnstableApi +public interface PartitionDataProvider { + Partitions loadPartitions(); +} diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/syntax/Identifier.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/syntax/Identifier.java new file mode 100644 index 00000000000..bc8769309db --- /dev/null +++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/syntax/Identifier.java @@ -0,0 +1,76 @@ +/* + * 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.language.syntax; + +import java.util.Objects; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.StringNode; +import software.amazon.smithy.model.node.ToNode; +import software.amazon.smithy.rulesengine.language.util.MandatorySourceLocation; +import software.amazon.smithy.rulesengine.language.util.SourceLocationUtils; +import software.amazon.smithy.utils.SmithyUnstableApi; + +@SmithyUnstableApi +public final class Identifier extends MandatorySourceLocation implements ToNode { + private final StringNode name; + + Identifier(StringNode name) { + super(name); + this.name = name; + } + + public static Identifier of(String name) { + return new Identifier(new StringNode(name, SourceLocationUtils.javaLocation())); + } + + public static Identifier of(StringNode name) { + return new Identifier(name); + } + + public StringNode getName() { + return name; + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj == null || obj.getClass() != this.getClass()) { + return false; + } + Identifier that = (Identifier) obj; + return Objects.equals(this.name, that.name); + } + + public String toString() { + return name.getValue(); + } + + public String asString() { + return name.getValue(); + } + + @Override + public Node toNode() { + return name; + } +} diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/syntax/expr/Expression.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/syntax/expr/Expression.java new file mode 100644 index 00000000000..bd51fa93e03 --- /dev/null +++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/syntax/expr/Expression.java @@ -0,0 +1,314 @@ +/* + * 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.language.syntax.expr; + +import static software.amazon.smithy.rulesengine.language.error.RuleError.context; + +import java.util.Optional; +import software.amazon.smithy.model.FromSourceLocation; +import software.amazon.smithy.model.SourceException; +import software.amazon.smithy.model.node.Node; +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.language.error.InnerParseError; +import software.amazon.smithy.rulesengine.language.eval.Scope; +import software.amazon.smithy.rulesengine.language.eval.Type; +import software.amazon.smithy.rulesengine.language.eval.TypeCheck; +import software.amazon.smithy.rulesengine.language.stdlib.AwsIsVirtualHostableS3Bucket; +import software.amazon.smithy.rulesengine.language.stdlib.BooleanEquals; +import software.amazon.smithy.rulesengine.language.stdlib.IsValidHostLabel; +import software.amazon.smithy.rulesengine.language.stdlib.ParseArn; +import software.amazon.smithy.rulesengine.language.stdlib.ParseUrl; +import software.amazon.smithy.rulesengine.language.stdlib.StringEquals; +import software.amazon.smithy.rulesengine.language.stdlib.Substring; +import software.amazon.smithy.rulesengine.language.syntax.Identifier; +import software.amazon.smithy.rulesengine.language.syntax.fn.Function; +import software.amazon.smithy.rulesengine.language.syntax.fn.FunctionNode; +import software.amazon.smithy.rulesengine.language.syntax.fn.GetAttr; +import software.amazon.smithy.rulesengine.language.syntax.fn.IsSet; +import software.amazon.smithy.rulesengine.language.syntax.fn.Not; +import software.amazon.smithy.rulesengine.language.util.MandatorySourceLocation; +import software.amazon.smithy.rulesengine.language.visit.ExpressionVisitor; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * A dynamically computed expression. + *

+ * Expressions are the fundamental building block of the rule language. + */ +@SmithyUnstableApi +public abstract class Expression extends MandatorySourceLocation implements TypeCheck, ToNode { + private Type cachedType; + + public Expression(FromSourceLocation sourceLocation) { + super(sourceLocation); + } + + public static Expression of(boolean value) { + return Literal.bool(value); + } + + /** + * Construct an integer literal for the given value, and returns it as an expression. + * + * @param value the integer value. + * @return the integer value as a literal expression. + */ + public static Expression of(int value) { + return Literal.integer(value); + } + + /** + * Constructs a string literal for the given values. + * + * @param value the string value. + * @return the string value as a literal. + */ + public static Literal of(String value) { + return literal(StringNode.from(value)); + } + + /** + * Constructs an expression from the provided {@link Node}. + * + * @param node the node to construct the expression from. + * @return the expression. + */ + public static Expression fromNode(Node node) { + if (node.asObjectNode().isPresent()) { + ObjectNode on = node.asObjectNode().get(); + Optional ref = on.getMember("ref"); + Optional fn = on.getMember("fn"); + if ((ref.isPresent() ? 1 : 0) + (fn.isPresent() ? 1 : 0) != 1) { + throw new SourceException("expected exactly one of `ref` or `fn` to be set, found " + + Node.printJson(node), node); + } + if (ref.isPresent()) { + return reference(Identifier.of(ref.get().expectStringNode("ref must be a string")), ref.get()); + } + return context("while parsing fn", node, () -> FunctionNode.fromNode(on).validate()); + } else { + return Literal.fromNode(node); + } + } + + /** + * Parse a value from a "short form" used within a template. + * + * @param shortForm the shortform value + * @return the parsed expression + */ + public static Expression parseShortform(String shortForm, FromSourceLocation context) { + return context("while parsing `" + shortForm + "` within a template", context, () -> { + if (shortForm.contains("#")) { + String[] parts = shortForm.split("#", 2); + String base = parts[0]; + String pattern = parts[1]; + return GetAttr.builder() + .sourceLocation(context) + .target(reference(Identifier.of(base), context)) + .path(pattern).build(); + } else { + return Expression.reference(Identifier.of(shortForm), context); + } + }); + } + + /** + * Constructs a {@link Reference} for the given {@link Identifier} at the given location. + * + * @param name the referenced identifier. + * @param context the source location. + * @return the reference. + */ + public static Reference reference(Identifier name, FromSourceLocation context) { + return new Reference(name, context); + } + + /** + * Constructs a {@link Literal} from the given {@link StringNode}. + * + * @param node the node to construct the literal from. + * @return the string node as a literal. + */ + public static Literal literal(StringNode node) { + return Literal.string(new Template(node)); + } + + /** + * Invoke the {@link ExpressionVisitor} functions for this expression. + * + * @param visitor the visitor to be invoked. + * @param the visitor return type. + * @return the return value of the visitor. + */ + public abstract R accept(ExpressionVisitor visitor); + + @Override + public abstract int hashCode(); + + @Override + public abstract boolean equals(Object obj); + + /** + * Constructs a {@link GetAttr} expression containing the given path string. + * + * @param path the path. + * @return the {@link GetAttr} expression. + */ + public GetAttr getAttr(String path) { + return GetAttr.builder() + .sourceLocation(this) + .target(this).path(path).build(); + } + + /** + * Constructs a {@link GetAttr} expression containing the given {@link Identifier}. + * + * @param path the path {@link Identifier}. + * @return the {@link GetAttr} expression. + */ + public GetAttr getAttr(Identifier path) { + return GetAttr.builder() + .sourceLocation(this) + .target(this).path(path.asString()).build(); + } + + @Override + public Type typeCheck(Scope scope) { + // TODO: Could remove all typecheckLocal functions (maybe?) + Type t = context( + String.format("while typechecking %s", this), + this, + () -> typeCheckLocal(scope) + ); + assert cachedType == null || t.equals(cachedType); + cachedType = t; + return cachedType; + } + + /** + * Returns the type for this expression, throws a runtime exception if {@code typeCheck} has not been invoked. + * + * @return the type. + */ + public Type type() { + if (cachedType == null) { + throw new RuntimeException("you must call typeCheck first"); + } + return cachedType; + } + + protected abstract Type typeCheckLocal(Scope scope) throws InnerParseError; + + /** + * Returns an IsSet expression for this instance. + * + * @return the IsSet expression. + */ + public IsSet isSet() { + return IsSet.ofExpression(this); + } + + /** + * Returns a {@link BooleanEquals} expression comparing this expression to the provided boolean value. + * + * @param value the value to compare against. + * @return the BooleanEquals {@link Function}. + */ + public Function equal(boolean value) { + return BooleanEquals.ofExpressions(this, Expression.of(value)); + } + + /** + * Returns a Not expression of this instance. + * + * @return the {@link Not} expression. + */ + public Not not() { + return Not.ofExpression(this); + } + + /** + * Returns a StringEquals function of this expression and the given string value. + * + * @param value the string value to compare this expression to. + * @return the StringEquals {@link Function}. + */ + public Function equal(String value) { + return StringEquals.ofExpressions(this, Expression.of(value)); + } + + /** + * Returns a ParseArn function of this expression. + * + * @return the ParseArn function expression. + */ + public Function parseArn() { + return ParseArn.ofExpression(this); + } + + /** + * Returns a substring expression of this expression. + * + * @param startIndex the starting index of the string. + * @param stopIndex the ending index of the string. + * @param reverse whether the indexing is should start from end of the string to start. + * @return the Substring function expression. + */ + public Function substring(int startIndex, int stopIndex, Boolean reverse) { + return Substring.ofExpression(this, startIndex, stopIndex, reverse); + } + + /** + * Returns a isValidHostLabel expression of this expression. + * + * @param allowDots whether the UTF-8 {@code .} is considered valid within a host label. + * @return the isValidHostLabel function expression. + */ + public Function isValidHostLabel(boolean allowDots) { + return IsValidHostLabel.ofExpression(this, allowDots); + } + + /** + * Returns a isVirtualHostableS3Bucket expression of this expression. + * + * @param allowDots whether the UTF-8 {@code .} is considered valid within a host label. + * @return the isVirtualHostableS3Bucket function expression. + */ + public Function isVirtualHostableS3Bucket(boolean allowDots) { + return AwsIsVirtualHostableS3Bucket.ofExpression(this, allowDots); + } + + /** + * Returns a parseUrl expression of this expression. + * + * @return the parseUrl function expression. + */ + public Function parseUrl() { + return ParseUrl.ofExpression(this); + } + + /** + * Converts this expression to a string template. By default this implementation returns a {@link RuntimeException}. + * + * @return the string template. + */ + public String template() { + throw new RuntimeException(String.format("cannot convert %s to a string template", this)); + } +} diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/syntax/expr/Literal.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/syntax/expr/Literal.java new file mode 100644 index 00000000000..fa5a64dc834 --- /dev/null +++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/syntax/expr/Literal.java @@ -0,0 +1,676 @@ +/* + * 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.language.syntax.expr; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; +import software.amazon.smithy.model.FromSourceLocation; +import software.amazon.smithy.model.SourceException; +import software.amazon.smithy.model.SourceLocation; +import software.amazon.smithy.model.node.ArrayNode; +import software.amazon.smithy.model.node.BooleanNode; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.NodeVisitor; +import software.amazon.smithy.model.node.NullNode; +import software.amazon.smithy.model.node.NumberNode; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.node.StringNode; +import software.amazon.smithy.rulesengine.language.error.RuleError; +import software.amazon.smithy.rulesengine.language.eval.RuleEvaluator; +import software.amazon.smithy.rulesengine.language.eval.Scope; +import software.amazon.smithy.rulesengine.language.eval.Type; +import software.amazon.smithy.rulesengine.language.eval.Value; +import software.amazon.smithy.rulesengine.language.syntax.Identifier; +import software.amazon.smithy.rulesengine.language.util.SourceLocationUtils; +import software.amazon.smithy.rulesengine.language.visit.ExpressionVisitor; +import software.amazon.smithy.rulesengine.language.visit.TemplateVisitor; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * Literals allow rules and properties to define arbitrarily nested JSON structures (e.g.for properties) + *

+ * They support template strings, but _do not_ support template objects since that creates ambiguity. {@link Template}s + * are a basic example of literals–literal strings. Literals can also be booleans, objects, integers or tuples. + */ +@SmithyUnstableApi +public final class Literal extends Expression { + + private final ILiteral source; + + private Literal(ILiteral source, FromSourceLocation sourceLocation) { + super(sourceLocation.getSourceLocation()); + this.source = source; + } + + /** + * Constructs a tuple literal of values. + * + * @param values the values. + * @return the tuple literal. + */ + public static Literal tuple(List values) { + return new Literal(new Tuple(values), SourceLocationUtils.javaLocation()); + } + + /** + * Constructs a record literal of values. + * + * @param record a map of values to be converted to a record. + * @return the record literal. + */ + public static Literal record(Map record) { + return new Literal(new Record(record), SourceLocationUtils.javaLocation()); + } + + /** + * Constructs a string literal from a {@link Template} value. + * + * @param value the template value. + * @return the string literal. + */ + public static Literal string(Template value) { + return new Literal(new String(value), SourceLocationUtils.javaLocation()); + } + + /** + * Constructs an integer literal from an integer value. + * + * @param value the integer value. + * @return the integer literal. + */ + public static Literal integer(int value) { + return new Literal(new Integer(Node.from(value)), SourceLocationUtils.javaLocation()); + } + + /** + * Constructs a bool literal from a boolean value. + * + * @param value the boolean value. + * @return the bool literal. + */ + public static Literal bool(boolean value) { + return new Literal(new Bool(Node.from(value)), SourceLocationUtils.javaLocation()); + } + + /** + * Constructs a literal from a {@link Node} based on the Node's type. + * + * @param node a node to construct as a literal. + * @return the literal representation of the node. + */ + public static Literal fromNode(Node node) { + ILiteral iLiteral = node.accept(new NodeVisitor() { + @Override + public ILiteral arrayNode(ArrayNode arrayNode) { + return new Tuple(arrayNode.getElements().stream() + .map(el -> new Literal(el.accept(this), el)) + .collect(Collectors.toList())); + } + + @Override + public ILiteral booleanNode(BooleanNode booleanNode) { + return new Bool(booleanNode); + } + + @Override + public ILiteral nullNode(NullNode nullNode) { + throw new RuntimeException("null node not supported"); + } + + @Override + public ILiteral numberNode(NumberNode numberNode) { + return new Integer(numberNode); + } + + @Override + public ILiteral objectNode(ObjectNode objectNode) { + Map obj = new HashMap<>(); + objectNode.getMembers().forEach((k, v) -> { + obj.put(Identifier.of(k), new Literal(v.accept(this), v)); + }); + return new Record(obj); + } + + @Override + public ILiteral stringNode(StringNode stringNode) { + return new String(new Template(stringNode)); + } + }); + return new Literal(iLiteral, node.getSourceLocation()); + } + + /** + * Attempts to convert the literal to a {@link java.lang.String}. Otherwise throws an exception. + * + * @return the literal as a string. + */ + public java.lang.String expectLiteralString() { + if (source instanceof String) { + final String s = (String) source; + + return s.value.expectLiteral(); + } else { + throw new RuleError(new SourceException("Expected a literal string, got " + source, this)); + } + } + + /** + * Attempts to convert the literal to a {@link Boolean} if possible. Otherwise, returns an empty optional. + * + * @return an optional boolean. + */ + public Optional asBool() { + return source.asBool(); + } + + /** + * Attempts to convert the literal to a {@link Template} if possible. Otherwise, returns an empty optional. + * + * @return an optional boolean. + */ + public Optional