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/settings.gradle.kts b/settings.gradle.kts index f3c9eba093a..c030032e5ab 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -26,6 +26,7 @@ include(":smithy-openapi-traits") include(":smithy-utils") include(":smithy-protocol-test-traits") include(":smithy-jmespath") +include(":smithy-jmespath-tests") include(":smithy-waiters") include(":smithy-aws-cloudformation-traits") include(":smithy-aws-cloudformation") diff --git a/smithy-jmespath-tests/build.gradle.kts b/smithy-jmespath-tests/build.gradle.kts new file mode 100644 index 00000000000..5ee14f98921 --- /dev/null +++ b/smithy-jmespath-tests/build.gradle.kts @@ -0,0 +1,24 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +plugins { + id("smithy.module-conventions") +} + +description = "Compliance tests for JMESPath" + +extra["displayName"] = "Smithy :: JMESPath Tests" +extra["moduleName"] = "software.amazon.smithy.jmespathtests" + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +dependencies { + api(libs.junit.jupiter.api) + api(libs.junit.jupiter.params) + api(project(":smithy-jmespath")) + implementation(project(":smithy-utils")) +} 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 new file mode 100644 index 00000000000..187b5fab8ad --- /dev/null +++ b/smithy-jmespath-tests/src/main/java/software/amazon/smithy/jmespath/tests/ComplianceTestRunner.java @@ -0,0 +1,152 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.jmespath.tests; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +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.Evaluator; +import software.amazon.smithy.jmespath.evaluation.JmespathRuntime; +import software.amazon.smithy.utils.IoUtils; + +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"; + private static final String COMMENT_MEMBER = "comment"; + private static final String EXPRESSION_MEMBER = "expression"; + private static final String RESULT_MEMBER = "result"; + 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 ComplianceTestRunner(JmespathRuntime runtime) { + this.runtime = runtime; + } + + public static Stream defaultParameterizedTestSource(JmespathRuntime runtime) { + ComplianceTestRunner runner = new ComplianceTestRunner<>(runtime); + 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)); + }); + } catch (IOException e) { + throw new RuntimeException(e); + } + return runner.parameterizedTestSource(); + } + + public Stream parameterizedTestSource() { + return testCases.stream().map(testCase -> new Object[] {testCase.name(), testCase}); + } + + private record TestCase( + JmespathRuntime runtime, + String testSuite, + String comment, + T given, + String expression, + T expectedResult, + JmespathExceptionType expectedError, + String benchmark) + implements Runnable { + public static List> from(URL url, JmespathRuntime runtime) { + var path = url.getPath(); + var testSuiteName = path.substring(path.lastIndexOf('/') + 1, path.lastIndexOf('.')); + var testCases = new ArrayList>(); + String text = IoUtils.readUtf8Url(url); + T tests = JmespathExpression.parseJson(text, runtime); + + for (var test : runtime.asIterable(tests)) { + var given = value(runtime, test, SUBJECT_MEMBER); + for (var testCase : runtime.asIterable(value(runtime, test, CASES_MEMBER))) { + String comment = valueAsString(runtime, testCase, COMMENT_MEMBER); + String expression = valueAsString(runtime, testCase, EXPRESSION_MEMBER); + var result = value(runtime, testCase, RESULT_MEMBER); + var expectedErrorString = valueAsString(runtime, testCase, ERROR_MEMBER); + var expectedError = + expectedErrorString != null ? JmespathExceptionType.fromID(expectedErrorString) : null; + + // Special case: The spec says function names cannot be quoted, + // but our parser allows it, and this may be useful in the future. + if ("function names cannot be quoted".equals(comment)) { + expectedError = JmespathExceptionType.UNKNOWN_FUNCTION; + } else if (expression.contains("\"to_string\"")) { + expectedError = null; + result = runtime.createString("1.0"); + } + + var benchmark = valueAsString(runtime, testCase, BENCH_MEMBER); + testCases.add(new TestCase<>(runtime, + testSuiteName, + comment, + given, + expression, + result, + expectedError, + benchmark)); + } + } + return testCases; + } + + private static T value(JmespathRuntime runtime, T object, String key) { + return runtime.value(object, runtime.createString(key)); + } + + private static String valueAsString(JmespathRuntime runtime, T object, String key) { + T result = runtime.value(object, runtime.createString(key)); + return runtime.is(result, RuntimeType.NULL) ? null : runtime.asString(result); + } + + private String name() { + return testSuite + (comment != null ? " - " + comment : "") + " (" + runtime.toString(given) + ")[" + + expression + "]"; + } + + @Override + public void run() { + try { + var parsed = JmespathExpression.parse(expression); + var result = new Evaluator<>(given, runtime).visit(parsed); + if (benchmark != null) { + // Benchmarks don't include expected results or errors + return; + } + if (expectedError != null) { + throw new AssertionError("Expected " + expectedError + " error but no error occurred. \n" + + "Actual: " + runtime.toString(result) + "\n" + + "For query: " + expression + "\n"); + } else { + if (!runtime.equal(expectedResult, result)) { + throw new AssertionError("Expected does not match actual. \n" + + "Expected: " + runtime.toString(expectedResult) + "\n" + + "Actual: " + runtime.toString(result) + "\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); + } + } + } + } +} diff --git a/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/MANIFEST b/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/MANIFEST new file mode 100644 index 00000000000..aaf7f0b4c55 --- /dev/null +++ b/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/MANIFEST @@ -0,0 +1,16 @@ +basic.json +benchmarks.json +boolean.json +current.json +escape.json +filters.json +functions.json +identifiers.json +indices.json +literal.json +multiselect.json +pipe.json +slice.json +syntax.json +unicode.json +wildcard.json diff --git a/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/README.md b/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/README.md new file mode 100644 index 00000000000..44ebdd88c36 --- /dev/null +++ b/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/README.md @@ -0,0 +1,8 @@ +# Compliance tests + +This directory is copied from this snapshot of the JMESPath compliance tests repository: + +https://github.com/jmespath/jmespath.test/tree/53abcc37901891cf4308fcd910eab287416c4609/tests + +The MANIFEST file is added so that these can be retrieved as resources +(which don't support any notion of directories or listing). diff --git a/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/basic.json b/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/basic.json new file mode 100644 index 00000000000..d550e969547 --- /dev/null +++ b/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/basic.json @@ -0,0 +1,96 @@ +[{ + "given": + {"foo": {"bar": {"baz": "correct"}}}, + "cases": [ + { + "expression": "foo", + "result": {"bar": {"baz": "correct"}} + }, + { + "expression": "foo.bar", + "result": {"baz": "correct"} + }, + { + "expression": "foo.bar.baz", + "result": "correct" + }, + { + "expression": "foo\n.\nbar\n.baz", + "result": "correct" + }, + { + "expression": "foo.bar.baz.bad", + "result": null + }, + { + "expression": "foo.bar.bad", + "result": null + }, + { + "expression": "foo.bad", + "result": null + }, + { + "expression": "bad", + "result": null + }, + { + "expression": "bad.morebad.morebad", + "result": null + } + ] +}, +{ + "given": + {"foo": {"bar": ["one", "two", "three"]}}, + "cases": [ + { + "expression": "foo", + "result": {"bar": ["one", "two", "three"]} + }, + { + "expression": "foo.bar", + "result": ["one", "two", "three"] + } + ] +}, +{ + "given": ["one", "two", "three"], + "cases": [ + { + "expression": "one", + "result": null + }, + { + "expression": "two", + "result": null + }, + { + "expression": "three", + "result": null + }, + { + "expression": "one.two", + "result": null + } + ] +}, +{ + "given": + {"foo": {"1": ["one", "two", "three"], "-1": "bar"}}, + "cases": [ + { + "expression": "foo.\"1\"", + "result": ["one", "two", "three"] + }, + { + "expression": "foo.\"1\"[0]", + "result": "one" + }, + { + "expression": "foo.\"-1\"", + "result": "bar" + } + ] +} +] diff --git a/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/benchmarks.json b/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/benchmarks.json new file mode 100644 index 00000000000..024a5904f86 --- /dev/null +++ b/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/benchmarks.json @@ -0,0 +1,138 @@ +[ + { + "given": { + "long_name_for_a_field": true, + "a": { + "b": { + "c": { + "d": { + "e": { + "f": { + "g": { + "h": { + "i": { + "j": { + "k": { + "l": { + "m": { + "n": { + "o": { + "p": true + } + } + } + } + } + } + } + } + } + } + } + } + } + } + }, + "b": true, + "c": { + "d": true + } + }, + "cases": [ + { + "comment": "simple field", + "expression": "b", + "bench": "full" + }, + { + "comment": "simple subexpression", + "expression": "c.d", + "bench": "full" + }, + { + "comment": "deep field selection no match", + "expression": "a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s", + "bench": "full" + }, + { + "comment": "deep field selection", + "expression": "a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p", + "bench": "full" + }, + { + "comment": "simple or", + "expression": "not_there || b", + "bench": "full" + } + ] + }, + { + "given": { + "a":0,"b":1,"c":2,"d":3,"e":4,"f":5,"g":6,"h":7,"i":8,"j":9,"k":10, + "l":11,"m":12,"n":13,"o":14,"p":15,"q":16,"r":17,"s":18,"t":19,"u":20, + "v":21,"w":22,"x":23,"y":24,"z":25 + }, + "cases": [ + { + "comment": "deep ands", + "expression": "a && b && c && d && e && f && g && h && i && j && k && l && m && n && o && p && q && r && s && t && u && v && w && x && y && z", + "bench": "full" + }, + { + "comment": "deep ors", + "expression": "z || y || x || w || v || u || t || s || r || q || p || o || n || m || l || k || j || i || h || g || f || e || d || c || b || a", + "bench": "full" + }, + { + "comment": "lots of summing", + "expression": "sum([z, y, x, w, v, u, t, s, r, q, p, o, n, m, l, k, j, i, h, g, f, e, d, c, b, a])", + "bench": "full" + }, + { + "comment": "lots of function application", + "expression": "sum([z, sum([y, sum([x, sum([w, sum([v, sum([u, sum([t, sum([s, sum([r, sum([q, sum([p, sum([o, sum([n, sum([m, sum([l, sum([k, sum([j, sum([i, sum([h, sum([g, sum([f, sum([e, sum([d, sum([c, sum([b, a])])])])])])])])])])])])])])])])])])])])])])])])])", + "bench": "full" + }, + { + "comment": "lots of multi list", + "expression": "[z, y, x, w, v, u, t, s, r, q, p, o, n, m, l, k, j, i, h, g, f, e, d, c, b, a]", + "bench": "full" + } + ] + }, + { + "given": {}, + "cases": [ + { + "comment": "field 50", + "expression": "j49.j48.j47.j46.j45.j44.j43.j42.j41.j40.j39.j38.j37.j36.j35.j34.j33.j32.j31.j30.j29.j28.j27.j26.j25.j24.j23.j22.j21.j20.j19.j18.j17.j16.j15.j14.j13.j12.j11.j10.j9.j8.j7.j6.j5.j4.j3.j2.j1.j0", + "bench": "parse" + }, + { + "comment": "pipe 50", + "expression": "j49|j48|j47|j46|j45|j44|j43|j42|j41|j40|j39|j38|j37|j36|j35|j34|j33|j32|j31|j30|j29|j28|j27|j26|j25|j24|j23|j22|j21|j20|j19|j18|j17|j16|j15|j14|j13|j12|j11|j10|j9|j8|j7|j6|j5|j4|j3|j2|j1|j0", + "bench": "parse" + }, + { + "comment": "index 50", + "expression": "[49][48][47][46][45][44][43][42][41][40][39][38][37][36][35][34][33][32][31][30][29][28][27][26][25][24][23][22][21][20][19][18][17][16][15][14][13][12][11][10][9][8][7][6][5][4][3][2][1][0]", + "bench": "parse" + }, + { + "comment": "long raw string literal", + "expression": "'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'", + "bench": "parse" + }, + { + "comment": "deep projection 104", + "expression": "a[*].b[*].c[*].d[*].e[*].f[*].g[*].h[*].i[*].j[*].k[*].l[*].m[*].n[*].o[*].p[*].q[*].r[*].s[*].t[*].u[*].v[*].w[*].x[*].y[*].z[*].a[*].b[*].c[*].d[*].e[*].f[*].g[*].h[*].i[*].j[*].k[*].l[*].m[*].n[*].o[*].p[*].q[*].r[*].s[*].t[*].u[*].v[*].w[*].x[*].y[*].z[*].a[*].b[*].c[*].d[*].e[*].f[*].g[*].h[*].i[*].j[*].k[*].l[*].m[*].n[*].o[*].p[*].q[*].r[*].s[*].t[*].u[*].v[*].w[*].x[*].y[*].z[*].a[*].b[*].c[*].d[*].e[*].f[*].g[*].h[*].i[*].j[*].k[*].l[*].m[*].n[*].o[*].p[*].q[*].r[*].s[*].t[*].u[*].v[*].w[*].x[*].y[*].z[*]", + "bench": "parse" + }, + { + "comment": "filter projection", + "expression": "foo[?bar > baz][?qux > baz]", + "bench": "parse" + } + ] + } +] diff --git a/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/boolean.json b/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/boolean.json new file mode 100644 index 00000000000..dd7ee588229 --- /dev/null +++ b/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/boolean.json @@ -0,0 +1,288 @@ +[ + { + "given": { + "outer": { + "foo": "foo", + "bar": "bar", + "baz": "baz" + } + }, + "cases": [ + { + "expression": "outer.foo || outer.bar", + "result": "foo" + }, + { + "expression": "outer.foo||outer.bar", + "result": "foo" + }, + { + "expression": "outer.bar || outer.baz", + "result": "bar" + }, + { + "expression": "outer.bar||outer.baz", + "result": "bar" + }, + { + "expression": "outer.bad || outer.foo", + "result": "foo" + }, + { + "expression": "outer.bad||outer.foo", + "result": "foo" + }, + { + "expression": "outer.foo || outer.bad", + "result": "foo" + }, + { + "expression": "outer.foo||outer.bad", + "result": "foo" + }, + { + "expression": "outer.bad || outer.alsobad", + "result": null + }, + { + "expression": "outer.bad||outer.alsobad", + "result": null + } + ] + }, + { + "given": { + "outer": { + "foo": "foo", + "bool": false, + "empty_list": [], + "empty_string": "" + } + }, + "cases": [ + { + "expression": "outer.empty_string || outer.foo", + "result": "foo" + }, + { + "expression": "outer.nokey || outer.bool || outer.empty_list || outer.empty_string || outer.foo", + "result": "foo" + } + ] + }, + { + "given": { + "True": true, + "False": false, + "Number": 5, + "EmptyList": [], + "Zero": 0, + "ZeroFloat": 0.0 + }, + "cases": [ + { + "expression": "True && False", + "result": false + }, + { + "expression": "False && True", + "result": false + }, + { + "expression": "True && True", + "result": true + }, + { + "expression": "False && False", + "result": false + }, + { + "expression": "True && Number", + "result": 5 + }, + { + "expression": "Number && True", + "result": true + }, + { + "expression": "Number && False", + "result": false + }, + { + "expression": "Number && EmptyList", + "result": [] + }, + { + "expression": "Number && True", + "result": true + }, + { + "expression": "EmptyList && True", + "result": [] + }, + { + "expression": "EmptyList && False", + "result": [] + }, + { + "expression": "True || False", + "result": true + }, + { + "expression": "True || True", + "result": true + }, + { + "expression": "False || True", + "result": true + }, + { + "expression": "False || False", + "result": false + }, + { + "expression": "Number || EmptyList", + "result": 5 + }, + { + "expression": "Number || True", + "result": 5 + }, + { + "expression": "Number || True && False", + "result": 5 + }, + { + "expression": "(Number || True) && False", + "result": false + }, + { + "expression": "Number || (True && False)", + "result": 5 + }, + { + "expression": "!True", + "result": false + }, + { + "expression": "!False", + "result": true + }, + { + "expression": "!Number", + "result": false + }, + { + "expression": "!EmptyList", + "result": true + }, + { + "expression": "True && !False", + "result": true + }, + { + "expression": "True && !EmptyList", + "result": true + }, + { + "expression": "!False && !EmptyList", + "result": true + }, + { + "expression": "!True && False", + "result": false + }, + { + "expression": "!(True && False)", + "result": true + }, + { + "expression": "!Zero", + "result": false + }, + { + "expression": "!!Zero", + "result": true + }, + { + "expression": "Zero || Number", + "result": 0 + }, + { + "expression": "ZeroFloat || Number", + "result": 0.0 + } + ] + }, + { + "given": { + "one": 1, + "two": 2, + "three": 3, + "emptylist": [], + "boolvalue": false + }, + "cases": [ + { + "expression": "one < two", + "result": true + }, + { + "expression": "one <= two", + "result": true + }, + { + "expression": "one == one", + "result": true + }, + { + "expression": "one == two", + "result": false + }, + { + "expression": "one > two", + "result": false + }, + { + "expression": "one >= two", + "result": false + }, + { + "expression": "one != two", + "result": true + }, + { + "expression": "emptylist < one", + "result": null + }, + { + "expression": "emptylist < nullvalue", + "result": null + }, + { + "expression": "emptylist < boolvalue", + "result": null + }, + { + "expression": "one < boolvalue", + "result": null + }, + { + "expression": "one < two && three > one", + "result": true + }, + { + "expression": "one < two || three > one", + "result": true + }, + { + "expression": "one < two || three < one", + "result": true + }, + { + "expression": "two < one || three < one", + "result": false + } + ] + } +] diff --git a/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/current.json b/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/current.json new file mode 100644 index 00000000000..0c26248d079 --- /dev/null +++ b/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/current.json @@ -0,0 +1,25 @@ +[ + { + "given": { + "foo": [{"name": "a"}, {"name": "b"}], + "bar": {"baz": "qux"} + }, + "cases": [ + { + "expression": "@", + "result": { + "foo": [{"name": "a"}, {"name": "b"}], + "bar": {"baz": "qux"} + } + }, + { + "expression": "@.bar", + "result": {"baz": "qux"} + }, + { + "expression": "@.foo[0]", + "result": {"name": "a"} + } + ] + } +] diff --git a/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/escape.json b/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/escape.json new file mode 100644 index 00000000000..4a62d951a65 --- /dev/null +++ b/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/escape.json @@ -0,0 +1,46 @@ +[{ + "given": { + "foo.bar": "dot", + "foo bar": "space", + "foo\nbar": "newline", + "foo\"bar": "doublequote", + "c:\\\\windows\\path": "windows", + "/unix/path": "unix", + "\"\"\"": "threequotes", + "bar": {"baz": "qux"} + }, + "cases": [ + { + "expression": "\"foo.bar\"", + "result": "dot" + }, + { + "expression": "\"foo bar\"", + "result": "space" + }, + { + "expression": "\"foo\\nbar\"", + "result": "newline" + }, + { + "expression": "\"foo\\\"bar\"", + "result": "doublequote" + }, + { + "expression": "\"c:\\\\\\\\windows\\\\path\"", + "result": "windows" + }, + { + "expression": "\"/unix/path\"", + "result": "unix" + }, + { + "expression": "\"\\\"\\\"\\\"\"", + "result": "threequotes" + }, + { + "expression": "\"bar\".\"baz\"", + "result": "qux" + } + ] +}] diff --git a/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/filters.json b/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/filters.json new file mode 100644 index 00000000000..41c20ae3473 --- /dev/null +++ b/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/filters.json @@ -0,0 +1,594 @@ +[ + { + "given": {"foo": [{"name": "a"}, {"name": "b"}]}, + "cases": [ + { + "comment": "Matching a literal", + "expression": "foo[?name == 'a']", + "result": [{"name": "a"}] + } + ] + }, + { + "given": {"foo": [0, 1], "bar": [2, 3]}, + "cases": [ + { + "comment": "Matching a literal", + "expression": "*[?[0] == `0`]", + "result": [[], []] + } + ] + }, + { + "given": {"foo": [{"first": "foo", "last": "bar"}, + {"first": "foo", "last": "foo"}, + {"first": "foo", "last": "baz"}]}, + "cases": [ + { + "comment": "Matching an expression", + "expression": "foo[?first == last]", + "result": [{"first": "foo", "last": "foo"}] + }, + { + "comment": "Verify projection created from filter", + "expression": "foo[?first == last].first", + "result": ["foo"] + } + ] + }, + { + "given": {"foo": [{"age": 20}, + {"age": 25}, + {"age": 30}]}, + "cases": [ + { + "comment": "Greater than with a number", + "expression": "foo[?age > `25`]", + "result": [{"age": 30}] + }, + { + "expression": "foo[?age >= `25`]", + "result": [{"age": 25}, {"age": 30}] + }, + { + "comment": "Greater than with a number", + "expression": "foo[?age > `30`]", + "result": [] + }, + { + "comment": "Greater than with a number", + "expression": "foo[?age < `25`]", + "result": [{"age": 20}] + }, + { + "comment": "Greater than with a number", + "expression": "foo[?age <= `25`]", + "result": [{"age": 20}, {"age": 25}] + }, + { + "comment": "Greater than with a number", + "expression": "foo[?age < `20`]", + "result": [] + }, + { + "expression": "foo[?age == `20`]", + "result": [{"age": 20}] + }, + { + "expression": "foo[?age != `20`]", + "result": [{"age": 25}, {"age": 30}] + } + ] + }, + { + "given": {"foo": [{"weight": 33.3}, + {"weight": 44.4}, + {"weight": 55.5}]}, + "cases": [ + { + "comment": "Greater than with a number", + "expression": "foo[?weight > `44.4`]", + "result": [{"weight": 55.5}] + }, + { + "expression": "foo[?weight >= `44.4`]", + "result": [{"weight": 44.4}, {"weight": 55.5}] + }, + { + "comment": "Greater than with a number", + "expression": "foo[?weight > `55.5`]", + "result": [] + }, + { + "comment": "Greater than with a number", + "expression": "foo[?weight < `44.4`]", + "result": [{"weight": 33.3}] + }, + { + "comment": "Greater than with a number", + "expression": "foo[?weight <= `44.4`]", + "result": [{"weight": 33.3}, {"weight": 44.4}] + }, + { + "comment": "Greater than with a number", + "expression": "foo[?weight < `33.3`]", + "result": [] + }, + { + "expression": "foo[?weight == `33.3`]", + "result": [{"weight": 33.3}] + }, + { + "expression": "foo[?weight != `33.3`]", + "result": [{"weight": 44.4}, {"weight": 55.5}] + } + ] + }, + { + "given": {"foo": [{"top": {"name": "a"}}, + {"top": {"name": "b"}}]}, + "cases": [ + { + "comment": "Filter with subexpression", + "expression": "foo[?top.name == 'a']", + "result": [{"top": {"name": "a"}}] + } + ] + }, + { + "given": {"foo": [{"top": {"first": "foo", "last": "bar"}}, + {"top": {"first": "foo", "last": "foo"}}, + {"top": {"first": "foo", "last": "baz"}}]}, + "cases": [ + { + "comment": "Matching an expression", + "expression": "foo[?top.first == top.last]", + "result": [{"top": {"first": "foo", "last": "foo"}}] + }, + { + "comment": "Matching a JSON array", + "expression": "foo[?top == `{\"first\": \"foo\", \"last\": \"bar\"}`]", + "result": [{"top": {"first": "foo", "last": "bar"}}] + } + ] + }, + { + "given": {"foo": [ + {"key": true}, + {"key": false}, + {"key": 0}, + {"key": 1}, + {"key": [0]}, + {"key": {"bar": [0]}}, + {"key": null}, + {"key": [1]}, + {"key": {"a":2}} + ]}, + "cases": [ + { + "expression": "foo[?key == `true`]", + "result": [{"key": true}] + }, + { + "expression": "foo[?key == `false`]", + "result": [{"key": false}] + }, + { + "expression": "foo[?key == `0`]", + "result": [{"key": 0}] + }, + { + "expression": "foo[?key == `1`]", + "result": [{"key": 1}] + }, + { + "expression": "foo[?key == `[0]`]", + "result": [{"key": [0]}] + }, + { + "expression": "foo[?key == `{\"bar\": [0]}`]", + "result": [{"key": {"bar": [0]}}] + }, + { + "expression": "foo[?key == `null`]", + "result": [{"key": null}] + }, + { + "expression": "foo[?key == `[1]`]", + "result": [{"key": [1]}] + }, + { + "expression": "foo[?key == `{\"a\":2}`]", + "result": [{"key": {"a":2}}] + }, + { + "expression": "foo[?`true` == key]", + "result": [{"key": true}] + }, + { + "expression": "foo[?`false` == key]", + "result": [{"key": false}] + }, + { + "expression": "foo[?`0` == key]", + "result": [{"key": 0}] + }, + { + "expression": "foo[?`1` == key]", + "result": [{"key": 1}] + }, + { + "expression": "foo[?`[0]` == key]", + "result": [{"key": [0]}] + }, + { + "expression": "foo[?`{\"bar\": [0]}` == key]", + "result": [{"key": {"bar": [0]}}] + }, + { + "expression": "foo[?`null` == key]", + "result": [{"key": null}] + }, + { + "expression": "foo[?`[1]` == key]", + "result": [{"key": [1]}] + }, + { + "expression": "foo[?`{\"a\":2}` == key]", + "result": [{"key": {"a":2}}] + }, + { + "expression": "foo[?key != `true`]", + "result": [{"key": false}, {"key": 0}, {"key": 1}, {"key": [0]}, + {"key": {"bar": [0]}}, {"key": null}, {"key": [1]}, {"key": {"a":2}}] + }, + { + "expression": "foo[?key != `false`]", + "result": [{"key": true}, {"key": 0}, {"key": 1}, {"key": [0]}, + {"key": {"bar": [0]}}, {"key": null}, {"key": [1]}, {"key": {"a":2}}] + }, + { + "expression": "foo[?key != `0`]", + "result": [{"key": true}, {"key": false}, {"key": 1}, {"key": [0]}, + {"key": {"bar": [0]}}, {"key": null}, {"key": [1]}, {"key": {"a":2}}] + }, + { + "expression": "foo[?key != `1`]", + "result": [{"key": true}, {"key": false}, {"key": 0}, {"key": [0]}, + {"key": {"bar": [0]}}, {"key": null}, {"key": [1]}, {"key": {"a":2}}] + }, + { + "expression": "foo[?key != `null`]", + "result": [{"key": true}, {"key": false}, {"key": 0}, {"key": 1}, {"key": [0]}, + {"key": {"bar": [0]}}, {"key": [1]}, {"key": {"a":2}}] + }, + { + "expression": "foo[?key != `[1]`]", + "result": [{"key": true}, {"key": false}, {"key": 0}, {"key": 1}, {"key": [0]}, + {"key": {"bar": [0]}}, {"key": null}, {"key": {"a":2}}] + }, + { + "expression": "foo[?key != `{\"a\":2}`]", + "result": [{"key": true}, {"key": false}, {"key": 0}, {"key": 1}, {"key": [0]}, + {"key": {"bar": [0]}}, {"key": null}, {"key": [1]}] + }, + { + "expression": "foo[?`true` != key]", + "result": [{"key": false}, {"key": 0}, {"key": 1}, {"key": [0]}, + {"key": {"bar": [0]}}, {"key": null}, {"key": [1]}, {"key": {"a":2}}] + }, + { + "expression": "foo[?`false` != key]", + "result": [{"key": true}, {"key": 0}, {"key": 1}, {"key": [0]}, + {"key": {"bar": [0]}}, {"key": null}, {"key": [1]}, {"key": {"a":2}}] + }, + { + "expression": "foo[?`0` != key]", + "result": [{"key": true}, {"key": false}, {"key": 1}, {"key": [0]}, + {"key": {"bar": [0]}}, {"key": null}, {"key": [1]}, {"key": {"a":2}}] + }, + { + "expression": "foo[?`1` != key]", + "result": [{"key": true}, {"key": false}, {"key": 0}, {"key": [0]}, + {"key": {"bar": [0]}}, {"key": null}, {"key": [1]}, {"key": {"a":2}}] + }, + { + "expression": "foo[?`null` != key]", + "result": [{"key": true}, {"key": false}, {"key": 0}, {"key": 1}, {"key": [0]}, + {"key": {"bar": [0]}}, {"key": [1]}, {"key": {"a":2}}] + }, + { + "expression": "foo[?`[1]` != key]", + "result": [{"key": true}, {"key": false}, {"key": 0}, {"key": 1}, {"key": [0]}, + {"key": {"bar": [0]}}, {"key": null}, {"key": {"a":2}}] + }, + { + "expression": "foo[?`{\"a\":2}` != key]", + "result": [{"key": true}, {"key": false}, {"key": 0}, {"key": 1}, {"key": [0]}, + {"key": {"bar": [0]}}, {"key": null}, {"key": [1]}] + } + ] + }, + { + "given": {"foo": [ + {"key": true}, + {"key": false}, + {"key": 0}, + {"key": 0.0}, + {"key": 1}, + {"key": 1.0}, + {"key": [0]}, + {"key": null}, + {"key": [1]}, + {"key": []}, + {"key": {}}, + {"key": {"a":2}} + ]}, + "cases": [ + { + "expression": "foo[?key == `true`]", + "result": [{"key": true}] + }, + { + "expression": "foo[?key == `false`]", + "result": [{"key": false}] + }, + { + "expression": "foo[?key]", + "result": [ + {"key": true}, + {"key": 0}, + {"key": 0.0}, + {"key": 1}, + {"key": 1.0}, + {"key": [0]}, + {"key": [1]}, + {"key": {"a": 2}} + ] + }, + { + "expression": "foo[? !key]", + "result": [ + {"key": false}, + {"key": null}, + {"key": []}, + {"key": {}} + ] + }, + { + "expression": "foo[? !!key]", + "result": [ + {"key": true}, + {"key": 0}, + {"key": 0.0}, + {"key": 1}, + {"key": 1.0}, + {"key": [0]}, + {"key": [1]}, + {"key": {"a": 2}} + ] + }, + { + "expression": "foo[? `true`]", + "result": [ + {"key": true}, + {"key": false}, + {"key": 0}, + {"key": 0.0}, + {"key": 1}, + {"key": 1.0}, + {"key": [0]}, + {"key": null}, + {"key": [1]}, + {"key": []}, + {"key": {}}, + {"key": {"a":2}} + ] + }, + { + "expression": "foo[? `false`]", + "result": [] + } + ] + }, + { + "given": {"reservations": [ + {"instances": [ + {"foo": 1, "bar": 2}, {"foo": 1, "bar": 3}, + {"foo": 1, "bar": 2}, {"foo": 2, "bar": 1}]}]}, + "cases": [ + { + "expression": "reservations[].instances[?bar==`1`]", + "result": [[{"foo": 2, "bar": 1}]] + }, + { + "expression": "reservations[*].instances[?bar==`1`]", + "result": [[{"foo": 2, "bar": 1}]] + }, + { + "expression": "reservations[].instances[?bar==`1`][]", + "result": [{"foo": 2, "bar": 1}] + } + ] + }, + { + "given": { + "baz": "other", + "foo": [ + {"bar": 1}, {"bar": 2}, {"bar": 3}, {"bar": 4}, {"bar": 1, "baz": 2} + ] + }, + "cases": [ + { + "expression": "foo[?bar==`1`].bar[0]", + "result": [] + } + ] + }, + { + "given": { + "foo": [ + {"a": 1, "b": {"c": "x"}}, + {"a": 1, "b": {"c": "y"}}, + {"a": 1, "b": {"c": "z"}}, + {"a": 2, "b": {"c": "z"}}, + {"a": 1, "baz": 2} + ] + }, + "cases": [ + { + "expression": "foo[?a==`1`].b.c", + "result": ["x", "y", "z"] + } + ] + }, + { + "given": {"foo": [{"name": "a"}, {"name": "b"}, {"name": "c"}]}, + "cases": [ + { + "comment": "Filter with or expression", + "expression": "foo[?name == 'a' || name == 'b']", + "result": [{"name": "a"}, {"name": "b"}] + }, + { + "expression": "foo[?name == 'a' || name == 'e']", + "result": [{"name": "a"}] + }, + { + "expression": "foo[?name == 'a' || name == 'b' || name == 'c']", + "result": [{"name": "a"}, {"name": "b"}, {"name": "c"}] + } + ] + }, + { + "given": {"foo": [{"a": 1, "b": 2}, {"a": 1, "b": 3}]}, + "cases": [ + { + "comment": "Filter with and expression", + "expression": "foo[?a == `1` && b == `2`]", + "result": [{"a": 1, "b": 2}] + }, + { + "expression": "foo[?a == `1` && b == `4`]", + "result": [] + } + ] + }, + { + "given": {"foo": [{"a": 1, "b": 2, "c": 3}, {"a": 3, "b": 4}]}, + "cases": [ + { + "comment": "Filter with Or and And expressions", + "expression": "foo[?c == `3` || a == `1` && b == `4`]", + "result": [{"a": 1, "b": 2, "c": 3}] + }, + { + "expression": "foo[?b == `2` || a == `3` && b == `4`]", + "result": [{"a": 1, "b": 2, "c": 3}, {"a": 3, "b": 4}] + }, + { + "expression": "foo[?a == `3` && b == `4` || b == `2`]", + "result": [{"a": 1, "b": 2, "c": 3}, {"a": 3, "b": 4}] + }, + { + "expression": "foo[?(a == `3` && b == `4`) || b == `2`]", + "result": [{"a": 1, "b": 2, "c": 3}, {"a": 3, "b": 4}] + }, + { + "expression": "foo[?((a == `3` && b == `4`)) || b == `2`]", + "result": [{"a": 1, "b": 2, "c": 3}, {"a": 3, "b": 4}] + }, + { + "expression": "foo[?a == `3` && (b == `4` || b == `2`)]", + "result": [{"a": 3, "b": 4}] + }, + { + "expression": "foo[?a == `3` && ((b == `4` || b == `2`))]", + "result": [{"a": 3, "b": 4}] + } + ] + }, + { + "given": {"foo": [{"a": 1, "b": 2, "c": 3}, {"a": 3, "b": 4}]}, + "cases": [ + { + "comment": "Verify precedence of or/and expressions", + "expression": "foo[?a == `1` || b ==`2` && c == `5`]", + "result": [{"a": 1, "b": 2, "c": 3}] + }, + { + "comment": "Parentheses can alter precedence", + "expression": "foo[?(a == `1` || b ==`2`) && c == `5`]", + "result": [] + }, + { + "comment": "Not expressions combined with and/or", + "expression": "foo[?!(a == `1` || b ==`2`)]", + "result": [{"a": 3, "b": 4}] + } + ] + }, + { + "given": { + "foo": [ + {"key": true}, + {"key": false}, + {"key": []}, + {"key": {}}, + {"key": [0]}, + {"key": {"a": "b"}}, + {"key": 0}, + {"key": 1}, + {"key": null}, + {"notkey": true} + ] + }, + "cases": [ + { + "comment": "Unary filter expression", + "expression": "foo[?key]", + "result": [ + {"key": true}, {"key": [0]}, {"key": {"a": "b"}}, + {"key": 0}, {"key": 1} + ] + }, + { + "comment": "Unary not filter expression", + "expression": "foo[?!key]", + "result": [ + {"key": false}, {"key": []}, {"key": {}}, + {"key": null}, {"notkey": true} + ] + }, + { + "comment": "Equality with null RHS", + "expression": "foo[?key == `null`]", + "result": [ + {"key": null}, {"notkey": true} + ] + } + ] + }, + { + "given": { + "foo": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + }, + "cases": [ + { + "comment": "Using @ in a filter expression", + "expression": "foo[?@ < `5`]", + "result": [0, 1, 2, 3, 4] + }, + { + "comment": "Using @ in a filter expression", + "expression": "foo[?`5` > @]", + "result": [0, 1, 2, 3, 4] + }, + { + "comment": "Using @ in a filter expression", + "expression": "foo[?@ == @]", + "result": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + } + ] + } +] diff --git a/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/functions.json b/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/functions.json new file mode 100644 index 00000000000..7b55445061d --- /dev/null +++ b/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/functions.json @@ -0,0 +1,841 @@ +[{ + "given": + { + "foo": -1, + "zero": 0, + "numbers": [-1, 3, 4, 5], + "array": [-1, 3, 4, 5, "a", "100"], + "strings": ["a", "b", "c"], + "decimals": [1.01, 1.2, -1.5], + "str": "Str", + "false": false, + "empty_list": [], + "empty_hash": {}, + "objects": {"foo": "bar", "bar": "baz"}, + "null_key": null + }, + "cases": [ + { + "expression": "abs(foo)", + "result": 1 + }, + { + "expression": "abs(foo)", + "result": 1 + }, + { + "expression": "abs(str)", + "error": "invalid-type" + }, + { + "expression": "abs(array[1])", + "result": 3 + }, + { + "expression": "abs(array[1])", + "result": 3 + }, + { + "expression": "abs(`false`)", + "error": "invalid-type" + }, + { + "expression": "abs(`-24`)", + "result": 24 + }, + { + "expression": "abs(`-24`)", + "result": 24 + }, + { + "expression": "abs(`1`, `2`)", + "error": "invalid-arity" + }, + { + "expression": "abs()", + "error": "invalid-arity" + }, + { + "expression": "unknown_function(`1`, `2`)", + "error": "unknown-function" + }, + { + "expression": "avg(numbers)", + "result": 2.75 + }, + { + "expression": "avg(array)", + "error": "invalid-type" + }, + { + "expression": "avg('abc')", + "error": "invalid-type" + }, + { + "expression": "avg(foo)", + "error": "invalid-type" + }, + { + "expression": "avg(@)", + "error": "invalid-type" + }, + { + "expression": "avg(strings)", + "error": "invalid-type" + }, + { + "expression": "avg(empty_list)", + "result": null + }, + { + "expression": "ceil(`1.2`)", + "result": 2 + }, + { + "expression": "ceil(decimals[0])", + "result": 2 + }, + { + "expression": "ceil(decimals[1])", + "result": 2 + }, + { + "expression": "ceil(decimals[2])", + "result": -1 + }, + { + "expression": "ceil('string')", + "error": "invalid-type" + }, + { + "expression": "contains('abc', 'a')", + "result": true + }, + { + "expression": "contains('abc', 'd')", + "result": false + }, + { + "expression": "contains(`false`, 'd')", + "error": "invalid-type" + }, + { + "expression": "contains(strings, 'a')", + "result": true + }, + { + "expression": "contains(decimals, `1.2`)", + "result": true + }, + { + "expression": "contains(decimals, `false`)", + "result": false + }, + { + "expression": "ends_with(str, 'r')", + "result": true + }, + { + "expression": "ends_with(str, 'tr')", + "result": true + }, + { + "expression": "ends_with(str, 'Str')", + "result": true + }, + { + "expression": "ends_with(str, 'SStr')", + "result": false + }, + { + "expression": "ends_with(str, 'foo')", + "result": false + }, + { + "expression": "ends_with(str, `0`)", + "error": "invalid-type" + }, + { + "expression": "floor(`1.2`)", + "result": 1 + }, + { + "expression": "floor('string')", + "error": "invalid-type" + }, + { + "expression": "floor(decimals[0])", + "result": 1 + }, + { + "expression": "floor(foo)", + "result": -1 + }, + { + "expression": "floor(str)", + "error": "invalid-type" + }, + { + "expression": "length('abc')", + "result": 3 + }, + { + "expression": "length('✓foo')", + "result": 4 + }, + { + "expression": "length('')", + "result": 0 + }, + { + "expression": "length(@)", + "result": 12 + }, + { + "expression": "length(strings[0])", + "result": 1 + }, + { + "expression": "length(str)", + "result": 3 + }, + { + "expression": "length(array)", + "result": 6 + }, + { + "expression": "length(objects)", + "result": 2 + }, + { + "expression": "length(`false`)", + "error": "invalid-type" + }, + { + "expression": "length(foo)", + "error": "invalid-type" + }, + { + "expression": "length(strings[0])", + "result": 1 + }, + { + "expression": "max(numbers)", + "result": 5 + }, + { + "expression": "max(decimals)", + "result": 1.2 + }, + { + "expression": "max(strings)", + "result": "c" + }, + { + "expression": "max(abc)", + "error": "invalid-type" + }, + { + "expression": "max(array)", + "error": "invalid-type" + }, + { + "expression": "max(decimals)", + "result": 1.2 + }, + { + "expression": "max(empty_list)", + "result": null + }, + { + "expression": "merge(`{}`)", + "result": {} + }, + { + "expression": "merge(`{}`, `{}`)", + "result": {} + }, + { + "expression": "merge(`{\"a\": 1}`, `{\"b\": 2}`)", + "result": {"a": 1, "b": 2} + }, + { + "expression": "merge(`{\"a\": 1}`, `{\"a\": 2}`)", + "result": {"a": 2} + }, + { + "expression": "merge(`{\"a\": 1, \"b\": 2}`, `{\"a\": 2, \"c\": 3}`, `{\"d\": 4}`)", + "result": {"a": 2, "b": 2, "c": 3, "d": 4} + }, + { + "expression": "min(numbers)", + "result": -1 + }, + { + "expression": "min(decimals)", + "result": -1.5 + }, + { + "expression": "min(abc)", + "error": "invalid-type" + }, + { + "expression": "min(array)", + "error": "invalid-type" + }, + { + "expression": "min(empty_list)", + "result": null + }, + { + "expression": "min(decimals)", + "result": -1.5 + }, + { + "expression": "min(strings)", + "result": "a" + }, + { + "expression": "type('abc')", + "result": "string" + }, + { + "expression": "type(`1.0`)", + "result": "number" + }, + { + "expression": "type(`2`)", + "result": "number" + }, + { + "expression": "type(`true`)", + "result": "boolean" + }, + { + "expression": "type(`false`)", + "result": "boolean" + }, + { + "expression": "type(`null`)", + "result": "null" + }, + { + "expression": "type(`[0]`)", + "result": "array" + }, + { + "expression": "type(`{\"a\": \"b\"}`)", + "result": "object" + }, + { + "expression": "type(@)", + "result": "object" + }, + { + "expression": "sort(keys(objects))", + "result": ["bar", "foo"] + }, + { + "expression": "keys(foo)", + "error": "invalid-type" + }, + { + "expression": "keys(strings)", + "error": "invalid-type" + }, + { + "expression": "keys(`false`)", + "error": "invalid-type" + }, + { + "expression": "sort(values(objects))", + "result": ["bar", "baz"] + }, + { + "expression": "keys(empty_hash)", + "result": [] + }, + { + "expression": "values(foo)", + "error": "invalid-type" + }, + { + "expression": "join(', ', strings)", + "result": "a, b, c" + }, + { + "expression": "join(', ', strings)", + "result": "a, b, c" + }, + { + "expression": "join(',', `[\"a\", \"b\"]`)", + "result": "a,b" + }, + { + "expression": "join(',', `[\"a\", 0]`)", + "error": "invalid-type" + }, + { + "expression": "join(', ', str)", + "error": "invalid-type" + }, + { + "expression": "join('|', strings)", + "result": "a|b|c" + }, + { + "expression": "join(`2`, strings)", + "error": "invalid-type" + }, + { + "expression": "join('|', decimals)", + "error": "invalid-type" + }, + { + "expression": "join('|', decimals[].to_string(@))", + "result": "1.01|1.2|-1.5" + }, + { + "expression": "join('|', empty_list)", + "result": "" + }, + { + "expression": "reverse(numbers)", + "result": [5, 4, 3, -1] + }, + { + "expression": "reverse(array)", + "result": ["100", "a", 5, 4, 3, -1] + }, + { + "expression": "reverse(`[]`)", + "result": [] + }, + { + "expression": "reverse('')", + "result": "" + }, + { + "expression": "reverse('hello world')", + "result": "dlrow olleh" + }, + { + "expression": "starts_with(str, 'S')", + "result": true + }, + { + "expression": "starts_with(str, 'St')", + "result": true + }, + { + "expression": "starts_with(str, 'Str')", + "result": true + }, + { + "expression": "starts_with(str, 'String')", + "result": false + }, + { + "expression": "starts_with(str, `0`)", + "error": "invalid-type" + }, + { + "expression": "sum(numbers)", + "result": 11 + }, + { + "expression": "sum(decimals)", + "result": 0.71 + }, + { + "expression": "sum(array)", + "error": "invalid-type" + }, + { + "expression": "sum(array[].to_number(@))", + "result": 111 + }, + { + "expression": "sum(`[]`)", + "result": 0 + }, + { + "expression": "to_array('foo')", + "result": ["foo"] + }, + { + "expression": "to_array(`0`)", + "result": [0] + }, + { + "expression": "to_array(objects)", + "result": [{"foo": "bar", "bar": "baz"}] + }, + { + "expression": "to_array(`[1, 2, 3]`)", + "result": [1, 2, 3] + }, + { + "expression": "to_array(false)", + "result": [false] + }, + { + "expression": "to_string('foo')", + "result": "foo" + }, + { + "expression": "to_string(`1.2`)", + "result": "1.2" + }, + { + "expression": "to_string(`[0, 1]`)", + "result": "[0,1]" + }, + { + "expression": "to_number('1.0')", + "result": 1.0 + }, + { + "expression": "to_number('1e21')", + "result": 1e21 + }, + { + "expression": "to_number('1.1')", + "result": 1.1 + }, + { + "expression": "to_number('4')", + "result": 4 + }, + { + "expression": "to_number('notanumber')", + "result": null + }, + { + "expression": "to_number(`false`)", + "result": null + }, + { + "expression": "to_number(`null`)", + "result": null + }, + { + "expression": "to_number(`[0]`)", + "result": null + }, + { + "expression": "to_number(`{\"foo\": 0}`)", + "result": null + }, + { + "expression": "\"to_string\"(`1.0`)", + "error": "syntax" + }, + { + "expression": "sort(numbers)", + "result": [-1, 3, 4, 5] + }, + { + "expression": "sort(strings)", + "result": ["a", "b", "c"] + }, + { + "expression": "sort(decimals)", + "result": [-1.5, 1.01, 1.2] + }, + { + "expression": "sort(array)", + "error": "invalid-type" + }, + { + "expression": "sort(abc)", + "error": "invalid-type" + }, + { + "expression": "sort(empty_list)", + "result": [] + }, + { + "expression": "sort(@)", + "error": "invalid-type" + }, + { + "expression": "not_null(unknown_key, str)", + "result": "Str" + }, + { + "expression": "not_null(unknown_key, foo.bar, empty_list, str)", + "result": [] + }, + { + "expression": "not_null(unknown_key, null_key, empty_list, str)", + "result": [] + }, + { + "expression": "not_null(all, expressions, are_null)", + "result": null + }, + { + "expression": "not_null()", + "error": "invalid-arity" + }, + { + "comment": "function projection on single arg function", + "expression": "numbers[].to_string(@)", + "result": ["-1", "3", "4", "5"] + }, + { + "comment": "function projection on single arg function", + "expression": "array[].to_number(@)", + "result": [-1, 3, 4, 5, 100] + } + ] +}, { + "given": + { + "foo": [ + {"b": "b", "a": "a"}, + {"c": "c", "b": "b"}, + {"d": "d", "c": "c"}, + {"e": "e", "d": "d"}, + {"f": "f", "e": "e"} + ] + }, + "cases": [ + { + "comment": "function projection on variadic function", + "expression": "foo[].not_null(f, e, d, c, b, a)", + "result": ["b", "c", "d", "e", "f"] + } + ] +}, { + "given": + { + "people": [ + {"age": 20, "age_str": "20", "bool": true, "name": "a", "extra": "foo"}, + {"age": 40, "age_str": "40", "bool": false, "name": "b", "extra": "bar"}, + {"age": 30, "age_str": "30", "bool": true, "name": "c"}, + {"age": 50, "age_str": "50", "bool": false, "name": "d"}, + {"age": 10, "age_str": "10", "bool": true, "name": 3} + ] + }, + "cases": [ + { + "comment": "sort by field expression", + "expression": "sort_by(people, &age)", + "result": [ + {"age": 10, "age_str": "10", "bool": true, "name": 3}, + {"age": 20, "age_str": "20", "bool": true, "name": "a", "extra": "foo"}, + {"age": 30, "age_str": "30", "bool": true, "name": "c"}, + {"age": 40, "age_str": "40", "bool": false, "name": "b", "extra": "bar"}, + {"age": 50, "age_str": "50", "bool": false, "name": "d"} + ] + }, + { + "expression": "sort_by(people, &age_str)", + "result": [ + {"age": 10, "age_str": "10", "bool": true, "name": 3}, + {"age": 20, "age_str": "20", "bool": true, "name": "a", "extra": "foo"}, + {"age": 30, "age_str": "30", "bool": true, "name": "c"}, + {"age": 40, "age_str": "40", "bool": false, "name": "b", "extra": "bar"}, + {"age": 50, "age_str": "50", "bool": false, "name": "d"} + ] + }, + { + "comment": "sort by function expression", + "expression": "sort_by(people, &to_number(age_str))", + "result": [ + {"age": 10, "age_str": "10", "bool": true, "name": 3}, + {"age": 20, "age_str": "20", "bool": true, "name": "a", "extra": "foo"}, + {"age": 30, "age_str": "30", "bool": true, "name": "c"}, + {"age": 40, "age_str": "40", "bool": false, "name": "b", "extra": "bar"}, + {"age": 50, "age_str": "50", "bool": false, "name": "d"} + ] + }, + { + "comment": "function projection on sort_by function", + "expression": "sort_by(people, &age)[].name", + "result": [3, "a", "c", "b", "d"] + }, + { + "expression": "sort_by(people, &extra)", + "error": "invalid-type" + }, + { + "expression": "sort_by(people, &bool)", + "error": "invalid-type" + }, + { + "expression": "sort_by(people, &name)", + "error": "invalid-type" + }, + { + "expression": "sort_by(people, name)", + "error": "invalid-type" + }, + { + "expression": "sort_by(people, &age)[].extra", + "result": ["foo", "bar"] + }, + { + "expression": "sort_by(`[]`, &age)", + "result": [] + }, + { + "expression": "max_by(people, &age)", + "result": {"age": 50, "age_str": "50", "bool": false, "name": "d"} + }, + { + "expression": "max_by(people, &age_str)", + "result": {"age": 50, "age_str": "50", "bool": false, "name": "d"} + }, + { + "expression": "max_by(people, &bool)", + "error": "invalid-type" + }, + { + "expression": "max_by(people, &extra)", + "error": "invalid-type" + }, + { + "expression": "max_by(people, &to_number(age_str))", + "result": {"age": 50, "age_str": "50", "bool": false, "name": "d"} + }, + { + "expression": "max_by(`[]`, &age)", + "result": null + }, + { + "expression": "min_by(people, &age)", + "result": {"age": 10, "age_str": "10", "bool": true, "name": 3} + }, + { + "expression": "min_by(people, &age_str)", + "result": {"age": 10, "age_str": "10", "bool": true, "name": 3} + }, + { + "expression": "min_by(people, &bool)", + "error": "invalid-type" + }, + { + "expression": "min_by(people, &extra)", + "error": "invalid-type" + }, + { + "expression": "min_by(people, &to_number(age_str))", + "result": {"age": 10, "age_str": "10", "bool": true, "name": 3} + }, + { + "expression": "min_by(`[]`, &age)", + "result": null + } + ] +}, { + "given": + { + "people": [ + {"age": 10, "order": "1"}, + {"age": 10, "order": "2"}, + {"age": 10, "order": "3"}, + {"age": 10, "order": "4"}, + {"age": 10, "order": "5"}, + {"age": 10, "order": "6"}, + {"age": 10, "order": "7"}, + {"age": 10, "order": "8"}, + {"age": 10, "order": "9"}, + {"age": 10, "order": "10"}, + {"age": 10, "order": "11"} + ] + }, + "cases": [ + { + "comment": "stable sort order", + "expression": "sort_by(people, &age)", + "result": [ + {"age": 10, "order": "1"}, + {"age": 10, "order": "2"}, + {"age": 10, "order": "3"}, + {"age": 10, "order": "4"}, + {"age": 10, "order": "5"}, + {"age": 10, "order": "6"}, + {"age": 10, "order": "7"}, + {"age": 10, "order": "8"}, + {"age": 10, "order": "9"}, + {"age": 10, "order": "10"}, + {"age": 10, "order": "11"} + ] + } + ] +}, { + "given": + { + "people": [ + {"a": 10, "b": 1, "c": "z"}, + {"a": 10, "b": 2, "c": null}, + {"a": 10, "b": 3}, + {"a": 10, "b": 4, "c": "z"}, + {"a": 10, "b": 5, "c": null}, + {"a": 10, "b": 6}, + {"a": 10, "b": 7, "c": "z"}, + {"a": 10, "b": 8, "c": null}, + {"a": 10, "b": 9} + ], + "empty": [] + }, + "cases": [ + { + "expression": "map(&a, people)", + "result": [10, 10, 10, 10, 10, 10, 10, 10, 10] + }, + { + "expression": "map(&c, people)", + "result": ["z", null, null, "z", null, null, "z", null, null] + }, + { + "expression": "map(&a, badkey)", + "error": "invalid-type" + }, + { + "expression": "map(&foo, empty)", + "result": [] + } + ] +}, { + "given": { + "array": [ + { + "foo": {"bar": "yes1"} + }, + { + "foo": {"bar": "yes2"} + }, + { + "foo1": {"bar": "no"} + } + ]}, + "cases": [ + { + "expression": "map(&foo.bar, array)", + "result": ["yes1", "yes2", null] + }, + { + "expression": "map(&foo1.bar, array)", + "result": [null, null, "no"] + }, + { + "expression": "map(&foo.bar.baz, array)", + "result": [null, null, null] + } + ] +}, { + "given": { + "array": [[1, 2, 3, [4]], [5, 6, 7, [8, 9]]] + }, + "cases": [ + { + "expression": "map(&[], array)", + "result": [[1, 2, 3, 4], [5, 6, 7, 8, 9]] + } + ] +} +] diff --git a/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/identifiers.json b/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/identifiers.json new file mode 100644 index 00000000000..7998a41ac9d --- /dev/null +++ b/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/identifiers.json @@ -0,0 +1,1377 @@ +[ + { + "given": { + "__L": true + }, + "cases": [ + { + "expression": "__L", + "result": true + } + ] + }, + { + "given": { + "!\r": true + }, + "cases": [ + { + "expression": "\"!\\r\"", + "result": true + } + ] + }, + { + "given": { + "Y_1623": true + }, + "cases": [ + { + "expression": "Y_1623", + "result": true + } + ] + }, + { + "given": { + "x": true + }, + "cases": [ + { + "expression": "x", + "result": true + } + ] + }, + { + "given": { + "\tF\uCebb": true + }, + "cases": [ + { + "expression": "\"\\tF\\uCebb\"", + "result": true + } + ] + }, + { + "given": { + " \t": true + }, + "cases": [ + { + "expression": "\" \\t\"", + "result": true + } + ] + }, + { + "given": { + " ": true + }, + "cases": [ + { + "expression": "\" \"", + "result": true + } + ] + }, + { + "given": { + "v2": true + }, + "cases": [ + { + "expression": "v2", + "result": true + } + ] + }, + { + "given": { + "\t": true + }, + "cases": [ + { + "expression": "\"\\t\"", + "result": true + } + ] + }, + { + "given": { + "_X": true + }, + "cases": [ + { + "expression": "_X", + "result": true + } + ] + }, + { + "given": { + "\t4\ud9da\udd15": true + }, + "cases": [ + { + "expression": "\"\\t4\\ud9da\\udd15\"", + "result": true + } + ] + }, + { + "given": { + "v24_W": true + }, + "cases": [ + { + "expression": "v24_W", + "result": true + } + ] + }, + { + "given": { + "H": true + }, + "cases": [ + { + "expression": "\"H\"", + "result": true + } + ] + }, + { + "given": { + "\f": true + }, + "cases": [ + { + "expression": "\"\\f\"", + "result": true + } + ] + }, + { + "given": { + "E4": true + }, + "cases": [ + { + "expression": "\"E4\"", + "result": true + } + ] + }, + { + "given": { + "!": true + }, + "cases": [ + { + "expression": "\"!\"", + "result": true + } + ] + }, + { + "given": { + "tM": true + }, + "cases": [ + { + "expression": "tM", + "result": true + } + ] + }, + { + "given": { + " [": true + }, + "cases": [ + { + "expression": "\" [\"", + "result": true + } + ] + }, + { + "given": { + "R!": true + }, + "cases": [ + { + "expression": "\"R!\"", + "result": true + } + ] + }, + { + "given": { + "_6W": true + }, + "cases": [ + { + "expression": "_6W", + "result": true + } + ] + }, + { + "given": { + "\uaBA1\r": true + }, + "cases": [ + { + "expression": "\"\\uaBA1\\r\"", + "result": true + } + ] + }, + { + "given": { + "tL7": true + }, + "cases": [ + { + "expression": "tL7", + "result": true + } + ] + }, + { + "given": { + "<": true + }, + "cases": [ + { + "expression": "\">\"", + "result": true + } + ] + }, + { + "given": { + "hvu": true + }, + "cases": [ + { + "expression": "hvu", + "result": true + } + ] + }, + { + "given": { + "; !": true + }, + "cases": [ + { + "expression": "\"; !\"", + "result": true + } + ] + }, + { + "given": { + "hU": true + }, + "cases": [ + { + "expression": "hU", + "result": true + } + ] + }, + { + "given": { + "!I\n\/": true + }, + "cases": [ + { + "expression": "\"!I\\n\\/\"", + "result": true + } + ] + }, + { + "given": { + "\uEEbF": true + }, + "cases": [ + { + "expression": "\"\\uEEbF\"", + "result": true + } + ] + }, + { + "given": { + "U)\t": true + }, + "cases": [ + { + "expression": "\"U)\\t\"", + "result": true + } + ] + }, + { + "given": { + "fa0_9": true + }, + "cases": [ + { + "expression": "fa0_9", + "result": true + } + ] + }, + { + "given": { + "/": true + }, + "cases": [ + { + "expression": "\"/\"", + "result": true + } + ] + }, + { + "given": { + "Gy": true + }, + "cases": [ + { + "expression": "Gy", + "result": true + } + ] + }, + { + "given": { + "\b": true + }, + "cases": [ + { + "expression": "\"\\b\"", + "result": true + } + ] + }, + { + "given": { + "<": true + }, + "cases": [ + { + "expression": "\"<\"", + "result": true + } + ] + }, + { + "given": { + "\t": true + }, + "cases": [ + { + "expression": "\"\\t\"", + "result": true + } + ] + }, + { + "given": { + "\t&\\\r": true + }, + "cases": [ + { + "expression": "\"\\t&\\\\\\r\"", + "result": true + } + ] + }, + { + "given": { + "#": true + }, + "cases": [ + { + "expression": "\"#\"", + "result": true + } + ] + }, + { + "given": { + "B__": true + }, + "cases": [ + { + "expression": "B__", + "result": true + } + ] + }, + { + "given": { + "\nS \n": true + }, + "cases": [ + { + "expression": "\"\\nS \\n\"", + "result": true + } + ] + }, + { + "given": { + "Bp": true + }, + "cases": [ + { + "expression": "Bp", + "result": true + } + ] + }, + { + "given": { + ",\t;": true + }, + "cases": [ + { + "expression": "\",\\t;\"", + "result": true + } + ] + }, + { + "given": { + "B_q": true + }, + "cases": [ + { + "expression": "B_q", + "result": true + } + ] + }, + { + "given": { + "\/+\t\n\b!Z": true + }, + "cases": [ + { + "expression": "\"\\/+\\t\\n\\b!Z\"", + "result": true + } + ] + }, + { + "given": { + "\udadd\udfc7\\ueFAc": true + }, + "cases": [ + { + "expression": "\"\udadd\udfc7\\\\ueFAc\"", + "result": true + } + ] + }, + { + "given": { + ":\f": true + }, + "cases": [ + { + "expression": "\":\\f\"", + "result": true + } + ] + }, + { + "given": { + "\/": true + }, + "cases": [ + { + "expression": "\"\\/\"", + "result": true + } + ] + }, + { + "given": { + "_BW_6Hg_Gl": true + }, + "cases": [ + { + "expression": "_BW_6Hg_Gl", + "result": true + } + ] + }, + { + "given": { + "\udbcf\udc02": true + }, + "cases": [ + { + "expression": "\"\udbcf\udc02\"", + "result": true + } + ] + }, + { + "given": { + "zs1DC": true + }, + "cases": [ + { + "expression": "zs1DC", + "result": true + } + ] + }, + { + "given": { + "__434": true + }, + "cases": [ + { + "expression": "__434", + "result": true + } + ] + }, + { + "given": { + "\udb94\udd41": true + }, + "cases": [ + { + "expression": "\"\udb94\udd41\"", + "result": true + } + ] + }, + { + "given": { + "Z_5": true + }, + "cases": [ + { + "expression": "Z_5", + "result": true + } + ] + }, + { + "given": { + "z_M_": true + }, + "cases": [ + { + "expression": "z_M_", + "result": true + } + ] + }, + { + "given": { + "YU_2": true + }, + "cases": [ + { + "expression": "YU_2", + "result": true + } + ] + }, + { + "given": { + "_0": true + }, + "cases": [ + { + "expression": "_0", + "result": true + } + ] + }, + { + "given": { + "\b+": true + }, + "cases": [ + { + "expression": "\"\\b+\"", + "result": true + } + ] + }, + { + "given": { + "\"": true + }, + "cases": [ + { + "expression": "\"\\\"\"", + "result": true + } + ] + }, + { + "given": { + "D7": true + }, + "cases": [ + { + "expression": "D7", + "result": true + } + ] + }, + { + "given": { + "_62L": true + }, + "cases": [ + { + "expression": "_62L", + "result": true + } + ] + }, + { + "given": { + "\tK\t": true + }, + "cases": [ + { + "expression": "\"\\tK\\t\"", + "result": true + } + ] + }, + { + "given": { + "\n\\\f": true + }, + "cases": [ + { + "expression": "\"\\n\\\\\\f\"", + "result": true + } + ] + }, + { + "given": { + "I_": true + }, + "cases": [ + { + "expression": "I_", + "result": true + } + ] + }, + { + "given": { + "W_a0_": true + }, + "cases": [ + { + "expression": "W_a0_", + "result": true + } + ] + }, + { + "given": { + "BQ": true + }, + "cases": [ + { + "expression": "BQ", + "result": true + } + ] + }, + { + "given": { + "\tX$\uABBb": true + }, + "cases": [ + { + "expression": "\"\\tX$\\uABBb\"", + "result": true + } + ] + }, + { + "given": { + "Z9": true + }, + "cases": [ + { + "expression": "Z9", + "result": true + } + ] + }, + { + "given": { + "\b%\"\uda38\udd0f": true + }, + "cases": [ + { + "expression": "\"\\b%\\\"\uda38\udd0f\"", + "result": true + } + ] + }, + { + "given": { + "_F": true + }, + "cases": [ + { + "expression": "_F", + "result": true + } + ] + }, + { + "given": { + "!,": true + }, + "cases": [ + { + "expression": "\"!,\"", + "result": true + } + ] + }, + { + "given": { + "\"!": true + }, + "cases": [ + { + "expression": "\"\\\"!\"", + "result": true + } + ] + }, + { + "given": { + "Hh": true + }, + "cases": [ + { + "expression": "Hh", + "result": true + } + ] + }, + { + "given": { + "&": true + }, + "cases": [ + { + "expression": "\"&\"", + "result": true + } + ] + }, + { + "given": { + "9\r\\R": true + }, + "cases": [ + { + "expression": "\"9\\r\\\\R\"", + "result": true + } + ] + }, + { + "given": { + "M_k": true + }, + "cases": [ + { + "expression": "M_k", + "result": true + } + ] + }, + { + "given": { + "!\b\n\udb06\ude52\"\"": true + }, + "cases": [ + { + "expression": "\"!\\b\\n\udb06\ude52\\\"\\\"\"", + "result": true + } + ] + }, + { + "given": { + "6": true + }, + "cases": [ + { + "expression": "\"6\"", + "result": true + } + ] + }, + { + "given": { + "_7": true + }, + "cases": [ + { + "expression": "_7", + "result": true + } + ] + }, + { + "given": { + "0": true + }, + "cases": [ + { + "expression": "\"0\"", + "result": true + } + ] + }, + { + "given": { + "\\8\\": true + }, + "cases": [ + { + "expression": "\"\\\\8\\\\\"", + "result": true + } + ] + }, + { + "given": { + "b7eo": true + }, + "cases": [ + { + "expression": "b7eo", + "result": true + } + ] + }, + { + "given": { + "xIUo9": true + }, + "cases": [ + { + "expression": "xIUo9", + "result": true + } + ] + }, + { + "given": { + "5": true + }, + "cases": [ + { + "expression": "\"5\"", + "result": true + } + ] + }, + { + "given": { + "?": true + }, + "cases": [ + { + "expression": "\"?\"", + "result": true + } + ] + }, + { + "given": { + "sU": true + }, + "cases": [ + { + "expression": "sU", + "result": true + } + ] + }, + { + "given": { + "VH2&H\\\/": true + }, + "cases": [ + { + "expression": "\"VH2&H\\\\\\/\"", + "result": true + } + ] + }, + { + "given": { + "_C": true + }, + "cases": [ + { + "expression": "_C", + "result": true + } + ] + }, + { + "given": { + "_": true + }, + "cases": [ + { + "expression": "_", + "result": true + } + ] + }, + { + "given": { + "<\t": true + }, + "cases": [ + { + "expression": "\"<\\t\"", + "result": true + } + ] + }, + { + "given": { + "\uD834\uDD1E": true + }, + "cases": [ + { + "expression": "\"\\uD834\\uDD1E\"", + "result": true + } + ] + } +] diff --git a/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/indices.json b/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/indices.json new file mode 100644 index 00000000000..aa03b35dd7f --- /dev/null +++ b/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/indices.json @@ -0,0 +1,346 @@ +[{ + "given": + {"foo": {"bar": ["zero", "one", "two"]}}, + "cases": [ + { + "expression": "foo.bar[0]", + "result": "zero" + }, + { + "expression": "foo.bar[1]", + "result": "one" + }, + { + "expression": "foo.bar[2]", + "result": "two" + }, + { + "expression": "foo.bar[3]", + "result": null + }, + { + "expression": "foo.bar[-1]", + "result": "two" + }, + { + "expression": "foo.bar[-2]", + "result": "one" + }, + { + "expression": "foo.bar[-3]", + "result": "zero" + }, + { + "expression": "foo.bar[-4]", + "result": null + } + ] +}, +{ + "given": + {"foo": [{"bar": "one"}, {"bar": "two"}, {"bar": "three"}, {"notbar": "four"}]}, + "cases": [ + { + "expression": "foo.bar", + "result": null + }, + { + "expression": "foo[0].bar", + "result": "one" + }, + { + "expression": "foo[1].bar", + "result": "two" + }, + { + "expression": "foo[2].bar", + "result": "three" + }, + { + "expression": "foo[3].notbar", + "result": "four" + }, + { + "expression": "foo[3].bar", + "result": null + }, + { + "expression": "foo[0]", + "result": {"bar": "one"} + }, + { + "expression": "foo[1]", + "result": {"bar": "two"} + }, + { + "expression": "foo[2]", + "result": {"bar": "three"} + }, + { + "expression": "foo[3]", + "result": {"notbar": "four"} + }, + { + "expression": "foo[4]", + "result": null + } + ] +}, +{ + "given": [ + "one", "two", "three" + ], + "cases": [ + { + "expression": "[0]", + "result": "one" + }, + { + "expression": "[1]", + "result": "two" + }, + { + "expression": "[2]", + "result": "three" + }, + { + "expression": "[-1]", + "result": "three" + }, + { + "expression": "[-2]", + "result": "two" + }, + { + "expression": "[-3]", + "result": "one" + } + ] +}, +{ + "given": {"reservations": [ + {"instances": [{"foo": 1}, {"foo": 2}]} + ]}, + "cases": [ + { + "expression": "reservations[].instances[].foo", + "result": [1, 2] + }, + { + "expression": "reservations[].instances[].bar", + "result": [] + }, + { + "expression": "reservations[].notinstances[].foo", + "result": [] + }, + { + "expression": "reservations[].notinstances[].foo", + "result": [] + } + ] +}, +{ + "given": {"reservations": [{ + "instances": [ + {"foo": [{"bar": 1}, {"bar": 2}, {"notbar": 3}, {"bar": 4}]}, + {"foo": [{"bar": 5}, {"bar": 6}, {"notbar": [7]}, {"bar": 8}]}, + {"foo": "bar"}, + {"notfoo": [{"bar": 20}, {"bar": 21}, {"notbar": [7]}, {"bar": 22}]}, + {"bar": [{"baz": [1]}, {"baz": [2]}, {"baz": [3]}, {"baz": [4]}]}, + {"baz": [{"baz": [1, 2]}, {"baz": []}, {"baz": []}, {"baz": [3, 4]}]}, + {"qux": [{"baz": []}, {"baz": [1, 2, 3]}, {"baz": [4]}, {"baz": []}]} + ], + "otherkey": {"foo": [{"bar": 1}, {"bar": 2}, {"notbar": 3}, {"bar": 4}]} + }, { + "instances": [ + {"a": [{"bar": 1}, {"bar": 2}, {"notbar": 3}, {"bar": 4}]}, + {"b": [{"bar": 5}, {"bar": 6}, {"notbar": [7]}, {"bar": 8}]}, + {"c": "bar"}, + {"notfoo": [{"bar": 23}, {"bar": 24}, {"notbar": [7]}, {"bar": 25}]}, + {"qux": [{"baz": []}, {"baz": [1, 2, 3]}, {"baz": [4]}, {"baz": []}]} + ], + "otherkey": {"foo": [{"bar": 1}, {"bar": 2}, {"notbar": 3}, {"bar": 4}]} + } + ]}, + "cases": [ + { + "expression": "reservations[].instances[].foo[].bar", + "result": [1, 2, 4, 5, 6, 8] + }, + { + "expression": "reservations[].instances[].foo[].baz", + "result": [] + }, + { + "expression": "reservations[].instances[].notfoo[].bar", + "result": [20, 21, 22, 23, 24, 25] + }, + { + "expression": "reservations[].instances[].notfoo[].notbar", + "result": [[7], [7]] + }, + { + "expression": "reservations[].notinstances[].foo", + "result": [] + }, + { + "expression": "reservations[].instances[].foo[].notbar", + "result": [3, [7]] + }, + { + "expression": "reservations[].instances[].bar[].baz", + "result": [[1], [2], [3], [4]] + }, + { + "expression": "reservations[].instances[].baz[].baz", + "result": [[1, 2], [], [], [3, 4]] + }, + { + "expression": "reservations[].instances[].qux[].baz", + "result": [[], [1, 2, 3], [4], [], [], [1, 2, 3], [4], []] + }, + { + "expression": "reservations[].instances[].qux[].baz[]", + "result": [1, 2, 3, 4, 1, 2, 3, 4] + } + ] +}, +{ + "given": { + "foo": [ + [["one", "two"], ["three", "four"]], + [["five", "six"], ["seven", "eight"]], + [["nine"], ["ten"]] + ] + }, + "cases": [ + { + "expression": "foo[]", + "result": [["one", "two"], ["three", "four"], ["five", "six"], + ["seven", "eight"], ["nine"], ["ten"]] + }, + { + "expression": "foo[][0]", + "result": ["one", "three", "five", "seven", "nine", "ten"] + }, + { + "expression": "foo[][1]", + "result": ["two", "four", "six", "eight"] + }, + { + "expression": "foo[][0][0]", + "result": [] + }, + { + "expression": "foo[][2][2]", + "result": [] + }, + { + "expression": "foo[][0][0][100]", + "result": [] + } + ] +}, +{ + "given": { + "foo": [{ + "bar": [ + { + "qux": 2, + "baz": 1 + }, + { + "qux": 4, + "baz": 3 + } + ] + }, + { + "bar": [ + { + "qux": 6, + "baz": 5 + }, + { + "qux": 8, + "baz": 7 + } + ] + } + ] + }, + "cases": [ + { + "expression": "foo", + "result": [{"bar": [{"qux": 2, "baz": 1}, {"qux": 4, "baz": 3}]}, + {"bar": [{"qux": 6, "baz": 5}, {"qux": 8, "baz": 7}]}] + }, + { + "expression": "foo[]", + "result": [{"bar": [{"qux": 2, "baz": 1}, {"qux": 4, "baz": 3}]}, + {"bar": [{"qux": 6, "baz": 5}, {"qux": 8, "baz": 7}]}] + }, + { + "expression": "foo[].bar", + "result": [[{"qux": 2, "baz": 1}, {"qux": 4, "baz": 3}], + [{"qux": 6, "baz": 5}, {"qux": 8, "baz": 7}]] + }, + { + "expression": "foo[].bar[]", + "result": [{"qux": 2, "baz": 1}, {"qux": 4, "baz": 3}, + {"qux": 6, "baz": 5}, {"qux": 8, "baz": 7}] + }, + { + "expression": "foo[].bar[].baz", + "result": [1, 3, 5, 7] + } + ] +}, +{ + "given": { + "string": "string", + "hash": {"foo": "bar", "bar": "baz"}, + "number": 23, + "nullvalue": null + }, + "cases": [ + { + "expression": "string[]", + "result": null + }, + { + "expression": "hash[]", + "result": null + }, + { + "expression": "number[]", + "result": null + }, + { + "expression": "nullvalue[]", + "result": null + }, + { + "expression": "string[].foo", + "result": null + }, + { + "expression": "hash[].foo", + "result": null + }, + { + "expression": "number[].foo", + "result": null + }, + { + "expression": "nullvalue[].foo", + "result": null + }, + { + "expression": "nullvalue[].foo[].bar", + "result": null + } + ] +} +] diff --git a/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/literal.json b/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/literal.json new file mode 100644 index 00000000000..b5ddbeda185 --- /dev/null +++ b/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/literal.json @@ -0,0 +1,200 @@ +[ + { + "given": { + "foo": [{"name": "a"}, {"name": "b"}], + "bar": {"baz": "qux"} + }, + "cases": [ + { + "expression": "`\"foo\"`", + "result": "foo" + }, + { + "comment": "Interpret escaped unicode.", + "expression": "`\"\\u03a6\"`", + "result": "Φ" + }, + { + "expression": "`\"✓\"`", + "result": "✓" + }, + { + "expression": "`[1, 2, 3]`", + "result": [1, 2, 3] + }, + { + "expression": "`{\"a\": \"b\"}`", + "result": {"a": "b"} + }, + { + "expression": "`true`", + "result": true + }, + { + "expression": "`false`", + "result": false + }, + { + "expression": "`null`", + "result": null + }, + { + "expression": "`0`", + "result": 0 + }, + { + "expression": "`1`", + "result": 1 + }, + { + "expression": "`2`", + "result": 2 + }, + { + "expression": "`3`", + "result": 3 + }, + { + "expression": "`4`", + "result": 4 + }, + { + "expression": "`5`", + "result": 5 + }, + { + "expression": "`6`", + "result": 6 + }, + { + "expression": "`7`", + "result": 7 + }, + { + "expression": "`8`", + "result": 8 + }, + { + "expression": "`9`", + "result": 9 + }, + { + "comment": "Escaping a backtick in quotes", + "expression": "`\"foo\\`bar\"`", + "result": "foo`bar" + }, + { + "comment": "Double quote in literal", + "expression": "`\"foo\\\"bar\"`", + "result": "foo\"bar" + }, + { + "expression": "`\"1\\`\"`", + "result": "1`" + }, + { + "comment": "Multiple literal expressions with escapes", + "expression": "`\"\\\\\"`.{a:`\"b\"`}", + "result": {"a": "b"} + }, + { + "comment": "literal . identifier", + "expression": "`{\"a\": \"b\"}`.a", + "result": "b" + }, + { + "comment": "literal . identifier . identifier", + "expression": "`{\"a\": {\"b\": \"c\"}}`.a.b", + "result": "c" + }, + { + "comment": "literal . identifier bracket-expr", + "expression": "`[0, 1, 2]`[1]", + "result": 1 + } + ] + }, + { + "comment": "Literals", + "given": {"type": "object"}, + "cases": [ + { + "comment": "Literal with leading whitespace", + "expression": "` {\"foo\": true}`", + "result": {"foo": true} + }, + { + "comment": "Literal with trailing whitespace", + "expression": "`{\"foo\": true} `", + "result": {"foo": true} + }, + { + "comment": "Literal on RHS of subexpr not allowed", + "expression": "foo.`\"bar\"`", + "error": "syntax" + } + ] + }, + { + "comment": "Raw String Literals", + "given": {}, + "cases": [ + { + "expression": "'foo'", + "result": "foo" + }, + { + "expression": "' foo '", + "result": " foo " + }, + { + "expression": "'0'", + "result": "0" + }, + { + "expression": "'newline\n'", + "result": "newline\n" + }, + { + "expression": "'\n'", + "result": "\n" + }, + { + "expression": "'✓'", + "result": "✓" + }, + { + "expression": "'𝄞'", + "result": "𝄞" + }, + { + "expression": "' [foo] '", + "result": " [foo] " + }, + { + "expression": "'[foo]'", + "result": "[foo]" + }, + { + "comment": "Do not interpret escaped unicode.", + "expression": "'\\u03a6'", + "result": "\\u03a6" + }, + { + "comment": "Can escape the single quote", + "expression": "'foo\\'bar'", + "result": "foo'bar" + }, + { + "comment": "Backslash not followed by single quote is treated as any other character", + "expression": "'\\z'", + "result": "\\z" + }, + { + "comment": "Backslash not followed by single quote is treated as any other character", + "expression": "'\\\\'", + "result": "\\\\" + } + ] + } +] diff --git a/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/multiselect.json b/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/multiselect.json new file mode 100644 index 00000000000..4f464822b46 --- /dev/null +++ b/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/multiselect.json @@ -0,0 +1,398 @@ +[{ + "given": { + "foo": { + "bar": "bar", + "baz": "baz", + "qux": "qux", + "nested": { + "one": { + "a": "first", + "b": "second", + "c": "third" + }, + "two": { + "a": "first", + "b": "second", + "c": "third" + }, + "three": { + "a": "first", + "b": "second", + "c": {"inner": "third"} + } + } + }, + "bar": 1, + "baz": 2, + "qux\"": 3 + }, + "cases": [ + { + "expression": "foo.{bar: bar}", + "result": {"bar": "bar"} + }, + { + "expression": "foo.{\"bar\": bar}", + "result": {"bar": "bar"} + }, + { + "expression": "foo.{\"foo.bar\": bar}", + "result": {"foo.bar": "bar"} + }, + { + "expression": "foo.{bar: bar, baz: baz}", + "result": {"bar": "bar", "baz": "baz"} + }, + { + "expression": "foo.{\"bar\": bar, \"baz\": baz}", + "result": {"bar": "bar", "baz": "baz"} + }, + { + "expression": "{\"baz\": baz, \"qux\\\"\": \"qux\\\"\"}", + "result": {"baz": 2, "qux\"": 3} + }, + { + "expression": "foo.{bar:bar,baz:baz}", + "result": {"bar": "bar", "baz": "baz"} + }, + { + "expression": "foo.{bar: bar,qux: qux}", + "result": {"bar": "bar", "qux": "qux"} + }, + { + "expression": "foo.{bar: bar, noexist: noexist}", + "result": {"bar": "bar", "noexist": null} + }, + { + "expression": "foo.{noexist: noexist, alsonoexist: alsonoexist}", + "result": {"noexist": null, "alsonoexist": null} + }, + { + "expression": "foo.badkey.{nokey: nokey, alsonokey: alsonokey}", + "result": null + }, + { + "expression": "foo.nested.*.{a: a,b: b}", + "result": [{"a": "first", "b": "second"}, + {"a": "first", "b": "second"}, + {"a": "first", "b": "second"}] + }, + { + "expression": "foo.nested.three.{a: a, cinner: c.inner}", + "result": {"a": "first", "cinner": "third"} + }, + { + "expression": "foo.nested.three.{a: a, c: c.inner.bad.key}", + "result": {"a": "first", "c": null} + }, + { + "expression": "foo.{a: nested.one.a, b: nested.two.b}", + "result": {"a": "first", "b": "second"} + }, + { + "expression": "{bar: bar, baz: baz}", + "result": {"bar": 1, "baz": 2} + }, + { + "expression": "{bar: bar}", + "result": {"bar": 1} + }, + { + "expression": "{otherkey: bar}", + "result": {"otherkey": 1} + }, + { + "expression": "{no: no, exist: exist}", + "result": {"no": null, "exist": null} + }, + { + "expression": "foo.[bar]", + "result": ["bar"] + }, + { + "expression": "foo.[bar,baz]", + "result": ["bar", "baz"] + }, + { + "expression": "foo.[bar,qux]", + "result": ["bar", "qux"] + }, + { + "expression": "foo.[bar,noexist]", + "result": ["bar", null] + }, + { + "expression": "foo.[noexist,alsonoexist]", + "result": [null, null] + } + ] +}, { + "given": { + "foo": {"bar": 1, "baz": [2, 3, 4]} + }, + "cases": [ + { + "expression": "foo.{bar:bar,baz:baz}", + "result": {"bar": 1, "baz": [2, 3, 4]} + }, + { + "expression": "foo.[bar,baz[0]]", + "result": [1, 2] + }, + { + "expression": "foo.[bar,baz[1]]", + "result": [1, 3] + }, + { + "expression": "foo.[bar,baz[2]]", + "result": [1, 4] + }, + { + "expression": "foo.[bar,baz[3]]", + "result": [1, null] + }, + { + "expression": "foo.[bar[0],baz[3]]", + "result": [null, null] + } + ] +}, { + "given": { + "foo": {"bar": 1, "baz": 2} + }, + "cases": [ + { + "expression": "foo.{bar: bar, baz: baz}", + "result": {"bar": 1, "baz": 2} + }, + { + "expression": "foo.[bar,baz]", + "result": [1, 2] + } + ] +}, { + "given": { + "foo": { + "bar": {"baz": [{"common": "first", "one": 1}, + {"common": "second", "two": 2}]}, + "ignoreme": 1, + "includeme": true + } + }, + "cases": [ + { + "expression": "foo.{bar: bar.baz[1],includeme: includeme}", + "result": {"bar": {"common": "second", "two": 2}, "includeme": true} + }, + { + "expression": "foo.{\"bar.baz.two\": bar.baz[1].two, includeme: includeme}", + "result": {"bar.baz.two": 2, "includeme": true} + }, + { + "expression": "foo.[includeme, bar.baz[*].common]", + "result": [true, ["first", "second"]] + }, + { + "expression": "foo.[includeme, bar.baz[*].none]", + "result": [true, []] + }, + { + "expression": "foo.[includeme, bar.baz[].common]", + "result": [true, ["first", "second"]] + } + ] +}, { + "given": { + "reservations": [{ + "instances": [ + {"id": "id1", + "name": "first"}, + {"id": "id2", + "name": "second"} + ]}, { + "instances": [ + {"id": "id3", + "name": "third"}, + {"id": "id4", + "name": "fourth"} + ]} + ]}, + "cases": [ + { + "expression": "reservations[*].instances[*].{id: id, name: name}", + "result": [[{"id": "id1", "name": "first"}, {"id": "id2", "name": "second"}], + [{"id": "id3", "name": "third"}, {"id": "id4", "name": "fourth"}]] + }, + { + "expression": "reservations[].instances[].{id: id, name: name}", + "result": [{"id": "id1", "name": "first"}, + {"id": "id2", "name": "second"}, + {"id": "id3", "name": "third"}, + {"id": "id4", "name": "fourth"}] + }, + { + "expression": "reservations[].instances[].[id, name]", + "result": [["id1", "first"], + ["id2", "second"], + ["id3", "third"], + ["id4", "fourth"]] + } + ] +}, +{ + "given": { + "foo": [{ + "bar": [ + { + "qux": 2, + "baz": 1 + }, + { + "qux": 4, + "baz": 3 + } + ] + }, + { + "bar": [ + { + "qux": 6, + "baz": 5 + }, + { + "qux": 8, + "baz": 7 + } + ] + } + ] + }, + "cases": [ + { + "expression": "foo", + "result": [{"bar": [{"qux": 2, "baz": 1}, {"qux": 4, "baz": 3}]}, + {"bar": [{"qux": 6, "baz": 5}, {"qux": 8, "baz": 7}]}] + }, + { + "expression": "foo[]", + "result": [{"bar": [{"qux": 2, "baz": 1}, {"qux": 4, "baz": 3}]}, + {"bar": [{"qux": 6, "baz": 5}, {"qux": 8, "baz": 7}]}] + }, + { + "expression": "foo[].bar", + "result": [[{"qux": 2, "baz": 1}, {"qux": 4, "baz": 3}], + [{"qux": 6, "baz": 5}, {"qux": 8, "baz": 7}]] + }, + { + "expression": "foo[].bar[]", + "result": [{"qux": 2, "baz": 1}, {"qux": 4, "baz": 3}, + {"qux": 6, "baz": 5}, {"qux": 8, "baz": 7}] + }, + { + "expression": "foo[].bar[].[baz, qux]", + "result": [[1, 2], [3, 4], [5, 6], [7, 8]] + }, + { + "expression": "foo[].bar[].[baz]", + "result": [[1], [3], [5], [7]] + }, + { + "expression": "foo[].bar[].[baz, qux][]", + "result": [1, 2, 3, 4, 5, 6, 7, 8] + } + ] +}, +{ + "given": { + "foo": { + "baz": [ + { + "bar": "abc" + }, { + "bar": "def" + } + ], + "qux": ["zero"] + } + }, + "cases": [ + { + "expression": "foo.[baz[*].bar, qux[0]]", + "result": [["abc", "def"], "zero"] + } + ] +}, +{ + "given": { + "foo": { + "baz": [ + { + "bar": "a", + "bam": "b", + "boo": "c" + }, { + "bar": "d", + "bam": "e", + "boo": "f" + } + ], + "qux": ["zero"] + } + }, + "cases": [ + { + "expression": "foo.[baz[*].[bar, boo], qux[0]]", + "result": [[["a", "c" ], ["d", "f" ]], "zero"] + } + ] +}, +{ + "given": { + "foo": { + "baz": [ + { + "bar": "a", + "bam": "b", + "boo": "c" + }, { + "bar": "d", + "bam": "e", + "boo": "f" + } + ], + "qux": ["zero"] + } + }, + "cases": [ + { + "expression": "foo.[baz[*].not_there || baz[*].bar, qux[0]]", + "result": [["a", "d"], "zero"] + } + ] +}, +{ + "given": {"type": "object"}, + "cases": [ + { + "comment": "Nested multiselect", + "expression": "[[*],*]", + "result": [null, ["object"]] + } + ] +}, +{ + "given": [], + "cases": [ + { + "comment": "Nested multiselect", + "expression": "[[*]]", + "result": [[]] + }, + { + "comment": "Select on null", + "expression": "missing.{foo: bar}", + "result": null + } + ] +} +] diff --git a/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/pipe.json b/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/pipe.json new file mode 100644 index 00000000000..b10c0a496d6 --- /dev/null +++ b/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/pipe.json @@ -0,0 +1,131 @@ +[{ + "given": { + "foo": { + "bar": { + "baz": "subkey" + }, + "other": { + "baz": "subkey" + }, + "other2": { + "baz": "subkey" + }, + "other3": { + "notbaz": ["a", "b", "c"] + }, + "other4": { + "notbaz": ["a", "b", "c"] + } + } + }, + "cases": [ + { + "expression": "foo.*.baz | [0]", + "result": "subkey" + }, + { + "expression": "foo.*.baz | [1]", + "result": "subkey" + }, + { + "expression": "foo.*.baz | [2]", + "result": "subkey" + }, + { + "expression": "foo.bar.* | [0]", + "result": "subkey" + }, + { + "expression": "foo.*.notbaz | [*]", + "result": [["a", "b", "c"], ["a", "b", "c"]] + }, + { + "expression": "{\"a\": foo.bar, \"b\": foo.other} | *.baz", + "result": ["subkey", "subkey"] + } + ] +}, { + "given": { + "foo": { + "bar": { + "baz": "one" + }, + "other": { + "baz": "two" + }, + "other2": { + "baz": "three" + }, + "other3": { + "notbaz": ["a", "b", "c"] + }, + "other4": { + "notbaz": ["d", "e", "f"] + } + } + }, + "cases": [ + { + "expression": "foo | bar", + "result": {"baz": "one"} + }, + { + "expression": "foo | bar | baz", + "result": "one" + }, + { + "expression": "foo|bar| baz", + "result": "one" + }, + { + "expression": "not_there | [0]", + "result": null + }, + { + "expression": "not_there | [0]", + "result": null + }, + { + "expression": "[foo.bar, foo.other] | [0]", + "result": {"baz": "one"} + }, + { + "expression": "{\"a\": foo.bar, \"b\": foo.other} | a", + "result": {"baz": "one"} + }, + { + "expression": "{\"a\": foo.bar, \"b\": foo.other} | b", + "result": {"baz": "two"} + }, + { + "expression": "foo.bam || foo.bar | baz", + "result": "one" + }, + { + "expression": "foo | not_there || bar", + "result": {"baz": "one"} + } + ] +}, { + "given": { + "foo": [{ + "bar": [{ + "baz": "one" + }, { + "baz": "two" + }] + }, { + "bar": [{ + "baz": "three" + }, { + "baz": "four" + }] + }] + }, + "cases": [ + { + "expression": "foo[*].bar[*] | [0][0]", + "result": {"baz": "one"} + } + ] +}] diff --git a/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/slice.json b/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/slice.json new file mode 100644 index 00000000000..359477278c8 --- /dev/null +++ b/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/slice.json @@ -0,0 +1,187 @@ +[{ + "given": { + "foo": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + "bar": { + "baz": 1 + } + }, + "cases": [ + { + "expression": "bar[0:10]", + "result": null + }, + { + "expression": "foo[0:10:1]", + "result": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + }, + { + "expression": "foo[0:10]", + "result": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + }, + { + "expression": "foo[0:10:]", + "result": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + }, + { + "expression": "foo[0::1]", + "result": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + }, + { + "expression": "foo[0::]", + "result": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + }, + { + "expression": "foo[0:]", + "result": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + }, + { + "expression": "foo[:10:1]", + "result": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + }, + { + "expression": "foo[::1]", + "result": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + }, + { + "expression": "foo[:10:]", + "result": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + }, + { + "expression": "foo[::]", + "result": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + }, + { + "expression": "foo[:]", + "result": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + }, + { + "expression": "foo[1:9]", + "result": [1, 2, 3, 4, 5, 6, 7, 8] + }, + { + "expression": "foo[0:10:2]", + "result": [0, 2, 4, 6, 8] + }, + { + "expression": "foo[5:]", + "result": [5, 6, 7, 8, 9] + }, + { + "expression": "foo[5::2]", + "result": [5, 7, 9] + }, + { + "expression": "foo[::2]", + "result": [0, 2, 4, 6, 8] + }, + { + "expression": "foo[::-1]", + "result": [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] + }, + { + "expression": "foo[1::2]", + "result": [1, 3, 5, 7, 9] + }, + { + "expression": "foo[10:0:-1]", + "result": [9, 8, 7, 6, 5, 4, 3, 2, 1] + }, + { + "expression": "foo[10:5:-1]", + "result": [9, 8, 7, 6] + }, + { + "expression": "foo[8:2:-2]", + "result": [8, 6, 4] + }, + { + "expression": "foo[0:20]", + "result": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + }, + { + "expression": "foo[10:-20:-1]", + "result": [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] + }, + { + "expression": "foo[10:-20]", + "result": [] + }, + { + "expression": "foo[-4:-1]", + "result": [6, 7, 8] + }, + { + "expression": "foo[:-5:-1]", + "result": [9, 8, 7, 6] + }, + { + "expression": "foo[8:2:0]", + "error": "invalid-value" + }, + { + "expression": "foo[8:2:0:1]", + "error": "syntax" + }, + { + "expression": "foo[8:2&]", + "error": "syntax" + }, + { + "expression": "foo[2:a:3]", + "error": "syntax" + } + ] +}, { + "given": { + "foo": [{"a": 1}, {"a": 2}, {"a": 3}], + "bar": [{"a": {"b": 1}}, {"a": {"b": 2}}, + {"a": {"b": 3}}], + "baz": 50 + }, + "cases": [ + { + "expression": "foo[:2].a", + "result": [1, 2] + }, + { + "expression": "foo[:2].b", + "result": [] + }, + { + "expression": "foo[:2].a.b", + "result": [] + }, + { + "expression": "bar[::-1].a.b", + "result": [3, 2, 1] + }, + { + "expression": "bar[:2].a.b", + "result": [1, 2] + }, + { + "expression": "baz[:2].a", + "result": null + } + ] +}, { + "given": [{"a": 1}, {"a": 2}, {"a": 3}], + "cases": [ + { + "expression": "[:]", + "result": [{"a": 1}, {"a": 2}, {"a": 3}] + }, + { + "expression": "[:2].a", + "result": [1, 2] + }, + { + "expression": "[::-1].a", + "result": [3, 2, 1] + }, + { + "expression": "[:2].b", + "result": [] + } + ] +}] diff --git a/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/syntax.json b/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/syntax.json new file mode 100644 index 00000000000..538337b660e --- /dev/null +++ b/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/syntax.json @@ -0,0 +1,692 @@ +[{ + "comment": "Dot syntax", + "given": {"type": "object"}, + "cases": [ + { + "expression": "foo.bar", + "result": null + }, + { + "expression": "foo", + "result": null + }, + { + "expression": "foo.1", + "error": "syntax" + }, + { + "expression": "foo.-11", + "error": "syntax" + }, + { + "expression": "foo.", + "error": "syntax" + }, + { + "expression": ".foo", + "error": "syntax" + }, + { + "expression": "foo..bar", + "error": "syntax" + }, + { + "expression": "foo.bar.", + "error": "syntax" + }, + { + "expression": "foo[.]", + "error": "syntax" + } + ] +}, + { + "comment": "Simple token errors", + "given": {"type": "object"}, + "cases": [ + { + "expression": ".", + "error": "syntax" + }, + { + "expression": ":", + "error": "syntax" + }, + { + "expression": ",", + "error": "syntax" + }, + { + "expression": "]", + "error": "syntax" + }, + { + "expression": "[", + "error": "syntax" + }, + { + "expression": "}", + "error": "syntax" + }, + { + "expression": "{", + "error": "syntax" + }, + { + "expression": ")", + "error": "syntax" + }, + { + "expression": "(", + "error": "syntax" + }, + { + "expression": "((&", + "error": "syntax" + }, + { + "expression": "a[", + "error": "syntax" + }, + { + "expression": "a]", + "error": "syntax" + }, + { + "expression": "a][", + "error": "syntax" + }, + { + "expression": "!", + "error": "syntax" + }, + { + "expression": "@=", + "error": "syntax" + }, + { + "expression": "@``", + "error": "syntax" + } + ] + }, + { + "comment": "Boolean syntax errors", + "given": {"type": "object"}, + "cases": [ + { + "expression": "![!(!", + "error": "syntax" + } + ] + }, + { + "comment": "Paren syntax errors", + "given": {}, + "cases": [ + { + "comment": "missing closing paren", + "expression": "(@", + "error": "syntax" + } + ] + }, + { + "comment": "Function syntax errors", + "given": {}, + "cases": [ + { + "comment": "invalid start of function", + "expression": "@(foo)", + "error": "syntax" + }, + { + "comment": "function names cannot be quoted", + "expression": "\"foo\"(bar)", + "error": "syntax" + } + ] + }, + { + "comment": "Wildcard syntax", + "given": {"type": "object"}, + "cases": [ + { + "expression": "*", + "result": ["object"] + }, + { + "expression": "*.*", + "result": [] + }, + { + "expression": "*.foo", + "result": [] + }, + { + "expression": "*[0]", + "result": [] + }, + { + "expression": ".*", + "error": "syntax" + }, + { + "expression": "*foo", + "error": "syntax" + }, + { + "expression": "*0", + "error": "syntax" + }, + { + "expression": "foo[*]bar", + "error": "syntax" + }, + { + "expression": "foo[*]*", + "error": "syntax" + } + ] + }, + { + "comment": "Flatten syntax", + "given": {"type": "object"}, + "cases": [ + { + "expression": "[]", + "result": null + } + ] + }, + { + "comment": "Simple bracket syntax", + "given": {"type": "object"}, + "cases": [ + { + "expression": "[0]", + "result": null + }, + { + "expression": "[*]", + "result": null + }, + { + "expression": "*.[0]", + "error": "syntax" + }, + { + "expression": "*.[\"0\"]", + "result": [[null]] + }, + { + "expression": "[*].bar", + "result": null + }, + { + "expression": "[*][0]", + "result": null + }, + { + "expression": "foo[#]", + "error": "syntax" + }, + { + "comment": "missing rbracket for led wildcard index", + "expression": "led[*", + "error": "syntax" + } + ] + }, + { + "comment": "slice syntax", + "given": {}, + "cases": [ + { + "comment": "slice expected colon or rbracket", + "expression": "[:@]", + "error": "syntax" + }, + { + "comment": "slice has too many colons", + "expression": "[:::]", + "error": "syntax" + }, + { + "comment": "slice expected number", + "expression": "[:@:]", + "error": "syntax" + }, + { + "comment": "slice expected number of colon", + "expression": "[:1@]", + "error": "syntax" + } + ] + }, + { + "comment": "Multi-select list syntax", + "given": {"type": "object"}, + "cases": [ + { + "expression": "foo[0]", + "result": null + }, + { + "comment": "Valid multi-select of a list", + "expression": "foo[0, 1]", + "error": "syntax" + }, + { + "expression": "foo.[0]", + "error": "syntax" + }, + { + "expression": "foo.[*]", + "result": null + }, + { + "comment": "Multi-select of a list with trailing comma", + "expression": "foo[0, ]", + "error": "syntax" + }, + { + "comment": "Multi-select of a list with trailing comma and no close", + "expression": "foo[0,", + "error": "syntax" + }, + { + "comment": "Multi-select of a list with trailing comma and no close", + "expression": "foo.[a", + "error": "syntax" + }, + { + "comment": "Multi-select of a list with extra comma", + "expression": "foo[0,, 1]", + "error": "syntax" + }, + { + "comment": "Multi-select of a list using an identifier index", + "expression": "foo[abc]", + "error": "syntax" + }, + { + "comment": "Multi-select of a list using identifier indices", + "expression": "foo[abc, def]", + "error": "syntax" + }, + { + "comment": "Multi-select of a list using an identifier index", + "expression": "foo[abc, 1]", + "error": "syntax" + }, + { + "comment": "Multi-select of a list using an identifier index with trailing comma", + "expression": "foo[abc, ]", + "error": "syntax" + }, + { + "comment": "Valid multi-select of a hash using an identifier index", + "expression": "foo.[abc]", + "result": null + }, + { + "comment": "Valid multi-select of a hash", + "expression": "foo.[abc, def]", + "result": null + }, + { + "comment": "Multi-select of a hash using a numeric index", + "expression": "foo.[abc, 1]", + "error": "syntax" + }, + { + "comment": "Multi-select of a hash with a trailing comma", + "expression": "foo.[abc, ]", + "error": "syntax" + }, + { + "comment": "Multi-select of a hash with extra commas", + "expression": "foo.[abc,, def]", + "error": "syntax" + }, + { + "comment": "Multi-select of a hash using number indices", + "expression": "foo.[0, 1]", + "error": "syntax" + } + ] + }, + { + "comment": "Multi-select hash syntax", + "given": {"type": "object"}, + "cases": [ + { + "comment": "No key or value", + "expression": "a{}", + "error": "syntax" + }, + { + "comment": "No closing token", + "expression": "a{", + "error": "syntax" + }, + { + "comment": "Not a key value pair", + "expression": "a{foo}", + "error": "syntax" + }, + { + "comment": "Missing value and closing character", + "expression": "a{foo:", + "error": "syntax" + }, + { + "comment": "Missing closing character", + "expression": "a{foo: 0", + "error": "syntax" + }, + { + "comment": "Missing value", + "expression": "a{foo:}", + "error": "syntax" + }, + { + "comment": "Trailing comma and no closing character", + "expression": "a{foo: 0, ", + "error": "syntax" + }, + { + "comment": "Missing value with trailing comma", + "expression": "a{foo: ,}", + "error": "syntax" + }, + { + "comment": "Accessing Array using an identifier", + "expression": "a{foo: bar}", + "error": "syntax" + }, + { + "expression": "a{foo: 0}", + "error": "syntax" + }, + { + "comment": "Missing key-value pair", + "expression": "a.{}", + "error": "syntax" + }, + { + "comment": "Not a key-value pair", + "expression": "a.{foo}", + "error": "syntax" + }, + { + "comment": "Missing value", + "expression": "a.{foo:}", + "error": "syntax" + }, + { + "comment": "Missing value with trailing comma", + "expression": "a.{foo: ,}", + "error": "syntax" + }, + { + "comment": "Valid multi-select hash extraction", + "expression": "a.{foo: bar}", + "result": null + }, + { + "comment": "Valid multi-select hash extraction", + "expression": "a.{foo: bar, baz: bam}", + "result": null + }, + { + "comment": "Trailing comma", + "expression": "a.{foo: bar, }", + "error": "syntax" + }, + { + "comment": "Missing key in second key-value pair", + "expression": "a.{foo: bar, baz}", + "error": "syntax" + }, + { + "comment": "Missing value in second key-value pair", + "expression": "a.{foo: bar, baz:}", + "error": "syntax" + }, + { + "comment": "Trailing comma", + "expression": "a.{foo: bar, baz: bam, }", + "error": "syntax" + }, + { + "comment": "Nested multi select", + "expression": "{\"\\\\\":{\" \":*}}", + "result": {"\\": {" ": ["object"]}} + }, + { + "comment": "Missing closing } after a valid nud", + "expression": "{a: @", + "error": "syntax" + } + ] + }, + { + "comment": "Or expressions", + "given": {"type": "object"}, + "cases": [ + { + "expression": "foo || bar", + "result": null + }, + { + "expression": "foo ||", + "error": "syntax" + }, + { + "expression": "foo.|| bar", + "error": "syntax" + }, + { + "expression": " || foo", + "error": "syntax" + }, + { + "expression": "foo || || foo", + "error": "syntax" + }, + { + "expression": "foo.[a || b]", + "result": null + }, + { + "expression": "foo.[a ||]", + "error": "syntax" + }, + { + "expression": "\"foo", + "error": "syntax" + } + ] + }, + { + "comment": "Filter expressions", + "given": {"type": "object"}, + "cases": [ + { + "expression": "foo[?bar==`\"baz\"`]", + "result": null + }, + { + "expression": "foo[? bar == `\"baz\"` ]", + "result": null + }, + { + "expression": "foo[ ?bar==`\"baz\"`]", + "error": "syntax" + }, + { + "expression": "foo[?bar==]", + "error": "syntax" + }, + { + "expression": "foo[?==]", + "error": "syntax" + }, + { + "expression": "foo[?==bar]", + "error": "syntax" + }, + { + "expression": "foo[?bar==baz?]", + "error": "syntax" + }, + { + "expression": "foo[?a.b.c==d.e.f]", + "result": null + }, + { + "expression": "foo[?bar==`[0, 1, 2]`]", + "result": null + }, + { + "expression": "foo[?bar==`[\"a\", \"b\", \"c\"]`]", + "result": null + }, + { + "comment": "Literal char not escaped", + "expression": "foo[?bar==`[\"foo`bar\"]`]", + "error": "syntax" + }, + { + "comment": "Literal char escaped", + "expression": "foo[?bar==`[\"foo\\`bar\"]`]", + "result": null + }, + { + "comment": "Unknown comparator", + "expression": "foo[?bar<>baz]", + "error": "syntax" + }, + { + "comment": "Unknown comparator", + "expression": "foo[?bar^baz]", + "error": "syntax" + }, + { + "expression": "foo[bar==baz]", + "error": "syntax" + }, + { + "comment": "Quoted identifier in filter expression no spaces", + "expression": "[?\"\\\\\">`\"foo\"`]", + "result": null + }, + { + "comment": "Quoted identifier in filter expression with spaces", + "expression": "[?\"\\\\\" > `\"foo\"`]", + "result": null + } + ] + }, + { + "comment": "Filter expression errors", + "given": {"type": "object"}, + "cases": [ + { + "expression": "bar.`\"anything\"`", + "error": "syntax" + }, + { + "expression": "bar.baz.noexists.`\"literal\"`", + "error": "syntax" + }, + { + "comment": "Literal wildcard projection", + "expression": "foo[*].`\"literal\"`", + "error": "syntax" + }, + { + "expression": "foo[*].name.`\"literal\"`", + "error": "syntax" + }, + { + "expression": "foo[].name.`\"literal\"`", + "error": "syntax" + }, + { + "expression": "foo[].name.`\"literal\"`.`\"subliteral\"`", + "error": "syntax" + }, + { + "comment": "Projecting a literal onto an empty list", + "expression": "foo[*].name.noexist.`\"literal\"`", + "error": "syntax" + }, + { + "expression": "foo[].name.noexist.`\"literal\"`", + "error": "syntax" + }, + { + "expression": "twolen[*].`\"foo\"`", + "error": "syntax" + }, + { + "comment": "Two level projection of a literal", + "expression": "twolen[*].threelen[*].`\"bar\"`", + "error": "syntax" + }, + { + "comment": "Two level flattened projection of a literal", + "expression": "twolen[].threelen[].`\"bar\"`", + "error": "syntax" + }, + { + "comment": "expects closing ]", + "expression": "foo[? @ | @", + "error": "syntax" + } + ] + }, + { + "comment": "Identifiers", + "given": {"type": "object"}, + "cases": [ + { + "expression": "foo", + "result": null + }, + { + "expression": "\"foo\"", + "result": null + }, + { + "expression": "\"\\\\\"", + "result": null + }, + { + "expression": "\"\\u\"", + "error": "syntax" + } + ] + }, + { + "comment": "Combined syntax", + "given": [], + "cases": [ + { + "expression": "*||*|*|*", + "result": null + }, + { + "expression": "*[]||[*]", + "result": [] + }, + { + "expression": "[*.*]", + "result": [null] + } + ] + } +] diff --git a/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/unicode.json b/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/unicode.json new file mode 100644 index 00000000000..6b07b0b6dae --- /dev/null +++ b/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/unicode.json @@ -0,0 +1,38 @@ +[ + { + "given": {"foo": [{"✓": "✓"}, {"✓": "✗"}]}, + "cases": [ + { + "expression": "foo[].\"✓\"", + "result": ["✓", "✗"] + } + ] + }, + { + "given": {"☯": true}, + "cases": [ + { + "expression": "\"☯\"", + "result": true + } + ] + }, + { + "given": {"♪♫•*¨*•.¸¸❤¸¸.•*¨*•♫♪": true}, + "cases": [ + { + "expression": "\"♪♫•*¨*•.¸¸❤¸¸.•*¨*•♫♪\"", + "result": true + } + ] + }, + { + "given": {"☃": true}, + "cases": [ + { + "expression": "\"☃\"", + "result": true + } + ] + } +] diff --git a/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/wildcard.json b/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/wildcard.json new file mode 100644 index 00000000000..3bcec302815 --- /dev/null +++ b/smithy-jmespath-tests/src/main/resources/software/amazon/smithy/jmespath/tests/compliance/wildcard.json @@ -0,0 +1,460 @@ +[{ + "given": { + "foo": { + "bar": { + "baz": "val" + }, + "other": { + "baz": "val" + }, + "other2": { + "baz": "val" + }, + "other3": { + "notbaz": ["a", "b", "c"] + }, + "other4": { + "notbaz": ["a", "b", "c"] + }, + "other5": { + "other": { + "a": 1, + "b": 1, + "c": 1 + } + } + } + }, + "cases": [ + { + "expression": "foo.*.baz", + "result": ["val", "val", "val"] + }, + { + "expression": "foo.bar.*", + "result": ["val"] + }, + { + "expression": "foo.*.notbaz", + "result": [["a", "b", "c"], ["a", "b", "c"]] + }, + { + "expression": "foo.*.notbaz[0]", + "result": ["a", "a"] + }, + { + "expression": "foo.*.notbaz[-1]", + "result": ["c", "c"] + } + ] +}, { + "given": { + "foo": { + "first-1": { + "second-1": "val" + }, + "first-2": { + "second-1": "val" + }, + "first-3": { + "second-1": "val" + } + } + }, + "cases": [ + { + "expression": "foo.*", + "result": [{"second-1": "val"}, {"second-1": "val"}, + {"second-1": "val"}] + }, + { + "expression": "foo.*.*", + "result": [["val"], ["val"], ["val"]] + }, + { + "expression": "foo.*.*.*", + "result": [[], [], []] + }, + { + "expression": "foo.*.*.*.*", + "result": [[], [], []] + } + ] +}, { + "given": { + "foo": { + "bar": "one" + }, + "other": { + "bar": "one" + }, + "nomatch": { + "notbar": "three" + } + }, + "cases": [ + { + "expression": "*.bar", + "result": ["one", "one"] + } + ] +}, { + "given": { + "top1": { + "sub1": {"foo": "one"} + }, + "top2": { + "sub1": {"foo": "one"} + } + }, + "cases": [ + { + "expression": "*", + "result": [{"sub1": {"foo": "one"}}, + {"sub1": {"foo": "one"}}] + }, + { + "expression": "*.sub1", + "result": [{"foo": "one"}, + {"foo": "one"}] + }, + { + "expression": "*.*", + "result": [[{"foo": "one"}], + [{"foo": "one"}]] + }, + { + "expression": "*.*.foo[]", + "result": ["one", "one"] + }, + { + "expression": "*.sub1.foo", + "result": ["one", "one"] + } + ] +}, +{ + "given": + {"foo": [{"bar": "one"}, {"bar": "two"}, {"bar": "three"}, {"notbar": "four"}]}, + "cases": [ + { + "expression": "foo[*].bar", + "result": ["one", "two", "three"] + }, + { + "expression": "foo[*].notbar", + "result": ["four"] + } + ] +}, +{ + "given": + [{"bar": "one"}, {"bar": "two"}, {"bar": "three"}, {"notbar": "four"}], + "cases": [ + { + "expression": "[*]", + "result": [{"bar": "one"}, {"bar": "two"}, {"bar": "three"}, {"notbar": "four"}] + }, + { + "expression": "[*].bar", + "result": ["one", "two", "three"] + }, + { + "expression": "[*].notbar", + "result": ["four"] + } + ] +}, +{ + "given": { + "foo": { + "bar": [ + {"baz": ["one", "two", "three"]}, + {"baz": ["four", "five", "six"]}, + {"baz": ["seven", "eight", "nine"]} + ] + } + }, + "cases": [ + { + "expression": "foo.bar[*].baz", + "result": [["one", "two", "three"], ["four", "five", "six"], ["seven", "eight", "nine"]] + }, + { + "expression": "foo.bar[*].baz[0]", + "result": ["one", "four", "seven"] + }, + { + "expression": "foo.bar[*].baz[1]", + "result": ["two", "five", "eight"] + }, + { + "expression": "foo.bar[*].baz[2]", + "result": ["three", "six", "nine"] + }, + { + "expression": "foo.bar[*].baz[3]", + "result": [] + } + ] +}, +{ + "given": { + "foo": { + "bar": [["one", "two"], ["three", "four"]] + } + }, + "cases": [ + { + "expression": "foo.bar[*]", + "result": [["one", "two"], ["three", "four"]] + }, + { + "expression": "foo.bar[0]", + "result": ["one", "two"] + }, + { + "expression": "foo.bar[0][0]", + "result": "one" + }, + { + "expression": "foo.bar[0][0][0]", + "result": null + }, + { + "expression": "foo.bar[0][0][0][0]", + "result": null + }, + { + "expression": "foo[0][0]", + "result": null + } + ] +}, +{ + "given": { + "foo": [ + {"bar": [{"kind": "basic"}, {"kind": "intermediate"}]}, + {"bar": [{"kind": "advanced"}, {"kind": "expert"}]}, + {"bar": "string"} + ] + + }, + "cases": [ + { + "expression": "foo[*].bar[*].kind", + "result": [["basic", "intermediate"], ["advanced", "expert"]] + }, + { + "expression": "foo[*].bar[0].kind", + "result": ["basic", "advanced"] + } + ] +}, +{ + "given": { + "foo": [ + {"bar": {"kind": "basic"}}, + {"bar": {"kind": "intermediate"}}, + {"bar": {"kind": "advanced"}}, + {"bar": {"kind": "expert"}}, + {"bar": "string"} + ] + }, + "cases": [ + { + "expression": "foo[*].bar.kind", + "result": ["basic", "intermediate", "advanced", "expert"] + } + ] +}, +{ + "given": { + "foo": [{"bar": ["one", "two"]}, {"bar": ["three", "four"]}, {"bar": ["five"]}] + }, + "cases": [ + { + "expression": "foo[*].bar[0]", + "result": ["one", "three", "five"] + }, + { + "expression": "foo[*].bar[1]", + "result": ["two", "four"] + }, + { + "expression": "foo[*].bar[2]", + "result": [] + } + ] +}, +{ + "given": { + "foo": [{"bar": []}, {"bar": []}, {"bar": []}] + }, + "cases": [ + { + "expression": "foo[*].bar[0]", + "result": [] + } + ] +}, +{ + "given": { + "foo": [["one", "two"], ["three", "four"], ["five"]] + }, + "cases": [ + { + "expression": "foo[*][0]", + "result": ["one", "three", "five"] + }, + { + "expression": "foo[*][1]", + "result": ["two", "four"] + } + ] +}, +{ + "given": { + "foo": [ + [ + ["one", "two"], ["three", "four"] + ], [ + ["five", "six"], ["seven", "eight"] + ], [ + ["nine"], ["ten"] + ] + ] + }, + "cases": [ + { + "expression": "foo[*][0]", + "result": [["one", "two"], ["five", "six"], ["nine"]] + }, + { + "expression": "foo[*][1]", + "result": [["three", "four"], ["seven", "eight"], ["ten"]] + }, + { + "expression": "foo[*][0][0]", + "result": ["one", "five", "nine"] + }, + { + "expression": "foo[*][1][0]", + "result": ["three", "seven", "ten"] + }, + { + "expression": "foo[*][0][1]", + "result": ["two", "six"] + }, + { + "expression": "foo[*][1][1]", + "result": ["four", "eight"] + }, + { + "expression": "foo[*][2]", + "result": [] + }, + { + "expression": "foo[*][2][2]", + "result": [] + }, + { + "expression": "bar[*]", + "result": null + }, + { + "expression": "bar[*].baz[*]", + "result": null + } + ] +}, +{ + "given": { + "string": "string", + "hash": {"foo": "bar", "bar": "baz"}, + "number": 23, + "nullvalue": null + }, + "cases": [ + { + "expression": "string[*]", + "result": null + }, + { + "expression": "hash[*]", + "result": null + }, + { + "expression": "number[*]", + "result": null + }, + { + "expression": "nullvalue[*]", + "result": null + }, + { + "expression": "string[*].foo", + "result": null + }, + { + "expression": "hash[*].foo", + "result": null + }, + { + "expression": "number[*].foo", + "result": null + }, + { + "expression": "nullvalue[*].foo", + "result": null + }, + { + "expression": "nullvalue[*].foo[*].bar", + "result": null + } + ] +}, +{ + "given": { + "string": "string", + "hash": {"foo": "val", "bar": "val"}, + "number": 23, + "array": [1, 2, 3], + "nullvalue": null + }, + "cases": [ + { + "expression": "string.*", + "result": null + }, + { + "expression": "hash.*", + "result": ["val", "val"] + }, + { + "expression": "number.*", + "result": null + }, + { + "expression": "array.*", + "result": null + }, + { + "expression": "nullvalue.*", + "result": null + } + ] +}, +{ + "given": { + "a": [0, 1, 2], + "b": [0, 1, 2] + }, + "cases": [ + { + "expression": "*[0]", + "result": [0, 0] + } + ] +} +] 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 new file mode 100644 index 00000000000..ddbbd0f5e56 --- /dev/null +++ b/smithy-jmespath-tests/src/test/java/software/amazon/smithy/jmespath/tests/LiteralExpressionJmespathRuntimeComplianceTests.java @@ -0,0 +1,22 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.jmespath.tests; + +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.smithy.jmespath.LiteralExpressionJmespathRuntime; + +public class LiteralExpressionJmespathRuntimeComplianceTests { + @ParameterizedTest(name = "{0}") + @MethodSource("source") + public void testRunner(String filename, Runnable callable) throws Exception { + callable.run(); + } + + public static Stream source() { + return ComplianceTestRunner.defaultParameterizedTestSource(LiteralExpressionJmespathRuntime.INSTANCE); + } +} diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/JmespathException.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/JmespathException.java index 2f709d81df2..1913d4af138 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/JmespathException.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/JmespathException.java @@ -8,11 +8,28 @@ * Thrown when any JMESPath error occurs. */ public class JmespathException extends RuntimeException { + + private final JmespathExceptionType errorType; + public JmespathException(String message) { + this(JmespathExceptionType.OTHER, message); + } + + public JmespathException(JmespathExceptionType errorType, String message) { super(message); + this.errorType = errorType; } public JmespathException(String message, Throwable previous) { + this(JmespathExceptionType.OTHER, message, previous); + } + + public JmespathException(JmespathExceptionType errorType, String message, Throwable previous) { super(message, previous); + this.errorType = errorType; + } + + public JmespathExceptionType getType() { + return errorType; } } diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/JmespathExceptionType.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/JmespathExceptionType.java new file mode 100644 index 00000000000..f56c2f4c274 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/JmespathExceptionType.java @@ -0,0 +1,25 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.jmespath; + +public enum JmespathExceptionType { + SYNTAX, + INVALID_TYPE, + INVALID_VALUE, + UNKNOWN_FUNCTION, + INVALID_ARITY, + OTHER; + + /** + * Returns the corresponding enum value for one of the identifiers used in + * the JMESPath specification. + *

+ * "syntax" is not listed in the specification, but it is used + * in the compliance tests to indicate invalid expressions. + */ + public static JmespathExceptionType fromID(String id) { + return valueOf(id.toUpperCase().replace('-', '_')); + } +} 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 e0f6408c543..c49fe976273 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,6 +7,8 @@ import java.util.Set; import java.util.TreeSet; import software.amazon.smithy.jmespath.ast.LiteralExpression; +import software.amazon.smithy.jmespath.evaluation.Evaluator; +import software.amazon.smithy.jmespath.evaluation.JmespathRuntime; /** * Represents a JMESPath AST node. @@ -29,7 +31,32 @@ protected JmespathExpression(int line, int column) { * @throws JmespathException if the expression is invalid. */ public static JmespathExpression parse(String text) { - return Parser.parse(text); + return Parser.parse(text, LiteralExpressionJmespathRuntime.INSTANCE); + } + + /** + * Parse a JMESPath expression. + * + * @param text Expression to parse. + * @param runtime The JmespathRuntime used to instantiate literal values. + * @return Returns the parsed expression. + * @throws JmespathException if the expression is invalid. + */ + public static JmespathExpression parse(String text, JmespathRuntime runtime) { + return Parser.parse(text, runtime); + } + + /** + * Parse a JSON value. + * + * @param text JSON value to parse. + * @param runtime The JmespathRuntime used to instantiate the parsed JSON value. + * @return Returns the parsed JSON value. + * @throws JmespathException if the text is invalid. + */ + public static T parseJson(String text, JmespathRuntime runtime) { + Lexer lexer = new Lexer(text, runtime); + return lexer.parseJsonValue(); } /** @@ -81,4 +108,25 @@ public LinterResult lint(LiteralExpression currentNode) { LiteralExpression result = this.accept(typeChecker); return new LinterResult(result.getType(), problems); } + + /** + * Evaluate the expression for the given current node. + * + * @param currentNode The value to set as the current node. + * @return Returns the result of evaluating the expression. + */ + public LiteralExpression evaluate(LiteralExpression currentNode) { + return evaluate(currentNode, new LiteralExpressionJmespathRuntime()); + } + + /** + * Evaluate the expression for the given current node. + * + * @param currentNode The value to set as the current node. + * @param runtime The JmespathRuntime used to manipulate node values. + * @return Returns the result of evaluating the expression. + */ + public T evaluate(T currentNode, JmespathRuntime runtime) { + return new Evaluator<>(currentNode, runtime).visit(this); + } } 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 0e6dcf9503f..25b9a678d86 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 @@ -5,17 +5,17 @@ package software.amazon.smithy.jmespath; import java.util.ArrayList; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.function.Predicate; import software.amazon.smithy.jmespath.ast.LiteralExpression; +import software.amazon.smithy.jmespath.evaluation.JmespathRuntime; -final class Lexer { +final class Lexer { private static final int MAX_NESTING_LEVEL = 50; + private final JmespathRuntime runtime; private final String expression; private final int length; private int position = 0; @@ -25,13 +25,18 @@ final class Lexer { private final List tokens = new ArrayList<>(); private boolean currentlyParsingLiteral; - private Lexer(String expression) { + Lexer(String expression, JmespathRuntime 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(); } static TokenIterator tokenize(String expression) { - return new Lexer(expression).doTokenize(); + return tokenize(expression, LiteralExpressionJmespathRuntime.INSTANCE); + } + + static TokenIterator tokenize(String expression, JmespathRuntime runtime) { + return new Lexer<>(expression, runtime).doTokenize(); } TokenIterator doTokenize() { @@ -184,7 +189,8 @@ private char expect(char... tokens) { } private JmespathException syntax(String message) { - return new JmespathException("Syntax error at line " + line + " column " + column + ": " + message); + return new JmespathException(JmespathExceptionType.SYNTAX, + "Syntax error at line " + line + " column " + column + ": " + message); } private void skip() { @@ -396,10 +402,9 @@ private Token parseRawStringLiteral() { skip(); builder.append('\''); } else { - if (peek() == '\\') { - skip(); - } builder.append('\\'); + builder.append(peek()); + skip(); } } else if (peek() == '\'') { skip(); @@ -463,7 +468,12 @@ private Token parseNumber() { String lexeme = sliceFrom(start); try { - double number = Double.parseDouble(lexeme); + Number number; + if (lexeme.contains(".") || lexeme.toLowerCase().contains("e")) { + number = Double.parseDouble(lexeme); + } else { + number = Long.parseLong(lexeme); + } LiteralExpression node = new LiteralExpression(number, currentLine, currentColumn); return new Token(TokenType.NUMBER, node, currentLine, currentColumn); } catch (NumberFormatException e) { @@ -506,30 +516,30 @@ private Token parseLiteral() { return new Token(TokenType.LITERAL, expression, currentLine, currentColumn); } - private Object parseJsonValue() { + T parseJsonValue() { ws(); switch (expect('\"', '{', '[', 't', 'f', 'n', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-')) { case 't': expect('r'); expect('u'); expect('e'); - return true; + return runtime.createBoolean(true); case 'f': expect('a'); expect('l'); expect('s'); expect('e'); - return false; + return runtime.createBoolean(false); case 'n': expect('u'); expect('l'); expect('l'); - return null; + return runtime.createNull(); case '"': // Backtrack for positioning. position--; column--; - return parseString().value.expectStringValue(); + return runtime.createString(parseString().value.expectStringValue()); case '{': return parseJsonObject(); case '[': @@ -538,44 +548,44 @@ private Object parseJsonValue() { // Backtrack. position--; column--; - return parseNumber().value.expectNumberValue(); + return runtime.createNumber(parseNumber().value.expectNumberValue()); } } - private Object parseJsonArray() { + private T parseJsonArray() { increaseNestingLevel(); - List values = new ArrayList<>(); + JmespathRuntime.ArrayBuilder builder = runtime.arrayBuilder(); ws(); if (peek() == ']') { skip(); decreaseNestingLevel(); - return values; + return builder.build(); } while (!eof() && peek() != '`') { - values.add(parseJsonValue()); + builder.add(parseJsonValue()); ws(); if (expect(',', ']') == ',') { ws(); } else { decreaseNestingLevel(); - return values; + return builder.build(); } } throw syntax("Unclosed JSON array"); } - private Object parseJsonObject() { + private T parseJsonObject() { increaseNestingLevel(); - Map values = new LinkedHashMap<>(); + JmespathRuntime.ObjectBuilder builder = runtime.objectBuilder(); ws(); if (peek() == '}') { skip(); decreaseNestingLevel(); - return values; + return builder.build(); } while (!eof() && peek() != '`') { @@ -583,13 +593,13 @@ private Object parseJsonObject() { ws(); expect(':'); ws(); - values.put(key, parseJsonValue()); + builder.put(runtime.createString(key), parseJsonValue()); ws(); if (expect(',', '}') == ',') { ws(); } else { decreaseNestingLevel(); - return values; + return builder.build(); } } 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 new file mode 100644 index 00000000000..8c568b14124 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/LiteralExpressionJmespathRuntime.java @@ -0,0 +1,157 @@ +/* + * 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.HashMap; +import java.util.List; +import java.util.Map; +import software.amazon.smithy.jmespath.ast.LiteralExpression; +import software.amazon.smithy.jmespath.evaluation.EvaluationUtils; +import software.amazon.smithy.jmespath.evaluation.JmespathRuntime; +import software.amazon.smithy.jmespath.evaluation.MappingIterable; +import software.amazon.smithy.jmespath.evaluation.NumberType; + +public final class LiteralExpressionJmespathRuntime implements JmespathRuntime { + + public static final LiteralExpressionJmespathRuntime INSTANCE = new LiteralExpressionJmespathRuntime(); + + @Override + public RuntimeType typeOf(LiteralExpression value) { + return value.getType(); + } + + @Override + public LiteralExpression createNull() { + return LiteralExpression.NULL; + } + + @Override + public LiteralExpression createBoolean(boolean b) { + return LiteralExpression.from(b); + } + + @Override + public boolean asBoolean(LiteralExpression value) { + return value.expectBooleanValue(); + } + + @Override + public LiteralExpression createString(String string) { + return LiteralExpression.from(string); + } + + @Override + public String asString(LiteralExpression value) { + return value.expectStringValue(); + } + + @Override + public LiteralExpression createNumber(Number value) { + return LiteralExpression.from(value); + } + + @Override + public NumberType numberType(LiteralExpression value) { + return EvaluationUtils.numberType(value.expectNumberValue()); + } + + @Override + public Number asNumber(LiteralExpression value) { + return value.expectNumberValue(); + } + + @Override + public int length(LiteralExpression value) { + switch (value.getType()) { + case STRING: + return EvaluationUtils.codePointCount(value.expectStringValue()); + case ARRAY: + return value.expectArrayValue().size(); + case OBJECT: + return value.expectObjectValue().size(); + default: + throw new IllegalStateException(); + } + } + + @Override + public LiteralExpression element(LiteralExpression array, int index) { + return LiteralExpression.from(array.expectArrayValue().get(index)); + } + + @Override + public Iterable asIterable(LiteralExpression array) { + switch (array.getType()) { + case ARRAY: + return new MappingIterable<>(LiteralExpression::from, array.expectArrayValue()); + case OBJECT: + return new MappingIterable<>(LiteralExpression::from, array.expectObjectValue().keySet()); + default: + throw new IllegalStateException("invalid-type"); + } + } + + @Override + public ArrayBuilder arrayBuilder() { + return new ArrayLiteralExpressionBuilder(); + } + + private static final class ArrayLiteralExpressionBuilder implements ArrayBuilder { + private final List result = new ArrayList<>(); + + @Override + public void add(LiteralExpression value) { + result.add(value.getValue()); + } + + @Override + public void addAll(LiteralExpression array) { + if (array.isArrayValue()) { + result.addAll(array.expectArrayValue()); + } else { + result.addAll(array.expectObjectValue().keySet()); + } + } + + @Override + public LiteralExpression build() { + return LiteralExpression.from(result); + } + } + + @Override + public LiteralExpression value(LiteralExpression value, LiteralExpression name) { + if (value.isObjectValue()) { + return value.getObjectField(name.expectStringValue()); + } else { + return createNull(); + } + } + + @Override + public ObjectBuilder objectBuilder() { + return new ObjectLiteralExpressionBuilder(); + } + + private static final class ObjectLiteralExpressionBuilder implements ObjectBuilder { + private final Map result = new HashMap<>(); + + @Override + public void put(LiteralExpression key, LiteralExpression value) { + result.put(key.expectStringValue(), value.getValue()); + } + + @Override + public void putAll(LiteralExpression object) { + result.putAll(object.expectObjectValue()); + } + + @Override + public LiteralExpression build() { + return LiteralExpression.from(result); + } + } +} 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 729ae6f6346..cbe701518df 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.JmespathRuntime; /** * A top-down operator precedence parser (aka Pratt parser) for JMESPath. @@ -74,13 +75,13 @@ final class Parser { private final String expression; private final TokenIterator iterator; - private Parser(String expression) { + private Parser(String expression, JmespathRuntime runtime) { this.expression = expression; - iterator = Lexer.tokenize(expression); + iterator = Lexer.tokenize(expression, runtime); } - static JmespathExpression parse(String expression) { - Parser parser = new Parser(expression); + static JmespathExpression parse(String expression, JmespathRuntime runtime) { + Parser parser = new Parser(expression, runtime); JmespathExpression result = parser.expression(0); parser.iterator.expect(TokenType.EOF); return result; diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/TokenIterator.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/TokenIterator.java index c8035bf0a3f..c08ccce604b 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/TokenIterator.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/TokenIterator.java @@ -89,7 +89,8 @@ Token expect(TokenType... types) { } JmespathException syntax(String message) { - return new JmespathException("Syntax error at line " + line() + " column " + column() + ": " + message); + return new JmespathException(JmespathExceptionType.SYNTAX, + "Syntax error at line " + line() + " column " + column() + ": " + message); } int line() { diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/TypeChecker.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/TypeChecker.java index 243de778ac3..ddd5cdaf9a1 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/TypeChecker.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/TypeChecker.java @@ -43,6 +43,13 @@ import software.amazon.smithy.jmespath.ast.SliceExpression; import software.amazon.smithy.jmespath.ast.Subexpression; +// Note this does not yet use the Evaluator with a LiteralExpressionJmespathRuntime, +// even though this visitor closely resembles the Evaluator. +// That's because there are several places where this visitor deviates +// from evaluation semantics in order to approximate answers, +// and I'm not convinced that level of extension support in the evaluator or the runtime interface +// is actually a good thing. +// I'd rather put effort into a more robust type system and checker. final class TypeChecker implements ExpressionVisitor { private static final Map FUNCTIONS = new HashMap<>(); diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/ast/LiteralExpression.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/ast/LiteralExpression.java index 3554e4af28e..be36f0a30b8 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/ast/LiteralExpression.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/ast/LiteralExpression.java @@ -12,6 +12,7 @@ import java.util.function.Function; 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; @@ -75,6 +76,14 @@ public static LiteralExpression from(Object value) { } } + public static Object unwrap(Object value) { + if (value instanceof LiteralExpression) { + return ((LiteralExpression) value).getValue(); + } else { + return value; + } + } + @Override public T accept(ExpressionVisitor visitor) { return visitor.visitLiteral(this); @@ -253,7 +262,8 @@ public String expectStringValue() { return (String) value; } - throw new JmespathException("Expected a string literal, but found " + value.getClass()); + throw new JmespathException(JmespathExceptionType.INVALID_TYPE, + "Expected a string literal, but found " + value); } /** @@ -267,7 +277,8 @@ public Number expectNumberValue() { return (Number) value; } - throw new JmespathException("Expected a number literal, but found " + value.getClass()); + throw new JmespathException(JmespathExceptionType.INVALID_TYPE, + "Expected a number literal, but found " + value); } /** @@ -281,7 +292,8 @@ public boolean expectBooleanValue() { return (Boolean) value; } - throw new JmespathException("Expected a boolean literal, but found " + value.getClass()); + throw new JmespathException(JmespathExceptionType.INVALID_TYPE, + "Expected a boolean literal, but found " + value); } /** @@ -295,7 +307,8 @@ public List expectArrayValue() { try { return (List) value; } catch (ClassCastException e) { - throw new JmespathException("Expected an array literal, but found " + value.getClass()); + throw new JmespathException(JmespathExceptionType.INVALID_TYPE, + "Expected an array literal, but found " + value); } } @@ -310,7 +323,8 @@ public Map expectObjectValue() { try { return (Map) value; } catch (ClassCastException e) { - throw new JmespathException("Expected a map literal, but found " + value.getClass()); + throw new JmespathException(JmespathExceptionType.INVALID_TYPE, + "Expected a map literal, but found " + value); } } 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 new file mode 100644 index 00000000000..7e14dc380ae --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/AbsFunction.java @@ -0,0 +1,42 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.jmespath.evaluation; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.List; + +class AbsFunction implements Function { + @Override + public String name() { + return "abs"; + } + + @Override + public T apply(JmespathRuntime runtime, List> functionArguments) { + checkArgumentCount(1, functionArguments); + T value = functionArguments.get(0).expectNumber(); + Number number = runtime.asNumber(value); + + switch (runtime.numberType(value)) { + case BYTE: + case SHORT: + case INTEGER: + return runtime.createNumber(Math.abs(number.intValue())); + case LONG: + return runtime.createNumber(Math.abs(number.longValue())); + case FLOAT: + return runtime.createNumber(Math.abs(number.floatValue())); + case DOUBLE: + return runtime.createNumber(Math.abs(number.doubleValue())); + case BIG_INTEGER: + return runtime.createNumber(((BigInteger) number).abs()); + case BIG_DECIMAL: + return runtime.createNumber(((BigDecimal) number).abs()); + default: + throw new IllegalArgumentException("`abs` only supports numeric arguments"); + } + } +} 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 new file mode 100644 index 00000000000..5f1090ccbe4 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/AvgFunction.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 AvgFunction implements Function { + @Override + public String name() { + return "avg"; + } + + @Override + public T apply(JmespathRuntime runtime, List> functionArguments) { + checkArgumentCount(1, functionArguments); + T array = functionArguments.get(0).expectArray(); + Number length = runtime.length(array); + if (length.intValue() == 0) { + return runtime.createNull(); + } + Number sum = 0D; + for (T element : runtime.asIterable(array)) { + sum = EvaluationUtils.addNumbers(sum, runtime.asNumber(element)); + } + return runtime.createNumber(EvaluationUtils.divideNumbers(sum, length)); + } +} 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 new file mode 100644 index 00000000000..c67a3cdf47c --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/CeilFunction.java @@ -0,0 +1,40 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.jmespath.evaluation; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.List; + +class CeilFunction implements Function { + @Override + public String name() { + return "ceil"; + } + + @Override + public T apply(JmespathRuntime runtime, List> functionArguments) { + checkArgumentCount(1, functionArguments); + T value = functionArguments.get(0).expectNumber(); + Number number = runtime.asNumber(value); + + switch (runtime.numberType(value)) { + case BYTE: + case SHORT: + case INTEGER: + case LONG: + case BIG_INTEGER: + return value; + case BIG_DECIMAL: + return runtime.createNumber(((BigDecimal) number).setScale(0, RoundingMode.CEILING)); + case DOUBLE: + return runtime.createNumber(Math.ceil(number.doubleValue())); + case FLOAT: + return 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/ContainsFunction.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/ContainsFunction.java new file mode 100644 index 00000000000..1e0a5191703 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/ContainsFunction.java @@ -0,0 +1,39 @@ +/* + * 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; +import software.amazon.smithy.jmespath.JmespathException; +import software.amazon.smithy.jmespath.JmespathExceptionType; + +class ContainsFunction implements Function { + @Override + public String name() { + return "contains"; + } + + @Override + public T apply(JmespathRuntime runtime, List> functionArguments) { + checkArgumentCount(2, functionArguments); + T subject = functionArguments.get(0).expectValue(); + T search = functionArguments.get(1).expectValue(); + switch (runtime.typeOf(subject)) { + case STRING: + String searchString = runtime.asString(search); + String subjectString = runtime.asString(subject); + return runtime.createBoolean(subjectString.contains(searchString)); + case ARRAY: + for (T item : runtime.asIterable(subject)) { + if (runtime.equal(item, search)) { + return runtime.createBoolean(true); + } + } + return runtime.createBoolean(false); + default: + throw new JmespathException(JmespathExceptionType.INVALID_TYPE, + "contains is not supported for " + runtime.typeOf(subject)); + } + } +} 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 new file mode 100644 index 00000000000..4ad75a8c126 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/EndsWithFunction.java @@ -0,0 +1,26 @@ +/* + * 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 EndsWithFunction implements Function { + @Override + public String name() { + return "ends_with"; + } + + @Override + public T apply(JmespathRuntime runtime, List> functionArguments) { + checkArgumentCount(2, functionArguments); + T subject = functionArguments.get(0).expectString(); + T suffix = functionArguments.get(1).expectString(); + + String subjectStr = runtime.asString(subject); + String suffixStr = runtime.asString(suffix); + + return runtime.createBoolean(subjectStr.endsWith(suffixStr)); + } +} 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 new file mode 100644 index 00000000000..24d58f7fda8 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/EvaluationUtils.java @@ -0,0 +1,141 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.jmespath.evaluation; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Iterator; +import java.util.Objects; +import software.amazon.smithy.jmespath.RuntimeType; + +public final class EvaluationUtils { + + private static final InheritingClassMap numberTypeForClass = InheritingClassMap.builder() + .put(Byte.class, NumberType.BYTE) + .put(Short.class, NumberType.SHORT) + .put(Integer.class, NumberType.INTEGER) + .put(Long.class, NumberType.LONG) + .put(Float.class, NumberType.FLOAT) + .put(Double.class, NumberType.DOUBLE) + .put(BigInteger.class, NumberType.BIG_INTEGER) + .put(BigDecimal.class, NumberType.BIG_DECIMAL) + .build(); + + public static NumberType numberType(Number number) { + return numberTypeForClass.get(number.getClass()); + } + + // Emulate JLS 5.1.2 type promotion. + static int compareNumbersWithPromotion(Number a, Number b) { + // Exact matches. + if (a.equals(b)) { + return 0; + } else if (isBig(a, b)) { + // When the values have a BigDecimal or BigInteger, normalize them both to BigDecimal. This is used even + // for BigInteger to avoid dropping decimals from doubles or floats (e.g., 10.01 != 10). + return toBigDecimal(a) + .stripTrailingZeros() + .compareTo(toBigDecimal(b).stripTrailingZeros()); + } else if (a instanceof Double || b instanceof Double || a instanceof Float || b instanceof Float) { + // Treat floats as double to allow for comparing larger values from rhs, like longs. + return Double.compare(a.doubleValue(), b.doubleValue()); + } else { + return Long.compare(a.longValue(), b.longValue()); + } + } + + private static boolean isBig(Number a, Number b) { + return a instanceof BigDecimal || b instanceof BigDecimal + || a instanceof BigInteger + || b instanceof BigInteger; + } + + private static BigDecimal toBigDecimal(Number number) { + if (number instanceof BigDecimal) { + return (BigDecimal) number; + } else if (number instanceof BigInteger) { + return new BigDecimal((BigInteger) number); + } else if (number instanceof Integer || number instanceof Long + || number instanceof Byte + || number instanceof Short) { + return BigDecimal.valueOf(number.longValue()); + } else { + return BigDecimal.valueOf(number.doubleValue()); + } + } + + static Number addNumbers(Number a, Number b) { + if (isBig(a, b)) { + return toBigDecimal(a).add(toBigDecimal(b)); + } else if (a instanceof Double || b instanceof Double || a instanceof Float || b instanceof Float) { + return a.doubleValue() + b.doubleValue(); + } else { + return Math.addExact(a.longValue(), b.longValue()); + } + } + + static Number divideNumbers(Number a, Number b) { + if (isBig(a, b)) { + return toBigDecimal(a).divide(toBigDecimal(b)); + } else { + return a.doubleValue() / b.doubleValue(); + } + } + + public static int codePointCount(String string) { + return string.codePointCount(0, string.length()); + } + + /** + * Default implementation of equality. + * Objects.equals() is not generally adequate because it will not + * consider equivalent Number values of different types equal. + */ + public static boolean equals(JmespathRuntime runtime, T a, T b) { + switch (runtime.typeOf(a)) { + case NULL: + case STRING: + case BOOLEAN: + return Objects.equals(a, b); + case NUMBER: + if (!runtime.is(b, RuntimeType.NUMBER)) { + return false; + } + return runtime.compare(a, b) == 0; + case ARRAY: + if (!runtime.is(b, RuntimeType.ARRAY)) { + return false; + } + Iterator aIter = runtime.asIterable(a).iterator(); + Iterator bIter = runtime.asIterable(b).iterator(); + while (aIter.hasNext()) { + if (!bIter.hasNext()) { + return false; + } + if (!runtime.equal(aIter.next(), bIter.next())) { + return false; + } + } + return !bIter.hasNext(); + case OBJECT: + if (!runtime.is(b, RuntimeType.OBJECT)) { + return false; + } + if (runtime.length(a) != runtime.length(b)) { + return false; + } + for (T key : runtime.asIterable(a)) { + T aValue = runtime.value(a, key); + T bValue = runtime.value(b, key); + if (!runtime.equal(aValue, bValue)) { + return false; + } + } + return true; + default: + throw new IllegalStateException(); + } + } +} 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 new file mode 100644 index 00000000000..3f3ed3621da --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/Evaluator.java @@ -0,0 +1,340 @@ +/* + * 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.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; +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 Evaluator implements ExpressionVisitor { + + 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 runtime) { + this.current = current; + this.runtime = 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.createBoolean(runtime.equal(left, right)); + case NOT_EQUAL: + return runtime.createBoolean(!runtime.equal(left, right)); + // NOTE: Ordering operators >, >=, <, <= are only valid for numbers. All invalid + // comparisons return null. + case LESS_THAN: + if (runtime.is(left, RuntimeType.NUMBER) && runtime.is(right, RuntimeType.NUMBER)) { + return runtime.createBoolean(runtime.compare(left, right) < 0); + } else { + return runtime.createNull(); + } + case LESS_THAN_EQUAL: + if (runtime.is(left, RuntimeType.NUMBER) && runtime.is(right, RuntimeType.NUMBER)) { + return runtime.createBoolean(runtime.compare(left, right) <= 0); + } else { + return runtime.createNull(); + } + case GREATER_THAN: + if (runtime.is(left, RuntimeType.NUMBER) && runtime.is(right, RuntimeType.NUMBER)) { + return runtime.createBoolean(runtime.compare(left, right) > 0); + } else { + return runtime.createNull(); + } + case GREATER_THAN_EQUAL: + if (runtime.is(left, RuntimeType.NUMBER) && runtime.is(right, RuntimeType.NUMBER)) { + return runtime.createBoolean(runtime.compare(left, right) >= 0); + } else { + return runtime.createNull(); + } + default: + throw new IllegalArgumentException("Unsupported comparator: " + comparatorExpression.getComparator()); + } + } + + @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()); + + // Only lists can be flattened. + if (!runtime.is(value, RuntimeType.ARRAY)) { + return runtime.createNull(); + } + JmespathRuntime.ArrayBuilder flattened = runtime.arrayBuilder(); + 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))); + } + } + return function.apply(runtime, arguments); + } + + @Override + public T visitField(FieldExpression fieldExpression) { + return runtime.value(current, runtime.createString(fieldExpression.getName())); + } + + @Override + public T visitIndex(IndexExpression indexExpression) { + int index = indexExpression.getIndex(); + if (!runtime.is(current, RuntimeType.ARRAY)) { + return runtime.createNull(); + } + int length = runtime.length(current); + // Negative indices indicate reverse indexing in JMESPath + if (index < 0) { + index = length + index; + } + if (length <= index || index < 0) { + return runtime.createNull(); + } + 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)) { + return current; + } + + JmespathRuntime.ArrayBuilder output = runtime.arrayBuilder(); + for (JmespathExpression exp : multiSelectListExpression.getExpressions()) { + output.add(visit(exp)); + } + return output.build(); + } + + @Override + public T visitMultiSelectHash(MultiSelectHashExpression multiSelectHashExpression) { + if (runtime.is(current, RuntimeType.NULL)) { + return current; + } + + JmespathRuntime.ObjectBuilder output = runtime.objectBuilder(); + for (Map.Entry expEntry : multiSelectHashExpression.getExpressions().entrySet()) { + output.put(runtime.createString(expEntry.getKey()), visit(expEntry.getValue())); + } + return output.build(); + } + + @Override + public T visitAnd(AndExpression andExpression) { + T left = visit(andExpression.getLeft()); + return runtime.isTruthy(left) ? visit(andExpression.getRight()) : left; + } + + @Override + public T visitOr(OrExpression orExpression) { + T left = visit(orExpression.getLeft()); + if (runtime.isTruthy(left)) { + return left; + } + return orExpression.getRight().accept(this); + } + + @Override + public T visitNot(NotExpression notExpression) { + T output = visit(notExpression.getExpression()); + return runtime.createBoolean(!runtime.isTruthy(output)); + } + + @Override + public T visitProjection(ProjectionExpression projectionExpression) { + T resultList = visit(projectionExpression.getLeft()); + if (!runtime.is(resultList, RuntimeType.ARRAY)) { + return runtime.createNull(); + } + JmespathRuntime.ArrayBuilder projectedResults = runtime.arrayBuilder(); + for (T result : runtime.asIterable(resultList)) { + T projected = new Evaluator(result, runtime).visit(projectionExpression.getRight()); + if (!runtime.typeOf(projected).equals(RuntimeType.NULL)) { + projectedResults.add(projected); + } + } + return projectedResults.build(); + } + + @Override + public T visitFilterProjection(FilterProjectionExpression filterProjectionExpression) { + T left = visit(filterProjectionExpression.getLeft()); + if (!runtime.is(left, RuntimeType.ARRAY)) { + return runtime.createNull(); + } + JmespathRuntime.ArrayBuilder results = runtime.arrayBuilder(); + for (T val : runtime.asIterable(left)) { + T output = new Evaluator<>(val, runtime).visit(filterProjectionExpression.getComparison()); + if (runtime.isTruthy(output)) { + T result = new Evaluator<>(val, runtime).visit(filterProjectionExpression.getRight()); + if (!runtime.is(result, RuntimeType.NULL)) { + results.add(result); + } + } + } + return results.build(); + } + + @Override + public T visitObjectProjection(ObjectProjectionExpression objectProjectionExpression) { + T resultObject = visit(objectProjectionExpression.getLeft()); + if (!runtime.is(resultObject, RuntimeType.OBJECT)) { + return runtime.createNull(); + } + JmespathRuntime.ArrayBuilder projectedResults = runtime.arrayBuilder(); + 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()); + if (!runtime.is(projectedResult, RuntimeType.NULL)) { + projectedResults.add(projectedResult); + } + } + } + return projectedResults.build(); + } + + @Override + public T visitSlice(SliceExpression sliceExpression) { + if (!runtime.is(current, RuntimeType.ARRAY)) { + return runtime.createNull(); + } + + int length = runtime.length(current); + + int step = sliceExpression.getStep(); + if (step == 0) { + throw new JmespathException(JmespathExceptionType.INVALID_VALUE, "invalid-value"); + } + + int start; + if (!sliceExpression.getStart().isPresent()) { + start = step > 0 ? 0 : length - 1; + } else { + start = sliceExpression.getStart().getAsInt(); + if (start < 0) { + start = length + start; + } + if (start < 0) { + start = 0; + } else if (start > length - 1) { + start = length - 1; + } + } + + int stop; + if (!sliceExpression.getStop().isPresent()) { + stop = step > 0 ? length : -1; + } else { + stop = sliceExpression.getStop().getAsInt(); + if (stop < 0) { + stop = length + stop; + } + + if (stop < 0) { + stop = -1; + } else if (stop > length) { + stop = length; + } + } + + return runtime.slice(current, start, stop, step); + } + + @Override + public T visitSubexpression(Subexpression subexpression) { + T left = visit(subexpression.getLeft()); + return new Evaluator<>(left, runtime).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 new file mode 100644 index 00000000000..433c3c2fb51 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/FloorFunction.java @@ -0,0 +1,40 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.jmespath.evaluation; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.List; + +class FloorFunction implements Function { + @Override + public String name() { + return "floor"; + } + + @Override + public T apply(JmespathRuntime runtime, List> functionArguments) { + checkArgumentCount(1, functionArguments); + T value = functionArguments.get(0).expectNumber(); + Number number = runtime.asNumber(value); + + switch (runtime.numberType(value)) { + case BYTE: + case SHORT: + case INTEGER: + case LONG: + case BIG_INTEGER: + return value; + case BIG_DECIMAL: + return runtime.createNumber(((BigDecimal) number).setScale(0, RoundingMode.FLOOR)); + case DOUBLE: + return runtime.createNumber(Math.floor(number.doubleValue())); + case FLOAT: + return 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/Function.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/Function.java new file mode 100644 index 00000000000..bfea8376d6c --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/Function.java @@ -0,0 +1,25 @@ +/* + * 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; +import software.amazon.smithy.jmespath.JmespathException; +import software.amazon.smithy.jmespath.JmespathExceptionType; + +interface Function { + + String name(); + + T apply(JmespathRuntime runtime, List> arguments); + + // Helpers + + 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())); + } + } +} 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 new file mode 100644 index 00000000000..d8d1ce18e68 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/FunctionArgument.java @@ -0,0 +1,121 @@ +/* + * 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.Set; +import software.amazon.smithy.jmespath.JmespathException; +import software.amazon.smithy.jmespath.JmespathExceptionType; +import software.amazon.smithy.jmespath.JmespathExpression; +import software.amazon.smithy.jmespath.RuntimeType; + +abstract class FunctionArgument { + + protected final JmespathRuntime runtime; + + protected FunctionArgument(JmespathRuntime runtime) { + this.runtime = runtime; + } + + public T expectValue() { + throw new JmespathException(JmespathExceptionType.INVALID_TYPE, "invalid-type"); + } + + public T expectString() { + throw new JmespathException(JmespathExceptionType.INVALID_TYPE, "invalid-type"); + } + + public T expectNumber() { + throw new JmespathException(JmespathExceptionType.INVALID_TYPE, "invalid-type"); + } + + public T expectArray() { + throw new JmespathException(JmespathExceptionType.INVALID_TYPE, "invalid-type"); + } + + public T expectObject() { + throw new JmespathException(JmespathExceptionType.INVALID_TYPE, "invalid-type"); + } + + public T expectAnyOf(Set types) { + 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); + } + + public static FunctionArgument of(JmespathRuntime runtime, T value) { + return new Value(runtime, value); + } + + static class Value extends FunctionArgument { + T value; + + public Value(JmespathRuntime runtime, T value) { + super(runtime); + this.value = value; + } + + @Override + public T expectValue() { + return value; + } + + protected T expectType(RuntimeType runtimeType) { + if (runtime.is(value, runtimeType)) { + return value; + } else { + throw new JmespathException(JmespathExceptionType.INVALID_TYPE, "invalid-type"); + } + } + + public T expectAnyOf(Set types) { + 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); + } + + @Override + public T expectNumber() { + return expectType(RuntimeType.NUMBER); + } + + @Override + public T expectArray() { + return expectType(RuntimeType.ARRAY); + } + + @Override + public T expectObject() { + return expectType(RuntimeType.OBJECT); + } + } + + static class Expression extends FunctionArgument { + JmespathExpression expression; + + public Expression(JmespathRuntime runtime, JmespathExpression expression) { + super(runtime); + this.expression = expression; + } + + @Override + public JmespathExpression expectExpression() { + return expression; + } + } + +} 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 new file mode 100644 index 00000000000..6d24d143e12 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/FunctionRegistry.java @@ -0,0 +1,52 @@ +/* + * 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.HashMap; +import java.util.Map; + +final class FunctionRegistry { + + private static final Map BUILTINS = new HashMap<>(); + + private static void registerFunction(Function function) { + if (BUILTINS.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()); + } + + static Function lookup(String name) { + return BUILTINS.get(name); + } +} diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/InheritingClassMap.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/InheritingClassMap.java new file mode 100644 index 00000000000..6f593115390 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/InheritingClassMap.java @@ -0,0 +1,75 @@ +/* + * 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.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * A map using Class values as keys that accounts for subtyping. + *

+ * Useful for external implementations of polymorphism, + * such as attaching behavior to an existing type hierarchy you cannot modify. + * Can be more efficient than a chain of if statements using instanceof. + */ +class InheritingClassMap { + + public static Builder builder() { + return new Builder<>(); + } + + private final Map, T> map; + + private InheritingClassMap(Map, T> map) { + this.map = map; + } + + public T get(Class clazz) { + // Fast path + T result = map.get(clazz); + if (result != null) { + return result; + } + + // Slow path (cache miss) + // Recursively check supertypes, throwing if there is any conflict + Class superclass = clazz.getSuperclass(); + Class matchingClass = superclass; + if (superclass != null) { + result = get(superclass); + } + for (Class interfaceClass : clazz.getInterfaces()) { + T interfaceResult = get(interfaceClass); + if (interfaceResult != null) { + if (result != null && !result.equals(interfaceResult)) { + throw new RuntimeException("Duplicate match for " + clazz + ": " + + matchingClass + " and " + interfaceClass); + } + matchingClass = interfaceClass; + result = interfaceResult; + } + } + + // Cache the value directly even if it's a null. + map.put(clazz, result); + + return result; + } + + public static class Builder { + + private final Map, T> map = new HashMap<>(); + + public Builder put(Class clazz, T value) { + map.put(clazz, Objects.requireNonNull(value)); + return this; + } + + public InheritingClassMap build() { + return new InheritingClassMap<>(map); + } + } +} 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 new file mode 100644 index 00000000000..6cdbf1cb991 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/JmespathRuntime.java @@ -0,0 +1,306 @@ +/* + * 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.Collection; +import java.util.Comparator; +import software.amazon.smithy.jmespath.JmespathException; +import software.amazon.smithy.jmespath.JmespathExceptionType; +import software.amazon.smithy.jmespath.RuntimeType; + +/** + * An interface to provide the operations needed for JMESPath expression evaluation + * based on any runtime representation of JSON values. + *

+ * Several methods have default implementations that are at least correct, + * but implementors can override them with more efficient implementations. + *

+ * In the documentation of the required behavior of each method, + * note that conditions like "if the value is NULL", + * 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 { + + /////////////////////////////// + // General Operations + /////////////////////////////// + + /** + * Returns the basic type of the given value: NULL, BOOLEAN, STRING, NUMBER, OBJECT, or ARRAY. + *

+ * MUST NOT ever return EXPRESSION or ANY. + */ + RuntimeType typeOf(T value); + + /** + * Shorthand for {@code typeOf(value).equals(type)}. + */ + default boolean is(T value, RuntimeType type) { + return typeOf(value).equals(type); + } + + /** + * Returns true iff the given value is truthy according + * to the JMESPath specification. + */ + default boolean isTruthy(T value) { + switch (typeOf(value)) { + case NULL: + return false; + case BOOLEAN: + return asBoolean(value); + case STRING: + return !asString(value).isEmpty(); + case NUMBER: + return true; + case ARRAY: + case OBJECT: + Iterable iterable = asIterable(value); + if (iterable instanceof Collection) { + return !((Collection) iterable).isEmpty(); + } else { + return iterable.iterator().hasNext(); + } + default: + throw new IllegalStateException(); + } + } + + /** + * Returns true iff the two given values are equal. + *

+ * Note that just calling Objects.equals() is generally not correct + * because it does not consider different Number representations of the same value + * the same. + */ + default boolean equal(T a, T b) { + return EvaluationUtils.equals(this, a, b); + } + + @Override + default int compare(T a, T b) { + if (is(a, RuntimeType.STRING) && is(b, RuntimeType.STRING)) { + return asString(a).compareTo(asString(b)); + } else if (is(a, RuntimeType.NUMBER) && is(b, RuntimeType.NUMBER)) { + return EvaluationUtils.compareNumbersWithPromotion(asNumber(a), asNumber(b)); + } else { + throw new JmespathException(JmespathExceptionType.INVALID_TYPE, "invalid-type"); + } + } + + /** + * Returns a JSON string representation of the given value. + *

+ * Note the distinction between this method and asString(), + * which can be called only on STRINGs and just casts the value to a String. + */ + default String toString(T value) { + // Quick and dirty implementation just for test names for now + switch (typeOf(value)) { + case NULL: + return "null"; + case BOOLEAN: + return asBoolean(value) ? "true" : "false"; + case STRING: + return '"' + asString(value) + '"'; + case NUMBER: + return asNumber(value).toString(); + case ARRAY: + StringBuilder arrayStringBuilder = new StringBuilder(); + arrayStringBuilder.append('['); + boolean first = true; + for (T element : asIterable(value)) { + if (first) { + first = false; + } else { + arrayStringBuilder.append(','); + } + arrayStringBuilder.append(toString(element)); + } + arrayStringBuilder.append(']'); + return arrayStringBuilder.toString(); + case OBJECT: + StringBuilder objectStringBuilder = new StringBuilder(); + objectStringBuilder.append('{'); + boolean firstKey = true; + for (T key : asIterable(value)) { + if (firstKey) { + firstKey = false; + } else { + objectStringBuilder.append(", "); + } + objectStringBuilder.append(toString(key)); + objectStringBuilder.append(": "); + objectStringBuilder.append(toString(value(value, key))); + } + objectStringBuilder.append('}'); + return objectStringBuilder.toString(); + default: + throw new IllegalStateException(); + } + } + + /////////////////////////////// + // NULLs + /////////////////////////////// + + /** + * Returns `null`. + *

+ * Runtimes may or may not use a Java null value to represent a JSON null value. + */ + T createNull(); + + /////////////////////////////// + // BOOLEANs + /////////////////////////////// + + T createBoolean(boolean b); + + /** + * If the given value is a BOOLEAN, return it as a boolean. + * Otherwise, throws a JmespathException of type INVALID_TYPE. + */ + boolean asBoolean(T value); + + /////////////////////////////// + // STRINGs + /////////////////////////////// + + T createString(String string); + + /** + * If the given value is a STRING, return it as a String. + * Otherwise, throws a JmespathException of type INVALID_TYPE. + *

+ * Note the distinction between this method and toString(), + * which can be called on any value and produces a JSON string. + */ + String asString(T value); + + /////////////////////////////// + // NUMBERs + /////////////////////////////// + + 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(). + */ + NumberType numberType(T value); + + /** + * If the given value is a NUMBER, return it as a Number. + * Otherwise, throws a JmespathException of type INVALID_TYPE. + */ + Number asNumber(T value); + + /////////////////////////////// + // ARRAYs + /////////////////////////////// + + ArrayBuilder arrayBuilder(); + + 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); + + 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); + + /** + * If the given value is an ARRAY, returns the specified slice. + * Otherwise, throws a JmespathException of type INVALID_TYPE. + *

+ * Start and stop will always be non-negative, and step will always be non-zero. + */ + default T slice(T array, int start, int stop, int step) { + if (is(array, RuntimeType.NULL)) { + return createNull(); + } + + JmespathRuntime.ArrayBuilder output = arrayBuilder(); + + if (start < stop) { + // If step is negative, the result is an empty array. + if (step > 0) { + for (int idx = start; idx < stop; idx += step) { + output.add(element(array, idx)); + } + } + } else { + // If step is positive, the result is an empty array. + if (step < 0) { + // List is iterating in reverse + for (int idx = start; idx > stop; idx += step) { + output.add(element(array, idx)); + } + } + } + return output.build(); + } + + /////////////////////////////// + // OBJECTs + /////////////////////////////// + + ObjectBuilder objectBuilder(); + + 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); + + 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 + /////////////////////////////// + + /** + * Returns the number of elements in an ARRAY or the number of keys in an OBJECT. + * Otherwise, throws a JmespathException of type INVALID_TYPE. + */ + int length(T value); + + /** + * 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); +} 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 new file mode 100644 index 00000000000..6171a0fcd60 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/JoinFunction.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 java.util.List; + +class JoinFunction implements Function { + @Override + public String name() { + return "join"; + } + + @Override + public T apply(JmespathRuntime runtime, List> functionArguments) { + checkArgumentCount(2, functionArguments); + String separator = runtime.asString(functionArguments.get(0).expectString()); + T array = functionArguments.get(1).expectArray(); + + StringBuilder result = new StringBuilder(); + boolean first = true; + for (T element : runtime.asIterable(array)) { + if (!first) { + result.append(separator); + } + result.append(runtime.asString(element)); + first = false; + } + + return runtime.createString(result.toString()); + } +} 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 new file mode 100644 index 00000000000..941ba87db1b --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/KeysFunction.java @@ -0,0 +1,24 @@ +/* + * 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 KeysFunction implements Function { + @Override + public String name() { + return "keys"; + } + + @Override + public T apply(JmespathRuntime runtime, List> functionArguments) { + checkArgumentCount(1, functionArguments); + T value = functionArguments.get(0).expectObject(); + + JmespathRuntime.ArrayBuilder arrayBuilder = runtime.arrayBuilder(); + arrayBuilder.addAll(value); + return arrayBuilder.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 new file mode 100644 index 00000000000..9fffa767267 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/LengthFunction.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 java.util.HashSet; +import java.util.List; +import java.util.Set; +import software.amazon.smithy.jmespath.RuntimeType; + +class LengthFunction implements Function { + + private static final Set PARAMETER_TYPES = new HashSet<>(); + static { + PARAMETER_TYPES.add(RuntimeType.STRING); + PARAMETER_TYPES.add(RuntimeType.ARRAY); + PARAMETER_TYPES.add(RuntimeType.OBJECT); + } + + @Override + public String name() { + return "length"; + } + + @Override + public T apply(JmespathRuntime runtime, List> functionArguments) { + checkArgumentCount(1, functionArguments); + T value = functionArguments.get(0).expectAnyOf(PARAMETER_TYPES); + + return runtime.createNumber(runtime.length(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 new file mode 100644 index 00000000000..ee54e506a9e --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/ListArrayBuilder.java @@ -0,0 +1,48 @@ +/* + * 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.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.Function; + +/** + * A default implementation of {@link JmespathRuntime.ArrayBuilder}. + * using a {@link List} as the backing store. + */ +public class ListArrayBuilder implements JmespathRuntime.ArrayBuilder { + + private final JmespathRuntime runtime; + private final List result = new ArrayList<>(); + private final Function, T> wrapping; + + public ListArrayBuilder(JmespathRuntime runtime, Function, T> wrapping) { + this.runtime = runtime; + this.wrapping = wrapping; + } + + @Override + public void add(T value) { + result.add(value); + } + + @Override + public void addAll(T array) { + Iterable iterable = runtime.asIterable(array); + if (iterable instanceof Collection) { + result.addAll((Collection) iterable); + } else { + for (T value : iterable) { + result.add(value); + } + } + } + + @Override + public T build() { + return wrapping.apply(result); + } +} 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 new file mode 100644 index 00000000000..0683477208d --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/MapFunction.java @@ -0,0 +1,28 @@ +/* + * 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; +import software.amazon.smithy.jmespath.JmespathExpression; + +class MapFunction implements Function { + @Override + public String name() { + return "map"; + } + + @Override + public T apply(JmespathRuntime runtime, List> functionArguments) { + checkArgumentCount(2, functionArguments); + JmespathExpression expression = functionArguments.get(0).expectExpression(); + T array = functionArguments.get(1).expectArray(); + + JmespathRuntime.ArrayBuilder builder = runtime.arrayBuilder(); + for (T element : runtime.asIterable(array)) { + builder.add(expression.evaluate(element, runtime)); + } + return builder.build(); + } +} 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 new file mode 100644 index 00000000000..a8c481a087f --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/MapObjectBuilder.java @@ -0,0 +1,44 @@ +/* + * 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.HashMap; +import java.util.Map; +import java.util.function.Function; + +/** + * A default implementation of {@link JmespathRuntime.ObjectBuilder}. + * using a {@link Map} as the backing store. + */ +public class MapObjectBuilder implements JmespathRuntime.ObjectBuilder { + + private final JmespathRuntime runtime; + private final Map result = new HashMap<>(); + private final Function, T> wrapping; + + public MapObjectBuilder(JmespathRuntime runtime, Function, T> wrapping) { + this.runtime = runtime; + this.wrapping = wrapping; + } + + @Override + public void put(T key, T value) { + result.put(runtime.asString(key), value); + } + + @Override + public void 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)); + } + } + + @Override + public T build() { + return wrapping.apply(result); + } +} diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/MappingIterable.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/MappingIterable.java new file mode 100644 index 00000000000..44d3a48e246 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/MappingIterable.java @@ -0,0 +1,43 @@ +/* + * 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.Iterator; +import java.util.function.Function; + +public final class MappingIterable implements Iterable { + + private final Iterable inner; + private final Function mapping; + + public MappingIterable(Function mapping, Iterable inner) { + this.inner = inner; + this.mapping = mapping; + } + + @Override + public Iterator iterator() { + return new WrappingIterator(inner.iterator()); + } + + private class WrappingIterator implements Iterator { + + private final Iterator inner; + + private WrappingIterator(Iterator inner) { + this.inner = inner; + } + + @Override + public boolean hasNext() { + return inner.hasNext(); + } + + @Override + public R next() { + return mapping.apply(inner.next()); + } + } +} 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 new file mode 100644 index 00000000000..464fe7fb490 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/MaxByFunction.java @@ -0,0 +1,42 @@ +/* + * 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; +import software.amazon.smithy.jmespath.JmespathExpression; + +class MaxByFunction implements Function { + @Override + public String name() { + return "max_by"; + } + + @Override + public T apply(JmespathRuntime runtime, 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(); + } + + T max = null; + T maxBy = null; + boolean first = true; + for (T element : runtime.asIterable(array)) { + T by = expression.evaluate(element, runtime); + if (first) { + first = false; + max = element; + maxBy = by; + } else if (runtime.compare(by, maxBy) > 0) { + max = element; + maxBy = by; + } + } + // max should never be null at this point + return max; + } +} 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 new file mode 100644 index 00000000000..76661558e99 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/MaxFunction.java @@ -0,0 +1,36 @@ +/* + * 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 MaxFunction implements Function { + @Override + public String name() { + return "max"; + } + + @Override + public T apply(JmespathRuntime runtime, List> functionArguments) { + checkArgumentCount(1, functionArguments); + T array = functionArguments.get(0).expectArray(); + if (runtime.length(array) == 0) { + return runtime.createNull(); + } + + T max = null; + boolean first = true; + for (T element : runtime.asIterable(array)) { + if (first) { + first = false; + max = element; + } else if (runtime.compare(element, max) > 0) { + max = element; + } + } + // max should never be null at this point + return max; + } +} 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 new file mode 100644 index 00000000000..d50356eb504 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/MergeFunction.java @@ -0,0 +1,26 @@ +/* + * 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 MergeFunction implements Function { + @Override + public String name() { + return "merge"; + } + + @Override + public T apply(JmespathRuntime runtime, List> functionArguments) { + JmespathRuntime.ObjectBuilder builder = runtime.objectBuilder(); + + for (FunctionArgument arg : functionArguments) { + T object = arg.expectObject(); + builder.putAll(object); + } + + return builder.build(); + } +} 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 new file mode 100644 index 00000000000..0d1e90fff0c --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/MinByFunction.java @@ -0,0 +1,42 @@ +/* + * 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; +import software.amazon.smithy.jmespath.JmespathExpression; + +class MinByFunction implements Function { + @Override + public String name() { + return "min_by"; + } + + @Override + public T apply(JmespathRuntime runtime, 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(); + } + + T min = null; + T minBy = null; + boolean first = true; + for (T element : runtime.asIterable(array)) { + T by = expression.evaluate(element, runtime); + if (first) { + first = false; + min = element; + minBy = by; + } else if (runtime.compare(by, minBy) < 0) { + min = element; + minBy = by; + } + } + // max should never be null at this point + return min; + } +} 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 new file mode 100644 index 00000000000..058bb459380 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/MinFunction.java @@ -0,0 +1,36 @@ +/* + * 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 MinFunction implements Function { + @Override + public String name() { + return "min"; + } + + @Override + public T apply(JmespathRuntime runtime, List> functionArguments) { + checkArgumentCount(1, functionArguments); + T array = functionArguments.get(0).expectArray(); + if (runtime.length(array) == 0) { + return runtime.createNull(); + } + + T min = null; + boolean first = true; + for (T element : runtime.asIterable(array)) { + if (first) { + first = false; + min = element; + } else if (runtime.compare(element, min) < 0) { + min = element; + } + } + // min should never be null at this point + return min; + } +} 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 new file mode 100644 index 00000000000..1092f7fcca0 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/NotNullFunction.java @@ -0,0 +1,32 @@ +/* + * 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; +import software.amazon.smithy.jmespath.JmespathException; +import software.amazon.smithy.jmespath.JmespathExceptionType; +import software.amazon.smithy.jmespath.RuntimeType; + +class NotNullFunction implements Function { + @Override + public String name() { + return "not_null"; + } + + @Override + public T apply(JmespathRuntime runtime, List> functionArguments) { + if (functionArguments.isEmpty()) { + throw new JmespathException(JmespathExceptionType.INVALID_ARITY, + "Expected at least 1 arguments, got 0"); + } + for (FunctionArgument arg : functionArguments) { + T value = arg.expectValue(); + if (!runtime.is(value, RuntimeType.NULL)) { + return value; + } + } + return runtime.createNull(); + } +} diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/NumberType.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/NumberType.java new file mode 100644 index 00000000000..6c449b6abbd --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/NumberType.java @@ -0,0 +1,16 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.jmespath.evaluation; + +public enum NumberType { + BYTE, + SHORT, + INTEGER, + LONG, + FLOAT, + DOUBLE, + BIG_INTEGER, + BIG_DECIMAL, +} 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 new file mode 100644 index 00000000000..b2089b5059f --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/ReverseFunction.java @@ -0,0 +1,48 @@ +/* + * 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.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import software.amazon.smithy.jmespath.RuntimeType; + +class ReverseFunction implements Function { + private static final Set PARAMETER_TYPES = new HashSet<>(); + static { + PARAMETER_TYPES.add(RuntimeType.STRING); + PARAMETER_TYPES.add(RuntimeType.ARRAY); + } + + @Override + public String name() { + return "reverse"; + } + + @Override + public T apply(JmespathRuntime runtime, List> functionArguments) { + checkArgumentCount(1, functionArguments); + T value = functionArguments.get(0).expectAnyOf(PARAMETER_TYPES); + + if (runtime.is(value, RuntimeType.STRING)) { + String str = runtime.asString(value); + return runtime.createString(new StringBuilder(str).reverse().toString()); + } else { + List elements = new ArrayList<>(); + for (T element : runtime.asIterable(value)) { + elements.add(element); + } + Collections.reverse(elements); + + JmespathRuntime.ArrayBuilder builder = runtime.arrayBuilder(); + for (T element : elements) { + builder.add(element); + } + return builder.build(); + } + } +} 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 new file mode 100644 index 00000000000..ef9287445c8 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/SortByFunction.java @@ -0,0 +1,41 @@ +/* + * 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.ArrayList; +import java.util.Collections; +import java.util.List; +import software.amazon.smithy.jmespath.JmespathExpression; + +class SortByFunction implements Function { + @Override + public String name() { + return "sort_by"; + } + + @Override + public T apply(JmespathRuntime runtime, List> functionArguments) { + checkArgumentCount(2, functionArguments); + T array = functionArguments.get(0).expectArray(); + JmespathExpression expression = functionArguments.get(1).expectExpression(); + + List elements = new ArrayList<>(); + for (T element : runtime.asIterable(array)) { + elements.add(element); + } + + Collections.sort(elements, (a, b) -> { + T aValue = expression.evaluate(a, runtime); + T bValue = expression.evaluate(b, runtime); + return runtime.compare(aValue, bValue); + }); + + JmespathRuntime.ArrayBuilder builder = runtime.arrayBuilder(); + for (T element : elements) { + builder.add(element); + } + return builder.build(); + } +} 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 new file mode 100644 index 00000000000..e5aa1cc1e2e --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/SortFunction.java @@ -0,0 +1,34 @@ +/* + * 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.ArrayList; +import java.util.List; + +class SortFunction implements Function { + @Override + public String name() { + return "sort"; + } + + @Override + public T apply(JmespathRuntime runtime, List> functionArguments) { + checkArgumentCount(1, functionArguments); + T array = functionArguments.get(0).expectArray(); + + List elements = new ArrayList<>(); + for (T element : runtime.asIterable(array)) { + elements.add(element); + } + + elements.sort(runtime); + + JmespathRuntime.ArrayBuilder builder = runtime.arrayBuilder(); + for (T element : elements) { + builder.add(element); + } + return builder.build(); + } +} 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 new file mode 100644 index 00000000000..2403cc9cc6f --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/StartsWithFunction.java @@ -0,0 +1,26 @@ +/* + * 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 StartsWithFunction implements Function { + @Override + public String name() { + return "starts_with"; + } + + @Override + public T apply(JmespathRuntime runtime, 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); + + return 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 new file mode 100644 index 00000000000..35a16ef3599 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/SumFunction.java @@ -0,0 +1,25 @@ +/* + * 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 SumFunction implements Function { + @Override + public String name() { + return "sum"; + } + + @Override + public T apply(JmespathRuntime runtime, List> functionArguments) { + 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)); + } + return runtime.createNumber(sum); + } +} 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 new file mode 100644 index 00000000000..f7258473025 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/ToArrayFunction.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; +import software.amazon.smithy.jmespath.RuntimeType; + +class ToArrayFunction implements Function { + @Override + public String name() { + return "to_array"; + } + + @Override + public T apply(JmespathRuntime runtime, List> functionArguments) { + 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(); + } + } +} 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 new file mode 100644 index 00000000000..acb5e7cbf36 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/ToNumberFunction.java @@ -0,0 +1,38 @@ +/* + * 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 ToNumberFunction implements Function { + @Override + public String name() { + return "to_number"; + } + + @Override + public T apply(JmespathRuntime runtime, List> functionArguments) { + checkArgumentCount(1, functionArguments); + T value = functionArguments.get(0).expectValue(); + + switch (runtime.typeOf(value)) { + case NUMBER: + return value; + case STRING: + try { + String str = runtime.asString(value); + if (str.contains(".") || str.toLowerCase().contains("e")) { + return runtime.createNumber(Double.parseDouble(str)); + } else { + return runtime.createNumber(Long.parseLong(str)); + } + } catch (NumberFormatException e) { + return runtime.createNull(); + } + default: + return runtime.createNull(); + } + } +} 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 new file mode 100644 index 00000000000..fb6025be3c8 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/ToStringFunction.java @@ -0,0 +1,27 @@ +/* + * 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 ToStringFunction implements Function { + @Override + public String name() { + return "to_string"; + } + + @Override + public T apply(JmespathRuntime runtime, List> functionArguments) { + checkArgumentCount(1, functionArguments); + T value = functionArguments.get(0).expectValue(); + + switch (runtime.typeOf(value)) { + case STRING: + return value; + default: + 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 new file mode 100644 index 00000000000..4039e2c43af --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/TypeFunction.java @@ -0,0 +1,21 @@ +/* + * 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 TypeFunction implements Function { + @Override + public String name() { + return "type"; + } + + @Override + public T apply(JmespathRuntime runtime, List> functionArguments) { + checkArgumentCount(1, functionArguments); + T value = functionArguments.get(0).expectValue(); + return runtime.createString(runtime.typeOf(value).toString()); + } +} 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 new file mode 100644 index 00000000000..154acc170b3 --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/evaluation/ValuesFunction.java @@ -0,0 +1,26 @@ +/* + * 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 ValuesFunction implements Function { + @Override + public String name() { + return "values"; + } + + @Override + public T apply(JmespathRuntime runtime, List> functionArguments) { + checkArgumentCount(1, functionArguments); + T value = functionArguments.get(0).expectObject(); + + JmespathRuntime.ArrayBuilder arrayBuilder = runtime.arrayBuilder(); + for (T key : runtime.asIterable(value)) { + arrayBuilder.add(runtime.value(value, key)); + } ; + return arrayBuilder.build(); + } +} diff --git a/smithy-jmespath/src/test/java/software/amazon/smithy/jmespath/LexerTest.java b/smithy-jmespath/src/test/java/software/amazon/smithy/jmespath/LexerTest.java index 5e2722c5453..a5b4e7797cd 100644 --- a/smithy-jmespath/src/test/java/software/amazon/smithy/jmespath/LexerTest.java +++ b/smithy-jmespath/src/test/java/software/amazon/smithy/jmespath/LexerTest.java @@ -5,7 +5,7 @@ package software.amazon.smithy.jmespath; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; @@ -79,7 +79,7 @@ public void tokenizesJsonArray() { assertThat(tokens, hasSize(2)); assertThat(tokens.get(0).type, equalTo(TokenType.LITERAL)); - assertThat(tokens.get(0).value.expectArrayValue(), equalTo(Arrays.asList(1.0, true, false, null, -2.0, "hi"))); + assertThat(tokens.get(0).value.expectArrayValue(), equalTo(Arrays.asList(1L, true, false, null, -2L, "hi"))); assertThat(tokens.get(0).line, equalTo(1)); assertThat(tokens.get(0).column, equalTo(1)); @@ -140,7 +140,7 @@ public void tokenizesJsonObject() { assertThat(tokens.get(0).type, equalTo(TokenType.LITERAL)); Map obj = tokens.get(0).value.expectObjectValue(); assertThat(obj.entrySet(), hasSize(2)); - assertThat(obj.keySet(), contains("foo", "bar")); + assertThat(obj.keySet(), containsInAnyOrder("foo", "bar")); assertThat(obj.get("foo"), equalTo(true)); assertThat(obj.get("bar"), equalTo(Collections.singletonMap("bam", Collections.emptyList()))); assertThat(tokens.get(0).line, equalTo(1)); @@ -599,7 +599,7 @@ public void convertsLexemeTokensToString() { assertThat(tokens.get(0).toString(), equalTo("'abc'")); assertThat(tokens.get(1).toString(), equalTo("'.'")); assertThat(tokens.get(2).toString(), equalTo("':'")); - assertThat(tokens.get(3).toString(), equalTo("'10.0'")); + assertThat(tokens.get(3).toString(), equalTo("'10'")); } @Test @@ -615,7 +615,7 @@ public void tracksLineAndColumn() { assertThat(tokens.get(2).line, is(3)); assertThat(tokens.get(2).column, is(1)); - assertThat(tokens.get(3).toString(), equalTo("'10.0'")); + assertThat(tokens.get(3).toString(), equalTo("'10'")); assertThat(tokens.get(3).line, is(4)); assertThat(tokens.get(3).column, is(1));