From 737b80b6b65aafee228a979e5286ee43a6213b23 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Wed, 17 Dec 2025 09:12:33 -0800 Subject: [PATCH 1/8] WIP - start adding generator --- .../EndpointResolverInterceptorSpec.java | 2 + .../OperationContextParamsGenerator.java | 221 ++++++++++++++++++ 2 files changed, 223 insertions(+) create mode 100644 codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/OperationContextParamsGenerator.java diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverInterceptorSpec.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverInterceptorSpec.java index 60f30fc3fccd..14a451adb59e 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverInterceptorSpec.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverInterceptorSpec.java @@ -553,6 +553,8 @@ private MethodSpec setOperationContextParamsMethod(OperationModel opModel) { String setterName = endpointRulesSpecUtils.paramMethodName(key); String jmesPathString = ((JrsString) value.getPath()).getValue(); + OperationContextParamsGenerator gen = new OperationContextParamsGenerator(jmesPathString, opModel); + gen.generate(); CodeBlock addParam = CodeBlock.builder() .add("params.$N(", setterName) .add(jmesPathGenerator.interpret(jmesPathString, "input")) diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/OperationContextParamsGenerator.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/OperationContextParamsGenerator.java new file mode 100644 index 000000000000..2c25239a1cc8 --- /dev/null +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/OperationContextParamsGenerator.java @@ -0,0 +1,221 @@ +/* + * Copyright 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.awssdk.codegen.poet.rules; + +import com.squareup.javapoet.CodeBlock; +import java.util.ArrayDeque; +import java.util.Deque; +import software.amazon.awssdk.codegen.jmespath.component.AndExpression; +import software.amazon.awssdk.codegen.jmespath.component.BracketSpecifier; +import software.amazon.awssdk.codegen.jmespath.component.BracketSpecifierWithContents; +import software.amazon.awssdk.codegen.jmespath.component.BracketSpecifierWithQuestionMark; +import software.amazon.awssdk.codegen.jmespath.component.BracketSpecifierWithoutContents; +import software.amazon.awssdk.codegen.jmespath.component.ComparatorExpression; +import software.amazon.awssdk.codegen.jmespath.component.CurrentNode; +import software.amazon.awssdk.codegen.jmespath.component.Expression; +import software.amazon.awssdk.codegen.jmespath.component.ExpressionType; +import software.amazon.awssdk.codegen.jmespath.component.FunctionExpression; +import software.amazon.awssdk.codegen.jmespath.component.IndexExpression; +import software.amazon.awssdk.codegen.jmespath.component.Literal; +import software.amazon.awssdk.codegen.jmespath.component.MultiSelectHash; +import software.amazon.awssdk.codegen.jmespath.component.MultiSelectList; +import software.amazon.awssdk.codegen.jmespath.component.NotExpression; +import software.amazon.awssdk.codegen.jmespath.component.OrExpression; +import software.amazon.awssdk.codegen.jmespath.component.ParenExpression; +import software.amazon.awssdk.codegen.jmespath.component.PipeExpression; +import software.amazon.awssdk.codegen.jmespath.component.SliceExpression; +import software.amazon.awssdk.codegen.jmespath.component.SubExpression; +import software.amazon.awssdk.codegen.jmespath.component.SubExpressionRight; +import software.amazon.awssdk.codegen.jmespath.component.WildcardExpression; +import software.amazon.awssdk.codegen.jmespath.parser.JmesPathParser; +import software.amazon.awssdk.codegen.jmespath.parser.JmesPathVisitor; +import software.amazon.awssdk.codegen.model.intermediate.OperationModel; + +public class OperationContextParamsGenerator { + private final String pathExpression; + private final OperationModel opModel; + private final Expression parsedPathExpression; + + public OperationContextParamsGenerator(String pathExpression, OperationModel opModel) { + this.pathExpression = pathExpression; + this.parsedPathExpression = JmesPathParser.parse(pathExpression); + this.opModel = opModel; + } + + public CodeBlock generate() { + CodeBlock.Builder block = CodeBlock.builder(); + parsedPathExpression.visit(new Visitor(block, "request")); + return block.build(); + } + + /** + * An implementation of {@link JmesPathVisitor} used by {@link #interpret(String, String)}. + */ + private class Visitor implements JmesPathVisitor { + private final CodeBlock.Builder codeBlock; + private final Deque variables = new ArrayDeque<>(); + private int variableIndex = 0; + + private Visitor(CodeBlock.Builder codeBlock, String inputValue) { + this.codeBlock = codeBlock; + this.codeBlock.add(inputValue); + this.variables.push(inputValue); + } + + @Override + public void visitExpression(Expression input) { + input.visit(this); + } + + @Override + public void visitSubExpression(SubExpression input) { + visitExpression(input.leftExpression()); + visitSubExpressionRight(input.rightSubExpression()); + } + + @Override + public void visitSubExpressionRight(SubExpressionRight input) { + input.visit(this); + } + + @Override + public void visitIdentifier(String input) { + System.out.println("Identifier: " + input); + // TODO + } + + @Override + public void visitWildcardExpression(WildcardExpression input) { + // TODO + System.out.println("Wildcard expression: " + input); + } + + @Override + public void visitMultiSelectList(MultiSelectList input) { + // TODO: + System.out.println("Multi-select list: " + input); + } + + @Override + public void visitFunctionExpression(FunctionExpression input) { + if ("keys".equals(input.function())) { + input.functionArgs().forEach(arg -> {visitExpression(arg.asExpression());}); + // keys always has exactly one argument + // TODO: Okay, what do we do now?? + } else { + throw new IllegalStateException("Unsupported function: " + input.function()) + } + } + + @Override + public void visitIndexExpression(IndexExpression input) { + // This is really a projection expression + System.out.println("Index expression: " + input); + } + + @Override + public void visitBracketSpecifier(BracketSpecifier input) { + throw new IllegalStateException("Unsupported bracketSpecifier expression"); + + } + + @Override + public void visitBracketSpecifierWithContents(BracketSpecifierWithContents input) { + throw new IllegalStateException("Unsupported bracketSpecifier expression"); + } + + @Override + public void visitSliceExpression(SliceExpression input) { + throw new IllegalStateException("Unsupported slice expression"); + + } + + @Override + public void visitBracketSpecifierWithoutContents(BracketSpecifierWithoutContents input) { + throw new IllegalStateException("Unsupported bracketSpecifier expression"); + } + + @Override + public void visitBracketSpecifierWithQuestionMark(BracketSpecifierWithQuestionMark input) { + throw new IllegalStateException("Unsupported bracketSpecifier expression"); + } + + @Override + public void visitComparatorExpression(ComparatorExpression input) { + throw new IllegalStateException("Unsupported comparator expression"); + + } + + @Override + public void visitOrExpression(OrExpression input) { + throw new IllegalStateException("Unsupported or expression"); + + } + + @Override + public void visitAndExpression(AndExpression input) { + throw new IllegalStateException("Unsupported and expression"); + } + + @Override + public void visitNotExpression(NotExpression input) { + throw new IllegalStateException("Unsupported not expression"); + + } + + @Override + public void visitParenExpression(ParenExpression input) { + throw new IllegalStateException("Unsupported paren expression"); + } + + + @Override + public void visitMultiSelectHash(MultiSelectHash input) { + throw new IllegalStateException("Unsupported multiselect map expression"); + } + + @Override + public void visitExpressionType(ExpressionType asExpressionType) { + throw new IllegalStateException("Unsupported expression type expression"); + } + + @Override + public void visitLiteral(Literal input) { + throw new IllegalStateException("Unsupported literal expression"); + + } + + @Override + public void visitPipeExpression(PipeExpression input) { + throw new IllegalStateException("Unsupported pipe expression"); + } + + @Override + public void visitRawString(String input) { + throw new IllegalStateException("Unsupported raw string expression"); + } + + @Override + public void visitCurrentNode(CurrentNode input) { + throw new IllegalStateException("Unsupported current Node expression"); + } + + @Override + public void visitNumber(int input) { + throw new IllegalStateException("Unsupported number literal expression"); + } + } +} From 7d595f8a605e11d86c819431660b9e0b6da0aec7 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Tue, 3 Feb 2026 08:52:09 -0800 Subject: [PATCH 2/8] Add support for stringArray staticContextParams --- .../poet/rules/EndpointResolverInterceptorSpec.java | 9 ++++++++- .../poet/rules/OperationContextParamsGenerator.java | 2 +- .../amazon/awssdk/codegen/poet/PoetMatchers.java | 2 +- .../poet/rules/EndpointResolverInterceptorSpecTest.java | 8 ++++++++ 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverInterceptorSpec.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverInterceptorSpec.java index 14a451adb59e..d4a27017e46e 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverInterceptorSpec.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverInterceptorSpec.java @@ -17,6 +17,7 @@ import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.core.TreeNode; +import com.fasterxml.jackson.jr.stree.JrsArray; import com.fasterxml.jackson.jr.stree.JrsBoolean; import com.fasterxml.jackson.jr.stree.JrsString; import com.squareup.javapoet.ClassName; @@ -364,6 +365,11 @@ private MethodSpec addStaticContextParamsMethod(OperationModel opModel) { case VALUE_FALSE: b.addStatement("params.$N($L)", setterName, ((JrsBoolean) value).booleanValue()); break; + case START_ARRAY: + JrsArray arrayValue = (JrsArray) value; + CodeBlock arrayCode = endpointRulesSpecUtils.treeNodeToLiteral(arrayValue); + b.addStatement("params.$N($L)", setterName, arrayCode); + break; default: throw new RuntimeException("Don't know how to set parameter of type " + value.asToken()); } @@ -554,7 +560,8 @@ private MethodSpec setOperationContextParamsMethod(OperationModel opModel) { String jmesPathString = ((JrsString) value.getPath()).getValue(); OperationContextParamsGenerator gen = new OperationContextParamsGenerator(jmesPathString, opModel); - gen.generate(); + CodeBlock newAddParam = gen.generate(); // TODO: This is the new codeblock we must generate, it should replace + // the below CodeBlock addParam = CodeBlock.builder() .add("params.$N(", setterName) .add(jmesPathGenerator.interpret(jmesPathString, "input")) diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/OperationContextParamsGenerator.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/OperationContextParamsGenerator.java index 2c25239a1cc8..0a65996d18bc 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/OperationContextParamsGenerator.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/OperationContextParamsGenerator.java @@ -116,7 +116,7 @@ public void visitFunctionExpression(FunctionExpression input) { // keys always has exactly one argument // TODO: Okay, what do we do now?? } else { - throw new IllegalStateException("Unsupported function: " + input.function()) + throw new IllegalStateException("Unsupported function: " + input.function()); } } diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/PoetMatchers.java b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/PoetMatchers.java index 7f4f6f43dcec..164688700648 100644 --- a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/PoetMatchers.java +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/PoetMatchers.java @@ -95,7 +95,7 @@ private static String getExpectedClass(ClassSpec spec, String testFile, boolean } } - private static String generateClass(ClassSpec spec) { + public static String generateClass(ClassSpec spec) { StringBuilder output = new StringBuilder(); try { buildJavaFile(spec).writeTo(output); diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverInterceptorSpecTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverInterceptorSpecTest.java index c31fd4c70690..beb291e84b43 100644 --- a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverInterceptorSpecTest.java +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverInterceptorSpecTest.java @@ -16,6 +16,7 @@ package software.amazon.awssdk.codegen.poet.rules; import static org.hamcrest.MatcherAssert.assertThat; +import static software.amazon.awssdk.codegen.poet.PoetMatchers.generateClass; import static software.amazon.awssdk.codegen.poet.PoetMatchers.generatesTo; import org.junit.jupiter.api.Test; @@ -46,4 +47,11 @@ void endpointResolverInterceptorClassWithEndpointBasedAuth() { ClassSpec endpointProviderInterceptor = new EndpointResolverInterceptorSpec(ClientTestModels.queryServiceModelsEndpointAuthParamsWithoutAllowList()); assertThat(endpointProviderInterceptor, generatesTo("endpoint-resolve-interceptor-with-endpointsbasedauth.java")); } + + @Test + public void endpointProviderTestClassWithStringArray() { + ClassSpec endpointProviderInterceptor = new EndpointResolverInterceptorSpec(ClientTestModels.stringArrayServiceModels()); + // assertThat(endpointProviderSpec, generatesTo("endpoint-rules-stringarray-test-class.java")); + System.out.println(generateClass(endpointProviderInterceptor)); + } } From 9b291dba8b4f45bbea28cb437928ab1f4e39f426 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Tue, 3 Feb 2026 11:01:38 -0800 Subject: [PATCH 3/8] Improve support for parsing multi-select lists in JMESPath --- .../jmespath/component/BracketSpecifier.java | 4 + .../BracketSpecifierWithContents.java | 20 +++ .../jmespath/parser/JmesPathParser.java | 116 ++++++++++++++- .../codegen/jmespath/JmesPathParserTest.java | 33 +++++ .../stringarray/customization.config | 4 + .../stringarray/endoint-rule-set.json | 38 +++++ .../stringarray/endpoint-tests.json | 93 ++++++++++++ .../stringarray/service-2.json | 137 ++++++++++++++++++ 8 files changed, 442 insertions(+), 3 deletions(-) create mode 100644 test/codegen-generated-classes-test/src/main/resources/codegen-resources/stringarray/customization.config create mode 100644 test/codegen-generated-classes-test/src/main/resources/codegen-resources/stringarray/endoint-rule-set.json create mode 100644 test/codegen-generated-classes-test/src/main/resources/codegen-resources/stringarray/endpoint-tests.json create mode 100644 test/codegen-generated-classes-test/src/main/resources/codegen-resources/stringarray/service-2.json diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/jmespath/component/BracketSpecifier.java b/codegen/src/main/java/software/amazon/awssdk/codegen/jmespath/component/BracketSpecifier.java index 510e84892d65..007c9d4edd65 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/jmespath/component/BracketSpecifier.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/jmespath/component/BracketSpecifier.java @@ -50,6 +50,10 @@ public static BracketSpecifier withWildcardExpressionContents(WildcardExpression return withContents(BracketSpecifierWithContents.wildcardExpression(wildcardExpression)); } + public static BracketSpecifier withMultiSelectListContents(MultiSelectList multiSelectList) { + return withContents(BracketSpecifierWithContents.multiSelectList(multiSelectList)); + } + public static BracketSpecifier withoutContents() { BracketSpecifier result = new BracketSpecifier(); result.bracketSpecifierWithoutContents = new BracketSpecifierWithoutContents(); diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/jmespath/component/BracketSpecifierWithContents.java b/codegen/src/main/java/software/amazon/awssdk/codegen/jmespath/component/BracketSpecifierWithContents.java index 69974a94a54d..38e945339ade 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/jmespath/component/BracketSpecifierWithContents.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/jmespath/component/BracketSpecifierWithContents.java @@ -24,12 +24,14 @@ *
  • A number, as in [1]
  • *
  • A star expression, as in [*]
  • *
  • A slice expression, as in [1:2:3]
  • + *
  • A multi-select-list, as in [string, object.key]
  • * */ public class BracketSpecifierWithContents { private Integer number; private WildcardExpression wildcardExpression; private SliceExpression sliceExpression; + private MultiSelectList multiSelectList; private BracketSpecifierWithContents() { } @@ -55,6 +57,13 @@ public static BracketSpecifierWithContents sliceExpression(SliceExpression slice return result; } + public static BracketSpecifierWithContents multiSelectList(MultiSelectList multiSelectList) { + Validate.notNull(multiSelectList, "multiSelectList"); + BracketSpecifierWithContents result = new BracketSpecifierWithContents(); + result.multiSelectList = multiSelectList; + return result; + } + public boolean isNumber() { return number != null; } @@ -67,6 +76,10 @@ public boolean isSliceExpression() { return sliceExpression != null; } + public boolean isMultiSelectList() { + return multiSelectList != null; + } + public int asNumber() { Validate.validState(isNumber(), "Not a Number"); return number; @@ -82,6 +95,11 @@ public SliceExpression asSliceExpression() { return sliceExpression; } + public MultiSelectList asMultiSelectList() { + Validate.validState(isMultiSelectList(), "Not a MultiSelectList"); + return multiSelectList; + } + public void visit(JmesPathVisitor visitor) { if (isNumber()) { visitor.visitNumber(asNumber()); @@ -89,6 +107,8 @@ public void visit(JmesPathVisitor visitor) { visitor.visitWildcardExpression(asWildcardExpression()); } else if (isSliceExpression()) { visitor.visitSliceExpression(asSliceExpression()); + } else if (isMultiSelectList()) { + visitor.visitMultiSelectList(asMultiSelectList()); } else { throw new IllegalStateException(); } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/jmespath/parser/JmesPathParser.java b/codegen/src/main/java/software/amazon/awssdk/codegen/jmespath/parser/JmesPathParser.java index b4f33226b8a4..b0947b1d701f 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/jmespath/parser/JmesPathParser.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/jmespath/parser/JmesPathParser.java @@ -73,7 +73,7 @@ public static Expression parse(String jmesPathString) { private Expression parse() { ParseResult expression = parseExpression(0, input.length()); if (!expression.hasResult()) { - throw new IllegalArgumentException("Failed to parse expression."); + throw new IllegalArgumentException("Failed to parse expression: " + input); } return expression.result(); @@ -246,13 +246,22 @@ private ParseResult parseIndexExpressionWithLhsExpression(int s endPosition = trimRightWhitespace(startPosition, endPosition); List bracketPositions = findCharacters(startPosition + 1, endPosition - 1, "["); - for (Integer bracketPosition : bracketPositions) { + + // Try bracket positions from right to left to support chaining multiple bracket specifiers + // e.g., listOfUnions[*][string, object.key][] should parse as: + // - left: listOfUnions[*][string, object.key] + // - right: [] + // This allows the left side to be recursively parsed as another index expression + for (int i = bracketPositions.size() - 1; i >= 0; i--) { + Integer bracketPosition = bracketPositions.get(i); ParseResult leftSide = parseExpression(startPosition, bracketPosition); if (!leftSide.hasResult()) { continue; } - ParseResult rightSide = parseBracketSpecifier(bracketPosition, endPosition); + // Use the special bracket specifier parser that supports multi-select-lists + // This is safe here because we know there's a left-hand expression + ParseResult rightSide = parseBracketSpecifierWithMultiSelect(bracketPosition, endPosition); if (!rightSide.hasResult()) { continue; } @@ -272,6 +281,74 @@ private ParseResult parseMultiSelectList(int startPosition, int .mapResult(MultiSelectList::new); } + /** + * Parse multi-select-list content without the surrounding brackets. + * Used when parsing bracket specifier contents like [string, object.key] or [string] + * This parses: expression *( "," expression ) + */ + private ParseResult parseMultiSelectListContent(int startPosition, int endPosition) { + startPosition = trimLeftWhitespace(startPosition, endPosition); + endPosition = trimRightWhitespace(startPosition, endPosition); + + List commaPositions = findCharacters(startPosition, endPosition, ","); + + // Single expression case (no commas) + if (commaPositions.isEmpty()) { + ParseResult singleExpr = parseExpression(startPosition, endPosition); + if (!singleExpr.hasResult()) { + return ParseResult.error(); + } + return ParseResult.success(new MultiSelectList(Collections.singletonList(singleExpr.result()))); + } + + // Multiple expressions separated by commas + List expressions = new ArrayList<>(); + + // Find first valid entry before a comma + int startOfSecondEntry = -1; + for (Integer comma : commaPositions) { + ParseResult result = parseExpression(startPosition, comma); + if (!result.hasResult()) { + continue; + } + + expressions.add(result.result()); + startOfSecondEntry = comma + 1; + break; + } + + if (expressions.size() == 0) { + logError("multi-select-list-content", "Invalid value", startPosition); + return ParseResult.error(); + } + + // Find any subsequent entries + int startPositionAfterComma = startOfSecondEntry; + for (Integer commaPosition : commaPositions) { + if (startPositionAfterComma > commaPosition) { + continue; + } + + ParseResult entry = parseExpression(startPositionAfterComma, commaPosition); + if (!entry.hasResult()) { + continue; + } + + expressions.add(entry.result()); + startPositionAfterComma = commaPosition + 1; + } + + // Parse the last entry after the final comma + ParseResult entry = parseExpression(startPositionAfterComma, endPosition); + if (!entry.hasResult()) { + logError("multi-select-list-content", "Invalid final entry", startPositionAfterComma); + return ParseResult.error(); + } + expressions.add(entry.result()); + + return ParseResult.success(new MultiSelectList(expressions)); + } + /** * multi-select-hash = "{" ( keyval-expr *( "," keyval-expr ) ) "}" */ @@ -410,6 +487,39 @@ private ParseResult parseBracketSpecifier(int startPosition, i .parse(startPosition + 1, endPosition - 1); } + /** + * bracket-specifier-with-multiselect = "[" multi-select-list-content "]" + * This is a special case for bracket specifiers that can contain multi-select-lists, + * only used when there's a left-hand expression (to avoid conflicting with standalone multi-select-lists). + */ + private ParseResult parseBracketSpecifierWithMultiSelect(int startPosition, int endPosition) { + startPosition = trimLeftWhitespace(startPosition, endPosition); + endPosition = trimRightWhitespace(startPosition, endPosition); + + if (!startsAndEndsWith(startPosition, endPosition, '[', ']')) { + logError("bracket-specifier-with-multiselect", "Expecting '[' and ']'", startPosition); + return ParseResult.error(); + } + + // "[]" + if (charsInRange(startPosition, endPosition) == 2) { + return ParseResult.success(BracketSpecifier.withoutContents()); + } + + // "[?" expression "]" + if (input.charAt(startPosition + 1) == '?') { + return parseExpression(startPosition + 2, endPosition - 1) + .mapResult(e -> BracketSpecifier.withQuestionMark(new BracketSpecifierWithQuestionMark(e))); + } + + // "[" (number / "*" / slice-expression / multi-select-list-content) "]" + return CompositeParser.firstTry(this::parseNumber, BracketSpecifier::withNumberContents) + .thenTry(this::parseWildcardExpression, BracketSpecifier::withWildcardExpressionContents) + .thenTry(this::parseSliceExpression, BracketSpecifier::withSliceExpressionContents) + .thenTry(this::parseMultiSelectListContent, BracketSpecifier::withMultiSelectListContents) + .parse(startPosition + 1, endPosition - 1); + } + /** * comparator-expression = expression comparator expression */ diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/jmespath/JmesPathParserTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/jmespath/JmesPathParserTest.java index 7687a36fbcc4..366012fcc46c 100644 --- a/codegen/src/test/java/software/amazon/awssdk/codegen/jmespath/JmesPathParserTest.java +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/jmespath/JmesPathParserTest.java @@ -20,6 +20,7 @@ import org.junit.jupiter.api.Test; import software.amazon.awssdk.codegen.jmespath.component.Comparator; import software.amazon.awssdk.codegen.jmespath.component.Expression; +import software.amazon.awssdk.codegen.jmespath.component.MultiSelectList; import software.amazon.awssdk.codegen.jmespath.parser.JmesPathParser; public class JmesPathParserTest { @@ -38,6 +39,38 @@ public void testSubExpressionWithMultiSelectList() { assertThat(expression.asSubExpression().rightSubExpression().asMultiSelectList().expressions().get(0).asIdentifier()).isEqualTo("bar"); } + @Test + public void testSubExpressionWithMultiSelectListAndFlatten() { + Expression expression = JmesPathParser.parse("listOfUnions[*][string, object.key][]"); + + // The expression should be: listOfUnions[*][string, object.key][] + // Parsed as: IndexExpression(IndexExpression(IndexExpression(listOfUnions, [*]), [string, object.key]), []) + assertThat(expression.isIndexExpression()).isTrue(); + + // Outermost: [] + assertThat(expression.asIndexExpression().bracketSpecifier().isBracketSpecifierWithoutContents()).isTrue(); + + // Middle: [string, object.key] + Expression middleExpr = expression.asIndexExpression().expression().get(); + assertThat(middleExpr.isIndexExpression()).isTrue(); + assertThat(middleExpr.asIndexExpression().bracketSpecifier().isBracketSpecifierWithContents()).isTrue(); + assertThat(middleExpr.asIndexExpression().bracketSpecifier().asBracketSpecifierWithContents().isMultiSelectList()).isTrue(); + + MultiSelectList multiSelectList = middleExpr.asIndexExpression().bracketSpecifier() + .asBracketSpecifierWithContents().asMultiSelectList(); + assertThat(multiSelectList.expressions()).hasSize(2); + assertThat(multiSelectList.expressions().get(0).asIdentifier()).isEqualTo("string"); + assertThat(multiSelectList.expressions().get(1).asSubExpression().leftExpression().asIdentifier()).isEqualTo("object"); + assertThat(multiSelectList.expressions().get(1).asSubExpression().rightSubExpression().asIdentifier()).isEqualTo("key"); + + // Innermost: listOfUnions[*] + Expression innerExpr = middleExpr.asIndexExpression().expression().get(); + assertThat(innerExpr.isIndexExpression()).isTrue(); + assertThat(innerExpr.asIndexExpression().expression().get().asIdentifier()).isEqualTo("listOfUnions"); + assertThat(innerExpr.asIndexExpression().bracketSpecifier().isBracketSpecifierWithContents()).isTrue(); + assertThat(innerExpr.asIndexExpression().bracketSpecifier().asBracketSpecifierWithContents().isWildcardExpression()).isTrue(); + } + @Test public void testSubExpressionWithMultiSelectHash() { Expression expression = JmesPathParser.parse("foo.{bar : baz}"); diff --git a/test/codegen-generated-classes-test/src/main/resources/codegen-resources/stringarray/customization.config b/test/codegen-generated-classes-test/src/main/resources/codegen-resources/stringarray/customization.config new file mode 100644 index 000000000000..bb763afd276b --- /dev/null +++ b/test/codegen-generated-classes-test/src/main/resources/codegen-resources/stringarray/customization.config @@ -0,0 +1,4 @@ +{ + "skipEndpointTestGeneration": true, + "enableGenerateCompiledEndpointRules": true +} diff --git a/test/codegen-generated-classes-test/src/main/resources/codegen-resources/stringarray/endoint-rule-set.json b/test/codegen-generated-classes-test/src/main/resources/codegen-resources/stringarray/endoint-rule-set.json new file mode 100644 index 000000000000..c3cab529249d --- /dev/null +++ b/test/codegen-generated-classes-test/src/main/resources/codegen-resources/stringarray/endoint-rule-set.json @@ -0,0 +1,38 @@ +{ + "version": "1.0", + "parameters": { + "stringArrayParam": { + "type": "stringArray", + "required": true, + "default": ["defaultValue1", "defaultValue2"], + "documentation": "docs" + } + }, + "rules": [ + { + "documentation": "Template first array value into URI if set", + "conditions": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "stringArrayParam" + }, + "[0]" + ], + "assign": "arrayValue" + } + ], + "endpoint": { + "url": "https://example.com/{arrayValue}" + }, + "type": "endpoint" + }, + { + "conditions": [], + "documentation": "error fallthrough", + "error": "no array values set", + "type": "error" + } + ] +} \ No newline at end of file diff --git a/test/codegen-generated-classes-test/src/main/resources/codegen-resources/stringarray/endpoint-tests.json b/test/codegen-generated-classes-test/src/main/resources/codegen-resources/stringarray/endpoint-tests.json new file mode 100644 index 000000000000..96ceb05e7f0a --- /dev/null +++ b/test/codegen-generated-classes-test/src/main/resources/codegen-resources/stringarray/endpoint-tests.json @@ -0,0 +1,93 @@ +{ + "version": "1.0", + "testCases": [ + { + "documentation": "Default array values used", + "params": {}, + "expect": { + "endpoint": { + "url": "https://example.com/defaultValue1" + } + }, + "operationInputs": [ + { + "operationName": "NoBindingsOperation" + } + ] + }, + { + "documentation": "Empty array", + "params": { + "stringArrayParam": [] + }, + "expect": { + "error": "no array values set" + }, + "operationInputs": [ + { + "operationName": "EmptyStaticContextOperation" + } + ] + }, + { + "documentation": "Static value", + "params": { + "stringArrayParam": ["staticValue1"] + }, + "expect": { + "endpoint": { + "url": "https://example.com/staticValue1" + } + }, + "operationInputs": [ + { + "operationName": "StaticContextOperation" + } + ] + }, + { + "documentation": "bound value from input", + "params": { + "stringArrayParam": ["key1"] + }, + "expect": { + "endpoint": { + "url": "https://example.com/key1" + } + }, + "operationInputs": [ + { + "operationName": "ListOfObjectsOperation", + "operationParams": { + "nested": { + "listOfObjects": [{"key": "key1"}] + } + } + }, + { + "operationName": "ListOfUnionsOperation", + "operationParams": { + "listOfUnions": [ + { + "string": "key1" + }, + { + "object": { + "key": "key2" + } + } + ] + } + }, + { + "operationName": "MapOperation", + "operationParams": { + "map": { + "key1": "value1" + } + } + } + ] + } + ] +} \ No newline at end of file diff --git a/test/codegen-generated-classes-test/src/main/resources/codegen-resources/stringarray/service-2.json b/test/codegen-generated-classes-test/src/main/resources/codegen-resources/stringarray/service-2.json new file mode 100644 index 000000000000..7f630bcea8d3 --- /dev/null +++ b/test/codegen-generated-classes-test/src/main/resources/codegen-resources/stringarray/service-2.json @@ -0,0 +1,137 @@ +{ + "metadata": { + "endpointPrefix": "svcname", + "serviceId": "StringArrayService", + "jsonVersion":"1.1", + "protocol":"json", + "signatureVersion":"v4" + }, + "operations": { + "NoBindingsOperation": { + "input": { "shape": "EmptyOperationRequest" }, + "http": { + "method": "POST", + "requestUri": "/" + } + }, + "EmptyStaticContextOperation": { + "input": { "shape": "EmptyOperationRequest" }, + "staticContextParams": { + "stringArrayParam": { + "value": [] + } + }, + "http": { + "method": "POST", + "requestUri": "/" + } + }, + "StaticContextOperation": { + "input": { "shape": "EmptyOperationRequest" }, + "staticContextParams": { + "stringArrayParam": { + "value": ["staticValue1"] + } + }, + "http": { + "method": "POST", + "requestUri": "/" + } + }, + "ListOfObjectsOperation": { + "input": { "shape": "ListOfObjectsOperationRequest" }, + "operationContextParams": { + "stringArrayParam": { + "path": "nested.listOfObjects[*].key" + } + }, + "http": { + "method": "POST", + "requestUri": "/" + } + }, + "ListOfUnionsOperation": { + "input": { "shape": "ListOfUnionsOperationRequest" }, + "operationContextParams": { + "stringArrayParam": { + "path": "listOfUnions[*][string, object.key][]" + } + }, + "http": { + "method": "POST", + "requestUri": "/" + } + }, + "MapOperation": { + "input": { "shape": "MapOperationRequest" }, + "operationContextParams": { + "stringArrayParam": { + "path": "keys(map)" + } + }, + "http": { + "method": "POST", + "requestUri": "/" + } + } + }, + "shapes": { + "EmptyOperationRequest": { + "type": "structure", + "members": {} + }, + "ListOfObjectsOperationRequest": { + "type": "structure", + "members": { + "nested":{"shape":"Nested"} + } + }, + "Nested": { + "type": "structure", + "members": { + "listOfObjects":{"shape":"ListOfObjects"} + } + }, + "ListOfObjects": { + "type": "list", + "member":{"shape":"ObjectMember"} + }, + "ObjectMember": { + "type": "structure", + "members": { + "key":{"shape":"String"} + } + }, + "ListOfUnionsOperationRequest": { + "type": "structure", + "members": { + "listOfUnions":{"shape":"ListOfUnions"} + } + }, + "ListOfUnions": { + "type": "list", + "member":{"shape":"UnionMember"} + }, + "UnionMember": { + "type": "structure", + "members": { + "string":{"shape":"String"}, + "object":{"shape":"ObjectMember"} + } + }, + "MapOperationRequest": { + "type": "structure", + "members": { + "map":{"shape":"Map"} + } + }, + "Map":{ + "type":"map", + "key":{"shape":"String"}, + "value":{"shape":"String"} + }, + "String":{ + "type":"string" + } + } +} \ No newline at end of file From 20a5159fbde7ccf5c1e5b4b0ea124c158b64f0ed Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Wed, 4 Feb 2026 10:35:27 -0800 Subject: [PATCH 4/8] Added support for multiselect expressions in jmespath acceptor generator + fix endpoitprovider task checks for operationContextParams --- .../emitters/tasks/EndpointProviderTasks.java | 20 +++++++----------- .../waiters/JmesPathAcceptorGenerator.java | 2 ++ .../JmesPathAcceptorGeneratorTest.java | 7 +++++++ ...t-rule-set.json => endpoint-rule-set.json} | 21 ++++++++++++++++--- 4 files changed, 35 insertions(+), 15 deletions(-) rename test/codegen-generated-classes-test/src/main/resources/codegen-resources/stringarray/{endoint-rule-set.json => endpoint-rule-set.json} (56%) diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/emitters/tasks/EndpointProviderTasks.java b/codegen/src/main/java/software/amazon/awssdk/codegen/emitters/tasks/EndpointProviderTasks.java index 6b64bd61f743..e65381fa4b39 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/emitters/tasks/EndpointProviderTasks.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/emitters/tasks/EndpointProviderTasks.java @@ -21,6 +21,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.stream.Collectors; import software.amazon.awssdk.codegen.emitters.GeneratorTask; import software.amazon.awssdk.codegen.emitters.GeneratorTaskParams; import software.amazon.awssdk.codegen.emitters.PoetGeneratorTask; @@ -160,21 +161,16 @@ private boolean shouldGenerateJmesPathRuntime() { return true; } - Map endpointParameters = model.getCustomizationConfig().getEndpointParameters(); + Map endpointParameters = model.getEndpointRuleSetModel().getParameters(); if (endpointParameters == null) { return false; } - return endpointParameters.values().stream().anyMatch(this::paramRequiresPathParserRuntime); - } - - private boolean paramRequiresPathParserRuntime(ParameterModel parameterModel) { - return paramIsOperationalContextParam(parameterModel) && - "stringarray".equals(parameterModel.getType().toLowerCase(Locale.US)); - } - - //TODO (string-array-params): resolve this logical test before finalizing coding - private boolean paramIsOperationalContextParam(ParameterModel parameterModel) { - return true; + // if any operation has operationContextParams then we must include jmesPathRuntime + return model.getOperations().values().stream() + .anyMatch(op -> { + Map opContextParams = op.getOperationContextParams(); + return opContextParams != null && !opContextParams.isEmpty(); + }); } } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/waiters/JmesPathAcceptorGenerator.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/waiters/JmesPathAcceptorGenerator.java index 21a6d6bf00cd..638019950488 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/waiters/JmesPathAcceptorGenerator.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/waiters/JmesPathAcceptorGenerator.java @@ -123,6 +123,8 @@ public void visitBracketSpecifierWithContents(BracketSpecifierWithContents input codeBlock.add(".index(" + input.asNumber() + ")"); } else if (input.isWildcardExpression()) { codeBlock.add(".wildcard()"); + } else if (input.isMultiSelectList()) { + visitMultiSelectList(input.asMultiSelectList()); } else { throw new UnsupportedOperationException(); } diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/waiters/JmesPathAcceptorGeneratorTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/waiters/JmesPathAcceptorGeneratorTest.java index 285d1e477f3f..42d3f54d9964 100644 --- a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/waiters/JmesPathAcceptorGeneratorTest.java +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/waiters/JmesPathAcceptorGeneratorTest.java @@ -185,6 +185,13 @@ void testNestedMultiSelectListsLeft() { + "x3 -> x3.field(\"baz\"))"); } + @Test + void testMultiSelectListAndFlatten() { + testConversion("listOfUnions[*][string, object.key][]", + "input.field(\"listOfUnions\").wildcard().multiSelectList(x0 -> x0.field(\"string\"), " + + "x1 -> x1.field(\"object\").field(\"key\")).flatten()"); + } + @Test void testMultiSelectHash2() { assertThatThrownBy(() -> testConversion("{fooK : fooV, barK : barV}", "")) diff --git a/test/codegen-generated-classes-test/src/main/resources/codegen-resources/stringarray/endoint-rule-set.json b/test/codegen-generated-classes-test/src/main/resources/codegen-resources/stringarray/endpoint-rule-set.json similarity index 56% rename from test/codegen-generated-classes-test/src/main/resources/codegen-resources/stringarray/endoint-rule-set.json rename to test/codegen-generated-classes-test/src/main/resources/codegen-resources/stringarray/endpoint-rule-set.json index c3cab529249d..309db8dce6f3 100644 --- a/test/codegen-generated-classes-test/src/main/resources/codegen-resources/stringarray/endoint-rule-set.json +++ b/test/codegen-generated-classes-test/src/main/resources/codegen-resources/stringarray/endpoint-rule-set.json @@ -2,10 +2,16 @@ "version": "1.0", "parameters": { "stringArrayParam": { - "type": "stringArray", + "type": "StringArray", "required": true, "default": ["defaultValue1", "defaultValue2"], - "documentation": "docs" + "documentation": "StringArray test parameters" + }, + "Region": { + "builtIn": "AWS::Region", + "required": true, + "documentation": "The AWS region used to dispatch the request.", + "type": "String" } }, "rules": [ @@ -24,7 +30,16 @@ } ], "endpoint": { - "url": "https://example.com/{arrayValue}" + "url": "https://example.com/{arrayValue}", + "properties": { + "authSchemes": [ + { + "name": "sigv4", + "signingRegion": "{Region}", + "signingName": "stringarray" + } + ] + } }, "type": "endpoint" }, From d02744171a73824cba4c6d5424324ac209c63c2f Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Wed, 4 Feb 2026 10:54:47 -0800 Subject: [PATCH 5/8] Cleanup and fix checkstyles --- .../emitters/tasks/EndpointProviderTasks.java | 2 - .../EndpointResolverInterceptorSpec.java | 3 - .../OperationContextParamsGenerator.java | 221 ------------------ .../stringarray/StringArrayBindingsTest.java | 19 ++ 4 files changed, 19 insertions(+), 226 deletions(-) delete mode 100644 codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/OperationContextParamsGenerator.java create mode 100644 test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/stringarray/StringArrayBindingsTest.java diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/emitters/tasks/EndpointProviderTasks.java b/codegen/src/main/java/software/amazon/awssdk/codegen/emitters/tasks/EndpointProviderTasks.java index e65381fa4b39..eb1a3bbd9560 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/emitters/tasks/EndpointProviderTasks.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/emitters/tasks/EndpointProviderTasks.java @@ -19,9 +19,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; -import java.util.Locale; import java.util.Map; -import java.util.stream.Collectors; import software.amazon.awssdk.codegen.emitters.GeneratorTask; import software.amazon.awssdk.codegen.emitters.GeneratorTaskParams; import software.amazon.awssdk.codegen.emitters.PoetGeneratorTask; diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverInterceptorSpec.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverInterceptorSpec.java index d4a27017e46e..726d61cdb99e 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverInterceptorSpec.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverInterceptorSpec.java @@ -559,9 +559,6 @@ private MethodSpec setOperationContextParamsMethod(OperationModel opModel) { String setterName = endpointRulesSpecUtils.paramMethodName(key); String jmesPathString = ((JrsString) value.getPath()).getValue(); - OperationContextParamsGenerator gen = new OperationContextParamsGenerator(jmesPathString, opModel); - CodeBlock newAddParam = gen.generate(); // TODO: This is the new codeblock we must generate, it should replace - // the below CodeBlock addParam = CodeBlock.builder() .add("params.$N(", setterName) .add(jmesPathGenerator.interpret(jmesPathString, "input")) diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/OperationContextParamsGenerator.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/OperationContextParamsGenerator.java deleted file mode 100644 index 0a65996d18bc..000000000000 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/OperationContextParamsGenerator.java +++ /dev/null @@ -1,221 +0,0 @@ -/* - * Copyright 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.awssdk.codegen.poet.rules; - -import com.squareup.javapoet.CodeBlock; -import java.util.ArrayDeque; -import java.util.Deque; -import software.amazon.awssdk.codegen.jmespath.component.AndExpression; -import software.amazon.awssdk.codegen.jmespath.component.BracketSpecifier; -import software.amazon.awssdk.codegen.jmespath.component.BracketSpecifierWithContents; -import software.amazon.awssdk.codegen.jmespath.component.BracketSpecifierWithQuestionMark; -import software.amazon.awssdk.codegen.jmespath.component.BracketSpecifierWithoutContents; -import software.amazon.awssdk.codegen.jmespath.component.ComparatorExpression; -import software.amazon.awssdk.codegen.jmespath.component.CurrentNode; -import software.amazon.awssdk.codegen.jmespath.component.Expression; -import software.amazon.awssdk.codegen.jmespath.component.ExpressionType; -import software.amazon.awssdk.codegen.jmespath.component.FunctionExpression; -import software.amazon.awssdk.codegen.jmespath.component.IndexExpression; -import software.amazon.awssdk.codegen.jmespath.component.Literal; -import software.amazon.awssdk.codegen.jmespath.component.MultiSelectHash; -import software.amazon.awssdk.codegen.jmespath.component.MultiSelectList; -import software.amazon.awssdk.codegen.jmespath.component.NotExpression; -import software.amazon.awssdk.codegen.jmespath.component.OrExpression; -import software.amazon.awssdk.codegen.jmespath.component.ParenExpression; -import software.amazon.awssdk.codegen.jmespath.component.PipeExpression; -import software.amazon.awssdk.codegen.jmespath.component.SliceExpression; -import software.amazon.awssdk.codegen.jmespath.component.SubExpression; -import software.amazon.awssdk.codegen.jmespath.component.SubExpressionRight; -import software.amazon.awssdk.codegen.jmespath.component.WildcardExpression; -import software.amazon.awssdk.codegen.jmespath.parser.JmesPathParser; -import software.amazon.awssdk.codegen.jmespath.parser.JmesPathVisitor; -import software.amazon.awssdk.codegen.model.intermediate.OperationModel; - -public class OperationContextParamsGenerator { - private final String pathExpression; - private final OperationModel opModel; - private final Expression parsedPathExpression; - - public OperationContextParamsGenerator(String pathExpression, OperationModel opModel) { - this.pathExpression = pathExpression; - this.parsedPathExpression = JmesPathParser.parse(pathExpression); - this.opModel = opModel; - } - - public CodeBlock generate() { - CodeBlock.Builder block = CodeBlock.builder(); - parsedPathExpression.visit(new Visitor(block, "request")); - return block.build(); - } - - /** - * An implementation of {@link JmesPathVisitor} used by {@link #interpret(String, String)}. - */ - private class Visitor implements JmesPathVisitor { - private final CodeBlock.Builder codeBlock; - private final Deque variables = new ArrayDeque<>(); - private int variableIndex = 0; - - private Visitor(CodeBlock.Builder codeBlock, String inputValue) { - this.codeBlock = codeBlock; - this.codeBlock.add(inputValue); - this.variables.push(inputValue); - } - - @Override - public void visitExpression(Expression input) { - input.visit(this); - } - - @Override - public void visitSubExpression(SubExpression input) { - visitExpression(input.leftExpression()); - visitSubExpressionRight(input.rightSubExpression()); - } - - @Override - public void visitSubExpressionRight(SubExpressionRight input) { - input.visit(this); - } - - @Override - public void visitIdentifier(String input) { - System.out.println("Identifier: " + input); - // TODO - } - - @Override - public void visitWildcardExpression(WildcardExpression input) { - // TODO - System.out.println("Wildcard expression: " + input); - } - - @Override - public void visitMultiSelectList(MultiSelectList input) { - // TODO: - System.out.println("Multi-select list: " + input); - } - - @Override - public void visitFunctionExpression(FunctionExpression input) { - if ("keys".equals(input.function())) { - input.functionArgs().forEach(arg -> {visitExpression(arg.asExpression());}); - // keys always has exactly one argument - // TODO: Okay, what do we do now?? - } else { - throw new IllegalStateException("Unsupported function: " + input.function()); - } - } - - @Override - public void visitIndexExpression(IndexExpression input) { - // This is really a projection expression - System.out.println("Index expression: " + input); - } - - @Override - public void visitBracketSpecifier(BracketSpecifier input) { - throw new IllegalStateException("Unsupported bracketSpecifier expression"); - - } - - @Override - public void visitBracketSpecifierWithContents(BracketSpecifierWithContents input) { - throw new IllegalStateException("Unsupported bracketSpecifier expression"); - } - - @Override - public void visitSliceExpression(SliceExpression input) { - throw new IllegalStateException("Unsupported slice expression"); - - } - - @Override - public void visitBracketSpecifierWithoutContents(BracketSpecifierWithoutContents input) { - throw new IllegalStateException("Unsupported bracketSpecifier expression"); - } - - @Override - public void visitBracketSpecifierWithQuestionMark(BracketSpecifierWithQuestionMark input) { - throw new IllegalStateException("Unsupported bracketSpecifier expression"); - } - - @Override - public void visitComparatorExpression(ComparatorExpression input) { - throw new IllegalStateException("Unsupported comparator expression"); - - } - - @Override - public void visitOrExpression(OrExpression input) { - throw new IllegalStateException("Unsupported or expression"); - - } - - @Override - public void visitAndExpression(AndExpression input) { - throw new IllegalStateException("Unsupported and expression"); - } - - @Override - public void visitNotExpression(NotExpression input) { - throw new IllegalStateException("Unsupported not expression"); - - } - - @Override - public void visitParenExpression(ParenExpression input) { - throw new IllegalStateException("Unsupported paren expression"); - } - - - @Override - public void visitMultiSelectHash(MultiSelectHash input) { - throw new IllegalStateException("Unsupported multiselect map expression"); - } - - @Override - public void visitExpressionType(ExpressionType asExpressionType) { - throw new IllegalStateException("Unsupported expression type expression"); - } - - @Override - public void visitLiteral(Literal input) { - throw new IllegalStateException("Unsupported literal expression"); - - } - - @Override - public void visitPipeExpression(PipeExpression input) { - throw new IllegalStateException("Unsupported pipe expression"); - } - - @Override - public void visitRawString(String input) { - throw new IllegalStateException("Unsupported raw string expression"); - } - - @Override - public void visitCurrentNode(CurrentNode input) { - throw new IllegalStateException("Unsupported current Node expression"); - } - - @Override - public void visitNumber(int input) { - throw new IllegalStateException("Unsupported number literal expression"); - } - } -} diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/stringarray/StringArrayBindingsTest.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/stringarray/StringArrayBindingsTest.java new file mode 100644 index 000000000000..f60a4fb15100 --- /dev/null +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/stringarray/StringArrayBindingsTest.java @@ -0,0 +1,19 @@ +/* + * Copyright 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.awssdk.services.stringarray; + +public class StringArrayBindingsTest { +} From 5b94b13058f888a061369ba1bd60631e5f058eef Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Wed, 4 Feb 2026 11:26:50 -0800 Subject: [PATCH 6/8] Implement tests for all string array bindings --- .../stringarray/StringArrayBindingsTest.java | 290 +++++++++++++++++- 1 file changed, 289 insertions(+), 1 deletion(-) diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/stringarray/StringArrayBindingsTest.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/stringarray/StringArrayBindingsTest.java index f60a4fb15100..1dba2c537874 100644 --- a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/stringarray/StringArrayBindingsTest.java +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/stringarray/StringArrayBindingsTest.java @@ -15,5 +15,293 @@ package software.amazon.awssdk.services.stringarray; -public class StringArrayBindingsTest { +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.stringarray.endpoints.StringArrayEndpointParams; +import software.amazon.awssdk.services.stringarray.endpoints.StringArrayEndpointProvider; +import software.amazon.awssdk.services.stringarray.model.ObjectMember; +import software.amazon.awssdk.services.stringarray.model.UnionMember; + +class StringArrayBindingsTest { + private static final AwsCredentialsProvider CREDENTIALS = StaticCredentialsProvider.create( + AwsBasicCredentials.create("akid", "skid")); + + private static final Region REGION = Region.of("us-east-1"); + + private StringArrayEndpointProvider mockEndpointProvider; + + @BeforeEach + public void setup() { + mockEndpointProvider = mock(StringArrayEndpointProvider.class); + when(mockEndpointProvider.resolveEndpoint(any(StringArrayEndpointParams.class))) + .thenThrow(new RuntimeException("boom")); + } + + @Test + void noBindingsOperation_usesDefaultValues() { + assertThatThrownBy(() -> createClient().noBindingsOperation(r -> {})) + .isInstanceOf(RuntimeException.class); + + StringArrayEndpointParams params = captureEndpointParams(); + + assertThat(params.stringArrayParam()).isNotEmpty() + .hasSize(2) + .containsExactly("defaultValue1", "defaultValue2"); + } + + @Test + void emptyStaticContextOperation_hasEmptyArray() { + assertThatThrownBy(() -> createClient().emptyStaticContextOperation(r -> {})) + .isInstanceOf(RuntimeException.class); + + StringArrayEndpointParams params = captureEndpointParams(); + + assertThat(params.stringArrayParam()).isEmpty(); + } + + @Test + void staticContextOperation_hasStaticValue() { + assertThatThrownBy(() -> createClient().staticContextOperation(r -> {})) + .isInstanceOf(RuntimeException.class); + + StringArrayEndpointParams params = captureEndpointParams(); + + assertThat(params.stringArrayParam()).isNotEmpty() + .hasSize(1) + .containsExactly("staticValue1"); + } + + @Test + void listOfObjectsOperation_extractsSingleKeyFromNestedList() { + assertThatThrownBy(() -> createClient().listOfObjectsOperation(r -> r.nested(n -> n.listOfObjects( + ObjectMember.builder().key("key1").build() + )))).isInstanceOf(RuntimeException.class); + + StringArrayEndpointParams params = captureEndpointParams(); + + assertThat(params.stringArrayParam()).isNotEmpty() + .hasSize(1) + .containsExactly("key1"); + } + + @Test + void listOfObjectsOperation_extractsMultipleKeysFromNestedList() { + assertThatThrownBy(() -> createClient().listOfObjectsOperation(r -> r.nested(n -> n.listOfObjects( + ObjectMember.builder().key("key1").build(), + ObjectMember.builder().key("key2").build(), + ObjectMember.builder().key("key3").build() + )))).isInstanceOf(RuntimeException.class); + + StringArrayEndpointParams params = captureEndpointParams(); + + assertThat(params.stringArrayParam()).isNotEmpty() + .hasSize(3) + .containsExactly("key1", "key2", "key3"); + } + + @Test + void listOfObjectsOperation_withNullNested_returnsEmptyArray() { + assertThatThrownBy(() -> createClient().listOfObjectsOperation(r -> {})) + .isInstanceOf(RuntimeException.class); + + StringArrayEndpointParams params = captureEndpointParams(); + + assertThat(params.stringArrayParam()).isEmpty(); + } + + @Test + void listOfObjectsOperation_withEmptyList_returnsEmptyArray() { + assertThatThrownBy(() -> createClient().listOfObjectsOperation(r -> r.nested(n -> n.listOfObjects(Collections.emptyList())))) + .isInstanceOf(RuntimeException.class); + + StringArrayEndpointParams params = captureEndpointParams(); + + assertThat(params.stringArrayParam()).isEmpty(); + } + + @Test + void listOfObjectsOperation_filtersOutNullKeys() { + assertThatThrownBy(() -> createClient().listOfObjectsOperation(r -> r.nested(n -> n.listOfObjects( + ObjectMember.builder().key("key1").build(), + ObjectMember.builder().build(), // null key + ObjectMember.builder().key("key2").build() + )))).isInstanceOf(RuntimeException.class); + + StringArrayEndpointParams params = captureEndpointParams(); + + assertThat(params.stringArrayParam()).isNotEmpty() + .hasSize(2) + .containsExactly("key1", "key2"); + } + + @Test + void listOfUnionsOperation_extractsStringValuesFromUnions() { + assertThatThrownBy(() -> createClient().listOfUnionsOperation(r -> r.listOfUnions( + UnionMember.builder().string("value1").build(), + UnionMember.builder().string("value2").build() + ))).isInstanceOf(RuntimeException.class); + + StringArrayEndpointParams params = captureEndpointParams(); + + assertThat(params.stringArrayParam()).isNotEmpty() + .hasSize(2) + .containsExactly("value1", "value2"); + } + + @Test + void listOfUnionsOperation_extractsKeysFromObjectMembers() { + assertThatThrownBy(() -> createClient().listOfUnionsOperation(r -> r.listOfUnions( + UnionMember.builder().object(o -> o.key("objKey1")).build(), + UnionMember.builder().object(o -> o.key("objKey2")).build() + ))).isInstanceOf(RuntimeException.class); + + StringArrayEndpointParams params = captureEndpointParams(); + + assertThat(params.stringArrayParam()).isNotEmpty() + .hasSize(2) + .containsExactly("objKey1", "objKey2"); + } + + @Test + void listOfUnionsOperation_extractsBothStringsAndObjectKeys() { + assertThatThrownBy(() -> createClient().listOfUnionsOperation(r -> r.listOfUnions( + UnionMember.builder().string("stringValue").build(), + UnionMember.builder().object(o -> o.key("objectKey")).build() + ))).isInstanceOf(RuntimeException.class); + + StringArrayEndpointParams params = captureEndpointParams(); + + assertThat(params.stringArrayParam()).isNotEmpty() + .hasSize(2) + .containsExactly("stringValue", "objectKey"); + } + + @Test + void listOfUnionsOperation_withNullList_returnsEmptyArray() { + assertThatThrownBy(() -> createClient().listOfUnionsOperation(r -> {})) + .isInstanceOf(RuntimeException.class); + + StringArrayEndpointParams params = captureEndpointParams(); + + assertThat(params.stringArrayParam()).isEmpty(); + } + + @Test + void listOfUnionsOperation_withEmptyList_returnsEmptyArray() { + assertThatThrownBy(() -> createClient().listOfUnionsOperation(r -> r.listOfUnions(Collections.emptyList()))) + .isInstanceOf(RuntimeException.class); + + StringArrayEndpointParams params = captureEndpointParams(); + + assertThat(params.stringArrayParam()).isEmpty(); + } + + @Test + void listOfUnionsOperation_filtersOutUnionsWithNoValues() { + assertThatThrownBy(() -> createClient().listOfUnionsOperation(r -> r.listOfUnions( + UnionMember.builder().string("value1").build(), + UnionMember.builder().build(), // no string or object + UnionMember.builder().string("value2").build() + ))).isInstanceOf(RuntimeException.class); + + StringArrayEndpointParams params = captureEndpointParams(); + + assertThat(params.stringArrayParam()).isNotEmpty() + .hasSize(2) + .containsExactly("value1", "value2"); + } + + @Test + void listOfUnionsOperation_filtersOutObjectsWithNullKeys() { + assertThatThrownBy(() -> createClient().listOfUnionsOperation(r -> r.listOfUnions( + UnionMember.builder().string("value1").build(), + UnionMember.builder().object(o -> {}).build(), // null key + UnionMember.builder().object(o -> o.key("value2")).build() + ))).isInstanceOf(RuntimeException.class); + + StringArrayEndpointParams params = captureEndpointParams(); + + assertThat(params.stringArrayParam()).isNotEmpty() + .hasSize(2) + .containsExactly("value1", "value2"); + } + + @Test + void mapOperation_extractsSingleKeyFromMap() { + assertThatThrownBy(() -> createClient().mapOperation(r -> r.map(Collections.singletonMap("key1", "value1")))) + .isInstanceOf(RuntimeException.class); + + StringArrayEndpointParams params = captureEndpointParams(); + + assertThat(params.stringArrayParam()).isNotEmpty() + .hasSize(1) + .containsExactly("key1"); + } + + @Test + void mapOperation_extractsMultipleKeysFromMap() { + Map map = new LinkedHashMap<>(); + map.put("key1", "value1"); + map.put("key2", "value2"); + map.put("key3", "value3"); + + assertThatThrownBy(() -> createClient().mapOperation(r -> r.map(map))) + .isInstanceOf(RuntimeException.class); + + StringArrayEndpointParams params = captureEndpointParams(); + + assertThat(params.stringArrayParam()).isNotEmpty() + .hasSize(3) + .containsExactly("key1", "key2", "key3"); + } + + @Test + void mapOperation_withNullMap_returnsEmptyArray() { + assertThatThrownBy(() -> createClient().mapOperation(r -> {})) + .isInstanceOf(RuntimeException.class); + + StringArrayEndpointParams params = captureEndpointParams(); + + assertThat(params.stringArrayParam()).isEmpty(); + } + + @Test + void mapOperation_withEmptyMap_returnsEmptyArray() { + assertThatThrownBy(() -> createClient().mapOperation(r -> r.map(Collections.emptyMap()))) + .isInstanceOf(RuntimeException.class); + + StringArrayEndpointParams params = captureEndpointParams(); + + assertThat(params.stringArrayParam()).isEmpty(); + } + + private StringArrayClient createClient() { + return StringArrayClient.builder() + .region(REGION) + .credentialsProvider(CREDENTIALS) + .endpointProvider(mockEndpointProvider) + .build(); + } + + private StringArrayEndpointParams captureEndpointParams() { + ArgumentCaptor paramsCaptor = + ArgumentCaptor.forClass(StringArrayEndpointParams.class); + verify(mockEndpointProvider).resolveEndpoint(paramsCaptor.capture()); + return paramsCaptor.getValue(); + } } From 575d7cd9893afc5d5867d1333b1173b5de3d3cb2 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Wed, 4 Feb 2026 12:16:12 -0800 Subject: [PATCH 7/8] Improve comments/naming + fix generated class test --- .../jmespath/parser/JmesPathParser.java | 12 +- .../codegen/jmespath/JmesPathParserTest.java | 28 +-- .../awssdk/codegen/poet/PoetMatchers.java | 2 +- .../EndpointResolverInterceptorSpecTest.java | 4 +- ...-resolve-interceptor-with-stringarray.java | 194 ++++++++++++++++++ 5 files changed, 215 insertions(+), 25 deletions(-) create mode 100644 codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor-with-stringarray.java diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/jmespath/parser/JmesPathParser.java b/codegen/src/main/java/software/amazon/awssdk/codegen/jmespath/parser/JmesPathParser.java index b0947b1d701f..4a9126bf1019 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/jmespath/parser/JmesPathParser.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/jmespath/parser/JmesPathParser.java @@ -251,7 +251,7 @@ private ParseResult parseIndexExpressionWithLhsExpression(int s // e.g., listOfUnions[*][string, object.key][] should parse as: // - left: listOfUnions[*][string, object.key] // - right: [] - // This allows the left side to be recursively parsed as another index expression + // This allows both the left and right side to be recursively parsed for (int i = bracketPositions.size() - 1; i >= 0; i--) { Integer bracketPosition = bracketPositions.get(i); ParseResult leftSide = parseExpression(startPosition, bracketPosition); @@ -259,9 +259,8 @@ private ParseResult parseIndexExpressionWithLhsExpression(int s continue; } - // Use the special bracket specifier parser that supports multi-select-lists - // This is safe here because we know there's a left-hand expression - ParseResult rightSide = parseBracketSpecifierWithMultiSelect(bracketPosition, endPosition); + // we know there is a left hand expression and that the Rhs is a bracketSpecifier + ParseResult rightSide = parseBracketSpecifierWithLhsExpression(bracketPosition, endPosition); if (!rightSide.hasResult()) { continue; } @@ -488,11 +487,10 @@ private ParseResult parseBracketSpecifier(int startPosition, i } /** + * This is a special case for bracket specifiers with a left-hand expression and which can contain multi-select-lists. * bracket-specifier-with-multiselect = "[" multi-select-list-content "]" - * This is a special case for bracket specifiers that can contain multi-select-lists, - * only used when there's a left-hand expression (to avoid conflicting with standalone multi-select-lists). */ - private ParseResult parseBracketSpecifierWithMultiSelect(int startPosition, int endPosition) { + private ParseResult parseBracketSpecifierWithLhsExpression(int startPosition, int endPosition) { startPosition = trimLeftWhitespace(startPosition, endPosition); endPosition = trimRightWhitespace(startPosition, endPosition); diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/jmespath/JmesPathParserTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/jmespath/JmesPathParserTest.java index 366012fcc46c..c1ee52ee68ce 100644 --- a/codegen/src/test/java/software/amazon/awssdk/codegen/jmespath/JmesPathParserTest.java +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/jmespath/JmesPathParserTest.java @@ -43,32 +43,32 @@ public void testSubExpressionWithMultiSelectList() { public void testSubExpressionWithMultiSelectListAndFlatten() { Expression expression = JmesPathParser.parse("listOfUnions[*][string, object.key][]"); - // The expression should be: listOfUnions[*][string, object.key][] - // Parsed as: IndexExpression(IndexExpression(IndexExpression(listOfUnions, [*]), [string, object.key]), []) + // The expression should be parsed as: + // IndexExpression(IndexExpression(IndexExpression(listOfUnions, [*]), [string, object.key]), []) assertThat(expression.isIndexExpression()).isTrue(); - // Outermost: [] + // the right most flatten assertThat(expression.asIndexExpression().bracketSpecifier().isBracketSpecifierWithoutContents()).isTrue(); // Middle: [string, object.key] - Expression middleExpr = expression.asIndexExpression().expression().get(); - assertThat(middleExpr.isIndexExpression()).isTrue(); - assertThat(middleExpr.asIndexExpression().bracketSpecifier().isBracketSpecifierWithContents()).isTrue(); - assertThat(middleExpr.asIndexExpression().bracketSpecifier().asBracketSpecifierWithContents().isMultiSelectList()).isTrue(); + Expression middleBracketsExpr = expression.asIndexExpression().expression().get(); + assertThat(middleBracketsExpr.isIndexExpression()).isTrue(); + assertThat(middleBracketsExpr.asIndexExpression().bracketSpecifier().isBracketSpecifierWithContents()).isTrue(); + assertThat(middleBracketsExpr.asIndexExpression().bracketSpecifier().asBracketSpecifierWithContents().isMultiSelectList()).isTrue(); - MultiSelectList multiSelectList = middleExpr.asIndexExpression().bracketSpecifier() + MultiSelectList multiSelectList = middleBracketsExpr.asIndexExpression().bracketSpecifier() .asBracketSpecifierWithContents().asMultiSelectList(); assertThat(multiSelectList.expressions()).hasSize(2); assertThat(multiSelectList.expressions().get(0).asIdentifier()).isEqualTo("string"); assertThat(multiSelectList.expressions().get(1).asSubExpression().leftExpression().asIdentifier()).isEqualTo("object"); assertThat(multiSelectList.expressions().get(1).asSubExpression().rightSubExpression().asIdentifier()).isEqualTo("key"); - // Innermost: listOfUnions[*] - Expression innerExpr = middleExpr.asIndexExpression().expression().get(); - assertThat(innerExpr.isIndexExpression()).isTrue(); - assertThat(innerExpr.asIndexExpression().expression().get().asIdentifier()).isEqualTo("listOfUnions"); - assertThat(innerExpr.asIndexExpression().bracketSpecifier().isBracketSpecifierWithContents()).isTrue(); - assertThat(innerExpr.asIndexExpression().bracketSpecifier().asBracketSpecifierWithContents().isWildcardExpression()).isTrue(); + // left most wildcard: listOfUnions[*] + Expression wildCardExpr = middleBracketsExpr.asIndexExpression().expression().get(); + assertThat(wildCardExpr.isIndexExpression()).isTrue(); + assertThat(wildCardExpr.asIndexExpression().expression().get().asIdentifier()).isEqualTo("listOfUnions"); + assertThat(wildCardExpr.asIndexExpression().bracketSpecifier().isBracketSpecifierWithContents()).isTrue(); + assertThat(wildCardExpr.asIndexExpression().bracketSpecifier().asBracketSpecifierWithContents().isWildcardExpression()).isTrue(); } @Test diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/PoetMatchers.java b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/PoetMatchers.java index 164688700648..7f4f6f43dcec 100644 --- a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/PoetMatchers.java +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/PoetMatchers.java @@ -95,7 +95,7 @@ private static String getExpectedClass(ClassSpec spec, String testFile, boolean } } - public static String generateClass(ClassSpec spec) { + private static String generateClass(ClassSpec spec) { StringBuilder output = new StringBuilder(); try { buildJavaFile(spec).writeTo(output); diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverInterceptorSpecTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverInterceptorSpecTest.java index beb291e84b43..89f87ff41952 100644 --- a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverInterceptorSpecTest.java +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverInterceptorSpecTest.java @@ -16,7 +16,6 @@ package software.amazon.awssdk.codegen.poet.rules; import static org.hamcrest.MatcherAssert.assertThat; -import static software.amazon.awssdk.codegen.poet.PoetMatchers.generateClass; import static software.amazon.awssdk.codegen.poet.PoetMatchers.generatesTo; import org.junit.jupiter.api.Test; @@ -51,7 +50,6 @@ void endpointResolverInterceptorClassWithEndpointBasedAuth() { @Test public void endpointProviderTestClassWithStringArray() { ClassSpec endpointProviderInterceptor = new EndpointResolverInterceptorSpec(ClientTestModels.stringArrayServiceModels()); - // assertThat(endpointProviderSpec, generatesTo("endpoint-rules-stringarray-test-class.java")); - System.out.println(generateClass(endpointProviderInterceptor)); + assertThat(endpointProviderInterceptor, generatesTo("endpoint-resolve-interceptor-with-stringarray.java")); } } diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor-with-stringarray.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor-with-stringarray.java new file mode 100644 index 000000000000..0f5033376ffc --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolve-interceptor-with-stringarray.java @@ -0,0 +1,194 @@ +package software.amazon.awssdk.services.samplesvc.endpoints.internal; + +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletionException; +import software.amazon.awssdk.annotations.Generated; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.awscore.AwsExecutionAttribute; +import software.amazon.awssdk.awscore.endpoints.AwsEndpointAttribute; +import software.amazon.awssdk.awscore.endpoints.authscheme.EndpointAuthScheme; +import software.amazon.awssdk.awscore.endpoints.authscheme.SigV4AuthScheme; +import software.amazon.awssdk.awscore.endpoints.authscheme.SigV4aAuthScheme; +import software.amazon.awssdk.core.SdkRequest; +import software.amazon.awssdk.core.SelectedAuthScheme; +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.core.interceptor.Context; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; +import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; +import software.amazon.awssdk.core.interceptor.SdkExecutionAttribute; +import software.amazon.awssdk.core.interceptor.SdkInternalExecutionAttribute; +import software.amazon.awssdk.core.metrics.CoreMetric; +import software.amazon.awssdk.endpoints.Endpoint; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.http.auth.aws.signer.AwsV4HttpSigner; +import software.amazon.awssdk.http.auth.aws.signer.AwsV4aHttpSigner; +import software.amazon.awssdk.http.auth.aws.signer.RegionSet; +import software.amazon.awssdk.http.auth.spi.scheme.AuthSchemeOption; +import software.amazon.awssdk.identity.spi.Identity; +import software.amazon.awssdk.metrics.MetricCollector; +import software.amazon.awssdk.services.samplesvc.endpoints.SampleSvcEndpointParams; +import software.amazon.awssdk.services.samplesvc.endpoints.SampleSvcEndpointProvider; +import software.amazon.awssdk.services.samplesvc.jmespath.internal.JmesPathRuntime; +import software.amazon.awssdk.services.samplesvc.model.ListOfObjectsOperationRequest; +import software.amazon.awssdk.utils.CollectionUtils; + +@Generated("software.amazon.awssdk:codegen") +@SdkInternalApi +public final class SampleSvcResolveEndpointInterceptor implements ExecutionInterceptor { + @Override + public SdkRequest modifyRequest(Context.ModifyRequest context, ExecutionAttributes executionAttributes) { + SdkRequest result = context.request(); + if (AwsEndpointProviderUtils.endpointIsDiscovered(executionAttributes)) { + return result; + } + SampleSvcEndpointProvider provider = (SampleSvcEndpointProvider) executionAttributes + .getAttribute(SdkInternalExecutionAttribute.ENDPOINT_PROVIDER); + try { + long resolveEndpointStart = System.nanoTime(); + SampleSvcEndpointParams endpointParams = ruleParams(result, executionAttributes); + Endpoint endpoint = provider.resolveEndpoint(endpointParams).join(); + Duration resolveEndpointDuration = Duration.ofNanos(System.nanoTime() - resolveEndpointStart); + Optional metricCollector = executionAttributes + .getOptionalAttribute(SdkExecutionAttribute.API_CALL_METRIC_COLLECTOR); + metricCollector.ifPresent(mc -> mc.reportMetric(CoreMetric.ENDPOINT_RESOLVE_DURATION, resolveEndpointDuration)); + if (!AwsEndpointProviderUtils.disableHostPrefixInjection(executionAttributes)) { + Optional hostPrefix = hostPrefix(executionAttributes.getAttribute(SdkExecutionAttribute.OPERATION_NAME), + result); + if (hostPrefix.isPresent()) { + endpoint = AwsEndpointProviderUtils.addHostPrefix(endpoint, hostPrefix.get()); + } + } + List endpointAuthSchemes = endpoint.attribute(AwsEndpointAttribute.AUTH_SCHEMES); + SelectedAuthScheme selectedAuthScheme = executionAttributes + .getAttribute(SdkInternalExecutionAttribute.SELECTED_AUTH_SCHEME); + if (endpointAuthSchemes != null && selectedAuthScheme != null) { + selectedAuthScheme = authSchemeWithEndpointSignerProperties(endpointAuthSchemes, selectedAuthScheme); + executionAttributes.putAttribute(SdkInternalExecutionAttribute.SELECTED_AUTH_SCHEME, selectedAuthScheme); + } + executionAttributes.putAttribute(SdkInternalExecutionAttribute.RESOLVED_ENDPOINT, endpoint); + setMetricValues(endpoint, executionAttributes); + return result; + } catch (CompletionException e) { + Throwable cause = e.getCause(); + if (cause instanceof SdkClientException) { + throw (SdkClientException) cause; + } else { + throw SdkClientException.create("Endpoint resolution failed", cause); + } + } + } + + @Override + public SdkHttpRequest modifyHttpRequest(Context.ModifyHttpRequest context, ExecutionAttributes executionAttributes) { + Endpoint resolvedEndpoint = executionAttributes.getAttribute(SdkInternalExecutionAttribute.RESOLVED_ENDPOINT); + if (resolvedEndpoint.headers().isEmpty()) { + return context.httpRequest(); + } + SdkHttpRequest.Builder httpRequestBuilder = context.httpRequest().toBuilder(); + resolvedEndpoint.headers().forEach((name, values) -> { + values.forEach(v -> httpRequestBuilder.appendHeader(name, v)); + }); + return httpRequestBuilder.build(); + } + + public static SampleSvcEndpointParams ruleParams(SdkRequest request, ExecutionAttributes executionAttributes) { + SampleSvcEndpointParams.Builder builder = SampleSvcEndpointParams.builder(); + setContextParams(builder, executionAttributes.getAttribute(AwsExecutionAttribute.OPERATION_NAME), request); + setStaticContextParams(builder, executionAttributes.getAttribute(AwsExecutionAttribute.OPERATION_NAME)); + setOperationContextParams(builder, executionAttributes.getAttribute(AwsExecutionAttribute.OPERATION_NAME), request); + return builder.build(); + } + + private static void setContextParams(SampleSvcEndpointParams.Builder params, String operationName, SdkRequest request) { + } + + private static void setStaticContextParams(SampleSvcEndpointParams.Builder params, String operationName) { + switch (operationName) { + case "EmptyStaticContextOperation": + emptyStaticContextOperationStaticContextParams(params); + break; + case "StaticContextOperation": + staticContextOperationStaticContextParams(params); + break; + default: + break; + } + } + + private static void emptyStaticContextOperationStaticContextParams(SampleSvcEndpointParams.Builder params) { + params.stringArrayParam(Arrays.asList()); + } + + private static void staticContextOperationStaticContextParams(SampleSvcEndpointParams.Builder params) { + params.stringArrayParam(Arrays.asList("staticValue1")); + } + + private SelectedAuthScheme authSchemeWithEndpointSignerProperties( + List endpointAuthSchemes, SelectedAuthScheme selectedAuthScheme) { + for (EndpointAuthScheme endpointAuthScheme : endpointAuthSchemes) { + if (!endpointAuthScheme.schemeId().equals(selectedAuthScheme.authSchemeOption().schemeId())) { + continue; + } + AuthSchemeOption.Builder option = selectedAuthScheme.authSchemeOption().toBuilder(); + if (endpointAuthScheme instanceof SigV4AuthScheme) { + SigV4AuthScheme v4AuthScheme = (SigV4AuthScheme) endpointAuthScheme; + if (v4AuthScheme.isDisableDoubleEncodingSet()) { + option.putSignerProperty(AwsV4HttpSigner.DOUBLE_URL_ENCODE, !v4AuthScheme.disableDoubleEncoding()); + } + if (v4AuthScheme.signingRegion() != null) { + option.putSignerProperty(AwsV4HttpSigner.REGION_NAME, v4AuthScheme.signingRegion()); + } + if (v4AuthScheme.signingName() != null) { + option.putSignerProperty(AwsV4HttpSigner.SERVICE_SIGNING_NAME, v4AuthScheme.signingName()); + } + return new SelectedAuthScheme<>(selectedAuthScheme.identity(), selectedAuthScheme.signer(), option.build()); + } + if (endpointAuthScheme instanceof SigV4aAuthScheme) { + SigV4aAuthScheme v4aAuthScheme = (SigV4aAuthScheme) endpointAuthScheme; + if (v4aAuthScheme.isDisableDoubleEncodingSet()) { + option.putSignerProperty(AwsV4aHttpSigner.DOUBLE_URL_ENCODE, !v4aAuthScheme.disableDoubleEncoding()); + } + if (!CollectionUtils.isNullOrEmpty(v4aAuthScheme.signingRegionSet())) { + RegionSet regionSet = RegionSet.create(v4aAuthScheme.signingRegionSet()); + option.putSignerProperty(AwsV4aHttpSigner.REGION_SET, regionSet); + } + if (v4aAuthScheme.signingName() != null) { + option.putSignerProperty(AwsV4aHttpSigner.SERVICE_SIGNING_NAME, v4aAuthScheme.signingName()); + } + return new SelectedAuthScheme<>(selectedAuthScheme.identity(), selectedAuthScheme.signer(), option.build()); + } + throw new IllegalArgumentException("Endpoint auth scheme '" + endpointAuthScheme.name() + + "' cannot be mapped to the SDK auth scheme. Was it declared in the service's model?"); + } + return selectedAuthScheme; + } + + private static void setOperationContextParams(SampleSvcEndpointParams.Builder params, String operationName, SdkRequest request) { + switch (operationName) { + case "ListOfObjectsOperation": + setOperationContextParams(params, (ListOfObjectsOperationRequest) request); + break; + default: + break; + } + } + + private static void setOperationContextParams(SampleSvcEndpointParams.Builder params, ListOfObjectsOperationRequest request) { + JmesPathRuntime.Value input = new JmesPathRuntime.Value(request); + params.stringArrayParam(input.field("nested").field("listOfObjects").wildcard().field("key").stringValues()); + } + + private static Optional hostPrefix(String operationName, SdkRequest request) { + return Optional.empty(); + } + + private void setMetricValues(Endpoint endpoint, ExecutionAttributes executionAttributes) { + if (endpoint.attribute(AwsEndpointAttribute.METRIC_VALUES) != null) { + executionAttributes.getOptionalAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS).ifPresent( + metrics -> endpoint.attribute(AwsEndpointAttribute.METRIC_VALUES).forEach(v -> metrics.addMetric(v))); + } + } +} From a3ef6b6d3992b2ed719a58146d734e1e5c9efe1b Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Wed, 4 Feb 2026 12:43:35 -0800 Subject: [PATCH 8/8] Add changelog --- .../next-release/bugfix-AWSSDKforJavav2-eade5c9.json | 6 ++++++ .../codegen/emitters/tasks/EndpointProviderTasks.java | 10 +++++----- 2 files changed, 11 insertions(+), 5 deletions(-) create mode 100644 .changes/next-release/bugfix-AWSSDKforJavav2-eade5c9.json diff --git a/.changes/next-release/bugfix-AWSSDKforJavav2-eade5c9.json b/.changes/next-release/bugfix-AWSSDKforJavav2-eade5c9.json new file mode 100644 index 000000000000..1360b17def49 --- /dev/null +++ b/.changes/next-release/bugfix-AWSSDKforJavav2-eade5c9.json @@ -0,0 +1,6 @@ +{ + "type": "bugfix", + "category": "AWS SDK for Java v2", + "contributor": "", + "description": "Improve support for operationContextParams with chained index and multi-select expressions and improve support for StringArray endpoint parametes." +} diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/emitters/tasks/EndpointProviderTasks.java b/codegen/src/main/java/software/amazon/awssdk/codegen/emitters/tasks/EndpointProviderTasks.java index eb1a3bbd9560..378ec9863a91 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/emitters/tasks/EndpointProviderTasks.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/emitters/tasks/EndpointProviderTasks.java @@ -165,10 +165,10 @@ private boolean shouldGenerateJmesPathRuntime() { } // if any operation has operationContextParams then we must include jmesPathRuntime - return model.getOperations().values().stream() - .anyMatch(op -> { - Map opContextParams = op.getOperationContextParams(); - return opContextParams != null && !opContextParams.isEmpty(); - }); + return model.getOperations().values().stream() + .anyMatch(op -> { + Map opContextParams = op.getOperationContextParams(); + return opContextParams != null && !opContextParams.isEmpty(); + }); } }