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 6b64bd61f743..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 @@ -19,7 +19,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; -import java.util.Locale; import java.util.Map; import software.amazon.awssdk.codegen.emitters.GeneratorTask; import software.amazon.awssdk.codegen.emitters.GeneratorTaskParams; @@ -160,21 +159,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/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..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 @@ -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,21 @@ 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 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); if (!leftSide.hasResult()) { continue; } - ParseResult rightSide = parseBracketSpecifier(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; } @@ -272,6 +280,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 +486,38 @@ private ParseResult parseBracketSpecifier(int startPosition, i .parse(startPosition + 1, endPosition - 1); } + /** + * 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 "]" + */ + private ParseResult parseBracketSpecifierWithLhsExpression(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/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..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 @@ -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()); } 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/jmespath/JmesPathParserTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/jmespath/JmesPathParserTest.java index 7687a36fbcc4..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 @@ -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 parsed as: + // IndexExpression(IndexExpression(IndexExpression(listOfUnions, [*]), [string, object.key]), []) + assertThat(expression.isIndexExpression()).isTrue(); + + // the right most flatten + assertThat(expression.asIndexExpression().bracketSpecifier().isBracketSpecifierWithoutContents()).isTrue(); + + // Middle: [string, object.key] + 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 = 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"); + + // 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 public void testSubExpressionWithMultiSelectHash() { Expression expression = JmesPathParser.parse("foo.{bar : baz}"); 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..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 @@ -46,4 +46,10 @@ 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(endpointProviderInterceptor, generatesTo("endpoint-resolve-interceptor-with-stringarray.java")); + } } 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/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))); + } + } +} 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/endpoint-rule-set.json b/test/codegen-generated-classes-test/src/main/resources/codegen-resources/stringarray/endpoint-rule-set.json new file mode 100644 index 000000000000..309db8dce6f3 --- /dev/null +++ b/test/codegen-generated-classes-test/src/main/resources/codegen-resources/stringarray/endpoint-rule-set.json @@ -0,0 +1,53 @@ +{ + "version": "1.0", + "parameters": { + "stringArrayParam": { + "type": "StringArray", + "required": true, + "default": ["defaultValue1", "defaultValue2"], + "documentation": "StringArray test parameters" + }, + "Region": { + "builtIn": "AWS::Region", + "required": true, + "documentation": "The AWS region used to dispatch the request.", + "type": "String" + } + }, + "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}", + "properties": { + "authSchemes": [ + { + "name": "sigv4", + "signingRegion": "{Region}", + "signingName": "stringarray" + } + ] + } + }, + "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 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..1dba2c537874 --- /dev/null +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/stringarray/StringArrayBindingsTest.java @@ -0,0 +1,307 @@ +/* + * 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; + +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(); + } +}