diff --git a/.changes/next-release/feature-dedce719f4dc02f94249c5b65ce4d54e508f1232.json b/.changes/next-release/feature-dedce719f4dc02f94249c5b65ce4d54e508f1232.json new file mode 100644 index 00000000000..cf007041b56 --- /dev/null +++ b/.changes/next-release/feature-dedce719f4dc02f94249c5b65ce4d54e508f1232.json @@ -0,0 +1,7 @@ +{ + "type": "feature", + "description": "Added a generic evaluator/interpreter for JMESPath expressions.", + "pull_requests": [ + "[#2878](https://github.com/smithy-lang/smithy/pull/2878)" + ] +} diff --git a/smithy-jmespath-tests/src/main/java/software/amazon/smithy/jmespath/tests/ComplianceTestRunner.java b/smithy-jmespath-tests/src/main/java/software/amazon/smithy/jmespath/tests/ComplianceTestRunner.java index 187b5fab8ad..d498bfbef3f 100644 --- a/smithy-jmespath-tests/src/main/java/software/amazon/smithy/jmespath/tests/ComplianceTestRunner.java +++ b/smithy-jmespath-tests/src/main/java/software/amazon/smithy/jmespath/tests/ComplianceTestRunner.java @@ -11,16 +11,20 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; +import java.util.function.BiPredicate; import java.util.stream.Stream; import software.amazon.smithy.jmespath.JmespathException; import software.amazon.smithy.jmespath.JmespathExceptionType; import software.amazon.smithy.jmespath.JmespathExpression; import software.amazon.smithy.jmespath.RuntimeType; +import software.amazon.smithy.jmespath.evaluation.EvaluationUtils; import software.amazon.smithy.jmespath.evaluation.Evaluator; +import software.amazon.smithy.jmespath.evaluation.JmespathAbstractRuntime; import software.amazon.smithy.jmespath.evaluation.JmespathRuntime; +import software.amazon.smithy.jmespath.type.Type; import software.amazon.smithy.utils.IoUtils; -public class ComplianceTestRunner { +public class ComplianceTestRunner { private static final String DEFAULT_TEST_CASE_LOCATION = "compliance"; private static final String SUBJECT_MEMBER = "given"; private static final String CASES_MEMBER = "cases"; @@ -30,19 +34,25 @@ public class ComplianceTestRunner { private static final String ERROR_MEMBER = "error"; private static final String BENCH_MEMBER = "bench"; private final JmespathRuntime runtime; - private final List> testCases = new ArrayList<>(); + private final JmespathAbstractRuntime abstractRuntime; + private final List> testCases = new ArrayList<>(); - private ComplianceTestRunner(JmespathRuntime runtime) { + private ComplianceTestRunner(JmespathRuntime runtime, JmespathAbstractRuntime abstractRuntime) { this.runtime = runtime; + this.abstractRuntime = abstractRuntime; } public static Stream defaultParameterizedTestSource(JmespathRuntime runtime) { - ComplianceTestRunner runner = new ComplianceTestRunner<>(runtime); + return defaultParameterizedTestSource(runtime, null); + } + + public static Stream defaultParameterizedTestSource(JmespathRuntime runtime, JmespathAbstractRuntime abstractRuntime) { + ComplianceTestRunner runner = new ComplianceTestRunner<>(runtime, abstractRuntime); URL manifest = ComplianceTestRunner.class.getResource(DEFAULT_TEST_CASE_LOCATION + "/MANIFEST"); try (var reader = new BufferedReader(new InputStreamReader(manifest.openStream(), StandardCharsets.UTF_8))) { reader.lines().forEach(line -> { var url = ComplianceTestRunner.class.getResource(DEFAULT_TEST_CASE_LOCATION + "/" + line.trim()); - runner.testCases.addAll(TestCase.from(url, runtime)); + runner.testCases.addAll(TestCase.from(url, runtime, abstractRuntime)); }); } catch (IOException e) { throw new RuntimeException(e); @@ -54,8 +64,13 @@ public Stream parameterizedTestSource() { return testCases.stream().map(testCase -> new Object[] {testCase.name(), testCase}); } - private record TestCase( + public Stream parameterizedAbstractTestSource(JmespathRuntime abstractRuntime, BiPredicate abstractPredicate) { + return testCases.stream().map(testCase -> new Object[] {testCase.name(), (Runnable)() -> testCase.abstractRun(abstractRuntime, abstractPredicate)}); + } + + private record TestCase( JmespathRuntime runtime, + JmespathAbstractRuntime abstractRuntime, String testSuite, String comment, T given, @@ -64,10 +79,10 @@ private record TestCase( JmespathExceptionType expectedError, String benchmark) implements Runnable { - public static List> from(URL url, JmespathRuntime runtime) { + public static List> from(URL url, JmespathRuntime runtime, JmespathAbstractRuntime abstractRuntime) { var path = url.getPath(); var testSuiteName = path.substring(path.lastIndexOf('/') + 1, path.lastIndexOf('.')); - var testCases = new ArrayList>(); + var testCases = new ArrayList>(); String text = IoUtils.readUtf8Url(url); T tests = JmespathExpression.parseJson(text, runtime); @@ -92,6 +107,7 @@ public static List> from(URL url, JmespathRuntime runtime) { var benchmark = valueAsString(runtime, testCase, BENCH_MEMBER); testCases.add(new TestCase<>(runtime, + abstractRuntime, testSuiteName, comment, given, @@ -122,9 +138,10 @@ private String name() { public void run() { try { var parsed = JmespathExpression.parse(expression); - var result = new Evaluator<>(given, runtime).visit(parsed); + var result = parsed.evaluate(given, runtime); if (benchmark != null) { // Benchmarks don't include expected results or errors + // TODO: Could still run these? return; } if (expectedError != null) { @@ -138,7 +155,34 @@ public void run() { + "Actual: " + runtime.toString(result) + "\n" + "For query: " + expression + "\n"); } + + if (abstractRuntime != null) { + var abstractedGiven = EvaluationUtils.convert(runtime, given, abstractRuntime); + var abstractResult = parsed.evaluate(abstractedGiven, abstractRuntime); + + if (!abstractResult.isInstance(result, runtime)) { + parsed.evaluate(abstractedGiven, abstractRuntime); + abstractResult.isInstance(result, runtime); + throw new AssertionError("Expected " + result + " to be an instance of " + abstractResult + ".\n" + + "For query: " + expression + "\n"); + } + } + } + } catch (JmespathException e) { + if (!e.getType().equals(expectedError)) { + throw new AssertionError("Expected error does not match actual error. \n" + + "Expected: " + (expectedError != null ? expectedError : "(no error)") + "\n" + + "Actual: " + e.getType() + " - " + e.getMessage() + "\n" + + "For query: " + expression + "\n", e); } + } + } + + public void abstractRun(JmespathRuntime abstractRuntime, BiPredicate abstractPredicate) { + try { + var parsed = JmespathExpression.parse(expression); + var result = new Evaluator<>(given, runtime).visit(parsed); + } catch (JmespathException e) { if (!e.getType().equals(expectedError)) { throw new AssertionError("Expected error does not match actual error. \n" diff --git a/smithy-jmespath-tests/src/test/java/software/amazon/smithy/jmespath/tests/LiteralExpressionJmespathRuntimeComplianceTests.java b/smithy-jmespath-tests/src/test/java/software/amazon/smithy/jmespath/tests/LiteralExpressionJmespathRuntimeComplianceTests.java index ddbbd0f5e56..b024339b4f8 100644 --- a/smithy-jmespath-tests/src/test/java/software/amazon/smithy/jmespath/tests/LiteralExpressionJmespathRuntimeComplianceTests.java +++ b/smithy-jmespath-tests/src/test/java/software/amazon/smithy/jmespath/tests/LiteralExpressionJmespathRuntimeComplianceTests.java @@ -8,6 +8,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import software.amazon.smithy.jmespath.LiteralExpressionJmespathRuntime; +import software.amazon.smithy.jmespath.type.TypeJmespathRuntime; public class LiteralExpressionJmespathRuntimeComplianceTests { @ParameterizedTest(name = "{0}") @@ -17,6 +18,8 @@ public void testRunner(String filename, Runnable callable) throws Exception { } public static Stream source() { - return ComplianceTestRunner.defaultParameterizedTestSource(LiteralExpressionJmespathRuntime.INSTANCE); + return ComplianceTestRunner.defaultParameterizedTestSource( + LiteralExpressionJmespathRuntime.INSTANCE, + new TypeJmespathRuntime()); } } diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/JmespathExpression.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/JmespathExpression.java index 33719579a99..26358875acb 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/JmespathExpression.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/JmespathExpression.java @@ -7,7 +7,9 @@ import java.util.Set; import java.util.TreeSet; import software.amazon.smithy.jmespath.ast.LiteralExpression; +import software.amazon.smithy.jmespath.evaluation.AbstractEvaluator; import software.amazon.smithy.jmespath.evaluation.Evaluator; +import software.amazon.smithy.jmespath.evaluation.JmespathAbstractRuntime; import software.amazon.smithy.jmespath.evaluation.JmespathRuntime; /** @@ -42,7 +44,7 @@ public static JmespathExpression parse(String text) { * @return Returns the parsed expression. * @throws JmespathException if the expression is invalid. */ - public static JmespathExpression parse(String text, JmespathRuntime runtime) { + public static JmespathExpression parse(String text, JmespathAbstractRuntime runtime) { return Parser.parse(text, runtime); } @@ -54,7 +56,7 @@ public static JmespathExpression parse(String text, JmespathRuntime runti * @return Returns the parsed JSON value. * @throws JmespathException if the text is invalid. */ - public static T parseJson(String text, JmespathRuntime runtime) { + public static T parseJson(String text, JmespathAbstractRuntime runtime) { Lexer lexer = new Lexer(text, runtime); return lexer.parseJsonValue(); } @@ -129,4 +131,12 @@ public LiteralExpression evaluate(LiteralExpression currentNode) { public T evaluate(T currentNode, JmespathRuntime runtime) { return new Evaluator<>(currentNode, runtime).visit(this); } + + public T evaluate(T currentNode, JmespathAbstractRuntime runtime) { + if (runtime instanceof JmespathRuntime) { + return evaluate(currentNode, (JmespathRuntime)runtime); + } else { + return new AbstractEvaluator<>(currentNode, runtime).visit(this); + } + } } diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/JmespathExtension.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/JmespathExtension.java new file mode 100644 index 00000000000..f6b5917e2be --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/JmespathExtension.java @@ -0,0 +1,11 @@ +package software.amazon.smithy.jmespath; + + +import software.amazon.smithy.jmespath.evaluation.Function; + +import java.util.List; + +public interface JmespathExtension { + + List> getFunctions(); +} diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/JmespathQuery.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/JmespathQuery.java new file mode 100644 index 00000000000..6f308fbefc9 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/JmespathQuery.java @@ -0,0 +1,11 @@ +package software.amazon.smithy.jmespath; + + +import software.amazon.smithy.jmespath.evaluation.JmespathRuntime; + +import java.util.function.Function; + +public interface JmespathQuery extends Function { + + JmespathRuntime runtime(); +} diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/Lexer.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/Lexer.java index 25b9a678d86..28eda0bb20e 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/Lexer.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/Lexer.java @@ -9,13 +9,14 @@ import java.util.Objects; import java.util.function.Predicate; import software.amazon.smithy.jmespath.ast.LiteralExpression; +import software.amazon.smithy.jmespath.evaluation.JmespathAbstractRuntime; import software.amazon.smithy.jmespath.evaluation.JmespathRuntime; final class Lexer { private static final int MAX_NESTING_LEVEL = 50; - private final JmespathRuntime runtime; + private final JmespathAbstractRuntime runtime; private final String expression; private final int length; private int position = 0; @@ -25,7 +26,7 @@ final class Lexer { private final List tokens = new ArrayList<>(); private boolean currentlyParsingLiteral; - Lexer(String expression, JmespathRuntime runtime) { + Lexer(String expression, JmespathAbstractRuntime runtime) { this.runtime = Objects.requireNonNull(runtime, "runtime must not be null"); this.expression = Objects.requireNonNull(expression, "expression must not be null"); this.length = expression.length(); @@ -35,7 +36,7 @@ static TokenIterator tokenize(String expression) { return tokenize(expression, LiteralExpressionJmespathRuntime.INSTANCE); } - static TokenIterator tokenize(String expression, JmespathRuntime runtime) { + static TokenIterator tokenize(String expression, JmespathAbstractRuntime runtime) { return new Lexer<>(expression, runtime).doTokenize(); } diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/LiteralExpressionJmespathRuntime.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/LiteralExpressionJmespathRuntime.java index 526beef8ddb..8d9d9fefea2 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/LiteralExpressionJmespathRuntime.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/LiteralExpressionJmespathRuntime.java @@ -88,6 +88,11 @@ public LiteralExpression element(LiteralExpression array, int index) { return LiteralExpression.from(array.expectArrayValue().get(index)); } + @Override + public LiteralExpression abstractElement(LiteralExpression array, LiteralExpression index) { + return element(array, index.expectNumberValue().intValue()); + } + @Override public Iterable asIterable(LiteralExpression array) { switch (array.getType()) { @@ -109,17 +114,19 @@ private static final class ArrayLiteralExpressionBuilder implements ArrayBuilder private final List result = new ArrayList<>(); @Override - public void add(LiteralExpression value) { + public ArrayLiteralExpressionBuilder add(LiteralExpression value) { result.add(value.getValue()); + return this; } @Override - public void addAll(LiteralExpression array) { + public ArrayLiteralExpressionBuilder addAll(LiteralExpression array) { if (array.isArrayValue()) { result.addAll(array.expectArrayValue()); } else { result.addAll(array.expectObjectValue().keySet()); } + return this; } @Override @@ -146,13 +153,15 @@ private static final class ObjectLiteralExpressionBuilder implements ObjectBuild private final Map result = new HashMap<>(); @Override - public void put(LiteralExpression key, LiteralExpression value) { + public ObjectLiteralExpressionBuilder put(LiteralExpression key, LiteralExpression value) { result.put(key.expectStringValue(), value.getValue()); + return this; } @Override - public void putAll(LiteralExpression object) { + public ObjectLiteralExpressionBuilder putAll(LiteralExpression object) { result.putAll(object.expectObjectValue()); + return this; } @Override diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/Parser.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/Parser.java index cbe701518df..5a1a5ed1545 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/Parser.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/Parser.java @@ -27,6 +27,7 @@ import software.amazon.smithy.jmespath.ast.ProjectionExpression; import software.amazon.smithy.jmespath.ast.SliceExpression; import software.amazon.smithy.jmespath.ast.Subexpression; +import software.amazon.smithy.jmespath.evaluation.JmespathAbstractRuntime; import software.amazon.smithy.jmespath.evaluation.JmespathRuntime; /** @@ -75,12 +76,12 @@ final class Parser { private final String expression; private final TokenIterator iterator; - private Parser(String expression, JmespathRuntime runtime) { + private Parser(String expression, JmespathAbstractRuntime runtime) { this.expression = expression; iterator = Lexer.tokenize(expression, runtime); } - static JmespathExpression parse(String expression, JmespathRuntime runtime) { + static JmespathExpression parse(String expression, JmespathAbstractRuntime runtime) { Parser parser = new Parser(expression, runtime); JmespathExpression result = parser.expression(0); parser.iterator.expect(TokenType.EOF); diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/RuntimeType.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/RuntimeType.java index d57a3e8ec02..e7085a50a39 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/RuntimeType.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/RuntimeType.java @@ -4,6 +4,7 @@ */ package software.amazon.smithy.jmespath; +import java.util.EnumSet; import java.util.Locale; import software.amazon.smithy.jmespath.ast.ComparatorType; import software.amazon.smithy.jmespath.ast.LiteralExpression; @@ -150,4 +151,8 @@ public abstract LiteralExpression compare( LiteralExpression right, ComparatorType comparator ); + + public static EnumSet valueTypes() { + return EnumSet.complementOf(EnumSet.of(EXPRESSION, ANY)); + } } diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/SubstitutionVisitor.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/SubstitutionVisitor.java new file mode 100644 index 00000000000..1a0af23769e --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/SubstitutionVisitor.java @@ -0,0 +1,145 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.jmespath; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import software.amazon.smithy.jmespath.ast.AndExpression; +import software.amazon.smithy.jmespath.ast.ComparatorExpression; +import software.amazon.smithy.jmespath.ast.CurrentExpression; +import software.amazon.smithy.jmespath.ast.ExpressionTypeExpression; +import software.amazon.smithy.jmespath.ast.FieldExpression; +import software.amazon.smithy.jmespath.ast.FilterProjectionExpression; +import software.amazon.smithy.jmespath.ast.FlattenExpression; +import software.amazon.smithy.jmespath.ast.FunctionExpression; +import software.amazon.smithy.jmespath.ast.IndexExpression; +import software.amazon.smithy.jmespath.ast.LiteralExpression; +import software.amazon.smithy.jmespath.ast.MultiSelectHashExpression; +import software.amazon.smithy.jmespath.ast.MultiSelectListExpression; +import software.amazon.smithy.jmespath.ast.NotExpression; +import software.amazon.smithy.jmespath.ast.ObjectProjectionExpression; +import software.amazon.smithy.jmespath.ast.OrExpression; +import software.amazon.smithy.jmespath.ast.ProjectionExpression; +import software.amazon.smithy.jmespath.ast.SliceExpression; +import software.amazon.smithy.jmespath.ast.Subexpression; + +public class SubstitutionVisitor implements ExpressionVisitor { + + private final Function substitution; + + public SubstitutionVisitor(Function substitution) { + this.substitution = substitution; + } + + private JmespathExpression visit(JmespathExpression expression) { + JmespathExpression result = substitution.apply(expression); + return result != null ? result : expression.accept(this); + } + + @Override + public JmespathExpression visitComparator(ComparatorExpression expression) { + return new ComparatorExpression(expression.getComparator(), visit(expression.getLeft()), visit(expression.getRight()), expression.getLine(), expression.getColumn()); + } + + @Override + public JmespathExpression visitCurrentNode(CurrentExpression expression) { + return expression; + } + + @Override + public JmespathExpression visitExpressionType(ExpressionTypeExpression expression) { + return new ExpressionTypeExpression(visit(expression.getExpression()), expression.getLine(), expression.getColumn()); + } + + @Override + public JmespathExpression visitFlatten(FlattenExpression expression) { + return new FlattenExpression(visit(expression.getExpression()), expression.getLine(), expression.getColumn()); + } + + @Override + public JmespathExpression visitFunction(FunctionExpression expression) { + List args = new ArrayList<>(); + for (JmespathExpression arg : expression.getArguments()) { + args.add(visit(arg)); + } + return new FunctionExpression(expression.getName(), args, expression.getLine(), expression.getColumn()); + } + + @Override + public JmespathExpression visitField(FieldExpression expression) { + return expression; + } + + @Override + public JmespathExpression visitIndex(IndexExpression expression) { + return expression; + } + + @Override + public JmespathExpression visitLiteral(LiteralExpression expression) { + return expression; + } + + @Override + public JmespathExpression visitMultiSelectList(MultiSelectListExpression expression) { + List exprs = new ArrayList<>(); + for (JmespathExpression expr : expression.getExpressions()) { + exprs.add(visit(expr)); + } + return new MultiSelectListExpression(exprs, expression.getLine(), expression.getColumn()); + } + + @Override + public JmespathExpression visitMultiSelectHash(MultiSelectHashExpression expression) { + Map exprs = new LinkedHashMap<>(); + for (Map.Entry entry : expression.getExpressions().entrySet()) { + exprs.put(entry.getKey(), visit(entry.getValue())); + } + return new MultiSelectHashExpression(exprs, expression.getLine(), expression.getColumn()); + } + + @Override + public JmespathExpression visitAnd(AndExpression expression) { + return new AndExpression(visit(expression.getLeft()), visit(expression.getRight()), expression.getLine(), expression.getColumn()); + } + + @Override + public JmespathExpression visitOr(OrExpression expression) { + return new OrExpression(visit(expression.getLeft()), visit(expression.getRight()), expression.getLine(), expression.getColumn()); + } + + @Override + public JmespathExpression visitNot(NotExpression expression) { + return new NotExpression(visit(expression.getExpression()), expression.getLine(), expression.getColumn()); + } + + @Override + public JmespathExpression visitProjection(ProjectionExpression expression) { + return new ProjectionExpression(visit(expression.getLeft()), visit(expression.getRight()), expression.getLine(), expression.getColumn()); + } + + @Override + public JmespathExpression visitFilterProjection(FilterProjectionExpression expression) { + return new FilterProjectionExpression(visit(expression.getLeft()), visit(expression.getComparison()), visit(expression.getRight()), expression.getLine(), expression.getColumn()); + } + + @Override + public JmespathExpression visitObjectProjection(ObjectProjectionExpression expression) { + return new ObjectProjectionExpression(visit(expression.getLeft()), visit(expression.getRight()), expression.getLine(), expression.getColumn()); + } + + @Override + public JmespathExpression visitSlice(SliceExpression expression) { + return expression; + } + + @Override + public JmespathExpression visitSubexpression(Subexpression expression) { + return new Subexpression(visit(expression.getLeft()), visit(expression.getRight()), expression.getLine(), expression.getColumn()); + } +} diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/ast/FunctionExpression.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/ast/FunctionExpression.java index 376be14fb57..56ddde6152b 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/ast/FunctionExpression.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/ast/FunctionExpression.java @@ -14,7 +14,7 @@ * * @see Function Expressions */ -public final class FunctionExpression extends JmespathExpression { +public class FunctionExpression extends JmespathExpression { public String name; public List arguments; diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/ast/ResolvedFunctionExpression.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/ast/ResolvedFunctionExpression.java new file mode 100644 index 00000000000..a11a8c862be --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/ast/ResolvedFunctionExpression.java @@ -0,0 +1,21 @@ +package software.amazon.smithy.jmespath.ast; + +import software.amazon.smithy.jmespath.ExpressionVisitor; +import software.amazon.smithy.jmespath.JmespathExpression; +import software.amazon.smithy.jmespath.evaluation.Function; + +import java.util.List; + +public class ResolvedFunctionExpression extends FunctionExpression { + + private final Function function; + + public ResolvedFunctionExpression(Function function, List arguments) { + super(function.name(), arguments); + this.function = function; + } + + public Function function() { + return function; + } +} diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/ast/SliceExpression.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/ast/SliceExpression.java index 6e045dd94fd..6232916e258 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/ast/SliceExpression.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/ast/SliceExpression.java @@ -8,6 +8,7 @@ import java.util.OptionalInt; import software.amazon.smithy.jmespath.ExpressionVisitor; import software.amazon.smithy.jmespath.JmespathExpression; +import software.amazon.smithy.jmespath.type.Type; /** * Represents a slice expression, containing an optional zero-based @@ -71,4 +72,9 @@ public int hashCode() { public String toString() { return "SliceExpression{start=" + start + ", stop=" + stop + ", step=" + step + '}'; } +// +// @Override +// public Type typeCheck(Type currentType) { +// return currentType; +// } } diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/AbsFunction.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/AbsFunction.java index 7e14dc380ae..f4d8f38731f 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/AbsFunction.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/AbsFunction.java @@ -4,23 +4,31 @@ */ package software.amazon.smithy.jmespath.evaluation; +import software.amazon.smithy.jmespath.RuntimeType; + import java.math.BigDecimal; import java.math.BigInteger; import java.util.List; -class AbsFunction implements Function { +class AbsFunction implements Function { @Override public String name() { return "abs"; } @Override - public T apply(JmespathRuntime runtime, List> functionArguments) { + public T abstractApply(AbstractEvaluator evaluator, List> functionArguments) { + return evaluator.runtime().createAny(RuntimeType.NUMBER); + } + + @Override + public T concreteApply(Evaluator evaluator, List> functionArguments) { checkArgumentCount(1, functionArguments); T value = functionArguments.get(0).expectNumber(); - Number number = runtime.asNumber(value); + Number number = evaluator.runtime().asNumber(value); - switch (runtime.numberType(value)) { + JmespathRuntime runtime = evaluator.runtime(); + switch (evaluator.runtime().numberType(value)) { case BYTE: case SHORT: case INTEGER: diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/AbstractEvaluator.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/AbstractEvaluator.java new file mode 100644 index 00000000000..10692cd46f7 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/AbstractEvaluator.java @@ -0,0 +1,360 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.jmespath.evaluation; + +import software.amazon.smithy.jmespath.ExpressionVisitor; +import software.amazon.smithy.jmespath.JmespathException; +import software.amazon.smithy.jmespath.JmespathExceptionType; +import software.amazon.smithy.jmespath.JmespathExpression; +import software.amazon.smithy.jmespath.RuntimeType; +import software.amazon.smithy.jmespath.SubstitutionVisitor; +import software.amazon.smithy.jmespath.ast.AndExpression; +import software.amazon.smithy.jmespath.ast.ComparatorExpression; +import software.amazon.smithy.jmespath.ast.CurrentExpression; +import software.amazon.smithy.jmespath.ast.ExpressionTypeExpression; +import software.amazon.smithy.jmespath.ast.FieldExpression; +import software.amazon.smithy.jmespath.ast.FilterProjectionExpression; +import software.amazon.smithy.jmespath.ast.FlattenExpression; +import software.amazon.smithy.jmespath.ast.FunctionExpression; +import software.amazon.smithy.jmespath.ast.IndexExpression; +import software.amazon.smithy.jmespath.ast.LiteralExpression; +import software.amazon.smithy.jmespath.ast.MultiSelectHashExpression; +import software.amazon.smithy.jmespath.ast.MultiSelectListExpression; +import software.amazon.smithy.jmespath.ast.NotExpression; +import software.amazon.smithy.jmespath.ast.ObjectProjectionExpression; +import software.amazon.smithy.jmespath.ast.OrExpression; +import software.amazon.smithy.jmespath.ast.ProjectionExpression; +import software.amazon.smithy.jmespath.ast.SliceExpression; +import software.amazon.smithy.jmespath.ast.Subexpression; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; + +// TODO: Consider whether "abstract" is confusing (since the class is not abstract, the interpretation is) +// TODO: Difference between abstract meaning "can't read concrete java values from T values" +// and abstract meaning "loses information and approximates". +// Question is whether the former is useful without the latter (i.e. abstracting implies +// multiple possible values implies can't read Java values) +public class AbstractEvaluator implements ExpressionVisitor { + + private final JmespathAbstractRuntime runtime; + protected final FunctionRegistry functions; + + // We could make this state mutable instead of creating lots of sub-Evaluators. + // This would make evaluation not thread-safe, but it's unclear how much that matters. + protected final T current; + + public AbstractEvaluator(T current, JmespathAbstractRuntime abstractRuntime) { + this(current, abstractRuntime, FunctionRegistry.getSPIRegistry()); + } + + public AbstractEvaluator(T current, JmespathAbstractRuntime runtime, FunctionRegistry functions) { + this.current = current; + this.runtime = runtime; + this.functions = functions; + } + + public JmespathAbstractRuntime runtime() { + return runtime; + } + + public T visit(JmespathExpression expression) { + return expression.accept(this); + } + + @Override + public T visitComparator(ComparatorExpression comparatorExpression) { + T left = visit(comparatorExpression.getLeft()); + T right = visit(comparatorExpression.getRight()); + switch (comparatorExpression.getComparator()) { + case EQUAL: + return runtime.abstractEqual(left, right); + case NOT_EQUAL: + return ifThenElse( + runtime.abstractEqual(left, right), + runtime.createBoolean(false), + runtime.createBoolean(true)); + // NOTE: Ordering operators >, >=, <, <= are only valid for numbers. All invalid + // comparisons return null. + case LESS_THAN: + return runtime.abstractLessThan(left, right); + case LESS_THAN_EQUAL: + return not(runtime.abstractLessThan(right, left)); + case GREATER_THAN: + return runtime.abstractLessThan(right, left); + case GREATER_THAN_EQUAL: + return not(runtime.abstractLessThan(left, right)); + default: + throw new IllegalArgumentException("Unsupported comparator: " + comparatorExpression.getComparator()); + } + } + + @Override + public T visitCurrentNode(CurrentExpression currentExpression) { + return current; + } + + @Override + public T visitExpressionType(ExpressionTypeExpression expressionTypeExpression) { + return runtime.createExpression(expressionTypeExpression.getExpression()); + } + + @Override + public T visitFlatten(FlattenExpression flattenExpression) { + T value = visit(flattenExpression.getExpression()); + + return ifThenElse(runtime.abstractIs(value, RuntimeType.ARRAY), + foldLeft(runtime.arrayBuilder().build(), + JmespathExpression.parse("concat(acc, to_array(element))"), + value), + runtime.createNull()); + } + + @Override + public T visitFunction(FunctionExpression functionExpression) { + // TODO: Change API so we can resolve ahead of time once + Function resolved = functions.lookup(runtime, functionExpression.getName()); + List> arguments = new ArrayList<>(); + for (JmespathExpression expr : functionExpression.getArguments()) { + if (expr instanceof ExpressionTypeExpression) { + arguments.add(runtime.createFunctionArgument(((ExpressionTypeExpression) expr).getExpression())); + } else { + arguments.add(runtime.createFunctionArgument(visit(expr))); + } + } + return resolved.apply(this, arguments); + } + + @Override + public T visitField(FieldExpression fieldExpression) { + return runtime.value(current, runtime.createString(fieldExpression.getName())); + } + + @Override + public T visitIndex(IndexExpression indexExpression) { + T index = runtime.createNumber(indexExpression.getIndex()); + T length = runtime.abstractLength(current); + T adjustedIndex = ifThenElse( + runtime.abstractLessThan(index, runtime.createNumber(0)), + add(length, index), + index); + T result = runtime.abstractElement(current, adjustedIndex); + + return ifThenElse( + runtime.abstractIs(current, RuntimeType.ARRAY), + result, + runtime.createNull()); + } + + @Override + public T visitLiteral(LiteralExpression literalExpression) { + // TODO: Handle when the literal is already wrapping a T + if (literalExpression.isStringValue()) { + return runtime.createString(literalExpression.expectStringValue()); + } else if (literalExpression.isBooleanValue()) { + return runtime.createBoolean(literalExpression.expectBooleanValue()); + } else if (literalExpression.isNumberValue()) { + return runtime.createNumber(literalExpression.expectNumberValue()); + } else if (literalExpression.isArrayValue()) { + JmespathRuntime.ArrayBuilder result = runtime.arrayBuilder(); + for (Object item : literalExpression.expectArrayValue()) { + result.add(visit(LiteralExpression.from(item))); + } + return result.build(); + } else if (literalExpression.isObjectValue()) { + JmespathRuntime.ObjectBuilder result = runtime.objectBuilder(); + for (Map.Entry entry : literalExpression.expectObjectValue().entrySet()) { + T key = runtime.createString(entry.getKey()); + T value = visit(LiteralExpression.from(entry.getValue())); + result.put(key, value); + } + return result.build(); + } else if (literalExpression.isNullValue()) { + return runtime.createNull(); + } + throw new IllegalArgumentException(String.format("Unrecognized literal: %s", literalExpression)); + } + + @Override + public T visitMultiSelectList(MultiSelectListExpression multiSelectListExpression) { + JmespathRuntime.ArrayBuilder output = runtime.arrayBuilder(); + for (JmespathExpression exp : multiSelectListExpression.getExpressions()) { + output.add(visit(exp)); + } + T result = output.build(); + + return ifThenElse( + runtime.abstractIs(current, RuntimeType.NULL), + current, + result); + } + + @Override + public T visitMultiSelectHash(MultiSelectHashExpression multiSelectHashExpression) { + JmespathRuntime.ObjectBuilder output = runtime.objectBuilder(); + for (Map.Entry expEntry : multiSelectHashExpression.getExpressions().entrySet()) { + output.put(runtime.createString(expEntry.getKey()), visit(expEntry.getValue())); + } + T result = output.build(); + + return ifThenElse( + runtime.abstractIs(current, RuntimeType.NULL), + current, + result); + } + + @Override + public T visitAnd(AndExpression andExpression) { + T left = visit(andExpression.getLeft()); + T right = visit(andExpression.getRight()); + + return ifThenElse(left, right, left); + } + + @Override + public T visitOr(OrExpression orExpression) { + T left = visit(orExpression.getLeft()); + T right = visit(orExpression.getRight()); + + return ifThenElse(left, left, right); + } + + @Override + public T visitNot(NotExpression notExpression) { + T output = visit(notExpression.getExpression()); + + return ifThenElse(output, runtime.createBoolean(false), runtime.createBoolean(true)); + } + + private static final JmespathExpression PROJECTION_FOLDER_TEMPLATE = + JmespathExpression.parse("append_if_not_null(acc, eval('rightExpr', element))"); + + @Override + public T visitProjection(ProjectionExpression projectionExpression) { + T left = visit(projectionExpression.getLeft()); + JmespathExpression rightExpr = projectionExpression.getRight(); + JmespathExpression folder = substitute(LiteralExpression.from("rightExpr"), rightExpr, PROJECTION_FOLDER_TEMPLATE); + + return ifThenElse( + runtime.abstractIs(left, RuntimeType.ARRAY), + foldLeft(runtime.arrayBuilder().build(), + folder, + left), + runtime.createNull()); + } + + private static final JmespathExpression FILTER_PROJECTION_FOLDER_TEMPLATE = + JmespathExpression.parse("append_if_not_null(acc, if(eval('condExpr', element), eval('rightExpr', element), null))"); + + @Override + public T visitFilterProjection(FilterProjectionExpression filterProjectionExpression) { + T left = visit(filterProjectionExpression.getLeft()); + JmespathExpression condExpr = filterProjectionExpression.getComparison(); + JmespathExpression rightExpr = filterProjectionExpression.getRight(); + JmespathExpression folder = substitute( + LiteralExpression.from("rightExpr"), rightExpr, + LiteralExpression.from("condExpr"), condExpr, + PROJECTION_FOLDER_TEMPLATE); + + return ifThenElse(runtime.abstractIs(left, RuntimeType.ARRAY), + foldLeft(runtime.arrayBuilder().build(), folder, left), + runtime.createNull()); + } + + private static final JmespathExpression OBJECT_PROJECTION_FOLDER_TEMPLATE = + JmespathExpression.parse("append_if_not_null(acc, if(value('left', element) != null, eval('rightExpr', value('left', element)), null))"); + + @Override + public T visitObjectProjection(ObjectProjectionExpression objectProjectionExpression) { + T left = visit(objectProjectionExpression.getLeft()); + JmespathExpression rightExpr = objectProjectionExpression.getRight(); + JmespathExpression folder = substitute( + LiteralExpression.from("left"), LiteralExpression.from(left), + LiteralExpression.from("rightExpr"), rightExpr, + PROJECTION_FOLDER_TEMPLATE); + + return ifThenElse(runtime.abstractIs(left, RuntimeType.ARRAY), + foldLeft(runtime.arrayBuilder().build(), folder, left), + runtime.createNull()); + } + + @Override + public T visitSlice(SliceExpression sliceExpression) { + // Just abstract this as an arbitrary subset of array elements for simplicity + // A fully precise abstract implementation of the logic would be a real pain + // and not worth the extra precision. + return ifThenElse(runtime.abstractIs(current, RuntimeType.ARRAY), + foldLeft(runtime.arrayBuilder().build(), + JmespathExpression.parse("append_if_not_null(acc, either(element, `null`))"), + current), + runtime.createNull()); + } + + @Override + public T visitSubexpression(Subexpression subexpression) { + T left = visit(subexpression.getLeft()); + return new AbstractEvaluator<>(left, runtime, functions).visit(subexpression.getRight()); + } + + // Helpers + + public T ifThenElse(T condition, T then, T otherwise) { + return functions.lookup(runtime, "if").apply(this, condition, then, otherwise); + } + + public T not(T value) { + return ifThenElse(value, runtime.createBoolean(false), runtime.createBoolean(true)); + } + + public T add(T left, T right) { + return functions.lookup(runtime, "add").apply(this, Arrays.asList( + runtime.createFunctionArgument(left), + runtime.createFunctionArgument(right) + )); + } + + public T foldLeft(T init, JmespathExpression folder, T collection) { + return functions.lookup(runtime, "fold_left").apply(this, Arrays.asList( + runtime.createFunctionArgument(init), + runtime.createFunctionArgument(folder), + runtime.createFunctionArgument(collection) + )); + } + + public T createAny() { + return RuntimeType.valueTypes().stream() + .map(runtime::createAny) + .reduce(runtime::either) + .orElseThrow(NoSuchElementException::new); + } + + JmespathExpression substitute(JmespathExpression from, JmespathExpression to, JmespathExpression expression) { + return expression.accept(new SubstitutionVisitor(e -> { + if (e.equals(from)) { + return to; + } + return null; + })); + } + + JmespathExpression substitute(JmespathExpression from1, JmespathExpression to1, JmespathExpression from2, JmespathExpression to2, JmespathExpression expression) { + return expression.accept(new SubstitutionVisitor(e -> { + if (e.equals(from1)) { + return to1; + } + if (e.equals(from2)) { + return to2; + } + return null; + })); + } + + JmespathExpression substitute(Map substitutions, JmespathExpression expression) { + return expression.accept(new SubstitutionVisitor(substitutions::get)); + } +} diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/AddFunction.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/AddFunction.java new file mode 100644 index 00000000000..3e21776c884 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/AddFunction.java @@ -0,0 +1,28 @@ +package software.amazon.smithy.jmespath.evaluation; + +import software.amazon.smithy.jmespath.RuntimeType; + +import java.util.List; + +class AddFunction implements Function { + + @Override + public String name() { + return "add"; + } + + @Override + public T abstractApply(AbstractEvaluator evaluator, List> functionArguments) { + return evaluator.runtime().createAny(RuntimeType.NUMBER); + } + + @Override + public T concreteApply(Evaluator evaluator, List> functionArguments) { + checkArgumentCount(2, functionArguments); + T left = functionArguments.get(0).expectNumber(); + T right = functionArguments.get(1).expectNumber(); + + Number result = EvaluationUtils.addNumbers(evaluator.runtime().asNumber(left), evaluator.runtime().asNumber(right)); + return evaluator.runtime().createNumber(result); + } +} diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/AppendFunction.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/AppendFunction.java new file mode 100644 index 00000000000..33b1586b835 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/AppendFunction.java @@ -0,0 +1,33 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.jmespath.evaluation; + +import software.amazon.smithy.jmespath.RuntimeType; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.List; + +class AppendFunction implements Function { + @Override + public String name() { + return "append"; + } + + @Override + public T abstractApply(AbstractEvaluator evaluator, List> functionArguments) { + JmespathAbstractRuntime runtime = evaluator.runtime(); + checkArgumentCount(2, functionArguments); + T array = functionArguments.get(0).expectArray(); + T value = functionArguments.get(1).expectValue(); + + return runtime.arrayBuilder().addAll(array).add(value).build(); + } + + @Override + public T concreteApply(Evaluator evaluator, List> functionArguments) { + return abstractApply(evaluator, functionArguments); + } +} diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/AppendIfNotNullFunction.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/AppendIfNotNullFunction.java new file mode 100644 index 00000000000..7ad69304ff9 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/AppendIfNotNullFunction.java @@ -0,0 +1,38 @@ +package software.amazon.smithy.jmespath.evaluation; + +import software.amazon.smithy.jmespath.RuntimeType; + +import java.util.List; + +class AppendIfNotNullFunction implements Function { + + @Override + public String name() { + return "append_if_not_null"; + } + + @Override + public T abstractApply(AbstractEvaluator evaluator, List> functionArguments) { + checkArgumentCount(2, functionArguments); + T array = functionArguments.get(0).expectArray(); + T value = functionArguments.get(1).expectValue(); + + return evaluator.ifThenElse( + evaluator.runtime().abstractIs(value, RuntimeType.NULL), + array, + evaluator.runtime().arrayBuilder().addAll(array).add(value).build()); + } + + @Override + public T concreteApply(Evaluator evaluator, List> functionArguments) { + checkArgumentCount(2, functionArguments); + T array = functionArguments.get(0).expectArray(); + T value = functionArguments.get(1).expectValue(); + + if (evaluator.runtime().is(value, RuntimeType.NULL)) { + return array; + } else { + return evaluator.runtime().arrayBuilder().addAll(array).add(value).build(); + } + } +} diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/AvgFunction.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/AvgFunction.java index 5f1090ccbe4..cb28493877b 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/AvgFunction.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/AvgFunction.java @@ -4,16 +4,25 @@ */ package software.amazon.smithy.jmespath.evaluation; +import software.amazon.smithy.jmespath.RuntimeType; + import java.util.List; -class AvgFunction implements Function { +class AvgFunction implements Function { @Override public String name() { return "avg"; } @Override - public T apply(JmespathRuntime runtime, List> functionArguments) { + public T abstractApply(AbstractEvaluator evaluator, List> functionArguments) { + JmespathAbstractRuntime runtime = evaluator.runtime(); + return runtime.either(runtime.createAny(RuntimeType.NUMBER), runtime.createNull()); + } + + @Override + public T concreteApply(Evaluator evaluator, List> functionArguments) { + JmespathRuntime runtime = evaluator.runtime(); checkArgumentCount(1, functionArguments); T array = functionArguments.get(0).expectArray(); Number length = runtime.length(array); diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/CeilFunction.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/CeilFunction.java index c67a3cdf47c..1b601c68592 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/CeilFunction.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/CeilFunction.java @@ -4,23 +4,31 @@ */ package software.amazon.smithy.jmespath.evaluation; +import software.amazon.smithy.jmespath.RuntimeType; + import java.math.BigDecimal; import java.math.RoundingMode; import java.util.List; -class CeilFunction implements Function { +class CeilFunction implements Function { @Override public String name() { return "ceil"; } @Override - public T apply(JmespathRuntime runtime, List> functionArguments) { + public T abstractApply(AbstractEvaluator evaluator, List> functionArguments) { + return evaluator.runtime().createAny(RuntimeType.NUMBER); + } + + @Override + public T concreteApply(Evaluator evaluator, List> functionArguments) { checkArgumentCount(1, functionArguments); T value = functionArguments.get(0).expectNumber(); - Number number = runtime.asNumber(value); - switch (runtime.numberType(value)) { + Number number = evaluator.runtime().asNumber(value); + + switch (evaluator.runtime().numberType(value)) { case BYTE: case SHORT: case INTEGER: @@ -28,11 +36,11 @@ public T apply(JmespathRuntime runtime, List> functio case BIG_INTEGER: return value; case BIG_DECIMAL: - return runtime.createNumber(((BigDecimal) number).setScale(0, RoundingMode.CEILING)); + return evaluator.runtime().createNumber(((BigDecimal) number).setScale(0, RoundingMode.CEILING)); case DOUBLE: - return runtime.createNumber(Math.ceil(number.doubleValue())); + return evaluator.runtime().createNumber(Math.ceil(number.doubleValue())); case FLOAT: - return runtime.createNumber(Math.ceil(number.floatValue())); + return evaluator.runtime().createNumber(Math.ceil(number.floatValue())); default: throw new RuntimeException("Unknown number type: " + number.getClass().getName()); } diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/ConcatFunction.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/ConcatFunction.java new file mode 100644 index 00000000000..4eb7c72521e --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/ConcatFunction.java @@ -0,0 +1,25 @@ +package software.amazon.smithy.jmespath.evaluation; + +import java.util.List; + +class ConcatFunction implements Function { + + @Override + public String name() { + return "concat"; + } + + @Override + public T abstractApply(AbstractEvaluator evaluator, List> functionArguments) { + return apply(evaluator, functionArguments); + } + + @Override + public T apply(AbstractEvaluator evaluator, List> functionArguments) { + checkArgumentCount(2, functionArguments); + T left = functionArguments.get(0).expectArray(); + T right = functionArguments.get(1).expectArray(); + + return evaluator.runtime().arrayBuilder().addAll(left).addAll(right).build(); + } +} diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/ContainsFunction.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/ContainsFunction.java index 1e0a5191703..238ec8db874 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/ContainsFunction.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/ContainsFunction.java @@ -7,15 +7,22 @@ import java.util.List; import software.amazon.smithy.jmespath.JmespathException; import software.amazon.smithy.jmespath.JmespathExceptionType; +import software.amazon.smithy.jmespath.RuntimeType; -class ContainsFunction implements Function { +class ContainsFunction implements Function { @Override public String name() { return "contains"; } @Override - public T apply(JmespathRuntime runtime, List> functionArguments) { + public T abstractApply(AbstractEvaluator evaluator, List> functionArguments) { + return evaluator.runtime().createAny(RuntimeType.BOOLEAN); + } + + @Override + public T concreteApply(Evaluator evaluator, List> functionArguments) { + JmespathRuntime runtime = evaluator.runtime(); checkArgumentCount(2, functionArguments); T subject = functionArguments.get(0).expectValue(); T search = functionArguments.get(1).expectValue(); diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/CoreExtension.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/CoreExtension.java new file mode 100644 index 00000000000..111d8805639 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/CoreExtension.java @@ -0,0 +1,54 @@ +package software.amazon.smithy.jmespath.evaluation; + +import software.amazon.smithy.jmespath.JmespathExtension; + +import java.util.ArrayList; +import java.util.List; + +public class CoreExtension implements JmespathExtension { + @Override + public List> getFunctions() { + List> result = new ArrayList<>(); + + // Builtins from the specification + result.add(new AbsFunction<>()); + result.add(new AvgFunction<>()); + result.add(new CeilFunction<>()); + result.add(new ContainsFunction<>()); + result.add(new EndsWithFunction<>()); + result.add(new FloorFunction<>()); + result.add(new JoinFunction<>()); + result.add(new KeysFunction<>()); + result.add(new LengthFunction<>()); + result.add(new MapFunction<>()); + result.add(new MaxFunction<>()); + result.add(new MergeFunction<>()); + result.add(new MaxByFunction<>()); + result.add(new MinFunction<>()); + result.add(new MinByFunction<>()); + result.add(new NotNullFunction<>()); + result.add(new ReverseFunction<>()); + result.add(new SortFunction<>()); + result.add(new SortByFunction<>()); + result.add(new StartsWithFunction<>()); + result.add(new SumFunction<>()); + result.add(new ToArrayFunction<>()); + result.add(new ToNumberFunction<>()); + result.add(new ToStringFunction<>()); + result.add(new TypeFunction<>()); + result.add(new ValuesFunction<>()); + + // TODO: Separate extension? + result.add(new AddFunction<>()); + result.add(new AppendFunction<>()); + result.add(new AppendIfNotNullFunction<>()); + result.add(new ConcatFunction<>()); + result.add(new EvalFunction<>()); + result.add(new EitherFunction<>()); + result.add(new IfFunction<>()); + result.add(new FoldLeftFunction<>()); + result.add(new OneNotNullFunction<>()); + + return result; + } +} diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/EitherFunction.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/EitherFunction.java new file mode 100644 index 00000000000..3d0cf8f0965 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/EitherFunction.java @@ -0,0 +1,29 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.jmespath.evaluation; + +import java.util.List; + +class EitherFunction implements Function { + @Override + public String name() { + return "either"; + } + + @Override + public T abstractApply(AbstractEvaluator evaluator, List> functionArguments) { + JmespathAbstractRuntime runtime = evaluator.runtime(); + checkArgumentCount(2, functionArguments); + T left = functionArguments.get(0).expectArray(); + T right = functionArguments.get(1).expectValue(); + + return runtime.either(left, right); + } + + @Override + public T concreteApply(Evaluator evaluator, List> functionArguments) { + return abstractApply(evaluator, functionArguments); + } +} diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/EndsWithFunction.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/EndsWithFunction.java index 4ad75a8c126..9b0ad545ce2 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/EndsWithFunction.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/EndsWithFunction.java @@ -4,16 +4,24 @@ */ package software.amazon.smithy.jmespath.evaluation; +import software.amazon.smithy.jmespath.RuntimeType; + import java.util.List; -class EndsWithFunction implements Function { +class EndsWithFunction implements Function { @Override public String name() { return "ends_with"; } @Override - public T apply(JmespathRuntime runtime, List> functionArguments) { + public T abstractApply(AbstractEvaluator evaluator, List> functionArguments) { + return evaluator.runtime().createAny(RuntimeType.BOOLEAN); + } + + @Override + public T concreteApply(Evaluator evaluator, List> functionArguments) { + JmespathRuntime runtime = evaluator.runtime(); checkArgumentCount(2, functionArguments); T subject = functionArguments.get(0).expectString(); T suffix = functionArguments.get(1).expectString(); diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/EvalFunction.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/EvalFunction.java new file mode 100644 index 00000000000..8464f7908b9 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/EvalFunction.java @@ -0,0 +1,28 @@ +package software.amazon.smithy.jmespath.evaluation; + +import software.amazon.smithy.jmespath.JmespathExpression; + +import java.util.List; + +public class EvalFunction implements Function { + @Override + public String name() { + return "eval"; + } + + @Override + public T abstractApply(AbstractEvaluator evaluator, List> functionArguments) { + // TODO: more precise, if the argument types are correct + return evaluator.createAny(); + } + + @Override + public T concreteApply(Evaluator evaluator, List> functionArguments) { + JmespathAbstractRuntime runtime = evaluator.runtime(); + checkArgumentCount(2, functionArguments); + JmespathExpression f = functionArguments.get(0).expectExpression(); + T value = functionArguments.get(1).expectValue(); + + return f.evaluate(value, runtime); + } +} diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/EvaluationUtils.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/EvaluationUtils.java index fef8a6188dc..f2fdb924fb9 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/EvaluationUtils.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/EvaluationUtils.java @@ -6,8 +6,12 @@ import java.math.BigDecimal; import java.math.BigInteger; +import java.util.Arrays; import java.util.Iterator; +import java.util.NoSuchElementException; import java.util.Objects; + +import software.amazon.smithy.jmespath.JmespathExpression; import software.amazon.smithy.jmespath.RuntimeType; /** @@ -141,4 +145,34 @@ public static boolean equals(JmespathRuntime runtime, T a, T b) { throw new IllegalStateException(); } } + + public static R convert(JmespathRuntime fromRuntime, T value, JmespathAbstractRuntime toRuntime) { + RuntimeType type = fromRuntime.typeOf(value); + switch (type) { + case NULL: + return toRuntime.createNull(); + case BOOLEAN: + return toRuntime.createBoolean(fromRuntime.asBoolean(value)); + case NUMBER: + return toRuntime.createNumber(fromRuntime.asNumber(value)); + case STRING: + return toRuntime.createString(fromRuntime.asString(value)); + case ARRAY: + JmespathAbstractRuntime.ArrayBuilder arrayBuilder = toRuntime.arrayBuilder(); + for (T element : fromRuntime.asIterable(value)) { + arrayBuilder.add(convert(fromRuntime, element, toRuntime)); + } + return arrayBuilder.build(); + case OBJECT: + JmespathAbstractRuntime.ObjectBuilder objectBuilder = toRuntime.objectBuilder(); + for (T key : fromRuntime.asIterable(value)) { + objectBuilder.put( + convert(fromRuntime, key, toRuntime), + convert(fromRuntime, fromRuntime.value(value, key), toRuntime)); + } + return objectBuilder.build(); + default: + throw new IllegalArgumentException("Unknown runtime type: " + type); + } + } } diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/Evaluator.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/Evaluator.java index 3f3ed3621da..9796c4d2280 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/Evaluator.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/Evaluator.java @@ -4,24 +4,17 @@ */ package software.amazon.smithy.jmespath.evaluation; -import java.util.ArrayList; -import java.util.List; import java.util.Map; -import software.amazon.smithy.jmespath.ExpressionVisitor; + import software.amazon.smithy.jmespath.JmespathException; import software.amazon.smithy.jmespath.JmespathExceptionType; import software.amazon.smithy.jmespath.JmespathExpression; import software.amazon.smithy.jmespath.RuntimeType; import software.amazon.smithy.jmespath.ast.AndExpression; import software.amazon.smithy.jmespath.ast.ComparatorExpression; -import software.amazon.smithy.jmespath.ast.CurrentExpression; -import software.amazon.smithy.jmespath.ast.ExpressionTypeExpression; -import software.amazon.smithy.jmespath.ast.FieldExpression; import software.amazon.smithy.jmespath.ast.FilterProjectionExpression; import software.amazon.smithy.jmespath.ast.FlattenExpression; -import software.amazon.smithy.jmespath.ast.FunctionExpression; import software.amazon.smithy.jmespath.ast.IndexExpression; -import software.amazon.smithy.jmespath.ast.LiteralExpression; import software.amazon.smithy.jmespath.ast.MultiSelectHashExpression; import software.amazon.smithy.jmespath.ast.MultiSelectListExpression; import software.amazon.smithy.jmespath.ast.NotExpression; @@ -31,21 +24,21 @@ import software.amazon.smithy.jmespath.ast.SliceExpression; import software.amazon.smithy.jmespath.ast.Subexpression; -public class Evaluator implements ExpressionVisitor { +public class Evaluator extends AbstractEvaluator { private final JmespathRuntime runtime; - // We could make this state mutable instead of creating lots of sub-Evaluators. - // This would make evaluation not thread-safe, but it's unclear how much that matters. - private final T current; + public Evaluator(T current, JmespathRuntime abstractRuntime) { + this(current, abstractRuntime, FunctionRegistry.getSPIRegistry()); + } - public Evaluator(T current, JmespathRuntime runtime) { - this.current = current; + public Evaluator(T current, JmespathRuntime runtime, FunctionRegistry functions) { + super(current, runtime, functions); this.runtime = runtime; } - public T visit(JmespathExpression expression) { - return expression.accept(this); + public JmespathRuntime runtime() { + return runtime; } @Override @@ -54,7 +47,7 @@ public T visitComparator(ComparatorExpression comparatorExpression) { T right = visit(comparatorExpression.getRight()); switch (comparatorExpression.getComparator()) { case EQUAL: - return runtime.createBoolean(runtime.equal(left, right)); + return runtime.abstractEqual(left, right); case NOT_EQUAL: return runtime.createBoolean(!runtime.equal(left, right)); // NOTE: Ordering operators >, >=, <, <= are only valid for numbers. All invalid @@ -88,16 +81,6 @@ public T visitComparator(ComparatorExpression comparatorExpression) { } } - @Override - public T visitCurrentNode(CurrentExpression currentExpression) { - return current; - } - - @Override - public T visitExpressionType(ExpressionTypeExpression expressionTypeExpression) { - return expressionTypeExpression.getExpression().accept(this); - } - @Override public T visitFlatten(FlattenExpression flattenExpression) { T value = visit(flattenExpression.getExpression()); @@ -110,33 +93,11 @@ public T visitFlatten(FlattenExpression flattenExpression) { for (T val : runtime.asIterable(value)) { if (runtime.is(val, RuntimeType.ARRAY)) { flattened.addAll(val); - continue; - } - flattened.add(val); - } - return flattened.build(); - } - - @Override - public T visitFunction(FunctionExpression functionExpression) { - Function function = FunctionRegistry.lookup(functionExpression.getName()); - if (function == null) { - throw new JmespathException(JmespathExceptionType.UNKNOWN_FUNCTION, functionExpression.getName()); - } - List> arguments = new ArrayList<>(); - for (JmespathExpression expr : functionExpression.getArguments()) { - if (expr instanceof ExpressionTypeExpression) { - arguments.add(FunctionArgument.of(runtime, ((ExpressionTypeExpression) expr).getExpression())); } else { - arguments.add(FunctionArgument.of(runtime, visit(expr))); + flattened.add(val); } } - return function.apply(runtime, arguments); - } - - @Override - public T visitField(FieldExpression fieldExpression) { - return runtime.value(current, runtime.createString(fieldExpression.getName())); + return flattened.build(); } @Override @@ -156,34 +117,6 @@ public T visitIndex(IndexExpression indexExpression) { return runtime.element(current, index); } - @Override - public T visitLiteral(LiteralExpression literalExpression) { - if (literalExpression.isStringValue()) { - return runtime.createString(literalExpression.expectStringValue()); - } else if (literalExpression.isBooleanValue()) { - return runtime.createBoolean(literalExpression.expectBooleanValue()); - } else if (literalExpression.isNumberValue()) { - return runtime.createNumber(literalExpression.expectNumberValue()); - } else if (literalExpression.isArrayValue()) { - JmespathRuntime.ArrayBuilder result = runtime.arrayBuilder(); - for (Object item : literalExpression.expectArrayValue()) { - result.add(visit(LiteralExpression.from(item))); - } - return result.build(); - } else if (literalExpression.isObjectValue()) { - JmespathRuntime.ObjectBuilder result = runtime.objectBuilder(); - for (Map.Entry entry : literalExpression.expectObjectValue().entrySet()) { - T key = runtime.createString(entry.getKey()); - T value = visit(LiteralExpression.from(entry.getValue())); - result.put(key, value); - } - return result.build(); - } else if (literalExpression.isNullValue()) { - return runtime.createNull(); - } - throw new IllegalArgumentException(String.format("Unrecognized literal: %s", literalExpression)); - } - @Override public T visitMultiSelectList(MultiSelectListExpression multiSelectListExpression) { if (runtime.is(current, RuntimeType.NULL)) { @@ -213,21 +146,23 @@ public T visitMultiSelectHash(MultiSelectHashExpression multiSelectHashExpressio @Override public T visitAnd(AndExpression andExpression) { T left = visit(andExpression.getLeft()); - return runtime.isTruthy(left) ? visit(andExpression.getRight()) : left; + T right = visit(andExpression.getRight()); + + return runtime.isTruthy(left) ? right : left; } @Override public T visitOr(OrExpression orExpression) { T left = visit(orExpression.getLeft()); - if (runtime.isTruthy(left)) { - return left; - } - return orExpression.getRight().accept(this); + T right = visit(orExpression.getRight()); + + return runtime.isTruthy(left) ? left : right; } @Override public T visitNot(NotExpression notExpression) { T output = visit(notExpression.getExpression()); + return runtime.createBoolean(!runtime.isTruthy(output)); } @@ -239,7 +174,7 @@ public T visitProjection(ProjectionExpression projectionExpression) { } JmespathRuntime.ArrayBuilder projectedResults = runtime.arrayBuilder(); for (T result : runtime.asIterable(resultList)) { - T projected = new Evaluator(result, runtime).visit(projectionExpression.getRight()); + T projected = new Evaluator(result, runtime, functions).visit(projectionExpression.getRight()); if (!runtime.typeOf(projected).equals(RuntimeType.NULL)) { projectedResults.add(projected); } @@ -255,9 +190,9 @@ public T visitFilterProjection(FilterProjectionExpression filterProjectionExpres } JmespathRuntime.ArrayBuilder results = runtime.arrayBuilder(); for (T val : runtime.asIterable(left)) { - T output = new Evaluator<>(val, runtime).visit(filterProjectionExpression.getComparison()); + T output = new Evaluator<>(val, runtime, functions).visit(filterProjectionExpression.getComparison()); if (runtime.isTruthy(output)) { - T result = new Evaluator<>(val, runtime).visit(filterProjectionExpression.getRight()); + T result = new Evaluator<>(val, runtime, functions).visit(filterProjectionExpression.getRight()); if (!runtime.is(result, RuntimeType.NULL)) { results.add(result); } @@ -276,7 +211,7 @@ public T visitObjectProjection(ObjectProjectionExpression objectProjectionExpres for (T member : runtime.asIterable(resultObject)) { T memberValue = runtime.value(resultObject, member); if (!runtime.is(memberValue, RuntimeType.NULL)) { - T projectedResult = new Evaluator(memberValue, runtime).visit(objectProjectionExpression.getRight()); + T projectedResult = new Evaluator<>(memberValue, runtime, functions).visit(objectProjectionExpression.getRight()); if (!runtime.is(projectedResult, RuntimeType.NULL)) { projectedResults.add(projectedResult); } @@ -335,6 +270,6 @@ public T visitSlice(SliceExpression sliceExpression) { @Override public T visitSubexpression(Subexpression subexpression) { T left = visit(subexpression.getLeft()); - return new Evaluator<>(left, runtime).visit(subexpression.getRight()); + return new Evaluator<>(left, runtime, functions).visit(subexpression.getRight()); } } diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/FloorFunction.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/FloorFunction.java index 433c3c2fb51..95030423be9 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/FloorFunction.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/FloorFunction.java @@ -4,23 +4,30 @@ */ package software.amazon.smithy.jmespath.evaluation; +import software.amazon.smithy.jmespath.RuntimeType; + import java.math.BigDecimal; import java.math.RoundingMode; import java.util.List; -class FloorFunction implements Function { +class FloorFunction implements Function { @Override public String name() { return "floor"; } @Override - public T apply(JmespathRuntime runtime, List> functionArguments) { + public T abstractApply(AbstractEvaluator evaluator, List> functionArguments) { + return evaluator.runtime().createAny(RuntimeType.NUMBER); + } + + @Override + public T concreteApply(Evaluator evaluator, List> functionArguments) { checkArgumentCount(1, functionArguments); T value = functionArguments.get(0).expectNumber(); - Number number = runtime.asNumber(value); + Number number = evaluator.runtime().asNumber(value); - switch (runtime.numberType(value)) { + switch (evaluator.runtime().numberType(value)) { case BYTE: case SHORT: case INTEGER: @@ -28,11 +35,11 @@ public T apply(JmespathRuntime runtime, List> functio case BIG_INTEGER: return value; case BIG_DECIMAL: - return runtime.createNumber(((BigDecimal) number).setScale(0, RoundingMode.FLOOR)); + return evaluator.runtime().createNumber(((BigDecimal) number).setScale(0, RoundingMode.FLOOR)); case DOUBLE: - return runtime.createNumber(Math.floor(number.doubleValue())); + return evaluator.runtime().createNumber(Math.floor(number.doubleValue())); case FLOAT: - return runtime.createNumber(Math.floor(number.floatValue())); + return evaluator.runtime().createNumber(Math.floor(number.floatValue())); default: throw new RuntimeException("Unknown number type: " + number.getClass().getName()); } diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/FoldLeftFunction.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/FoldLeftFunction.java new file mode 100644 index 00000000000..df637121153 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/FoldLeftFunction.java @@ -0,0 +1,38 @@ +package software.amazon.smithy.jmespath.evaluation; + +import software.amazon.smithy.jmespath.JmespathExpression; + +import java.util.List; + +/** + * fold_left(0, &(acc + element), [1, 2, 3]) == 6 + */ +class FoldLeftFunction implements Function { + @Override + public String name() { + return "fold_left"; + } + + @Override + public T abstractApply(AbstractEvaluator evaluator, List> functionArguments) { + throw new UnsupportedOperationException(); + } + + @Override + public T concreteApply(Evaluator evaluator, List> functionArguments) { + JmespathRuntime runtime = evaluator.runtime(); + checkArgumentCount(3, functionArguments); + T result = functionArguments.get(0).expectValue(); + JmespathExpression f = functionArguments.get(1).expectExpression(); + T collection = functionArguments.get(2).expectValue(); + + for (T element : runtime.asIterable(collection)) { + T fCurrent = runtime.objectBuilder() + .put(runtime.createString("acc"), result) + .put(runtime.createString("element"), element) + .build(); + result = f.evaluate(fCurrent, runtime); + } + return result; + } +} diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/Function.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/Function.java index bfea8376d6c..4c96a991a5d 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/Function.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/Function.java @@ -4,22 +4,54 @@ */ package software.amazon.smithy.jmespath.evaluation; +import java.util.Arrays; +import java.util.Collections; import java.util.List; import software.amazon.smithy.jmespath.JmespathException; import software.amazon.smithy.jmespath.JmespathExceptionType; +import software.amazon.smithy.jmespath.type.Type; -interface Function { +public interface Function { String name(); - T apply(JmespathRuntime runtime, List> arguments); + default T apply(AbstractEvaluator evaluator, List> arguments) { + if (evaluator instanceof Evaluator) { + return concreteApply((Evaluator)evaluator, arguments); + } else { + return abstractApply(evaluator, arguments); + } + } + + T abstractApply(AbstractEvaluator evaluator, List> arguments); + + default T concreteApply(Evaluator evaluator, List> arguments) { + return abstractApply(evaluator, arguments); + } // Helpers - default void checkArgumentCount(int n, List> arguments) { + default void checkArgumentCount(int n, List> arguments) { if (arguments.size() != n) { throw new JmespathException(JmespathExceptionType.INVALID_ARITY, String.format("Expected %d arguments, got %d", n, arguments.size())); } } + + default T apply(AbstractEvaluator evaluator, T arg0) { + return apply(evaluator, Collections.singletonList(evaluator.runtime().createFunctionArgument(arg0))); + } + + default T apply(AbstractEvaluator evaluator, T arg0, T arg1) { + return apply(evaluator, Arrays.asList( + evaluator.runtime().createFunctionArgument(arg0), + evaluator.runtime().createFunctionArgument(arg1))); + } + + default T apply(AbstractEvaluator evaluator, T arg0, T arg1, T arg2) { + return apply(evaluator, Arrays.asList( + evaluator.runtime().createFunctionArgument(arg0), + evaluator.runtime().createFunctionArgument(arg1), + evaluator.runtime().createFunctionArgument(arg2))); + } } diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/FunctionArgument.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/FunctionArgument.java index d8d1ce18e68..118a7d34e9c 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/FunctionArgument.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/FunctionArgument.java @@ -10,55 +10,48 @@ import software.amazon.smithy.jmespath.JmespathExpression; import software.amazon.smithy.jmespath.RuntimeType; -abstract class FunctionArgument { +public interface FunctionArgument { - protected final JmespathRuntime runtime; + T expectValue(); - protected FunctionArgument(JmespathRuntime runtime) { - this.runtime = runtime; - } + T expectType(RuntimeType runtimeType); - public T expectValue() { - throw new JmespathException(JmespathExceptionType.INVALID_TYPE, "invalid-type"); - } + T expectAnyOf(Set types); - public T expectString() { - throw new JmespathException(JmespathExceptionType.INVALID_TYPE, "invalid-type"); + default T expectString() { + return expectType(RuntimeType.STRING); } - public T expectNumber() { - throw new JmespathException(JmespathExceptionType.INVALID_TYPE, "invalid-type"); + default T expectNumber() { + return expectType(RuntimeType.NUMBER); } - public T expectArray() { - throw new JmespathException(JmespathExceptionType.INVALID_TYPE, "invalid-type"); + default T expectArray() { + return expectType(RuntimeType.ARRAY); } - public T expectObject() { - throw new JmespathException(JmespathExceptionType.INVALID_TYPE, "invalid-type"); + default T expectObject() { + return expectType(RuntimeType.OBJECT); } - public T expectAnyOf(Set types) { + default JmespathExpression expectExpression() { throw new JmespathException(JmespathExceptionType.INVALID_TYPE, "invalid-type"); } - public JmespathExpression expectExpression() { - throw new JmespathException(JmespathExceptionType.INVALID_TYPE, "invalid-type"); - } - - public static FunctionArgument of(JmespathRuntime runtime, JmespathExpression expression) { - return new Expression(runtime, expression); + static FunctionArgument of(JmespathRuntime runtime, JmespathExpression expression) { + return new Expression<>(runtime, expression); } - public static FunctionArgument of(JmespathRuntime runtime, T value) { - return new Value(runtime, value); + static FunctionArgument of(JmespathRuntime runtime, T value) { + return new Value<>(runtime, value); } - static class Value extends FunctionArgument { + class Value implements FunctionArgument { + JmespathRuntime runtime; T value; public Value(JmespathRuntime runtime, T value) { - super(runtime); + this.runtime = runtime; this.value = value; } @@ -67,7 +60,7 @@ public T expectValue() { return value; } - protected T expectType(RuntimeType runtimeType) { + public T expectType(RuntimeType runtimeType) { if (runtime.is(value, runtimeType)) { return value; } else { @@ -76,40 +69,38 @@ protected T expectType(RuntimeType runtimeType) { } public T expectAnyOf(Set types) { + // TODO: Handle abstract runtimes with a chained ifThenElse + // OR have abstract implementations of functions check types inline instead if (types.contains(runtime.typeOf(value))) { return value; } else { throw new JmespathException(JmespathExceptionType.INVALID_TYPE, "invalid-type"); } } + } - @Override - public T expectString() { - return expectType(RuntimeType.STRING); - } + class Expression implements FunctionArgument { + JmespathRuntime runtime; + JmespathExpression expression; - @Override - public T expectNumber() { - return expectType(RuntimeType.NUMBER); + public Expression(JmespathRuntime runtime, JmespathExpression expression) { + this.runtime = runtime; + this.expression = expression; } @Override - public T expectArray() { - return expectType(RuntimeType.ARRAY); + public T expectValue() { + return runtime.createError(JmespathExceptionType.INVALID_TYPE, "invalid-type"); } @Override - public T expectObject() { - return expectType(RuntimeType.OBJECT); + public T expectType(RuntimeType runtimeType) { + return runtime.createError(JmespathExceptionType.INVALID_TYPE, "invalid-type"); } - } - static class Expression extends FunctionArgument { - JmespathExpression expression; - - public Expression(JmespathRuntime runtime, JmespathExpression expression) { - super(runtime); - this.expression = expression; + @Override + public T expectAnyOf(Set types) { + return runtime.createError(JmespathExceptionType.INVALID_TYPE, "invalid-type"); } @Override diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/FunctionRegistry.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/FunctionRegistry.java index 6d24d143e12..c13733bb10f 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/FunctionRegistry.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/FunctionRegistry.java @@ -4,49 +4,49 @@ */ package software.amazon.smithy.jmespath.evaluation; +import software.amazon.smithy.jmespath.JmespathException; +import software.amazon.smithy.jmespath.JmespathExceptionType; +import software.amazon.smithy.jmespath.JmespathExtension; + import java.util.HashMap; import java.util.Map; +import java.util.ServiceLoader; + +public final class FunctionRegistry { + + public static FunctionRegistry getSPIRegistry() { + FunctionRegistry result = new FunctionRegistry<>(); + + for (JmespathExtension extension : ServiceLoader.load(JmespathExtension.class, FunctionRegistry.class.getClassLoader())) { + extension.getFunctions().forEach(result::registerFunction); + } -final class FunctionRegistry { + return result; + } - private static final Map BUILTINS = new HashMap<>(); + private final Map> functions = new HashMap<>(); - private static void registerFunction(Function function) { - if (BUILTINS.put(function.name(), function) != null) { + public void registerFunction(Function function) { + if (functions.put(function.name(), function) != null) { throw new IllegalArgumentException("Duplicate function name: " + function.name()); } } - static { - registerFunction(new AbsFunction()); - registerFunction(new AvgFunction()); - registerFunction(new CeilFunction()); - registerFunction(new ContainsFunction()); - registerFunction(new EndsWithFunction()); - registerFunction(new FloorFunction()); - registerFunction(new JoinFunction()); - registerFunction(new KeysFunction()); - registerFunction(new LengthFunction()); - registerFunction(new MapFunction()); - registerFunction(new MaxFunction()); - registerFunction(new MergeFunction()); - registerFunction(new MaxByFunction()); - registerFunction(new MinFunction()); - registerFunction(new MinByFunction()); - registerFunction(new NotNullFunction()); - registerFunction(new ReverseFunction()); - registerFunction(new SortFunction()); - registerFunction(new SortByFunction()); - registerFunction(new StartsWithFunction()); - registerFunction(new SumFunction()); - registerFunction(new ToArrayFunction()); - registerFunction(new ToNumberFunction()); - registerFunction(new ToStringFunction()); - registerFunction(new TypeFunction()); - registerFunction(new ValuesFunction()); + public Function lookup(String name) { + return functions.get(name); } - static Function lookup(String name) { - return BUILTINS.get(name); + public Function lookup(JmespathAbstractRuntime runtime, String name) { + Function result = runtime.resolveFunction(name); + if (result != null) { + return result; + } + + result = functions.get(name); + if (result != null) { + return result; + } + + throw new JmespathException(JmespathExceptionType.UNKNOWN_FUNCTION, "Unknown function: " + name); } } diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/IfFunction.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/IfFunction.java new file mode 100644 index 00000000000..d0b04941e98 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/IfFunction.java @@ -0,0 +1,30 @@ +package software.amazon.smithy.jmespath.evaluation; + +import java.util.List; + +class IfFunction implements Function { + @Override + public String name() { + return "if"; + } + + @Override + public T abstractApply(AbstractEvaluator evaluator, List> functionArguments) { + T thenValue = functionArguments.get(1).expectValue(); + T elseValue = functionArguments.get(2).expectValue(); + + // TODO: Have to pass on any error from the condition + return evaluator.runtime().either(thenValue, elseValue); + } + + @Override + public T concreteApply(Evaluator evaluator, List> functionArguments) { + checkArgumentCount(3, functionArguments); + T condition = functionArguments.get(0).expectValue(); + T thenValue = functionArguments.get(1).expectValue(); + // TODO: could be optional, defaulting to NULL or true? + T elseValue = functionArguments.get(2).expectValue(); + + return evaluator.runtime().isTruthy(condition) ? thenValue : elseValue; + } +} diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/JmespathAbstractRuntime.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/JmespathAbstractRuntime.java new file mode 100644 index 00000000000..304cb1d543c --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/JmespathAbstractRuntime.java @@ -0,0 +1,186 @@ +package software.amazon.smithy.jmespath.evaluation; + +import software.amazon.smithy.jmespath.JmespathExceptionType; +import software.amazon.smithy.jmespath.JmespathExpression; +import software.amazon.smithy.jmespath.RuntimeType; + +public interface JmespathAbstractRuntime { + + /////////////////////////////// + // General Operations + /////////////////////////////// + + T abstractTypeOf(T value); + + T abstractIs(T value, RuntimeType type); + + T abstractEqual(T a, T b); + + T abstractLessThan(T a, T b); + + T abstractToString(T value); + + /////////////////////////////// + // Arbitrary values + /////////////////////////////// + + // Throws unsupported if the runtime is concrete + T createAny(RuntimeType runtimeType); + + // Throws unsupported if the runtime is concrete + T either(T left, T right); + + /////////////////////////////// + // NULLs + /////////////////////////////// + + /** + * Returns `null`. + *

+ * Runtimes may or may not use a Java null value to represent a JSON null value. + */ + T createNull(); + + /////////////////////////////// + // BOOLEANs + /////////////////////////////// + + /** + * Creates a BOOLEAN value. + */ + T createBoolean(boolean b); + + /////////////////////////////// + // STRINGs + /////////////////////////////// + + /** + * Creates a STRING value. + */ + T createString(String string); + + /////////////////////////////// + // NUMBERs + /////////////////////////////// + + /** + * Creates a NUMBER value. + */ + T createNumber(Number value); + + /////////////////////////////// + // ARRAYs + /////////////////////////////// + + /** + * Creates a new ArrayBuilder. + */ + // TODO: Default implementation of wrapping an immutable array value and using append and concat? + JmespathRuntime.ArrayBuilder arrayBuilder(); + + /** + * A builder interface for new ARRAY values. + */ + interface ArrayBuilder { + + /** + * Adds the given value to the array being built. + */ + JmespathRuntime.ArrayBuilder add(T value); + + /** + * If the given value is an ARRAY, adds all the elements of the array. + * If the given value is an OBJECT, adds all the keys of the object. + * Otherwise, throws a JmespathException of type INVALID_TYPE. + */ + JmespathRuntime.ArrayBuilder addAll(T collection); + + /** + * Builds the new ARRAY value being built. + */ + T build(); + } + + /** + * If the given value is an ARRAY, returns the element at the given index. + * Otherwise, throws a JmespathException of type INVALID_TYPE. + */ + T element(T array, int index); + + T abstractElement(T array, T index); + + /////////////////////////////// + // OBJECTs + /////////////////////////////// + + /** + * Creates a new ObjectBuilder. + */ + // TODO: Default implementation of wrapping an immutable object value and using merge? + // Don't want any concrete runtime to use that though. + JmespathRuntime.ObjectBuilder objectBuilder(); + + /** + * A builder interface for new OBJECT values. + */ + interface ObjectBuilder { + + /** + * Adds the given key/value pair to the object being built. + */ + JmespathRuntime.ObjectBuilder put(T key, T value); + + /** + * If the given value is an OBJECT, adds all of its key/value pairs. + * Otherwise, throws a JmespathException of type INVALID_TYPE. + */ + JmespathRuntime.ObjectBuilder putAll(T object); + + /** + * Builds the new OBJECT value being built. + */ + T build(); + } + + /** + * If the given value is an OBJECT, returns the value mapped to the given key. + * Otherwise, returns NULL. + */ + T value(T object, T key); + + /////////////////////////////// + // Common collection operations for ARRAYs and OBJECTs + ///////////////////////////////34e + + T abstractLength(T value); + + /////////////////////////////// + // Functions + /////////////////////////////// + + /** + * Resolve a function expression. + * The runtime can provide more optimized implementations of specific functions, + * or more abstracted versions for abstract runtimes. + * It can also provide runtime-native functions. + * + * @return + */ + default Function resolveFunction(String name) { + return null; + } + + FunctionArgument createFunctionArgument(T value); + + FunctionArgument createFunctionArgument(JmespathExpression expression); + + /////////////////////////////// + // Errors + /////////////////////////////// + + // Throws the error immediately if the runtime is concrete + T createError(JmespathExceptionType type, String message); + + // Throws unsupported if the runtime is concrete + T createExpression(JmespathExpression expression); +} diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/JmespathRuntime.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/JmespathRuntime.java index e2fe1dacc14..c95b7ed5ed5 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/JmespathRuntime.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/JmespathRuntime.java @@ -6,9 +6,16 @@ import java.util.Collection; import java.util.Comparator; +import java.util.EnumSet; +import java.util.function.BiFunction; + import software.amazon.smithy.jmespath.JmespathException; import software.amazon.smithy.jmespath.JmespathExceptionType; +import software.amazon.smithy.jmespath.JmespathExpression; import software.amazon.smithy.jmespath.RuntimeType; +import software.amazon.smithy.jmespath.ast.FunctionExpression; +import software.amazon.smithy.jmespath.ast.ResolvedFunctionExpression; +import software.amazon.smithy.jmespath.type.Type; /** * An interface to provide the operations needed for JMESPath expression evaluation @@ -22,7 +29,7 @@ * refer to T value where typeOf(value) returns RuntimeType.NULL. * A runtime may or may not use a Java `null` value for this purpose. */ -public interface JmespathRuntime extends Comparator { +public interface JmespathRuntime extends JmespathAbstractRuntime, Comparator { /////////////////////////////// // General Operations @@ -35,6 +42,11 @@ public interface JmespathRuntime extends Comparator { */ RuntimeType typeOf(T value); + @Override + default T abstractTypeOf(T value) { + return createString(typeOf(value).toString()); + } + /** * Shorthand for {@code typeOf(value).equals(type)}. */ @@ -42,6 +54,11 @@ default boolean is(T value, RuntimeType type) { return typeOf(value).equals(type); } + @Override + default T abstractIs(T value, RuntimeType type) { + return abstractEqual(abstractTypeOf(value), createString(type.toString())); + } + /** * Returns true iff the given value is truthy according * to the JMESPath specification. @@ -80,6 +97,11 @@ default boolean equal(T a, T b) { return EvaluationUtils.equals(this, a, b); } + @Override + default T abstractEqual(T a, T b) { + return createBoolean(equal(a, b)); + } + @Override default int compare(T a, T b) { if (is(a, RuntimeType.STRING) && is(b, RuntimeType.STRING)) { @@ -91,6 +113,20 @@ default int compare(T a, T b) { } } + default T abstractLessThan(T a, T b) { + return createBoolean(compare(a, b) < 0); + } + + @Override + default T createAny(RuntimeType runtimeType) { + throw new UnsupportedOperationException("anyValue called on concrete runtime"); + } + + @Override + default T either(T left, T right) { + throw new UnsupportedOperationException("either called on concrete runtime"); + } + /** * Returns a JSON string representation of the given value. *

@@ -144,26 +180,14 @@ default String toString(T value) { } } - /////////////////////////////// - // NULLs - /////////////////////////////// - - /** - * Returns `null`. - *

- * Runtimes may or may not use a Java null value to represent a JSON null value. - */ - T createNull(); + default T abstractToString(T value) { + return createString(toString(value)); + } /////////////////////////////// // BOOLEANs /////////////////////////////// - /** - * Creates a BOOLEAN value. - */ - T createBoolean(boolean b); - /** * If the given value is a BOOLEAN, return it as a boolean. * Otherwise, throws a JmespathException of type INVALID_TYPE. @@ -174,11 +198,6 @@ default String toString(T value) { // STRINGs /////////////////////////////// - /** - * Creates a STRING value. - */ - T createString(String string); - /** * If the given value is a STRING, return it as a String. * Otherwise, throws a JmespathException of type INVALID_TYPE. @@ -192,11 +211,6 @@ default String toString(T value) { // NUMBERs /////////////////////////////// - /** - * Creates a NUMBER value. - */ - T createNumber(Number value); - /** * Returns the type of Number that asNumber() will produce for this value. * Will be more efficient for some runtimes than checking the class of asNumber(). @@ -213,40 +227,15 @@ default String toString(T value) { // ARRAYs /////////////////////////////// - /** - * Creates a new ArrayBuilder. - */ - ArrayBuilder arrayBuilder(); - - /** - * A builder interface for new ARRAY values. - */ - interface ArrayBuilder { - - /** - * Adds the given value to the array being built. - */ - void add(T value); - - /** - * If the given value is an ARRAY, adds all the elements of the array. - * If the given value is an OBJECT, adds all the keys of the object. - * Otherwise, throws a JmespathException of type INVALID_TYPE. - */ - void addAll(T collection); - - /** - * Builds the new ARRAY value being built. - */ - T build(); + @Override + default T abstractElement(T array, T index) { + if (is(index, RuntimeType.NUMBER)) { + return element(array, asNumber(index).intValue()); + } else { + return createError(JmespathExceptionType.INVALID_TYPE, "Expected number"); + } } - /** - * If the given value is an ARRAY, returns the element at the given index. - * Otherwise, throws a JmespathException of type INVALID_TYPE. - */ - T element(T array, int index); - /** * If the given value is an ARRAY, returns the specified slice. * Otherwise, throws a JmespathException of type INVALID_TYPE. @@ -279,43 +268,6 @@ default T slice(T array, int start, int stop, int step) { return output.build(); } - /////////////////////////////// - // OBJECTs - /////////////////////////////// - - /** - * Creates a new ObjectBuilder. - */ - ObjectBuilder objectBuilder(); - - /** - * A builder interface for new OBJECT values. - */ - interface ObjectBuilder { - - /** - * Adds the given key/value pair to the object being built. - */ - void put(T key, T value); - - /** - * If the given value is an OBJECT, adds all of its key/value pairs. - * Otherwise, throws a JmespathException of type INVALID_TYPE. - */ - void putAll(T object); - - /** - * Builds the new OBJECT value being built. - */ - T build(); - } - - /** - * If the given value is an OBJECT, returns the value mapped to the given key. - * Otherwise, returns NULL. - */ - T value(T object, T key); - /////////////////////////////// // Common collection operations for ARRAYs and OBJECTs /////////////////////////////// @@ -326,9 +278,45 @@ interface ObjectBuilder { */ int length(T value); + default T abstractLength(T value) { + if (is(value, RuntimeType.ARRAY) || is(value, RuntimeType.OBJECT) || is(value, RuntimeType.STRING)) { + return createNumber(length(value)); + } else { + return createNull(); + } + } + /** * Iterate over the elements of an ARRAY or the keys of an OBJECT. * Otherwise, throws a JmespathException of type INVALID_TYPE. */ Iterable asIterable(T value); + + /////////////////////////////// + // Functions + /////////////////////////////// + + @Override + default FunctionArgument createFunctionArgument(T value) { + return FunctionArgument.of(this, value); + } + + @Override + default FunctionArgument createFunctionArgument(JmespathExpression expression) { + return FunctionArgument.of(this, expression); + } + + /////////////////////////////// + // Errors + /////////////////////////////// + + @Override + default T createError(JmespathExceptionType type, String message) { + throw new JmespathException(type, message); + } + + @Override + default T createExpression(JmespathExpression expression) { + throw new UnsupportedOperationException("createExpression"); + } } diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/JoinFunction.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/JoinFunction.java index 6171a0fcd60..dec94fa4139 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/JoinFunction.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/JoinFunction.java @@ -4,16 +4,24 @@ */ package software.amazon.smithy.jmespath.evaluation; +import software.amazon.smithy.jmespath.RuntimeType; + import java.util.List; -class JoinFunction implements Function { +class JoinFunction implements Function { @Override public String name() { return "join"; } @Override - public T apply(JmespathRuntime runtime, List> functionArguments) { + public T abstractApply(AbstractEvaluator evaluator, List> functionArguments) { + return evaluator.runtime().createAny(RuntimeType.STRING); + } + + @Override + public T concreteApply(Evaluator evaluator, List> functionArguments) { + JmespathRuntime runtime = evaluator.runtime(); checkArgumentCount(2, functionArguments); String separator = runtime.asString(functionArguments.get(0).expectString()); T array = functionArguments.get(1).expectArray(); diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/KeysFunction.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/KeysFunction.java index 941ba87db1b..e39cf9e01ee 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/KeysFunction.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/KeysFunction.java @@ -4,21 +4,26 @@ */ package software.amazon.smithy.jmespath.evaluation; +import software.amazon.smithy.jmespath.RuntimeType; + import java.util.List; -class KeysFunction implements Function { +class KeysFunction implements Function { @Override public String name() { return "keys"; } @Override - public T apply(JmespathRuntime runtime, List> functionArguments) { + public T abstractApply(AbstractEvaluator evaluator, List> functionArguments) { + return evaluator.runtime().createAny(RuntimeType.ARRAY); + } + + @Override + public T concreteApply(Evaluator evaluator, List> functionArguments) { checkArgumentCount(1, functionArguments); T value = functionArguments.get(0).expectObject(); - JmespathRuntime.ArrayBuilder arrayBuilder = runtime.arrayBuilder(); - arrayBuilder.addAll(value); - return arrayBuilder.build(); + return evaluator.runtime().arrayBuilder().addAll(value).build(); } } diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/LengthFunction.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/LengthFunction.java index 9fffa767267..f094d0f4dae 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/LengthFunction.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/LengthFunction.java @@ -9,7 +9,7 @@ import java.util.Set; import software.amazon.smithy.jmespath.RuntimeType; -class LengthFunction implements Function { +class LengthFunction implements Function { private static final Set PARAMETER_TYPES = new HashSet<>(); static { @@ -23,11 +23,17 @@ public String name() { return "length"; } + + @Override + public T abstractApply(AbstractEvaluator evaluator, List> functionArguments) { + return evaluator.runtime().createAny(RuntimeType.NUMBER); + } + @Override - public T apply(JmespathRuntime runtime, List> functionArguments) { + public T concreteApply(Evaluator evaluator, List> functionArguments) { checkArgumentCount(1, functionArguments); T value = functionArguments.get(0).expectAnyOf(PARAMETER_TYPES); - return runtime.createNumber(runtime.length(value)); + return evaluator.runtime().abstractLength(value); } } diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/ListArrayBuilder.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/ListArrayBuilder.java index ee54e506a9e..a75997b77e2 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/ListArrayBuilder.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/ListArrayBuilder.java @@ -25,12 +25,13 @@ public ListArrayBuilder(JmespathRuntime runtime, Function, T> wrappin } @Override - public void add(T value) { + public ListArrayBuilder add(T value) { result.add(value); + return this; } @Override - public void addAll(T array) { + public ListArrayBuilder addAll(T array) { Iterable iterable = runtime.asIterable(array); if (iterable instanceof Collection) { result.addAll((Collection) iterable); @@ -39,6 +40,7 @@ public void addAll(T array) { result.add(value); } } + return this; } @Override diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/MapFunction.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/MapFunction.java index 0683477208d..3cb55bba45b 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/MapFunction.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/MapFunction.java @@ -5,16 +5,35 @@ package software.amazon.smithy.jmespath.evaluation; import java.util.List; +import java.util.Map; + import software.amazon.smithy.jmespath.JmespathExpression; +import software.amazon.smithy.jmespath.ast.LiteralExpression; + +class MapFunction implements Function { + + private static final JmespathExpression FOLDER_TEMPLATE = JmespathExpression.parse("append(acc, eval('mapper', element))"); -class MapFunction implements Function { @Override public String name() { return "map"; } @Override - public T apply(JmespathRuntime runtime, List> functionArguments) { + public T abstractApply(AbstractEvaluator evaluator, List> functionArguments) { + JmespathAbstractRuntime runtime = evaluator.runtime(); + checkArgumentCount(2, functionArguments); + JmespathExpression mapper = functionArguments.get(0).expectExpression(); + T array = functionArguments.get(1).expectArray(); + + T acc = runtime.arrayBuilder().build(); + JmespathExpression folder = evaluator.substitute(LiteralExpression.from("mapper"), mapper, FOLDER_TEMPLATE); + return evaluator.foldLeft(acc, folder, array); + } + + @Override + public T concreteApply(Evaluator evaluator, List> functionArguments) { + JmespathRuntime runtime = evaluator.runtime(); checkArgumentCount(2, functionArguments); JmespathExpression expression = functionArguments.get(0).expectExpression(); T array = functionArguments.get(1).expectArray(); diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/MapObjectBuilder.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/MapObjectBuilder.java index a8c481a087f..fcfa8704701 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/MapObjectBuilder.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/MapObjectBuilder.java @@ -24,17 +24,19 @@ public MapObjectBuilder(JmespathRuntime runtime, Function, T> } @Override - public void put(T key, T value) { + public MapObjectBuilder put(T key, T value) { result.put(runtime.asString(key), value); + return this; } @Override - public void putAll(T object) { + public MapObjectBuilder putAll(T object) { // A fastpath for when object is a Map doesn't quite work, // because you would need to know that it's specifically a Map. for (T key : runtime.asIterable(object)) { result.put(runtime.asString(key), runtime.value(object, key)); } + return this; } @Override diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/MaxByFunction.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/MaxByFunction.java index 464fe7fb490..332221638b5 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/MaxByFunction.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/MaxByFunction.java @@ -7,14 +7,21 @@ import java.util.List; import software.amazon.smithy.jmespath.JmespathExpression; -class MaxByFunction implements Function { +class MaxByFunction implements Function { @Override public String name() { return "max_by"; } @Override - public T apply(JmespathRuntime runtime, List> functionArguments) { + public T abstractApply(AbstractEvaluator evaluator, List> functionArguments) { + // TODO: Can do better via fold_left + return evaluator.createAny(); + } + + @Override + public T concreteApply(Evaluator evaluator, List> functionArguments) { + JmespathRuntime runtime = evaluator.runtime(); checkArgumentCount(2, functionArguments); T array = functionArguments.get(0).expectArray(); JmespathExpression expression = functionArguments.get(1).expectExpression(); diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/MaxFunction.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/MaxFunction.java index 76661558e99..56d9205c33e 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/MaxFunction.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/MaxFunction.java @@ -4,16 +4,27 @@ */ package software.amazon.smithy.jmespath.evaluation; +import software.amazon.smithy.jmespath.RuntimeType; + import java.util.List; -class MaxFunction implements Function { +class MaxFunction implements Function { @Override public String name() { return "max"; } @Override - public T apply(JmespathRuntime runtime, List> functionArguments) { + public T abstractApply(AbstractEvaluator evaluator, List> functionArguments) { + JmespathAbstractRuntime runtime = evaluator.runtime(); + return runtime.either(runtime.createAny(RuntimeType.NUMBER), + runtime.either(runtime.createAny(RuntimeType.STRING), + runtime.createNull())); + } + + @Override + public T concreteApply(Evaluator evaluator, List> functionArguments) { + JmespathRuntime runtime = evaluator.runtime(); checkArgumentCount(1, functionArguments); T array = functionArguments.get(0).expectArray(); if (runtime.length(array) == 0) { diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/MergeFunction.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/MergeFunction.java index d50356eb504..bce4d4fe67c 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/MergeFunction.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/MergeFunction.java @@ -6,15 +6,20 @@ import java.util.List; -class MergeFunction implements Function { +class MergeFunction implements Function { @Override public String name() { return "merge"; } @Override - public T apply(JmespathRuntime runtime, List> functionArguments) { - JmespathRuntime.ObjectBuilder builder = runtime.objectBuilder(); + public T concreteApply(Evaluator evaluator, List> functionArguments) { + return abstractApply(evaluator, functionArguments); + } + + @Override + public T abstractApply(AbstractEvaluator evaluator, List> functionArguments) { + JmespathRuntime.ObjectBuilder builder = evaluator.runtime().objectBuilder(); for (FunctionArgument arg : functionArguments) { T object = arg.expectObject(); diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/MinByFunction.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/MinByFunction.java index 0d1e90fff0c..10c6c26a48c 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/MinByFunction.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/MinByFunction.java @@ -7,31 +7,37 @@ import java.util.List; import software.amazon.smithy.jmespath.JmespathExpression; -class MinByFunction implements Function { +class MinByFunction implements Function { @Override public String name() { return "min_by"; } @Override - public T apply(JmespathRuntime runtime, List> functionArguments) { + public T abstractApply(AbstractEvaluator evaluator, List> functionArguments) { + // TODO: Can do better via fold_left + return evaluator.createAny(); + } + + @Override + public T concreteApply(Evaluator evaluator, List> functionArguments) { checkArgumentCount(2, functionArguments); T array = functionArguments.get(0).expectArray(); JmespathExpression expression = functionArguments.get(1).expectExpression(); - if (runtime.length(array) == 0) { - return runtime.createNull(); + if (evaluator.runtime().length(array) == 0) { + return evaluator.runtime().createNull(); } T min = null; T minBy = null; boolean first = true; - for (T element : runtime.asIterable(array)) { - T by = expression.evaluate(element, runtime); + for (T element : evaluator.runtime().asIterable(array)) { + T by = expression.evaluate(element, evaluator.runtime()); if (first) { first = false; min = element; minBy = by; - } else if (runtime.compare(by, minBy) < 0) { + } else if (evaluator.runtime().compare(by, minBy) < 0) { min = element; minBy = by; } diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/MinFunction.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/MinFunction.java index 058bb459380..bfbf38d9a83 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/MinFunction.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/MinFunction.java @@ -4,16 +4,27 @@ */ package software.amazon.smithy.jmespath.evaluation; +import software.amazon.smithy.jmespath.RuntimeType; + import java.util.List; -class MinFunction implements Function { +class MinFunction implements Function { @Override public String name() { return "min"; } @Override - public T apply(JmespathRuntime runtime, List> functionArguments) { + public T abstractApply(AbstractEvaluator evaluator, List> functionArguments) { + JmespathAbstractRuntime runtime = evaluator.runtime(); + return runtime.either(runtime.createAny(RuntimeType.NUMBER), + runtime.either(runtime.createAny(RuntimeType.STRING), + runtime.createNull())); + } + + @Override + public T concreteApply(Evaluator evaluator, List> functionArguments) { + JmespathRuntime runtime = evaluator.runtime(); checkArgumentCount(1, functionArguments); T array = functionArguments.get(0).expectArray(); if (runtime.length(array) == 0) { diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/NotNullFunction.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/NotNullFunction.java index 1092f7fcca0..c6d63112bd3 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/NotNullFunction.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/NotNullFunction.java @@ -5,28 +5,44 @@ package software.amazon.smithy.jmespath.evaluation; import java.util.List; +import java.util.ListIterator; + import software.amazon.smithy.jmespath.JmespathException; import software.amazon.smithy.jmespath.JmespathExceptionType; import software.amazon.smithy.jmespath.RuntimeType; -class NotNullFunction implements Function { +class NotNullFunction implements Function { @Override public String name() { return "not_null"; } @Override - public T apply(JmespathRuntime runtime, List> functionArguments) { + public T abstractApply(AbstractEvaluator evaluator, List> functionArguments) { + T result = evaluator.runtime().createNull(); + ListIterator> iter = functionArguments.listIterator(functionArguments.size()); + while (iter.hasPrevious()) { + T value = iter.previous().expectValue(); + result = evaluator.ifThenElse( + evaluator.runtime().abstractIs(value, RuntimeType.NULL), result, value); + } + return result; + } + + + @Override + public T concreteApply(Evaluator evaluator, List> functionArguments) { if (functionArguments.isEmpty()) { throw new JmespathException(JmespathExceptionType.INVALID_ARITY, - "Expected at least 1 arguments, got 0"); + "Expected at least 1 argument, got 0"); } + for (FunctionArgument arg : functionArguments) { T value = arg.expectValue(); - if (!runtime.is(value, RuntimeType.NULL)) { + if (!evaluator.runtime().is(value, RuntimeType.NULL)) { return value; } } - return runtime.createNull(); + return evaluator.runtime().createNull(); } } diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/OneNotNullFunction.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/OneNotNullFunction.java new file mode 100644 index 00000000000..a42ec06458d --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/OneNotNullFunction.java @@ -0,0 +1,48 @@ +package software.amazon.smithy.jmespath.evaluation; + +import software.amazon.smithy.jmespath.JmespathException; +import software.amazon.smithy.jmespath.JmespathExceptionType; +import software.amazon.smithy.jmespath.RuntimeType; + +import java.util.List; +import java.util.ListIterator; + +class OneNotNullFunction implements Function { + @Override + public String name() { + return "one_not_null"; + } + + @Override + public T abstractApply(AbstractEvaluator evaluator, List> functionArguments) { + T result = evaluator.runtime().createNull(); + ListIterator> iter = functionArguments.listIterator(functionArguments.size()); + while (iter.hasPrevious()) { + T value = iter.previous().expectValue(); + result = evaluator.ifThenElse( + evaluator.runtime().abstractIs(value, RuntimeType.NULL), result, value); + } + return result; + } + + @Override + public T concreteApply(Evaluator evaluator, List> functionArguments) { + if (functionArguments.isEmpty()) { + throw new JmespathException(JmespathExceptionType.INVALID_ARITY, + "Expected at least 1 argument, got 0"); + } + + boolean found = false; + for (FunctionArgument arg : functionArguments) { + T value = arg.expectValue(); + if (!evaluator.runtime().is(value, RuntimeType.NULL)) { + if (found) { + return evaluator.runtime().createBoolean(false); + } else { + found = true; + } + } + } + return evaluator.runtime().createBoolean(found); + } +} diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/ReverseFunction.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/ReverseFunction.java index b2089b5059f..3064ee53032 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/ReverseFunction.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/ReverseFunction.java @@ -11,7 +11,7 @@ import java.util.Set; import software.amazon.smithy.jmespath.RuntimeType; -class ReverseFunction implements Function { +class ReverseFunction implements Function { private static final Set PARAMETER_TYPES = new HashSet<>(); static { PARAMETER_TYPES.add(RuntimeType.STRING); @@ -24,7 +24,15 @@ public String name() { } @Override - public T apply(JmespathRuntime runtime, List> functionArguments) { + public T abstractApply(AbstractEvaluator evaluator, List> functionArguments) { + JmespathAbstractRuntime runtime = evaluator.runtime(); + return runtime.either(runtime.createAny(RuntimeType.STRING), + runtime.createAny(RuntimeType.ARRAY)); + } + + @Override + public T concreteApply(Evaluator evaluator, List> functionArguments) { + JmespathRuntime runtime = evaluator.runtime(); checkArgumentCount(1, functionArguments); T value = functionArguments.get(0).expectAnyOf(PARAMETER_TYPES); diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/SortByFunction.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/SortByFunction.java index ef9287445c8..6b1c54ed841 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/SortByFunction.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/SortByFunction.java @@ -8,15 +8,22 @@ import java.util.Collections; import java.util.List; import software.amazon.smithy.jmespath.JmespathExpression; +import software.amazon.smithy.jmespath.RuntimeType; -class SortByFunction implements Function { +class SortByFunction implements Function { @Override public String name() { return "sort_by"; } @Override - public T apply(JmespathRuntime runtime, List> functionArguments) { + public T abstractApply(AbstractEvaluator evaluator, List> functionArguments) { + return evaluator.runtime().createAny(RuntimeType.ARRAY); + } + + @Override + public T concreteApply(Evaluator evaluator, List> functionArguments) { + JmespathRuntime runtime = evaluator.runtime(); checkArgumentCount(2, functionArguments); T array = functionArguments.get(0).expectArray(); JmespathExpression expression = functionArguments.get(1).expectExpression(); @@ -26,7 +33,7 @@ public T apply(JmespathRuntime runtime, List> functio elements.add(element); } - Collections.sort(elements, (a, b) -> { + elements.sort((a, b) -> { T aValue = expression.evaluate(a, runtime); T bValue = expression.evaluate(b, runtime); return runtime.compare(aValue, bValue); diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/SortFunction.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/SortFunction.java index e5aa1cc1e2e..c72c26492f9 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/SortFunction.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/SortFunction.java @@ -4,17 +4,25 @@ */ package software.amazon.smithy.jmespath.evaluation; +import software.amazon.smithy.jmespath.RuntimeType; + import java.util.ArrayList; import java.util.List; -class SortFunction implements Function { +class SortFunction implements Function { @Override public String name() { return "sort"; } @Override - public T apply(JmespathRuntime runtime, List> functionArguments) { + public T abstractApply(AbstractEvaluator evaluator, List> functionArguments) { + return evaluator.runtime().createAny(RuntimeType.ARRAY); + } + + @Override + public T concreteApply(Evaluator evaluator, List> functionArguments) { + JmespathRuntime runtime = evaluator.runtime(); checkArgumentCount(1, functionArguments); T array = functionArguments.get(0).expectArray(); diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/StartsWithFunction.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/StartsWithFunction.java index 2403cc9cc6f..c6ac61271bb 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/StartsWithFunction.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/StartsWithFunction.java @@ -4,23 +4,30 @@ */ package software.amazon.smithy.jmespath.evaluation; +import software.amazon.smithy.jmespath.RuntimeType; + import java.util.List; -class StartsWithFunction implements Function { +class StartsWithFunction implements Function { @Override public String name() { return "starts_with"; } @Override - public T apply(JmespathRuntime runtime, List> functionArguments) { + public T abstractApply(AbstractEvaluator evaluator, List> functionArguments) { + return evaluator.runtime().createAny(RuntimeType.BOOLEAN); + } + + @Override + public T concreteApply(Evaluator evaluator, List> functionArguments) { checkArgumentCount(2, functionArguments); T subject = functionArguments.get(0).expectString(); T prefix = functionArguments.get(1).expectString(); - String subjectStr = runtime.asString(subject); - String prefixStr = runtime.asString(prefix); + String subjectStr = evaluator.runtime().asString(subject); + String prefixStr = evaluator.runtime().asString(prefix); - return runtime.createBoolean(subjectStr.startsWith(prefixStr)); + return evaluator.runtime().createBoolean(subjectStr.startsWith(prefixStr)); } } diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/SumFunction.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/SumFunction.java index 35a16ef3599..8558b8e9c49 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/SumFunction.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/SumFunction.java @@ -4,18 +4,37 @@ */ package software.amazon.smithy.jmespath.evaluation; +import software.amazon.smithy.jmespath.JmespathExpression; +import software.amazon.smithy.jmespath.RuntimeType; + import java.util.List; -class SumFunction implements Function { +class SumFunction implements Function { + + private static final JmespathExpression EXPRESSION = JmespathExpression.parse("fold_left(`0`, &add(acc, element), [0])"); + @Override public String name() { return "sum"; } @Override - public T apply(JmespathRuntime runtime, List> functionArguments) { + public T abstractApply(AbstractEvaluator evaluator, List> functionArguments) { + JmespathAbstractRuntime runtime = evaluator.runtime(); checkArgumentCount(1, functionArguments); T array = functionArguments.get(0).expectArray(); + + T args = runtime.arrayBuilder().add(array).build(); + return EXPRESSION.evaluate(args, runtime); + } + + + @Override + public T concreteApply(Evaluator evaluator, List> functionArguments) { + JmespathRuntime runtime = evaluator.runtime(); + checkArgumentCount(1, functionArguments); + T array = functionArguments.get(0).expectArray(); + Number sum = 0L; for (T element : runtime.asIterable(array)) { sum = EvaluationUtils.addNumbers(sum, runtime.asNumber(element)); diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/ToArrayFunction.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/ToArrayFunction.java index f7258473025..c68f28d1c40 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/ToArrayFunction.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/ToArrayFunction.java @@ -4,26 +4,36 @@ */ package software.amazon.smithy.jmespath.evaluation; +import java.util.Arrays; import java.util.List; import software.amazon.smithy.jmespath.RuntimeType; -class ToArrayFunction implements Function { +class ToArrayFunction implements Function { @Override public String name() { return "to_array"; } @Override - public T apply(JmespathRuntime runtime, List> functionArguments) { + public T abstractApply(AbstractEvaluator evaluator, List> functionArguments) { + JmespathAbstractRuntime runtime = evaluator.runtime(); + checkArgumentCount(1, functionArguments); + T value = functionArguments.get(0).expectValue(); + + T isArray = runtime.abstractIs(value, RuntimeType.ARRAY); + return evaluator.ifThenElse(isArray, value, runtime.arrayBuilder().add(value).build()); + } + + @Override + public T concreteApply(Evaluator evaluator, List> functionArguments) { + JmespathRuntime runtime = evaluator.runtime(); checkArgumentCount(1, functionArguments); T value = functionArguments.get(0).expectValue(); if (runtime.is(value, RuntimeType.ARRAY)) { return value; } else { - JmespathRuntime.ArrayBuilder builder = runtime.arrayBuilder(); - builder.add(value); - return builder.build(); + return runtime.arrayBuilder().add(value).build(); } } } diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/ToNumberFunction.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/ToNumberFunction.java index acb5e7cbf36..3f35c849ed4 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/ToNumberFunction.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/ToNumberFunction.java @@ -4,16 +4,25 @@ */ package software.amazon.smithy.jmespath.evaluation; +import software.amazon.smithy.jmespath.RuntimeType; + import java.util.List; -class ToNumberFunction implements Function { +class ToNumberFunction implements Function { @Override public String name() { return "to_number"; } @Override - public T apply(JmespathRuntime runtime, List> functionArguments) { + public T abstractApply(AbstractEvaluator evaluator, List> functionArguments) { + JmespathAbstractRuntime runtime = evaluator.runtime(); + return runtime.either(runtime.createAny(RuntimeType.NUMBER), runtime.createNull()); + } + + @Override + public T concreteApply(Evaluator evaluator, List> functionArguments) { + JmespathRuntime runtime = evaluator.runtime(); checkArgumentCount(1, functionArguments); T value = functionArguments.get(0).expectValue(); diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/ToStringFunction.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/ToStringFunction.java index fb6025be3c8..2a7d826bddd 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/ToStringFunction.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/ToStringFunction.java @@ -4,24 +4,38 @@ */ package software.amazon.smithy.jmespath.evaluation; +import software.amazon.smithy.jmespath.RuntimeType; + import java.util.List; -class ToStringFunction implements Function { +class ToStringFunction implements Function { @Override public String name() { return "to_string"; } @Override - public T apply(JmespathRuntime runtime, List> functionArguments) { + public T abstractApply(AbstractEvaluator evaluator, List> functionArguments) { + JmespathAbstractRuntime runtime = evaluator.runtime(); + checkArgumentCount(1, functionArguments); + T value = functionArguments.get(0).expectValue(); + + return evaluator.ifThenElse( + runtime.abstractIs(value, RuntimeType.STRING), + value, + runtime.abstractToString(value)); + } + + @Override + public T concreteApply(Evaluator evaluator, List> functionArguments) { + JmespathRuntime runtime = evaluator.runtime(); checkArgumentCount(1, functionArguments); T value = functionArguments.get(0).expectValue(); - switch (runtime.typeOf(value)) { - case STRING: - return value; - default: - return runtime.createString(runtime.toString(value)); + if (runtime.is(value, RuntimeType.STRING)) { + return value; + } else { + return runtime.createString(runtime.toString(value)); } } } diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/TypeFunction.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/TypeFunction.java index 4039e2c43af..bc6611d0a4f 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/TypeFunction.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/TypeFunction.java @@ -6,16 +6,15 @@ import java.util.List; -class TypeFunction implements Function { +class TypeFunction implements Function { @Override public String name() { return "type"; } @Override - public T apply(JmespathRuntime runtime, List> functionArguments) { + public T abstractApply(AbstractEvaluator evaluator, List> functionArguments) { checkArgumentCount(1, functionArguments); - T value = functionArguments.get(0).expectValue(); - return runtime.createString(runtime.typeOf(value).toString()); + return evaluator.runtime().abstractTypeOf(functionArguments.get(0).expectValue()); } } diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/ValuesFunction.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/ValuesFunction.java index 154acc170b3..a6b25ab0ab9 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/ValuesFunction.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/ValuesFunction.java @@ -4,16 +4,24 @@ */ package software.amazon.smithy.jmespath.evaluation; +import software.amazon.smithy.jmespath.RuntimeType; + import java.util.List; -class ValuesFunction implements Function { +class ValuesFunction implements Function { @Override public String name() { return "values"; } @Override - public T apply(JmespathRuntime runtime, List> functionArguments) { + public T abstractApply(AbstractEvaluator evaluator, List> functionArguments) { + return evaluator.runtime().createAny(RuntimeType.ARRAY); + } + + @Override + public T concreteApply(Evaluator evaluator, List> functionArguments) { + JmespathRuntime runtime = evaluator.runtime(); checkArgumentCount(1, functionArguments); T value = functionArguments.get(0).expectObject(); diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/type/AbstractType.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/type/AbstractType.java new file mode 100644 index 00000000000..1b0e40dfa70 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/type/AbstractType.java @@ -0,0 +1,20 @@ +package software.amazon.smithy.jmespath.type; + +import software.amazon.smithy.jmespath.JmespathExceptionType; +import software.amazon.smithy.jmespath.RuntimeType; + +import java.util.Set; + +public abstract class AbstractType implements Type { + + protected abstract RuntimeType runtimeType(); + + @Override + public Type expectAnyOf(Set types) { + if (types.contains(runtimeType())) { + return this; + } else { + return new ErrorType(JmespathExceptionType.INVALID_TYPE); + } + } +} diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/type/AnyType.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/type/AnyType.java new file mode 100644 index 00000000000..1f35f36cdd4 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/type/AnyType.java @@ -0,0 +1,42 @@ +package software.amazon.smithy.jmespath.type; + +import software.amazon.smithy.jmespath.RuntimeType; +import software.amazon.smithy.jmespath.evaluation.JmespathRuntime; + +import java.util.EnumSet; +import java.util.Set; + +public class AnyType implements Type { + + public static final AnyType INSTANCE = new AnyType(); + + @Override + public boolean equals(Object obj) { + return obj instanceof AnyType; + } + + @Override + public int hashCode() { + return AnyType.class.hashCode(); + } + + @Override + public boolean isInstance(T value, JmespathRuntime runtime) { + return true; + } + + @Override + public Type elementType() { + return INSTANCE; + } + + @Override + public Type valueType(Type key) { + return INSTANCE; + } + + @Override + public Type expectAnyOf(Set types) { + return this; + } +} diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/type/ArrayType.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/type/ArrayType.java new file mode 100644 index 00000000000..d7c992b361b --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/type/ArrayType.java @@ -0,0 +1,66 @@ +package software.amazon.smithy.jmespath.type; + +import software.amazon.smithy.jmespath.RuntimeType; +import software.amazon.smithy.jmespath.evaluation.JmespathRuntime; + +import java.util.EnumSet; + +public final class ArrayType extends AbstractType { + + // Never null - array is equivalent to array + private final Type member; + + public ArrayType(Type member) { + this.member = member; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof ArrayType) { + ArrayType that = (ArrayType) obj; + return this.member.equals(that.member); + } + + return false; + } + + @Override + public int hashCode() { + return ArrayType.class.hashCode() + member.hashCode(); + } + + @Override + protected RuntimeType runtimeType() { + return RuntimeType.ARRAY; + } + + @Override + public boolean isInstance(T array, JmespathRuntime runtime) { + if (!runtime.is(array, RuntimeType.ARRAY)) { + return false; + } + + for (T value : runtime.asIterable(array)) { + if (!member.isInstance(value, runtime)) { + return false; + } + } + + return true; + } + + @Override + public Type elementType(Type index) { + return elementType(); + } + + @Override + public Type elementType() { + return Type.unionType(member, Type.nullType()); + } + + @Override + public String toString() { + return "array[" + member + "]"; + } +} diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/type/BottomType.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/type/BottomType.java new file mode 100644 index 00000000000..1e806a4ca1d --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/type/BottomType.java @@ -0,0 +1,50 @@ +package software.amazon.smithy.jmespath.type; + +import software.amazon.smithy.jmespath.JmespathExceptionType; +import software.amazon.smithy.jmespath.RuntimeType; +import software.amazon.smithy.jmespath.evaluation.JmespathRuntime; + +import java.util.EnumSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class BottomType implements Type { + + public static final BottomType INSTANCE = new BottomType(); + + @Override + public boolean equals(Object obj) { + return obj instanceof BottomType; + } + + @Override + public int hashCode() { + return BottomType.class.hashCode(); + } + + @Override + public boolean isInstance(T value, JmespathRuntime runtime) { + return false; + } + + @Override + public Type elementType() { + return INSTANCE; + } + + @Override + public Type valueType(Type key) { + return INSTANCE; + } + + @Override + public Type expectAnyOf(Set types) { + return new ErrorType(JmespathExceptionType.INVALID_TYPE); + } + + @Override + public String toString() { + return "bottom"; + } +} diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/type/ErrorType.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/type/ErrorType.java new file mode 100644 index 00000000000..8ead1fa8054 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/type/ErrorType.java @@ -0,0 +1,53 @@ +package software.amazon.smithy.jmespath.type; + +import software.amazon.smithy.jmespath.JmespathExceptionType; +import software.amazon.smithy.jmespath.RuntimeType; +import software.amazon.smithy.jmespath.evaluation.JmespathRuntime; + +import java.util.EnumSet; +import java.util.Objects; +import java.util.Set; + +public class ErrorType implements Type { + + private static final EnumSet TYPES = EnumSet.noneOf(RuntimeType.class); + + // The type of error, or null if unknown + private final JmespathExceptionType errorType; + + public ErrorType(JmespathExceptionType errorType) { + this.errorType = errorType; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ErrorType)) { + return false; + } + + ErrorType that = (ErrorType)obj; + return Objects.equals(errorType, that.errorType); + } + + @Override + public int hashCode() { + return ErrorType.class.hashCode() + Objects.hashCode(errorType); + } + + @Override + public boolean isInstance(T value, JmespathRuntime runtime) { + // Errors are not actually runtime values + return false; + } + + @Override + public Type expectAnyOf(Set types) { + // We're already an error :) + return this; + } + + @Override + public String toString() { + return "error[" + errorType + "]"; + } +} diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/type/ExpressionType.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/type/ExpressionType.java new file mode 100644 index 00000000000..ed9e65c2a3d --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/type/ExpressionType.java @@ -0,0 +1,57 @@ +package software.amazon.smithy.jmespath.type; + +import software.amazon.smithy.jmespath.JmespathExceptionType; +import software.amazon.smithy.jmespath.JmespathExpression; +import software.amazon.smithy.jmespath.RuntimeType; +import software.amazon.smithy.jmespath.evaluation.JmespathRuntime; + +import java.util.EnumSet; +import java.util.Objects; +import java.util.Set; + +public class ExpressionType extends AbstractType { + + private static final EnumSet TYPES = EnumSet.of(RuntimeType.EXPRESSION); + + private final JmespathExpression expression; + + public ExpressionType(JmespathExpression expression) { + this.expression = expression; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ExpressionType)) { + return false; + } + + ExpressionType that = (ExpressionType)obj; + return Objects.equals(expression, that.expression); + } + + @Override + public int hashCode() { + return ExpressionType.class.hashCode() + Objects.hashCode(expression); + } + + @Override + protected RuntimeType runtimeType() { + return RuntimeType.EXPRESSION; + } + + @Override + public boolean isInstance(T value, JmespathRuntime runtime) { + // Expressions are not actually runtime values + return false; + } + + @Override + public Type expectAnyOf(Set types) { + return new ErrorType(JmespathExceptionType.INVALID_TYPE); + } + + @Override + public JmespathExpression expectExpression() { + return expression; + } +} diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/type/FoldLeftFunction.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/type/FoldLeftFunction.java new file mode 100644 index 00000000000..a685c0e3dd0 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/type/FoldLeftFunction.java @@ -0,0 +1,48 @@ +package software.amazon.smithy.jmespath.type; + +import software.amazon.smithy.jmespath.JmespathExpression; +import software.amazon.smithy.jmespath.evaluation.AbstractEvaluator; +import software.amazon.smithy.jmespath.evaluation.Evaluator; +import software.amazon.smithy.jmespath.evaluation.Function; +import software.amazon.smithy.jmespath.evaluation.FunctionArgument; +import software.amazon.smithy.jmespath.evaluation.FunctionRegistry; +import software.amazon.smithy.jmespath.evaluation.JmespathAbstractRuntime; +import software.amazon.smithy.jmespath.evaluation.JmespathRuntime; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class FoldLeftFunction implements Function { + @Override + public String name() { + return "fold_left"; + } + + @Override + public Type abstractApply(AbstractEvaluator evaluator, List> functionArguments) { + Type init = functionArguments.get(0).expectValue(); + JmespathExpression f = functionArguments.get(1).expectExpression(); + Type array = functionArguments.get(2).expectArray(); + + // "evaluate" f in a typing context of {acc: result, element: array.elementType()} + // and determine the fix point + // TODO: If `array` is more specific (say a @length limit) we may not need to do that. + // TODO: This may actually not terminate in some cases, say if init is a TupleType and f extends the tuple. + // But we could detect that and widen to an ArrayType first, which won't grow in the same way. + Type result = init; + Type prevResult = null; + while (!result.equals(prevResult)) { + Map contextTypes = new HashMap<>(); + contextTypes.put(evaluator.runtime().createString("acc"), result); + contextTypes.put(evaluator.runtime().createString("element"), array.elementType()); + Type fContextType = new ObjectType(contextTypes); + + prevResult = result; + // TODO: Not passing along the function registry + result = evaluator.runtime().either(result, f.evaluate(fContextType, evaluator.runtime())); + } + return result; + } +} diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/type/JustRuntimeType.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/type/JustRuntimeType.java new file mode 100644 index 00000000000..6b47cee299f --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/type/JustRuntimeType.java @@ -0,0 +1,45 @@ +package software.amazon.smithy.jmespath.type; + +import software.amazon.smithy.jmespath.RuntimeType; +import software.amazon.smithy.jmespath.evaluation.JmespathRuntime; + +import java.util.EnumSet; + +public class JustRuntimeType extends AbstractType { + + private final RuntimeType runtimeType; + + public JustRuntimeType(RuntimeType runtimeType) { + this.runtimeType = runtimeType; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof JustRuntimeType)) { + return false; + } + + JustRuntimeType other = (JustRuntimeType) obj; + return runtimeType.equals(other.runtimeType); + } + + @Override + public int hashCode() { + return runtimeType.hashCode(); + } + + @Override + protected RuntimeType runtimeType() { + return runtimeType; + } + + @Override + public boolean isInstance(T value, JmespathRuntime runtime) { + return runtime.is(value, runtimeType); + } + + @Override + public String toString() { + return "null"; + } +} diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/type/MapType.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/type/MapType.java new file mode 100644 index 00000000000..197c05e1f05 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/type/MapType.java @@ -0,0 +1,65 @@ +package software.amazon.smithy.jmespath.type; + +import software.amazon.smithy.jmespath.RuntimeType; +import software.amazon.smithy.jmespath.evaluation.JmespathRuntime; + +import java.util.EnumSet; +import java.util.Map; + +public class MapType extends AbstractType { + + private final Type keyType; + private final Type valueType; + + public MapType(Type keyType, Type valueType) { + this.keyType = keyType; + this.valueType = valueType; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof MapType) { + MapType other = (MapType) obj; + return keyType.equals(other.keyType) && valueType.equals(other.valueType); + } + return false; + } + + @Override + public int hashCode() { + return keyType.hashCode() * 31 + valueType.hashCode(); + } + + @Override + protected RuntimeType runtimeType() { + return RuntimeType.OBJECT; + } + + @Override + public boolean isInstance(T object, JmespathRuntime runtime) { + if (!runtime.is(object, RuntimeType.OBJECT)) { + return false; + } + for (T key : runtime.asIterable(object)) { + if (!keyType.isInstance(key, runtime)) { + return false; + } + T value = runtime.value(object, key); + if (!valueType.isInstance(value, runtime)) { + return false; + } + } + + return true; + } + + @Override + public Type valueType(Type key) { + return Type.unionType(valueType, Type.nullType()); + } + + @Override + public String toString() { + return "map[" + keyType + ", " + valueType + "]"; + } +} diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/type/ObjectType.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/type/ObjectType.java new file mode 100644 index 00000000000..7b7929a8db4 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/type/ObjectType.java @@ -0,0 +1,74 @@ +package software.amazon.smithy.jmespath.type; + +import software.amazon.smithy.jmespath.RuntimeType; +import software.amazon.smithy.jmespath.evaluation.JmespathRuntime; + +import java.util.EnumSet; +import java.util.Map; +import java.util.Objects; + +// TODO: RecordType? StructureType? +public class ObjectType extends AbstractType { + + // TODO: Optional keys as well (may not be present, but if so has type X) + // Not the same thing as always present but mapped to null + private final Map properties; + + public ObjectType(Map properties) { + this.properties = properties; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ObjectType)) { + return false; + } + + ObjectType other = (ObjectType) obj; + return Objects.equals(properties, other.properties); + } + + @Override + public int hashCode() { + return Objects.hashCode(properties); + } + + @Override + protected RuntimeType runtimeType() { + return RuntimeType.OBJECT; + } + + @Override + public boolean isInstance(T value, JmespathRuntime runtime) { + if (!runtime.is(value, RuntimeType.OBJECT)){ + return false; + } + + if (properties != null) { + // TODO + return false; + } else { + return true; + } + } + + @Override + public Type valueType(Type key) { + return properties == null ? Type.anyType() : properties.getOrDefault(key, Type.nullType()); + } + + @Override + public String toString() { + if (properties == null) { + return "object"; + } + + StringBuilder builder = new StringBuilder(); + builder.append("object<"); + for (Map.Entry entry : properties.entrySet()) { + builder.append(entry.getKey()).append(": ").append(entry.getValue()).append(", "); + } + builder.append('>'); + return builder.toString(); + } +} diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/type/TupleType.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/type/TupleType.java new file mode 100644 index 00000000000..5b31999b0fc --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/type/TupleType.java @@ -0,0 +1,81 @@ +package software.amazon.smithy.jmespath.type; + +import software.amazon.smithy.jmespath.RuntimeType; +import software.amazon.smithy.jmespath.evaluation.JmespathRuntime; + +import java.util.EnumSet; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; + +public class TupleType extends AbstractType { + + // Never null - array is equivalent to array + private final List members; + + public TupleType(List members) { + this.members = members; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof TupleType)) { + return false; + } + TupleType other = (TupleType) obj; + return members.equals(other.members); + } + + @Override + public int hashCode() { + return members.hashCode(); + } + + @Override + protected RuntimeType runtimeType() { + return RuntimeType.ARRAY; + } + + @Override + public boolean isInstance(T array, JmespathRuntime runtime) { + if (!runtime.is(array, RuntimeType.ARRAY)) { + return false; + } + + Iterator memberIter = members.iterator(); + Iterator valueIter = runtime.asIterable(array).iterator(); + while (valueIter.hasNext()) { + if (!memberIter.hasNext()) { + return false; + } + Type member = memberIter.next(); + T value = valueIter.next(); + if (!member.isInstance(value, runtime)) { + return false; + } + } + return !memberIter.hasNext(); + } + + @Override + public Type elementType(Type index) { + // Can be more precise if we have literal types + return elementType(); + } + + @Override + public Type elementType() { + // TODO: precalculate + // TODO: helper method + Type result = BottomType.INSTANCE; + for (Type member : members) { + result = Type.unionType(result, member); + } + return result; + } + + @Override + public String toString() { + return "tuple[" + members.stream().map(Type::toString).collect(Collectors.joining(", ")) + "]"; + } +} diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/type/Type.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/type/Type.java new file mode 100644 index 00000000000..4d467cdefc5 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/type/Type.java @@ -0,0 +1,65 @@ +package software.amazon.smithy.jmespath.type; + +import software.amazon.smithy.jmespath.JmespathException; +import software.amazon.smithy.jmespath.JmespathExceptionType; +import software.amazon.smithy.jmespath.RuntimeType; +import software.amazon.smithy.jmespath.evaluation.FunctionArgument; +import software.amazon.smithy.jmespath.evaluation.JmespathRuntime; + +import java.util.EnumSet; +import java.util.Set; + +public interface Type extends FunctionArgument { + + static Type anyType() { return AnyType.INSTANCE; } + + static Type bottomType() { + return BottomType.INSTANCE; + } + + static Type nullType() { return new JustRuntimeType(RuntimeType.NULL); } + + static Type booleanType() { return new JustRuntimeType(RuntimeType.BOOLEAN); } + + static Type stringType() { return new JustRuntimeType(RuntimeType.STRING); } + + static Type numberType() { return new JustRuntimeType(RuntimeType.NUMBER); } + + static Type arrayType() { return new ArrayType(anyType()); } + + static Type arrayType(Type elementType) { return new ArrayType(elementType); } + + static Type objectType() { return new ObjectType(null); } + + static Type unionType(Type ... types) { + return new UnionType(types); + } + + boolean isInstance(T value, JmespathRuntime runtime); + + default Type elementType() { + return Type.nullType(); + } + + default Type elementType(Type index) { + return elementType(); + } + + default Type valueType(Type key) { + return Type.nullType(); + } + + default boolean isArray() { + return false; + } + + @Override + default Type expectValue() { + return expectAnyOf(RuntimeType.valueTypes()); + } + + @Override + default Type expectType(RuntimeType type) { + return expectAnyOf(EnumSet.of(type)); + } +} diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/type/TypeJmespathRuntime.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/type/TypeJmespathRuntime.java new file mode 100644 index 00000000000..db6f6f6c47b --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/type/TypeJmespathRuntime.java @@ -0,0 +1,190 @@ +package software.amazon.smithy.jmespath.type; + +import software.amazon.smithy.jmespath.JmespathExceptionType; +import software.amazon.smithy.jmespath.JmespathExpression; +import software.amazon.smithy.jmespath.RuntimeType; +import software.amazon.smithy.jmespath.evaluation.Function; +import software.amazon.smithy.jmespath.evaluation.FunctionArgument; +import software.amazon.smithy.jmespath.evaluation.FunctionRegistry; +import software.amazon.smithy.jmespath.evaluation.JmespathAbstractRuntime; + +// POC of an abstract runtime based on a semi-arbitrary Type value +public class TypeJmespathRuntime implements JmespathAbstractRuntime { + + private final FunctionRegistry overrides = new FunctionRegistry<>(); + + public TypeJmespathRuntime() { + overrides.registerFunction(new FoldLeftFunction()); + } + + @Override + public Type abstractTypeOf(Type value) { + return Type.stringType(); + } + + @Override + public Type abstractIs(Type value, RuntimeType type) { + return Type.booleanType(); + } + + @Override + public Type abstractEqual(Type a, Type b) { + return Type.booleanType(); + } + + @Override + public Type abstractLessThan(Type a, Type b) { + return Type.unionType(Type.booleanType(), Type.nullType()); + } + + @Override + public Type abstractToString(Type value) { + return Type.stringType(); + } + + @Override + public Type createNull() { + return Type.nullType(); + } + + @Override + public Type createBoolean(boolean b) { + return Type.booleanType(); + } + + @Override + public Type createString(String string) { + return Type.stringType(); + } + + @Override + public Type createNumber(Number value) { + return Type.numberType(); + } + + @Override + public ArrayBuilder arrayBuilder() { + return new TypeArrayBuilder(); + } + + private static class TypeArrayBuilder implements ArrayBuilder { + + // Must always be an array type + private Type type = Type.arrayType(Type.bottomType()); + + @Override + public ArrayBuilder add(Type value) { + type = Type.arrayType(Type.unionType(type.elementType(), value)); + return this; + } + + @Override + public ArrayBuilder addAll(Type collection) { + // TODO: what about map? Need keysOf operation on Type + type = Type.unionType(type, collection); + return this; + } + + @Override + public Type build() { + return type; + } + } + + @Override + public Type element(Type array, int index) { + return array.elementType(createNumber(index)); + } + + @Override + public Type abstractElement(Type array, Type index) { + return array.elementType(index); + } + + @Override + public ObjectBuilder objectBuilder() { + return new TypeObjectBuilder(); + } + + private static class TypeObjectBuilder implements ObjectBuilder { + + // Must always be an object type + private Type type = Type.objectType(); + + @Override + public ObjectBuilder put(Type key, Type value) { + // TODO: Try out record type + return this; + } + + @Override + public ObjectBuilder putAll(Type object) { + return this; + } + + @Override + public Type build() { + return type; + } + } + + @Override + public Type value(Type object, Type key) { + return object.valueType(key); + } + + @Override + public Type abstractLength(Type value) { + return Type.numberType(); + } + + @Override + public Type createAny(RuntimeType runtimeType) { + switch (runtimeType) { + case STRING: + return Type.stringType(); + case NUMBER: + return Type.numberType(); + case BOOLEAN: + return Type.booleanType(); + case NULL: + return Type.nullType(); + case ARRAY: + return Type.arrayType(); + case OBJECT: + return Type.objectType(); + default: + throw new IllegalArgumentException("Unexpected runtime type: " + runtimeType); + } + } + + @Override + public Type either(Type left, Type right) { + return Type.unionType(left, right); + } + + @Override + public Function resolveFunction(String name) { + return overrides.lookup(name); + } + + @Override + public FunctionArgument createFunctionArgument(Type value) { + return value; + } + + @Override + public FunctionArgument createFunctionArgument(JmespathExpression expression) { + return new ExpressionType(expression); + } + + @Override + public Type createError(JmespathExceptionType type, String message) { + return new ErrorType(type); + } + + @Override + public Type createExpression(JmespathExpression expression) { + return new ExpressionType(expression); + } +} diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/type/UnionType.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/type/UnionType.java new file mode 100644 index 00000000000..7a743ce2eb6 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/type/UnionType.java @@ -0,0 +1,89 @@ +package software.amazon.smithy.jmespath.type; + +import software.amazon.smithy.jmespath.RuntimeType; +import software.amazon.smithy.jmespath.evaluation.JmespathRuntime; + +import java.util.Arrays; +import java.util.Collection; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class UnionType implements Type { + + private final Set types; + + public UnionType(Type ... types) { + this(new HashSet<>(Arrays.asList(types))); + } + + public UnionType(Collection types) { + this.types = new HashSet<>(); + // TODO: Extract out of constructor, so we can collapse to a non-union type sometimes + for (Type type : types) { + if (type == null) { + throw new NullPointerException(); + } + if (type instanceof UnionType) { + this.types.addAll(((UnionType)type).types); + } else { + this.types.add(type); + } + } + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof UnionType)) { + return false; + } + UnionType other = (UnionType) obj; + return types.equals(other.types); + } + + @Override + public int hashCode() { + return UnionType.class.hashCode() + types.hashCode(); + } + + @Override + public boolean isInstance(T value, JmespathRuntime runtime) { + for (Type type : types) { + if (type.isInstance(value, runtime)) { + return true; + } + } + return false; + } + + @Override + public Type elementType() { + return map(Type::elementType); + } + + public Type valueType(Type key) { + return map(t -> t.valueType(key)); + } + + public Type expectType(RuntimeType type) { + return map(t -> t.expectType(type)); + } + + @Override + public Type expectAnyOf(Set runtimeTypes) { + return map(t -> t.expectAnyOf(runtimeTypes)); + } + + private Type map(Function f) { + return new UnionType(types.stream().map(f).collect(Collectors.toSet())); + + } + + @Override + public String toString() { + return types.stream().map(Type::toString).collect(Collectors.joining(" | ")); + } +} diff --git a/smithy-jmespath/src/main/resources/META-INF/services/software.amazon.smithy.jmespath.JmespathExtension b/smithy-jmespath/src/main/resources/META-INF/services/software.amazon.smithy.jmespath.JmespathExtension new file mode 100644 index 00000000000..6fd111af4ef --- /dev/null +++ b/smithy-jmespath/src/main/resources/META-INF/services/software.amazon.smithy.jmespath.JmespathExtension @@ -0,0 +1 @@ +software.amazon.smithy.jmespath.evaluation.CoreExtension diff --git a/smithy-model-jmespath/src/main/java/software/amazon/smithy/model/jmespath/node/ModelJmespathUtils.java b/smithy-model-jmespath/src/main/java/software/amazon/smithy/model/jmespath/node/ModelJmespathUtils.java index b95fda2eb9f..00d9c12ebd0 100644 --- a/smithy-model-jmespath/src/main/java/software/amazon/smithy/model/jmespath/node/ModelJmespathUtils.java +++ b/smithy-model-jmespath/src/main/java/software/amazon/smithy/model/jmespath/node/ModelJmespathUtils.java @@ -7,6 +7,7 @@ import software.amazon.smithy.jmespath.JmespathExpression; import software.amazon.smithy.jmespath.LinterResult; import software.amazon.smithy.jmespath.ast.LiteralExpression; +import software.amazon.smithy.jmespath.type.Type; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.Shape; @@ -49,4 +50,8 @@ public static LiteralExpression sampleShapeValue(Model model, Shape shape) { ? LiteralExpression.ANY : new LiteralExpression(shape.accept(new ModelRuntimeTypeGenerator(model))); } + + public static Type typeForShape(Model model, Shape shape) { + return shape == null ? Type.anyType() : shape.accept(new ShapeTyper(model)); + } } diff --git a/smithy-model-jmespath/src/main/java/software/amazon/smithy/model/jmespath/node/NodeJmespathRuntime.java b/smithy-model-jmespath/src/main/java/software/amazon/smithy/model/jmespath/node/NodeJmespathRuntime.java index c01a7ed22ce..0c8ae1ffcd9 100644 --- a/smithy-model-jmespath/src/main/java/software/amazon/smithy/model/jmespath/node/NodeJmespathRuntime.java +++ b/smithy-model-jmespath/src/main/java/software/amazon/smithy/model/jmespath/node/NodeJmespathRuntime.java @@ -137,12 +137,13 @@ private static final class ArrayNodeBuilder implements ArrayBuilder { private final ArrayNode.Builder builder = ArrayNode.builder(); @Override - public void add(Node value) { + public ArrayNodeBuilder add(Node value) { builder.withValue(value); + return this; } @Override - public void addAll(Node value) { + public ArrayNodeBuilder addAll(Node value) { if (value.isArrayNode()) { builder.merge(value.expectArrayNode()); } else { @@ -150,6 +151,7 @@ public void addAll(Node value) { builder.withValue(key); } } + return this; } @Override @@ -177,13 +179,15 @@ private static final class ObjectNodeBuilder implements ObjectBuilder { private final ObjectNode.Builder builder = ObjectNode.builder(); @Override - public void put(Node key, Node value) { + public ObjectNodeBuilder put(Node key, Node value) { builder.withMember(key.expectStringNode(), value); + return this; } @Override - public void putAll(Node object) { + public ObjectNodeBuilder putAll(Node object) { builder.merge(object.expectObjectNode()); + return this; } @Override diff --git a/smithy-model-jmespath/src/main/java/software/amazon/smithy/model/jmespath/node/ShapeTyper.java b/smithy-model-jmespath/src/main/java/software/amazon/smithy/model/jmespath/node/ShapeTyper.java new file mode 100644 index 00000000000..ec17fbc3a49 --- /dev/null +++ b/smithy-model-jmespath/src/main/java/software/amazon/smithy/model/jmespath/node/ShapeTyper.java @@ -0,0 +1,199 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.model.jmespath.node; + +import software.amazon.smithy.jmespath.type.ArrayType; +import software.amazon.smithy.jmespath.type.MapType; +import software.amazon.smithy.jmespath.type.ObjectType; +import software.amazon.smithy.jmespath.type.Type; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.BigDecimalShape; +import software.amazon.smithy.model.shapes.BigIntegerShape; +import software.amazon.smithy.model.shapes.BlobShape; +import software.amazon.smithy.model.shapes.BooleanShape; +import software.amazon.smithy.model.shapes.ByteShape; +import software.amazon.smithy.model.shapes.DocumentShape; +import software.amazon.smithy.model.shapes.DoubleShape; +import software.amazon.smithy.model.shapes.FloatShape; +import software.amazon.smithy.model.shapes.IntegerShape; +import software.amazon.smithy.model.shapes.ListShape; +import software.amazon.smithy.model.shapes.LongShape; +import software.amazon.smithy.model.shapes.MapShape; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.shapes.ResourceShape; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeVisitor; +import software.amazon.smithy.model.shapes.ShortShape; +import software.amazon.smithy.model.shapes.StringShape; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.shapes.TimestampShape; +import software.amazon.smithy.model.shapes.UnionShape; + +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; + +/** + * Generates fake data from a modeled shape for static JMESPath analysis. + */ +final class ShapeTyper implements ShapeVisitor { + + private final Model model; + private Set visited = new HashSet<>(); + + ShapeTyper(Model model) { + this.model = model; + } + + @Override + public Type blobShape(BlobShape shape) { + return Type.stringType(); + } + + @Override + public Type booleanShape(BooleanShape shape) { + return Type.booleanType(); + } + + @Override + public Type byteShape(ByteShape shape) { + return Type.numberType(); + } + + @Override + public Type shortShape(ShortShape shape) { + return Type.numberType(); + } + + @Override + public Type integerShape(IntegerShape shape) { + return Type.numberType(); + } + + @Override + public Type longShape(LongShape shape) { + return Type.numberType(); + } + + @Override + public Type floatShape(FloatShape shape) { + return Type.numberType(); + } + + @Override + public Type doubleShape(DoubleShape shape) { + return Type.numberType(); + } + + @Override + public Type bigIntegerShape(BigIntegerShape shape) { + return Type.numberType(); + } + + @Override + public Type bigDecimalShape(BigDecimalShape shape) { + return Type.numberType(); + } + + @Override + public Type documentShape(DocumentShape shape) { + return Type.anyType(); + } + + @Override + public Type stringShape(StringShape shape) { + return Type.stringType(); + } + + @Override + public Type listShape(ListShape shape) { + return withCopiedVisitors(() -> { + Type memberType = shape.getMember().accept(this); + return new ArrayType(memberType); + }); + } + + // Visits members and mutates a copy of the current set of visited + // shapes rather than a shared set. This allows a shape to be used + // multiple times in the closure of a single shape without causing the + // reuse of the shape to always be assumed to be a recursive type. + private Type withCopiedVisitors(Supplier supplier) { + // Account for recursive shapes at the current + Set visitedCopy = new HashSet<>(visited); + Type result = supplier.get(); + visited = visitedCopy; + return result; + } + + @Override + public Type mapShape(MapShape shape) { + return withCopiedVisitors(() -> { + Type keyType = shape.getKey().accept(this); + Type valueType = shape.getValue().accept(this); + return new MapType(keyType, valueType); + }); + } + + @Override + public Type structureShape(StructureShape shape) { + return structureOrUnion(shape); + } + + @Override + public Type unionShape(UnionShape shape) { + return structureOrUnion(shape); + } + + private Type structureOrUnion(Shape shape) { + return withCopiedVisitors(() -> { + Map result = new LinkedHashMap<>(); + for (MemberShape member : shape.members()) { + Type memberType = member.accept(this); + result.put(member.getMemberName(), memberType); + } + return new ObjectType(result); + }); + } + + @Override + public Type memberShape(MemberShape shape) { + // Account for recursive shapes. + // A false return value means it was in the set. + // TODO: Can Type represent recursive types? + if (!visited.add(shape)) { + return Type.anyType(); + } + + return model.getShape(shape.getTarget()) + .map(target -> target.accept(this)) + // Rather than fail on broken models during waiter validation, + // return an ANY to get *some* validation. + .orElse(Type.anyType()); + } + + @Override + public Type timestampShape(TimestampShape shape) { + return Type.numberType(); + } + + @Override + public Type operationShape(OperationShape shape) { + throw new UnsupportedOperationException(shape.toString()); + } + + @Override + public Type resourceShape(ResourceShape shape) { + throw new UnsupportedOperationException(shape.toString()); + } + + @Override + public Type serviceShape(ServiceShape shape) { + throw new UnsupportedOperationException(shape.toString()); + } +}