From 6d6bd13fc5bbdbc18f26f043109baef2ea212a7c Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Fri, 14 Jan 2022 12:02:10 -0800 Subject: [PATCH 1/2] Add ability to serialize the JMESPath AST This allows the JMESPath AST to be parsed, then modified, then reserialized. Use cases include things like being able to modify JMESPath expressions found in waiters so that field extraction matches any normalization changes code generators make to object keys (e.g., using snake_case in Ruby as opposed to the member names provided in the model). --- .../smithy/jmespath/ExpressionSerializer.java | 383 ++++++++++++++++++ .../amazon/smithy/jmespath/Parser.java | 2 +- .../smithy/jmespath/ast/BinaryExpression.java | 8 +- .../ast/FilterProjectionExpression.java | 24 +- .../jmespath/ast/LiteralExpression.java | 8 +- .../smithy/jmespath/ast/Subexpression.java | 18 + .../jmespath/ExpressionSerializerTest.java | 138 +++++++ .../NewLineExpressionsDataSource.java | 46 +++ .../amazon/smithy/jmespath/RunnerTest.java | 28 +- .../software/amazon/smithy/jmespath/valid | 118 ++++++ 10 files changed, 728 insertions(+), 45 deletions(-) create mode 100644 smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/ExpressionSerializer.java create mode 100644 smithy-jmespath/src/test/java/software/amazon/smithy/jmespath/ExpressionSerializerTest.java create mode 100644 smithy-jmespath/src/test/java/software/amazon/smithy/jmespath/NewLineExpressionsDataSource.java 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..2f1840b9a47 --- /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 project. 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\"}`" From 135e35153a72c0fe7ba42d6fcd309e68cbc2fbc3 Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Tue, 18 Jan 2022 13:53:49 -0800 Subject: [PATCH 2/2] Update smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/ExpressionSerializer.java Co-authored-by: Jordon Phillips --- .../software/amazon/smithy/jmespath/ExpressionSerializer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 2f1840b9a47..8780f70c21e 100644 --- a/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/ExpressionSerializer.java +++ b/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath/ExpressionSerializer.java @@ -273,7 +273,7 @@ public Void visitProjection(ProjectionExpression expression) { // 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 project. Unroll + // Flatten expressions, when parsed, inherently create a projection. Unroll // that here. if (!(expression.getLeft() instanceof FlattenExpression)) { builder.append("[*]");