diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/ExpressionSerializer.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/ExpressionSerializer.java new file mode 100644 index 00000000000..8780f70c21e --- /dev/null +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/ExpressionSerializer.java @@ -0,0 +1,383 @@ +/* + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.jmespath; + +import java.util.Map; +import java.util.Set; +import software.amazon.smithy.jmespath.ast.AndExpression; +import software.amazon.smithy.jmespath.ast.BinaryExpression; +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; + +/** + * Serializes the JMESPath expression AST back to a JMESPath expression. + */ +public final class ExpressionSerializer { + + /** + * Serialize the given JMESPath expression to a string. + * + * @param expression JMESPath expression to serialize. + * @return Returns the serialized expression. + */ + public String serialize(JmespathExpression expression) { + StringBuilder builder = new StringBuilder(); + Visitor visitor = new Visitor(builder); + expression.accept(visitor); + return builder.toString(); + } + + private static final class Visitor implements ExpressionVisitor { + private final StringBuilder builder; + + Visitor(StringBuilder builder) { + this.builder = builder; + } + + @Override + public Void visitComparator(ComparatorExpression expression) { + // e.g.: foo > bar + expression.getLeft().accept(this); + builder.append(' '); + builder.append(expression.getComparator()); + builder.append(' '); + expression.getRight().accept(this); + return null; + } + + @Override + public Void visitCurrentNode(CurrentExpression expression) { + builder.append('@'); + return null; + } + + @Override + public Void visitExpressionType(ExpressionTypeExpression expression) { + // e.g.: &(foo.bar) + builder.append("&("); + expression.getExpression().accept(this); + builder.append(')'); + return null; + } + + @Override + public Void visitFlatten(FlattenExpression expression) { + // e.g.: foo[] + expression.getExpression().accept(this); + builder.append("[]"); + return null; + } + + @Override + public Void visitFunction(FunctionExpression expression) { + // e.g.: some_function(@, foo) + builder.append(expression.getName()); + builder.append('('); + for (int i = 0; i < expression.getArguments().size(); i++) { + expression.getArguments().get(i).accept(this); + if (i < expression.getArguments().size() - 1) { + builder.append(", "); + } + } + builder.append(')'); + return null; + } + + @Override + public Void visitField(FieldExpression expression) { + // Always quote fields: "foo" + builder.append('"'); + builder.append(sanitizeString(expression.getName(), false)); + builder.append('"'); + return null; + } + + private String sanitizeString(String str, boolean escapeBackticks) { + String result = str.replace("\\", "\\\\").replace("\"", "\\\""); + if (escapeBackticks) { + result = result.replace("`", "\\`"); + } + return result; + } + + @Override + public Void visitIndex(IndexExpression expression) { + // e.g.: [1] + builder.append('[').append(expression.getIndex()).append(']'); + return null; + } + + @Override + public Void visitLiteral(LiteralExpression expression) { + // e.g.: `[true]` + visitLiteral(expression, false); + return null; + } + + private void visitLiteral(LiteralExpression expression, boolean nestedInsideLiteral) { + if (!nestedInsideLiteral) { + builder.append('`'); + } + + switch (expression.getType()) { + case NUMBER: + builder.append(expression.expectNumberValue()); + break; + case NULL: + case ANY: // treat "any" like null + builder.append("null"); + break; + case ARRAY: + builder.append('['); + int ai = 0; + for (Object value : expression.expectArrayValue()) { + LiteralExpression exp = LiteralExpression.from(value); + visitLiteral(exp, true); + if (ai++ < expression.expectArrayValue().size() - 1) { + builder.append(", "); + } + } + builder.append(']'); + break; + case OBJECT: + builder.append('{'); + int oi = 0; + Set> objectEntries = expression.expectObjectValue().entrySet(); + for (Map.Entry objectEntry : objectEntries) { + builder.append('"') + .append(sanitizeString(objectEntry.getKey(), true)) + .append("\": "); + LiteralExpression exp = LiteralExpression.from(objectEntry.getValue()); + visitLiteral(exp, true); + if (oi++ < objectEntries.size() - 1) { + builder.append(", "); + } + } + builder.append('}'); + break; + case STRING: + builder.append('"') + .append(sanitizeString(expression.expectStringValue(), true)) + .append('"'); + break; + case BOOLEAN: + builder.append(expression.expectBooleanValue()); + break; + case EXPRESSION: + // fall-through + default: + throw new JmespathException("Unable to serialize literal runtime value: " + expression); + } + + if (!nestedInsideLiteral) { + builder.append('`'); + } + } + + @Override + public Void visitMultiSelectList(MultiSelectListExpression expression) { + // e.g.: [foo, bar[].baz] + builder.append('['); + for (int i = 0; i < expression.getExpressions().size(); i++) { + expression.getExpressions().get(i).accept(this); + if (i < expression.getExpressions().size() - 1) { + builder.append(", "); + } + } + builder.append(']'); + return null; + } + + @Override + public Void visitMultiSelectHash(MultiSelectHashExpression expression) { + // e.g.: {foo: `true`, bar: bar} + builder.append('{'); + int i = 0; + for (Map.Entry entry : expression.getExpressions().entrySet()) { + builder.append('"').append(sanitizeString(entry.getKey(), false)).append("\": "); + entry.getValue().accept(this); + if (i < expression.getExpressions().entrySet().size() - 1) { + builder.append(", "); + } + i++; + } + builder.append('}'); + return null; + } + + @Override + public Void visitAnd(AndExpression expression) { + // e.g.: (a && b) + builder.append('('); + expression.getLeft().accept(this); + builder.append(" && "); + expression.getRight().accept(this); + builder.append(')'); + return null; + } + + @Override + public Void visitOr(OrExpression expression) { + // e.g.: (a || b) + builder.append('('); + expression.getLeft().accept(this); + builder.append(" || "); + expression.getRight().accept(this); + builder.append(')'); + return null; + } + + @Override + public Void visitNot(NotExpression expression) { + // e.g.: !(foo.bar) + builder.append("!("); + expression.getExpression().accept(this); + builder.append(')'); + return null; + } + + @Override + public Void visitProjection(ProjectionExpression expression) { + if (!(expression.getLeft() instanceof CurrentExpression)) { + expression.getLeft().accept(this); + } + + // Without this check, [::1].a would be reserialized into [::-1][*]."a", + // which is equivalent, but convoluted. + if (!(expression.getLeft() instanceof SliceExpression)) { + // Flatten expressions, when parsed, inherently create a projection. Unroll + // that here. + if (!(expression.getLeft() instanceof FlattenExpression)) { + builder.append("[*]"); + } + } + + // Avoid unnecessary addition of extracting the current node. + // We add the current node in projections if there's no RHS. + if (!(expression.getRight() instanceof CurrentExpression)) { + // Add a "." if the right hand side needs it. + if (rhsNeedsDot(expression.getRight())) { + builder.append('.'); + } + expression.getRight().accept(this); + } + + return null; + } + + @Override + public Void visitFilterProjection(FilterProjectionExpression expression) { + // e.g.: foo[?bar == `10`].baz + // Don't emit a pointless the current node select. + if (!(expression.getLeft() instanceof CurrentExpression)) { + expression.getLeft().accept(this); + } + + builder.append("[?"); + expression.getComparison().accept(this); + builder.append(']'); + + // Avoid unnecessary addition of extracting the current node. + if (!(expression.getRight() instanceof CurrentExpression)) { + // Add a "." if the right hand side needs it. + if (rhsNeedsDot(expression.getRight())) { + builder.append('.'); + } + expression.getRight().accept(this); + } + + return null; + } + + @Override + public Void visitObjectProjection(ObjectProjectionExpression expression) { + // Avoid the unnecessary "@.*" by just emitting "*". + if (!(expression.getLeft() instanceof CurrentExpression)) { + expression.getLeft().accept(this); + builder.append(".*"); + } else { + builder.append('*'); + } + + // Avoid unnecessary addition of extracting the current node. + // We add the current node in projections if there's no RHS. + if (!(expression.getRight() instanceof CurrentExpression)) { + // Add a "." if the right hand side needs it. + if (rhsNeedsDot(expression.getRight())) { + builder.append('.'); + } + expression.getRight().accept(this); + } + + return null; + } + + @Override + public Void visitSlice(SliceExpression expression) { + // e.g.: [0::1], [0:1:2], etc + builder.append('['); + expression.getStart().ifPresent(builder::append); + builder.append(':'); + expression.getStop().ifPresent(builder::append); + builder.append(':'); + builder.append(expression.getStep()); + builder.append(']'); + return null; + } + + @Override + public Void visitSubexpression(Subexpression expression) { + // e.g.: "foo"."bar", "foo" | "bar" + expression.getLeft().accept(this); + + if (expression.isPipe()) { + // pipe has a different precedence than dot, so this is important. + builder.append(" | "); + } else if (rhsNeedsDot(expression.getRight())) { + builder.append('.'); + } + + expression.getRight().accept(this); + return null; + } + + // These expression need to be preceded by a "." in a binary expression. + private boolean rhsNeedsDot(JmespathExpression expression) { + return expression instanceof FieldExpression + || expression instanceof MultiSelectHashExpression + || expression instanceof MultiSelectListExpression + || expression instanceof ObjectProjectionExpression + || expression instanceof FunctionExpression + || (expression instanceof BinaryExpression + && rhsNeedsDot(((BinaryExpression) expression).getLeft())); + } + } +} 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 f46809bb5ff..c99dc26aa26 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 @@ -170,7 +170,7 @@ private JmespathExpression led(JmespathExpression left) { case AND: // Example: a && b return new AndExpression(left, expression(token.type.lbp), token.line, token.column); case PIPE: // Example: a | b - return new Subexpression(left, expression(token.type.lbp), token.line, token.column); + return new Subexpression(left, expression(token.type.lbp), token.line, token.column, true); case FILTER: // Example: a[?foo == bar] return parseFilter(left); case LBRACKET: diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/ast/BinaryExpression.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/ast/BinaryExpression.java index ea685c8f8c7..dec880c5a1d 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/ast/BinaryExpression.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/ast/BinaryExpression.java @@ -52,9 +52,11 @@ public final JmespathExpression getRight() { @Override public boolean equals(Object o) { - if (this == o) { + if (o == null) { + return false; + } else if (this == o) { return true; - } else if (!(o instanceof BinaryExpression) || o.getClass() != o.getClass()) { + } else if (!o.getClass().equals(getClass())) { return false; } BinaryExpression that = (BinaryExpression) o; @@ -63,7 +65,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(getLeft(), getRight()); + return Objects.hash(getClass().getSimpleName(), getLeft(), getRight()); } @Override diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/ast/FilterProjectionExpression.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/ast/FilterProjectionExpression.java index 98f707e7cca..f61061998ad 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/ast/FilterProjectionExpression.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/ast/FilterProjectionExpression.java @@ -27,13 +27,15 @@ * a {@link ComparatorExpression}, and yields any value from the comparison * expression that returns {@code true} to the right AST expression. * + *

Note: while this expression does have a comparator expression, it is + * still considered a binary expression because it has a left hand side and + * a right hand side. + * * @see Filter Expressions */ -public final class FilterProjectionExpression extends JmespathExpression { +public final class FilterProjectionExpression extends BinaryExpression { private final JmespathExpression comparison; - private final JmespathExpression left; - private final JmespathExpression right; public FilterProjectionExpression( JmespathExpression left, @@ -50,20 +52,10 @@ public FilterProjectionExpression( int line, int column ) { - super(line, column); - this.left = left; - this.right = right; + super(left, right, line, column); this.comparison = comparison; } - public JmespathExpression getLeft() { - return left; - } - - public JmespathExpression getRight() { - return right; - } - public JmespathExpression getComparison() { return comparison; } @@ -95,7 +87,7 @@ public int hashCode() { public String toString() { return "FilterProjectionExpression{" + "comparison=" + comparison - + ", left=" + left - + ", right=" + right + '}'; + + ", left=" + getLeft() + + ", right=" + getRight() + '}'; } } 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 0c560bab841..bed863a236f 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 @@ -107,7 +107,13 @@ public boolean equals(Object o) { } else if (!(o instanceof LiteralExpression)) { return false; } else { - return Objects.equals(value, ((LiteralExpression) o).value); + LiteralExpression other = (LiteralExpression) o; + // Compare all numbers as doubles to remove conflicts between integers, floats, and doubles. + if (value instanceof Number && other.getValue() instanceof Number) { + return ((Number) value).doubleValue() == ((Number) other.getValue()).doubleValue(); + } else { + return Objects.equals(value, ((LiteralExpression) o).value); + } } } diff --git a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/ast/Subexpression.java b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/ast/Subexpression.java index 63ed814ded8..848c95a9e5d 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/ast/Subexpression.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/ast/Subexpression.java @@ -29,16 +29,34 @@ */ public final class Subexpression extends BinaryExpression { + private final boolean isPipe; + public Subexpression(JmespathExpression left, JmespathExpression right) { this(left, right, 1, 1); } public Subexpression(JmespathExpression left, JmespathExpression right, int line, int column) { + this(left, right, line, column, false); + } + + public Subexpression(JmespathExpression left, JmespathExpression right, boolean isPipe) { + this(left, right, 1, 1); + } + + public Subexpression(JmespathExpression left, JmespathExpression right, int line, int column, boolean isPipe) { super(left, right, line, column); + this.isPipe = isPipe; } @Override public T accept(ExpressionVisitor visitor) { return visitor.visitSubexpression(this); } + + /** + * @return Returns true if this node was created from a pipe "|". + */ + public boolean isPipe() { + return isPipe; + } } diff --git a/smithy-jmespath/src/test/java/software/amazon/smithy/jmespath/ExpressionSerializerTest.java b/smithy-jmespath/src/test/java/software/amazon/smithy/jmespath/ExpressionSerializerTest.java new file mode 100644 index 00000000000..4c04ca3f00d --- /dev/null +++ b/smithy-jmespath/src/test/java/software/amazon/smithy/jmespath/ExpressionSerializerTest.java @@ -0,0 +1,138 @@ +/* + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.jmespath; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +import java.util.Arrays; +import java.util.Collection; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +public class ExpressionSerializerTest { + @ParameterizedTest + @MethodSource("shapeSource") + public void serializesExpressions(String expressionString, String expectedString) { + JmespathExpression expression = JmespathExpression.parse(expressionString); + ExpressionSerializer serializer = new ExpressionSerializer(); + String serialized = serializer.serialize(expression); + + // First make sure we made a valid expression. + JmespathExpression reparsed = JmespathExpression.parse(serialized); + + // Now make sure it's equivalent to the origin expression. + assertThat(expression, equalTo(reparsed)); + + assertThat(serialized, equalTo(expectedString)); + } + + public static Collection shapeSource() { + return Arrays.asList(new Object[][] { + // Current node + {"@", "@"}, + + // Identifiers + {"foo", "\"foo\""}, + {"\"a\\\"b\"", "\"a\\\"b\""}, + {"\"a\\\\b\"", "\"a\\\\b\""}, + + // Array index + {"[0]", "[0]"}, + {"[10]", "[10]"}, + + // Array slices + {"[0:]", "[0::1]"}, // projection that contains the slice + {"[0:].foo", "[0::1].\"foo\""}, + + // Subexpressions + {"foo | bar", "\"foo\" | \"bar\""}, + {"foo.bar", "\"foo\".\"bar\""}, + {"foo.bar.baz", "\"foo\".\"bar\".\"baz\""}, + {"foo | bar | baz", "\"foo\" | \"bar\" | \"baz\""}, + {"foo | bar | baz | \"a.b\"", "\"foo\" | \"bar\" | \"baz\" | \"a.b\""}, + + // Object projections + {"*", "*"}, + {"foo.*", "\"foo\".*"}, + {"foo.* | @", "\"foo\".* | @"}, + {"foo.*.bar", "\"foo\".*.\"bar\""}, + {"foo.*.bar | bam", "\"foo\".*.\"bar\" | \"bam\""}, + + // Array projections / flatten + {"[]", "@[]"}, + {"foo[]", "\"foo\"[]"}, + {"foo[].bar", "\"foo\"[].\"bar\""}, + {"foo[] | bar", "\"foo\"[] | \"bar\""}, + + // Not + {"!@", "!(@)"}, + {"!foo.bar", "!(\"foo\").\"bar\""}, // this expression in nonsensical, but valid. + + // And + {"@ && @", "(@ && @)"}, + {"foo.bar && foo.baz", "(\"foo\".\"bar\" && \"foo\".\"baz\")"}, + + // Or + {"@ || @", "(@ || @)"}, + {"foo.bar || foo.baz", "(\"foo\".\"bar\" || \"foo\".\"baz\")"}, + + // functions + {"length(@)", "length(@)"}, + {"ends_with(@, @)", "ends_with(@, @)"}, + {"min_by(@, &foo)", "min_by(@, &(\"foo\"))"}, + + // comparator + {"@ == @", "@ == @"}, + + // multi-select list + {"[@]", "[@]"}, + {"[@, @]", "[@, @]"}, + + // multi-select hash + {"{foo: foo, bar: bar}", "{\"foo\": \"foo\", \"bar\": \"bar\"}"}, + + // Filter expressions. + {"foo[?bar > baz][?qux > baz]", "\"foo\"[?\"bar\" > \"baz\"][?\"qux\" > \"baz\"]"} + }); + } + + @ParameterizedTest() + @MethodSource("validExpressions") + public void canSerializeEveryValidExpressionFromFile(String line) { + JmespathExpression expression = JmespathExpression.parse(line); + ExpressionSerializer serializer = new ExpressionSerializer(); + String serialized = serializer.serialize(expression); + + try { + JmespathExpression reparsed = JmespathExpression.parse(serialized); + + // The AST of the originally parsed value must be equal to the AST of the reserialized then + // parsed value. + assertThat(line, expression, equalTo(reparsed)); + } catch (JmespathException e) { + throw new RuntimeException("Error parsing " + serialized + ": " + e.getMessage(), e); + } + } + + // Ensures that every expression in "valid" can be serialized and reparsed correctly. + // The serialized string my be different, but the AST must be the same. + public static Stream validExpressions() { + return new NewLineExpressionsDataSource().validTests() + .map(line -> new Object[] {line}); + } +} diff --git a/smithy-jmespath/src/test/java/software/amazon/smithy/jmespath/NewLineExpressionsDataSource.java b/smithy-jmespath/src/test/java/software/amazon/smithy/jmespath/NewLineExpressionsDataSource.java new file mode 100644 index 00000000000..31c394dacfa --- /dev/null +++ b/smithy-jmespath/src/test/java/software/amazon/smithy/jmespath/NewLineExpressionsDataSource.java @@ -0,0 +1,46 @@ +/* + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.jmespath; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.stream.Stream; + +final class NewLineExpressionsDataSource { + + public Stream validTests() { + return readFile(getClass().getResourceAsStream("valid")); + } + + public Stream invalidTests() { + return readFile(getClass().getResourceAsStream("invalid")); + } + + private Stream readFile(InputStream stream) { + return new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8)) + .lines() + .map(line -> { + if (line.endsWith(",")) { + return line.substring(0, line.length() - 1); + } else { + return line; + } + }) + .map(line -> Lexer.tokenize(line).next().value.expectStringValue()); + } +} diff --git a/smithy-jmespath/src/test/java/software/amazon/smithy/jmespath/RunnerTest.java b/smithy-jmespath/src/test/java/software/amazon/smithy/jmespath/RunnerTest.java index bd419f69f4f..9efeb863c64 100644 --- a/smithy-jmespath/src/test/java/software/amazon/smithy/jmespath/RunnerTest.java +++ b/smithy-jmespath/src/test/java/software/amazon/smithy/jmespath/RunnerTest.java @@ -1,11 +1,5 @@ package software.amazon.smithy.jmespath; -import java.io.BufferedReader; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.stream.Collectors; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -16,7 +10,7 @@ public class RunnerTest { @Test public void validTests() { - for (String line : readFile(getClass().getResourceAsStream("valid"))) { + new NewLineExpressionsDataSource().validTests().forEach(line -> { try { JmespathExpression expression = JmespathExpression.parse(line); for (ExpressionProblem problem : expression.lint().getProblems()) { @@ -27,32 +21,18 @@ public void validTests() { } catch (JmespathException e) { Assertions.fail("Error loading line:\n" + line + "\n" + e.getMessage(), e); } - } + }); } @Test public void invalidTests() { - for (String line : readFile(getClass().getResourceAsStream("invalid"))) { + new NewLineExpressionsDataSource().invalidTests().forEach(line -> { try { JmespathExpression.parse(line); Assertions.fail("Expected line to fail: " + line); } catch (JmespathException e) { // pass } - } - } - - private List readFile(InputStream stream) { - return new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8)) - .lines() - .map(line -> { - if (line.endsWith(",")) { - return line.substring(0, line.length() - 1); - } else { - return line; - } - }) - .map(line -> Lexer.tokenize(line).next().value.expectStringValue()) - .collect(Collectors.toList()); + }); } } diff --git a/smithy-jmespath/src/test/resources/software/amazon/smithy/jmespath/valid b/smithy-jmespath/src/test/resources/software/amazon/smithy/jmespath/valid index 61f5abe5afe..1da44efaa28 100644 --- a/smithy-jmespath/src/test/resources/software/amazon/smithy/jmespath/valid +++ b/smithy-jmespath/src/test/resources/software/amazon/smithy/jmespath/valid @@ -570,3 +570,121 @@ "foo[*][1][1]", "hash.*", "*[0]" +"CacheClusters[].CacheClusterStatus", +"ReplicationGroups[].Status", +"workspace.status.statusCode", +"environment.deploymentStatus", +"environmentTemplateVersion.status", +"service.status", +"service.pipeline.deploymentStatus", +"serviceInstance.deploymentStatus", +"serviceTemplateVersion.status", +"CodeReview.State", +"RepositoryAssociation.State", +"Fleets[].State", +"LoadBalancers[].State.Code", +"TargetHealthDescriptions[].TargetHealth.State", +"Status", +"Stacks[].StackStatus", +"ProgressStatus", +"deploymentInfo.status", +"length(MetricAlarms[]) > `0`", +"length(CompositeAlarms[]) > `0`", +"Flow.Status", +"botStatus", +"botAliasStatus", +"botLocaleStatus", +"exportStatus", +"importStatus", +"status", +"imageScanStatus.status", +"EndpointStatus", +"ImageStatus", +"ImageVersionStatus", +"NotebookInstanceStatus", +"ProcessingJobStatus", +"TrainingJobStatus", +"TransformJobStatus", +"failures[].reason", +"services[].status", +"tasks[].lastStatus", +"Cluster.Status", +"ControlPanel.Status", +"RoutingControl.Status", +"Certificate.DomainValidationOptions[].ValidationStatus", +"Certificate.Status", +"Deployments[].Status", +"Instances[].Status", +"VerificationAttributes.*.VerificationStatus", +"assetStatus.state", +"assetModelStatus.state", +"portalStatus.state", +"Connections[].Status", +"Endpoints[].Status", +"ReplicationInstances[].ReplicationInstanceStatus", +"ReplicationTasks[].Status", +"BundleTasks[].State", +"ConversionTasks[].State", +"CustomerGateways[].State", +"ExportTasks[].State", +"Images[].State", +"length(Images[]) > `0`", +"InstanceStatuses[].InstanceStatus.Status", +"InstanceStatuses[].SystemStatus.Status", +"length(Reservations[]) > `0`", +"Reservations[].Instances[].State.Name", +"length(InternetGateways[].InternetGatewayId) > `0`", +"length(KeyPairs[].KeyName) > `0`", +"NatGateways[].State", +"NetworkInterfaces[].Status", +"length(SecurityGroups[].GroupId) > `0`", +"Snapshots[].State", +"SpotInstanceRequests[].Status.Code", +"Subnets[].State", +"Volumes[].State", +"VpcPeeringConnections[].Status.Code", +"Vpcs[].State", +"VpnConnections[].State", +"length(PasswordData) > `0`", +"AuditReportStatus", +"Environments[].Status", +"Job.Status", +"launchProfile.state", +"streamingImage.state", +"session.state", +"stream.state", +"studio.state", +"studioComponent.state", +"DBInstances[].DBInstanceStatus", +"addon.status", +"cluster.status", +"fargateProfile.status", +"nodegroup.status", +"Cluster.Status.State", +"Step.Status.State", +"Results[].Status", +"DBClusterSnapshots[].Status", +"length(DBClusterSnapshots) == `0`", +"length(DBInstances) == `0`", +"DBSnapshots[].Status", +"length(DBSnapshots) == `0`", +"NodeAssociationStatus", +"ProjectVersionDescriptions[].Status", +"ProgressEvent.OperationStatus", +"ChangeInfo.Status", +"Snapshots[].Status", +"Clusters[].ClusterStatus", +"Clusters[].RestoreStatus.Status", +"length(AutoScalingGroups) > `0`", +"contains(AutoScalingGroups[].[length(Instances[?LifecycleState=='InService']) >= MinSize][], `false`)", +"State", +"LastUpdateStatus", +"InstanceStates[].State", +"Distribution.Status", +"Invalidation.Status", +"StreamingDistribution.Status", +"StreamDescription.StreamStatus", +"replicationSet.status", +"Table.TableStatus" +"`[\"foo\\`bar\"]`", +"`{\"foo\\`bar\": \"foo\\`bar\"}`"