From c02709f262dbb0fe5c7a33623a2e3264858bab80 Mon Sep 17 00:00:00 2001 From: Luc Talatinian Date: Wed, 10 Jul 2024 10:08:01 -0400 Subject: [PATCH] support rest of jmespath required in downstream sdk v2 --- .../GoJmespathExpressionGenerator.java | 349 +++++++++++++-- ...ntParameterOperationBindingsGenerator.java | 4 +- .../go/codegen/integration/Waiters.java | 252 +++-------- .../smithy/go/codegen/util/ShapeUtil.java | 15 + .../GoJmespathExpressionGeneratorTest.java | 420 ++++++++++++++++-- 5 files changed, 773 insertions(+), 267 deletions(-) diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/GoJmespathExpressionGenerator.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/GoJmespathExpressionGenerator.java index 3bff1f18..9206ecbd 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/GoJmespathExpressionGenerator.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/GoJmespathExpressionGenerator.java @@ -15,25 +15,44 @@ package software.amazon.smithy.go.codegen; +import static software.amazon.smithy.go.codegen.GoWriter.goTemplate; +import static software.amazon.smithy.go.codegen.SymbolUtils.isPointable; +import static software.amazon.smithy.go.codegen.SymbolUtils.sliceOf; +import static software.amazon.smithy.go.codegen.util.ShapeUtil.BOOL_SHAPE; +import static software.amazon.smithy.go.codegen.util.ShapeUtil.INT_SHAPE; import static software.amazon.smithy.go.codegen.util.ShapeUtil.STRING_SHAPE; -import static software.amazon.smithy.go.codegen.util.ShapeUtil.expectMember; import static software.amazon.smithy.go.codegen.util.ShapeUtil.listOf; import static software.amazon.smithy.utils.StringUtils.capitalize; import java.util.List; +import java.util.Map; import software.amazon.smithy.codegen.core.CodegenException; +import software.amazon.smithy.codegen.core.Symbol; +import software.amazon.smithy.go.codegen.util.ShapeUtil; import software.amazon.smithy.jmespath.JmespathExpression; +import software.amazon.smithy.jmespath.ast.AndExpression; +import software.amazon.smithy.jmespath.ast.ComparatorExpression; +import software.amazon.smithy.jmespath.ast.ComparatorType; +import software.amazon.smithy.jmespath.ast.CurrentExpression; 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.LiteralExpression; +import software.amazon.smithy.jmespath.ast.MultiSelectListExpression; +import software.amazon.smithy.jmespath.ast.NotExpression; import software.amazon.smithy.jmespath.ast.ProjectionExpression; import software.amazon.smithy.jmespath.ast.Subexpression; -import software.amazon.smithy.model.shapes.ListShape; +import software.amazon.smithy.model.shapes.CollectionShape; +import software.amazon.smithy.model.shapes.MapShape; +import software.amazon.smithy.model.shapes.NumberShape; import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.StringShape; import software.amazon.smithy.utils.SmithyInternalApi; /** * Traverses a JMESPath expression, producing a series of statements that evaluate the entire expression. The generator - * is shape-aware and the return indicates the underlying shape being referenced in the final result. + * is shape-aware and the return indicates the underlying shape/symbol being referenced in the final result. *
* Note that the use of writer.write() here is deliberate, it's easier to structure the code in that way instead of * trying to recursively compose/organize Writable templates. @@ -42,24 +61,19 @@ public class GoJmespathExpressionGenerator { private final GoCodegenContext ctx; private final GoWriter writer; - private final Shape input; - private final JmespathExpression root; - private int idIndex = 1; + private int idIndex = 0; - public GoJmespathExpressionGenerator(GoCodegenContext ctx, GoWriter writer, Shape input, JmespathExpression expr) { + public GoJmespathExpressionGenerator(GoCodegenContext ctx, GoWriter writer) { this.ctx = ctx; this.writer = writer; - this.input = input; - this.root = expr; } - public Result generate(String ident) { - writer.write("v1 := $L", ident); - return visit(root, input); + public Variable generate(JmespathExpression expr, Variable input) { + return visit(expr, input); } - private Result visit(JmespathExpression expr, Shape current) { + private Variable visit(JmespathExpression expr, Variable current) { if (expr instanceof FunctionExpression tExpr) { return visitFunction(tExpr, current); } else if (expr instanceof FieldExpression tExpr) { @@ -68,61 +82,257 @@ private Result visit(JmespathExpression expr, Shape current) { return visitSub(tExpr, current); } else if (expr instanceof ProjectionExpression tExpr) { return visitProjection(tExpr, current); + } else if (expr instanceof FlattenExpression tExpr) { + return visitFlatten(tExpr, current); + } else if (expr instanceof ComparatorExpression tExpr) { + return visitComparator(tExpr, current); + } else if (expr instanceof LiteralExpression tExpr) { + return visitLiteral(tExpr); + } else if (expr instanceof AndExpression tExpr) { + return visitAnd(tExpr, current); + } else if (expr instanceof NotExpression tExpr) { + return visitNot(tExpr, current); + } else if (expr instanceof FilterProjectionExpression tExpr) { + return visitFilterProjection(tExpr, current); + } else if (expr instanceof MultiSelectListExpression tExpr) { + return visitMultiSelectList(tExpr, current); + } else if (expr instanceof CurrentExpression) { + return current; } else { throw new CodegenException("unhandled jmespath expression " + expr.getClass().getSimpleName()); } } - private Result visitProjection(ProjectionExpression expr, Shape current) { + private Variable visitNot(NotExpression expr, Variable current) { + var inner = visit(expr.getExpression(), current); + var ident = nextIdent(); + writer.write("$L := !$L", ident, inner.ident); + return new Variable(BOOL_SHAPE, ident, GoUniverseTypes.Bool); + } + + private Variable visitMultiSelectList(MultiSelectListExpression expr, Variable current) { + if (expr.getExpressions().isEmpty()) { + throw new CodegenException("multi-select list w/ no expressions"); + } + + var items = expr.getExpressions().stream() + .map(it -> visit(it, current)) + .toList(); + var first = items.get(0); + + var ident = nextIdent(); + writer.write("$L := []$P{$L}", ident, first.type, + String.join(",", items.stream().map(it -> it.ident).toList())); + + return new Variable(listOf(first.shape), ident, sliceOf(first.type)); + } + + private Variable visitFilterProjection(FilterProjectionExpression expr, Variable current) { + var unfiltered = visitProjection(new ProjectionExpression(expr.getLeft(), expr.getRight()), current); + if (!(unfiltered.shape instanceof CollectionShape unfilteredCol)) { + throw new CodegenException("projection did not create a list"); + } + + var member = expectMember(unfilteredCol); + var type = ctx.symbolProvider().toSymbol(unfiltered.shape); + + var ident = nextIdent(); + writer.write("var $L $T", ident, type); + writer.openBlock("for _, v := range $L {", "}", unfiltered.ident, () -> { + var filterResult = visit(expr.getComparison(), new Variable(member, "v", type)); + writer.write(""" + if $1L { + $2L = append($2L, v) + }""", filterResult.ident, ident); + }); + + return new Variable(unfiltered.shape, ident, type); + } + + private Variable visitAnd(AndExpression expr, Variable current) { + var left = visit(expr.getLeft(), current); + var right = visit(expr.getRight(), current); + var ident = nextIdent(); + writer.write("$L := $L && $L", ident, left.ident, right.ident); + return new Variable(BOOL_SHAPE, ident, GoUniverseTypes.Bool); + } + + private Variable visitLiteral(LiteralExpression expr) { + var ident = nextIdent(); + if (expr.isNumberValue()) { + // FUTURE: recognize floating-point, for now we just use int + writer.write("$L := $L", ident, expr.expectNumberValue().intValue()); + return new Variable(INT_SHAPE, ident, GoUniverseTypes.Int); + } else if (expr.isStringValue()) { + writer.write("$L := $S", ident, expr.expectStringValue()); + return new Variable(STRING_SHAPE, ident, GoUniverseTypes.String); + } else if (expr.isBooleanValue()) { + writer.write("$L := $L", ident, expr.expectBooleanValue()); + return new Variable(STRING_SHAPE, ident, GoUniverseTypes.Bool); + } else { + throw new CodegenException("unhandled literal expression " + expr.getValue()); + } + } + + private Variable visitComparator(ComparatorExpression expr, Variable current) { var left = visit(expr.getLeft(), current); + var right = visit(expr.getRight(), current); + + String cast; + if (left.shape instanceof StringShape) { + cast = "string"; + } else if (left.shape instanceof NumberShape) { + cast = "int64"; + } else { + throw new CodegenException("don't know how to compare shape type" + left.shape.getType()); + } + + var ident = nextIdent(); + writer.write(compareVariables(ident, left, right, expr.getComparator(), cast)); + return new Variable(BOOL_SHAPE, ident, GoUniverseTypes.Bool); + } + + private Variable visitFlatten(FlattenExpression tExpr, Variable current) { + var inner = visit(tExpr.getExpression(), current); + + // inner HAS to be a list by spec, otherwise something is wrong + if (!(inner.shape instanceof CollectionShape innerList)) { + throw new CodegenException("left side of projection did not create a list"); + } + + // inner expression may not be a list-of-list - if so, we're done, the result is passed up as-is + var innerMember = expectMember(innerList); + if (!(innerMember instanceof CollectionShape)) { + return inner; + } + + var innerSymbol = ctx.symbolProvider().toSymbol(innerMember); + var ident = nextIdent(); + writer.write(""" + var $1L $3P + for _, v := range $2L { + $1L = append($1L, v...) + }""", ident, inner.ident, innerSymbol); + return new Variable(innerMember, ident, innerSymbol); + } + + private Variable visitProjection(ProjectionExpression expr, Variable current) { + var left = visit(expr.getLeft(), current); + if (expr.getRight() instanceof CurrentExpression) { // e.g. "Field[]" - the projection is just itself + return left; + } - // left of projection HAS to be an array by spec, otherwise something is wrong - if (!left.shape.isListShape()) { + Shape leftMember; + if (left.shape instanceof CollectionShape col) { + leftMember = expectMember(col); + } else if (left.shape instanceof MapShape map) { + leftMember = expectMember(map); + } else { + // left of projection HAS to be an array/map by spec, otherwise something is wrong throw new CodegenException("left side of projection did not create a list"); } - var leftMember = expectMember(ctx.model(), (ListShape) left.shape); + var leftSymbol = ctx.symbolProvider().toSymbol(leftMember); // We have to know the element type for the list that we're generating, use a dummy writer to "peek" ahead and // get the traversal result - var lookahead = new GoJmespathExpressionGenerator(ctx, new GoWriter(""), leftMember, expr.getRight()) - .generate("v"); + var lookahead = new GoJmespathExpressionGenerator(ctx, new GoWriter("")) + .generate(expr.getRight(), new Variable(leftMember, "v", leftSymbol)); - ++idIndex; + var ident = nextIdent(); + // projections implicitly filter out nil evaluations of RHS + var needsDeref = isPointable(lookahead.type); writer.write(""" - var v$L []$P - for _, v := range $L {""", idIndex, ctx.symbolProvider().toSymbol(lookahead.shape), left.ident); + var $L []$T + for _, v := range $L {""", ident, ctx.symbolProvider().toSymbol(lookahead.shape), left.ident); - // new scope inside loop, but now we actually want to write the contents + writer.indent(); // projected.shape is the _member_ of the resulting list - var projected = new GoJmespathExpressionGenerator(ctx, writer, leftMember, expr.getRight()) - .generate("v"); - - writer.write("v$1L = append(v$1L, $2L)", idIndex, projected.ident); + var projected = visit(expr.getRight(), new Variable(leftMember, "v", leftSymbol)); + if (needsDeref) { + writer.write(""" + if $2L != nil { + $1L = append($1L, *$2L) + }""", ident, projected.ident); + } else { + writer.write("$1L = append($1L, $2L)", ident, projected.ident); + } + writer.dedent(); writer.write("}"); - return new Result(listOf(projected.shape), "v" + idIndex); + return new Variable(listOf(projected.shape), ident, sliceOf(ctx.symbolProvider().toSymbol(projected.shape))); } - private Result visitSub(Subexpression expr, Shape current) { + private Variable visitSub(Subexpression expr, Variable current) { var left = visit(expr.getLeft(), current); - return visit(expr.getRight(), left.shape); + return visit(expr.getRight(), left); } - private Result visitField(FieldExpression expr, Shape current) { - ++idIndex; - writer.write("v$L := v$L.$L", idIndex, idIndex - 1, capitalize(expr.getName())); - return new Result(expectMember(ctx.model(), current, expr.getName()), "v" + idIndex); + private Variable visitField(FieldExpression expr, Variable current) { + var optMember = current.shape.getMember(expr.getName()); + if (optMember.isEmpty()) { + throw new CodegenException("expected member " + expr.getName() + " in shape " + current.shape); + } + + var member = optMember.get(); + var target = ctx.model().expectShape(member.getTarget()); + var ident = nextIdent(); + writer.write("$L := $L.$L", ident, current.ident, capitalize(expr.getName())); + return new Variable(target, ident, ctx.symbolProvider().toSymbol(member)); } - private Result visitFunction(FunctionExpression expr, Shape current) { + private Variable visitFunction(FunctionExpression expr, Variable current) { return switch (expr.name) { case "keys" -> visitKeysFunction(expr.arguments, current); + case "length" -> visitLengthFunction(expr.arguments, current); + case "contains" -> visitContainsFunction(expr.arguments, current); default -> throw new CodegenException("unsupported function " + expr.name); }; } - private Result visitKeysFunction(List args, Shape current) { + private Variable visitContainsFunction(List args, Variable current) { + if (args.size() != 2) { + throw new CodegenException("unexpected contains() arg length " + args.size()); + } + + var list = visit(args.get(0), current); + var item = visit(args.get(1), current); + var ident = nextIdent(); + writer.write(""" + var $1L bool + for _, v := range $2L { + if v == $3L { + $1L = true + break + } + }""", ident, list.ident, item.ident); + return new Variable(BOOL_SHAPE, ident, GoUniverseTypes.Bool); + } + + private Variable visitLengthFunction(List args, Variable current) { + if (args.size() != 1) { + throw new CodegenException("unexpected length() arg length " + args.size()); + } + + var arg = visit(args.get(0), current); + var ident = nextIdent(); + + // length() can be used on a string (so also *string) - dereference if required + if (arg.shape instanceof StringShape && isPointable(arg.type)) { + writer.write(""" + var _$1L string + if $1L != nil { + _$1L = *$1L + } + $2L := len(_$1L)""", arg.ident, ident); + } else { + writer.write("$L := len($L)", ident, arg.ident); + } + + return new Variable(INT_SHAPE, ident, GoUniverseTypes.Int); + } + + private Variable visitKeysFunction(List args, Variable current) { if (args.size() != 1) { throw new CodegenException("unexpected keys() arg length " + args.size()); } @@ -135,8 +345,71 @@ private Result visitKeysFunction(List args, Shape current) { v$1L = append(v$1L, k) }""", idIndex, arg.ident); - return new Result(listOf(STRING_SHAPE), "v" + idIndex); + return new Variable(listOf(STRING_SHAPE), "v" + idIndex, sliceOf(GoUniverseTypes.String)); + } + + private String nextIdent() { + ++idIndex; + return "v" + idIndex; + } + + private Shape expectMember(CollectionShape shape) { + return switch (shape.getMember().getTarget().toString()) { + case "smithy.go.synthetic#StringList" -> listOf(STRING_SHAPE); + case "smithy.go.synthetic#IntegerList" -> listOf(INT_SHAPE); + case "smithy.go.synthetic#BooleanList" -> listOf(BOOL_SHAPE); + default -> ShapeUtil.expectMember(ctx.model(), shape); + }; } - public record Result(Shape shape, String ident) {} + private Shape expectMember(MapShape shape) { + return switch (shape.getValue().getTarget().toString()) { + case "smithy.go.synthetic#StringList" -> listOf(STRING_SHAPE); + case "smithy.go.synthetic#IntegerList" -> listOf(INT_SHAPE); + case "smithy.go.synthetic#BooleanList" -> listOf(BOOL_SHAPE); + default -> ShapeUtil.expectMember(ctx.model(), shape); + }; + } + + // helper to generate comparisons from two results, automatically handling any dereferencing in the process + private GoWriter.Writable compareVariables(String ident, Variable left, Variable right, ComparatorType cmp, + String cast) { + var isLPtr = isPointable(left.type); + var isRPtr = isPointable(right.type); + if (!isLPtr && !isRPtr) { + return goTemplate("$1L := $5L($2L) $4L $5L($3L)", ident, left.ident, right.ident, cmp, cast); + } + + return goTemplate(""" + var $ident:L bool + if $lif:L $amp:L $rif:L { + $ident:L = $cast:L($lhs:L) $cmp:L $cast:L($rhs:L) + }""", + Map.of( + "ident", ident, + "lif", isLPtr ? left.ident + " != nil" : "", + "rif", isRPtr ? right.ident + " != nil" : "", + "amp", isLPtr && isRPtr ? "&&" : "", + "cmp", cmp, + "lhs", isLPtr ? "*" + left.ident : left.ident, + "rhs", isRPtr ? "*" + right.ident : right.ident, + "cast", cast + )); + } + + /** + * Represents a variable (input, intermediate, or final output) of a JMESPath traversal. + * @param shape The underlying shape referenced by this variable. For certain jmespath expressions (e.g. + * LiteralExpression) the value here is a synthetic shape and does not necessarily have meaning. + * @param ident The identifier of the variable in the generated traversal. + * @param type The symbol that records the type of the variable. This does NOT necessarily correspond to the result + * of toSymbol(shape) because certain jmespath expressions (such as projections) may affect the type of + * the resulting variable in a way that severs that relationship. The caller MUST use this field to + * determine whether the variable is pointable/nillable. + */ + public record Variable(Shape shape, String ident, Symbol type) { + public Variable(Shape shape, String ident) { + this(shape, ident, null); + } + } } diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/endpoints/EndpointParameterOperationBindingsGenerator.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/endpoints/EndpointParameterOperationBindingsGenerator.java index faf9980c..5a63aad4 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/endpoints/EndpointParameterOperationBindingsGenerator.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/endpoints/EndpointParameterOperationBindingsGenerator.java @@ -107,10 +107,10 @@ private GoWriter.Writable generateOpContextParamBinding(String paramName, Operat var expr = JmespathExpression.parse(def.getPath()); return writer -> { - var generator = new GoJmespathExpressionGenerator(ctx, writer, input, expr); + var generator = new GoJmespathExpressionGenerator(ctx, writer); writer.write("func() {"); // contain the scope for each binding - var result = generator.generate("in"); + var result = generator.generate(expr, new GoJmespathExpressionGenerator.Variable(input, "in")); if (param.getType().equals(ParameterType.STRING_ARRAY)) { // projections can result in either []string OR []*string -- if the latter, we have to unwrap diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/Waiters.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/Waiters.java index b23be030..02ce3a29 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/Waiters.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/Waiters.java @@ -18,6 +18,7 @@ import static java.util.Collections.emptySet; import static software.amazon.smithy.go.codegen.GoWriter.autoDocTemplate; import static software.amazon.smithy.go.codegen.GoWriter.goTemplate; +import static software.amazon.smithy.go.codegen.SymbolUtils.isPointable; import java.util.Map; import java.util.Optional; @@ -27,18 +28,16 @@ import software.amazon.smithy.codegen.core.SymbolProvider; import software.amazon.smithy.go.codegen.ClientOptions; import software.amazon.smithy.go.codegen.GoCodegenContext; +import software.amazon.smithy.go.codegen.GoJmespathExpressionGenerator; import software.amazon.smithy.go.codegen.GoWriter; import software.amazon.smithy.go.codegen.SmithyGoDependency; import software.amazon.smithy.go.codegen.SymbolUtils; +import software.amazon.smithy.jmespath.JmespathExpression; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.knowledge.TopDownIndex; -import software.amazon.smithy.model.shapes.ListShape; -import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.OperationShape; -import software.amazon.smithy.model.shapes.ServiceShape; import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.ShapeId; -import software.amazon.smithy.model.shapes.SimpleShape; import software.amazon.smithy.model.shapes.StructureShape; import software.amazon.smithy.utils.StringUtils; import software.amazon.smithy.waiters.Acceptor; @@ -80,14 +79,13 @@ public void writeAdditionalFiles(GoCodegenContext ctx) { private void generateOperationWaiter(GoCodegenContext ctx, OperationShape operation, Map waiters) { var model = ctx.model(); var symbolProvider = ctx.symbolProvider(); - var service = ctx.settings().getService(model); ctx.writerDelegator().useShapeWriter(operation, writer -> { waiters.forEach((name, waiter) -> { generateWaiterOptions(model, symbolProvider, writer, operation, name, waiter); generateWaiterClient(model, symbolProvider, writer, operation, name, waiter); generateWaiterInvoker(model, symbolProvider, writer, operation, name, waiter); generateWaiterInvokerWithOutput(model, symbolProvider, writer, operation, name, waiter); - generateRetryable(model, symbolProvider, writer, service, operation, name, waiter); + generateRetryable(ctx, writer, operation, name, waiter); }); }); } @@ -495,23 +493,22 @@ private void generateWaiterInvokerWithOutput( * Generates a waiter state mutator function which is used by the waiter retrier Middleware to mutate * waiter state as per the defined logic and returned operation response. * - * @param model the smithy model - * @param symbolProvider symbol provider + * @param ctx the GoCodegenContext * @param writer the Gowriter - * @param serviceShape service shape for which operation waiter is modeled * @param operationShape operation shape on which the waiter is modeled * @param waiterName the waiter name * @param waiter the waiter structure that contains info on modeled waiter */ private void generateRetryable( - Model model, - SymbolProvider symbolProvider, + GoCodegenContext ctx, GoWriter writer, - ServiceShape serviceShape, OperationShape operationShape, String waiterName, Waiter waiter ) { + var model = ctx.model(); + var symbolProvider = ctx.symbolProvider(); + var serviceShape = ctx.settings().getService(model); StructureShape inputShape = model.expectShape( operationShape.getInput().get(), StructureShape.class ); @@ -531,7 +528,6 @@ private void generateRetryable( Matcher matcher = acceptor.getMatcher(); switch (matcher.getMemberName()) { case "output": - writer.addUseImports(SmithyGoDependency.GO_JMESPATH); writer.addUseImports(SmithyGoDependency.FMT); Matcher.OutputMember outputMember = (Matcher.OutputMember) matcher; @@ -539,39 +535,12 @@ private void generateRetryable( String expectedValue = outputMember.getValue().getExpected(); PathComparator comparator = outputMember.getValue().getComparator(); writer.openBlock("if err == nil {", "}", () -> { - writer.write("pathValue, err := jmespath.Search($S, output)", path); - writer.openBlock("if err != nil {", "}", () -> { - writer.write( - "return false, " - + "fmt.Errorf(\"error evaluating waiter state: %w\", err)"); - }).write(""); - writer.write("expectedValue := $S", expectedValue); + var pathInput = new GoJmespathExpressionGenerator.Variable(outputShape, "output"); + var searchResult = new GoJmespathExpressionGenerator(ctx, writer) + .generate(JmespathExpression.parse(path), pathInput); - if (comparator == PathComparator.BOOLEAN_EQUALS) { - writeWaiterComparator(writer, acceptor, comparator, null, "pathValue", - "expectedValue"); - } else { - String[] pathMembers = path.split("\\."); - Shape targetShape = outputShape; - for (int i = 0; i < pathMembers.length; i++) { - MemberShape member = getComparedMember(model, targetShape, pathMembers[i]); - if (member == null) { - targetShape = null; - break; - } - targetShape = model.expectShape(member.getTarget()); - } - - if (targetShape == null) { - writeWaiterComparator(writer, acceptor, comparator, null, "pathValue", - "expectedValue"); - } else { - Symbol targetSymbol = symbolProvider.toSymbol(targetShape); - writeWaiterComparator(writer, acceptor, comparator, targetSymbol, - "pathValue", - "expectedValue"); - } - } + writer.write("expectedValue := $S", expectedValue); + writeWaiterComparator(writer, acceptor, comparator, searchResult); }); break; @@ -583,22 +552,32 @@ private void generateRetryable( path = ioMember.getValue().getPath(); expectedValue = ioMember.getValue().getExpected(); comparator = ioMember.getValue().getComparator(); + + // inputOutput matchers operate on a synthetic structure with operation input and output + // as top-level fields - we set that up here both in codegen for jmespathing and for + // the actual generated code to work + var inputOutputShape = StructureShape.builder() + .addMember("input", inputShape.toShapeId()) + .addMember("output", outputShape.toShapeId()) + .build(); + writer.write(""" + inputOutput := struct{ + Input $P + Output $P + }{ + Input: input, + Output: output, + } + """); + writer.openBlock("if err == nil {", "}", () -> { - writer.openBlock("pathValue, err := jmespath.Search($S, &struct{", - "})", path, () -> { - writer.write("Input $P \n Output $P \n }{", inputSymbol, - outputSymbol); - writer.write("Input: input, \n Output: output, \n"); - }); - writer.openBlock("if err != nil {", "}", () -> { - writer.write( - "return false, " - + "fmt.Errorf(\"error evaluating waiter state: %w\", err)"); - }); - writer.write(""); + var pathInput = new GoJmespathExpressionGenerator.Variable( + inputOutputShape, "inputOutput"); + var searchResult = new GoJmespathExpressionGenerator(ctx, writer) + .generate(JmespathExpression.parse(path), pathInput); + writer.write("expectedValue := $S", expectedValue); - writeWaiterComparator(writer, acceptor, comparator, outputSymbol, "pathValue", - "expectedValue"); + writeWaiterComparator(writer, acceptor, comparator, searchResult); }); break; @@ -666,87 +645,44 @@ private void generateRetryable( }); } - /** - * writes comparators for a given waiter. The comparators are defined within the waiter acceptor. - * - * @param writer the Gowriter - * @param acceptor the waiter acceptor that defines the comparator and acceptor states - * @param comparator the comparator - * @param targetSymbol the shape symbol of the compared type. - * @param actual the variable carrying the actual value obtained. - * This may be computed via a jmespath expression or operation response status (success/failure) - * @param expected the variable carrying the expected value. This value is as per the modeled waiter. - */ - private void writeWaiterComparator( - GoWriter writer, - Acceptor acceptor, - PathComparator comparator, - Symbol targetSymbol, - String actual, - String expected - ) { - if (targetSymbol == null) { - targetSymbol = SymbolUtils.createValueSymbolBuilder("string").build(); - } - - String valueAccessor = "string(value)"; - Optional isPointable = targetSymbol.getProperty(SymbolUtils.POINTABLE, Boolean.class); - if (isPointable.isPresent() && isPointable.get().booleanValue()) { - valueAccessor = "string(*value)"; - } - + private void writeWaiterComparator(GoWriter writer, Acceptor acceptor, PathComparator comparator, + GoJmespathExpressionGenerator.Variable searchResult) { switch (comparator) { case STRING_EQUALS: - writer.write("value, ok := $L.($P)", actual, targetSymbol); - writer.write("if !ok {"); - writer.write("return false, fmt.Errorf(\"waiter comparator expected $P value, got %T\", $L)}", - targetSymbol, actual); - writer.write(""); - - writer.openBlock("if $L == $L {", "}", valueAccessor, expected, () -> { + writer.write("var pathValue string"); + if (!isPointable(searchResult.type())) { + writer.write("pathValue = string($L)", searchResult.ident()); + } else { + writer.write(""" + if $1L != nil { + pathValue = string(*$1L) + }""", searchResult.ident()); + } + writer.openBlock("if pathValue == expectedValue {", "}", () -> { writeMatchedAcceptorReturn(writer, acceptor); }); break; case BOOLEAN_EQUALS: writer.addUseImports(SmithyGoDependency.STRCONV); - writer.write("bv, err := strconv.ParseBool($L)", expected); + writer.write("bv, err := strconv.ParseBool($L)", "expectedValue"); writer.write( "if err != nil { return false, " + "fmt.Errorf(\"error parsing boolean from string %w\", err)}"); - writer.write("value, ok := $L.(bool)", actual); - writer.openBlock(" if !ok {", "}", () -> { - writer.write("return false, " - + "fmt.Errorf(\"waiter comparator expected bool value got %T\", $L)", actual); - }); - writer.write(""); - - writer.openBlock("if value == bv {", "}", () -> { + writer.openBlock("if $L == bv {", "}", searchResult.ident(), () -> { writeMatchedAcceptorReturn(writer, acceptor); }); break; case ALL_STRING_EQUALS: - writer.write("var match = true"); - writer.write("listOfValues, ok := $L.([]interface{})", actual); - writer.openBlock(" if !ok {", "}", () -> { - writer.write("return false, " - + "fmt.Errorf(\"waiter comparator expected list got %T\", $L)", actual); - }); - writer.write(""); - - writer.write("if len(listOfValues) == 0 { match = false }"); - - String allStringValueAccessor = valueAccessor; - Symbol allStringTargetSymbol = targetSymbol; - writer.openBlock("for _, v := range listOfValues {", "}", () -> { - writer.write("value, ok := v.($P)", allStringTargetSymbol); - writer.write("if !ok {"); - writer.write("return false, fmt.Errorf(\"waiter comparator expected $P value, got %T\", $L)}", - allStringTargetSymbol, actual); - writer.write(""); - writer.write("if $L != $L { match = false }", allStringValueAccessor, expected); + writer.write("match := len($L) > 0", searchResult.ident()); + writer.openBlock("for _, v := range $L {", "}", searchResult.ident(), () -> { + writer.write(""" + if string(v) != expectedValue { + match = false + break + }"""); }); writer.write(""); @@ -756,24 +692,18 @@ private void writeWaiterComparator( break; case ANY_STRING_EQUALS: - writer.write("listOfValues, ok := $L.([]interface{})", actual); - writer.openBlock(" if !ok {", "}", () -> { - writer.write("return false, " - + "fmt.Errorf(\"waiter comparator expected list got %T\", $L)", actual); + writer.write("var match bool"); + writer.openBlock("for _, v := range $L {", "}", searchResult.ident(), () -> { + writer.write(""" + if string(v) == expectedValue { + match = true + break + }"""); }); writer.write(""); - String anyStringValueAccessor = valueAccessor; - Symbol anyStringTargetSymbol = targetSymbol; - writer.openBlock("for _, v := range listOfValues {", "}", () -> { - writer.write("value, ok := v.($P)", anyStringTargetSymbol); - writer.write("if !ok {"); - writer.write("return false, fmt.Errorf(\"waiter comparator expected $P value, got %T\", $L)}", - anyStringTargetSymbol, actual); - writer.write(""); - writer.openBlock("if $L == $L {", "}", anyStringValueAccessor, expected, () -> { - writeMatchedAcceptorReturn(writer, acceptor); - }); + writer.openBlock("if match {", "}", () -> { + writeMatchedAcceptorReturn(writer, acceptor); }); break; @@ -830,50 +760,4 @@ private String generateRetryableName( waiterName = StringUtils.uncapitalize(waiterName); return String.format("%sStateRetryable", waiterName); } - - - /** - * Returns the MemberShape wrt to the provided Shape and name. - * For eg, If shape `A` has MemberShape `B`, and the name provided is `B` as string. - * We return the MemberShape `B`. - * - * @param model the generation model. - * @param shape the shape that is walked to retreive the shape matching provided name. - * @param name name is a single scope path string, and should only match to one or less shapes. - * @return MemberShape matching the name. - */ - private MemberShape getComparedMember(Model model, Shape shape, String name) { - - name = name.replaceAll("\\[\\]", ""); - - // if shape is a simple shape, just return shape as member shape - if (shape instanceof SimpleShape) { - return shape.asMemberShape().get(); - } - - switch (shape.getType()) { - case STRUCTURE: - StructureShape st = shape.asStructureShape().get(); - for (MemberShape memberShape : st.getAllMembers().values()) { - if (name.equalsIgnoreCase(memberShape.getMemberName())) { - return memberShape; - } - } - break; - - case LIST: - ListShape listShape = shape.asListShape().get(); - MemberShape listMember = listShape.getMember(); - Shape listTarget = model.expectShape(listMember.getTarget()); - return getComparedMember(model, listTarget, name); - - default: - // TODO: add support for * usage with jmespath expression. - return null; - } - - // TODO: add support for * usage with jmespath expression. - // return null if no shape type matched (this would happen in case of * usage with jmespath expression). - return null; - } } diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/util/ShapeUtil.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/util/ShapeUtil.java index 68944603..fc03945a 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/util/ShapeUtil.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/util/ShapeUtil.java @@ -17,8 +17,11 @@ import software.amazon.smithy.codegen.core.CodegenException; import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.BooleanShape; import software.amazon.smithy.model.shapes.CollectionShape; +import software.amazon.smithy.model.shapes.IntegerShape; import software.amazon.smithy.model.shapes.ListShape; +import software.amazon.smithy.model.shapes.MapShape; import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.StringShape; @@ -27,6 +30,14 @@ public final class ShapeUtil { .id("smithy.go.synthetic#String") .build(); + public static final IntegerShape INT_SHAPE = IntegerShape.builder() + .id("smithy.api#Integer") + .build(); + + public static final BooleanShape BOOL_SHAPE = BooleanShape.builder() + .id("smithy.api#Boolean") + .build(); + private ShapeUtil() {} public static ListShape listOf(Shape member) { @@ -49,4 +60,8 @@ public static Shape expectMember(Model model, Shape shape, String memberName) { public static Shape expectMember(Model model, CollectionShape shape) { return model.expectShape(shape.getMember().getTarget()); } + + public static Shape expectMember(Model model, MapShape shape) { + return model.expectShape(shape.getValue().getTarget()); + } } diff --git a/codegen/smithy-go-codegen/src/test/java/software/amazon/smithy/go/codegen/GoJmespathExpressionGeneratorTest.java b/codegen/smithy-go-codegen/src/test/java/software/amazon/smithy/go/codegen/GoJmespathExpressionGeneratorTest.java index aecd1060..0e4e1e72 100644 --- a/codegen/smithy-go-codegen/src/test/java/software/amazon/smithy/go/codegen/GoJmespathExpressionGeneratorTest.java +++ b/codegen/smithy-go-codegen/src/test/java/software/amazon/smithy/go/codegen/GoJmespathExpressionGeneratorTest.java @@ -16,6 +16,7 @@ package software.amazon.smithy.go.codegen; import static org.hamcrest.MatcherAssert.assertThat; +import static software.amazon.smithy.go.codegen.util.ShapeUtil.listOf; import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; @@ -33,8 +34,12 @@ public class GoJmespathExpressionGeneratorTest { namespace smithy.go.test + service Test { + } + structure Struct { simpleShape: String + simpleShape2: String objectList: ObjectList objectMap: ObjectMap nested: NestedStruct @@ -42,6 +47,11 @@ public class GoJmespathExpressionGeneratorTest { structure Object { key: String + innerObjectList: InnerObjectList + } + + structure InnerObject { + innerKey: String } structure NestedStruct { @@ -52,6 +62,10 @@ public class GoJmespathExpressionGeneratorTest { member: Object } + list InnerObjectList { + member: InnerObject + } + map ObjectMap { key: String, value: Object @@ -63,10 +77,14 @@ public class GoJmespathExpressionGeneratorTest { .assemble().unwrap(); private static final GoSettings TEST_SETTINGS = GoSettings.from(ObjectNode.fromStringMap(Map.of( - "service", "smithy.go.test#foo", + "service", "smithy.go.test#Test", "module", "github.com/aws/aws-sdk-go-v2/test" ))); + private static GoWriter testWriter() { + return new GoWriter("test").setIndentText(" "); // for ease of string comparison + } + private static GoCodegenContext testContext() { return new GoCodegenContext( TEST_MODEL, TEST_SETTINGS, @@ -79,17 +97,16 @@ private static GoCodegenContext testContext() { public void testFieldExpression() { var expr = "simpleShape"; - var writer = new GoWriter("test"); - var generator = new GoJmespathExpressionGenerator(testContext(), writer, + var writer = testWriter(); + var generator = new GoJmespathExpressionGenerator(testContext(), writer); + var actual = generator.generate(JmespathExpression.parse(expr), new GoJmespathExpressionGenerator.Variable( TEST_MODEL.expectShape(ShapeId.from("smithy.go.test#Struct")), - JmespathExpression.parse(expr) - ); - var actual = generator.generate("input"); + "input" + )); assertThat(actual.shape(), Matchers.equalTo(TEST_MODEL.expectShape(ShapeId.from("smithy.api#String")))); - assertThat(actual.ident(), Matchers.equalTo("v2")); + assertThat(actual.ident(), Matchers.equalTo("v1")); assertThat(writer.toString(), Matchers.containsString(""" - v1 := input - v2 := v1.SimpleShape + v1 := input.SimpleShape """)); } @@ -97,18 +114,17 @@ public void testFieldExpression() { public void testSubexpression() { var expr = "nested.nestedField"; - var writer = new GoWriter("test"); - var generator = new GoJmespathExpressionGenerator(testContext(), writer, + var writer = testWriter(); + var generator = new GoJmespathExpressionGenerator(testContext(), writer); + var actual = generator.generate(JmespathExpression.parse(expr), new GoJmespathExpressionGenerator.Variable( TEST_MODEL.expectShape(ShapeId.from("smithy.go.test#Struct")), - JmespathExpression.parse(expr) - ); - var actual = generator.generate("input"); + "input" + )); assertThat(actual.shape(), Matchers.equalTo(TEST_MODEL.expectShape(ShapeId.from("smithy.api#String")))); - assertThat(actual.ident(), Matchers.equalTo("v3")); + assertThat(actual.ident(), Matchers.equalTo("v2")); assertThat(writer.toString(), Matchers.containsString(""" - v1 := input - v2 := v1.Nested - v3 := v2.NestedField + v1 := input.Nested + v2 := v1.NestedField """)); } @@ -116,20 +132,19 @@ public void testSubexpression() { public void testKeysFunctionExpression() { var expr = "keys(objectMap)"; - var writer = new GoWriter("test"); - var generator = new GoJmespathExpressionGenerator(testContext(), writer, + var writer = testWriter(); + var generator = new GoJmespathExpressionGenerator(testContext(), writer); + var actual = generator.generate(JmespathExpression.parse(expr), new GoJmespathExpressionGenerator.Variable( TEST_MODEL.expectShape(ShapeId.from("smithy.go.test#Struct")), - JmespathExpression.parse(expr) - ); - var actual = generator.generate("input"); - assertThat(actual.shape(), Matchers.equalTo(ShapeUtil.listOf(ShapeUtil.STRING_SHAPE))); - assertThat(actual.ident(), Matchers.equalTo("v3")); + "input" + )); + assertThat(actual.shape(), Matchers.equalTo(listOf(ShapeUtil.STRING_SHAPE))); + assertThat(actual.ident(), Matchers.equalTo("v2")); assertThat(writer.toString(), Matchers.containsString(""" - v1 := input - v2 := v1.ObjectMap - var v3 []string - for k := range v2 { - v3 = append(v3, k) + v1 := input.ObjectMap + var v2 []string + for k := range v1 { + v2 = append(v2, k) } """)); } @@ -138,24 +153,343 @@ public void testKeysFunctionExpression() { public void testProjectionExpression() { var expr = "objectList[*].key"; - var writer = new GoWriter("test"); - var generator = new GoJmespathExpressionGenerator(testContext(), writer, + var writer = testWriter(); + var generator = new GoJmespathExpressionGenerator(testContext(), writer); + var actual = generator.generate(JmespathExpression.parse(expr), new GoJmespathExpressionGenerator.Variable( TEST_MODEL.expectShape(ShapeId.from("smithy.go.test#Struct")), - JmespathExpression.parse(expr) - ); - var actual = generator.generate("input"); + "input" + )); assertThat(actual.shape(), Matchers.equalTo( - ShapeUtil.listOf(TEST_MODEL.expectShape(ShapeId.from("smithy.api#String"))))); - assertThat(actual.ident(), Matchers.equalTo("v3")); + listOf(TEST_MODEL.expectShape(ShapeId.from("smithy.api#String"))))); + assertThat(actual.ident(), Matchers.equalTo("v2")); assertThat(writer.toString(), Matchers.containsString(""" - v1 := input - v2 := v1.ObjectList - var v3 []*string + v1 := input.ObjectList + var v2 []string + for _, v := range v1 { + v3 := v.Key + if v3 != nil { + v2 = append(v2, *v3) + } + } + """)); + } + + @Test + public void testNopFlattenExpression() { + var expr = "objectList[].key"; + + var writer = testWriter(); + var generator = new GoJmespathExpressionGenerator(testContext(), writer); + var actual = generator.generate(JmespathExpression.parse(expr), new GoJmespathExpressionGenerator.Variable( + TEST_MODEL.expectShape(ShapeId.from("smithy.go.test#Struct")), + "input" + )); + assertThat(actual.shape(), Matchers.equalTo( + listOf(TEST_MODEL.expectShape(ShapeId.from("smithy.api#String"))))); + assertThat(actual.ident(), Matchers.equalTo("v2")); + assertThat(writer.toString(), Matchers.containsString(""" + v1 := input.ObjectList + var v2 []string + for _, v := range v1 { + v3 := v.Key + if v3 != nil { + v2 = append(v2, *v3) + } + } + """)); + } + + @Test + public void testActualFlattenExpression() { + var expr = "objectList[].innerObjectList[].innerKey"; + + var writer = testWriter(); + var generator = new GoJmespathExpressionGenerator(testContext(), writer); + var actual = generator.generate(JmespathExpression.parse(expr), new GoJmespathExpressionGenerator.Variable( + TEST_MODEL.expectShape(ShapeId.from("smithy.go.test#Struct")), + "input" + )); + assertThat(actual.shape(), Matchers.equalTo( + listOf(TEST_MODEL.expectShape(ShapeId.from("smithy.api#String"))))); + assertThat(actual.ident(), Matchers.equalTo("v5")); + assertThat(writer.toString(), Matchers.containsString(""" + v1 := input.ObjectList + var v2 [][]types.InnerObject + for _, v := range v1 { + v3 := v.InnerObjectList + v2 = append(v2, v3) + } + var v4 []types.InnerObject for _, v := range v2 { - v1 := v - v2 := v1.Key - v3 = append(v3, v2) + v4 = append(v4, v...) + } + var v5 []string + for _, v := range v4 { + v6 := v.InnerKey + if v6 != nil { + v5 = append(v5, *v6) + } } """)); } + + @Test + public void testLengthFunctionExpression() { + var expr = "length(objectList)"; + + var writer = testWriter(); + var generator = new GoJmespathExpressionGenerator(testContext(), writer); + var actual = generator.generate(JmespathExpression.parse(expr), new GoJmespathExpressionGenerator.Variable( + TEST_MODEL.expectShape(ShapeId.from("smithy.go.test#Struct")), + "input" + )); + assertThat(actual.shape(), Matchers.equalTo(ShapeUtil.INT_SHAPE)); + assertThat(actual.ident(), Matchers.equalTo("v2")); + assertThat(writer.toString(), Matchers.containsString(""" + v1 := input.ObjectList + v2 := len(v1) + """)); + } + + @Test + public void testLengthFunctionStringPtr() { + var expr = "length(simpleShape)"; + + var writer = testWriter(); + var generator = new GoJmespathExpressionGenerator(testContext(), writer); + var actual = generator.generate(JmespathExpression.parse(expr), new GoJmespathExpressionGenerator.Variable( + TEST_MODEL.expectShape(ShapeId.from("smithy.go.test#Struct")), + "input" + )); + assertThat(actual.shape(), Matchers.equalTo(ShapeUtil.INT_SHAPE)); + assertThat(actual.ident(), Matchers.equalTo("v2")); + assertThat(writer.toString(), Matchers.containsString(""" + v1 := input.SimpleShape + var _v1 string + if v1 != nil { + _v1 = *v1 + } + v2 := len(_v1) + """)); + } + + @Test + public void testComparatorInt() { + var expr = "length(objectList) > `0`"; + + var writer = testWriter(); + var generator = new GoJmespathExpressionGenerator(testContext(), writer); + var actual = generator.generate(JmespathExpression.parse(expr), new GoJmespathExpressionGenerator.Variable( + TEST_MODEL.expectShape(ShapeId.from("smithy.go.test#Struct")), + "input" + )); + assertThat(actual.shape(), Matchers.equalTo(ShapeUtil.BOOL_SHAPE)); + assertThat(actual.ident(), Matchers.equalTo("v4")); + assertThat(writer.toString(), Matchers.containsString(""" + v1 := input.ObjectList + v2 := len(v1) + v3 := 0 + v4 := int64(v2) > int64(v3) + """)); + } + + @Test + public void testComparatorStringLHSNil() { + var expr = "nested.nestedField == 'foo'"; + + var writer = testWriter(); + var generator = new GoJmespathExpressionGenerator(testContext(), writer); + var actual = generator.generate(JmespathExpression.parse(expr), new GoJmespathExpressionGenerator.Variable( + TEST_MODEL.expectShape(ShapeId.from("smithy.go.test#Struct")), + "input" + )); + assertThat(actual.shape(), Matchers.equalTo(ShapeUtil.BOOL_SHAPE)); + assertThat(actual.ident(), Matchers.equalTo("v4")); + assertThat(writer.toString(), Matchers.containsString(""" + v1 := input.Nested + v2 := v1.NestedField + v3 := "foo" + var v4 bool + if v2 != nil { + v4 = string(*v2) == string(v3) + } + """)); + } + + @Test + public void testComparatorStringRHSNil() { + var expr = "'foo' == nested.nestedField"; + + var writer = testWriter(); + var generator = new GoJmespathExpressionGenerator(testContext(), writer); + var actual = generator.generate(JmespathExpression.parse(expr), new GoJmespathExpressionGenerator.Variable( + TEST_MODEL.expectShape(ShapeId.from("smithy.go.test#Struct")), + "input" + )); + assertThat(actual.shape(), Matchers.equalTo(ShapeUtil.BOOL_SHAPE)); + assertThat(actual.ident(), Matchers.equalTo("v4")); + assertThat(writer.toString(), Matchers.containsString(""" + v1 := "foo" + v2 := input.Nested + v3 := v2.NestedField + var v4 bool + if v3 != nil { + v4 = string(v1) == string(*v3) + } + """)); + } + + @Test + public void testComparatorStringBothNil() { + var expr = "nested.nestedField == simpleShape"; + + var writer = testWriter(); + var generator = new GoJmespathExpressionGenerator(testContext(), writer); + var actual = generator.generate(JmespathExpression.parse(expr), new GoJmespathExpressionGenerator.Variable( + TEST_MODEL.expectShape(ShapeId.from("smithy.go.test#Struct")), + "input" + )); + assertThat(actual.shape(), Matchers.equalTo(ShapeUtil.BOOL_SHAPE)); + assertThat(actual.ident(), Matchers.equalTo("v4")); + assertThat(writer.toString(), Matchers.containsString(""" + v1 := input.Nested + v2 := v1.NestedField + v3 := input.SimpleShape + var v4 bool + if v2 != nil && v3 != nil { + v4 = string(*v2) == string(*v3) + } + """)); + } + + @Test + public void testContainsFunctionExpression() { + var expr = "contains(objectList[].key, 'foo')"; + + var writer = testWriter(); + var generator = new GoJmespathExpressionGenerator(testContext(), writer); + var actual = generator.generate(JmespathExpression.parse(expr), new GoJmespathExpressionGenerator.Variable( + TEST_MODEL.expectShape(ShapeId.from("smithy.go.test#Struct")), + "input" + )); + assertThat(actual.shape(), Matchers.equalTo(ShapeUtil.BOOL_SHAPE)); + assertThat(actual.ident(), Matchers.equalTo("v5")); + assertThat(writer.toString(), Matchers.containsString(""" + v1 := input.ObjectList + var v2 []string + for _, v := range v1 { + v3 := v.Key + if v3 != nil { + v2 = append(v2, *v3) + } + } + v4 := "foo" + var v5 bool + for _, v := range v2 { + if v == v4 { + v5 = true + break + } + } + """)); + } + + @Test + public void testAndExpression() { + var expr = "length(objectList) > `0` && length(objectList) <= `10`"; + + var writer = testWriter(); + var generator = new GoJmespathExpressionGenerator(testContext(), writer); + var actual = generator.generate(JmespathExpression.parse(expr), new GoJmespathExpressionGenerator.Variable( + TEST_MODEL.expectShape(ShapeId.from("smithy.go.test#Struct")), + "input" + )); + assertThat(actual.shape(), Matchers.equalTo(ShapeUtil.BOOL_SHAPE)); + assertThat(actual.ident(), Matchers.equalTo("v9")); + assertThat(writer.toString(), Matchers.containsString(""" + v1 := input.ObjectList + v2 := len(v1) + v3 := 0 + v4 := int64(v2) > int64(v3) + v5 := input.ObjectList + v6 := len(v5) + v7 := 10 + v8 := int64(v6) <= int64(v7) + v9 := v4 && v8 + """)); + } + + @Test + public void testFilterExpression() { + var expr = "objectList[?length(innerObjectList) > `0`]"; + + var writer = testWriter(); + var generator = new GoJmespathExpressionGenerator(testContext(), writer); + var actual = generator.generate(JmespathExpression.parse(expr), new GoJmespathExpressionGenerator.Variable( + TEST_MODEL.expectShape(ShapeId.from("smithy.go.test#Struct")), + "input" + )); + assertThat(actual.shape(), Matchers.equalTo(TEST_MODEL.expectShape(ShapeId.from("smithy.go.test#ObjectList")))); + assertThat(actual.ident(), Matchers.equalTo("v2")); + assertThat(writer.toString(), Matchers.containsString(""" + v1 := input.ObjectList + var v2 []types.Object + for _, v := range v1 { + v3 := v.InnerObjectList + v4 := len(v3) + v5 := 0 + v6 := int64(v4) > int64(v5) + if v6 { + v2 = append(v2, v) + } + } + """)); + } + + @Test + public void testNot() { + var expr = "objectList[?!(length(innerObjectList) > `0`)]"; + + var writer = testWriter(); + var generator = new GoJmespathExpressionGenerator(testContext(), writer); + var actual = generator.generate(JmespathExpression.parse(expr), new GoJmespathExpressionGenerator.Variable( + TEST_MODEL.expectShape(ShapeId.from("smithy.go.test#Struct")), + "input" + )); + assertThat(actual.shape(), Matchers.equalTo(TEST_MODEL.expectShape(ShapeId.from("smithy.go.test#ObjectList")))); + assertThat(actual.ident(), Matchers.equalTo("v2")); + assertThat(writer.toString(), Matchers.containsString(""" + v1 := input.ObjectList + var v2 []types.Object + for _, v := range v1 { + v3 := v.InnerObjectList + v4 := len(v3) + v5 := 0 + v6 := int64(v4) > int64(v5) + v7 := !v6 + if v7 { + v2 = append(v2, v) + } + } + """)); + } + + @Test + public void testMultiSelect() { + var expr = "[simpleShape, simpleShape2]"; + + var writer = testWriter(); + var generator = new GoJmespathExpressionGenerator(testContext(), writer); + var actual = generator.generate(JmespathExpression.parse(expr), new GoJmespathExpressionGenerator.Variable( + TEST_MODEL.expectShape(ShapeId.from("smithy.go.test#Struct")), + "input" + )); + assertThat(actual.shape().toShapeId().toString(), Matchers.equalTo("smithy.go.synthetic#StringList")); + assertThat(actual.ident(), Matchers.equalTo("v3")); + assertThat(writer.toString(), Matchers.containsString(""" + v1 := input.SimpleShape + v2 := input.SimpleShape2 + v3 := []*string{v1,v2} + """)); + } }