From bcb29c0cf627d95db582a6558ea1eeb748baba93 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Fri, 21 Aug 2026 13:28:28 -0700 Subject: [PATCH 1/7] [Endpoints BDD 1/5] BDD endpoint model and infrastructure (#7302) Add the foundational infrastructure for BDD (Binary Decision Diagram) endpoint resolution without the code generation visitors themselves: Model loading: - EndpointBddModel: parse BDD JSON (endpoint-bdd-1.json) with node graph, conditions, results, and parameters - C2jModels/IntermediateModel: wire EndpointBddModel into the codegen model pipeline - GenerationMojo: discover endpoint-bdd-1.json in codegen-resources Expression/type extensions: - ExpressionParser: support parseExpressionFrom for BDD result templates, negative indexed access, direct negative index syntax - Tokenizer: add directNegativeIndexedAccess and consumeDirectNegative support for BDD template expressions - RuleRuntimeTypeMirror: add LIST_OF_STRING, coalesce return type - EndpointUrlCodeEmitter: add urlFromExpression for BDD url emission - StringConcatExpression: add needsParenthesization flag Runtime additions (RulesFunctions.java.resource): - coalesce(T... args) / coalesce(String... args) - split(value, delimiter, limit) - ite(condition, ifTrue, ifFalse) - listAccess: support negative indices Supporting: - EndpointRulesSpecUtils: simplify rulesEngineFilesFromDirectory - EndpointProviderTestSpec: BDD test case loading support - EndpointProviderTestCase: add bddEndpointProvider field --- .../codegen/maven/plugin/GenerationMojo.java | 7 + .../amazon/awssdk/codegen/C2jModels.java | 18 ++- .../codegen/IntermediateModelBuilder.java | 7 +- .../model/intermediate/IntermediateModel.java | 12 +- .../model/service/EndpointBddModel.java | 142 ++++++++++++++++++ .../poet/rules/EndpointProviderTestSpec.java | 3 +- .../poet/rules2/EndpointUrlCodeEmitter.java | 22 ++- .../codegen/poet/rules2/ExpressionParser.java | 27 ++-- .../poet/rules2/RuleRuntimeTypeMirror.java | 26 +++- .../poet/rules2/StringConcatExpression.java | 5 + .../awssdk/codegen/poet/rules2/Tokenizer.java | 40 ++++- .../rules2/RulesFunctions.java.resource | 65 ++++++++ .../testing/EndpointProviderTestCase.java | 16 ++ 13 files changed, 366 insertions(+), 24 deletions(-) create mode 100644 codegen/src/main/java/software/amazon/awssdk/codegen/model/service/EndpointBddModel.java diff --git a/codegen-maven-plugin/src/main/java/software/amazon/awssdk/codegen/maven/plugin/GenerationMojo.java b/codegen-maven-plugin/src/main/java/software/amazon/awssdk/codegen/maven/plugin/GenerationMojo.java index 9e59e82abdb1..ce8d2d524628 100644 --- a/codegen-maven-plugin/src/main/java/software/amazon/awssdk/codegen/maven/plugin/GenerationMojo.java +++ b/codegen-maven-plugin/src/main/java/software/amazon/awssdk/codegen/maven/plugin/GenerationMojo.java @@ -42,6 +42,7 @@ import software.amazon.awssdk.codegen.model.config.customization.CustomizationConfig; import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel; import software.amazon.awssdk.codegen.model.rules.endpoints.EndpointTestSuiteModel; +import software.amazon.awssdk.codegen.model.service.EndpointBddModel; import software.amazon.awssdk.codegen.model.service.EndpointRuleSetModel; import software.amazon.awssdk.codegen.model.service.Paginators; import software.amazon.awssdk.codegen.model.service.ServiceModel; @@ -62,6 +63,7 @@ public class GenerationMojo extends AbstractMojo { private static final String PAGINATORS_FILE = "paginators-1.json"; private static final String ENDPOINT_RULE_SET_FILE = "endpoint-rule-set.json"; private static final String ENDPOINT_TESTS_FILE = "endpoint-tests.json"; + private static final String ENDPOINT_BDD_FILE = "endpoint-bdd-1.json"; @Parameter(property = "codeGenResources", defaultValue = "${basedir}/src/main/resources/codegen-resources/") @@ -144,6 +146,7 @@ private List initGenerationParams() throws MojoExecutionExcept .waitersModel(loadWaiterModel(modelRootPath)) .paginatorsModel(loadPaginatorModel(modelRootPath)) .endpointRuleSetModel(loadEndpointRuleSetModel(modelRootPath)) + .endpointBddModel(loadEndpointBddModel(modelRootPath)) .endpointTestSuiteModel(loadEndpointTestSuiteModel(modelRootPath)) .build(); String intermediateModelFileNamePrefix = intermediateModelFileNamePrefix(c2jModels); @@ -218,6 +221,10 @@ private EndpointTestSuiteModel loadEndpointTestSuiteModel(Path root) { return loadOptionalModel(EndpointTestSuiteModel.class, root.resolve(ENDPOINT_TESTS_FILE)).orElse(null); } + private EndpointBddModel loadEndpointBddModel(Path root) { + return loadOptionalModel(EndpointBddModel.class, root.resolve(ENDPOINT_BDD_FILE)).orElse(null); + } + /** * Load required model from the project resources. */ diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/C2jModels.java b/codegen/src/main/java/software/amazon/awssdk/codegen/C2jModels.java index 84d136782434..ecfccd1fd020 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/C2jModels.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/C2jModels.java @@ -17,6 +17,7 @@ import software.amazon.awssdk.codegen.model.config.customization.CustomizationConfig; import software.amazon.awssdk.codegen.model.rules.endpoints.EndpointTestSuiteModel; +import software.amazon.awssdk.codegen.model.service.EndpointBddModel; import software.amazon.awssdk.codegen.model.service.EndpointRuleSetModel; import software.amazon.awssdk.codegen.model.service.Paginators; import software.amazon.awssdk.codegen.model.service.ServiceModel; @@ -32,6 +33,7 @@ public class C2jModels { private final Waiters waitersModel; private final EndpointRuleSetModel endpointRuleSetModel; private final EndpointTestSuiteModel endpointTestSuiteModel; + private final EndpointBddModel endpointBddModel; private final CustomizationConfig customizationConfig; private final Paginators paginatorsModel; @@ -39,12 +41,14 @@ private C2jModels(ServiceModel serviceModel, Waiters waitersModel, EndpointRuleSetModel endpointRuleSetModel, EndpointTestSuiteModel endpointTestSuiteModel, + EndpointBddModel endpointBddModel, CustomizationConfig customizationConfig, Paginators paginatorsModel) { this.serviceModel = serviceModel; this.waitersModel = waitersModel; this.endpointRuleSetModel = endpointRuleSetModel; this.endpointTestSuiteModel = endpointTestSuiteModel; + this.endpointBddModel = endpointBddModel; this.customizationConfig = customizationConfig; this.paginatorsModel = paginatorsModel; } @@ -77,12 +81,17 @@ public EndpointTestSuiteModel endpointTestSuiteModel() { return endpointTestSuiteModel; } + public EndpointBddModel endpointBddModel() { + return endpointBddModel; + } + public static class Builder implements SdkBuilder { private ServiceModel serviceModel; private Waiters waitersModel; private EndpointRuleSetModel endpointRuleSetModel; private EndpointTestSuiteModel endpointTestSuiteModel; + private EndpointBddModel endpointBddModel; private CustomizationConfig customizationConfig; private Paginators paginatorsModel; @@ -119,12 +128,17 @@ public Builder endpointTestSuiteModel(EndpointTestSuiteModel endpointTestSuiteMo return this; } + public Builder endpointBddModel(EndpointBddModel endpointBddModel) { + this.endpointBddModel = endpointBddModel; + return this; + } + @Override public C2jModels build() { Waiters waiters = waitersModel != null ? waitersModel : Waiters.none(); Paginators paginators = paginatorsModel != null ? paginatorsModel : Paginators.none(); - return new C2jModels(serviceModel, waiters, endpointRuleSetModel, endpointTestSuiteModel, customizationConfig, - paginators); + return new C2jModels(serviceModel, waiters, endpointRuleSetModel, endpointTestSuiteModel, endpointBddModel, + customizationConfig, paginators); } } } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/IntermediateModelBuilder.java b/codegen/src/main/java/software/amazon/awssdk/codegen/IntermediateModelBuilder.java index 8848973412d9..231601f43482 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/IntermediateModelBuilder.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/IntermediateModelBuilder.java @@ -39,6 +39,7 @@ import software.amazon.awssdk.codegen.model.rules.endpoints.EndpointTestSuiteModel; import software.amazon.awssdk.codegen.model.service.AuthType; import software.amazon.awssdk.codegen.model.service.CustomOperationContextParam; +import software.amazon.awssdk.codegen.model.service.EndpointBddModel; import software.amazon.awssdk.codegen.model.service.EndpointRuleSetModel; import software.amazon.awssdk.codegen.model.service.Operation; import software.amazon.awssdk.codegen.model.service.Paginators; @@ -65,6 +66,7 @@ public class IntermediateModelBuilder { private final Waiters waiters; private final EndpointRuleSetModel endpointRuleSet; private final EndpointTestSuiteModel endpointTestSuiteModel; + private final EndpointBddModel endpointBddModel; public IntermediateModelBuilder(C2jModels models) { this.customConfig = models.customizationConfig(); @@ -76,6 +78,7 @@ public IntermediateModelBuilder(C2jModels models) { this.waiters = models.waitersModel(); this.endpointRuleSet = models.endpointRuleSetModel(); this.endpointTestSuiteModel = models.endpointTestSuiteModel(); + this.endpointBddModel = models.endpointBddModel(); } @@ -138,7 +141,8 @@ public IntermediateModel build() { IntermediateModel fullModel = new IntermediateModel( constructMetadata(service, customConfig), operations, shapes, customConfig, endpointOperation, paginators.getPagination(), namingStrategy, - waiters.getWaiters(), endpointRuleSet, endpointTestSuiteModel, service.getClientContextParams()); + waiters.getWaiters(), endpointRuleSet, endpointTestSuiteModel, endpointBddModel, + service.getClientContextParams()); customization.postprocess(fullModel); @@ -160,6 +164,7 @@ public IntermediateModel build() { fullModel.getWaiters(), fullModel.getEndpointRuleSetModel(), endpointTestSuiteModel, + fullModel.getEndpointBddModel(), service.getClientContextParams()); linkMembersToShapes(trimmedModel); diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/IntermediateModel.java b/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/IntermediateModel.java index 892245e0cffa..e5397ed32cdc 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/IntermediateModel.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/IntermediateModel.java @@ -28,6 +28,7 @@ import software.amazon.awssdk.codegen.model.config.customization.CustomizationConfig; import software.amazon.awssdk.codegen.model.rules.endpoints.EndpointTestSuiteModel; import software.amazon.awssdk.codegen.model.service.ClientContextParam; +import software.amazon.awssdk.codegen.model.service.EndpointBddModel; import software.amazon.awssdk.codegen.model.service.EndpointRuleSetModel; import software.amazon.awssdk.codegen.model.service.PaginatorDefinition; import software.amazon.awssdk.codegen.model.service.WaiterDefinition; @@ -60,6 +61,9 @@ public final class IntermediateModel { @JsonIgnore private NamingStrategy namingStrategy; + @JsonIgnore + private EndpointBddModel endpointBddModel; + private Map clientContextParams; static { @@ -80,7 +84,7 @@ public IntermediateModel(Metadata metadata, Map shapes, CustomizationConfig customizationConfig) { this(metadata, operations, shapes, customizationConfig, null, - Collections.emptyMap(), null, Collections.emptyMap(), null, null, null); + Collections.emptyMap(), null, Collections.emptyMap(), null, null, null, null); } public IntermediateModel( @@ -94,6 +98,7 @@ public IntermediateModel( Map waiters, EndpointRuleSetModel endpointRuleSetModel, EndpointTestSuiteModel endpointTestSuiteModel, + EndpointBddModel endpointBddModel, Map clientContextParams) { this.metadata = metadata; this.operations = operations; @@ -105,6 +110,7 @@ public IntermediateModel( this.waiters = waiters; this.endpointRuleSetModel = endpointRuleSetModel; this.endpointTestSuiteModel = endpointTestSuiteModel; + this.endpointBddModel = endpointBddModel; this.clientContextParams = clientContextParams; } @@ -183,6 +189,10 @@ public EndpointTestSuiteModel getEndpointTestSuiteModel() { return endpointTestSuiteModel; } + public EndpointBddModel getEndpointBddModel() { + return endpointBddModel; + } + public Map getClientContextParams() { return clientContextParams; } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/model/service/EndpointBddModel.java b/codegen/src/main/java/software/amazon/awssdk/codegen/model/service/EndpointBddModel.java new file mode 100644 index 000000000000..7173a9935ccd --- /dev/null +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/model/service/EndpointBddModel.java @@ -0,0 +1,142 @@ +/* + * 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.model.service; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import software.amazon.awssdk.codegen.model.rules.endpoints.ConditionModel; +import software.amazon.awssdk.codegen.model.rules.endpoints.ParameterModel; +import software.amazon.awssdk.codegen.model.rules.endpoints.RuleModel; + +public class EndpointBddModel { + private String serviceId; + private String version; + private Map parameters; + private List conditions; + private List results; + private int root; + private int nodeCount; + private String nodes; // Base64-encoded binary representation of BDD nodes. + + public String getServiceId() { + return serviceId; + } + + public void setServiceId(String serviceId) { + this.serviceId = serviceId; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public Map getParameters() { + return parameters; + } + + public void setParameters(Map parameters) { + this.parameters = parameters; + } + + public List getConditions() { + return conditions; + } + + public void setConditions(List conditions) { + this.conditions = conditions; + } + + public List getResults() { + return results; + } + + public void setResults(List results) { + this.results = results; + } + + public String getNodes() { + return nodes; + } + + public int getRoot() { + return root; + } + + public void setRoot(int root) { + this.root = root; + } + + public int getNodeCount() { + return nodeCount; + } + + public void setNodeCount(int nodeCount) { + this.nodeCount = nodeCount; + } + + public void setNodes(String nodes) { + this.nodes = nodes; + } + + public List getDecodedNodes() { + List out = new ArrayList<>(nodeCount); + byte[] data = Base64.getDecoder().decode(nodes); + ByteBuffer buf = ByteBuffer.wrap(data); // big-endian by default + while (buf.remaining() >= 12) { + int conditionIndex = buf.getInt(); + int highRef = buf.getInt(); + int lowRef = buf.getInt(); + out.add(new BddNode(conditionIndex, highRef, lowRef)); + } + return out; + } + + public static class BddNode { + int conditionIndex; + int highRef; + int lowRef; + + public BddNode(int conditionIndex, int highRef, int lowRef) { + this.conditionIndex = conditionIndex; + this.highRef = highRef; + this.lowRef = lowRef; + } + + public int getConditionIndex() { + return conditionIndex; + } + + public int getHighRef() { + return highRef; + } + + public int getLowRef() { + return lowRef; + } + + @Override + public String toString() { + return "[C" + conditionIndex + ", " + highRef + ", " + lowRef + "]"; + } + } +} diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointProviderTestSpec.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointProviderTestSpec.java index 4104703f88d8..b2c6030e2b1c 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointProviderTestSpec.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointProviderTestSpec.java @@ -87,8 +87,9 @@ private MethodSpec testsCasesMethod() { CustomizationConfig customizationConfig = model.getCustomizationConfig(); model.getEndpointTestSuiteModel().getTestCases().forEach(test -> { - b.addStatement("testCases.add(new $T($L, $L))", + b.addStatement("testCases.add(new $T($S, $L, $L))", EndpointProviderTestCase.class, + test.getDocumentation(), createTestCase(test), TestGeneratorUtils.createExpect(customizationConfig, test.getExpect(), null, null)); }); diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/EndpointUrlCodeEmitter.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/EndpointUrlCodeEmitter.java index 957c0a72e933..d69379396db8 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/EndpointUrlCodeEmitter.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/EndpointUrlCodeEmitter.java @@ -33,7 +33,7 @@ * base path (no query or fragment). Dynamic (template based) resolution are only supported in the host * and path segments in pre-parsing. Otherwise, we fall back to runtime parsing. */ -final class EndpointUrlCodeEmitter { +public final class EndpointUrlCodeEmitter { private static final String HTTPS_SCHEME_PREFIX = "https://"; private static final String HTTP_SCHEME_PREFIX = "http://"; @@ -56,16 +56,26 @@ private EndpointUrlCodeEmitter() { * @param codegenVisitor the parent code generator, used to emit sub-expressions */ static void emit(RuleExpression urlExpr, CodeBlock.Builder builder, CodeGeneratorVisitor codegenVisitor) { + emit(urlExpr, builder, (RuleExpressionVisitor) codegenVisitor); + } + + /** + * Emit the optimal EndpointUrl construction code for the given URL expression. + * + *

This overload accepts any {@link RuleExpressionVisitor} that emits code into the same builder, + * allowing both the standard CodeGeneratorVisitor and BDD visitors to share the same URL emission logic. + */ + public static void emit(RuleExpression urlExpr, CodeBlock.Builder builder, RuleExpressionVisitor visitor) { if (urlExpr instanceof LiteralStringExpression) { emitFromLiteralString(((LiteralStringExpression) urlExpr).value(), builder); return; } if (urlExpr instanceof StringConcatExpression) { - emitFromStringConcat((StringConcatExpression) urlExpr, builder, codegenVisitor); + emitFromStringConcat((StringConcatExpression) urlExpr, builder, visitor); return; } // Expression type we can't decompose (e.g. variable reference, function call) - emitRuntimeParse(urlExpr, builder, codegenVisitor); + emitRuntimeParse(urlExpr, builder, visitor); } /** @@ -140,7 +150,7 @@ private static void emitFromLiteralString(String url, CodeBlock.Builder builder) */ private static void emitFromStringConcat(StringConcatExpression concatExpr, CodeBlock.Builder builder, - CodeGeneratorVisitor codegenVisitor) { + RuleExpressionVisitor codegenVisitor) { List expressions = concatExpr.expressions(); if (expressions.isEmpty()) { emitRuntimeParse(concatExpr, builder, codegenVisitor); @@ -226,7 +236,7 @@ private static void emitFromStringConcat(StringConcatExpression concatExpr, * Emit EndpointUrl.fromString(urlExpr) for runtime parsing when static decomposition isn't possible. */ private static void emitRuntimeParse(RuleExpression urlExpr, CodeBlock.Builder builder, - CodeGeneratorVisitor codegenVisitor) { + RuleExpressionVisitor codegenVisitor) { builder.add("$T.fromString(", EndpointUrl.class); urlExpr.accept(codegenVisitor); builder.add(")"); @@ -240,7 +250,7 @@ private static void emitRuntimeParse(RuleExpression urlExpr, CodeBlock.Builder b * emission logic. */ private static void emitConcatExpression(List parts, CodeBlock.Builder builder, - CodeGeneratorVisitor codegenVisitor) { + RuleExpressionVisitor codegenVisitor) { if (parts.isEmpty()) { builder.add("$S", ""); return; diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/ExpressionParser.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/ExpressionParser.java index 189a8bb63cf0..da09d5fd0b54 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/ExpressionParser.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/ExpressionParser.java @@ -301,7 +301,7 @@ private static RuleExpression getAttrExpression(List argv) { } TreeNode argv0 = argv.get(0); TreeNode argv1 = argv.get(1); - RuleExpression variable = getReference(argv0); + RuleExpression variable = parseExpressionFrom(argv0); TreeNode nameNode = argv1; if (!(nameNode instanceof JrsString)) { throw new IllegalArgumentException("expecting node to be string, got instead starting token: " + nameNode.asToken()); @@ -325,6 +325,22 @@ private static RuleExpression getAttrExpression(List argv) { tokenizer.expectAtEof("indexed access"); return indexedAccessBuilder.build(); } + if (tokenizer.isDirectNegativeIndexedAccess()) { + IndexedAccessExpression.Builder indexedAccessBuilder = IndexedAccessExpression.builder(); + tokenizer.consumeDirectNegativeIndexed(i -> indexedAccessBuilder + .source(memberAccessBuilder.directIndex(true).build()) + .index(i) + .build()); + tokenizer.expectAtEof("negative indexed access"); + return indexedAccessBuilder.build(); + } + if (tokenizer.isNegativeIndexedAccess()) { + IndexedAccessExpression.Builder indexedAccessBuilder = IndexedAccessExpression.builder(); + tokenizer.consumeNegativeIndexed((n, i) -> indexedAccessBuilder.source(memberAccessBuilder.name(n).build()) + .index(i)); + tokenizer.expectAtEof("negative indexed access"); + return indexedAccessBuilder.build(); + } if (tokenizer.isIdentifier()) { tokenizer.consumeIdentifier(memberAccessBuilder::name); tokenizer.expectAtEof("member access"); @@ -335,15 +351,6 @@ private static RuleExpression getAttrExpression(List argv) { tokenizer.peek())); } - private static RuleExpression getReference(TreeNode node) { - if (!node.isObject()) { - throw new IllegalArgumentException("expecting reference object, got instead: " + node); - } - JrsObject obj = (JrsObject) node; - String reference = obj.get("ref").asText(); - return new VariableReferenceExpression(reference); - } - public static PropertiesExpression parsePropertiesExpression(JrsObject object) { PropertiesExpression.Builder builder = PropertiesExpression.builder(); Iterator fieldsIterator = object.fieldNames(); diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/RuleRuntimeTypeMirror.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/RuleRuntimeTypeMirror.java index ff42af43d8f4..6e101fb0cd2f 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/RuleRuntimeTypeMirror.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/RuleRuntimeTypeMirror.java @@ -192,10 +192,34 @@ static List builtInFunctions(Map types) { // still does the trick for codegen. RuleFunctionMirror .builder("listAccess") - .returns(BOOLEAN) + .returns(STRING) .addArgument("value", LIST_OF_STRING) .addArgument("index", INTEGER) .containingType(containingType) + .build(), + RuleFunctionMirror + .builder("split") + .returns(LIST_OF_STRING) + .addArgument("value", STRING) + .addArgument("delimiter", STRING) + .addArgument("limit", INTEGER) + .containingType(containingType) + .build(), + RuleFunctionMirror + .builder("ite") + .returns(STRING) + .addArgument("condition", BOOLEAN) + .addArgument("ifTrue", STRING) + .addArgument("ifFalse", STRING) + .containingType(containingType) + .build(), + // coalesce is variadic and generic (return type mirrors type of arguments) + RuleFunctionMirror + .builder("coalesce") + .returns(VOID) // generic, but we must provide a type + .addArgument("value1", VOID) // variadic and generic, but add 2 generic args + .addArgument("value2", VOID) + .containingType(containingType) .build() ); } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/StringConcatExpression.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/StringConcatExpression.java index 106f3cc0291a..f8cefb253356 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/StringConcatExpression.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/StringConcatExpression.java @@ -98,6 +98,11 @@ public Builder addExpression(RuleExpression expr) { return this; } + public Builder addExpressions(List expressions) { + this.expressions.addAll(expressions); + return this; + } + public StringConcatExpression build() { return new StringConcatExpression(this); } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/Tokenizer.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/Tokenizer.java index c1f48710c816..18afc8fe6efe 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/Tokenizer.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/Tokenizer.java @@ -37,7 +37,8 @@ private static List tokenize(String source) { List tokens = new ArrayList<>(); TokenizerState state = new TokenizerState(source); do { - Token token = next(state); + Token previous = tokens.isEmpty() ? null : tokens.get(tokens.size() - 1); + Token token = next(state, previous); tokens.add(token); if (token.type == TokenKind.EOF) { break; @@ -46,7 +47,7 @@ private static List tokenize(String source) { return tokens; } - private static Token next(TokenizerState state) { + private static Token next(TokenizerState state, Token previous) { if (!state.hasNext()) { return EOF; } @@ -82,6 +83,13 @@ private static Token next(TokenizerState state) { if (ch == '#') { return new Token(TokenKind.HASH, "#"); } + // Only treat '-' as a distinct token when it opens a negative index, i.e. directly after '[' and followed by a + // digit, as in "[-2]" or "resourceId[-1]". Anywhere else '-' is an ordinary string character (for example + // "s3-fips" or "not a valid host-label"), so it must fall through to string handling to avoid splitting + // literals into extra concatenation terms. + if (ch == '-' && previous != null && previous.type == TokenKind.OPEN_SQUARE && isDigit(state.peek())) { + return new Token(TokenKind.MINUS, "-"); + } if (isDigit(ch)) { return consumeNumber(state, ch); } @@ -224,6 +232,33 @@ public void consumeDirectIndexed(Consumer consumer) { index += 3; } + // e.g., [-2] + public boolean isDirectNegativeIndexedAccess() { + return matches(TokenKind.OPEN_SQUARE, TokenKind.MINUS, TokenKind.NUMBER, TokenKind.CLOSE_SQUARE); + } + + public void consumeDirectNegativeIndexed(Consumer consumer) { + if (!isDirectNegativeIndexedAccess()) { + throw new IllegalStateException("not at direct negative indexed"); + } + consumer.accept(-Integer.parseInt(tokens.get(index + 2).value)); + index += 4; + } + + // e.g., resourceId[-1] + public boolean isNegativeIndexedAccess() { + return matches(TokenKind.IDENTIFIER, TokenKind.OPEN_SQUARE, TokenKind.MINUS, TokenKind.NUMBER, + TokenKind.CLOSE_SQUARE); + } + + public void consumeNegativeIndexed(BiConsumer consumer) { + if (!isNegativeIndexedAccess()) { + throw new IllegalStateException("not at negative indexed"); + } + consumer.accept(tokens.get(index).value, -Integer.parseInt(tokens.get(index + 3).value)); + index += 5; + } + // e.g., {url#scheme} public boolean isNamedAccess() { return matches(TokenKind.OPEN_CURLY, TokenKind.IDENTIFIER, TokenKind.HASH, TokenKind.IDENTIFIER, TokenKind.CLOSE_CURLY); @@ -278,6 +313,7 @@ enum TokenKind { NUMBER, IDENTIFIER, HASH, + MINUS, OPEN_CURLY, CLOSE_CURLY, OPEN_SQUARE, diff --git a/codegen/src/main/resources/software/amazon/awssdk/codegen/rules2/RulesFunctions.java.resource b/codegen/src/main/resources/software/amazon/awssdk/codegen/rules2/RulesFunctions.java.resource index d1944d9ed01e..a4a08c124f24 100644 --- a/codegen/src/main/resources/software/amazon/awssdk/codegen/rules2/RulesFunctions.java.resource +++ b/codegen/src/main/resources/software/amazon/awssdk/codegen/rules2/RulesFunctions.java.resource @@ -2,6 +2,7 @@ import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URLEncoder; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -23,6 +24,64 @@ public class RulesFunctions { private static final int MAX_HOST_LABEL_SIZE = 63; private static final int MIN_BUCKET_SIZE = 3; + @SafeVarargs + public static T coalesce(T... args) { + if (args == null || args.length < 2) { + throw new IllegalArgumentException("coalesce requires at least two arguments"); + } + for (T arg : args) { + if (arg != null) { + return arg; + } + } + // All preceding arguments empty, return last argument (even if empty) + return args[args.length - 1]; + } + + @SafeVarargs + public static String coalesce(String... args) { + if (args == null || args.length < 2) { + throw new IllegalArgumentException("coalesce requires at least two arguments"); + } + for (String arg : args) { + if (arg != null && !arg.isEmpty()) { + return arg; + } + } + return args[args.length - 1]; + } + + public static List split(String value, String delimiter, int limit) { + if (limit == 1) { + return Collections.singletonList(value); + } + if (value.isEmpty()) { + return Collections.singletonList(""); + } + final int delimLen = delimiter.length(); + // Unlimited split if limit == 0 + final int maxSplits = (limit == 0) ? Integer.MAX_VALUE : limit - 1; + List result = new ArrayList<>(Math.min(limit, 8)); + int fromIndex = 0; + int splits = 0; + while (splits < maxSplits) { + int index = value.indexOf(delimiter, fromIndex); + if (index < 0) { + break; + } + result.add(value.substring(fromIndex, index)); + fromIndex = index + delimLen; + splits++; + } + // Remainder (always added) + result.add(value.substring(fromIndex)); + return result; + } + + public static String ite(boolean condition, String ifTrue, String ifFalse) { + return condition ? ifTrue : ifFalse; + } + public static String substring(String value, int startIndex, int stopIndex, boolean reverse) { if (value == null) { return null; @@ -170,6 +229,12 @@ public class RulesFunctions { if (values == null) { return null; } + if (index < 0) { + index = values.size() + index; + if (index < 0) { + return null; + } + } if (index >= values.size()) { return null; } diff --git a/test/ruleset-testing-core/src/main/java/software/amazon/awssdk/core/rules/testing/EndpointProviderTestCase.java b/test/ruleset-testing-core/src/main/java/software/amazon/awssdk/core/rules/testing/EndpointProviderTestCase.java index c731aad8fc78..56da1199e4a4 100644 --- a/test/ruleset-testing-core/src/main/java/software/amazon/awssdk/core/rules/testing/EndpointProviderTestCase.java +++ b/test/ruleset-testing-core/src/main/java/software/amazon/awssdk/core/rules/testing/EndpointProviderTestCase.java @@ -22,12 +22,19 @@ public final class EndpointProviderTestCase { private Supplier testMethod; private Expect expect; + private String name; public EndpointProviderTestCase(Supplier testMethod, Expect expect) { this.testMethod = testMethod; this.expect = expect; } + public EndpointProviderTestCase(String name, Supplier testMethod, Expect expect) { + this.name = name; + this.testMethod = testMethod; + this.expect = expect; + } + public Supplier getTestMethod() { return testMethod; } @@ -43,4 +50,13 @@ public Expect getExpect() { public void setExpect(Expect expect) { this.expect = expect; } + + @Override + public String toString() { + if (name != null) { + return name; + } else { + return super.toString(); + } + } } From e9adf1b0a289d9bbc1f562c4c52e6975362bb698 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Tue, 25 Aug 2026 12:17:49 -0700 Subject: [PATCH 2/7] [Endpoints BDD 2/5] Codegen baseline BDD Resolver (#7303) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(codegen): Add BDD endpoint resolver code generation Implement the BDD endpoint resolver code generation visitors that transform a Binary Decision Diagram into a Java endpoint provider. Code generation visitors: - BddEndpointProviderSpec: main spec emitting the provider class with nodeP/nodeN method dispatch — each BDD node becomes a method returning Endpoint directly (null = no match, throw = error) - BddResultCodeGeneratorVisitor: traverses the BDD node graph and emits node methods with condition checks and edge traversal - ConditionFnCodeGeneratorVisitor: emits condition evaluations (isSet, booleanEquals, stringEquals, function calls, getAttr) - ResultFnCodeGeneratorVisitor: emits endpoint construction and error results from BDD result nodes - AssignTypeInferringVisitor: infers types for BDD register variables from the condition/result expressions - RegistryInfo: metadata for BDD registers (name, type, index) Wiring: - EndpointProviderTasks: when EndpointBddModel is present, generate BddEndpointProvider instead of rules-based provider The generated resolver uses direct method dispatch per BDD node, with simple conditions inlined as ternary expressions for branch-predictor-friendly code paths. * fix(codegen): Fix null Boolean handling in BDD condition visitor Boolean.FALSE != x treats null as true, which is incorrect per the endpoint rules spec (booleanEquals(null, true) should be false). Use Boolean.TRUE.equals(x) instead, which correctly returns false when x is null. --- .../emitters/tasks/EndpointProviderTasks.java | 15 +- .../bdd/AssignTypeInferringVisitor.java | 180 ++ .../rules2/bdd/BddEndpointProviderSpec.java | 695 +++++ .../bdd/BddResultCodeGeneratorVisitor.java | 427 +++ .../bdd/ConditionFnCodeGeneratorVisitor.java | 286 ++ .../codegen/poet/rules2/bdd/RegistryInfo.java | 76 + .../awssdk/codegen/poet/ClientTestModels.java | 86 + .../codegen/poet/rules2/TokenizerTest.java | 52 + .../bdd/BddEndpointProviderSpecTest.java | 95 + .../c2j/query/endpoint-bdd-complement.json | 55 + .../query/endpoint-bdd-default-regional.json | 212 ++ .../client/c2j/query/endpoint-bdd-s3.json | 2551 +++++++++++++++++ .../endpoint-rule-set-default-regional.json | 339 +++ .../bdd/endpoint-provider-bdd-class.java | 198 ++ .../bddendpoints/customization.config | 2 + .../bddendpoints/endpoint-bdd-1.json | 212 ++ .../bddendpoints/endpoint-rule-set.json | 339 +++ .../bddendpoints/endpoint-tests.json | 413 +++ .../bddendpoints/service-2.json | 54 + 19 files changed, 6286 insertions(+), 1 deletion(-) create mode 100644 codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/bdd/AssignTypeInferringVisitor.java create mode 100644 codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/bdd/BddEndpointProviderSpec.java create mode 100644 codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/bdd/BddResultCodeGeneratorVisitor.java create mode 100644 codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/bdd/ConditionFnCodeGeneratorVisitor.java create mode 100644 codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/bdd/RegistryInfo.java create mode 100644 codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules2/bdd/BddEndpointProviderSpecTest.java create mode 100644 codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/query/endpoint-bdd-complement.json create mode 100644 codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/query/endpoint-bdd-default-regional.json create mode 100644 codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/query/endpoint-bdd-s3.json create mode 100644 codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/query/endpoint-rule-set-default-regional.json create mode 100644 codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/bdd/endpoint-provider-bdd-class.java create mode 100644 test/codegen-generated-classes-test/src/main/resources/codegen-resources/bddendpoints/customization.config create mode 100644 test/codegen-generated-classes-test/src/main/resources/codegen-resources/bddendpoints/endpoint-bdd-1.json create mode 100644 test/codegen-generated-classes-test/src/main/resources/codegen-resources/bddendpoints/endpoint-rule-set.json create mode 100644 test/codegen-generated-classes-test/src/main/resources/codegen-resources/bddendpoints/endpoint-tests.json create mode 100644 test/codegen-generated-classes-test/src/main/resources/codegen-resources/bddendpoints/service-2.json 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 b72a33263c5b..2e097a3fe9b1 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 @@ -35,6 +35,7 @@ import software.amazon.awssdk.codegen.poet.rules.EndpointResolverUtilsSpec; import software.amazon.awssdk.codegen.poet.rules.EndpointRulesClientTestSpec; import software.amazon.awssdk.codegen.poet.rules2.EndpointProviderSpec2; +import software.amazon.awssdk.codegen.poet.rules2.bdd.BddEndpointProviderSpec; public final class EndpointProviderTasks extends BaseGeneratorTasks { private final GeneratorTaskParams generatorTaskParams; @@ -49,7 +50,11 @@ protected List createTasks() throws Exception { List tasks = new ArrayList<>(); tasks.add(generateInterface()); tasks.add(generateParams()); - if (shouldGenerateCompiledEndpointRules()) { + if (generatorTaskParams.getModel().getEndpointBddModel() != null) { + tasks.add(generateDefaultProviderBdd()); + tasks.add(new RulesEngineRuntimeLiteGeneratorTask(generatorTaskParams)); + tasks.add(new RulesEngineRuntimeGeneratorTask2(generatorTaskParams)); + } else if (shouldGenerateCompiledEndpointRules()) { tasks.add(generateDefaultProvider2()); tasks.add(new RulesEngineRuntimeLiteGeneratorTask(generatorTaskParams)); tasks.add(new RulesEngineRuntimeGeneratorTask2(generatorTaskParams)); @@ -90,6 +95,14 @@ private GeneratorTask generateDefaultProvider2() { return new PoetGeneratorTask(endpointRulesInternalDir(), model.getFileHeader(), new EndpointProviderSpec2(model)); } + private GeneratorTask generateDefaultProviderBdd() { + return new PoetGeneratorTask( + endpointRulesInternalDir(), + model.getFileHeader(), + new BddEndpointProviderSpec(model) + ); + } + private GeneratorTask generateDefaultPartitionsProvider() { return new PoetGeneratorTask(endpointRulesInternalDir(), model.getFileHeader(), new DefaultPartitionDataProviderSpec(model)); diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/bdd/AssignTypeInferringVisitor.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/bdd/AssignTypeInferringVisitor.java new file mode 100644 index 000000000000..6c6de10483d7 --- /dev/null +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/bdd/AssignTypeInferringVisitor.java @@ -0,0 +1,180 @@ +/* + * 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.rules2.bdd; + +import java.util.Map; +import software.amazon.awssdk.codegen.poet.rules2.BooleanAndExpression; +import software.amazon.awssdk.codegen.poet.rules2.BooleanNotExpression; +import software.amazon.awssdk.codegen.poet.rules2.EndpointExpression; +import software.amazon.awssdk.codegen.poet.rules2.ErrorExpression; +import software.amazon.awssdk.codegen.poet.rules2.FunctionCallExpression; +import software.amazon.awssdk.codegen.poet.rules2.HeadersExpression; +import software.amazon.awssdk.codegen.poet.rules2.IndexedAccessExpression; +import software.amazon.awssdk.codegen.poet.rules2.LetExpression; +import software.amazon.awssdk.codegen.poet.rules2.ListExpression; +import software.amazon.awssdk.codegen.poet.rules2.LiteralBooleanExpression; +import software.amazon.awssdk.codegen.poet.rules2.LiteralIntegerExpression; +import software.amazon.awssdk.codegen.poet.rules2.LiteralStringExpression; +import software.amazon.awssdk.codegen.poet.rules2.MemberAccessExpression; +import software.amazon.awssdk.codegen.poet.rules2.MethodCallExpression; +import software.amazon.awssdk.codegen.poet.rules2.PropertiesExpression; +import software.amazon.awssdk.codegen.poet.rules2.RuleExpression; +import software.amazon.awssdk.codegen.poet.rules2.RuleExpressionVisitor; +import software.amazon.awssdk.codegen.poet.rules2.RuleFunctionMirror; +import software.amazon.awssdk.codegen.poet.rules2.RuleRuntimeTypeMirror; +import software.amazon.awssdk.codegen.poet.rules2.RuleSetExpression; +import software.amazon.awssdk.codegen.poet.rules2.RuleType; +import software.amazon.awssdk.codegen.poet.rules2.StringConcatExpression; +import software.amazon.awssdk.codegen.poet.rules2.VariableReferenceExpression; + +public class AssignTypeInferringVisitor implements RuleExpressionVisitor { + private final RuleRuntimeTypeMirror typeMirror; + private final Map registerInfoMap; + + public AssignTypeInferringVisitor(RuleRuntimeTypeMirror typeMirror, Map registerInfoMap) { + this.typeMirror = typeMirror; + this.registerInfoMap = registerInfoMap; + } + + @Override + public RuleType visitLiteralBooleanExpression(LiteralBooleanExpression e) { + return RuleRuntimeTypeMirror.BOOLEAN; + } + + @Override + public RuleType visitLiteralIntegerExpression(LiteralIntegerExpression e) { + return RuleRuntimeTypeMirror.INTEGER; + } + + @Override + public RuleType visitLiteralStringExpression(LiteralStringExpression e) { + return RuleRuntimeTypeMirror.STRING; + } + + @Override + public RuleType visitBooleanNotExpression(BooleanNotExpression e) { + return RuleRuntimeTypeMirror.BOOLEAN; + } + + @Override + public RuleType visitBooleanAndExpression(BooleanAndExpression e) { + return RuleRuntimeTypeMirror.BOOLEAN; + } + + @Override + public RuleType visitFunctionCallExpression(FunctionCallExpression e) { + String fn = e.name(); + if ("not".equals(fn)) { + return RuleRuntimeTypeMirror.BOOLEAN; + } + if ("isSet".equals(fn)) { + return RuleRuntimeTypeMirror.BOOLEAN; + } + if ("isNotSet".equals(fn)) { + return RuleRuntimeTypeMirror.BOOLEAN; + } + RuleFunctionMirror func = typeMirror.resolveFunction(e.name()); + return func.returns(); + } + + @Override + public RuleType visitMethodCallExpression(MethodCallExpression e) { + throw new IllegalStateException("Unexpected methodCallExpression"); + } + + @Override + public RuleType visitVariableReferenceExpression(VariableReferenceExpression e) { + RuleType type = registerInfoMap.get(e.variableName()).getRuleType(); + if (type == null) { + // visit the assign condition for this + registerInfoMap.get(e.variableName()).getRuleSetExpression().accept(this); + type = registerInfoMap.get(e.variableName()).getRuleType(); + if (type == null) { + throw new IllegalStateException("Unable to infer registry type information for `" + e.variableName() + "`"); + } + } + return type; + } + + @Override + public RuleType visitMemberAccessExpression(MemberAccessExpression e) { + RuleType sourceType = e.source().accept(this); + if (e.directIndex() && e.name() == null) { + return sourceType; + } + return sourceType.property(e.name()); + } + + @Override + public RuleType visitIndexedAccessExpression(IndexedAccessExpression e) { + RuleType sourceType = e.source().accept(this); + return sourceType.ruleTypeParam(); // get the list inner type + } + + @Override + public RuleType visitStringConcatExpression(StringConcatExpression e) { + return RuleRuntimeTypeMirror.STRING; + } + + @Override + public RuleType visitLetExpression(LetExpression e) { + if (e.bindings().size() != 1) { + throw new IllegalStateException("Expected exactly one binding"); + } + for (Map.Entry kvp : e.bindings().entrySet()) { + String k = kvp.getKey(); + RuleExpression v = kvp.getValue(); + RuleType assignedType = v.accept(this); + registerInfoMap.get(k).setRuleType(assignedType); + } + return RuleRuntimeTypeMirror.BOOLEAN; + } + + @Override + public RuleType visitRuleSetExpression(RuleSetExpression e) { + if (e.conditions().size() != 1) { + throw new IllegalStateException("Expected exactly one condition"); + } + e.conditions().get(0).accept(this); + return RuleRuntimeTypeMirror.VOID; + } + + @Override + public RuleType visitListExpression(ListExpression e) { + // TODO: this could potentially be another type + return RuleRuntimeTypeMirror.LIST_OF_STRING; + } + + @Override + public RuleType visitEndpointExpression(EndpointExpression e) { + throw new IllegalStateException("Unexpected EndpointExpression"); + } + + @Override + public RuleType visitErrorExpression(ErrorExpression e) { + throw new IllegalStateException("Unexpected ErrorExpression"); + } + + @Override + public RuleType visitPropertiesExpression(PropertiesExpression e) { + throw new IllegalStateException("Unexpected PropertiesExpression"); + } + + @Override + public RuleType visitHeadersExpression(HeadersExpression e) { + throw new IllegalStateException("Unexpected HeadersExpression"); + } +} diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/bdd/BddEndpointProviderSpec.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/bdd/BddEndpointProviderSpec.java new file mode 100644 index 000000000000..0f5c3c3fac47 --- /dev/null +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/bdd/BddEndpointProviderSpec.java @@ -0,0 +1,695 @@ +/* + * 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.rules2.bdd; + +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.TreeNode; +import com.fasterxml.jackson.jr.stree.JrsValue; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.TypeName; +import com.squareup.javapoet.TypeSpec; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import javax.lang.model.element.Modifier; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.codegen.model.config.customization.EndpointAuthSchemeConfig; +import software.amazon.awssdk.codegen.model.config.customization.KeyTypePair; +import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel; +import software.amazon.awssdk.codegen.model.intermediate.Metadata; +import software.amazon.awssdk.codegen.model.rules.endpoints.BuiltInParameter; +import software.amazon.awssdk.codegen.model.rules.endpoints.ConditionModel; +import software.amazon.awssdk.codegen.model.rules.endpoints.ParameterModel; +import software.amazon.awssdk.codegen.model.rules.endpoints.RuleModel; +import software.amazon.awssdk.codegen.model.service.EndpointBddModel; +import software.amazon.awssdk.codegen.poet.ClassSpec; +import software.amazon.awssdk.codegen.poet.PoetUtils; +import software.amazon.awssdk.codegen.poet.rules.EndpointRulesSpecUtils; +import software.amazon.awssdk.codegen.poet.rules2.ExpressionParser; +import software.amazon.awssdk.codegen.poet.rules2.PrepareForCodegenVisitor; +import software.amazon.awssdk.codegen.poet.rules2.RuleExpression; +import software.amazon.awssdk.codegen.poet.rules2.RuleRuntimeTypeMirror; +import software.amazon.awssdk.codegen.poet.rules2.RuleType; +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.endpoints.Endpoint; +import software.amazon.awssdk.utils.CompletableFutureUtils; +import software.amazon.awssdk.utils.Validate; + +/** + * Generates the BDD (binary decision diagram) based endpoint provider using a smithy-java-inspired + * approach: each BDD node is emitted as a {@code nodeP} method that returns {@code Endpoint} + * directly (null for no-match). When a complement edge references a node, a {@code nodeN} + * variant is also emitted with swapped branches (condition true → lowRef, false → highRef). + * Simple conditions (isSet, booleanEquals, stringEquals on plain register references) are inlined + * as ternary expressions. Complex conditions that compute and store values are emitted as separate + * {@code cond()} methods. + * + *

The evaluator is allocated per call (lightweight — just register fields, no maps). + * Function-level caches (awsPartition, uriEncode, isVirtualHostable) live on a ThreadLocal + * in the shared RulesFunctions class and benefit all code paths. + */ +public class BddEndpointProviderSpec implements ClassSpec { + /** + * Node references at or above this value denote results; {@code ref - RESULT_OFFSET} is the result index. + * The SEP defines 100_000_000 as the implicit NoMatchRule. The model's results list excludes NoMatchRule, + * so result indices start at 100_000_001 (result 0 in the list). + */ + private static final int RESULT_OFFSET = 100_000_001; + + /** + * The SEP's implicit NoMatchRule reference — indicates no endpoint matched. + */ + private static final int NO_MATCH_RESULT = 100_000_000; + + private final IntermediateModel intermediateModel; + private final EndpointBddModel endpointBddModel; + private final EndpointRulesSpecUtils endpointRulesSpecUtils; + private final Map knownEndpointAttributes; + private final RuleRuntimeTypeMirror typeMirror; + private final Map registerInfoMap; + private final ClassName evaluatorType; + private final List bddNodes; + private final List conditionTypes; + + public BddEndpointProviderSpec(IntermediateModel intermediateModel) { + this.intermediateModel = intermediateModel; + this.endpointBddModel = intermediateModel.getEndpointBddModel(); + this.endpointRulesSpecUtils = new EndpointRulesSpecUtils(intermediateModel); + String packageName = intermediateModel.getMetadata().getFullInternalEndpointRulesPackageName(); + this.typeMirror = new RuleRuntimeTypeMirror(packageName); + this.knownEndpointAttributes = knownEndpointAttributes(intermediateModel); + this.registerInfoMap = buildRegisterInfoMap(); + this.evaluatorType = className().nestedClass("Evaluator"); + this.bddNodes = endpointBddModel.getDecodedNodes(); + this.conditionTypes = analyzeConditions(); + } + + @Override + public TypeSpec poetSpec() { + TypeSpec.Builder builder = PoetUtils.createClassBuilder(className()) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addSuperinterface(endpointRulesSpecUtils.providerInterfaceName()) + .addAnnotation(SdkInternalApi.class); + + builder.addType(evaluatorClass()); + builder.addMethod(resolveEndpointMethod()); + + return builder.build(); + } + + private TypeSpec evaluatorClass() { + TypeSpec.Builder builder = TypeSpec.classBuilder(evaluatorType) + .addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL); + + // params field + builder.addField(FieldSpec.builder(endpointRulesSpecUtils.parametersClassName(), "params").build()); + + // Register fields + registerInfoMap.forEach((k, r) -> { + TypeName type = r.getRuleType().type(); + if (type.isPrimitive() && r.isNullable()) { + type = type.box(); + } + if (!r.isNonRegionParam()) { + builder.addField(FieldSpec.builder(type, r.getName()).build()); + } + }); + + // BDD node methods + builder.addMethods(bddNodeMethods()); + + // Condition methods (only for complex conditions) + builder.addMethods(conditionMethods()); + + // Result methods + builder.addMethods(resultMethods()); + + return builder.build(); + } + + /** + * Generates methods for each BDD node. For each node, emits: + *

    + *
  • {@code nodeP} — positive edge: condition true → highRef, false → lowRef
  • + *
  • {@code nodeN} — complement edge: condition true → lowRef, false → highRef (branches swapped)
  • + *
+ * + *

Complement edges allow the BDD to share nodes: a negative reference {@code -N} means + * "evaluate node N-1 with inverted condition result." Only nodeN variants that are actually + * referenced are emitted to avoid dead code. + * + *

Simple conditions are inlined as ternary expressions. Complex conditions call cond(). + */ + private List bddNodeMethods() { + // First pass: determine which nodes need a complement (nodeN) variant + boolean[] needsNodeN = new boolean[bddNodes.size()]; + for (EndpointBddModel.BddNode node : bddNodes) { + markComplementRef(needsNodeN, node.getHighRef()); + markComplementRef(needsNodeN, node.getLowRef()); + } + // Also check the root reference + markComplementRef(needsNodeN, endpointBddModel.getRoot()); + + List methods = new ArrayList<>(); + + for (int i = 0; i < bddNodes.size(); i++) { + EndpointBddModel.BddNode node = bddNodes.get(i); + int condIdx = node.getConditionIndex(); + + // Node 0 is the terminal sentinel (conditionIndex = -1) — returns null (no match) + if (i == 0) { + if (condIdx != -1) { + throw new IllegalStateException( + "BDD node 0 must be the terminal sentinel with conditionIndex=-1, got: " + condIdx); + } + methods.add(MethodSpec.methodBuilder("nodeP0") + .addModifiers(Modifier.PRIVATE) + .returns(Endpoint.class) + .addStatement("return null") + .build()); + if (needsNodeN[0]) { + methods.add(MethodSpec.methodBuilder("nodeN0") + .addModifiers(Modifier.PRIVATE) + .returns(Endpoint.class) + .addStatement("return null") + .build()); + } + continue; + } + + // All other nodes must have a valid condition index + if (condIdx < 0 || condIdx >= conditionTypes.size()) { + throw new IllegalStateException( + "BDD node " + i + " has invalid conditionIndex=" + condIdx + + " (valid range: 0.." + (conditionTypes.size() - 1) + ")"); + } + + ConditionType ct = conditionTypes.get(condIdx); + + // nodeP: condition true → highRef, false → lowRef + methods.add(MethodSpec.methodBuilder("nodeP" + i) + .addModifiers(Modifier.PRIVATE) + .returns(Endpoint.class) + .addCode(nodeBody(ct, condIdx, node.getHighRef(), node.getLowRef())) + .build()); + + // nodeN: complement — condition true → lowRef, false → highRef (swapped) + if (needsNodeN[i]) { + methods.add(MethodSpec.methodBuilder("nodeN" + i) + .addModifiers(Modifier.PRIVATE) + .returns(Endpoint.class) + .addCode(nodeBody(ct, condIdx, node.getLowRef(), node.getHighRef())) + .build()); + } + } + + return methods; + } + + /** + * Marks a node as needing a complement (nodeN) variant if the reference is a negative node ref. + */ + private void markComplementRef(boolean[] needsNodeN, int ref) { + if (ref < -1) { + int nodeIndex = (-ref) - 1; + if (nodeIndex < needsNodeN.length) { + needsNodeN[nodeIndex] = true; + } + } + } + + /** + * Generates the body of a nodeP method as a ternary return statement. + */ + private CodeBlock nodeBody(ConditionType ct, int condIdx, int highRef, int lowRef) { + CodeBlock.Builder code = CodeBlock.builder(); + String condExpr = conditionExpression(ct, condIdx); + code.addStatement("return $L\n ? $L\n : $L", + condExpr, + referenceExpression(highRef), + referenceExpression(lowRef)); + return code.build(); + } + + /** + * Returns the Java expression for evaluating a condition. + * Simple conditions are inlined; complex ones call cond(). + */ + private String conditionExpression(ConditionType ct, int condIdx) { + switch (ct.kind) { + case ISSET: + return ct.registerName + " != null"; + case BOOL_TRUE: + return "Boolean.TRUE.equals(" + ct.registerName + ")"; + case BOOL_FALSE: + return "Boolean.FALSE.equals(" + ct.registerName + ")"; + case STRING_EQ: + return ct.registerName + " != null && " + ct.registerName + ".equals(" + ct.stringConstant + ")"; + case COMPLEX: + default: + return "cond" + condIdx + "()"; + } + } + + /** + * Returns the Java expression for a BDD reference: a node call, a result call, or null (terminal/no-match). + * A negative reference (other than -1) is a complement edge: {@code -N} means "call nodeN()" + * which evaluates the same condition but swaps the branch targets. + */ + private String referenceExpression(int ref) { + // Terminal refs: SEP defines 1 and -1 as terminals (true/false sinks) + if (ref == 1 || ref == -1) { + return "null"; + } + // NoMatchRule: SEP defines 100_000_000 as implicit no-match + if (ref == NO_MATCH_RESULT) { + return "null"; + } + // Result refs + if (ref >= RESULT_OFFSET) { + int resultIndex = ref - RESULT_OFFSET; + if (resultIndex >= endpointBddModel.getResults().size()) { + throw new IllegalStateException( + "BDD result reference " + ref + " maps to index " + resultIndex + + " but only " + endpointBddModel.getResults().size() + " results exist"); + } + return "result" + resultIndex + "()"; + } + // Invalid ref + if (ref == 0) { + throw new IllegalStateException("BDD reference 0 is invalid (node indices are 1-based)"); + } + // Complement edge: negative ref → nodeN (swapped branches) + if (ref < 0) { + int nodeIndex = (-ref) - 1; + if (nodeIndex >= bddNodes.size()) { + throw new IllegalStateException( + "BDD complement reference " + ref + " maps to node index " + nodeIndex + + " but only " + bddNodes.size() + " nodes exist"); + } + return "nodeN" + nodeIndex + "()"; + } + // Positive node ref (1-based → 0-based) + int nodeIndex = ref - 1; + if (nodeIndex >= bddNodes.size()) { + throw new IllegalStateException( + "BDD node reference " + ref + " maps to index " + nodeIndex + + " but only " + bddNodes.size() + " nodes exist"); + } + return "nodeP" + nodeIndex + "()"; + } + + /** + * Generates condition methods only for complex conditions (those that compute and store values). + */ + private List conditionMethods() { + List methods = new ArrayList<>(); + for (int cI = 0; cI < endpointBddModel.getConditions().size(); cI++) { + if (conditionTypes.get(cI).kind != ConditionKind.COMPLEX) { + continue; + } + CodeBlock.Builder codeBuilder = CodeBlock.builder(); + ConditionModel c = endpointBddModel.getConditions().get(cI); + // Use existing expression parser for complex conditions + RuleModel synthetic = new RuleModel(); + synthetic.setType("error"); + synthetic.setError("synthetic"); + synthetic.setConditions(Collections.singletonList(c)); + RuleExpression parsedSynthetic = ExpressionParser + .parseRuleSetExpression(synthetic) + .accept(new PrepareForCodegenVisitor()); + parsedSynthetic.accept(new ConditionFnCodeGeneratorVisitor(codeBuilder, typeMirror, registerInfoMap, + endpointRulesSpecUtils)); + methods.add(MethodSpec.methodBuilder("cond" + cI) + .addModifiers(Modifier.PRIVATE) + .returns(boolean.class) + .addCode(codeBuilder.build()) + .build()); + } + return methods; + } + + /** + * Generates result methods that return Endpoint directly or throw SdkClientException for errors. + */ + private List resultMethods() { + List methods = new ArrayList<>(); + for (int rI = 0; rI < endpointBddModel.getResults().size(); rI++) { + CodeBlock.Builder codeBuilder = CodeBlock.builder(); + RuleExpression parsedSynthetic = ExpressionParser + .parseRuleSetExpression(endpointBddModel.getResults().get(rI)) + .accept(new PrepareForCodegenVisitor()); + parsedSynthetic.accept(new BddResultCodeGeneratorVisitor( + codeBuilder, typeMirror, registerInfoMap, knownEndpointAttributes, endpointRulesSpecUtils, + intermediateModel.getCustomizationConfig().useS3ExpressSessionAuth())); + methods.add(MethodSpec.methodBuilder("result" + rI) + .addModifiers(Modifier.PRIVATE) + .returns(Endpoint.class) + .addCode(codeBuilder.build()) + .build()); + } + return methods; + } + + private MethodSpec resolveEndpointMethod() { + MethodSpec.Builder builder = MethodSpec.methodBuilder("resolveEndpoint") + .addModifiers(Modifier.PUBLIC) + .returns(endpointRulesSpecUtils.resolverReturnType()) + .addAnnotation(Override.class) + .addParameter(endpointRulesSpecUtils.parametersClassName(), "endpointParams"); + + builder.addCode(validateRequiredParams()); + + builder.beginControlFlow("try"); + + // Allocate evaluator per call — lightweight (just fields, no maps), immediately young-gen collected. + // Function caches (awsPartition, uriEncode, isVirtualHostable) live on RulesFunctions' ThreadLocal + // and are shared across calls regardless. + builder.addStatement("$T evaluator = new $T()", evaluatorType, evaluatorType); + + // Initialize evaluator from params + builder.addStatement("evaluator.params = endpointParams"); + String regionParamName = regionParamName(); + if (regionParamName != null) { + String regionMethodName = endpointRulesSpecUtils.paramMethodName(regionParamName); + builder.addStatement("evaluator.$L = endpointParams.$L() == null ? null : endpointParams.$L().id()", + registerInfoMap.get(regionParamName).getName(), + regionMethodName, regionMethodName); + } + + // Evaluate BDD — returns Endpoint directly (null = no match) + builder.addStatement("$T result = evaluator.$L", + Endpoint.class, referenceExpression(endpointBddModel.getRoot())); + builder.beginControlFlow("if (result == null)") + .addStatement("return $T.failedFuture($T.create($S))", + CompletableFutureUtils.class, SdkClientException.class, + "Rule engine did not reach an error or endpoint result") + .endControlFlow(); + builder.addStatement("return $T.completedFuture(result)", CompletableFuture.class); + + // Catch errors thrown from result methods + builder.nextControlFlow("catch ($T e)", SdkClientException.class); + builder.addStatement("String errorMsg = e.getMessage()"); + builder.beginControlFlow("if (errorMsg != null && errorMsg.contains(\"Invalid ARN\") && errorMsg.contains(\":s3:::\"))") + .addStatement("return $T.failedFuture($T.create(errorMsg + $S, e))", + CompletableFutureUtils.class, SdkClientException.class, + ". Use the bucket name instead of simple bucket ARNs in GetBucketLocationRequest.") + .endControlFlow(); + builder.addStatement("return $T.failedFuture(e)", CompletableFutureUtils.class); + + builder.nextControlFlow("catch ($T error)", Exception.class); + builder.addStatement("return $T.failedFuture(error)", CompletableFutureUtils.class); + + builder.endControlFlow(); + + return builder.build(); + } + + @Override + public ClassName className() { + Metadata md = intermediateModel.getMetadata(); + return ClassName.get(md.getFullInternalEndpointRulesPackageName(), + "Default" + endpointRulesSpecUtils.providerInterfaceName().simpleName()); + } + + // ---- Condition analysis ---- + + /** + * Analyzes each condition in the BDD model to determine if it can be inlined in a node method + * or needs a full cond() method. + */ + private List analyzeConditions() { + List types = new ArrayList<>(); + for (ConditionModel condition : endpointBddModel.getConditions()) { + types.add(classifyCondition(condition)); + } + return types; + } + + private ConditionType classifyCondition(ConditionModel condition) { + // Conditions with assign always need a method (they have side effects) + if (condition.getAssign() != null) { + return ConditionType.complex(); + } + + String fn = condition.getFn(); + List argv = condition.getArgv(); + + // isSet({ref}) -> ISSET + if ("isSet".equals(fn) && argv.size() == 1 && isSimpleRef(argv.get(0))) { + String refName = getRefName(argv.get(0)); + return ConditionType.isSet(resolveRegisterAccessExpression(refName)); + } + + // booleanEquals({ref}, true/false) -> BOOL_TRUE or BOOL_FALSE + if ("booleanEquals".equals(fn) && argv.size() == 2 && isSimpleRef(argv.get(0)) && isBooleanLiteral(argv.get(1))) { + String refName = getRefName(argv.get(0)); + boolean value = getBooleanValue(argv.get(1)); + String registerExpr = resolveRegisterAccessExpression(refName); + return value ? ConditionType.boolTrue(registerExpr) : ConditionType.boolFalse(registerExpr); + } + + // stringEquals({ref}, "literal") -> STRING_EQ + if ("stringEquals".equals(fn) && argv.size() == 2 && isSimpleRef(argv.get(0)) && isStringLiteral(argv.get(1))) { + String refName = getRefName(argv.get(0)); + String literal = getStringValue(argv.get(1)); + String registerExpr = resolveRegisterAccessExpression(refName); + // Quote the string literal for use in generated code + String quotedLiteral = "\"" + escapeJavaString(literal) + "\""; + return ConditionType.stringEq(registerExpr, quotedLiteral); + } + + return ConditionType.complex(); + } + + /** + * Resolves a parameter/register name to the Java expression that accesses it in the Evaluator. + * For non-region params, this is {@code params.xxx()}; for registers, it's the field name. + */ + private String resolveRegisterAccessExpression(String name) { + RegistryInfo info = registerInfoMap.get(name); + if (info == null) { + // Fallback — shouldn't happen for well-formed models + return intermediateModel.getNamingStrategy().getVariableName(name); + } + if (info.isNonRegionParam()) { + return "params." + endpointRulesSpecUtils.paramMethodName(info.getNonRegionParamKey()) + "()"; + } + return info.getName(); + } + + private static boolean isSimpleRef(TreeNode node) { + // A simple ref is: {"ref": "SomeName"} — an object with exactly one field "ref" that is a string value + if (!node.isObject() || node.size() != 1) { + return false; + } + TreeNode refNode = node.get("ref"); + return refNode != null && refNode.isValueNode() && refNode.asToken() == JsonToken.VALUE_STRING; + } + + private static String getRefName(TreeNode node) { + return ((JrsValue) node.get("ref")).asText(); + } + + private static boolean isBooleanLiteral(TreeNode node) { + if (!node.isValueNode()) { + return false; + } + JsonToken token = node.asToken(); + return token == JsonToken.VALUE_TRUE || token == JsonToken.VALUE_FALSE; + } + + private static boolean getBooleanValue(TreeNode node) { + return node.asToken() == JsonToken.VALUE_TRUE; + } + + private static boolean isStringLiteral(TreeNode node) { + return node.isValueNode() && node.asToken() == JsonToken.VALUE_STRING; + } + + private static String getStringValue(TreeNode node) { + return ((JrsValue) node).asText(); + } + + private static String escapeJavaString(String s) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + switch (c) { + case '"': + sb.append("\\\""); + break; + case '\\': + sb.append("\\\\"); + break; + case '\n': + sb.append("\\n"); + break; + case '\r': + sb.append("\\r"); + break; + case '\t': + sb.append("\\t"); + break; + default: + sb.append(c); + } + } + return sb.toString(); + } + + // ---- Helpers ---- + + private String regionParamName() { + for (Map.Entry entry : endpointBddModel.getParameters().entrySet()) { + if (entry.getValue().getBuiltInEnum() == BuiltInParameter.AWS_REGION) { + return entry.getKey(); + } + } + return null; + } + + private CodeBlock validateRequiredParams() { + CodeBlock.Builder b = CodeBlock.builder(); + Map parameters = endpointBddModel.getParameters(); + parameters.entrySet().stream() + .filter(e -> Boolean.TRUE.equals(e.getValue().isRequired()) && e.getValue().getDefault() == null) + .forEach(e -> { + b.addStatement("$T.notNull($N.$N(), $S)", + Validate.class, + "endpointParams", + endpointRulesSpecUtils.paramMethodName(e.getKey()), + String.format("Parameter '%s' must not be null", e.getKey())); + }); + return b.build(); + } + + private Map buildRegisterInfoMap() { + Map registryInfo = new LinkedHashMap<>(); + // first add an entry for every parameter + for (Map.Entry entry : endpointBddModel.getParameters().entrySet()) { + String name = intermediateModel.getNamingStrategy().getVariableName(entry.getKey()); + boolean nullable = entry.getValue().getDefault() == null; + boolean isRegionBuiltIn = entry.getValue().getBuiltInEnum() == BuiltInParameter.AWS_REGION; + String nonRegionParamKey = isRegionBuiltIn ? null : entry.getKey(); + registryInfo.put( + entry.getKey(), + new RegistryInfo(name, fromParameterModel(entry.getValue()), + null, nullable, nonRegionParamKey)); + } + // add an entry for every assigned variable. assigns are guaranteed to be globally unique + for (ConditionModel conditionModel : endpointBddModel.getConditions()) { + if (conditionModel.getAssign() != null) { + RuleModel synthetic = new RuleModel(); + synthetic.setType("error"); + synthetic.setError("synthetic"); + synthetic.setConditions(Collections.singletonList(conditionModel)); + String name = intermediateModel.getNamingStrategy().getVariableName(conditionModel.getAssign()); + registryInfo.put( + conditionModel.getAssign(), + new RegistryInfo(name, ExpressionParser.parseRuleSetExpression(synthetic))); + } + } + // visit all the conditions/assignments and infer types + AssignTypeInferringVisitor typeVisitor = new AssignTypeInferringVisitor(typeMirror, registryInfo); + registryInfo.values().forEach(r -> { + if (r.getRuleSetExpression() != null) { + r.getRuleSetExpression().accept(typeVisitor); + } + }); + // assert that we have type information for all registry values + registryInfo.values().forEach(r -> { + if (r.getRuleType() == null) { + throw new IllegalStateException("Unable to infer type for `" + r.getName() + "`"); + } + }); + return Collections.unmodifiableMap(registryInfo); + } + + private static RuleType fromParameterModel(ParameterModel model) { + switch (model.getType().toLowerCase(Locale.ENGLISH)) { + case "boolean": + return RuleRuntimeTypeMirror.BOOLEAN; + case "string": + return RuleRuntimeTypeMirror.STRING; + case "stringarray": + return RuleRuntimeTypeMirror.LIST_OF_STRING; + default: + throw new IllegalStateException("Cannot find rule type for: " + model.getType()); + } + } + + private static Map knownEndpointAttributes(IntermediateModel intermediateModel) { + Map knownEndpointAttributes = null; + EndpointAuthSchemeConfig config = intermediateModel.getCustomizationConfig().getEndpointAuthSchemeConfig(); + if (config != null) { + knownEndpointAttributes = config.getEndpointProviderTestKeys(); + } + if (knownEndpointAttributes == null) { + knownEndpointAttributes = Collections.emptyMap(); + } + return knownEndpointAttributes; + } + + // ---- Condition type classification ---- + + enum ConditionKind { + ISSET, + BOOL_TRUE, + BOOL_FALSE, + STRING_EQ, + COMPLEX + } + + static class ConditionType { + final ConditionKind kind; + final String registerName; // Java expression to access the register/param + final String stringConstant; // Only for STRING_EQ + + private ConditionType(ConditionKind kind, String registerName, String stringConstant) { + this.kind = kind; + this.registerName = registerName; + this.stringConstant = stringConstant; + } + + static ConditionType complex() { + return new ConditionType(ConditionKind.COMPLEX, null, null); + } + + static ConditionType isSet(String registerName) { + return new ConditionType(ConditionKind.ISSET, registerName, null); + } + + static ConditionType boolTrue(String registerName) { + return new ConditionType(ConditionKind.BOOL_TRUE, registerName, null); + } + + static ConditionType boolFalse(String registerName) { + return new ConditionType(ConditionKind.BOOL_FALSE, registerName, null); + } + + static ConditionType stringEq(String registerName, String stringConstant) { + return new ConditionType(ConditionKind.STRING_EQ, registerName, stringConstant); + } + } +} diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/bdd/BddResultCodeGeneratorVisitor.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/bdd/BddResultCodeGeneratorVisitor.java new file mode 100644 index 000000000000..978ec98f7593 --- /dev/null +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/bdd/BddResultCodeGeneratorVisitor.java @@ -0,0 +1,427 @@ +/* + * 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.rules2.bdd; + +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.awssdk.awscore.endpoints.AwsEndpointAttribute; +import software.amazon.awssdk.awscore.endpoints.authscheme.SigV4AuthScheme; +import software.amazon.awssdk.awscore.endpoints.authscheme.SigV4aAuthScheme; +import software.amazon.awssdk.codegen.model.config.customization.KeyTypePair; +import software.amazon.awssdk.codegen.poet.rules.EndpointRulesSpecUtils; +import software.amazon.awssdk.codegen.poet.rules2.BooleanAndExpression; +import software.amazon.awssdk.codegen.poet.rules2.BooleanNotExpression; +import software.amazon.awssdk.codegen.poet.rules2.EndpointExpression; +import software.amazon.awssdk.codegen.poet.rules2.EndpointUrlCodeEmitter; +import software.amazon.awssdk.codegen.poet.rules2.ErrorExpression; +import software.amazon.awssdk.codegen.poet.rules2.FunctionCallExpression; +import software.amazon.awssdk.codegen.poet.rules2.HeadersExpression; +import software.amazon.awssdk.codegen.poet.rules2.IndexedAccessExpression; +import software.amazon.awssdk.codegen.poet.rules2.LetExpression; +import software.amazon.awssdk.codegen.poet.rules2.ListExpression; +import software.amazon.awssdk.codegen.poet.rules2.LiteralBooleanExpression; +import software.amazon.awssdk.codegen.poet.rules2.LiteralIntegerExpression; +import software.amazon.awssdk.codegen.poet.rules2.LiteralStringExpression; +import software.amazon.awssdk.codegen.poet.rules2.MemberAccessExpression; +import software.amazon.awssdk.codegen.poet.rules2.MethodCallExpression; +import software.amazon.awssdk.codegen.poet.rules2.PropertiesExpression; +import software.amazon.awssdk.codegen.poet.rules2.RuleExpression; +import software.amazon.awssdk.codegen.poet.rules2.RuleExpressionVisitor; +import software.amazon.awssdk.codegen.poet.rules2.RuleFunctionMirror; +import software.amazon.awssdk.codegen.poet.rules2.RuleRuntimeTypeMirror; +import software.amazon.awssdk.codegen.poet.rules2.RuleSetExpression; +import software.amazon.awssdk.codegen.poet.rules2.RuleType; +import software.amazon.awssdk.codegen.poet.rules2.StringConcatExpression; +import software.amazon.awssdk.codegen.poet.rules2.VariableReferenceExpression; +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.endpoints.Endpoint; + +/** + * Code generator visitor for BDD result methods that returns {@link Endpoint} directly + * and throws {@link SdkClientException} for error results. This eliminates the RuleResult + * wrapper allocation on the hot path. + */ +public class BddResultCodeGeneratorVisitor implements RuleExpressionVisitor { + private static final Logger log = LoggerFactory.getLogger(BddResultCodeGeneratorVisitor.class); + + private static final ClassName DYNAMIC_ENDPOINT_AUTH_SCHEME_FACTORY = + ClassName.get("software.amazon.awssdk.services.s3.endpoints.authscheme", "DynamicEndpointAuthSchemeFactory"); + + private final CodeBlock.Builder builder; + private final RuleRuntimeTypeMirror typeMirror; + private final Map registerInfoMap; + private final Map knownEndpointAttributes; + private final EndpointRulesSpecUtils endpointRulesSpecUtils; + private final boolean useS3ExpressSessionAuth; + + public BddResultCodeGeneratorVisitor( + CodeBlock.Builder builder, RuleRuntimeTypeMirror typeMirror, + Map registerInfoMap, + Map knownEndpointAttributes, + EndpointRulesSpecUtils endpointRulesSpecUtils, + boolean useS3ExpressSessionAuth) { + this.builder = builder; + this.typeMirror = typeMirror; + this.registerInfoMap = registerInfoMap; + this.knownEndpointAttributes = knownEndpointAttributes; + this.endpointRulesSpecUtils = endpointRulesSpecUtils; + this.useS3ExpressSessionAuth = useS3ExpressSessionAuth; + } + + @Override + public RuleType visitLiteralBooleanExpression(LiteralBooleanExpression e) { + builder.add(Boolean.toString(e.value())); + return RuleRuntimeTypeMirror.BOOLEAN; + } + + @Override + public RuleType visitLiteralIntegerExpression(LiteralIntegerExpression e) { + builder.add(Integer.toString(e.value())); + return RuleRuntimeTypeMirror.INTEGER; + } + + @Override + public RuleType visitLiteralStringExpression(LiteralStringExpression e) { + builder.add("$S", e.value()); + return RuleRuntimeTypeMirror.STRING; + } + + @Override + public RuleType visitBooleanNotExpression(BooleanNotExpression e) { + builder.add("!"); + e.expression().accept(this); + return RuleRuntimeTypeMirror.BOOLEAN; + } + + @Override + public RuleType visitBooleanAndExpression(BooleanAndExpression e) { + List expressions = e.expressions(); + boolean isFirst = true; + for (RuleExpression expr : expressions) { + if (!isFirst) { + builder.add(" && "); + } + expr.accept(this); + isFirst = false; + } + return RuleRuntimeTypeMirror.BOOLEAN; + } + + @Override + public RuleType visitFunctionCallExpression(FunctionCallExpression e) { + String fn = e.name(); + if ("not".equals(fn)) { + builder.add("!("); + e.arguments().get(0).accept(this); + builder.add(")"); + return RuleRuntimeTypeMirror.BOOLEAN; + } + if ("isSet".equals(fn)) { + e.arguments().get(0).accept(this); + builder.add(" != null"); + return RuleRuntimeTypeMirror.BOOLEAN; + } + if ("isNotSet".equals(fn)) { + e.arguments().get(0).accept(this); + builder.add(" == null"); + return RuleRuntimeTypeMirror.BOOLEAN; + } + RuleFunctionMirror func = typeMirror.resolveFunction(e.name()); + builder.add("$T.$L(", func.containingType().type(), func.javaName()); + List args = e.arguments(); + for (int i = 0; i < args.size(); i++) { + if (i > 0) { + builder.add(", "); + } + args.get(i).accept(this); + } + builder.add(")"); + return func.returns(); + } + + @Override + public RuleType visitMethodCallExpression(MethodCallExpression e) { + e.source().accept(this); + builder.add(".$L(", e.name()); + boolean isFirst = true; + for (RuleExpression arg : e.arguments()) { + if (!isFirst) { + builder.add(", "); + } + arg.accept(this); + isFirst = false; + } + builder.add(")"); + return e.type(); + } + + @Override + public RuleType visitVariableReferenceExpression(VariableReferenceExpression e) { + RegistryInfo registryInfo = registerInfoMap.get(e.variableName()); + if (registryInfo.isNonRegionParam()) { + builder.add("params.$L()", endpointRulesSpecUtils.paramMethodName(registryInfo.getNonRegionParamKey())); + } else { + builder.add("$L", registryInfo.getName()); + } + return registerInfoMap.get(e.variableName()).getRuleType(); + } + + @Override + public RuleType visitMemberAccessExpression(MemberAccessExpression e) { + RuleType sourceType = e.source().accept(this); + if (!e.directIndex()) { + builder.add(".$L()", e.name()); + } + return sourceType.property(e.name()); + } + + @Override + public RuleType visitIndexedAccessExpression(IndexedAccessExpression e) { + RuleFunctionMirror func = typeMirror.resolveFunction("listAccess"); + builder.add("$T.$L(", func.containingType().type(), func.javaName()); + RuleType sourceType = e.source().accept(this); + builder.add(", $L)", e.index()); + return sourceType.typeParam(); + } + + @Override + public RuleType visitStringConcatExpression(StringConcatExpression e) { + boolean isFirst = true; + for (RuleExpression expr : e.expressions()) { + if (!isFirst) { + builder.add(" + "); + } + expr.accept(this); + isFirst = false; + } + return RuleRuntimeTypeMirror.STRING; + } + + @Override + public RuleType visitLetExpression(LetExpression e) { + throw new IllegalStateException("Unexpected LetExpression in BDD result"); + } + + @Override + public RuleType visitRuleSetExpression(RuleSetExpression e) { + // BDD results MUST NOT contain any conditions + if (e.conditions().size() != 0) { + throw new IllegalStateException("Expected exactly zero conditions in BDD result"); + } + if (e.isError()) { + return e.error().accept(this); + } + if (e.isEndpoint()) { + return e.endpoint().accept(this); + } + throw new IllegalStateException("Expected Result to be either error or endpoint."); + } + + @Override + public RuleType visitListExpression(ListExpression e) { + builder.add("$T.asList(", Arrays.class); + boolean isFirst = true; + for (RuleExpression expr : e.expressions()) { + if (!isFirst) { + builder.add(", "); + } + expr.accept(this); + isFirst = false; + } + builder.add(")"); + return RuleRuntimeTypeMirror.LIST_OF_STRING; + } + + @Override + public RuleType visitEndpointExpression(EndpointExpression e) { + // Return Endpoint directly — no RuleResult wrapper + builder.add("return $T.builder().endpointUrl(", Endpoint.class); + EndpointUrlCodeEmitter.emit(e.url(), builder, this); + builder.add(")"); + e.headers().accept(this); + e.properties().accept(this); + builder.addStatement(".build()"); + return null; + } + + @Override + public RuleType visitPropertiesExpression(PropertiesExpression e) { + Map properties = e.properties(); + properties.forEach((k, v) -> { + if ("authSchemes".equals(k)) { + addAuthSchemesBlock(v); + } else if ("metricValues".equals(k)) { + addMetricValuesBlock(v); + } else if (knownEndpointAttributes.containsKey(k)) { + addAttributeBlock(k, v); + } else { + log.warn("Ignoring unknown endpoint property: {}", k); + } + }); + return null; + } + + @Override + public RuleType visitHeadersExpression(HeadersExpression e) { + e.headers().forEach((k, v) -> { + for (RuleExpression value : v.expressions()) { + builder.add(".putHeader($S, ", k); + value.accept(this); + builder.add(")"); + } + }); + return null; + } + + @Override + public RuleType visitErrorExpression(ErrorExpression e) { + // Throw SdkClientException directly — no RuleResult wrapper + builder.add("throw $T.create(", SdkClientException.class); + e.error().accept(this); + builder.addStatement(")"); + return null; + } + + private void addAuthSchemesBlock(RuleExpression e) { + ListExpression expr = (ListExpression) e; + builder.add(".putAttribute($T.AUTH_SCHEMES, ", AwsEndpointAttribute.class); + builder.add("$T.asList(", Arrays.class); + boolean isFirst = true; + for (RuleExpression authSchemeExpr : expr.expressions()) { + if (!isFirst) { + builder.add(", "); + } + addAuthSchemesBody(authSchemeExpr); + isFirst = false; + } + builder.add("))"); + } + + private void addAuthSchemesBody(RuleExpression e) { + if (e.kind() != RuleExpression.RuleExpressionKind.PROPERTIES) { + throw new RuntimeException("Expecting properties, got: " + e); + } + PropertiesExpression expr = (PropertiesExpression) e; + RuleExpression nameExpr = expr.properties().get("name"); + boolean isStaticName = nameExpr.kind() == RuleExpression.RuleExpressionKind.STRING_VALUE; + + if (isStaticName) { + builder.add("$T.builder()", authSchemeClass(stringValueOf(nameExpr))); + } else { + validateDynamicAuthSchemeSupported(nameExpr); + builder.add("$T.builder()", DYNAMIC_ENDPOINT_AUTH_SCHEME_FACTORY); + } + + expr.properties().forEach((k, v) -> { + if (!"name".equals(k)) { + builder.add(".$L(", k); + v.accept(this); + builder.add(")"); + } + }); + + if (isStaticName) { + builder.add(".build()"); + } else { + builder.add(".create("); + nameExpr.accept(this); + builder.add(")"); + } + } + + private void validateDynamicAuthSchemeSupported(RuleExpression nameExpr) { + if (!useS3ExpressSessionAuth) { + throw new IllegalStateException( + "Endpoint ruleset contains an auth scheme whose name is resolved at runtime (" + nameExpr + "), but the " + + "'useS3ExpressSessionAuth' customization is not enabled for this service. Dynamically resolved auth " + + "scheme names are currently only supported for S3."); + } + } + + private String stringValueOf(RuleExpression e) { + if (e.kind() != RuleExpression.RuleExpressionKind.STRING_VALUE) { + throw new RuntimeException("Expecting string value, got: " + e); + } + return ((LiteralStringExpression) e).value(); + } + + private ClassName authSchemeClass(String name) { + switch (name) { + case "sigv4": + return ClassName.get(SigV4AuthScheme.class); + case "sigv4a": + return ClassName.get(SigV4aAuthScheme.class); + case "sigv4-s3express": + return ClassName.get("software.amazon.awssdk.services.s3.endpoints.authscheme", + "S3ExpressEndpointAuthScheme"); + default: + throw new RuntimeException("Unknown auth scheme: " + name); + } + } + + private void addAttributeBlock(String k, RuleExpression v) { + KeyTypePair keyType = knownEndpointAttributes.get(k); + ClassConstant classConstant = parseClassConstant(keyType.getKey()); + builder.add(".putAttribute($T.$L, ", classConstant.className(), classConstant.fieldName()); + v.accept(this); + builder.add(")"); + } + + private ClassConstant parseClassConstant(String value) { + int lastDot = value.lastIndexOf('.'); + if (lastDot == -1) { + throw new IllegalArgumentException("cannot parse class constant: " + value); + } + String fieldName = value.substring(lastDot + 1); + String className = value.substring(0, lastDot); + int classLastDot = className.lastIndexOf('.'); + if (classLastDot == -1) { + throw new IllegalArgumentException("cannot parse class constant: " + value); + } + String simpleName = className.substring(classLastDot + 1); + String packageName = className.substring(0, classLastDot); + return new ClassConstant(ClassName.get(packageName, simpleName), fieldName); + } + + private void addMetricValuesBlock(RuleExpression v) { + builder.add(".putAttribute($T.METRIC_VALUES, ", AwsEndpointAttribute.class); + v.accept(this); + builder.add(")"); + } + + static class ClassConstant { + private final ClassName className; + private final String fieldName; + + ClassConstant(ClassName className, String fieldName) { + this.className = className; + this.fieldName = fieldName; + } + + public ClassName className() { + return className; + } + + public String fieldName() { + return fieldName; + } + } +} diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/bdd/ConditionFnCodeGeneratorVisitor.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/bdd/ConditionFnCodeGeneratorVisitor.java new file mode 100644 index 000000000000..f0557bc7b89e --- /dev/null +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/bdd/ConditionFnCodeGeneratorVisitor.java @@ -0,0 +1,286 @@ +/* + * 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.rules2.bdd; + +import com.squareup.javapoet.CodeBlock; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.awssdk.codegen.poet.rules.EndpointRulesSpecUtils; +import software.amazon.awssdk.codegen.poet.rules2.BooleanAndExpression; +import software.amazon.awssdk.codegen.poet.rules2.BooleanNotExpression; +import software.amazon.awssdk.codegen.poet.rules2.EndpointExpression; +import software.amazon.awssdk.codegen.poet.rules2.ErrorExpression; +import software.amazon.awssdk.codegen.poet.rules2.FunctionCallExpression; +import software.amazon.awssdk.codegen.poet.rules2.HeadersExpression; +import software.amazon.awssdk.codegen.poet.rules2.IndexedAccessExpression; +import software.amazon.awssdk.codegen.poet.rules2.LetExpression; +import software.amazon.awssdk.codegen.poet.rules2.ListExpression; +import software.amazon.awssdk.codegen.poet.rules2.LiteralBooleanExpression; +import software.amazon.awssdk.codegen.poet.rules2.LiteralIntegerExpression; +import software.amazon.awssdk.codegen.poet.rules2.LiteralStringExpression; +import software.amazon.awssdk.codegen.poet.rules2.MemberAccessExpression; +import software.amazon.awssdk.codegen.poet.rules2.MethodCallExpression; +import software.amazon.awssdk.codegen.poet.rules2.PropertiesExpression; +import software.amazon.awssdk.codegen.poet.rules2.RuleExpression; +import software.amazon.awssdk.codegen.poet.rules2.RuleExpressionVisitor; +import software.amazon.awssdk.codegen.poet.rules2.RuleFunctionMirror; +import software.amazon.awssdk.codegen.poet.rules2.RuleRuntimeTypeMirror; +import software.amazon.awssdk.codegen.poet.rules2.RuleSetExpression; +import software.amazon.awssdk.codegen.poet.rules2.RuleType; +import software.amazon.awssdk.codegen.poet.rules2.StringConcatExpression; +import software.amazon.awssdk.codegen.poet.rules2.VariableReferenceExpression; + +public class ConditionFnCodeGeneratorVisitor implements RuleExpressionVisitor { + private static final Logger log = LoggerFactory.getLogger(ConditionFnCodeGeneratorVisitor.class); + private final CodeBlock.Builder builder; + private final RuleRuntimeTypeMirror typeMirror; + private final Map registerInfoMap; + private final EndpointRulesSpecUtils endpointRulesSpecUtils; + + public ConditionFnCodeGeneratorVisitor(CodeBlock.Builder builder, RuleRuntimeTypeMirror typeMirror, + Map registerInfoMap, + EndpointRulesSpecUtils endpointRulesSpecUtils) { + this.builder = builder; + this.typeMirror = typeMirror; + this.registerInfoMap = registerInfoMap; + this.endpointRulesSpecUtils = endpointRulesSpecUtils; + } + + @Override + public RuleType visitLiteralBooleanExpression(LiteralBooleanExpression e) { + builder.add(Boolean.toString(e.value())); + return RuleRuntimeTypeMirror.BOOLEAN; + } + + @Override + public RuleType visitLiteralIntegerExpression(LiteralIntegerExpression e) { + builder.add(Integer.toString(e.value())); + return RuleRuntimeTypeMirror.INTEGER; + } + + @Override + public RuleType visitLiteralStringExpression(LiteralStringExpression e) { + builder.add("$S", e.value()); + return RuleRuntimeTypeMirror.STRING; + } + + @Override + public RuleType visitBooleanNotExpression(BooleanNotExpression e) { + builder.add("!"); + e.expression().accept(this); + return RuleRuntimeTypeMirror.BOOLEAN; + } + + @Override + public RuleType visitBooleanAndExpression(BooleanAndExpression e) { + List expressions = e.expressions(); + boolean isFirst = true; + for (RuleExpression expr : expressions) { + if (!isFirst) { + builder.add(" && "); + } + expr.accept(this); + isFirst = false; + } + return RuleRuntimeTypeMirror.BOOLEAN; + } + + @Override + public RuleType visitFunctionCallExpression(FunctionCallExpression e) { + String fn = e.name(); + if ("not".equals(fn)) { + builder.add("!("); + e.arguments().get(0).accept(this); + builder.add(")"); + return RuleRuntimeTypeMirror.BOOLEAN; + } + if ("isSet".equals(fn)) { + e.arguments().get(0).accept(this); + builder.add(" != null"); + return RuleRuntimeTypeMirror.BOOLEAN; + } + if ("isNotSet".equals(fn)) { + e.arguments().get(0).accept(this); + builder.add(" == null"); + return RuleRuntimeTypeMirror.BOOLEAN; + } + RuleFunctionMirror func = typeMirror.resolveFunction(e.name()); + builder.add("$T.$L(", func.containingType().type(), func.javaName()); + List args = e.arguments(); + RuleType lastArgType = RuleRuntimeTypeMirror.VOID; + for (int i = 0; i < args.size(); i++) { + if (i > 0) { + builder.add(", "); + } + lastArgType = args.get(i).accept(this); + } + builder.add(")"); + if ("coalesce".equals(fn)) { + // special case type inference for coalesce + return lastArgType; + } + return func.returns(); + } + + @Override + public RuleType visitMethodCallExpression(MethodCallExpression e) { + e.source().accept(this); + builder.add(".$L(", e.name()); + boolean isFirst = true; + for (RuleExpression arg : e.arguments()) { + if (!isFirst) { + builder.add(", "); + } + arg.accept(this); + isFirst = false; + } + builder.add(")"); + if ("equals".equals(e.name())) { + return RuleRuntimeTypeMirror.BOOLEAN; + } + return e.type(); + } + + @Override + public RuleType visitVariableReferenceExpression(VariableReferenceExpression e) { + RegistryInfo registryInfo = registerInfoMap.get(e.variableName()); + if (registryInfo.isNonRegionParam()) { + builder.add("params.$L()", endpointRulesSpecUtils.paramMethodName(registryInfo.getNonRegionParamKey())); + } else { + builder.add("$L", registryInfo.getName()); + } + return registerInfoMap.get(e.variableName()).getRuleType(); + } + + @Override + public RuleType visitMemberAccessExpression(MemberAccessExpression e) { + RuleType sourceType = e.source().accept(this); + if (!e.directIndex()) { + builder.add(".$L()", e.name()); + } + return sourceType.property(e.name()); + } + + @Override + public RuleType visitIndexedAccessExpression(IndexedAccessExpression e) { + RuleFunctionMirror func = typeMirror.resolveFunction("listAccess"); + builder.add("$T.$L(", func.containingType().type(), func.javaName()); + RuleType sourceType = e.source().accept(this); + builder.add(", $L)", e.index()); + return sourceType.typeParam(); + } + + @Override + public RuleType visitStringConcatExpression(StringConcatExpression e) { + boolean isFirst = true; + for (RuleExpression expr : e.expressions()) { + if (!isFirst) { + builder.add(" + "); + } + expr.accept(this); + isFirst = false; + } + return RuleRuntimeTypeMirror.STRING; + } + + @Override + public RuleType visitLetExpression(LetExpression e) { + if (e.bindings().size() != 1) { + throw new IllegalStateException("Expected exactly one binding"); + } + for (Map.Entry kvp : e.bindings().entrySet()) { + String k = kvp.getKey(); + RuleExpression v = kvp.getValue(); + String registerName = registerInfoMap.get(k).getName(); + builder.add("$L = ", registerName); + v.accept(this); + builder.addStatement(""); // end the statement we started + builder.addStatement("return $L != null", registerName); + } + return RuleRuntimeTypeMirror.BOOLEAN; + } + + @Override + public RuleType visitRuleSetExpression(RuleSetExpression e) { + // generate the conditions - there may be multiple assigns (LET) + // but there will be only one condition + if (e.conditions().size() != 1) { + throw new IllegalStateException("Expected exactly one condition"); + } + RuleExpression condition = e.conditions().get(0); + if (condition.kind() == RuleExpression.RuleExpressionKind.LET) { + condition.accept(this); // lets are self contained + } else { + builder.add("return ("); + if (RuleExpression.RuleExpressionKind.VARIABLE_REFERENCE == condition.kind()) { + VariableReferenceExpression varRef = (VariableReferenceExpression) condition; + RegistryInfo registryInfo = registerInfoMap.get(varRef.variableName()); + // special case optimization: do not auto-box booleanEquals! + if (registryInfo.isNullable() && RuleRuntimeTypeMirror.BOOLEAN.equals(registryInfo.getRuleType())) { + builder.add("Boolean.TRUE.equals($L)", registryInfo.getName()); + builder.addStatement(")"); + return RuleRuntimeTypeMirror.BOOLEAN; + } + } + RuleType type = condition.accept(this); + if (type != null && !RuleRuntimeTypeMirror.BOOLEAN.equals(type)) { + log.warn("Expected boolean, got {}. Rewriting condition with a != null. Condition: `{}`", type, condition); + builder.add(" != null"); + } + builder.addStatement(")"); // finish the expression we started + } + return RuleRuntimeTypeMirror.VOID; + } + + @Override + public RuleType visitListExpression(ListExpression e) { + builder.add("$T.asList(", Arrays.class); + boolean isFirst = true; + for (RuleExpression expr : e.expressions()) { + if (!isFirst) { + builder.add(", "); + } + expr.accept(this); + isFirst = false; + } + builder.add(")"); + // TODO: this could potentially be another type + return RuleRuntimeTypeMirror.LIST_OF_STRING; + } + + @Override + public RuleType visitEndpointExpression(EndpointExpression e) { + throw new IllegalStateException("Unexpected EndpointExpression"); + } + + @Override + public RuleType visitErrorExpression(ErrorExpression e) { + throw new IllegalStateException("Unexpected ErrorExpression"); + } + + @Override + public RuleType visitPropertiesExpression(PropertiesExpression e) { + throw new IllegalStateException("Unexpected PropertiesExpression"); + } + + @Override + public RuleType visitHeadersExpression(HeadersExpression e) { + throw new IllegalStateException("Unexpected HeadersExpression"); + } +} diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/bdd/RegistryInfo.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/bdd/RegistryInfo.java new file mode 100644 index 000000000000..e31000f7c4f8 --- /dev/null +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/bdd/RegistryInfo.java @@ -0,0 +1,76 @@ +/* + * 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.rules2.bdd; + +import software.amazon.awssdk.codegen.poet.rules2.RuleSetExpression; +import software.amazon.awssdk.codegen.poet.rules2.RuleType; + +public class RegistryInfo { + private final String name; + // ruleType may be set after construction by the type-inference pass (AssignTypeInferringVisitor) + private RuleType ruleType; + // set only when this value is assigned from a condition, NOT set for parameters + private final RuleSetExpression ruleSetExpression; + // defaults to true; false only when we guarantee that the value cannot be null. + private final boolean nullable; + // set only when this is an endpoint parameter, EXCEPT in the case of Region built-in which is a special case + private final String nonRegionParamKey; + + public RegistryInfo(String name, RuleType ruleType, RuleSetExpression ruleSetExpression, boolean nullable, + String nonRegionParamKey) { + this.name = name; + this.ruleType = ruleType; + this.ruleSetExpression = ruleSetExpression; + this.nullable = nullable; + this.nonRegionParamKey = nonRegionParamKey; + } + + public RegistryInfo(String name, RuleType ruleType, String paramKey) { + this(name, ruleType, null, true, paramKey); + } + + public RegistryInfo(String name, RuleSetExpression ruleSetExpression) { + this(name, null, ruleSetExpression, true, null); + } + + public String getName() { + return name; + } + + public RuleType getRuleType() { + return ruleType; + } + + public void setRuleType(RuleType ruleType) { + this.ruleType = ruleType; + } + + public RuleSetExpression getRuleSetExpression() { + return ruleSetExpression; + } + + public boolean isNullable() { + return nullable; + } + + public String getNonRegionParamKey() { + return nonRegionParamKey; + } + + public boolean isNonRegionParam() { + return nonRegionParamKey != null; + } +} diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/ClientTestModels.java b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/ClientTestModels.java index d0083c3386e1..8137ad2213f5 100644 --- a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/ClientTestModels.java +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/ClientTestModels.java @@ -22,6 +22,7 @@ import software.amazon.awssdk.codegen.model.config.customization.CustomizationConfig; import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel; import software.amazon.awssdk.codegen.model.rules.endpoints.EndpointTestSuiteModel; +import software.amazon.awssdk.codegen.model.service.EndpointBddModel; import software.amazon.awssdk.codegen.model.service.EndpointRuleSetModel; import software.amazon.awssdk.codegen.model.service.Paginators; import software.amazon.awssdk.codegen.model.service.ServiceModel; @@ -644,6 +645,87 @@ public static IntermediateModel presignedUrlExtensionModels() { return new IntermediateModelBuilder(models).build(); } + /** + * Uses the S3 BDD, which contains an auth scheme whose name is resolved at runtime. That requires the + * {@code useS3ExpressSessionAuth} customization to be enabled. + */ + public static IntermediateModel queryServiceModelsWithBddEndpoints() { + return queryServiceModelsWithBddEndpoints(true); + } + + public static IntermediateModel queryServiceModelsWithBddEndpoints(boolean useS3ExpressSessionAuth) { + File serviceModel = new File(ClientTestModels.class.getResource("client/c2j/query/service-2.json").getFile()); + File waitersModel = new File(ClientTestModels.class.getResource("client/c2j/query/waiters-2.json").getFile()); + File endpointRuleSetModel = + new File(ClientTestModels.class.getResource("client/c2j/query/endpoint-rule-set-default-regional.json").getFile()); + File endpointTestsModel = + new File(ClientTestModels.class.getResource("client/c2j/query/endpoint-tests.json").getFile()); + File endpointBddModel = + new File(ClientTestModels.class.getResource("client/c2j/query/endpoint-bdd-s3.json").getFile()); + CustomizationConfig customizationConfig = CustomizationConfig.create(); + customizationConfig.setUseS3ExpressSessionAuth(useS3ExpressSessionAuth); + C2jModels models = C2jModels + .builder() + .serviceModel(getServiceModel(serviceModel)) + .waitersModel(getWaiters(waitersModel)) + .customizationConfig(customizationConfig) + .endpointRuleSetModel(getEndpointRuleSet(endpointRuleSetModel)) + .endpointTestSuiteModel(getEndpointTestSuite(endpointTestsModel)) + .endpointBddModel(getEndpointBdd(endpointBddModel)) + .build(); + return new IntermediateModelBuilder(models).build(); + } + + /** + * Uses the simple default-regional BDD (Connect-like, no dynamic auth schemes). + * Suitable for fixture-based golden file tests. + */ + public static IntermediateModel queryServiceModelsWithSimpleBddEndpoints() { + File serviceModel = new File(ClientTestModels.class.getResource("client/c2j/query/service-2.json").getFile()); + File waitersModel = new File(ClientTestModels.class.getResource("client/c2j/query/waiters-2.json").getFile()); + File endpointRuleSetModel = + new File(ClientTestModels.class.getResource("client/c2j/query/endpoint-rule-set-default-regional.json").getFile()); + File endpointTestsModel = + new File(ClientTestModels.class.getResource("client/c2j/query/endpoint-tests.json").getFile()); + File endpointBddModel = + new File(ClientTestModels.class.getResource("client/c2j/query/endpoint-bdd-default-regional.json").getFile()); + C2jModels models = C2jModels + .builder() + .serviceModel(getServiceModel(serviceModel)) + .waitersModel(getWaiters(waitersModel)) + .customizationConfig(CustomizationConfig.create()) + .endpointRuleSetModel(getEndpointRuleSet(endpointRuleSetModel)) + .endpointTestSuiteModel(getEndpointTestSuite(endpointTestsModel)) + .endpointBddModel(getEndpointBdd(endpointBddModel)) + .build(); + return new IntermediateModelBuilder(models).build(); + } + + /** + * Uses a hand-crafted BDD with a complement edge (negative node reference) to verify that + * nodeN methods are generated correctly with swapped branches. + */ + public static IntermediateModel queryServiceModelsWithComplementBddEndpoints() { + File serviceModel = new File(ClientTestModels.class.getResource("client/c2j/query/service-2.json").getFile()); + File waitersModel = new File(ClientTestModels.class.getResource("client/c2j/query/waiters-2.json").getFile()); + File endpointRuleSetModel = + new File(ClientTestModels.class.getResource("client/c2j/query/endpoint-rule-set-default-regional.json").getFile()); + File endpointTestsModel = + new File(ClientTestModels.class.getResource("client/c2j/query/endpoint-tests.json").getFile()); + File endpointBddModel = + new File(ClientTestModels.class.getResource("client/c2j/query/endpoint-bdd-complement.json").getFile()); + C2jModels models = C2jModels + .builder() + .serviceModel(getServiceModel(serviceModel)) + .waitersModel(getWaiters(waitersModel)) + .customizationConfig(CustomizationConfig.create()) + .endpointRuleSetModel(getEndpointRuleSet(endpointRuleSetModel)) + .endpointTestSuiteModel(getEndpointTestSuite(endpointTestsModel)) + .endpointBddModel(getEndpointBdd(endpointBddModel)) + .build(); + return new IntermediateModelBuilder(models).build(); + } + private static ServiceModel getServiceModel(File file) { return ModelLoaderUtils.loadModel(ServiceModel.class, file); } @@ -688,4 +770,8 @@ private static EndpointTestSuiteModel getEndpointTestSuite(File file) { private static Paginators getPaginatorsModel(File file) { return ModelLoaderUtils.loadModel(Paginators.class, file); } + + private static EndpointBddModel getEndpointBdd(File file) { + return ModelLoaderUtils.loadModel(EndpointBddModel.class, file); + } } diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules2/TokenizerTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules2/TokenizerTest.java index f9d14eb3076d..de396180aade 100644 --- a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules2/TokenizerTest.java +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules2/TokenizerTest.java @@ -16,6 +16,7 @@ package software.amazon.awssdk.codegen.poet.rules2; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; @@ -59,4 +60,55 @@ public void recognizesNamedAccessExpression() { }); assertTrue(tokenizer.atEof()); } + + @Test + public void recognizesDirectNegativeIndexedExpression() { + Tokenizer tokenizer = new Tokenizer("[-2]"); + assertTrue(tokenizer.isDirectNegativeIndexedAccess()); + tokenizer.consumeDirectNegativeIndexed(i -> assertEquals(-2, i)); + assertTrue(tokenizer.atEof()); + } + + @Test + public void recognizesNegativeIndexedExpression() { + Tokenizer tokenizer = new Tokenizer("resourceId[-1]"); + assertTrue(tokenizer.isNegativeIndexedAccess()); + tokenizer.consumeNegativeIndexed((name, index) -> { + assertEquals("resourceId", name); + assertEquals(-1, index); + }); + assertTrue(tokenizer.atEof()); + } + + @Test + public void negativeIndexIsNotConfusedWithPositiveIndex() { + assertTrue(new Tokenizer("[2]").isDirectIndexedAccess()); + assertFalse(new Tokenizer("[2]").isDirectNegativeIndexedAccess()); + assertTrue(new Tokenizer("[-2]").isDirectNegativeIndexedAccess()); + assertFalse(new Tokenizer("[-2]").isDirectIndexedAccess()); + } + + /** + * A hyphen is only a distinct token when it opens a negative index. Everywhere else it is an ordinary string + * character, otherwise literals such as "s3-fips" would be split into extra concatenation terms by + * {@code ExpressionParser.parseStringConcat}. + */ + @Test + public void hyphenOutsideIndexRemainsPartOfString() { + // Hyphen followed by a digit, but not inside an index - must stay in the string. + Tokenizer tokenizer = new Tokenizer("{Region}-1a"); + assertTrue(tokenizer.isReference()); + tokenizer.consumeReferenceAccess(n -> assertEquals("Region", n)); + assertEquals("-1a", tokenizer.next().value()); + assertTrue(tokenizer.atEof()); + } + + @Test + public void hyphenatedHostLabelIsSingleToken() { + Tokenizer tokenizer = new Tokenizer("{Region}s3-fips-2.example"); + assertTrue(tokenizer.isReference()); + tokenizer.consumeReferenceAccess(n -> assertEquals("Region", n)); + assertEquals("s3-fips-2.example", tokenizer.next().value()); + assertTrue(tokenizer.atEof()); + } } \ No newline at end of file diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules2/bdd/BddEndpointProviderSpecTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules2/bdd/BddEndpointProviderSpecTest.java new file mode 100644 index 000000000000..a6a51746bdc7 --- /dev/null +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules2/bdd/BddEndpointProviderSpecTest.java @@ -0,0 +1,95 @@ +/* + * 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.rules2.bdd; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static software.amazon.awssdk.codegen.poet.PoetMatchers.generatesTo; + +import org.hamcrest.MatcherAssert; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.codegen.poet.ClientTestModels; + +public class BddEndpointProviderSpecTest { + + @Test + void endpointProviderClass_simpleBdd_generatesExpectedCode() { + BddEndpointProviderSpec spec = new BddEndpointProviderSpec( + ClientTestModels.queryServiceModelsWithSimpleBddEndpoints()); + MatcherAssert.assertThat(spec, generatesTo("endpoint-provider-bdd-class.java")); + } + + @Test + void endpointProviderClass_s3Bdd_generatesWithoutError() { + BddEndpointProviderSpec spec = new BddEndpointProviderSpec( + ClientTestModels.queryServiceModelsWithBddEndpoints()); + assertThat(spec.poetSpec()).isNotNull(); + } + + /** + * The S3 BDD merges results that differ only in their auth scheme name, so the name is resolved at runtime via + * {@code DynamicEndpointAuthSchemeFactory.create(name)}. The sibling properties must still be emitted, otherwise + * the signing configuration would be silently dropped. + */ + @Test + void dynamicAuthSchemeName_delegatesToFactoryAndKeepsProperties() { + BddEndpointProviderSpec spec = new BddEndpointProviderSpec( + ClientTestModels.queryServiceModelsWithBddEndpoints()); + + String generated = spec.poetSpec().toString(); + + assertThat(generated).contains("DynamicEndpointAuthSchemeFactory.builder()"); + assertThat(generated).contains(".create(_s3e_auth)"); + assertThat(generated).contains(".signingName(\"s3express\")"); + assertThat(generated).contains(".disableDoubleEncoding(true)"); + assertThat(generated).doesNotContain("DynamicEndpointAuthSchemeFactory.builder().build()"); + } + + /** + * Verifies that complement edges (negative node references) generate nodeN methods with + * swapped branches. A complement edge to node N means: evaluate the same condition, but + * follow lowRef when true and highRef when false (the inverse of nodeP). + */ + @Test + void complementEdge_generatesNodeNWithSwappedBranches() { + BddEndpointProviderSpec spec = new BddEndpointProviderSpec( + ClientTestModels.queryServiceModelsWithComplementBddEndpoints()); + + String generated = spec.poetSpec().toString(); + + // nodeP1 should exist with normal branch order + assertThat(generated).contains("nodeP1()"); + // nodeN1 should exist (complement variant with swapped branches) + assertThat(generated).contains("nodeN1()"); + // nodeP2's false branch should reference nodeN1 (the complement edge) + assertThat(generated).contains("Endpoint nodeN1()"); + } + + /** + * {@code DynamicEndpointAuthSchemeFactory} is S3-specific, so a dynamically resolved auth scheme name in any other + * service must fail codegen rather than emitting code that cannot compile. + */ + @Test + void dynamicAuthSchemeName_withoutS3ExpressCustomization_failsCodegen() { + BddEndpointProviderSpec spec = new BddEndpointProviderSpec( + ClientTestModels.queryServiceModelsWithBddEndpoints(false)); + + assertThatThrownBy(spec::poetSpec) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("useS3ExpressSessionAuth") + .hasMessageContaining("resolved at runtime"); + } +} diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/query/endpoint-bdd-complement.json b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/query/endpoint-bdd-complement.json new file mode 100644 index 000000000000..78e80db96af1 --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/query/endpoint-bdd-complement.json @@ -0,0 +1,55 @@ +{ + "version": "1.1", + "parameters": { + "Region": { + "builtIn": "AWS::Region", + "required": false, + "documentation": "The AWS region used to dispatch the request.", + "type": "string" + }, + "Endpoint": { + "builtIn": "SDK::Endpoint", + "required": false, + "documentation": "Override the endpoint used to send this request", + "type": "string" + } + }, + "conditions": [ + { + "fn": "isSet", + "argv": [{ "ref": "Region" }] + }, + { + "fn": "isSet", + "argv": [{ "ref": "Endpoint" }] + } + ], + "results": [ + { + "conditions": [], + "endpoint": { + "url": "https://service.{Region}.amazonaws.com", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "error": "Region is required when Endpoint is not set", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": "{Endpoint}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + } + ], + "root": 3, + "nodeCount": 3, + "nodes": "/////wAAAAH/////AAAAAAX14QEF9eECAAAAAQX14QP////+" +} diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/query/endpoint-bdd-default-regional.json b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/query/endpoint-bdd-default-regional.json new file mode 100644 index 000000000000..9f7c8fc3a7e4 --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/query/endpoint-bdd-default-regional.json @@ -0,0 +1,212 @@ +{ + "version": "1.1", + "parameters": { + "Region": { + "builtIn": "AWS::Region", + "required": false, + "documentation": "The AWS region used to dispatch the request.", + "type": "string" + }, + "UseDualStack": { + "builtIn": "AWS::UseDualStack", + "required": true, + "default": false, + "documentation": "When true, use the dual-stack endpoint. If the configured endpoint does not support dual-stack, dispatching the request MAY return an error.", + "type": "boolean" + }, + "UseFIPS": { + "builtIn": "AWS::UseFIPS", + "required": true, + "default": false, + "documentation": "When true, send this request to the FIPS-compliant regional endpoint. If the configured endpoint does not have a FIPS compliant endpoint, dispatching the request will return an error.", + "type": "boolean" + }, + "Endpoint": { + "builtIn": "SDK::Endpoint", + "required": false, + "documentation": "Override the endpoint used to send this request", + "type": "string" + } + }, + "conditions": [ + { + "fn": "isSet", + "argv": [ + { + "ref": "Endpoint" + } + ] + }, + { + "fn": "isSet", + "argv": [ + { + "ref": "Region" + } + ] + }, + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseFIPS" + }, + true + ] + }, + { + "fn": "aws.partition", + "argv": [ + { + "ref": "Region" + } + ], + "assign": "PartitionResult" + }, + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseDualStack" + }, + true + ] + }, + { + "fn": "booleanEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsFIPS" + ] + }, + true + ] + }, + { + "fn": "booleanEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsDualStack" + ] + }, + true + ] + }, + { + "fn": "stringEquals", + "argv": [ + "aws-us-gov", + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "name" + ] + } + ] + } + ], + "results": [ + { + "conditions": [], + "error": "Invalid Configuration: FIPS and custom endpoint are not supported", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid Configuration: Dualstack and custom endpoint are not supported", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": { + "ref": "Endpoint" + }, + "properties": {}, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://query-fips.{Region}.{PartitionResult#dualStackDnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "error": "FIPS and DualStack are enabled, but this partition does not support one or both", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": "https://query.{Region}.amazonaws.com", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://query-fips.{Region}.{PartitionResult#dnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "error": "FIPS is enabled but this partition does not support FIPS", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": "https://query.{Region}.{PartitionResult#dualStackDnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "error": "DualStack is enabled but this partition does not support DualStack", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": "https://query.{Region}.{PartitionResult#dnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "error": "Invalid Configuration: Missing Region", + "type": "error" + } + ], + "root": 15, + "nodeCount": 15, + "nodes": "/////wAAAAH/////AAAABAX14QIF9eEDAAAAAgX14QEAAAACAAAABgX14QQF9eEFAAAABQAAAAQF9eEFAAAABwX14QYF9eEHAAAABQAAAAYF9eEIAAAABAAAAAUAAAAHAAAAAwAAAAgF9eEMAAAABgX14QkF9eEKAAAABAAAAAoF9eELAAAAAwAAAAsF9eEMAAAAAgAAAAkAAAAMAAAAAQAAAA0F9eEMAAAAAAAAAAMAAAAO" +} \ No newline at end of file diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/query/endpoint-bdd-s3.json b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/query/endpoint-bdd-s3.json new file mode 100644 index 000000000000..3f427f9c104f --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/query/endpoint-bdd-s3.json @@ -0,0 +1,2551 @@ +{ + "version": "1.1", + "parameters": { + "Bucket": { + "required": false, + "documentation": "The S3 bucket used to send the request. This is an optional parameter that will be set automatically for operations that are scoped to an S3 bucket.", + "type": "string" + }, + "Region": { + "builtIn": "AWS::Region", + "required": false, + "documentation": "The AWS region used to dispatch the request.", + "type": "string" + }, + "UseFIPS": { + "builtIn": "AWS::UseFIPS", + "required": true, + "default": false, + "documentation": "When true, send this request to the FIPS-compliant regional endpoint. If the configured endpoint does not have a FIPS compliant endpoint, dispatching the request will return an error.", + "type": "boolean" + }, + "UseDualStack": { + "builtIn": "AWS::UseDualStack", + "required": true, + "default": false, + "documentation": "When true, use the dual-stack endpoint. If the configured endpoint does not support dual-stack, dispatching the request MAY return an error.", + "type": "boolean" + }, + "Endpoint": { + "builtIn": "SDK::Endpoint", + "required": false, + "documentation": "Override the endpoint used to send this request", + "type": "string" + }, + "ForcePathStyle": { + "builtIn": "AWS::S3::ForcePathStyle", + "required": true, + "default": false, + "documentation": "When true, force a path-style endpoint to be used where the bucket name is part of the path.", + "type": "boolean" + }, + "Accelerate": { + "builtIn": "AWS::S3::Accelerate", + "required": true, + "default": false, + "documentation": "When true, use S3 Accelerate. NOTE: Not all regions support S3 accelerate.", + "type": "boolean" + }, + "UseGlobalEndpoint": { + "builtIn": "AWS::S3::UseGlobalEndpoint", + "required": true, + "default": false, + "documentation": "Whether the global endpoint should be used, rather then the regional endpoint for us-east-1.", + "type": "boolean" + }, + "UseObjectLambdaEndpoint": { + "required": false, + "documentation": "Internal parameter to use object lambda endpoint for an operation (eg: WriteGetObjectResponse)", + "type": "boolean" + }, + "Key": { + "required": false, + "documentation": "The S3 Key used to send the request. This is an optional parameter that will be set automatically for operations that are scoped to an S3 Key.", + "type": "string" + }, + "Prefix": { + "required": false, + "documentation": "The S3 Prefix used to send the request. This is an optional parameter that will be set automatically for operations that are scoped to an S3 Prefix.", + "type": "string" + }, + "CopySource": { + "required": false, + "documentation": "The Copy Source used for Copy Object request. This is an optional parameter that will be set automatically for operations that are scoped to Copy Source.", + "type": "string" + }, + "DisableAccessPoints": { + "required": false, + "documentation": "Internal parameter to disable Access Point Buckets", + "type": "boolean" + }, + "DisableMultiRegionAccessPoints": { + "builtIn": "AWS::S3::DisableMultiRegionAccessPoints", + "required": true, + "default": false, + "documentation": "Whether multi-region access points (MRAP) should be disabled.", + "type": "boolean" + }, + "UseArnRegion": { + "builtIn": "AWS::S3::UseArnRegion", + "required": false, + "documentation": "When an Access Point ARN is provided and this flag is enabled, the SDK MUST use the ARN's region when constructing the endpoint instead of the client's configured region.", + "type": "boolean" + }, + "UseS3ExpressControlEndpoint": { + "required": false, + "documentation": "Internal parameter to indicate whether S3Express operation should use control plane, (ex. CreateBucket)", + "type": "boolean" + }, + "DisableS3ExpressSessionAuth": { + "required": false, + "documentation": "Parameter to indicate whether S3Express session auth should be disabled", + "type": "boolean" + } + }, + "conditions": [ + { + "fn": "isSet", + "argv": [ + { + "ref": "Region" + } + ] + }, + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "Accelerate" + }, + true + ] + }, + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseFIPS" + }, + true + ] + }, + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseDualStack" + }, + true + ] + }, + { + "fn": "isSet", + "argv": [ + { + "ref": "Endpoint" + } + ] + }, + { + "fn": "isSet", + "argv": [ + { + "ref": "Bucket" + } + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "coalesce", + "argv": [ + { + "fn": "substring", + "argv": [ + { + "ref": "Bucket" + }, + 0, + 6, + true + ] + }, + "" + ] + }, + "--x-s3" + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "coalesce", + "argv": [ + { + "fn": "substring", + "argv": [ + { + "ref": "Bucket" + }, + 0, + 7, + true + ] + }, + "" + ] + }, + "--xa-s3" + ] + }, + { + "fn": "aws.partition", + "argv": [ + { + "ref": "Region" + } + ], + "assign": "partitionResult" + }, + { + "fn": "substring", + "argv": [ + { + "ref": "Bucket" + }, + 0, + 7, + true + ], + "assign": "accessPointSuffix" + }, + { + "fn": "stringEquals", + "argv": [ + { + "ref": "accessPointSuffix" + }, + "--op-s3" + ] + }, + { + "fn": "substring", + "argv": [ + { + "ref": "Bucket" + }, + 8, + 12, + true + ], + "assign": "regionPrefix" + }, + { + "fn": "substring", + "argv": [ + { + "ref": "Bucket" + }, + 32, + 49, + true + ], + "assign": "outpostId_ssa_2" + }, + { + "fn": "substring", + "argv": [ + { + "ref": "Bucket" + }, + 49, + 50, + true + ], + "assign": "hardwareType" + }, + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "ForcePathStyle" + }, + true + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "partitionResult" + }, + "name" + ] + }, + "aws-cn" + ] + }, + { + "fn": "ite", + "argv": [ + { + "ref": "UseDualStack" + }, + ".dualstack", + "" + ], + "assign": "_s3e_ds" + }, + { + "fn": "isValidHostLabel", + "argv": [ + { + "ref": "outpostId_ssa_2" + }, + false + ] + }, + { + "fn": "ite", + "argv": [ + { + "ref": "UseFIPS" + }, + "-fips", + "" + ], + "assign": "_s3e_fips" + }, + { + "fn": "ite", + "argv": [ + { + "fn": "coalesce", + "argv": [ + { + "ref": "DisableS3ExpressSessionAuth" + }, + false + ] + }, + "sigv4", + "sigv4-s3express" + ], + "assign": "_s3e_auth" + }, + { + "fn": "aws.isVirtualHostableS3Bucket", + "argv": [ + { + "ref": "Bucket" + }, + false + ] + }, + { + "fn": "parseURL", + "argv": [ + { + "ref": "Endpoint" + } + ], + "assign": "url" + }, + { + "fn": "booleanEquals", + "argv": [ + { + "fn": "coalesce", + "argv": [ + { + "ref": "UseS3ExpressControlEndpoint" + }, + false + ] + }, + true + ] + }, + { + "fn": "aws.isVirtualHostableS3Bucket", + "argv": [ + { + "ref": "Bucket" + }, + true + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "url" + }, + "scheme" + ] + }, + "http" + ] + }, + { + "fn": "isValidHostLabel", + "argv": [ + { + "ref": "Region" + }, + false + ] + }, + { + "fn": "aws.parseArn", + "argv": [ + { + "ref": "Bucket" + } + ], + "assign": "bucketArn" + }, + { + "fn": "getAttr", + "argv": [ + { + "fn": "split", + "argv": [ + { + "ref": "Bucket" + }, + "--", + 0 + ] + }, + "[-2]" + ], + "assign": "s3expressAvailabilityZoneId" + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "coalesce", + "argv": [ + { + "fn": "substring", + "argv": [ + { + "ref": "Bucket" + }, + 0, + 4, + false + ] + }, + "" + ] + }, + "arn:" + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "coalesce", + "argv": [ + { + "fn": "substring", + "argv": [ + { + "ref": "Bucket" + }, + 16, + 18, + true + ] + }, + "" + ] + }, + "--" + ] + }, + { + "fn": "booleanEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "url" + }, + "isIp" + ] + }, + true + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "coalesce", + "argv": [ + { + "fn": "substring", + "argv": [ + { + "ref": "Bucket" + }, + 21, + 23, + true + ] + }, + "" + ] + }, + "--" + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "coalesce", + "argv": [ + { + "fn": "substring", + "argv": [ + { + "ref": "Bucket" + }, + 27, + 29, + true + ] + }, + "" + ] + }, + "--" + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "ref": "regionPrefix" + }, + "beta" + ] + }, + { + "fn": "uriEncode", + "argv": [ + { + "ref": "Bucket" + } + ], + "assign": "uri_encoded_bucket" + }, + { + "fn": "isValidHostLabel", + "argv": [ + { + "ref": "Region" + }, + true + ] + }, + { + "fn": "booleanEquals", + "argv": [ + { + "fn": "coalesce", + "argv": [ + { + "ref": "UseObjectLambdaEndpoint" + }, + false + ] + }, + true + ] + }, + { + "fn": "getAttr", + "argv": [ + { + "ref": "bucketArn" + }, + "resourceId[0]" + ], + "assign": "arnType" + }, + { + "fn": "stringEquals", + "argv": [ + { + "ref": "arnType" + }, + "" + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "ref": "arnType" + }, + "accesspoint" + ] + }, + { + "fn": "getAttr", + "argv": [ + { + "ref": "bucketArn" + }, + "resourceId[1]" + ], + "assign": "accessPointName_ssa_1" + }, + { + "fn": "stringEquals", + "argv": [ + { + "ref": "accessPointName_ssa_1" + }, + "" + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "bucketArn" + }, + "region" + ] + }, + "" + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "coalesce", + "argv": [ + { + "fn": "substring", + "argv": [ + { + "ref": "Bucket" + }, + 14, + 16, + true + ] + }, + "" + ] + }, + "--" + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "ref": "hardwareType" + }, + "e" + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "ref": "hardwareType" + }, + "o" + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "ref": "Region" + }, + "aws-global" + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "coalesce", + "argv": [ + { + "fn": "substring", + "argv": [ + { + "ref": "Bucket" + }, + 19, + 21, + true + ] + }, + "" + ] + }, + "--" + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "bucketArn" + }, + "service" + ] + }, + "s3-object-lambda" + ] + }, + { + "fn": "booleanEquals", + "argv": [ + { + "fn": "coalesce", + "argv": [ + { + "ref": "DisableAccessPoints" + }, + false + ] + }, + true + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "bucketArn" + }, + "service" + ] + }, + "s3-outposts" + ] + }, + { + "fn": "aws.partition", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "bucketArn" + }, + "region" + ] + } + ], + "assign": "bucketPartition" + }, + { + "fn": "isValidHostLabel", + "argv": [ + { + "ref": "accessPointName_ssa_1" + }, + true + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "coalesce", + "argv": [ + { + "fn": "substring", + "argv": [ + { + "ref": "Bucket" + }, + 26, + 28, + true + ] + }, + "" + ] + }, + "--" + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "coalesce", + "argv": [ + { + "fn": "substring", + "argv": [ + { + "ref": "Bucket" + }, + 15, + 17, + true + ] + }, + "" + ] + }, + "--" + ] + }, + { + "fn": "getAttr", + "argv": [ + { + "ref": "bucketArn" + }, + "resourceId[4]" + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "coalesce", + "argv": [ + { + "fn": "substring", + "argv": [ + { + "ref": "Bucket" + }, + 20, + 22, + true + ] + }, + "" + ] + }, + "--" + ] + }, + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseGlobalEndpoint" + }, + true + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "ref": "Region" + }, + "us-east-1" + ] + }, + { + "fn": "getAttr", + "argv": [ + { + "ref": "bucketArn" + }, + "resourceId[1]" + ], + "assign": "outpostId_ssa_1" + }, + { + "fn": "booleanEquals", + "argv": [ + { + "fn": "coalesce", + "argv": [ + { + "ref": "UseArnRegion" + }, + true + ] + }, + true + ] + }, + { + "fn": "isValidHostLabel", + "argv": [ + { + "ref": "outpostId_ssa_1" + }, + false + ] + }, + { + "fn": "getAttr", + "argv": [ + { + "ref": "bucketArn" + }, + "resourceId[2]" + ], + "assign": "outpostType" + }, + { + "fn": "stringEquals", + "argv": [ + { + "ref": "Region" + }, + { + "fn": "getAttr", + "argv": [ + { + "ref": "bucketArn" + }, + "region" + ] + } + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "bucketPartition" + }, + "name" + ] + }, + { + "fn": "getAttr", + "argv": [ + { + "ref": "partitionResult" + }, + "name" + ] + } + ] + }, + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "DisableMultiRegionAccessPoints" + }, + true + ] + }, + { + "fn": "isValidHostLabel", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "bucketArn" + }, + "region" + ] + }, + true + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "bucketArn" + }, + "partition" + ] + }, + { + "fn": "getAttr", + "argv": [ + { + "ref": "partitionResult" + }, + "name" + ] + } + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "bucketArn" + }, + "accountId" + ] + }, + "" + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "bucketArn" + }, + "service" + ] + }, + "s3" + ] + }, + { + "fn": "isValidHostLabel", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "bucketArn" + }, + "accountId" + ] + }, + false + ] + }, + { + "fn": "getAttr", + "argv": [ + { + "ref": "bucketArn" + }, + "resourceId[3]" + ], + "assign": "accessPointName_ssa_2" + }, + { + "fn": "isValidHostLabel", + "argv": [ + { + "ref": "accessPointName_ssa_1" + }, + false + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "ref": "outpostType" + }, + "accesspoint" + ] + }, + { + "fn": "isValidHostLabel", + "argv": [ + { + "ref": "accessPointName_ssa_2" + }, + false + ] + } + ], + "results": [ + { + "conditions": [], + "error": "Accelerate cannot be used with FIPS", + "type": "error" + }, + { + "conditions": [], + "error": "Cannot set dual-stack in combination with a custom endpoint.", + "type": "error" + }, + { + "conditions": [], + "error": "A custom endpoint cannot be combined with FIPS", + "type": "error" + }, + { + "conditions": [], + "error": "A custom endpoint cannot be combined with S3 Accelerate", + "type": "error" + }, + { + "conditions": [], + "error": "Partition does not support FIPS", + "type": "error" + }, + { + "conditions": [], + "error": "S3Express does not support S3 Accelerate.", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": "{url#scheme}://{url#authority}/{uri_encoded_bucket}{url#path}", + "properties": { + "backend": "S3Express", + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "{_s3e_auth}", + "signingName": "s3express", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "{url#scheme}://{Bucket}.{url#authority}{url#path}", + "properties": { + "backend": "S3Express", + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "{_s3e_auth}", + "signingName": "s3express", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "error": "S3Express bucket name is not a valid virtual hostable name.", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3express-control{_s3e_fips}{_s3e_ds}.{Region}.{partitionResult#dnsSuffix}/{uri_encoded_bucket}", + "properties": { + "backend": "S3Express", + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3express", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{Bucket}.s3express{_s3e_fips}-{s3expressAvailabilityZoneId}{_s3e_ds}.{Region}.{partitionResult#dnsSuffix}", + "properties": { + "backend": "S3Express", + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "{_s3e_auth}", + "signingName": "s3express", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "error": "Unrecognized S3Express bucket name format.", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": "{url#scheme}://{url#authority}{url#path}", + "properties": { + "backend": "S3Express", + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "{_s3e_auth}", + "signingName": "s3express", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3express-control{_s3e_fips}{_s3e_ds}.{Region}.{partitionResult#dnsSuffix}", + "properties": { + "backend": "S3Express", + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3express", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "error": "Expected a endpoint to be specified but no endpoint was found", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{Bucket}.ec2.{url#authority}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4a", + "signingName": "s3-outposts", + "signingRegionSet": [ + "*" + ] + }, + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3-outposts", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{Bucket}.ec2.s3-outposts.{Region}.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4a", + "signingName": "s3-outposts", + "signingRegionSet": [ + "*" + ] + }, + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3-outposts", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{Bucket}.op-{outpostId_ssa_2}.{url#authority}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4a", + "signingName": "s3-outposts", + "signingRegionSet": [ + "*" + ] + }, + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3-outposts", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{Bucket}.op-{outpostId_ssa_2}.s3-outposts.{Region}.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4a", + "signingName": "s3-outposts", + "signingRegionSet": [ + "*" + ] + }, + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3-outposts", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "error": "Unrecognized hardware type: \"Expected hardware type o or e but got {hardwareType}\"", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid Outposts Bucket alias - it must be a valid bucket name.", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid ARN: The outpost Id must only contain a-z, A-Z, 0-9 and `-`.", + "type": "error" + }, + { + "conditions": [], + "error": "Custom endpoint `{Endpoint}` was not a valid URI", + "type": "error" + }, + { + "conditions": [], + "error": "S3 Accelerate cannot be used in this region", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{Bucket}.s3-fips.dualstack.us-east-1.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "us-east-1" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{Bucket}.s3-fips.dualstack.{Region}.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{Bucket}.s3-fips.us-east-1.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "us-east-1" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{Bucket}.s3-fips.{Region}.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{Bucket}.s3-accelerate.dualstack.us-east-1.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "us-east-1" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{Bucket}.s3-accelerate.dualstack.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{Bucket}.s3.dualstack.us-east-1.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "us-east-1" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{Bucket}.s3.dualstack.{Region}.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "{url#scheme}://{url#authority}{url#normalizedPath}{Bucket}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "us-east-1" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "{url#scheme}://{Bucket}.{url#authority}{url#path}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "us-east-1" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "{url#scheme}://{url#authority}{url#normalizedPath}{Bucket}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "{url#scheme}://{Bucket}.{url#authority}{url#path}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{Bucket}.s3-accelerate.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "us-east-1" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{Bucket}.s3-accelerate.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{Bucket}.s3.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "us-east-1" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{Bucket}.s3.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{Bucket}.s3.{Region}.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "error": "Invalid region: region was not a valid DNS name.", + "type": "error" + }, + { + "conditions": [], + "error": "S3 Object Lambda does not support Dual-stack", + "type": "error" + }, + { + "conditions": [], + "error": "S3 Object Lambda does not support S3 Accelerate", + "type": "error" + }, + { + "conditions": [], + "error": "Access points are not supported for this operation", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid configuration: region from ARN `{bucketArn#region}` does not match client region `{Region}` and UseArnRegion is `false`", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid ARN: Missing account id", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": "{url#scheme}://{accessPointName_ssa_1}-{bucketArn#accountId}.{url#authority}{url#path}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3-object-lambda", + "signingRegion": "{bucketArn#region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{accessPointName_ssa_1}-{bucketArn#accountId}.s3-object-lambda-fips.{bucketArn#region}.{bucketPartition#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3-object-lambda", + "signingRegion": "{bucketArn#region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{accessPointName_ssa_1}-{bucketArn#accountId}.s3-object-lambda.{bucketArn#region}.{bucketPartition#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3-object-lambda", + "signingRegion": "{bucketArn#region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "error": "Invalid ARN: The access point name may only contain a-z, A-Z, 0-9 and `-`. Found: `{accessPointName_ssa_1}`", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid ARN: The account id may only contain a-z, A-Z, 0-9 and `-`. Found: `{bucketArn#accountId}`", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid region in ARN: `{bucketArn#region}` (invalid DNS name)", + "type": "error" + }, + { + "conditions": [], + "error": "Client was configured for partition `{partitionResult#name}` but ARN (`{Bucket}`) has `{bucketPartition#name}`", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid ARN: The ARN may only contain a single resource component after `accesspoint`.", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid ARN: bucket ARN is missing a region", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid ARN: Expected a resource of the format `accesspoint:` but no name was provided", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid ARN: Object Lambda ARNs only support `accesspoint` arn types, but found: `{arnType}`", + "type": "error" + }, + { + "conditions": [], + "error": "Access Points do not support S3 Accelerate", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{accessPointName_ssa_1}-{bucketArn#accountId}.s3-accesspoint-fips.dualstack.{bucketArn#region}.{bucketPartition#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{bucketArn#region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{accessPointName_ssa_1}-{bucketArn#accountId}.s3-accesspoint-fips.{bucketArn#region}.{bucketPartition#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{bucketArn#region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{accessPointName_ssa_1}-{bucketArn#accountId}.s3-accesspoint.dualstack.{bucketArn#region}.{bucketPartition#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{bucketArn#region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "{url#scheme}://{accessPointName_ssa_1}-{bucketArn#accountId}.{url#authority}{url#path}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{bucketArn#region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{accessPointName_ssa_1}-{bucketArn#accountId}.s3-accesspoint.{bucketArn#region}.{bucketPartition#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{bucketArn#region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "error": "Invalid ARN: The ARN was not for the S3 service, found: {bucketArn#service}", + "type": "error" + }, + { + "conditions": [], + "error": "S3 MRAP does not support dual-stack", + "type": "error" + }, + { + "conditions": [], + "error": "S3 MRAP does not support FIPS", + "type": "error" + }, + { + "conditions": [], + "error": "S3 MRAP does not support S3 Accelerate", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid configuration: Multi-Region Access Point ARNs are disabled.", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{accessPointName_ssa_1}.accesspoint.s3-global.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4a", + "signingName": "s3", + "signingRegionSet": [ + "*" + ] + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "error": "Client was configured for partition `{partitionResult#name}` but bucket referred to partition `{bucketArn#partition}`", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid Access Point Name", + "type": "error" + }, + { + "conditions": [], + "error": "S3 Outposts does not support Dual-stack", + "type": "error" + }, + { + "conditions": [], + "error": "S3 Outposts does not support FIPS", + "type": "error" + }, + { + "conditions": [], + "error": "S3 Outposts does not support S3 Accelerate", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid Arn: Outpost Access Point ARN contains sub resources", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{accessPointName_ssa_2}-{bucketArn#accountId}.{outpostId_ssa_1}.{url#authority}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4a", + "signingName": "s3-outposts", + "signingRegionSet": [ + "*" + ] + }, + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3-outposts", + "signingRegion": "{bucketArn#region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{accessPointName_ssa_2}-{bucketArn#accountId}.{outpostId_ssa_1}.s3-outposts.{bucketArn#region}.{bucketPartition#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4a", + "signingName": "s3-outposts", + "signingRegionSet": [ + "*" + ] + }, + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3-outposts", + "signingRegion": "{bucketArn#region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "error": "Invalid ARN: The access point name may only contain a-z, A-Z, 0-9 and `-`. Found: `{accessPointName_ssa_2}`", + "type": "error" + }, + { + "conditions": [], + "error": "Expected an outpost type `accesspoint`, found {outpostType}", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid ARN: expected an access point name", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid ARN: Expected a 4-component resource", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid ARN: The outpost Id may only contain a-z, A-Z, 0-9 and `-`. Found: `{outpostId_ssa_1}`", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid ARN: The Outpost Id was not set", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid ARN: Unrecognized format: {Bucket} (type: {arnType})", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid ARN: No ARN type specified", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid ARN: `{Bucket}` was not a valid ARN", + "type": "error" + }, + { + "conditions": [], + "error": "Path-style addressing cannot be used with ARN buckets", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3-fips.dualstack.us-east-1.{partitionResult#dnsSuffix}/{uri_encoded_bucket}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "us-east-1" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3-fips.dualstack.{Region}.{partitionResult#dnsSuffix}/{uri_encoded_bucket}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3-fips.us-east-1.{partitionResult#dnsSuffix}/{uri_encoded_bucket}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "us-east-1" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3-fips.{Region}.{partitionResult#dnsSuffix}/{uri_encoded_bucket}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3.dualstack.us-east-1.{partitionResult#dnsSuffix}/{uri_encoded_bucket}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "us-east-1" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3.dualstack.{Region}.{partitionResult#dnsSuffix}/{uri_encoded_bucket}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "{url#scheme}://{url#authority}{url#normalizedPath}{uri_encoded_bucket}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "us-east-1" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "{url#scheme}://{url#authority}{url#normalizedPath}{uri_encoded_bucket}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3.{partitionResult#dnsSuffix}/{uri_encoded_bucket}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "us-east-1" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3.{partitionResult#dnsSuffix}/{uri_encoded_bucket}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3.{Region}.{partitionResult#dnsSuffix}/{uri_encoded_bucket}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "error": "Path-style addressing cannot be used with S3 Accelerate", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": "{url#scheme}://{url#authority}{url#path}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3-object-lambda", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3-object-lambda-fips.{Region}.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3-object-lambda", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3-object-lambda.{Region}.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3-object-lambda", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3-fips.dualstack.us-east-1.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "us-east-1" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3-fips.dualstack.{Region}.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3-fips.us-east-1.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "us-east-1" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3-fips.{Region}.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3.dualstack.us-east-1.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "us-east-1" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3.dualstack.{Region}.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "{url#scheme}://{url#authority}{url#path}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "us-east-1" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "{url#scheme}://{url#authority}{url#path}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "us-east-1" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3.{Region}.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "error": "A region must be set when sending requests to S3.", + "type": "error" + } + ], + "root": 2, + "nodeCount": 553, + "nodes": "/////wAAAAH/////AAAAAAAAAAMF9eFzAAAAAQAAAagAAAAEAAAAAgAAARAAAAAFAAAAAwAAAOkAAAAGAAAABAAAAFUAAAAHAAAABQAAAA8AAAAIAAAACAAAAAkF9eFzAAAAEAAAAAoAAAANAAAAEgAAAAsAAAANAAAAEwAAAAwAAAANAAAAFgX14Q4AAAANAAAAIwAAAA4F9eEqAAAAJAX14WcAAAGzAAAABgAAAQ8AAAAQAAAABwAAAQ4AAAARAAAACAAAABMAAAASAAAADgAAAfUAAABqAAAACQAAABQAAAAYAAAACgAAABUAAAAYAAAACwAAABYAAAAYAAAADAAAABcAAAAYAAAADQAAAiMAAAAYAAAADgAAAE0AAAAZAAAAFAAAAEkAAAAaAAAAGgAAABsAAABOAAAAJQAAABwF9eFWAAAAJgX14VYAAAAdAAAAJwAAAC8AAAAeAAAAMAX14ToAAAAfAAAAMgAAACAF9eFVAAAAMwAAACEAAACIAAAANwX14UwAAAAiAAAAOwAAACMF9eFUAAAAPAAAACcAAAAkAAAAPQAAACUF9eFTAAAAPgAAACYAAACSAAAAPwAAACkF9eEuAAAAPQAAACgF9eFTAAAAPgAAACkAAACWAAAAQAAAACoF9eE2AAAAQgAAACsF9eE1AAAARgAAACwF9eE0AAAARwAAAC0F9eFRAAAASQAAAC4F9eFQAAAASgX14U4F9eFPAAAAKAAAADAF9eE5AAAAKQX14TkAAAAxAAAAKgAAALkAAAAyAAAAMAAAAD4AAAAzAAAAMQX14S0AAAA0AAAAMwAAADUAAAIOAAAAPAAAADgAAAA2AAAAPgX14TcAAAA3AAAAPwAAADkF9eEuAAAAPgX14TcAAAA5AAAAQAAAADoF9eE2AAAAQgAAADsF9eE1AAAARQAAADwF9eFBAAAARgAAAD0F9eE0AAAASAX14UAF9eEzAAAAMQX14S0AAAA/AAAAMwAAAEAAAAIOAAAAPAAAAEMAAABBAAAAPgX14TcAAABCAAAAPwAAAEQF9eEuAAAAPgX14TcAAABEAAAAQAAAAEUF9eE2AAAAQgAAAEYF9eE1AAAARAX14S8AAABHAAAARgAAAEgF9eE0AAAASAX14TIF9eEzAAAAGQAAAEoF9eEqAAAALgX14ScAAABLAAAAOQAAAEwF9eEpAAAAOgX14SgF9eEpAAAAGgX14VgAAABOAAAAHAX14VcAAABPAAAAIgAAAFIAAABQAAAAIwAAAFEAAAIhAAAAJAX14WcF9eFzAAAALgX14WEAAABTAAAAOQAAAFQF9eFjAAAAOgX14WIF9eFjAAAABQAAAGUAAABWAAAACAAAAFcF9eFzAAAAEAAAAFgAAABZAAAAEgAAAFsAAABZAAAAEwAAAFoAAABcAAAAFQAAAGEAAABfAAAAEwAAAF0AAABcAAAAFQAAAGIAAABfAAAAFQAAAGEAAABeAAAAFgX14Q4AAABfAAAAIwAAAGAF9eEqAAAAJAX14WcF9eEqAAAAFgX14Q0AAABiAAAAIwAAAGMF9eEqAAAAJAX14WUAAABkAAAALgX14W4F9eFvAAAABgAAANYAAABmAAAABwAAANAAAABnAAAACAAAAHcAAABoAAAADgAAAHYAAABpAAAAFQAAAGoF9eEXAAAAGgAAAGsAAAH2AAAAJQAAAGwF9eFWAAAAJgX14VYAAABtAAAAJwAAAHAAAABuAAAAMAX14ToAAABvAAAAMgAAAIgF9eFVAAAAKAAAAHEF9eE5AAAAKQX14TkAAAByAAAAKgAAAHMAAAH0AAAAMAX14TgAAAB0AAAANAAAAHUF9eFIAAAAQQX14UUF9eFIAAAAFQAAAfUF9eEXAAAACQAAAHgAAAB8AAAACgAAAHkAAAB8AAAACwAAAHoAAAB8AAAADAAAAHsAAAB8AAAADQAAAMoAAAB8AAAADgAAAMMAAAB9AAAAFAAAAL4AAAB+AAAAFQAAAH8F9eEXAAAAFwAAAIAAAACBAAAAGAAAAL0AAACBAAAAGgAAAIIAAADFAAAAJQAAAIMF9eFWAAAAJgX14VYAAACEAAAAJwAAAJ8AAACFAAAAMAX14ToAAACGAAAAMgAAAIcF9eFVAAAAMwAAAI0AAACIAAAANwX14UwAAACJAAAAOwAAAIoF9eFUAAAAPAX14VMAAACLAAAAPQAAAIwF9eFTAAAAPwX14VMF9eEuAAAANwX14UwAAACOAAAAOwAAAI8F9eFUAAAAPAAAAJQAAACQAAAAPQAAAJEF9eFTAAAAPgAAAJMAAACSAAAAPwAAAJYF9eEuAAAAPwAAAJkF9eEuAAAAPQAAAJUF9eFTAAAAPgAAAJkAAACWAAAAQAAAAJcF9eE2AAAAQgAAAJgF9eE1AAAARgX14VIF9eE0AAAAQAAAAJoF9eE2AAAAQgAAAJsF9eE1AAAARgAAAJwF9eE0AAAARwAAAJ0F9eFRAAAASQAAAJ4F9eFQAAAASgX14U0F9eFPAAAAKAAAAKAF9eE5AAAAKQX14TkAAAChAAAAKgAAALkAAACiAAAAMAAAAK4AAACjAAAAMQX14S0AAACkAAAAMwAAAKUAAAIOAAAAPAAAAKgAAACmAAAAPgX14TcAAACnAAAAPwAAAKkF9eEuAAAAPgX14TcAAACpAAAAQAAAAKoF9eE2AAAAQgAAAKsF9eE1AAAARQAAAKwF9eFBAAAARgAAAK0F9eE0AAAASAX14T8F9eEzAAAAMQX14S0AAACvAAAAMwAAALAAAAIOAAAAPAAAALMAAACxAAAAPgX14TcAAACyAAAAPwAAALQF9eEuAAAAPgX14TcAAAC0AAAAQAAAALUF9eE2AAAAQgAAALYF9eE1AAAARAX14S8AAAC3AAAARgAAALgF9eE0AAAASAX14TAF9eEzAAAAMAX14TgAAAC6AAAANAAAALsF9eFIAAAAQQX14UUAAAC8AAAAQwX14UYF9eFHAAAAGQX14SQF9eEqAAAAFQAAAL8F9eEXAAAAGQAAAMAF9eEqAAAAHgAAAMIAAADBAAAALgX14SIF9eEkAAAALgX14SEF9eEjAAAAFQAAAMQF9eEXAAAAGgX14VgAAADFAAAAHAX14VcAAADGAAAAIgAAAMkAAADHAAAAIwAAAMgAAAIhAAAAJAX14WUF9eFzAAAALgX14V8F9eFgAAAAEQAAAMsF9eEWAAAAFAAAAMwF9eEVAAAAFQAAAM0AAAImAAAAIQAAAM4AAAImAAAALAX14RAAAADPAAAALQX14RIF9eEUAAAACAAAANEAAADXAAAAEAAAANIAAADcAAAAEgAAANMAAADcAAAAEwAAANQAAADgAAAAFAAAANUAAADjAAAAFQAAAOcAAAGRAAAACAAAANoAAADXAAAAEwAAANgF9eEJAAAAFAAAANkAAADjAAAAFQAAAOcF9eEJAAAAEAAAANsAAADcAAAAEgAAAN8AAADcAAAAEwAAAN0AAADgAAAAFAAAAN4AAADjAAAAFQAAAOcF9eEMAAAAEwAAAOIAAADgAAAAFAAAAOEF9eEJAAAAFQX14QkF9eEMAAAAFAAAAOYAAADjAAAAFQAAAOQF9eEJAAAAHgAAAOUF9eEJAAAAIgX14QcF9eEJAAAAFQAAAOcAAAGfAAAAHgAAAOgF9eEIAAAAIgX14QcF9eEIAAAABAX14QIAAADqAAAABQAAAOsAAAHgAAAABgAAAQ8AAADsAAAABwAAAQ4AAADtAAAACAAAAO4AAAHrAAAACQAAAO8AAADzAAAACgAAAPAAAADzAAAACwAAAPEAAADzAAAADAAAAPIAAADzAAAADQAAAiMAAADzAAAADgAAAQoAAAD0AAAAFAAAAQgAAAD1AAAAGgAAAPYAAAELAAAAJQAAAPcF9eFWAAAAJgX14VYAAAD4AAAAJwAAAPkAAAIGAAAAKAAAAPoF9eE5AAAAKQX14TkAAAD7AAAAKgAAAhoAAAD8AAAAMAX14SsAAAD9AAAAMQX14S0AAAD+AAAAMwAAAP8AAAIOAAAAPAAAAQIAAAEAAAAAPgX14TcAAAEBAAAAPwAAAQMF9eEuAAAAPgX14TcAAAEDAAAAQAAAAQQF9eE2AAAAQgAAAQUF9eE1AAAARQAAAQYF9eFBAAAARgAAAQcF9eE0AAAASAX14T4F9eEzAAAAGQAAAQkF9eEqAAAALgX14R8F9eEgAAAAGgX14VgAAAELAAAAHAX14VcAAAEMAAAAIgAAAQ0AAAIgAAAALgX14V0F9eFeAAAACAAAAY0F9eEJAAAACAAAAZcF9eEJAAAAAwAAAVoAAAERAAAABAX14QMAAAESAAAABQAAARwAAAETAAAACAAAARQF9eFzAAAADwX14QUAAAEVAAAAEAAAARYAAAEZAAAAEgAAARcAAAEZAAAAEwAAARgAAAEZAAAAFgX14Q4AAAEZAAAAIwAAARoF9eEqAAAAJAX14WYAAAEbAAAALgX14WoF9eFrAAAABgAAAZUAAAEdAAAABwAAAYsAAAEeAAAACAAAAScAAAEfAAAADgAAAfUAAAEgAAAAGgAAASEAAAH2AAAAJQAAASIF9eFWAAAAJgX14VYAAAEjAAAAJwAAASQAAAEzAAAAKAAAASUF9eE5AAAAKQX14TkAAAEmAAAAKgAAAU8AAAH0AAAACQAAASgAAAEsAAAACgAAASkAAAEsAAAACwAAASoAAAEsAAAADAAAASsAAAEsAAAADQAAAYoAAAEsAAAADgAAAVMAAAEtAAAADwX14QUAAAEuAAAAFAAAAVEAAAEvAAAAGgAAATAAAAFVAAAAJQAAATEF9eFWAAAAJgX14VYAAAEyAAAAJwAAATUAAAEzAAAAMAX14ToAAAE0AAAAMgX14UoF9eFVAAAAKAAAATYF9eE5AAAAKQX14TkAAAE3AAAAKgAAAU8AAAE4AAAAMAAAAUQAAAE5AAAAMQX14S0AAAE6AAAAMwAAATsAAAIOAAAAPAAAAT4AAAE8AAAAPgX14TcAAAE9AAAAPwAAAT8F9eEuAAAAPgX14TcAAAE/AAAAQAAAAUAF9eE2AAAAQgAAAUEF9eE1AAAARQAAAUIF9eFBAAAARgAAAUMF9eE0AAAASAX14T0F9eEzAAAAMQX14S0AAAFFAAAAMwAAAUYAAAIOAAAAPAAAAUkAAAFHAAAAPgX14TcAAAFIAAAAPwAAAUoF9eEuAAAAPgX14TcAAAFKAAAAQAAAAUsF9eE2AAAAQgAAAUwF9eE1AAAARAX14S8AAAFNAAAARgAAAU4F9eE0AAAASAX14TEF9eEzAAAAMAX14TgAAAFQAAAANAX14UMF9eFIAAAAGQAAAVIF9eEqAAAALgX14RsF9eEcAAAADwX14QUAAAFUAAAAGgX14VgAAAFVAAAAHAX14VcAAAFWAAAAIgAAAVkAAAFXAAAAIwAAAVgAAAIhAAAAJAX14WYF9eFzAAAALgX14VsF9eFcAAAABAX14QIAAAFbAAAABQAAAWUAAAFcAAAACAAAAV0F9eFzAAAADwX14QUAAAFeAAAAEAAAAV8AAAFiAAAAEgAAAWAAAAFiAAAAEwAAAWEAAAFiAAAAFgX14Q4AAAFiAAAAIwAAAWMF9eEqAAAAJAX14SsAAAFkAAAALgX14WgF9eFpAAAABgAAAZUAAAFmAAAABwAAAYsAAAFnAAAACAAAAWgAAAHrAAAACQAAAWkAAAFtAAAACgAAAWoAAAFtAAAACwAAAWsAAAFtAAAADAAAAWwAAAFtAAAADQAAAYoAAAFtAAAADgAAAYUAAAFuAAAADwX14QUAAAFvAAAAFAAAAYMAAAFwAAAAGgAAAXEAAAGHAAAAJQAAAXIF9eFWAAAAJgX14VYAAAFzAAAAJwAAAXQAAAIGAAAAKAAAAXUF9eE5AAAAKQX14TkAAAF2AAAAKgAAAhoAAAF3AAAAMAX14SsAAAF4AAAAMQX14S0AAAF5AAAAMwAAAXoAAAIOAAAAPAAAAX0AAAF7AAAAPgX14TcAAAF8AAAAPwAAAX4F9eEuAAAAPgX14TcAAAF+AAAAQAAAAX8F9eE2AAAAQgAAAYAF9eE1AAAARQAAAYEF9eFBAAAARgAAAYIF9eE0AAAASAX14TwF9eEzAAAAGQAAAYQF9eEqAAAALgX14RkF9eEaAAAADwX14QUAAAGGAAAAGgX14VgAAAGHAAAAHAX14VcAAAGIAAAAIgAAAYkAAAIgAAAALgX14VkF9eFaAAAADwX14QUAAAIjAAAACAAAAYwF9eEJAAAADwX14QUAAAGNAAAAEAAAAY4AAAGaAAAAEgAAAY8AAAGaAAAAEwAAAZAAAAGaAAAAFAAAAZEF9eEJAAAAGwAAAZIF9eEMAAAAHQX14QsAAAGTAAAAHwX14QsAAAGUAAAAIAX14QsAAAGmAAAACAAAAZYF9eEJAAAADwX14QUAAAGXAAAAEAAAAZgAAAGaAAAAEgAAAZkAAAGaAAAAEwAAAZsAAAGaAAAAFAX14QwF9eEJAAAAFAAAAZ4AAAGcAAAAFgAAAZ0F9eEJAAAAIgX14QoF9eEJAAAAFgAAAaAAAAGfAAAAGwAAAaMF9eEMAAAAGwAAAaIAAAGhAAAAIgX14QoF9eEMAAAAIgX14QoAAAGjAAAAKwX14QsAAAGkAAAALwX14QsAAAGlAAAANQX14QsAAAGmAAAANgX14QsAAAGnAAAAOAX14QsF9eEMAAAAAgX14QEAAAGpAAAAAwAAAd4AAAGqAAAABAX14QQAAAGrAAAABQAAAbYAAAGsAAAACAAAAa0F9eFzAAAAEAAAAa4AAAGxAAAAEgAAAa8AAAGxAAAAEwAAAbAAAAGxAAAAFgX14Q4AAAGxAAAAIwAAAbIF9eEqAAAAJAX14SwAAAGzAAAALgX14XAAAAG0AAAAOQAAAbUF9eFyAAAAOgX14XEF9eFyAAAABgX14QYAAAG3AAAABwX14QYAAAG4AAAACAAAAcIAAAG5AAAADgAAAfUAAAG6AAAAGgAAAbsAAAH2AAAAJQAAAbwF9eFWAAAAJgX14VYAAAG9AAAAJwAAAb4AAAHRAAAAKAAAAb8F9eE5AAAAKQX14TkAAAHAAAAAKgAAAdcAAAHBAAAAMAX14SwAAAH0AAAACQAAAcMAAAHHAAAACgAAAcQAAAHHAAAACwAAAcUAAAHHAAAADAAAAcYAAAHHAAAADQAAAiMAAAHHAAAADgAAAdkAAAHIAAAADwAAAcwAAAHJAAAAFAAAAcoAAAHNAAAAGQAAAcsF9eEqAAAALgX14SUF9eEmAAAAFAAAAhwAAAHNAAAAGgAAAc4AAAHaAAAAJQAAAc8F9eFWAAAAJgX14VYAAAHQAAAAJwAAAdMAAAHRAAAAMAX14ToAAAHSAAAAMgX14UsF9eFVAAAAKAAAAdQF9eE5AAAAKQX14TkAAAHVAAAAKgAAAdcAAAHWAAAAMAX14SwAAAIMAAAAMAX14SwAAAHYAAAANAX14UQF9eFIAAAAGgX14VgAAAHaAAAAHAX14VcAAAHbAAAAIgX14WQAAAHcAAAAIwAAAd0AAAIhAAAAJAX14SwF9eFzAAAABAX14QIAAAHfAAAABQAAAegAAAHgAAAACAAAAeEF9eFzAAAAEAAAAeIAAAHlAAAAEgAAAeMAAAHlAAAAEwAAAeQAAAHlAAAAFgX14Q4AAAHlAAAAIwAAAeYF9eEqAAAAJAX14SsAAAHnAAAALgX14WwF9eFtAAAABgX14QYAAAHpAAAABwX14QYAAAHqAAAACAAAAfcAAAHrAAAADgAAAfUAAAHsAAAAGgAAAe0AAAH2AAAAJQAAAe4F9eFWAAAAJgX14VYAAAHvAAAAJwAAAfAAAAIGAAAAKAAAAfEF9eE5AAAAKQX14TkAAAHyAAAAKgAAAhoAAAHzAAAAMAX14SsAAAH0AAAAMQX14S0AAAIOAAAAGgX14VgAAAH2AAAAHAX14VcF9eFzAAAACQAAAfgAAAH8AAAACgAAAfkAAAH8AAAACwAAAfoAAAH8AAAADAAAAfsAAAH8AAAADQAAAiMAAAH8AAAADgAAAh0AAAH9AAAADwAAAgEAAAH+AAAAFAAAAf8AAAICAAAAGQAAAgAF9eEqAAAALgX14R0F9eEeAAAAFAAAAhwAAAICAAAAGgAAAgMAAAIeAAAAJQAAAgQF9eFWAAAAJgX14VYAAAIFAAAAJwAAAggAAAIGAAAAMAX14ToAAAIHAAAAMgX14UkF9eFVAAAAKAAAAgkF9eE5AAAAKQX14TkAAAIKAAAAKgAAAhoAAAILAAAAMAX14SsAAAIMAAAAMQX14S0AAAINAAAAMwAAAhEAAAIOAAAAPAX14TcAAAIPAAAAPgX14TcAAAIQAAAAPwX14TcF9eEuAAAAPAAAAhQAAAISAAAAPgX14TcAAAITAAAAPwAAAhUF9eEuAAAAPgX14TcAAAIVAAAAQAAAAhYF9eE2AAAAQgAAAhcF9eE1AAAARQAAAhgF9eFBAAAARgAAAhkF9eE0AAAASAX14TsF9eEzAAAAMAX14SsAAAIbAAAANAX14UIF9eFIAAAAGQX14RgF9eEqAAAAGgX14VgAAAIeAAAAHAX14VcAAAIfAAAAIgX14WQAAAIgAAAAIwAAAiIAAAIhAAAAJAX14SoF9eFzAAAAJAX14SsF9eFzAAAAEQAAAiQF9eEWAAAAFAAAAiUF9eEVAAAAIQAAAigAAAImAAAALAX14REAAAInAAAALQX14RMF9eEUAAAALAX14Q8AAAIpAAAALQX14Q8F9eEU" +} \ No newline at end of file diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/query/endpoint-rule-set-default-regional.json b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/query/endpoint-rule-set-default-regional.json new file mode 100644 index 000000000000..e0314767c0df --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/query/endpoint-rule-set-default-regional.json @@ -0,0 +1,339 @@ +{ + "version": "1.0", + "parameters": { + "Region": { + "builtIn": "AWS::Region", + "required": false, + "documentation": "The AWS region used to dispatch the request.", + "type": "string" + }, + "UseDualStack": { + "builtIn": "AWS::UseDualStack", + "required": true, + "default": false, + "documentation": "When true, use the dual-stack endpoint. If the configured endpoint does not support dual-stack, dispatching the request MAY return an error.", + "type": "boolean" + }, + "UseFIPS": { + "builtIn": "AWS::UseFIPS", + "required": true, + "default": false, + "documentation": "When true, send this request to the FIPS-compliant regional endpoint. If the configured endpoint does not have a FIPS compliant endpoint, dispatching the request will return an error.", + "type": "boolean" + }, + "Endpoint": { + "builtIn": "SDK::Endpoint", + "required": false, + "documentation": "Override the endpoint used to send this request", + "type": "string" + } + }, + "rules": [ + { + "conditions": [ + { + "fn": "isSet", + "argv": [ + { + "ref": "Endpoint" + } + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseFIPS" + }, + true + ] + } + ], + "error": "Invalid Configuration: FIPS and custom endpoint are not supported", + "type": "error" + }, + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseDualStack" + }, + true + ] + } + ], + "error": "Invalid Configuration: Dualstack and custom endpoint are not supported", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": { + "ref": "Endpoint" + }, + "properties": {}, + "headers": {} + }, + "type": "endpoint" + } + ], + "type": "tree" + }, + { + "conditions": [ + { + "fn": "isSet", + "argv": [ + { + "ref": "Region" + } + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "aws.partition", + "argv": [ + { + "ref": "Region" + } + ], + "assign": "PartitionResult" + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseFIPS" + }, + true + ] + }, + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseDualStack" + }, + true + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + true, + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsFIPS" + ] + } + ] + }, + { + "fn": "booleanEquals", + "argv": [ + true, + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsDualStack" + ] + } + ] + } + ], + "rules": [ + { + "conditions": [], + "endpoint": { + "url": "https://query-fips.{Region}.{PartitionResult#dualStackDnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + } + ], + "type": "tree" + }, + { + "conditions": [], + "error": "FIPS and DualStack are enabled, but this partition does not support one or both", + "type": "error" + } + ], + "type": "tree" + }, + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseFIPS" + }, + true + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsFIPS" + ] + }, + true + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "stringEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "name" + ] + }, + "aws-us-gov" + ] + } + ], + "endpoint": { + "url": "https://query.{Region}.amazonaws.com", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://query-fips.{Region}.{PartitionResult#dnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + } + ], + "type": "tree" + }, + { + "conditions": [], + "error": "FIPS is enabled but this partition does not support FIPS", + "type": "error" + } + ], + "type": "tree" + }, + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseDualStack" + }, + true + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + true, + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsDualStack" + ] + } + ] + } + ], + "rules": [ + { + "conditions": [], + "endpoint": { + "url": "https://query.{Region}.{PartitionResult#dualStackDnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + } + ], + "type": "tree" + }, + { + "conditions": [], + "error": "DualStack is enabled but this partition does not support DualStack", + "type": "error" + } + ], + "type": "tree" + }, + { + "conditions": [], + "endpoint": { + "url": "https://query.{Region}.{PartitionResult#dnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + } + ], + "type": "tree" + } + ], + "type": "tree" + }, + { + "conditions": [], + "error": "Invalid Configuration: Missing Region", + "type": "error" + } + ] +} \ No newline at end of file diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/bdd/endpoint-provider-bdd-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/bdd/endpoint-provider-bdd-class.java new file mode 100644 index 000000000000..39e5da5ba9ff --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/bdd/endpoint-provider-bdd-class.java @@ -0,0 +1,198 @@ +package software.amazon.awssdk.services.query.endpoints.internal; + +import java.util.concurrent.CompletableFuture; +import software.amazon.awssdk.annotations.Generated; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.endpoints.Endpoint; +import software.amazon.awssdk.endpoints.EndpointUrl; +import software.amazon.awssdk.services.query.endpoints.QueryEndpointParams; +import software.amazon.awssdk.services.query.endpoints.QueryEndpointProvider; +import software.amazon.awssdk.utils.CompletableFutureUtils; + +@Generated("software.amazon.awssdk:codegen") +@SdkInternalApi +public final class DefaultQueryEndpointProvider implements QueryEndpointProvider { + @Override + public CompletableFuture resolveEndpoint(QueryEndpointParams endpointParams) { + try { + Evaluator evaluator = new Evaluator(); + evaluator.params = endpointParams; + evaluator.region = endpointParams.region() == null ? null : endpointParams.region().id(); + Endpoint result = evaluator.nodeP14(); + if (result == null) { + return CompletableFutureUtils.failedFuture(SdkClientException.create("Rule engine did not reach an error or endpoint result")); + } + return CompletableFuture.completedFuture(result); + } catch (SdkClientException e) { + String errorMsg = e.getMessage(); + if (errorMsg != null && errorMsg.contains("Invalid ARN") && errorMsg.contains(":s3:::")) { + return CompletableFutureUtils.failedFuture(SdkClientException.create(errorMsg + ". Use the bucket name instead of simple bucket ARNs in GetBucketLocationRequest.", e)); + } + return CompletableFutureUtils.failedFuture(e); + } catch (Exception error) { + return CompletableFutureUtils.failedFuture(error); + } + } + + private static final class Evaluator { + QueryEndpointParams params; + + String region; + + RulePartition partitionResult; + + private Endpoint nodeP0() { + return null; + } + + private Endpoint nodeP1() { + return Boolean.TRUE.equals(params.useDualStack()) + ? result1() + : result2(); + } + + private Endpoint nodeP2() { + return Boolean.TRUE.equals(params.useFips()) + ? result0() + : nodeP1(); + } + + private Endpoint nodeP3() { + return cond6() + ? result3() + : result4(); + } + + private Endpoint nodeP4() { + return cond5() + ? nodeP3() + : result4(); + } + + private Endpoint nodeP5() { + return cond7() + ? result5() + : result6(); + } + + private Endpoint nodeP6() { + return cond5() + ? nodeP5() + : result7(); + } + + private Endpoint nodeP7() { + return Boolean.TRUE.equals(params.useDualStack()) + ? nodeP4() + : nodeP6(); + } + + private Endpoint nodeP8() { + return cond3() + ? nodeP7() + : result11(); + } + + private Endpoint nodeP9() { + return cond6() + ? result8() + : result9(); + } + + private Endpoint nodeP10() { + return Boolean.TRUE.equals(params.useDualStack()) + ? nodeP9() + : result10(); + } + + private Endpoint nodeP11() { + return cond3() + ? nodeP10() + : result11(); + } + + private Endpoint nodeP12() { + return Boolean.TRUE.equals(params.useFips()) + ? nodeP8() + : nodeP11(); + } + + private Endpoint nodeP13() { + return region != null + ? nodeP12() + : result11(); + } + + private Endpoint nodeP14() { + return params.endpoint() != null + ? nodeP2() + : nodeP13(); + } + + private boolean cond3() { + partitionResult = RulesFunctions.awsPartition(region); + return partitionResult != null; + } + + private boolean cond5() { + return (partitionResult.supportsFIPS()); + } + + private boolean cond6() { + return (partitionResult.supportsDualStack()); + } + + private boolean cond7() { + return ("aws-us-gov".equals(partitionResult.name())); + } + + private Endpoint result0() { + throw SdkClientException.create("Invalid Configuration: FIPS and custom endpoint are not supported"); + } + + private Endpoint result1() { + throw SdkClientException.create("Invalid Configuration: Dualstack and custom endpoint are not supported"); + } + + private Endpoint result2() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromString(params.endpoint())).build(); + } + + private Endpoint result3() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", "query-fips." + region + "." + partitionResult.dualStackDnsSuffix(), -1, "")).build(); + } + + private Endpoint result4() { + throw SdkClientException.create("FIPS and DualStack are enabled, but this partition does not support one or both"); + } + + private Endpoint result5() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", "query." + region + ".amazonaws.com", -1, "")).build(); + } + + private Endpoint result6() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", "query-fips." + region + "." + partitionResult.dnsSuffix(), -1, "")).build(); + } + + private Endpoint result7() { + throw SdkClientException.create("FIPS is enabled but this partition does not support FIPS"); + } + + private Endpoint result8() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", "query." + region + "." + partitionResult.dualStackDnsSuffix(), -1, "")).build(); + } + + private Endpoint result9() { + throw SdkClientException.create("DualStack is enabled but this partition does not support DualStack"); + } + + private Endpoint result10() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", "query." + region + "." + partitionResult.dnsSuffix(), -1, "")).build(); + } + + private Endpoint result11() { + throw SdkClientException.create("Invalid Configuration: Missing Region"); + } + } +} diff --git a/test/codegen-generated-classes-test/src/main/resources/codegen-resources/bddendpoints/customization.config b/test/codegen-generated-classes-test/src/main/resources/codegen-resources/bddendpoints/customization.config new file mode 100644 index 000000000000..2c63c0851048 --- /dev/null +++ b/test/codegen-generated-classes-test/src/main/resources/codegen-resources/bddendpoints/customization.config @@ -0,0 +1,2 @@ +{ +} diff --git a/test/codegen-generated-classes-test/src/main/resources/codegen-resources/bddendpoints/endpoint-bdd-1.json b/test/codegen-generated-classes-test/src/main/resources/codegen-resources/bddendpoints/endpoint-bdd-1.json new file mode 100644 index 000000000000..157f1b1237d4 --- /dev/null +++ b/test/codegen-generated-classes-test/src/main/resources/codegen-resources/bddendpoints/endpoint-bdd-1.json @@ -0,0 +1,212 @@ +{ + "version": "1.1", + "parameters": { + "Region": { + "builtIn": "AWS::Region", + "required": false, + "documentation": "The AWS region used to dispatch the request.", + "type": "string" + }, + "UseDualStack": { + "builtIn": "AWS::UseDualStack", + "required": true, + "default": false, + "documentation": "When true, use the dual-stack endpoint. If the configured endpoint does not support dual-stack, dispatching the request MAY return an error.", + "type": "boolean" + }, + "UseFIPS": { + "builtIn": "AWS::UseFIPS", + "required": true, + "default": false, + "documentation": "When true, send this request to the FIPS-compliant regional endpoint. If the configured endpoint does not have a FIPS compliant endpoint, dispatching the request will return an error.", + "type": "boolean" + }, + "Endpoint": { + "builtIn": "SDK::Endpoint", + "required": false, + "documentation": "Override the endpoint used to send this request", + "type": "string" + } + }, + "conditions": [ + { + "fn": "isSet", + "argv": [ + { + "ref": "Endpoint" + } + ] + }, + { + "fn": "isSet", + "argv": [ + { + "ref": "Region" + } + ] + }, + { + "fn": "aws.partition", + "argv": [ + { + "ref": "Region" + } + ], + "assign": "PartitionResult" + }, + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseFIPS" + }, + true + ] + }, + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseDualStack" + }, + true + ] + }, + { + "fn": "booleanEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsDualStack" + ] + }, + true + ] + }, + { + "fn": "booleanEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsFIPS" + ] + }, + true + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "name" + ] + }, + "aws-us-gov" + ] + } + ], + "results": [ + { + "conditions": [], + "error": "Invalid Configuration: FIPS and custom endpoint are not supported", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid Configuration: Dualstack and custom endpoint are not supported", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": { + "ref": "Endpoint" + }, + "properties": {}, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://connect-fips.{Region}.{PartitionResult#dualStackDnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "error": "FIPS and DualStack are enabled, but this partition does not support one or both", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": "https://connect.{Region}.amazonaws.com", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://connect-fips.{Region}.{PartitionResult#dnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "error": "FIPS is enabled but this partition does not support FIPS", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": "https://connect.{Region}.{PartitionResult#dualStackDnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "error": "DualStack is enabled but this partition does not support DualStack", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": "https://connect.{Region}.{PartitionResult#dnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "error": "Invalid Configuration: Missing Region", + "type": "error" + } + ], + "root": 2, + "nodeCount": 14, + "nodes": "/////wAAAAH/////AAAAAAAAAA0AAAADAAAAAQAAAAQF9eEMAAAAAgAAAAUF9eEMAAAAAwAAAAgAAAAGAAAABAAAAAcF9eELAAAABQX14QkF9eEKAAAABAAAAAsAAAAJAAAABgAAAAoF9eEIAAAABwX14QYF9eEHAAAABQAAAAwF9eEFAAAABgX14QQF9eEFAAAAAwX14QEAAAAOAAAABAX14QIF9eED" +} \ No newline at end of file diff --git a/test/codegen-generated-classes-test/src/main/resources/codegen-resources/bddendpoints/endpoint-rule-set.json b/test/codegen-generated-classes-test/src/main/resources/codegen-resources/bddendpoints/endpoint-rule-set.json new file mode 100644 index 000000000000..c87dbf336b6d --- /dev/null +++ b/test/codegen-generated-classes-test/src/main/resources/codegen-resources/bddendpoints/endpoint-rule-set.json @@ -0,0 +1,339 @@ +{ + "version": "1.0", + "parameters": { + "Region": { + "builtIn": "AWS::Region", + "required": false, + "documentation": "The AWS region used to dispatch the request.", + "type": "string" + }, + "UseDualStack": { + "builtIn": "AWS::UseDualStack", + "required": true, + "default": false, + "documentation": "When true, use the dual-stack endpoint. If the configured endpoint does not support dual-stack, dispatching the request MAY return an error.", + "type": "boolean" + }, + "UseFIPS": { + "builtIn": "AWS::UseFIPS", + "required": true, + "default": false, + "documentation": "When true, send this request to the FIPS-compliant regional endpoint. If the configured endpoint does not have a FIPS compliant endpoint, dispatching the request will return an error.", + "type": "boolean" + }, + "Endpoint": { + "builtIn": "SDK::Endpoint", + "required": false, + "documentation": "Override the endpoint used to send this request", + "type": "string" + } + }, + "rules": [ + { + "conditions": [ + { + "fn": "isSet", + "argv": [ + { + "ref": "Endpoint" + } + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseFIPS" + }, + true + ] + } + ], + "error": "Invalid Configuration: FIPS and custom endpoint are not supported", + "type": "error" + }, + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseDualStack" + }, + true + ] + } + ], + "error": "Invalid Configuration: Dualstack and custom endpoint are not supported", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": { + "ref": "Endpoint" + }, + "properties": {}, + "headers": {} + }, + "type": "endpoint" + } + ], + "type": "tree" + }, + { + "conditions": [ + { + "fn": "isSet", + "argv": [ + { + "ref": "Region" + } + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "aws.partition", + "argv": [ + { + "ref": "Region" + } + ], + "assign": "PartitionResult" + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseFIPS" + }, + true + ] + }, + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseDualStack" + }, + true + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + true, + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsFIPS" + ] + } + ] + }, + { + "fn": "booleanEquals", + "argv": [ + true, + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsDualStack" + ] + } + ] + } + ], + "rules": [ + { + "conditions": [], + "endpoint": { + "url": "https://connect-fips.{Region}.{PartitionResult#dualStackDnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + } + ], + "type": "tree" + }, + { + "conditions": [], + "error": "FIPS and DualStack are enabled, but this partition does not support one or both", + "type": "error" + } + ], + "type": "tree" + }, + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseFIPS" + }, + true + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsFIPS" + ] + }, + true + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "stringEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "name" + ] + }, + "aws-us-gov" + ] + } + ], + "endpoint": { + "url": "https://connect.{Region}.amazonaws.com", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://connect-fips.{Region}.{PartitionResult#dnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + } + ], + "type": "tree" + }, + { + "conditions": [], + "error": "FIPS is enabled but this partition does not support FIPS", + "type": "error" + } + ], + "type": "tree" + }, + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseDualStack" + }, + true + ] + } + ], + "rules": [ + { + "conditions": [ + { + "fn": "booleanEquals", + "argv": [ + true, + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsDualStack" + ] + } + ] + } + ], + "rules": [ + { + "conditions": [], + "endpoint": { + "url": "https://connect.{Region}.{PartitionResult#dualStackDnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + } + ], + "type": "tree" + }, + { + "conditions": [], + "error": "DualStack is enabled but this partition does not support DualStack", + "type": "error" + } + ], + "type": "tree" + }, + { + "conditions": [], + "endpoint": { + "url": "https://connect.{Region}.{PartitionResult#dnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + } + ], + "type": "tree" + } + ], + "type": "tree" + }, + { + "conditions": [], + "error": "Invalid Configuration: Missing Region", + "type": "error" + } + ] +} \ No newline at end of file diff --git a/test/codegen-generated-classes-test/src/main/resources/codegen-resources/bddendpoints/endpoint-tests.json b/test/codegen-generated-classes-test/src/main/resources/codegen-resources/bddendpoints/endpoint-tests.json new file mode 100644 index 000000000000..189614f2396c --- /dev/null +++ b/test/codegen-generated-classes-test/src/main/resources/codegen-resources/bddendpoints/endpoint-tests.json @@ -0,0 +1,413 @@ +{ + "testCases": [ + { + "documentation": "For region af-south-1 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://connect.af-south-1.amazonaws.com" + } + }, + "params": { + "Region": "af-south-1", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For region ap-northeast-1 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://connect.ap-northeast-1.amazonaws.com" + } + }, + "params": { + "Region": "ap-northeast-1", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For region ap-northeast-2 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://connect.ap-northeast-2.amazonaws.com" + } + }, + "params": { + "Region": "ap-northeast-2", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For region ap-southeast-1 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://connect.ap-southeast-1.amazonaws.com" + } + }, + "params": { + "Region": "ap-southeast-1", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For region ap-southeast-2 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://connect.ap-southeast-2.amazonaws.com" + } + }, + "params": { + "Region": "ap-southeast-2", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For region ca-central-1 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://connect.ca-central-1.amazonaws.com" + } + }, + "params": { + "Region": "ca-central-1", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For region eu-central-1 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://connect.eu-central-1.amazonaws.com" + } + }, + "params": { + "Region": "eu-central-1", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For region eu-west-2 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://connect.eu-west-2.amazonaws.com" + } + }, + "params": { + "Region": "eu-west-2", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For region us-east-1 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://connect.us-east-1.amazonaws.com" + } + }, + "params": { + "Region": "us-east-1", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For region us-west-2 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://connect.us-west-2.amazonaws.com" + } + }, + "params": { + "Region": "us-west-2", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For region us-east-1 with FIPS enabled and DualStack enabled", + "expect": { + "endpoint": { + "url": "https://connect-fips.us-east-1.api.aws" + } + }, + "params": { + "Region": "us-east-1", + "UseFIPS": true, + "UseDualStack": true + } + }, + { + "documentation": "For region us-east-1 with FIPS enabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://connect-fips.us-east-1.amazonaws.com" + } + }, + "params": { + "Region": "us-east-1", + "UseFIPS": true, + "UseDualStack": false + } + }, + { + "documentation": "For region us-east-1 with FIPS disabled and DualStack enabled", + "expect": { + "endpoint": { + "url": "https://connect.us-east-1.api.aws" + } + }, + "params": { + "Region": "us-east-1", + "UseFIPS": false, + "UseDualStack": true + } + }, + { + "documentation": "For region cn-north-1 with FIPS enabled and DualStack enabled", + "expect": { + "endpoint": { + "url": "https://connect-fips.cn-north-1.api.amazonwebservices.com.cn" + } + }, + "params": { + "Region": "cn-north-1", + "UseFIPS": true, + "UseDualStack": true + } + }, + { + "documentation": "For region cn-north-1 with FIPS enabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://connect-fips.cn-north-1.amazonaws.com.cn" + } + }, + "params": { + "Region": "cn-north-1", + "UseFIPS": true, + "UseDualStack": false + } + }, + { + "documentation": "For region cn-north-1 with FIPS disabled and DualStack enabled", + "expect": { + "endpoint": { + "url": "https://connect.cn-north-1.api.amazonwebservices.com.cn" + } + }, + "params": { + "Region": "cn-north-1", + "UseFIPS": false, + "UseDualStack": true + } + }, + { + "documentation": "For region cn-north-1 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://connect.cn-north-1.amazonaws.com.cn" + } + }, + "params": { + "Region": "cn-north-1", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For region us-gov-west-1 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://connect.us-gov-west-1.amazonaws.com" + } + }, + "params": { + "Region": "us-gov-west-1", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For region us-gov-west-1 with FIPS enabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://connect.us-gov-west-1.amazonaws.com" + } + }, + "params": { + "Region": "us-gov-west-1", + "UseFIPS": true, + "UseDualStack": false + } + }, + { + "documentation": "For region us-gov-east-1 with FIPS enabled and DualStack enabled", + "expect": { + "endpoint": { + "url": "https://connect-fips.us-gov-east-1.api.aws" + } + }, + "params": { + "Region": "us-gov-east-1", + "UseFIPS": true, + "UseDualStack": true + } + }, + { + "documentation": "For region us-gov-east-1 with FIPS enabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://connect.us-gov-east-1.amazonaws.com" + } + }, + "params": { + "Region": "us-gov-east-1", + "UseFIPS": true, + "UseDualStack": false + } + }, + { + "documentation": "For region us-gov-east-1 with FIPS disabled and DualStack enabled", + "expect": { + "endpoint": { + "url": "https://connect.us-gov-east-1.api.aws" + } + }, + "params": { + "Region": "us-gov-east-1", + "UseFIPS": false, + "UseDualStack": true + } + }, + { + "documentation": "For region us-gov-east-1 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://connect.us-gov-east-1.amazonaws.com" + } + }, + "params": { + "Region": "us-gov-east-1", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For region us-iso-east-1 with FIPS enabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://connect-fips.us-iso-east-1.c2s.ic.gov" + } + }, + "params": { + "Region": "us-iso-east-1", + "UseFIPS": true, + "UseDualStack": false + } + }, + { + "documentation": "For region us-iso-east-1 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://connect.us-iso-east-1.c2s.ic.gov" + } + }, + "params": { + "Region": "us-iso-east-1", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For region us-isob-east-1 with FIPS enabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://connect-fips.us-isob-east-1.sc2s.sgov.gov" + } + }, + "params": { + "Region": "us-isob-east-1", + "UseFIPS": true, + "UseDualStack": false + } + }, + { + "documentation": "For region us-isob-east-1 with FIPS disabled and DualStack disabled", + "expect": { + "endpoint": { + "url": "https://connect.us-isob-east-1.sc2s.sgov.gov" + } + }, + "params": { + "Region": "us-isob-east-1", + "UseFIPS": false, + "UseDualStack": false + } + }, + { + "documentation": "For custom endpoint with region set and fips disabled and dualstack disabled", + "expect": { + "endpoint": { + "url": "https://example.com" + } + }, + "params": { + "Region": "us-east-1", + "UseFIPS": false, + "UseDualStack": false, + "Endpoint": "https://example.com" + } + }, + { + "documentation": "For custom endpoint with region not set and fips disabled and dualstack disabled", + "expect": { + "endpoint": { + "url": "https://example.com" + } + }, + "params": { + "UseFIPS": false, + "UseDualStack": false, + "Endpoint": "https://example.com" + } + }, + { + "documentation": "For custom endpoint with fips enabled and dualstack disabled", + "expect": { + "error": "Invalid Configuration: FIPS and custom endpoint are not supported" + }, + "params": { + "Region": "us-east-1", + "UseFIPS": true, + "UseDualStack": false, + "Endpoint": "https://example.com" + } + }, + { + "documentation": "For custom endpoint with fips disabled and dualstack enabled", + "expect": { + "error": "Invalid Configuration: Dualstack and custom endpoint are not supported" + }, + "params": { + "Region": "us-east-1", + "UseFIPS": false, + "UseDualStack": true, + "Endpoint": "https://example.com" + } + }, + { + "documentation": "Missing region", + "expect": { + "error": "Invalid Configuration: Missing Region" + } + } + ], + "version": "1.0" +} \ No newline at end of file diff --git a/test/codegen-generated-classes-test/src/main/resources/codegen-resources/bddendpoints/service-2.json b/test/codegen-generated-classes-test/src/main/resources/codegen-resources/bddendpoints/service-2.json new file mode 100644 index 000000000000..c8ef99738ea8 --- /dev/null +++ b/test/codegen-generated-classes-test/src/main/resources/codegen-resources/bddendpoints/service-2.json @@ -0,0 +1,54 @@ +{ + "version": "2.0", + "metadata": { + "apiVersion": "2024-01-01", + "endpointPrefix": "bddendpoints", + "jsonVersion": "1.1", + "protocol": "rest-json", + "serviceAbbreviation": "BddEndpoints", + "serviceFullName": "BDD Endpoint Resolution Test Service", + "serviceId": "BddEndpoints", + "signatureVersion": "v4", + "signingName": "bddendpoints", + "uid": "bddendpoints-2024-01-01", + "auth": [ + "aws.auth#sigv4" + ] + }, + "operations": { + "TestOperation": { + "name": "TestOperation", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { + "shape": "TestOperationRequest" + }, + "output": { + "shape": "TestOperationResponse" + } + } + }, + "shapes": { + "TestOperationRequest": { + "type": "structure", + "members": { + "StringMember": { + "shape": "String" + } + } + }, + "TestOperationResponse": { + "type": "structure", + "members": { + "StringMember": { + "shape": "String" + } + } + }, + "String": { + "type": "string" + } + } +} From 252be0b91744023bb0ce6a65649ea536cdca6c4c Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Wed, 26 Aug 2026 14:49:28 -0700 Subject: [PATCH 3/7] [Endpoints BDD 3/5] Peephole optimizations for BDD endpoint codegen (#7304) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf(endpoints): Add peephole optimizations to BDD endpoint codegen Reduce allocations and method dispatch in BDD-generated endpoint resolvers. All codegen changes are scoped to the BDD path; the tree-based rules code generation is untouched. New BddPeepholeVisitor (BDD-only IR pass, runs before PrepareForCodegenVisitor) rewrites: - stringEquals(coalesce(substring(str, 0, N, false), ""), literal) -> str.startsWith(literal) - stringEquals(coalesce(substring(str, 0, N, true), ""), literal) -> str.endsWith(literal) - stringEquals(coalesce(substring(str, X, Y, rev), ""), literal) -> str.regionMatches(offset, literal, ...) for interior checks - stringEquals(a, b) -> a.equals(b), skipping RulesFunctions dispatch - coalesce(boolExpr, default) -> (expr != null ? expr : default) - ite(cond, a, b) -> (cond ? a : b) - isValidHostLabel(str, constBool) -> specialized runtime helper The BDD condition and result generators emit these synthetic forms as inline Java. Synthetic names are prefixed with __ so they cannot collide with endpoint rule standard library functions. RulesFunctions (shared generated runtime) additions: - awsPartition(region) gains a per-thread last-value cache. The same region resolves on nearly every call in steady state, so this avoids a HashMap lookup plus regex fallback each time. - isValidHostLabelSingle / isValidHostLabelMulti skip the allowDots branch when the flag is known at codegen time. On the S3 test BDD this eliminates all RulesFunctions.stringEquals, ite, coalesce, and isValidHostLabel dispatches, replacing them with 11 native String comparisons and 9 specialized host-label calls. Verified: 690 codegen tests (3 pre-existing EndpointRulesClientTestSpec failures also present on the base branch), rules2 golden files byte-identical to base, and all 32 bddendpoints endpoint test cases passing against the generated BDD resolver. * test(codegen): Update endpoint provider test fixtures for test case names EndpointProviderTestSpec was changed to pass each endpoint test case's documentation string to the EndpointProviderTestCase constructor, so generated test cases are named in test output instead of appearing as anonymous entries. The three affected golden files were not updated alongside that change, leaving EndpointRulesClientTestSpecTest failing on the base branch. Adds the expected name argument to the fixtures. Test-resource only — no production code change. * fix(endpoints): Make BDD peephole spec-faithful Addresses review findings on the BDD endpoint codegen peephole pass and the awsPartition memo. Two of the four are silent-wrong-endpoint or new-exception risks, so they are grouped as one verified changeset rather than split across commits that share hunks in the same files. Substring peephole diverged from spec substring semantics. The pass rewrote stringEquals(coalesce(substring(str, X, Y, rev), ""), literal) into inlined startsWith/endsWith/regionMatches, none of which reproduce substring's rejection of input containing a non-ASCII character. A bucket name with a non-ASCII character anywhere would take a different rule branch than the spec dictates. This fires today on the S3 BDD model: a name ending in --x-s3 would be routed to an S3 Express endpoint with sigv4-s3express instead of a regular S3 endpoint with SigV4. Replaced the three emitters with a single RulesFunctions.substringEquals helper that mirrors substring exactly. The positional comparison runs first and fails for almost every input, so the O(n) ASCII scan is only reached once the characters already match, which is where substring paid the same cost. The rewrite is skipped for an empty literal, where coalesce makes the spec comparison true even for null input. stringEquals lost null-safety for two nullable operands. The fallback emitted left.equals(right) whenever neither side was a string constant, throwing NullPointerException where the spec's stringEquals returns false. Reachable in the S3 BDD via stringEquals(region, bucketArn.region()) and two others. Removed the fallback; the pass now leaves plain comparisons to PrepareForCodegenVisitor, which already handles the constant cases identically and correctly keeps the null-safe RulesFunctions.stringEquals call otherwise. awsPartition ThreadLocal memo replaced with precomputed instances. RulesFunctions is copied into every service, so a static ThreadLocal holding a service-loaded RulePartition is the container classloader-leak pattern Tomcat warns about, and allocates per virtual thread with a near-zero hit rate. Benchmarking a volatile single-slot alternative showed it is worse than no cache at all once concurrent threads resolve different regions (122 ns/op vs 3.0 ns/op at 4 threads, from cache-line contention on the write). RulePartition is immutable and a pure function of its Partition, of which there are a handful loaded once, so the instances are now built up front in loadPartitionData and shared. That removes the allocation the memo existed to avoid rather than caching around it: 1.65-2.52 ns/op against a 2.93-3.50 ns/op baseline, flat across region count and thread count, with no per-call state. The now-redundant AWS_PARTITION LazyValue is gone. Removed dead peephole emitters from BddResultCodeGeneratorVisitor. All six were copy-pasted from ConditionFnCodeGeneratorVisitor but are unreachable: the BDD hoists computation into conditions, so results only consume assigned registers, and the visitor already rejects conditions and let-bindings outright. Dropping the pass from the result path leaves generated output byte-identical for all three BDD test models. Verified: 698 codegen tests, 393 S3 and 574 DynamoDB generated endpoint conformance tests, and a 1670-case differential test asserting substringEquals is indistinguishable from the spec composition across Latin-1, CJK and surrogate-pair inputs. Checkstyle and SpotBugs clean. * fix(endpoints): Address BDD peephole review findings Applies seven findings from the PR #7304 review. The two correctness ones are the same class as the bugs the previous commit fixed: an invariant asserted in a comment rather than enforced in code. isAlwaysNonNull elided a register null-check on an unenforced assumption (F1). It returned true for any __ite by name, while the comment claimed "a ternary between two string literals". simplifyIte only checks arity, RuleRuntimeTypeMirror types both branches as STRING, and the BDD model is produced by the endpoint compiler rather than by this repo, so a {"ref": ...} branch is legal input and can be null. For ite(cond, ref, "") the assign condition reported success with a null register, flipping a BDD edge and resolving an endpoint the spec does not permit. Both branches are now checked. The three literal-branch ite nodes in the S3 BDD keep the elided check, so the optimization is retained where it is provable. emitCoalesceBoolean evaluated its operand twice (F3). It emitted (expr != null ? expr : default), so a non-trivial operand such as a nested rules function ran twice per evaluation - a pessimization inside a performance change. Replaced with wrapper equality, which is exactly equivalent for both defaults, evaluates the subject once, and has no branch and no boxing: coalesce(x, false) becomes Boolean.TRUE.equals(x) and coalesce(x, true) becomes !Boolean.FALSE.equals(x). Removed the unreachable STRING_CONCAT receiver handling (F5). All three MethodCallExpression producers are in PrepareForCodegenVisitor and put either a string constant or a boolean-typed expression on the receiver, never a concatenation. It was needed by the left.equals(right) fallback removed in the previous commit. With it and a stray blank line gone, BddResultCodeGeneratorVisitor is byte-identical to the base branch and drops out of the PR diff entirely. awsPartition returned null where it previously threw (F6). Generated code null-checks the result, so an unloadable 'aws' partition silently made the condition false and took a different rule branch instead of failing. loadPartitionData now validates it and throws SdkClientException naming partitions.json. Restructuring for that also lets PartitionData hold all final fields, removing the non-final awsRulePartition that the previous commit had exposed to LazyValue's unsafe publication. Deduplicated host-label validation (F4). isValidHostLabelMulti was a character-for-character copy of the allowDots branch in isValidHostLabel, in a template copied into every service module, so the two could drift independently. Inverted the delegation: isValidHostLabel now dispatches to the two specialized helpers and there is one copy of each algorithm. Also dropped the redundant length guard in isValidHostLabelSingle, which isValidSingleLabel already applies. Moved RulesFunctionsSubstringEqualsTest to codegen-generated-classes-test (F8). RulesFunctions is a codegen template shared by every service, not a class S3 owns, so per docs/guidelines/testing-guidelines.md it belongs with "generated SDK common functionalities". It now sits beside EndpointUrlConformanceTest and RuleUrlTest, which cover other generated classes from the same template set, and gained coverage of the F4 delegation. Test names follow the methodToTest_when_expectedBehavior convention and the placement rationale is recorded in the class javadoc. Added the missing regression guards (F2). Three of the four rewrites had no assertions: only the substring peephole and the stringEquals null-safety decision were covered. The simple-BDD golden fixture contains none of these shapes and the S3 BDD was asserted only as non-null, so a change to the emitters or to PrepareForCodegenVisitor could silently restore the RulesFunctions dispatches with a green build. New ConditionFnCodeGeneratorVisitorTest covers the null-check elision decision and the single-evaluation coalesce form directly; BddEndpointProviderSpecTest now asserts the ite, coalesce and isValidHostLabel forms in the generated S3 BDD and that no dispatch remains. Confirmed the F1 tests are genuine guards: reverting isAlwaysNonNull to the name-only check fails two of them. Not changed, with reasons: the synthetic-name constants stay split across BddPeepholeVisitor and RuleRuntimeTypeMirror (F7), because moving them onto the visitor would make the rules2 package depend on rules2.bdd, which already depends on it, and because Checkstyle's DeclarationOrder forces SUBSTRING_EQUALS_FN to its current position. The fixture commit stays on this branch (F9) since relocating it means rewriting pushed history; it is called out in the PR description instead. Verified: 705 codegen tests; the 32 generated bddendpoints conformance tests, which exercise the BDD provider behaviourally and had not previously been run; 1671 substringEquals differential cases plus 65 sibling generated-class tests; 393 S3 and 574 DynamoDB endpoint conformance tests. rules2 golden fixtures remain byte-identical to feature/master/endpoints-bdd. Checkstyle and SpotBugs clean. * test(codegen): Add golden file for the S3 BDD provider The S3 BDD is the only test model that exercises all four peephole rewrites, the dynamic auth scheme name and the ite assign conditions, and it was covered only by assertThat(poetSpec()).isNotNull() plus targeted string assertions. Golden files are the established pattern for rules2 providers and make the effect of a codegen change reviewable as a diff, so the S3 BDD now has one. Replaces the isNotNull smoke test with generatesTo. Verified that codegen output is deterministic across separate JVM runs before adopting it, since a fixture is otherwise flaky, and that reverting an emitter makes the fixture test fail. Keeps the correctness invariants as explicit negative assertions rather than folding them into the fixture, consolidated into s3Bdd_neverEmitsSpecViolatingForms. Regenerating a golden file is a one-command operation and doing it without reading the diff is how a defect gets blessed into a fixture - this repo already has an instance of that. The assertions name what must never be emitted and why, so restoring a faster-but-spec-violating form means deleting a stated invariant rather than accepting a regenerated file. Confirmed both layers fail independently when an emitter regresses. Dropped the positive assertions the fixture now subsumes. literalBranchIteAssigns_keepElidedNullCheck stays, because it and ConditionFnCodeGeneratorVisitorTest only make sense as a pair. Fixture is 4102 lines, in the same raw JavaPoet form as the sibling endpoint-provider-bdd-class.java. PoetMatchers formats both sides and compares ignoring whitespace, so the fixture's own indentation is not significant. Verified: 703 codegen tests, Checkstyle and SpotBugs clean. Test-only change; no generated production code is affected. --- .../poet/rules2/RuleRuntimeTypeMirror.java | 21 + .../rules2/bdd/BddEndpointProviderSpec.java | 14 +- .../poet/rules2/bdd/BddPeepholeVisitor.java | 251 + .../bdd/ConditionFnCodeGeneratorVisitor.java | 115 +- .../rules2/RulesFunctions.java.resource | 215 +- .../bdd/BddEndpointProviderSpecTest.java | 72 +- .../rules2/bdd/BddPeepholeVisitorTest.java | 164 + .../ConditionFnCodeGeneratorVisitorTest.java | 164 + ...dpoint-rules-metric-values-test-class.java | 20 +- ...endpoint-rules-stringarray-test-class.java | 8 +- ...int-rules-unknown-property-test-class.java | 20 +- .../bdd/endpoint-provider-bdd-s3-class.java | 4102 +++++++++++++++++ .../RulesFunctionsSubstringEqualsTest.java | 232 + 13 files changed, 5306 insertions(+), 92 deletions(-) create mode 100644 codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/bdd/BddPeepholeVisitor.java create mode 100644 codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules2/bdd/BddPeepholeVisitorTest.java create mode 100644 codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules2/bdd/ConditionFnCodeGeneratorVisitorTest.java create mode 100644 codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/bdd/endpoint-provider-bdd-s3-class.java create mode 100644 test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/compiledendpointrules/endpoints/internal/RulesFunctionsSubstringEqualsTest.java diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/RuleRuntimeTypeMirror.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/RuleRuntimeTypeMirror.java index 6e101fb0cd2f..c0679cfc5f56 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/RuleRuntimeTypeMirror.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/RuleRuntimeTypeMirror.java @@ -49,6 +49,13 @@ public final class RuleRuntimeTypeMirror { .addTypeParam(ClassName.get(String.class)) .ruleTypeParam(STRING) .build(); + + /** + * Registry name of the synthetic {@code substringEquals} function emitted by the BDD peephole + * pass. Prefixed with {@code __} so it cannot collide with a rules standard library function. + */ + public static final String SUBSTRING_EQUALS_FN = "__substringEquals"; + private static final String URL_TYPE_NAME = "Url"; private static final String PARTITION_TYPE_NAME = "Partition"; private static final String ARN_TYPE_NAME = "Arn"; @@ -133,6 +140,20 @@ static List builtInFunctions(Map types) { .addArgument("reverse", BOOLEAN) .containingType(containingType) .build(), + // Synthetic. Not a rules standard library function; BddPeepholeVisitor rewrites + // stringEquals(coalesce(substring(...), ""), literal) into this to avoid allocating the + // intermediate substring. The "__" prefix cannot collide with a spec function name. + RuleFunctionMirror + .builder(SUBSTRING_EQUALS_FN) + .javaName("substringEquals") + .returns(BOOLEAN) + .addArgument("value", STRING) + .addArgument("startIdx", INTEGER) + .addArgument("stopIdx", INTEGER) + .addArgument("reverse", BOOLEAN) + .addArgument("literal", STRING) + .containingType(containingType) + .build(), RuleFunctionMirror .builder("stringEquals") .returns(BOOLEAN) diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/bdd/BddEndpointProviderSpec.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/bdd/BddEndpointProviderSpec.java index 0f5c3c3fac47..3fb181eaa637 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/bdd/BddEndpointProviderSpec.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/bdd/BddEndpointProviderSpec.java @@ -64,9 +64,8 @@ * as ternary expressions. Complex conditions that compute and store values are emitted as separate * {@code cond()} methods. * - *

The evaluator is allocated per call (lightweight — just register fields, no maps). - * Function-level caches (awsPartition, uriEncode, isVirtualHostable) live on a ThreadLocal - * in the shared RulesFunctions class and benefit all code paths. + *

The evaluator is allocated per call (lightweight — just register fields, no maps). The shared + * RulesFunctions helpers it calls are stateless. */ public class BddEndpointProviderSpec implements ClassSpec { /** @@ -337,6 +336,7 @@ private List conditionMethods() { synthetic.setConditions(Collections.singletonList(c)); RuleExpression parsedSynthetic = ExpressionParser .parseRuleSetExpression(synthetic) + .accept(new BddPeepholeVisitor()) .accept(new PrepareForCodegenVisitor()); parsedSynthetic.accept(new ConditionFnCodeGeneratorVisitor(codeBuilder, typeMirror, registerInfoMap, endpointRulesSpecUtils)); @@ -356,6 +356,12 @@ private List resultMethods() { List methods = new ArrayList<>(); for (int rI = 0; rI < endpointBddModel.getResults().size(); rI++) { CodeBlock.Builder codeBuilder = CodeBlock.builder(); + // BddPeepholeVisitor is deliberately not applied here. It rewrites condition-shaped + // expressions (stringEquals, coalesce-with-boolean-default, ite, isValidHostLabel), and a + // BDD result cannot contain any: the BDD hoists all computation into conditions, so + // results only consume already-assigned registers. BddResultCodeGeneratorVisitor enforces + // that by rejecting conditions and let-bindings outright. Applying the peephole here + // produced identical output while requiring a duplicate copy of every emitter. RuleExpression parsedSynthetic = ExpressionParser .parseRuleSetExpression(endpointBddModel.getResults().get(rI)) .accept(new PrepareForCodegenVisitor()); @@ -383,8 +389,6 @@ private MethodSpec resolveEndpointMethod() { builder.beginControlFlow("try"); // Allocate evaluator per call — lightweight (just fields, no maps), immediately young-gen collected. - // Function caches (awsPartition, uriEncode, isVirtualHostable) live on RulesFunctions' ThreadLocal - // and are shared across calls regardless. builder.addStatement("$T evaluator = new $T()", evaluatorType, evaluatorType); // Initialize evaluator from params diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/bdd/BddPeepholeVisitor.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/bdd/BddPeepholeVisitor.java new file mode 100644 index 000000000000..2eefb5d676f3 --- /dev/null +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/bdd/BddPeepholeVisitor.java @@ -0,0 +1,251 @@ +/* + * 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.rules2.bdd; + +import java.util.List; +import software.amazon.awssdk.codegen.poet.rules2.FunctionCallExpression; +import software.amazon.awssdk.codegen.poet.rules2.LiteralBooleanExpression; +import software.amazon.awssdk.codegen.poet.rules2.LiteralIntegerExpression; +import software.amazon.awssdk.codegen.poet.rules2.LiteralStringExpression; +import software.amazon.awssdk.codegen.poet.rules2.RewriteRuleExpressionVisitor; +import software.amazon.awssdk.codegen.poet.rules2.RuleExpression; +import software.amazon.awssdk.codegen.poet.rules2.RuleRuntimeTypeMirror; + +/** + * BDD-only peephole optimization pass. Rewrites endpoint rule expressions into synthetic function + * calls that the BDD code generators emit as allocation-free native Java. + * + *

This runs before {@link software.amazon.awssdk.codegen.poet.rules2.PrepareForCodegenVisitor} on + * the BDD path only, so the tree-based rules code generation is unaffected. + * + *

Optimizations applied: + *

    + *
  • {@code stringEquals(coalesce(substring(str, X, Y, reverse), ""), literal)} → + * {@code RulesFunctions.substringEquals(str, X, Y, reverse, literal)}, which compares in + * place instead of allocating the substring and the coalesce varargs array. The helper + * reproduces {@code substring}'s semantics exactly, including its rejection of non-ASCII + * input, so the rewrite cannot change which branch a rule takes.
  • + *
  • {@code coalesce(boolExpr, boolLiteral)} → {@code __coalesceBoolean(expr, default)}
  • + *
  • {@code ite(cond, ifTrue, ifFalse)} → {@code __ite(cond, ifTrue, ifFalse)}
  • + *
  • {@code isValidHostLabel(str, boolLiteral)} → {@code __isValidHostLabel(str, allowDots)}
  • + *
+ * + *

Synthetic function names are prefixed with {@code __} so they cannot collide with endpoint + * rule standard library function names. Those in {@link #isCustomEmitted(String)} are emitted as + * inline Java by the code generators; {@code __substringEquals} is instead registered as a real + * function in {@link RuleRuntimeTypeMirror} and emitted by the ordinary static-call path. + */ +public final class BddPeepholeVisitor extends RewriteRuleExpressionVisitor { + public static final String ITE = "__ite"; + public static final String COALESCE_BOOL = "__coalesceBoolean"; + public static final String IS_VALID_HOST_LABEL = "__isValidHostLabel"; + + /** + * Returns true for synthetic functions that the BDD code generator emits as inline Java rather + * than as a plain static call. {@link RuleRuntimeTypeMirror#SUBSTRING_EQUALS_FN} is deliberately + * absent: it is registered as a real function and so needs no custom emitter. + */ + public static boolean isCustomEmitted(String functionName) { + return ITE.equals(functionName) + || COALESCE_BOOL.equals(functionName) + || IS_VALID_HOST_LABEL.equals(functionName); + } + + @Override + public RuleExpression visitFunctionCallExpression(FunctionCallExpression e) { + e = (FunctionCallExpression) super.visitFunctionCallExpression(e); + switch (e.name()) { + case "stringEquals": + return simplifyStringEquals(e); + case "coalesce": + return simplifyCoalesce(e); + case "ite": + return simplifyIte(e); + case "isValidHostLabel": + return simplifyIsValidHostLabel(e); + default: + return e; + } + } + + /** + * Rewrites {@code stringEquals} into a {@code substringEquals} call when either side is a + * {@code coalesce(substring(..), "")}. Any other {@code stringEquals} is returned unchanged for + * {@link software.amazon.awssdk.codegen.poet.rules2.PrepareForCodegenVisitor} to handle. + */ + private RuleExpression simplifyStringEquals(FunctionCallExpression e) { + List args = e.arguments(); + if (args.size() != 2) { + return e; + } + RuleExpression left = args.get(0); + RuleExpression right = args.get(1); + + RuleExpression peephole = trySubstringPeephole(left, right); + if (peephole != null) { + return peephole; + } + peephole = trySubstringPeephole(right, left); + if (peephole != null) { + return peephole; + } + + // Leave the plain comparison alone. PrepareForCodegenVisitor runs immediately after and + // rewrites it to constant.equals(other) only when one side is a string constant, keeping the + // null-safe RulesFunctions.stringEquals call otherwise. Rewriting it here would emit + // left.equals(right) even when both sides are nullable at runtime, which turns the + // spec-mandated false into a NullPointerException. + return e; + } + + /** + * Matches {@code coalesce(substring(str, start, stop, reverse), "")} compared against a string + * literal, returning the corresponding synthetic expression, or null when the pattern does not + * apply. + */ + private RuleExpression trySubstringPeephole(RuleExpression coalesceCandidate, RuleExpression literalCandidate) { + if (literalCandidate.kind() != RuleExpression.RuleExpressionKind.STRING_VALUE) { + return null; + } + String literal = ((LiteralStringExpression) literalCandidate).value(); + + FunctionCallExpression coalesceExpr = extractCoalesceWithEmptyDefault(coalesceCandidate); + if (coalesceExpr == null) { + return null; + } + + RuleExpression substringCandidate = coalesceExpr.arguments().get(0); + if (!(substringCandidate instanceof FunctionCallExpression)) { + return null; + } + FunctionCallExpression substringExpr = (FunctionCallExpression) substringCandidate; + if (!"substring".equals(substringExpr.name())) { + return null; + } + + List subArgs = substringExpr.arguments(); + if (subArgs.size() != 4) { + return null; + } + if (!(subArgs.get(1) instanceof LiteralIntegerExpression) + || !(subArgs.get(2) instanceof LiteralIntegerExpression) + || !(subArgs.get(3) instanceof LiteralBooleanExpression)) { + return null; + } + + RuleExpression strExpr = subArgs.get(0); + int startIdx = ((LiteralIntegerExpression) subArgs.get(1)).value(); + int stopIdx = ((LiteralIntegerExpression) subArgs.get(2)).value(); + boolean reverse = ((LiteralBooleanExpression) subArgs.get(3)).value(); + + // The comparison can only ever be true when the lengths line up. + if (literal.length() != stopIdx - startIdx) { + return null; + } + + // Degenerate case: startIdx == stopIdx makes the spec's substring return null, so the + // coalesce yields "" and the comparison against an empty literal is true even for a null + // input. substringEquals cannot express that, so leave it to the general path. + if (literal.isEmpty()) { + return null; + } + + return FunctionCallExpression.builder() + .name(RuleRuntimeTypeMirror.SUBSTRING_EQUALS_FN) + .type(RuleRuntimeTypeMirror.BOOLEAN) + .addArgument(strExpr) + .addArgument(new LiteralIntegerExpression(startIdx)) + .addArgument(new LiteralIntegerExpression(stopIdx)) + .addArgument(new LiteralBooleanExpression(reverse)) + .addArgument(new LiteralStringExpression(literal)) + .build(); + } + + /** + * Returns the {@code coalesce(expr, "")} call when the candidate matches that shape, else null. + */ + private FunctionCallExpression extractCoalesceWithEmptyDefault(RuleExpression candidate) { + if (!(candidate instanceof FunctionCallExpression)) { + return null; + } + FunctionCallExpression fn = (FunctionCallExpression) candidate; + if (!"coalesce".equals(fn.name())) { + return null; + } + List args = fn.arguments(); + if (args.size() != 2) { + return null; + } + RuleExpression defaultArg = args.get(1); + if (defaultArg.kind() != RuleExpression.RuleExpressionKind.STRING_VALUE) { + return null; + } + if (!"".equals(((LiteralStringExpression) defaultArg).value())) { + return null; + } + return fn; + } + + /** + * Rewrites {@code coalesce(boolExpr, boolLiteral)} so the generator can emit + * {@code expr != null ? expr : default} instead of a varargs call that boxes its arguments. + */ + private RuleExpression simplifyCoalesce(FunctionCallExpression e) { + List args = e.arguments(); + if (args.size() != 2 || args.get(1).kind() != RuleExpression.RuleExpressionKind.BOOLEAN_VALUE) { + return e; + } + return booleanFunction(COALESCE_BOOL, args.get(0), args.get(1)); + } + + /** + * Rewrites {@code ite(condition, ifTrue, ifFalse)} so the generator can emit a native ternary. + */ + private RuleExpression simplifyIte(FunctionCallExpression e) { + List args = e.arguments(); + if (args.size() != 3) { + return e; + } + return FunctionCallExpression.builder() + .name(ITE) + .type(RuleRuntimeTypeMirror.STRING) + .addArgument(args.get(0)) + .addArgument(args.get(1)) + .addArgument(args.get(2)) + .build(); + } + + /** + * Rewrites {@code isValidHostLabel(str, allowDots)} when {@code allowDots} is a compile-time + * constant, letting the generator call the specialized runtime helper that skips the branch. + */ + private RuleExpression simplifyIsValidHostLabel(FunctionCallExpression e) { + List args = e.arguments(); + if (args.size() != 2 || !(args.get(1) instanceof LiteralBooleanExpression)) { + return e; + } + return booleanFunction(IS_VALID_HOST_LABEL, args.get(0), args.get(1)); + } + + private RuleExpression booleanFunction(String name, RuleExpression arg0, RuleExpression arg1) { + return FunctionCallExpression.builder() + .name(name) + .type(RuleRuntimeTypeMirror.BOOLEAN) + .addArgument(arg0) + .addArgument(arg1) + .build(); + } +} diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/bdd/ConditionFnCodeGeneratorVisitor.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/bdd/ConditionFnCodeGeneratorVisitor.java index f0557bc7b89e..bf78c6d67e8b 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/bdd/ConditionFnCodeGeneratorVisitor.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/bdd/ConditionFnCodeGeneratorVisitor.java @@ -120,6 +120,13 @@ public RuleType visitFunctionCallExpression(FunctionCallExpression e) { builder.add(" == null"); return RuleRuntimeTypeMirror.BOOLEAN; } + + // Synthetic functions that are emitted as inline Java rather than a static call. + // __substringEquals is not among them: it is a registered function and falls through below. + if (BddPeepholeVisitor.isCustomEmitted(fn)) { + return emitPeepholeOptimized(fn, e.arguments()); + } + RuleFunctionMirror func = typeMirror.resolveFunction(e.name()); builder.add("$T.$L(", func.containingType().type(), func.javaName()); List args = e.arguments(); @@ -138,6 +145,83 @@ public RuleType visitFunctionCallExpression(FunctionCallExpression e) { return func.returns(); } + /** + * Emits inline Java for synthetic function calls that have no static-call equivalent, avoiding + * the boxing and varargs allocation of the corresponding RulesFunctions calls. + */ + private RuleType emitPeepholeOptimized(String fn, List args) { + switch (fn) { + case BddPeepholeVisitor.ITE: + // __ite(cond, ifTrue, ifFalse) → (cond ? ifTrue : ifFalse) + return emitIte(args); + case BddPeepholeVisitor.COALESCE_BOOL: + // __coalesceBoolean(expr, default) → (expr != null ? expr : default) + return emitCoalesceBoolean(args); + case BddPeepholeVisitor.IS_VALID_HOST_LABEL: + // __isValidHostLabel(str, allowDots) → inline validation + return emitIsValidHostLabel(args); + default: + throw new IllegalStateException("Unknown peephole function: " + fn); + } + } + + /** + * Emits: {@code (cond ? ifTrue : ifFalse)} + */ + private RuleType emitIte(List args) { + builder.add("("); + args.get(0).accept(this); + builder.add(" ? "); + args.get(1).accept(this); + builder.add(" : "); + args.get(2).accept(this); + builder.add(")"); + return RuleRuntimeTypeMirror.STRING; + } + + /** + * Emits a single-evaluation equivalent of {@code coalesce(expr, defaultValue)} for a boolean + * default. Wrapper equality gives exact coalesce semantics with no branch and no boxing: + * + * + * + * + * + * + * + * + *
Rewrite table
rewriteemittednullTRUEFALSE
{@code coalesce(x, false)}{@code Boolean.TRUE.equals(x)}falsetruefalse
{@code coalesce(x, true)}{@code !Boolean.FALSE.equals(x)}truetruefalse
+ * + *

The subject is emitted once. A ternary would emit it twice, which runs any non-trivial + * operand (a nested rules function, for instance) twice per evaluation. + */ + private RuleType emitCoalesceBoolean(List args) { + boolean defaultValue = ((LiteralBooleanExpression) args.get(1)).value(); + if (defaultValue) { + builder.add("!Boolean.FALSE.equals("); + } else { + builder.add("Boolean.TRUE.equals("); + } + args.get(0).accept(this); + builder.add(")"); + return RuleRuntimeTypeMirror.BOOLEAN; + } + + /** + * Emits: {@code RulesFunctions.isValidHostLabelSingle(str)} or + * {@code RulesFunctions.isValidHostLabelMulti(str)} depending on the allowDots constant. + * Avoids the boolean parameter dispatch branch at runtime. + */ + private RuleType emitIsValidHostLabel(List args) { + boolean allowDots = ((LiteralBooleanExpression) args.get(1)).value(); + RuleFunctionMirror func = typeMirror.resolveFunction("isValidHostLabel"); + builder.add("$T.$L(", func.containingType().type(), + allowDots ? "isValidHostLabelMulti" : "isValidHostLabelSingle"); + args.get(0).accept(this); + builder.add(")"); + return RuleRuntimeTypeMirror.BOOLEAN; + } + @Override public RuleType visitMethodCallExpression(MethodCallExpression e) { e.source().accept(this); @@ -211,11 +295,40 @@ public RuleType visitLetExpression(LetExpression e) { builder.add("$L = ", registerName); v.accept(this); builder.addStatement(""); // end the statement we started - builder.addStatement("return $L != null", registerName); + // Assign conditions succeed only when the assigned value is non-null. Skip the check + // only where the value is provably non-null, otherwise emit it. + if (isAlwaysNonNull(v)) { + builder.addStatement("return true"); + } else { + builder.addStatement("return $L != null", registerName); + } } return RuleRuntimeTypeMirror.BOOLEAN; } + /** + * Returns true only if the expression is provably non-null at runtime. + * + *

Recognizes {@code __ite} whose two branches are both string literals, which + * {@link #emitIte} emits as a ternary between them. {@code BddPeepholeVisitor.simplifyIte} does + * not constrain the branches, and {@code RuleRuntimeTypeMirror} types them as {@code STRING}, so + * a {@code {"ref": ...}} branch is legal input from the endpoint compiler and can be null. Both + * branches are therefore checked here rather than assumed: eliding the register null-check for a + * nullable branch would report an assign condition as satisfied with a null register, flipping a + * BDD edge and resolving an endpoint the spec does not permit. + */ + private static boolean isAlwaysNonNull(RuleExpression expr) { + if (!(expr instanceof FunctionCallExpression)) { + return false; + } + FunctionCallExpression fn = (FunctionCallExpression) expr; + if (!BddPeepholeVisitor.ITE.equals(fn.name()) || fn.arguments().size() != 3) { + return false; + } + return fn.arguments().get(1).kind() == RuleExpression.RuleExpressionKind.STRING_VALUE + && fn.arguments().get(2).kind() == RuleExpression.RuleExpressionKind.STRING_VALUE; + } + @Override public RuleType visitRuleSetExpression(RuleSetExpression e) { // generate the conditions - there may be multiple assigns (LET) diff --git a/codegen/src/main/resources/software/amazon/awssdk/codegen/rules2/RulesFunctions.java.resource b/codegen/src/main/resources/software/amazon/awssdk/codegen/rules2/RulesFunctions.java.resource index a4a08c124f24..69b3d79dacbb 100644 --- a/codegen/src/main/resources/software/amazon/awssdk/codegen/rules2/RulesFunctions.java.resource +++ b/codegen/src/main/resources/software/amazon/awssdk/codegen/rules2/RulesFunctions.java.resource @@ -18,9 +18,6 @@ public class RulesFunctions { private static final LazyValue PARTITION_DATA = LazyValue. builder() .initializer(RulesFunctions::loadPartitionData).build(); - private static final LazyValue AWS_PARTITION = LazyValue. builder() - .initializer(RulesFunctions::findAwsPartition).build(); - private static final int MAX_HOST_LABEL_SIZE = 63; private static final int MIN_BUCKET_SIZE = 3; @@ -92,11 +89,8 @@ public class RulesFunctions { return null; } - for (int i = 0; i < len; i++) { - // non-ascii characters (values outside of the 7bit ASCII range) - if (value.charAt(i) > 127) { - return null; - } + if (!isAsciiOnly(value)) { + return null; } if (reverse) { @@ -108,6 +102,52 @@ public class RulesFunctions { } } + /** + * Equivalent to {@code stringEquals(coalesce(substring(value, startIndex, stopIndex, reverse), ""), literal)} + * without allocating the intermediate substring. Called by optimized codegen. + * + *

This mirrors {@link #substring} exactly, including its rejection of any input containing a + * character outside the 7-bit ASCII range. The positional comparison is done first because it + * fails for almost every input; the O(n) ASCII scan is only reached once the characters already + * match, which is precisely where {@link #substring} would have paid the same cost. + * + *

Only equivalent for a non-empty {@code literal}. When {@code startIndex == stopIndex} the + * spec's {@code substring} returns null and the coalesce makes the comparison against {@code ""} + * true even for a null input; codegen does not rewrite that case into this method. + */ + public static boolean substringEquals(String value, int startIndex, int stopIndex, boolean reverse, String literal) { + if (value == null) { + return false; + } + + int len = value.length(); + if (startIndex >= stopIndex || len < stopIndex) { + return false; + } + + int subLen = stopIndex - startIndex; + if (literal.length() != subLen) { + return false; + } + + int offset = reverse ? len - stopIndex : startIndex; + if (!value.regionMatches(offset, literal, 0, subLen)) { + return false; + } + + return isAsciiOnly(value); + } + + private static boolean isAsciiOnly(String value) { + for (int i = 0; i < value.length(); i++) { + // non-ascii characters (values outside of the 7bit ASCII range) + if (value.charAt(i) > 127) { + return false; + } + } + return true; + } + // URI related functions public static String uriEncode(String uri) { try { @@ -130,31 +170,7 @@ public class RulesFunctions { } public static boolean isValidHostLabel(String hostLabel, boolean allowDots) { - int len = hostLabel == null ? 0 : hostLabel.length(); - if (len == 0) { - return false; - } - - // Single-label mode - if (!allowDots) { - return isValidSingleLabel(hostLabel, 0, len); - } - - // Multi-label mode - int start = 0; - for (int i = 0; i <= len; i++) { - if (i == len || hostLabel.charAt(i) == '.') { - // chunk is hostLabel[start..i) - int chunkLen = i - start; - if (chunkLen < 1 || chunkLen > MAX_HOST_LABEL_SIZE) { - return false; - } else if (!isValidSingleLabel(hostLabel, start, i)) { - return false; - } - start = i + 1; - } - } - return true; + return allowDots ? isValidHostLabelMulti(hostLabel) : isValidHostLabelSingle(hostLabel); } // Validates a single label in s[start..end): ^[A-Za-z0-9][A-Za-z0-9\-]{0,62}$ @@ -184,34 +200,78 @@ public class RulesFunctions { return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); } + /** + * Validates a single DNS label (no dots allowed), the {@code allowDots == false} case of + * {@link #isValidHostLabel}. Called directly by optimized codegen when {@code allowDots} is a + * compile-time constant, which skips the dispatch. + */ + public static boolean isValidHostLabelSingle(String hostLabel) { + int len = hostLabel == null ? 0 : hostLabel.length(); + if (len == 0) { + return false; + } + return isValidSingleLabel(hostLabel, 0, len); + } + + /** + * Validates a multi-label DNS name (dots allowed), the {@code allowDots == true} case of + * {@link #isValidHostLabel}. Called directly by optimized codegen when {@code allowDots} is a + * compile-time constant, which skips the dispatch. + */ + public static boolean isValidHostLabelMulti(String hostLabel) { + int len = hostLabel == null ? 0 : hostLabel.length(); + if (len == 0) { + return false; + } + int start = 0; + for (int i = 0; i <= len; i++) { + if (i == len || hostLabel.charAt(i) == '.') { + int chunkLen = i - start; + if (chunkLen < 1 || chunkLen > MAX_HOST_LABEL_SIZE) { + return false; + } else if (!isValidSingleLabel(hostLabel, start, i)) { + return false; + } + start = i + 1; + } + } + return true; + } + private static boolean isLowerCaseAlphanumeric(char c) { return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'); } // AWS related functions + /** + * Resolves the partition a region belongs to. + * + *

{@link RulePartition} is immutable and is a pure function of the matched {@link Partition}, + * of which there are only a handful, loaded once. The instances are therefore built up front in + * {@link #loadPartitionData()} and shared, so this method allocates nothing and holds no + * per-call state. + */ public static RulePartition awsPartition(String regionName) { PartitionData data = PARTITION_DATA.value(); - Partition matchedPartition; // Known region - matchedPartition = data.regionMap.get(regionName); - if (matchedPartition == null) { - // try matching on region name pattern - for (Partition p : data.partitions) { - if (p.regionMatches(regionName)) { - matchedPartition = p; - break; - } - } + RulePartition matched = data.regionMap.get(regionName); + if (matched != null) { + return matched; } - // Couldn't find the region by name or pattern matching. Fallback to 'aws' partition. - if (matchedPartition == null) { - matchedPartition = AWS_PARTITION.value(); + // Try matching on region name pattern + List entries = data.partitions; + for (int i = 0; i < entries.size(); i++) { + PartitionEntry entry = entries.get(i); + if (entry.partition.regionMatches(regionName)) { + return entry.rulePartition; + } } - return RulePartition.from(matchedPartition.id(), matchedPartition.outputs()); + // Couldn't find the region by name or pattern matching. Fallback to 'aws' partition. + return data.awsRulePartition; } public static RuleArn awsParseArn(String value) { @@ -350,25 +410,60 @@ public class RulesFunctions { // TODO: support custom partitions.json Partitions partitions = provider.loadPartitions(); - PartitionData partitionData = new PartitionData(); - partitions.partitions().forEach(part -> { - partitionData.partitions.add(part); - part.regions().forEach((name, override) -> { - partitionData.regionMap.put(name, part); - }); - }); + List entries = new ArrayList<>(); + Map regionMap = new HashMap<>(); + RulePartition awsRulePartition = null; + + for (Partition part : partitions.partitions()) { + RulePartition rulePartition = RulePartition.from(part.id(), part.outputs()); + entries.add(new PartitionEntry(part, rulePartition)); + for (String regionName : part.regions().keySet()) { + regionMap.put(regionName, rulePartition); + } + if ("aws".equalsIgnoreCase(part.id())) { + awsRulePartition = rulePartition; + } + } - return partitionData; + // awsPartition falls back to the 'aws' partition for any region it cannot match. Fail loudly + // here rather than returning null from awsPartition, which generated code null-checks and + // would silently treat as "condition not satisfied", taking a different rule branch. + if (awsRulePartition == null) { + throw SdkClientException.create( + "Endpoint partition metadata is missing the 'aws' partition, which is required as the " + + "fallback for unmatched regions. This indicates a corrupt or incomplete partitions.json."); + } + + return new PartitionData(entries, regionMap, awsRulePartition); } - private static Partition findAwsPartition() { - return PARTITION_DATA.value().partitions.stream().filter(p -> p.id().equalsIgnoreCase("aws")).findFirst().orElse(null); + private static final class PartitionData { + private final List partitions; + private final Map regionMap; + // Fallback used when a region matches neither a known region name nor a partition pattern. + private final RulePartition awsRulePartition; + + private PartitionData(List partitions, Map regionMap, + RulePartition awsRulePartition) { + this.partitions = partitions; + this.regionMap = regionMap; + this.awsRulePartition = awsRulePartition; + } } - private static class PartitionData { - private final List partitions = new ArrayList<>(); - private final Map regionMap = new HashMap<>(); + /** + * Pairs a {@link Partition} with its precomputed {@link RulePartition}. The former is needed to + * evaluate the region pattern, the latter is what callers get back. + */ + private static final class PartitionEntry { + private final Partition partition; + private final RulePartition rulePartition; + + private PartitionEntry(Partition partition, RulePartition rulePartition) { + this.partition = partition; + this.rulePartition = rulePartition; + } } private static final class LazyValue { diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules2/bdd/BddEndpointProviderSpecTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules2/bdd/BddEndpointProviderSpecTest.java index a6a51746bdc7..32e819a3f791 100644 --- a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules2/bdd/BddEndpointProviderSpecTest.java +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules2/bdd/BddEndpointProviderSpecTest.java @@ -32,11 +32,79 @@ void endpointProviderClass_simpleBdd_generatesExpectedCode() { MatcherAssert.assertThat(spec, generatesTo("endpoint-provider-bdd-class.java")); } + /** + * The S3 BDD is the only model that exercises all four peephole rewrites, the dynamic auth scheme + * name, and the {@code ite} assign conditions. The simple-BDD fixture above contains none of + * those shapes, so this golden file is what makes the pass's effect on generated code reviewable + * and pins it against unintended change. + */ + @Test + void endpointProviderClass_s3Bdd_generatesExpectedCode() { + BddEndpointProviderSpec spec = new BddEndpointProviderSpec( + ClientTestModels.queryServiceModelsWithBddEndpoints()); + MatcherAssert.assertThat(spec, generatesTo("endpoint-provider-bdd-s3-class.java")); + } + + /** + * Correctness invariants that must hold no matter what the golden file above happens to contain. + * + *

These are deliberately not left to the fixture. Regenerating a golden file after changing an + * emitter is a one-command operation, and doing it without reading the diff is how a defect gets + * blessed into a fixture - this repo has an instance of exactly that. Every assertion here is + * phrased negatively and carries the reason, so restoring the faster-but-wrong form means + * consciously deleting a stated invariant rather than accepting a regenerated file. + */ @Test - void endpointProviderClass_s3Bdd_generatesWithoutError() { + void s3Bdd_neverEmitsSpecViolatingForms() { BddEndpointProviderSpec spec = new BddEndpointProviderSpec( ClientTestModels.queryServiceModelsWithBddEndpoints()); - assertThat(spec.poetSpec()).isNotNull(); + String generated = spec.poetSpec().toString(); + + // Inlined String comparisons skip substring's rejection of non-ASCII input, which would route + // a bucket like "mybuck\u00e9t--x-s3" to S3 Express with sigv4-s3express instead of to a + // regular S3 endpoint with SigV4. RulesFunctions.substringEquals exists to preserve that. + assertThat(generated) + .as("substring comparisons must go through substringEquals, which checks for non-ASCII input") + .doesNotContain(".startsWith(") + .doesNotContain(".endsWith(") + .doesNotContain(".regionMatches("); + + // The spec's stringEquals returns false for a null operand; .equals throws. Only a comparison + // with a string constant on the receiver is safe to inline. + assertThat(generated) + .as("stringEquals with two nullable operands must stay null-safe") + .doesNotContain("region.equals(bucketArn.region())") + .doesNotContain("bucketArn.partition().equals(") + .doesNotContain("bucketPartition.name().equals("); + + // A ternary would emit the coalesce subject twice, running any non-trivial operand twice. + assertThat(generated) + .as("boolean coalesce must evaluate its subject once") + .doesNotContain("!= null ?"); + + // Each rewrite must actually fire; falling back to the generic dispatch is a silent + // de-optimization that the fixture alone would absorb without comment. + assertThat(generated) + .as("no rewritten shape may fall back to a RulesFunctions dispatch") + .doesNotContain("RulesFunctions.ite(") + .doesNotContain("RulesFunctions.coalesce(") + .doesNotContain("RulesFunctions.isValidHostLabel("); + } + + /** + * The three {@code ite} nodes in the S3 BDD all have string-literal branches, so their assign + * conditions are provably non-null and keep the elided null check. Paired with + * {@code ConditionFnCodeGeneratorVisitorTest}, which pins the nullable-branch case that must emit + * the check. Stated here as an invariant because the two halves only make sense together. + */ + @Test + void literalBranchIteAssigns_keepElidedNullCheck() { + BddEndpointProviderSpec spec = new BddEndpointProviderSpec( + ClientTestModels.queryServiceModelsWithBddEndpoints()); + String generated = spec.poetSpec().toString(); + + assertThat(generated).contains("_s3e_ds = (params.useDualStack() ? \".dualstack\" : \"\");\n return true;"); + assertThat(generated).contains("_s3e_fips = (params.useFips() ? \"-fips\" : \"\");\n return true;"); } /** diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules2/bdd/BddPeepholeVisitorTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules2/bdd/BddPeepholeVisitorTest.java new file mode 100644 index 000000000000..4af415b0d518 --- /dev/null +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules2/bdd/BddPeepholeVisitorTest.java @@ -0,0 +1,164 @@ +/* + * 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.rules2.bdd; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.codegen.poet.rules2.FunctionCallExpression; +import software.amazon.awssdk.codegen.poet.rules2.LiteralBooleanExpression; +import software.amazon.awssdk.codegen.poet.rules2.LiteralIntegerExpression; +import software.amazon.awssdk.codegen.poet.rules2.LiteralStringExpression; +import software.amazon.awssdk.codegen.poet.rules2.RuleExpression; +import software.amazon.awssdk.codegen.poet.rules2.RuleRuntimeTypeMirror; +import software.amazon.awssdk.codegen.poet.rules2.VariableReferenceExpression; + +/** + * Covers the substring peephole matcher in {@link BddPeepholeVisitor}: when it rewrites + * {@code stringEquals(coalesce(substring(..), ""), literal)} into + * {@code RulesFunctions.substringEquals}, and when it must leave the expression alone. + */ +class BddPeepholeVisitorTest { + + @Test + void substringComparisonIsRewrittenToSubstringEquals() { + RuleExpression result = rewrite(substringEqualsExpr(0, 6, true, "--x-s3")); + + assertThat(result).isInstanceOf(FunctionCallExpression.class); + FunctionCallExpression fn = (FunctionCallExpression) result; + assertThat(fn.name()).isEqualTo(RuleRuntimeTypeMirror.SUBSTRING_EQUALS_FN); + assertThat(fn.arguments()).hasSize(5); + assertThat(((LiteralIntegerExpression) fn.arguments().get(1)).value()).isEqualTo(0); + assertThat(((LiteralIntegerExpression) fn.arguments().get(2)).value()).isEqualTo(6); + assertThat(((LiteralBooleanExpression) fn.arguments().get(3)).value()).isTrue(); + assertThat(((LiteralStringExpression) fn.arguments().get(4)).value()).isEqualTo("--x-s3"); + } + + @Test + void forwardInteriorComparisonIsRewritten() { + FunctionCallExpression fn = (FunctionCallExpression) rewrite(substringEqualsExpr(16, 18, false, "--")); + + assertThat(fn.name()).isEqualTo(RuleRuntimeTypeMirror.SUBSTRING_EQUALS_FN); + assertThat(((LiteralIntegerExpression) fn.arguments().get(1)).value()).isEqualTo(16); + assertThat(((LiteralIntegerExpression) fn.arguments().get(2)).value()).isEqualTo(18); + assertThat(((LiteralBooleanExpression) fn.arguments().get(3)).value()).isFalse(); + } + + @Test + void lengthMismatchIsNotRewritten() { + // The window is 6 wide but the literal is 4, so the comparison can never be true. Leave it to + // the general path rather than emitting a call whose arguments contradict each other. + assertThat(isSubstringEqualsCall(rewrite(substringEqualsExpr(0, 6, true, "arn:")))).isFalse(); + } + + /** + * {@code startIndex == stopIndex} makes the spec's {@code substring} return null, so the coalesce + * yields {@code ""} and comparing against an empty literal is true even for a null input. + * {@code substringEquals} returns false there, so this must not be rewritten. Paired with + * {@code RulesFunctionsSubstringEqualsTest.degenerateEmptyWindowDivergesFromTheSpecAndIsGuardedInCodegen} + * in the s3 module, which pins the runtime side of the same divergence. + */ + @Test + void emptyLiteralIsNotRewritten() { + assertThat(isSubstringEqualsCall(rewrite(substringEqualsExpr(3, 3, false, "")))).isFalse(); + } + + /** + * A {@code stringEquals} that is not a substring comparison must be left intact for + * {@link software.amazon.awssdk.codegen.poet.rules2.PrepareForCodegenVisitor}, which only rewrites + * to {@code constant.equals(other)} when one side is a string constant. + * + *

Rewriting it here would emit {@code left.equals(right)} for two nullable operands, and the + * spec's {@code stringEquals} returns false for a null operand rather than throwing. The S3 BDD + * reaches this with {@code stringEquals(region, bucketArn.region())} among others. + */ + @Test + void plainStringEqualsIsLeftForPrepareForCodegen() { + RuleExpression expr = FunctionCallExpression + .builder() + .name("stringEquals") + .type(RuleRuntimeTypeMirror.BOOLEAN) + .addArgument(VariableReferenceExpression.builder().variableName("region").build()) + .addArgument(VariableReferenceExpression.builder().variableName("arnRegion").build()) + .build(); + + RuleExpression result = rewrite(expr); + + assertThat(result).isInstanceOf(FunctionCallExpression.class); + assertThat(((FunctionCallExpression) result).name()).isEqualTo("stringEquals"); + } + + /** + * Same for a constant operand: the peephole must not pre-empt PrepareForCodegenVisitor, which + * already places the constant on the receiver side. + */ + @Test + void constantOperandStringEqualsIsAlsoLeftForPrepareForCodegen() { + RuleExpression expr = FunctionCallExpression + .builder() + .name("stringEquals") + .type(RuleRuntimeTypeMirror.BOOLEAN) + .addArgument(VariableReferenceExpression.builder().variableName("region").build()) + .addArgument(new LiteralStringExpression("us-east-1")) + .build(); + + assertThat(((FunctionCallExpression) rewrite(expr)).name()).isEqualTo("stringEquals"); + } + + private static RuleExpression rewrite(RuleExpression expr) { + return expr.accept(new BddPeepholeVisitor()); + } + + /** + * When the substring peephole declines, {@code simplifyStringEquals} still rewrites the comparison + * into a {@code MethodCallExpression} for {@code left.equals(right)}, so the result is not + * necessarily a {@link FunctionCallExpression} at all. + */ + private static boolean isSubstringEqualsCall(RuleExpression expr) { + return expr instanceof FunctionCallExpression + && RuleRuntimeTypeMirror.SUBSTRING_EQUALS_FN.equals(((FunctionCallExpression) expr).name()); + } + + /** + * Builds {@code stringEquals(coalesce(substring(Bucket, start, stop, reverse), ""), literal)}. + */ + private static RuleExpression substringEqualsExpr(int start, int stop, boolean reverse, String literal) { + FunctionCallExpression substring = + FunctionCallExpression.builder() + .name("substring") + .type(RuleRuntimeTypeMirror.STRING) + .addArgument(VariableReferenceExpression.builder().variableName("Bucket").build()) + .addArgument(new LiteralIntegerExpression(start)) + .addArgument(new LiteralIntegerExpression(stop)) + .addArgument(new LiteralBooleanExpression(reverse)) + .build(); + + FunctionCallExpression coalesce = + FunctionCallExpression.builder() + .name("coalesce") + .type(RuleRuntimeTypeMirror.STRING) + .addArgument(substring) + .addArgument(new LiteralStringExpression("")) + .build(); + + return FunctionCallExpression.builder() + .name("stringEquals") + .type(RuleRuntimeTypeMirror.BOOLEAN) + .addArgument(coalesce) + .addArgument(new LiteralStringExpression(literal)) + .build(); + } +} diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules2/bdd/ConditionFnCodeGeneratorVisitorTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules2/bdd/ConditionFnCodeGeneratorVisitorTest.java new file mode 100644 index 000000000000..a021f36c53fb --- /dev/null +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules2/bdd/ConditionFnCodeGeneratorVisitorTest.java @@ -0,0 +1,164 @@ +/* + * 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.rules2.bdd; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.squareup.javapoet.CodeBlock; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel; +import software.amazon.awssdk.codegen.poet.ClientTestModels; +import software.amazon.awssdk.codegen.poet.rules.EndpointRulesSpecUtils; +import software.amazon.awssdk.codegen.poet.rules2.FunctionCallExpression; +import software.amazon.awssdk.codegen.poet.rules2.LetExpression; +import software.amazon.awssdk.codegen.poet.rules2.LiteralBooleanExpression; +import software.amazon.awssdk.codegen.poet.rules2.LiteralStringExpression; +import software.amazon.awssdk.codegen.poet.rules2.RuleExpression; +import software.amazon.awssdk.codegen.poet.rules2.RuleRuntimeTypeMirror; +import software.amazon.awssdk.codegen.poet.rules2.VariableReferenceExpression; + +/** + * Covers the null-check elision in {@code visitLetExpression}. An assign condition is satisfied only + * when the assigned value is non-null, so emitting {@code return true} instead of + * {@code return reg != null} is only sound when the value is provably non-null. + * + *

{@code __ite} is emitted as a ternary between its two branches. Only string-literal branches + * make it provably non-null: {@code BddPeepholeVisitor.simplifyIte} does not constrain the branches, + * and the BDD model is produced by the endpoint compiler rather than by this repo, so a + * {@code {"ref": ...}} branch is legal input and may be null at runtime. + */ +class ConditionFnCodeGeneratorVisitorTest { + + private static final String REGISTER = "suffix"; + private static final String NULLABLE_REF = "someNullableString"; + + private RuleRuntimeTypeMirror typeMirror; + private EndpointRulesSpecUtils specUtils; + private Map registers; + + @BeforeEach + void setUp() { + IntermediateModel model = ClientTestModels.queryServiceModelsWithSimpleBddEndpoints(); + typeMirror = new RuleRuntimeTypeMirror(model.getMetadata().getFullInternalEndpointRulesPackageName()); + specUtils = new EndpointRulesSpecUtils(model); + + registers = new HashMap<>(); + registers.put(REGISTER, register(REGISTER)); + registers.put(NULLABLE_REF, register(NULLABLE_REF)); + } + + @Test + void visitLetExpression_whenIteBranchesAreStringLiterals_elidesNullCheck() { + String generated = generate(ite(new LiteralStringExpression(".dualstack"), new LiteralStringExpression(""))); + + assertThat(generated).contains("return true"); + assertThat(generated).doesNotContain(REGISTER + " != null"); + } + + @Test + void visitLetExpression_whenIteBranchIsNullableReference_emitsNullCheck() { + String generated = generate(ite(reference(NULLABLE_REF), new LiteralStringExpression(""))); + + assertThat(generated) + .as("a ref branch can be null at runtime, so the assign condition must check the register") + .contains("return " + REGISTER + " != null"); + assertThat(generated).doesNotContain("return true"); + } + + @Test + void visitLetExpression_whenIteFalseBranchIsNullableReference_emitsNullCheck() { + String generated = generate(ite(new LiteralStringExpression(""), reference(NULLABLE_REF))); + + assertThat(generated).contains("return " + REGISTER + " != null"); + assertThat(generated).doesNotContain("return true"); + } + + @Test + void visitLetExpression_whenValueIsNotIte_emitsNullCheck() { + String generated = generate(reference(NULLABLE_REF)); + + assertThat(generated).contains("return " + REGISTER + " != null"); + assertThat(generated).doesNotContain("return true"); + } + + /** + * The single-evaluation form for a boolean coalesce. A ternary would emit the subject twice, + * running any non-trivial operand twice per evaluation. + */ + @Test + void coalesceBoolean_emitsWrapperEqualityEvaluatingSubjectOnce() { + String withFalseDefault = generate(coalesceBoolean(false)); + assertThat(withFalseDefault).contains("Boolean.TRUE.equals(" + NULLABLE_REF + ")"); + assertThat(withFalseDefault).doesNotContain("!= null ?"); + + String withTrueDefault = generate(coalesceBoolean(true)); + assertThat(withTrueDefault).contains("!Boolean.FALSE.equals(" + NULLABLE_REF + ")"); + assertThat(withTrueDefault).doesNotContain("!= null ?"); + + // Subject appears exactly once in each. + assertThat(countOccurrences(withFalseDefault, NULLABLE_REF)).isEqualTo(1); + assertThat(countOccurrences(withTrueDefault, NULLABLE_REF)).isEqualTo(1); + } + + private String generate(RuleExpression boundValue) { + CodeBlock.Builder code = CodeBlock.builder(); + LetExpression let = LetExpression.builder() + .putBinding(REGISTER, boundValue) + .build(); + let.accept(new ConditionFnCodeGeneratorVisitor(code, typeMirror, registers, specUtils)); + return code.build().toString(); + } + + private static RuleExpression ite(RuleExpression ifTrue, RuleExpression ifFalse) { + return FunctionCallExpression.builder() + .name(BddPeepholeVisitor.ITE) + .type(RuleRuntimeTypeMirror.STRING) + .addArgument(new LiteralBooleanExpression(true)) + .addArgument(ifTrue) + .addArgument(ifFalse) + .build(); + } + + private static RuleExpression coalesceBoolean(boolean defaultValue) { + return FunctionCallExpression.builder() + .name(BddPeepholeVisitor.COALESCE_BOOL) + .type(RuleRuntimeTypeMirror.BOOLEAN) + .addArgument(reference(NULLABLE_REF)) + .addArgument(new LiteralBooleanExpression(defaultValue)) + .build(); + } + + private static RuleExpression reference(String name) { + return VariableReferenceExpression.builder().variableName(name).build(); + } + + private static RegistryInfo register(String name) { + return new RegistryInfo(name, RuleRuntimeTypeMirror.STRING, null, true, null); + } + + private static int countOccurrences(String haystack, String needle) { + int count = 0; + int i = 0; + while ((i = haystack.indexOf(needle, i)) >= 0) { + count++; + i += needle.length(); + } + return count; + } +} diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-rules-metric-values-test-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-rules-metric-values-test-class.java index 67576fdf1a95..a40e26d6a0e3 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-rules-metric-values-test-class.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-rules-metric-values-test-class.java @@ -26,7 +26,7 @@ public void resolvesCorrectEndpoint(EndpointProviderTestCase tc) { private static List testCases() { List testCases = new ArrayList<>(); - testCases.add(new EndpointProviderTestCase(() -> { + testCases.add(new EndpointProviderTestCase("test case 1", () -> { QueryEndpointParams.Builder builder = QueryEndpointParams.builder(); builder.region(Region.of("us-east-1")); return PROVIDER.resolveEndpoint(builder.build()).join(); @@ -35,50 +35,50 @@ private static List testCases() { .endpoint( Endpoint.builder().url(URI.create("https://myservice.aws")) .putAttribute(AwsEndpointAttribute.METRIC_VALUES, Arrays.asList("1", "2")).build()).build())); - testCases.add(new EndpointProviderTestCase(() -> { + testCases.add(new EndpointProviderTestCase("test case 2", () -> { QueryEndpointParams.Builder builder = QueryEndpointParams.builder(); builder.region(Region.of("us-east-1")); builder.booleanContextParam(true); builder.stringContextParam("this is a test"); return PROVIDER.resolveEndpoint(builder.build()).join(); }, Expect.builder().endpoint(Endpoint.builder().url(URI.create("https://myservice.aws")).build()).build())); - testCases.add(new EndpointProviderTestCase(() -> { + testCases.add(new EndpointProviderTestCase("test case 3", () -> { QueryEndpointParams.Builder builder = QueryEndpointParams.builder(); builder.region(Region.of("us-east-1")); return PROVIDER.resolveEndpoint(builder.build()).join(); }, Expect.builder().endpoint(Endpoint.builder().url(URI.create("https://myservice.aws")).build()).build())); - testCases.add(new EndpointProviderTestCase(() -> { + testCases.add(new EndpointProviderTestCase("test case 4", () -> { QueryEndpointParams.Builder builder = QueryEndpointParams.builder(); builder.region(Region.of("us-east-6")); return PROVIDER.resolveEndpoint(builder.build()).join(); }, Expect.builder().endpoint(Endpoint.builder().url(URI.create("https://myservice.aws")).build()).build())); - testCases.add(new EndpointProviderTestCase(() -> { + testCases.add(new EndpointProviderTestCase("test case 5", () -> { QueryEndpointParams.Builder builder = QueryEndpointParams.builder(); builder.accountId("012345678901"); builder.accountIdEndpointMode("required"); return PROVIDER.resolveEndpoint(builder.build()).join(); }, Expect.builder().endpoint(Endpoint.builder().url(URI.create("https://012345678901.myservice.aws")).build()).build())); - testCases.add(new EndpointProviderTestCase(() -> { + testCases.add(new EndpointProviderTestCase("test case 6", () -> { QueryEndpointParams.Builder builder = QueryEndpointParams.builder(); builder.accountId("012345678901"); builder.accountIdEndpointMode("required"); return PROVIDER.resolveEndpoint(builder.build()).join(); }, Expect.builder().endpoint(Endpoint.builder().url(URI.create("https://012345678901.myservice.aws")).build()).build())); - testCases.add(new EndpointProviderTestCase(() -> { + testCases.add(new EndpointProviderTestCase("test case 7", () -> { QueryEndpointParams.Builder builder = QueryEndpointParams.builder(); builder.accountId("012345678901"); builder.accountIdEndpointMode("required"); return PROVIDER.resolveEndpoint(builder.build()).join(); }, Expect.builder().endpoint(Endpoint.builder().url(URI.create("https://012345678901.myservice.aws")).build()).build())); - testCases.add(new EndpointProviderTestCase(() -> { + testCases.add(new EndpointProviderTestCase("For region us-iso-west-1 with FIPS enabled and DualStack enabled", () -> { QueryEndpointParams.Builder builder = QueryEndpointParams.builder(); return PROVIDER.resolveEndpoint(builder.build()).join(); }, Expect.builder().error("Should have been skipped!").build())); - testCases.add(new EndpointProviderTestCase(() -> { + testCases.add(new EndpointProviderTestCase("Has complex operation input", () -> { QueryEndpointParams.Builder builder = QueryEndpointParams.builder(); return PROVIDER.resolveEndpoint(builder.build()).join(); }, Expect.builder().error("Missing info").build())); - testCases.add(new EndpointProviderTestCase(() -> { + testCases.add(new EndpointProviderTestCase("Has has undeclared input parameter", () -> { QueryEndpointParams.Builder builder = QueryEndpointParams.builder(); return PROVIDER.resolveEndpoint(builder.build()).join(); }, Expect.builder().error("Missing info").build())); diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-rules-stringarray-test-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-rules-stringarray-test-class.java index 93ea4e6b281c..466cccc5d728 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-rules-stringarray-test-class.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-rules-stringarray-test-class.java @@ -24,21 +24,21 @@ public void resolvesCorrectEndpoint(EndpointProviderTestCase tc) { private static List testCases() { List testCases = new ArrayList<>(); - testCases.add(new EndpointProviderTestCase(() -> { + testCases.add(new EndpointProviderTestCase("Default array values used", () -> { SampleSvcEndpointParams.Builder builder = SampleSvcEndpointParams.builder(); return PROVIDER.resolveEndpoint(builder.build()).join(); }, Expect.builder().endpoint(Endpoint.builder().url(URI.create("https://example.com/defaultValue1")).build()).build())); - testCases.add(new EndpointProviderTestCase(() -> { + testCases.add(new EndpointProviderTestCase("Empty array", () -> { SampleSvcEndpointParams.Builder builder = SampleSvcEndpointParams.builder(); builder.stringArrayParam(Arrays.asList()); return PROVIDER.resolveEndpoint(builder.build()).join(); }, Expect.builder().error("no array values set").build())); - testCases.add(new EndpointProviderTestCase(() -> { + testCases.add(new EndpointProviderTestCase("Static value", () -> { SampleSvcEndpointParams.Builder builder = SampleSvcEndpointParams.builder(); builder.stringArrayParam(Arrays.asList("staticValue1")); return PROVIDER.resolveEndpoint(builder.build()).join(); }, Expect.builder().endpoint(Endpoint.builder().url(URI.create("https://example.com/staticValue1")).build()).build())); - testCases.add(new EndpointProviderTestCase(() -> { + testCases.add(new EndpointProviderTestCase("bound value from input", () -> { SampleSvcEndpointParams.Builder builder = SampleSvcEndpointParams.builder(); builder.stringArrayParam(Arrays.asList("key1")); return PROVIDER.resolveEndpoint(builder.build()).join(); diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-rules-unknown-property-test-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-rules-unknown-property-test-class.java index b471a05fad5c..69f06553b84d 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-rules-unknown-property-test-class.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-rules-unknown-property-test-class.java @@ -38,43 +38,43 @@ public void resolvesCorrectEndpoint(EndpointProviderTestCase tc) { private static List testCases() { List testCases = new ArrayList<>(); - testCases.add(new EndpointProviderTestCase(() -> { + testCases.add(new EndpointProviderTestCase("test case 1", () -> { QueryEndpointParams.Builder builder = QueryEndpointParams.builder(); return PROVIDER.resolveEndpoint(builder.build()).join(); }, Expect.builder().endpoint(Endpoint.builder().url(URI.create("https://myservice.aws")).build()).build())); - testCases.add(new EndpointProviderTestCase(() -> { + testCases.add(new EndpointProviderTestCase("test case 2", () -> { QueryEndpointParams.Builder builder = QueryEndpointParams.builder(); return PROVIDER.resolveEndpoint(builder.build()).join(); }, Expect.builder().endpoint(Endpoint.builder().url(URI.create("https://myservice.aws")).build()).build())); - testCases.add(new EndpointProviderTestCase(() -> { + testCases.add(new EndpointProviderTestCase("test case 3", () -> { QueryEndpointParams.Builder builder = QueryEndpointParams.builder(); return PROVIDER.resolveEndpoint(builder.build()).join(); }, Expect.builder().endpoint(Endpoint.builder().url(URI.create("https://myservice.aws")).build()).build())); - testCases.add(new EndpointProviderTestCase(() -> { + testCases.add(new EndpointProviderTestCase("test case 4", () -> { QueryEndpointParams.Builder builder = QueryEndpointParams.builder(); return PROVIDER.resolveEndpoint(builder.build()).join(); }, Expect.builder().endpoint(Endpoint.builder().url(URI.create("https://myservice.aws")).build()).build())); - testCases.add(new EndpointProviderTestCase(() -> { + testCases.add(new EndpointProviderTestCase("test case 5", () -> { QueryEndpointParams.Builder builder = QueryEndpointParams.builder(); return PROVIDER.resolveEndpoint(builder.build()).join(); }, Expect.builder().endpoint(Endpoint.builder().url(URI.create("https://012345678901.myservice.aws")).build()).build())); - testCases.add(new EndpointProviderTestCase(() -> { + testCases.add(new EndpointProviderTestCase("test case 6", () -> { QueryEndpointParams.Builder builder = QueryEndpointParams.builder(); return PROVIDER.resolveEndpoint(builder.build()).join(); }, Expect.builder().endpoint(Endpoint.builder().url(URI.create("https://012345678901.myservice.aws")).build()).build())); - testCases.add(new EndpointProviderTestCase(() -> { + testCases.add(new EndpointProviderTestCase("test case 7", () -> { QueryEndpointParams.Builder builder = QueryEndpointParams.builder(); return PROVIDER.resolveEndpoint(builder.build()).join(); }, Expect.builder().endpoint(Endpoint.builder().url(URI.create("https://012345678901.myservice.aws")).build()).build())); - testCases.add(new EndpointProviderTestCase(() -> { + testCases.add(new EndpointProviderTestCase("For region us-iso-west-1 with FIPS enabled and DualStack enabled", () -> { QueryEndpointParams.Builder builder = QueryEndpointParams.builder(); return PROVIDER.resolveEndpoint(builder.build()).join(); }, Expect.builder().error("Should have been skipped!").build())); - testCases.add(new EndpointProviderTestCase(() -> { + testCases.add(new EndpointProviderTestCase("Has complex operation input", () -> { QueryEndpointParams.Builder builder = QueryEndpointParams.builder(); return PROVIDER.resolveEndpoint(builder.build()).join(); }, Expect.builder().error("Missing info").build())); - testCases.add(new EndpointProviderTestCase(() -> { + testCases.add(new EndpointProviderTestCase("Has has undeclared input parameter", () -> { QueryEndpointParams.Builder builder = QueryEndpointParams.builder(); return PROVIDER.resolveEndpoint(builder.build()).join(); }, Expect.builder().error("Missing info").build())); diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/bdd/endpoint-provider-bdd-s3-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/bdd/endpoint-provider-bdd-s3-class.java new file mode 100644 index 000000000000..bb661bf8c9db --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/bdd/endpoint-provider-bdd-s3-class.java @@ -0,0 +1,4102 @@ +package software.amazon.awssdk.services.query.endpoints.internal; + +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; +import software.amazon.awssdk.annotations.Generated; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.awscore.endpoints.AwsEndpointAttribute; +import software.amazon.awssdk.awscore.endpoints.authscheme.SigV4AuthScheme; +import software.amazon.awssdk.awscore.endpoints.authscheme.SigV4aAuthScheme; +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.endpoints.Endpoint; +import software.amazon.awssdk.endpoints.EndpointUrl; +import software.amazon.awssdk.services.query.endpoints.QueryEndpointParams; +import software.amazon.awssdk.services.query.endpoints.QueryEndpointProvider; +import software.amazon.awssdk.services.s3.endpoints.authscheme.DynamicEndpointAuthSchemeFactory; +import software.amazon.awssdk.utils.CompletableFutureUtils; + +@Generated("software.amazon.awssdk:codegen") +@SdkInternalApi +public final class DefaultQueryEndpointProvider implements QueryEndpointProvider { + @Override + public CompletableFuture resolveEndpoint(QueryEndpointParams endpointParams) { + try { + Evaluator evaluator = new Evaluator(); + evaluator.params = endpointParams; + evaluator.region = endpointParams.region() == null ? null : endpointParams.region().id(); + Endpoint result = evaluator.nodeP1(); + if (result == null) { + return CompletableFutureUtils.failedFuture(SdkClientException.create("Rule engine did not reach an error or endpoint result")); + } + return CompletableFuture.completedFuture(result); + } catch (SdkClientException e) { + String errorMsg = e.getMessage(); + if (errorMsg != null && errorMsg.contains("Invalid ARN") && errorMsg.contains(":s3:::")) { + return CompletableFutureUtils.failedFuture(SdkClientException.create(errorMsg + ". Use the bucket name instead of simple bucket ARNs in GetBucketLocationRequest.", e)); + } + return CompletableFutureUtils.failedFuture(e); + } catch (Exception error) { + return CompletableFutureUtils.failedFuture(error); + } + } + + private static final class Evaluator { + QueryEndpointParams params; + + String region; + + RulePartition partitionResult; + + String accessPointSuffix; + + String regionPrefix; + + String outpostId_ssa_2; + + String hardwareType; + + String _s3e_ds; + + String _s3e_fips; + + String _s3e_auth; + + RuleUrl url; + + RuleArn bucketArn; + + String s3expressAvailabilityZoneId; + + String uri_encoded_bucket; + + String arnType; + + String accessPointName_ssa_1; + + RulePartition bucketPartition; + + String outpostId_ssa_1; + + String outpostType; + + String accessPointName_ssa_2; + + private Endpoint nodeP0() { + return null; + } + + private Endpoint nodeP1() { + return region != null + ? nodeP2() + : result114(); + } + + private Endpoint nodeP2() { + return Boolean.TRUE.equals(params.accelerate()) + ? nodeP423() + : nodeP3(); + } + + private Endpoint nodeP3() { + return Boolean.TRUE.equals(params.useFips()) + ? nodeP271() + : nodeP4(); + } + + private Endpoint nodeP4() { + return Boolean.TRUE.equals(params.useDualStack()) + ? nodeP232() + : nodeP5(); + } + + private Endpoint nodeP5() { + return params.endpoint() != null + ? nodeP84() + : nodeP6(); + } + + private Endpoint nodeP6() { + return params.bucket() != null + ? nodeP14() + : nodeP7(); + } + + private Endpoint nodeP7() { + return cond8() + ? nodeP8() + : result114(); + } + + private Endpoint nodeP8() { + return cond16() + ? nodeP9() + : nodeP12(); + } + + private Endpoint nodeP9() { + return cond18() + ? nodeP10() + : nodeP12(); + } + + private Endpoint nodeP10() { + return cond19() + ? nodeP11() + : nodeP12(); + } + + private Endpoint nodeP11() { + return cond22() + ? result13() + : nodeP12(); + } + + private Endpoint nodeP12() { + return cond35() + ? nodeP13() + : result41(); + } + + private Endpoint nodeP13() { + return cond36() + ? result102() + : nodeP434(); + } + + private Endpoint nodeP14() { + return cond6() + ? nodeP270() + : nodeP15(); + } + + private Endpoint nodeP15() { + return cond7() + ? nodeP269() + : nodeP16(); + } + + private Endpoint nodeP16() { + return cond8() + ? nodeP18() + : nodeP17(); + } + + private Endpoint nodeP17() { + return Boolean.TRUE.equals(params.forcePathStyle()) + ? nodeP500() + : nodeP105(); + } + + private Endpoint nodeP18() { + return cond9() + ? nodeP19() + : nodeP23(); + } + + private Endpoint nodeP19() { + return accessPointSuffix != null && accessPointSuffix.equals("--op-s3") + ? nodeP20() + : nodeP23(); + } + + private Endpoint nodeP20() { + return cond11() + ? nodeP21() + : nodeP23(); + } + + private Endpoint nodeP21() { + return cond12() + ? nodeP22() + : nodeP23(); + } + + private Endpoint nodeP22() { + return cond13() + ? nodeP546() + : nodeP23(); + } + + private Endpoint nodeP23() { + return Boolean.TRUE.equals(params.forcePathStyle()) + ? nodeP76() + : nodeP24(); + } + + private Endpoint nodeP24() { + return cond20() + ? nodeP72() + : nodeP25(); + } + + private Endpoint nodeP25() { + return cond26() + ? nodeP26() + : nodeP77(); + } + + private Endpoint nodeP26() { + return cond37() + ? nodeP27() + : result85(); + } + + private Endpoint nodeP27() { + return arnType != null && arnType.equals("") + ? result85() + : nodeP28(); + } + + private Endpoint nodeP28() { + return arnType != null && arnType.equals("accesspoint") + ? nodeP46() + : nodeP29(); + } + + private Endpoint nodeP29() { + return cond48() + ? result57() + : nodeP30(); + } + + private Endpoint nodeP30() { + return cond50() + ? nodeP31() + : result84(); + } + + private Endpoint nodeP31() { + return cond51() + ? nodeP32() + : nodeP135(); + } + + private Endpoint nodeP32() { + return cond55() + ? result75() + : nodeP33(); + } + + private Endpoint nodeP33() { + return cond59() + ? nodeP34() + : result83(); + } + + private Endpoint nodeP34() { + return cond60() + ? nodeP38() + : nodeP35(); + } + + private Endpoint nodeP35() { + return cond61() + ? nodeP36() + : result82(); + } + + private Endpoint nodeP36() { + return cond62() + ? nodeP37() + : nodeP145(); + } + + private Endpoint nodeP37() { + return cond63() + ? nodeP40() + : result45(); + } + + private Endpoint nodeP38() { + return cond61() + ? nodeP39() + : result82(); + } + + private Endpoint nodeP39() { + return cond62() + ? nodeP40() + : nodeP149(); + } + + private Endpoint nodeP40() { + return cond64() + ? nodeP41() + : result53(); + } + + private Endpoint nodeP41() { + return cond66() + ? nodeP42() + : result52(); + } + + private Endpoint nodeP42() { + return cond70() + ? nodeP43() + : result51(); + } + + private Endpoint nodeP43() { + return cond71() + ? nodeP44() + : result80(); + } + + private Endpoint nodeP44() { + return outpostType != null && outpostType.equals("accesspoint") + ? nodeP45() + : result79(); + } + + private Endpoint nodeP45() { + return cond74() + ? result77() + : result78(); + } + + private Endpoint nodeP46() { + return cond40() + ? nodeP47() + : result56(); + } + + private Endpoint nodeP47() { + return accessPointName_ssa_1 != null && accessPointName_ssa_1.equals("") + ? result56() + : nodeP48(); + } + + private Endpoint nodeP48() { + return cond42() + ? nodeP184() + : nodeP49(); + } + + private Endpoint nodeP49() { + return cond48() + ? nodeP61() + : nodeP50(); + } + + private Endpoint nodeP50() { + return cond49() + ? result44() + : nodeP51(); + } + + private Endpoint nodeP51() { + return cond51() + ? nodeP52() + : nodeP525(); + } + + private Endpoint nodeP52() { + return cond60() + ? nodeP55() + : nodeP53(); + } + + private Endpoint nodeP53() { + return cond62() + ? result54() + : nodeP54(); + } + + private Endpoint nodeP54() { + return cond63() + ? nodeP56() + : result45(); + } + + private Endpoint nodeP55() { + return cond62() + ? result54() + : nodeP56(); + } + + private Endpoint nodeP56() { + return cond64() + ? nodeP57() + : result53(); + } + + private Endpoint nodeP57() { + return cond66() + ? nodeP58() + : result52(); + } + + private Endpoint nodeP58() { + return cond69() + ? nodeP59() + : result64(); + } + + private Endpoint nodeP59() { + return cond70() + ? nodeP60() + : result51(); + } + + private Endpoint nodeP60() { + return cond72() + ? result63() + : result50(); + } + + private Endpoint nodeP61() { + return cond49() + ? result44() + : nodeP62(); + } + + private Endpoint nodeP62() { + return cond51() + ? nodeP63() + : nodeP525(); + } + + private Endpoint nodeP63() { + return cond60() + ? nodeP66() + : nodeP64(); + } + + private Endpoint nodeP64() { + return cond62() + ? result54() + : nodeP65(); + } + + private Endpoint nodeP65() { + return cond63() + ? nodeP67() + : result45(); + } + + private Endpoint nodeP66() { + return cond62() + ? result54() + : nodeP67(); + } + + private Endpoint nodeP67() { + return cond64() + ? nodeP68() + : result53(); + } + + private Endpoint nodeP68() { + return cond66() + ? nodeP69() + : result52(); + } + + private Endpoint nodeP69() { + return cond68() + ? result46() + : nodeP70(); + } + + private Endpoint nodeP70() { + return cond70() + ? nodeP71() + : result51(); + } + + private Endpoint nodeP71() { + return cond72() + ? result49() + : result50(); + } + + private Endpoint nodeP72() { + return cond25() + ? nodeP73() + : result41(); + } + + private Endpoint nodeP73() { + return region != null && region.equals("aws-global") + ? result38() + : nodeP74(); + } + + private Endpoint nodeP74() { + return Boolean.TRUE.equals(params.useGlobalEndpoint()) + ? nodeP75() + : result40(); + } + + private Endpoint nodeP75() { + return region != null && region.equals("us-east-1") + ? result39() + : result40(); + } + + private Endpoint nodeP76() { + return cond26() + ? result87() + : nodeP77(); + } + + private Endpoint nodeP77() { + return cond28() + ? result86() + : nodeP78(); + } + + private Endpoint nodeP78() { + return cond34() + ? nodeP81() + : nodeP79(); + } + + private Endpoint nodeP79() { + return cond35() + ? nodeP80() + : nodeP544(); + } + + private Endpoint nodeP80() { + return cond36() + ? result102() + : result114(); + } + + private Endpoint nodeP81() { + return region != null && region.equals("aws-global") + ? result96() + : nodeP82(); + } + + private Endpoint nodeP82() { + return Boolean.TRUE.equals(params.useGlobalEndpoint()) + ? nodeP83() + : result98(); + } + + private Endpoint nodeP83() { + return region != null && region.equals("us-east-1") + ? result97() + : result98(); + } + + private Endpoint nodeP84() { + return params.bucket() != null + ? nodeP100() + : nodeP85(); + } + + private Endpoint nodeP85() { + return cond8() + ? nodeP86() + : result114(); + } + + private Endpoint nodeP86() { + return cond16() + ? nodeP87() + : nodeP88(); + } + + private Endpoint nodeP87() { + return cond18() + ? nodeP90() + : nodeP88(); + } + + private Endpoint nodeP88() { + return cond19() + ? nodeP89() + : nodeP91(); + } + + private Endpoint nodeP89() { + return cond21() + ? nodeP96() + : nodeP94(); + } + + private Endpoint nodeP90() { + return cond19() + ? nodeP92() + : nodeP91(); + } + + private Endpoint nodeP91() { + return cond21() + ? nodeP97() + : nodeP94(); + } + + private Endpoint nodeP92() { + return cond21() + ? nodeP96() + : nodeP93(); + } + + private Endpoint nodeP93() { + return cond22() + ? result13() + : nodeP94(); + } + + private Endpoint nodeP94() { + return cond35() + ? nodeP95() + : result41(); + } + + private Endpoint nodeP95() { + return cond36() + ? result102() + : result41(); + } + + private Endpoint nodeP96() { + return cond22() + ? result12() + : nodeP97(); + } + + private Endpoint nodeP97() { + return cond35() + ? nodeP98() + : result41(); + } + + private Endpoint nodeP98() { + return cond36() + ? result100() + : nodeP99(); + } + + private Endpoint nodeP99() { + return region != null && region.equals("aws-global") + ? result109() + : result110(); + } + + private Endpoint nodeP100() { + return cond6() + ? nodeP213() + : nodeP101(); + } + + private Endpoint nodeP101() { + return cond7() + ? nodeP207() + : nodeP102(); + } + + private Endpoint nodeP102() { + return cond8() + ? nodeP118() + : nodeP103(); + } + + private Endpoint nodeP103() { + return Boolean.TRUE.equals(params.forcePathStyle()) + ? nodeP117() + : nodeP104(); + } + + private Endpoint nodeP104() { + return cond21() + ? nodeP105() + : result22(); + } + + private Endpoint nodeP105() { + return cond26() + ? nodeP106() + : nodeP501(); + } + + private Endpoint nodeP106() { + return cond37() + ? nodeP107() + : result85(); + } + + private Endpoint nodeP107() { + return arnType != null && arnType.equals("") + ? result85() + : nodeP108(); + } + + private Endpoint nodeP108() { + return arnType != null && arnType.equals("accesspoint") + ? nodeP111() + : nodeP109(); + } + + private Endpoint nodeP109() { + return cond48() + ? result57() + : nodeP110(); + } + + private Endpoint nodeP110() { + return cond50() + ? nodeP135() + : result84(); + } + + private Endpoint nodeP111() { + return cond40() + ? nodeP112() + : result56(); + } + + private Endpoint nodeP112() { + return accessPointName_ssa_1 != null && accessPointName_ssa_1.equals("") + ? result56() + : nodeP113(); + } + + private Endpoint nodeP113() { + return cond42() + ? nodeP114() + : nodeP499(); + } + + private Endpoint nodeP114() { + return cond48() + ? result55() + : nodeP115(); + } + + private Endpoint nodeP115() { + return cond52() + ? nodeP116() + : result71(); + } + + private Endpoint nodeP116() { + return Boolean.TRUE.equals(params.disableMultiRegionAccessPoints()) + ? result68() + : result71(); + } + + private Endpoint nodeP117() { + return cond21() + ? nodeP500() + : result22(); + } + + private Endpoint nodeP118() { + return cond9() + ? nodeP119() + : nodeP123(); + } + + private Endpoint nodeP119() { + return accessPointSuffix != null && accessPointSuffix.equals("--op-s3") + ? nodeP120() + : nodeP123(); + } + + private Endpoint nodeP120() { + return cond11() + ? nodeP121() + : nodeP123(); + } + + private Endpoint nodeP121() { + return cond12() + ? nodeP122() + : nodeP123(); + } + + private Endpoint nodeP122() { + return cond13() + ? nodeP201() + : nodeP123(); + } + + private Endpoint nodeP123() { + return Boolean.TRUE.equals(params.forcePathStyle()) + ? nodeP194() + : nodeP124(); + } + + private Endpoint nodeP124() { + return cond20() + ? nodeP189() + : nodeP125(); + } + + private Endpoint nodeP125() { + return cond21() + ? nodeP126() + : result22(); + } + + private Endpoint nodeP126() { + return cond23() + ? nodeP127() + : nodeP128(); + } + + private Endpoint nodeP127() { + return cond24() + ? nodeP188() + : nodeP128(); + } + + private Endpoint nodeP128() { + return cond26() + ? nodeP129() + : nodeP196(); + } + + private Endpoint nodeP129() { + return cond37() + ? nodeP130() + : result85(); + } + + private Endpoint nodeP130() { + return arnType != null && arnType.equals("") + ? result85() + : nodeP131(); + } + + private Endpoint nodeP131() { + return arnType != null && arnType.equals("accesspoint") + ? nodeP158() + : nodeP132(); + } + + private Endpoint nodeP132() { + return cond48() + ? result57() + : nodeP133(); + } + + private Endpoint nodeP133() { + return cond50() + ? nodeP134() + : result84(); + } + + private Endpoint nodeP134() { + return cond51() + ? nodeP140() + : nodeP135(); + } + + private Endpoint nodeP135() { + return cond55() + ? result75() + : nodeP136(); + } + + private Endpoint nodeP136() { + return cond59() + ? nodeP137() + : result83(); + } + + private Endpoint nodeP137() { + return cond60() + ? result82() + : nodeP138(); + } + + private Endpoint nodeP138() { + return cond61() + ? nodeP139() + : result82(); + } + + private Endpoint nodeP139() { + return cond63() + ? result82() + : result45(); + } + + private Endpoint nodeP140() { + return cond55() + ? result75() + : nodeP141(); + } + + private Endpoint nodeP141() { + return cond59() + ? nodeP142() + : result83(); + } + + private Endpoint nodeP142() { + return cond60() + ? nodeP147() + : nodeP143(); + } + + private Endpoint nodeP143() { + return cond61() + ? nodeP144() + : result82(); + } + + private Endpoint nodeP144() { + return cond62() + ? nodeP146() + : nodeP145(); + } + + private Endpoint nodeP145() { + return cond63() + ? nodeP149() + : result45(); + } + + private Endpoint nodeP146() { + return cond63() + ? nodeP152() + : result45(); + } + + private Endpoint nodeP147() { + return cond61() + ? nodeP148() + : result82(); + } + + private Endpoint nodeP148() { + return cond62() + ? nodeP152() + : nodeP149(); + } + + private Endpoint nodeP149() { + return cond64() + ? nodeP150() + : result53(); + } + + private Endpoint nodeP150() { + return cond66() + ? nodeP151() + : result52(); + } + + private Endpoint nodeP151() { + return cond70() + ? result81() + : result51(); + } + + private Endpoint nodeP152() { + return cond64() + ? nodeP153() + : result53(); + } + + private Endpoint nodeP153() { + return cond66() + ? nodeP154() + : result52(); + } + + private Endpoint nodeP154() { + return cond70() + ? nodeP155() + : result51(); + } + + private Endpoint nodeP155() { + return cond71() + ? nodeP156() + : result80(); + } + + private Endpoint nodeP156() { + return outpostType != null && outpostType.equals("accesspoint") + ? nodeP157() + : result79(); + } + + private Endpoint nodeP157() { + return cond74() + ? result76() + : result78(); + } + + private Endpoint nodeP158() { + return cond40() + ? nodeP159() + : result56(); + } + + private Endpoint nodeP159() { + return accessPointName_ssa_1 != null && accessPointName_ssa_1.equals("") + ? result56() + : nodeP160(); + } + + private Endpoint nodeP160() { + return cond42() + ? nodeP184() + : nodeP161(); + } + + private Endpoint nodeP161() { + return cond48() + ? nodeP173() + : nodeP162(); + } + + private Endpoint nodeP162() { + return cond49() + ? result44() + : nodeP163(); + } + + private Endpoint nodeP163() { + return cond51() + ? nodeP164() + : nodeP525(); + } + + private Endpoint nodeP164() { + return cond60() + ? nodeP167() + : nodeP165(); + } + + private Endpoint nodeP165() { + return cond62() + ? result54() + : nodeP166(); + } + + private Endpoint nodeP166() { + return cond63() + ? nodeP168() + : result45(); + } + + private Endpoint nodeP167() { + return cond62() + ? result54() + : nodeP168(); + } + + private Endpoint nodeP168() { + return cond64() + ? nodeP169() + : result53(); + } + + private Endpoint nodeP169() { + return cond66() + ? nodeP170() + : result52(); + } + + private Endpoint nodeP170() { + return cond69() + ? nodeP171() + : result64(); + } + + private Endpoint nodeP171() { + return cond70() + ? nodeP172() + : result51(); + } + + private Endpoint nodeP172() { + return cond72() + ? result62() + : result50(); + } + + private Endpoint nodeP173() { + return cond49() + ? result44() + : nodeP174(); + } + + private Endpoint nodeP174() { + return cond51() + ? nodeP175() + : nodeP525(); + } + + private Endpoint nodeP175() { + return cond60() + ? nodeP178() + : nodeP176(); + } + + private Endpoint nodeP176() { + return cond62() + ? result54() + : nodeP177(); + } + + private Endpoint nodeP177() { + return cond63() + ? nodeP179() + : result45(); + } + + private Endpoint nodeP178() { + return cond62() + ? result54() + : nodeP179(); + } + + private Endpoint nodeP179() { + return cond64() + ? nodeP180() + : result53(); + } + + private Endpoint nodeP180() { + return cond66() + ? nodeP181() + : result52(); + } + + private Endpoint nodeP181() { + return cond68() + ? result46() + : nodeP182(); + } + + private Endpoint nodeP182() { + return cond70() + ? nodeP183() + : result51(); + } + + private Endpoint nodeP183() { + return cond72() + ? result47() + : result50(); + } + + private Endpoint nodeP184() { + return cond48() + ? result55() + : nodeP185(); + } + + private Endpoint nodeP185() { + return cond52() + ? nodeP186() + : result71(); + } + + private Endpoint nodeP186() { + return Boolean.TRUE.equals(params.disableMultiRegionAccessPoints()) + ? result68() + : nodeP187(); + } + + private Endpoint nodeP187() { + return cond67() + ? result69() + : result70(); + } + + private Endpoint nodeP188() { + return cond25() + ? result35() + : result41(); + } + + private Endpoint nodeP189() { + return cond21() + ? nodeP190() + : result22(); + } + + private Endpoint nodeP190() { + return cond25() + ? nodeP191() + : result41(); + } + + private Endpoint nodeP191() { + return cond30() + ? nodeP193() + : nodeP192(); + } + + private Endpoint nodeP192() { + return region != null && region.equals("aws-global") + ? result33() + : result35(); + } + + private Endpoint nodeP193() { + return region != null && region.equals("aws-global") + ? result32() + : result34(); + } + + private Endpoint nodeP194() { + return cond21() + ? nodeP195() + : result22(); + } + + private Endpoint nodeP195() { + return cond26() + ? result87() + : nodeP196(); + } + + private Endpoint nodeP196() { + return cond28() + ? result86() + : nodeP197(); + } + + private Endpoint nodeP197() { + return cond34() + ? nodeP200() + : nodeP198(); + } + + private Endpoint nodeP198() { + return cond35() + ? nodeP199() + : nodeP544(); + } + + private Endpoint nodeP199() { + return cond36() + ? result100() + : result114(); + } + + private Endpoint nodeP200() { + return region != null && region.equals("aws-global") + ? result94() + : result95(); + } + + private Endpoint nodeP201() { + return cond17() + ? nodeP202() + : result21(); + } + + private Endpoint nodeP202() { + return cond20() + ? nodeP203() + : result20(); + } + + private Endpoint nodeP203() { + return cond21() + ? nodeP204() + : nodeP549(); + } + + private Endpoint nodeP204() { + return regionPrefix != null && regionPrefix.equals("beta") + ? nodeP205() + : nodeP549(); + } + + private Endpoint nodeP205() { + return hardwareType != null && hardwareType.equals("e") + ? result15() + : nodeP206(); + } + + private Endpoint nodeP206() { + return hardwareType != null && hardwareType.equals("o") + ? result17() + : result19(); + } + + private Endpoint nodeP207() { + return cond8() + ? nodeP208() + : nodeP214(); + } + + private Endpoint nodeP208() { + return cond16() + ? nodeP209() + : nodeP219(); + } + + private Endpoint nodeP209() { + return cond18() + ? nodeP210() + : nodeP219(); + } + + private Endpoint nodeP210() { + return cond19() + ? nodeP211() + : nodeP223(); + } + + private Endpoint nodeP211() { + return cond20() + ? nodeP212() + : nodeP226(); + } + + private Endpoint nodeP212() { + return cond21() + ? nodeP230() + : nodeP400(); + } + + private Endpoint nodeP213() { + return cond8() + ? nodeP217() + : nodeP214(); + } + + private Endpoint nodeP214() { + return cond19() + ? nodeP215() + : result8(); + } + + private Endpoint nodeP215() { + return cond20() + ? nodeP216() + : nodeP226(); + } + + private Endpoint nodeP216() { + return cond21() + ? nodeP230() + : result8(); + } + + private Endpoint nodeP217() { + return cond16() + ? nodeP218() + : nodeP219(); + } + + private Endpoint nodeP218() { + return cond18() + ? nodeP222() + : nodeP219(); + } + + private Endpoint nodeP219() { + return cond19() + ? nodeP220() + : nodeP223(); + } + + private Endpoint nodeP220() { + return cond20() + ? nodeP221() + : nodeP226(); + } + + private Endpoint nodeP221() { + return cond21() + ? nodeP230() + : result11(); + } + + private Endpoint nodeP222() { + return cond19() + ? nodeP225() + : nodeP223(); + } + + private Endpoint nodeP223() { + return cond20() + ? nodeP224() + : result8(); + } + + private Endpoint nodeP224() { + return cond21() + ? result8() + : result11(); + } + + private Endpoint nodeP225() { + return cond20() + ? nodeP229() + : nodeP226(); + } + + private Endpoint nodeP226() { + return cond21() + ? nodeP227() + : result8(); + } + + private Endpoint nodeP227() { + return cond30() + ? nodeP228() + : result8(); + } + + private Endpoint nodeP228() { + return cond34() + ? result6() + : result8(); + } + + private Endpoint nodeP229() { + return cond21() + ? nodeP230() + : nodeP414(); + } + + private Endpoint nodeP230() { + return cond30() + ? nodeP231() + : result7(); + } + + private Endpoint nodeP231() { + return cond34() + ? result6() + : result7(); + } + + private Endpoint nodeP232() { + return params.endpoint() != null + ? result1() + : nodeP233(); + } + + private Endpoint nodeP233() { + return params.bucket() != null + ? nodeP234() + : nodeP479(); + } + + private Endpoint nodeP234() { + return cond6() + ? nodeP270() + : nodeP235(); + } + + private Endpoint nodeP235() { + return cond7() + ? nodeP269() + : nodeP236(); + } + + private Endpoint nodeP236() { + return cond8() + ? nodeP237() + : nodeP490(); + } + + private Endpoint nodeP237() { + return cond9() + ? nodeP238() + : nodeP242(); + } + + private Endpoint nodeP238() { + return accessPointSuffix != null && accessPointSuffix.equals("--op-s3") + ? nodeP239() + : nodeP242(); + } + + private Endpoint nodeP239() { + return cond11() + ? nodeP240() + : nodeP242(); + } + + private Endpoint nodeP240() { + return cond12() + ? nodeP241() + : nodeP242(); + } + + private Endpoint nodeP241() { + return cond13() + ? nodeP546() + : nodeP242(); + } + + private Endpoint nodeP242() { + return Boolean.TRUE.equals(params.forcePathStyle()) + ? nodeP265() + : nodeP243(); + } + + private Endpoint nodeP243() { + return cond20() + ? nodeP263() + : nodeP244(); + } + + private Endpoint nodeP244() { + return cond26() + ? nodeP245() + : nodeP266(); + } + + private Endpoint nodeP245() { + return cond37() + ? nodeP246() + : result85(); + } + + private Endpoint nodeP246() { + return arnType != null && arnType.equals("") + ? result85() + : nodeP247(); + } + + private Endpoint nodeP247() { + return arnType != null && arnType.equals("accesspoint") + ? nodeP248() + : nodeP517(); + } + + private Endpoint nodeP248() { + return cond40() + ? nodeP249() + : result56(); + } + + private Endpoint nodeP249() { + return accessPointName_ssa_1 != null && accessPointName_ssa_1.equals("") + ? result56() + : nodeP250(); + } + + private Endpoint nodeP250() { + return cond42() + ? nodeP537() + : nodeP251(); + } + + private Endpoint nodeP251() { + return cond48() + ? result42() + : nodeP252(); + } + + private Endpoint nodeP252() { + return cond49() + ? result44() + : nodeP253(); + } + + private Endpoint nodeP253() { + return cond51() + ? nodeP254() + : nodeP525(); + } + + private Endpoint nodeP254() { + return cond60() + ? nodeP257() + : nodeP255(); + } + + private Endpoint nodeP255() { + return cond62() + ? result54() + : nodeP256(); + } + + private Endpoint nodeP256() { + return cond63() + ? nodeP258() + : result45(); + } + + private Endpoint nodeP257() { + return cond62() + ? result54() + : nodeP258(); + } + + private Endpoint nodeP258() { + return cond64() + ? nodeP259() + : result53(); + } + + private Endpoint nodeP259() { + return cond66() + ? nodeP260() + : result52(); + } + + private Endpoint nodeP260() { + return cond69() + ? nodeP261() + : result64(); + } + + private Endpoint nodeP261() { + return cond70() + ? nodeP262() + : result51(); + } + + private Endpoint nodeP262() { + return cond72() + ? result61() + : result50(); + } + + private Endpoint nodeP263() { + return cond25() + ? nodeP264() + : result41(); + } + + private Endpoint nodeP264() { + return region != null && region.equals("aws-global") + ? result30() + : result31(); + } + + private Endpoint nodeP265() { + return cond26() + ? result87() + : nodeP266(); + } + + private Endpoint nodeP266() { + return cond28() + ? result86() + : nodeP267(); + } + + private Endpoint nodeP267() { + return cond34() + ? nodeP268() + : nodeP543(); + } + + private Endpoint nodeP268() { + return region != null && region.equals("aws-global") + ? result92() + : result93(); + } + + private Endpoint nodeP269() { + return cond8() + ? nodeP396() + : result8(); + } + + private Endpoint nodeP270() { + return cond8() + ? nodeP406() + : result8(); + } + + private Endpoint nodeP271() { + return Boolean.TRUE.equals(params.useDualStack()) + ? nodeP345() + : nodeP272(); + } + + private Endpoint nodeP272() { + return params.endpoint() != null + ? result2() + : nodeP273(); + } + + private Endpoint nodeP273() { + return params.bucket() != null + ? nodeP283() + : nodeP274(); + } + + private Endpoint nodeP274() { + return cond8() + ? nodeP275() + : result114(); + } + + private Endpoint nodeP275() { + return cond15() + ? result4() + : nodeP276(); + } + + private Endpoint nodeP276() { + return cond16() + ? nodeP277() + : nodeP280(); + } + + private Endpoint nodeP277() { + return cond18() + ? nodeP278() + : nodeP280(); + } + + private Endpoint nodeP278() { + return cond19() + ? nodeP279() + : nodeP280(); + } + + private Endpoint nodeP279() { + return cond22() + ? result13() + : nodeP280(); + } + + private Endpoint nodeP280() { + return cond35() + ? nodeP281() + : result41(); + } + + private Endpoint nodeP281() { + return cond36() + ? result101() + : nodeP282(); + } + + private Endpoint nodeP282() { + return region != null && region.equals("aws-global") + ? result105() + : result106(); + } + + private Endpoint nodeP283() { + return cond6() + ? nodeP404() + : nodeP284(); + } + + private Endpoint nodeP284() { + return cond7() + ? nodeP394() + : nodeP285(); + } + + private Endpoint nodeP285() { + return cond8() + ? nodeP294() + : nodeP286(); + } + + private Endpoint nodeP286() { + return Boolean.TRUE.equals(params.forcePathStyle()) + ? nodeP500() + : nodeP287(); + } + + private Endpoint nodeP287() { + return cond26() + ? nodeP288() + : nodeP501(); + } + + private Endpoint nodeP288() { + return cond37() + ? nodeP289() + : result85(); + } + + private Endpoint nodeP289() { + return arnType != null && arnType.equals("") + ? result85() + : nodeP290(); + } + + private Endpoint nodeP290() { + return arnType != null && arnType.equals("accesspoint") + ? nodeP291() + : nodeP306(); + } + + private Endpoint nodeP291() { + return cond40() + ? nodeP292() + : result56(); + } + + private Endpoint nodeP292() { + return accessPointName_ssa_1 != null && accessPointName_ssa_1.equals("") + ? result56() + : nodeP293(); + } + + private Endpoint nodeP293() { + return cond42() + ? nodeP334() + : nodeP499(); + } + + private Endpoint nodeP294() { + return cond9() + ? nodeP295() + : nodeP299(); + } + + private Endpoint nodeP295() { + return accessPointSuffix != null && accessPointSuffix.equals("--op-s3") + ? nodeP296() + : nodeP299(); + } + + private Endpoint nodeP296() { + return cond11() + ? nodeP297() + : nodeP299(); + } + + private Endpoint nodeP297() { + return cond12() + ? nodeP298() + : nodeP299(); + } + + private Endpoint nodeP298() { + return cond13() + ? nodeP393() + : nodeP299(); + } + + private Endpoint nodeP299() { + return Boolean.TRUE.equals(params.forcePathStyle()) + ? nodeP338() + : nodeP300(); + } + + private Endpoint nodeP300() { + return cond15() + ? result4() + : nodeP301(); + } + + private Endpoint nodeP301() { + return cond20() + ? nodeP336() + : nodeP302(); + } + + private Endpoint nodeP302() { + return cond26() + ? nodeP303() + : nodeP340(); + } + + private Endpoint nodeP303() { + return cond37() + ? nodeP304() + : result85(); + } + + private Endpoint nodeP304() { + return arnType != null && arnType.equals("") + ? result85() + : nodeP305(); + } + + private Endpoint nodeP305() { + return arnType != null && arnType.equals("accesspoint") + ? nodeP308() + : nodeP306(); + } + + private Endpoint nodeP306() { + return cond48() + ? result57() + : nodeP307(); + } + + private Endpoint nodeP307() { + return cond50() + ? result73() + : result84(); + } + + private Endpoint nodeP308() { + return cond40() + ? nodeP309() + : result56(); + } + + private Endpoint nodeP309() { + return accessPointName_ssa_1 != null && accessPointName_ssa_1.equals("") + ? result56() + : nodeP310(); + } + + private Endpoint nodeP310() { + return cond42() + ? nodeP334() + : nodeP311(); + } + + private Endpoint nodeP311() { + return cond48() + ? nodeP323() + : nodeP312(); + } + + private Endpoint nodeP312() { + return cond49() + ? result44() + : nodeP313(); + } + + private Endpoint nodeP313() { + return cond51() + ? nodeP314() + : nodeP525(); + } + + private Endpoint nodeP314() { + return cond60() + ? nodeP317() + : nodeP315(); + } + + private Endpoint nodeP315() { + return cond62() + ? result54() + : nodeP316(); + } + + private Endpoint nodeP316() { + return cond63() + ? nodeP318() + : result45(); + } + + private Endpoint nodeP317() { + return cond62() + ? result54() + : nodeP318(); + } + + private Endpoint nodeP318() { + return cond64() + ? nodeP319() + : result53(); + } + + private Endpoint nodeP319() { + return cond66() + ? nodeP320() + : result52(); + } + + private Endpoint nodeP320() { + return cond69() + ? nodeP321() + : result64(); + } + + private Endpoint nodeP321() { + return cond70() + ? nodeP322() + : result51(); + } + + private Endpoint nodeP322() { + return cond72() + ? result60() + : result50(); + } + + private Endpoint nodeP323() { + return cond49() + ? result44() + : nodeP324(); + } + + private Endpoint nodeP324() { + return cond51() + ? nodeP325() + : nodeP525(); + } + + private Endpoint nodeP325() { + return cond60() + ? nodeP328() + : nodeP326(); + } + + private Endpoint nodeP326() { + return cond62() + ? result54() + : nodeP327(); + } + + private Endpoint nodeP327() { + return cond63() + ? nodeP329() + : result45(); + } + + private Endpoint nodeP328() { + return cond62() + ? result54() + : nodeP329(); + } + + private Endpoint nodeP329() { + return cond64() + ? nodeP330() + : result53(); + } + + private Endpoint nodeP330() { + return cond66() + ? nodeP331() + : result52(); + } + + private Endpoint nodeP331() { + return cond68() + ? result46() + : nodeP332(); + } + + private Endpoint nodeP332() { + return cond70() + ? nodeP333() + : result51(); + } + + private Endpoint nodeP333() { + return cond72() + ? result48() + : result50(); + } + + private Endpoint nodeP334() { + return cond48() + ? result55() + : nodeP335(); + } + + private Endpoint nodeP335() { + return cond52() + ? result66() + : result71(); + } + + private Endpoint nodeP336() { + return cond25() + ? nodeP337() + : result41(); + } + + private Endpoint nodeP337() { + return region != null && region.equals("aws-global") + ? result26() + : result27(); + } + + private Endpoint nodeP338() { + return cond15() + ? result4() + : nodeP339(); + } + + private Endpoint nodeP339() { + return cond26() + ? result87() + : nodeP340(); + } + + private Endpoint nodeP340() { + return cond28() + ? result86() + : nodeP341(); + } + + private Endpoint nodeP341() { + return cond34() + ? nodeP344() + : nodeP342(); + } + + private Endpoint nodeP342() { + return cond35() + ? nodeP343() + : nodeP544(); + } + + private Endpoint nodeP343() { + return cond36() + ? result101() + : result114(); + } + + private Endpoint nodeP344() { + return region != null && region.equals("aws-global") + ? result90() + : result91(); + } + + private Endpoint nodeP345() { + return params.endpoint() != null + ? result1() + : nodeP346(); + } + + private Endpoint nodeP346() { + return params.bucket() != null + ? nodeP356() + : nodeP347(); + } + + private Endpoint nodeP347() { + return cond8() + ? nodeP348() + : result114(); + } + + private Endpoint nodeP348() { + return cond15() + ? result4() + : nodeP349(); + } + + private Endpoint nodeP349() { + return cond16() + ? nodeP350() + : nodeP353(); + } + + private Endpoint nodeP350() { + return cond18() + ? nodeP351() + : nodeP353(); + } + + private Endpoint nodeP351() { + return cond19() + ? nodeP352() + : nodeP353(); + } + + private Endpoint nodeP352() { + return cond22() + ? result13() + : nodeP353(); + } + + private Endpoint nodeP353() { + return cond35() + ? nodeP354() + : result41(); + } + + private Endpoint nodeP354() { + return cond36() + ? result42() + : nodeP355(); + } + + private Endpoint nodeP355() { + return region != null && region.equals("aws-global") + ? result103() + : result104(); + } + + private Endpoint nodeP356() { + return cond6() + ? nodeP404() + : nodeP357(); + } + + private Endpoint nodeP357() { + return cond7() + ? nodeP394() + : nodeP358(); + } + + private Endpoint nodeP358() { + return cond8() + ? nodeP359() + : nodeP490(); + } + + private Endpoint nodeP359() { + return cond9() + ? nodeP360() + : nodeP364(); + } + + private Endpoint nodeP360() { + return accessPointSuffix != null && accessPointSuffix.equals("--op-s3") + ? nodeP361() + : nodeP364(); + } + + private Endpoint nodeP361() { + return cond11() + ? nodeP362() + : nodeP364(); + } + + private Endpoint nodeP362() { + return cond12() + ? nodeP363() + : nodeP364(); + } + + private Endpoint nodeP363() { + return cond13() + ? nodeP393() + : nodeP364(); + } + + private Endpoint nodeP364() { + return Boolean.TRUE.equals(params.forcePathStyle()) + ? nodeP388() + : nodeP365(); + } + + private Endpoint nodeP365() { + return cond15() + ? result4() + : nodeP366(); + } + + private Endpoint nodeP366() { + return cond20() + ? nodeP386() + : nodeP367(); + } + + private Endpoint nodeP367() { + return cond26() + ? nodeP368() + : nodeP390(); + } + + private Endpoint nodeP368() { + return cond37() + ? nodeP369() + : result85(); + } + + private Endpoint nodeP369() { + return arnType != null && arnType.equals("") + ? result85() + : nodeP370(); + } + + private Endpoint nodeP370() { + return arnType != null && arnType.equals("accesspoint") + ? nodeP371() + : nodeP517(); + } + + private Endpoint nodeP371() { + return cond40() + ? nodeP372() + : result56(); + } + + private Endpoint nodeP372() { + return accessPointName_ssa_1 != null && accessPointName_ssa_1.equals("") + ? result56() + : nodeP373(); + } + + private Endpoint nodeP373() { + return cond42() + ? nodeP537() + : nodeP374(); + } + + private Endpoint nodeP374() { + return cond48() + ? result42() + : nodeP375(); + } + + private Endpoint nodeP375() { + return cond49() + ? result44() + : nodeP376(); + } + + private Endpoint nodeP376() { + return cond51() + ? nodeP377() + : nodeP525(); + } + + private Endpoint nodeP377() { + return cond60() + ? nodeP380() + : nodeP378(); + } + + private Endpoint nodeP378() { + return cond62() + ? result54() + : nodeP379(); + } + + private Endpoint nodeP379() { + return cond63() + ? nodeP381() + : result45(); + } + + private Endpoint nodeP380() { + return cond62() + ? result54() + : nodeP381(); + } + + private Endpoint nodeP381() { + return cond64() + ? nodeP382() + : result53(); + } + + private Endpoint nodeP382() { + return cond66() + ? nodeP383() + : result52(); + } + + private Endpoint nodeP383() { + return cond69() + ? nodeP384() + : result64(); + } + + private Endpoint nodeP384() { + return cond70() + ? nodeP385() + : result51(); + } + + private Endpoint nodeP385() { + return cond72() + ? result59() + : result50(); + } + + private Endpoint nodeP386() { + return cond25() + ? nodeP387() + : result41(); + } + + private Endpoint nodeP387() { + return region != null && region.equals("aws-global") + ? result24() + : result25(); + } + + private Endpoint nodeP388() { + return cond15() + ? result4() + : nodeP389(); + } + + private Endpoint nodeP389() { + return cond26() + ? result87() + : nodeP390(); + } + + private Endpoint nodeP390() { + return cond28() + ? result86() + : nodeP391(); + } + + private Endpoint nodeP391() { + return cond34() + ? nodeP392() + : nodeP543(); + } + + private Endpoint nodeP392() { + return region != null && region.equals("aws-global") + ? result88() + : result89(); + } + + private Endpoint nodeP393() { + return cond15() + ? result4() + : nodeP546(); + } + + private Endpoint nodeP394() { + return cond8() + ? nodeP395() + : result8(); + } + + private Endpoint nodeP395() { + return cond15() + ? result4() + : nodeP396(); + } + + private Endpoint nodeP396() { + return cond16() + ? nodeP397() + : nodeP409(); + } + + private Endpoint nodeP397() { + return cond18() + ? nodeP398() + : nodeP409(); + } + + private Endpoint nodeP398() { + return cond19() + ? nodeP399() + : nodeP409(); + } + + private Endpoint nodeP399() { + return cond20() + ? nodeP400() + : result8(); + } + + private Endpoint nodeP400() { + return cond27() + ? nodeP401() + : result11(); + } + + private Endpoint nodeP401() { + return cond29() + ? result10() + : nodeP402(); + } + + private Endpoint nodeP402() { + return cond31() + ? result10() + : nodeP403(); + } + + private Endpoint nodeP403() { + return cond32() + ? result10() + : nodeP421(); + } + + private Endpoint nodeP404() { + return cond8() + ? nodeP405() + : result8(); + } + + private Endpoint nodeP405() { + return cond15() + ? result4() + : nodeP406(); + } + + private Endpoint nodeP406() { + return cond16() + ? nodeP407() + : nodeP409(); + } + + private Endpoint nodeP407() { + return cond18() + ? nodeP408() + : nodeP409(); + } + + private Endpoint nodeP408() { + return cond19() + ? nodeP410() + : nodeP409(); + } + + private Endpoint nodeP409() { + return cond20() + ? result11() + : result8(); + } + + private Endpoint nodeP410() { + return cond20() + ? nodeP413() + : nodeP411(); + } + + private Endpoint nodeP411() { + return cond22() + ? nodeP412() + : result8(); + } + + private Endpoint nodeP412() { + return cond34() + ? result9() + : result8(); + } + + private Endpoint nodeP413() { + return cond22() + ? nodeP415() + : nodeP414(); + } + + private Endpoint nodeP414() { + return cond27() + ? nodeP418() + : result11(); + } + + private Endpoint nodeP415() { + return cond27() + ? nodeP417() + : nodeP416(); + } + + private Endpoint nodeP416() { + return cond34() + ? result9() + : result11(); + } + + private Endpoint nodeP417() { + return cond34() + ? result9() + : nodeP418(); + } + + private Endpoint nodeP418() { + return cond43() + ? result10() + : nodeP419(); + } + + private Endpoint nodeP419() { + return cond47() + ? result10() + : nodeP420(); + } + + private Endpoint nodeP420() { + return cond53() + ? result10() + : nodeP421(); + } + + private Endpoint nodeP421() { + return cond54() + ? result10() + : nodeP422(); + } + + private Endpoint nodeP422() { + return cond56() + ? result10() + : result11(); + } + + private Endpoint nodeP423() { + return Boolean.TRUE.equals(params.useFips()) + ? result0() + : nodeP424(); + } + + private Endpoint nodeP424() { + return Boolean.TRUE.equals(params.useDualStack()) + ? nodeP477() + : nodeP425(); + } + + private Endpoint nodeP425() { + return params.endpoint() != null + ? result3() + : nodeP426(); + } + + private Endpoint nodeP426() { + return params.bucket() != null + ? nodeP437() + : nodeP427(); + } + + private Endpoint nodeP427() { + return cond8() + ? nodeP428() + : result114(); + } + + private Endpoint nodeP428() { + return cond16() + ? nodeP429() + : nodeP432(); + } + + private Endpoint nodeP429() { + return cond18() + ? nodeP430() + : nodeP432(); + } + + private Endpoint nodeP430() { + return cond19() + ? nodeP431() + : nodeP432(); + } + + private Endpoint nodeP431() { + return cond22() + ? result13() + : nodeP432(); + } + + private Endpoint nodeP432() { + return cond35() + ? nodeP433() + : result41(); + } + + private Endpoint nodeP433() { + return cond36() + ? result43() + : nodeP434(); + } + + private Endpoint nodeP434() { + return region != null && region.equals("aws-global") + ? result111() + : nodeP435(); + } + + private Endpoint nodeP435() { + return Boolean.TRUE.equals(params.useGlobalEndpoint()) + ? nodeP436() + : result113(); + } + + private Endpoint nodeP436() { + return region != null && region.equals("us-east-1") + ? result112() + : result113(); + } + + private Endpoint nodeP437() { + return cond6() + ? result5() + : nodeP438(); + } + + private Endpoint nodeP438() { + return cond7() + ? result5() + : nodeP439(); + } + + private Endpoint nodeP439() { + return cond8() + ? nodeP449() + : nodeP440(); + } + + private Endpoint nodeP440() { + return Boolean.TRUE.equals(params.forcePathStyle()) + ? nodeP500() + : nodeP441(); + } + + private Endpoint nodeP441() { + return cond26() + ? nodeP442() + : nodeP501(); + } + + private Endpoint nodeP442() { + return cond37() + ? nodeP443() + : result85(); + } + + private Endpoint nodeP443() { + return arnType != null && arnType.equals("") + ? result85() + : nodeP444(); + } + + private Endpoint nodeP444() { + return arnType != null && arnType.equals("accesspoint") + ? nodeP445() + : nodeP464(); + } + + private Endpoint nodeP445() { + return cond40() + ? nodeP446() + : result56(); + } + + private Endpoint nodeP446() { + return accessPointName_ssa_1 != null && accessPointName_ssa_1.equals("") + ? result56() + : nodeP447(); + } + + private Endpoint nodeP447() { + return cond42() + ? nodeP470() + : nodeP448(); + } + + private Endpoint nodeP448() { + return cond48() + ? result43() + : nodeP499(); + } + + private Endpoint nodeP449() { + return cond9() + ? nodeP450() + : nodeP454(); + } + + private Endpoint nodeP450() { + return accessPointSuffix != null && accessPointSuffix.equals("--op-s3") + ? nodeP451() + : nodeP454(); + } + + private Endpoint nodeP451() { + return cond11() + ? nodeP452() + : nodeP454(); + } + + private Endpoint nodeP452() { + return cond12() + ? nodeP453() + : nodeP454(); + } + + private Endpoint nodeP453() { + return cond13() + ? nodeP546() + : nodeP454(); + } + + private Endpoint nodeP454() { + return Boolean.TRUE.equals(params.forcePathStyle()) + ? nodeP472() + : nodeP455(); + } + + private Endpoint nodeP455() { + return cond15() + ? nodeP459() + : nodeP456(); + } + + private Endpoint nodeP456() { + return cond20() + ? nodeP457() + : nodeP460(); + } + + private Endpoint nodeP457() { + return cond25() + ? nodeP458() + : result41(); + } + + private Endpoint nodeP458() { + return region != null && region.equals("aws-global") + ? result36() + : result37(); + } + + private Endpoint nodeP459() { + return cond20() + ? nodeP539() + : nodeP460(); + } + + private Endpoint nodeP460() { + return cond26() + ? nodeP461() + : nodeP473(); + } + + private Endpoint nodeP461() { + return cond37() + ? nodeP462() + : result85(); + } + + private Endpoint nodeP462() { + return arnType != null && arnType.equals("") + ? result85() + : nodeP463(); + } + + private Endpoint nodeP463() { + return arnType != null && arnType.equals("accesspoint") + ? nodeP466() + : nodeP464(); + } + + private Endpoint nodeP464() { + return cond48() + ? result57() + : nodeP465(); + } + + private Endpoint nodeP465() { + return cond50() + ? result74() + : result84(); + } + + private Endpoint nodeP466() { + return cond40() + ? nodeP467() + : result56(); + } + + private Endpoint nodeP467() { + return accessPointName_ssa_1 != null && accessPointName_ssa_1.equals("") + ? result56() + : nodeP468(); + } + + private Endpoint nodeP468() { + return cond42() + ? nodeP470() + : nodeP469(); + } + + private Endpoint nodeP469() { + return cond48() + ? result43() + : nodeP523(); + } + + private Endpoint nodeP470() { + return cond48() + ? result43() + : nodeP471(); + } + + private Endpoint nodeP471() { + return cond52() + ? result67() + : result71(); + } + + private Endpoint nodeP472() { + return cond26() + ? result87() + : nodeP473(); + } + + private Endpoint nodeP473() { + return cond28() + ? result86() + : nodeP474(); + } + + private Endpoint nodeP474() { + return cond34() + ? result99() + : nodeP475(); + } + + private Endpoint nodeP475() { + return cond35() + ? nodeP476() + : nodeP544(); + } + + private Endpoint nodeP476() { + return cond36() + ? result43() + : result114(); + } + + private Endpoint nodeP477() { + return params.endpoint() != null + ? result1() + : nodeP478(); + } + + private Endpoint nodeP478() { + return params.bucket() != null + ? nodeP487() + : nodeP479(); + } + + private Endpoint nodeP479() { + return cond8() + ? nodeP480() + : result114(); + } + + private Endpoint nodeP480() { + return cond16() + ? nodeP481() + : nodeP484(); + } + + private Endpoint nodeP481() { + return cond18() + ? nodeP482() + : nodeP484(); + } + + private Endpoint nodeP482() { + return cond19() + ? nodeP483() + : nodeP484(); + } + + private Endpoint nodeP483() { + return cond22() + ? result13() + : nodeP484(); + } + + private Endpoint nodeP484() { + return cond35() + ? nodeP485() + : result41(); + } + + private Endpoint nodeP485() { + return cond36() + ? result42() + : nodeP486(); + } + + private Endpoint nodeP486() { + return region != null && region.equals("aws-global") + ? result107() + : result108(); + } + + private Endpoint nodeP487() { + return cond6() + ? result5() + : nodeP488(); + } + + private Endpoint nodeP488() { + return cond7() + ? result5() + : nodeP489(); + } + + private Endpoint nodeP489() { + return cond8() + ? nodeP502() + : nodeP490(); + } + + private Endpoint nodeP490() { + return Boolean.TRUE.equals(params.forcePathStyle()) + ? nodeP500() + : nodeP491(); + } + + private Endpoint nodeP491() { + return cond26() + ? nodeP492() + : nodeP501(); + } + + private Endpoint nodeP492() { + return cond37() + ? nodeP493() + : result85(); + } + + private Endpoint nodeP493() { + return arnType != null && arnType.equals("") + ? result85() + : nodeP494(); + } + + private Endpoint nodeP494() { + return arnType != null && arnType.equals("accesspoint") + ? nodeP495() + : nodeP517(); + } + + private Endpoint nodeP495() { + return cond40() + ? nodeP496() + : result56(); + } + + private Endpoint nodeP496() { + return accessPointName_ssa_1 != null && accessPointName_ssa_1.equals("") + ? result56() + : nodeP497(); + } + + private Endpoint nodeP497() { + return cond42() + ? nodeP537() + : nodeP498(); + } + + private Endpoint nodeP498() { + return cond48() + ? result42() + : nodeP499(); + } + + private Endpoint nodeP499() { + return cond49() + ? result44() + : nodeP525(); + } + + private Endpoint nodeP500() { + return cond26() + ? result87() + : nodeP501(); + } + + private Endpoint nodeP501() { + return cond28() + ? result86() + : result114(); + } + + private Endpoint nodeP502() { + return cond9() + ? nodeP503() + : nodeP507(); + } + + private Endpoint nodeP503() { + return accessPointSuffix != null && accessPointSuffix.equals("--op-s3") + ? nodeP504() + : nodeP507(); + } + + private Endpoint nodeP504() { + return cond11() + ? nodeP505() + : nodeP507(); + } + + private Endpoint nodeP505() { + return cond12() + ? nodeP506() + : nodeP507(); + } + + private Endpoint nodeP506() { + return cond13() + ? nodeP546() + : nodeP507(); + } + + private Endpoint nodeP507() { + return Boolean.TRUE.equals(params.forcePathStyle()) + ? nodeP540() + : nodeP508(); + } + + private Endpoint nodeP508() { + return cond15() + ? nodeP512() + : nodeP509(); + } + + private Endpoint nodeP509() { + return cond20() + ? nodeP510() + : nodeP513(); + } + + private Endpoint nodeP510() { + return cond25() + ? nodeP511() + : result41(); + } + + private Endpoint nodeP511() { + return region != null && region.equals("aws-global") + ? result28() + : result29(); + } + + private Endpoint nodeP512() { + return cond20() + ? nodeP539() + : nodeP513(); + } + + private Endpoint nodeP513() { + return cond26() + ? nodeP514() + : nodeP541(); + } + + private Endpoint nodeP514() { + return cond37() + ? nodeP515() + : result85(); + } + + private Endpoint nodeP515() { + return arnType != null && arnType.equals("") + ? result85() + : nodeP516(); + } + + private Endpoint nodeP516() { + return arnType != null && arnType.equals("accesspoint") + ? nodeP519() + : nodeP517(); + } + + private Endpoint nodeP517() { + return cond48() + ? result57() + : nodeP518(); + } + + private Endpoint nodeP518() { + return cond50() + ? result72() + : result84(); + } + + private Endpoint nodeP519() { + return cond40() + ? nodeP520() + : result56(); + } + + private Endpoint nodeP520() { + return accessPointName_ssa_1 != null && accessPointName_ssa_1.equals("") + ? result56() + : nodeP521(); + } + + private Endpoint nodeP521() { + return cond42() + ? nodeP537() + : nodeP522(); + } + + private Endpoint nodeP522() { + return cond48() + ? result42() + : nodeP523(); + } + + private Endpoint nodeP523() { + return cond49() + ? result44() + : nodeP524(); + } + + private Endpoint nodeP524() { + return cond51() + ? nodeP528() + : nodeP525(); + } + + private Endpoint nodeP525() { + return cond60() + ? result54() + : nodeP526(); + } + + private Endpoint nodeP526() { + return cond62() + ? result54() + : nodeP527(); + } + + private Endpoint nodeP527() { + return cond63() + ? result54() + : result45(); + } + + private Endpoint nodeP528() { + return cond60() + ? nodeP531() + : nodeP529(); + } + + private Endpoint nodeP529() { + return cond62() + ? result54() + : nodeP530(); + } + + private Endpoint nodeP530() { + return cond63() + ? nodeP532() + : result45(); + } + + private Endpoint nodeP531() { + return cond62() + ? result54() + : nodeP532(); + } + + private Endpoint nodeP532() { + return cond64() + ? nodeP533() + : result53(); + } + + private Endpoint nodeP533() { + return cond66() + ? nodeP534() + : result52(); + } + + private Endpoint nodeP534() { + return cond69() + ? nodeP535() + : result64(); + } + + private Endpoint nodeP535() { + return cond70() + ? nodeP536() + : result51(); + } + + private Endpoint nodeP536() { + return cond72() + ? result58() + : result50(); + } + + private Endpoint nodeP537() { + return cond48() + ? result42() + : nodeP538(); + } + + private Endpoint nodeP538() { + return cond52() + ? result65() + : result71(); + } + + private Endpoint nodeP539() { + return cond25() + ? result23() + : result41(); + } + + private Endpoint nodeP540() { + return cond26() + ? result87() + : nodeP541(); + } + + private Endpoint nodeP541() { + return cond28() + ? result86() + : nodeP542(); + } + + private Endpoint nodeP542() { + return cond34() + ? result99() + : nodeP543(); + } + + private Endpoint nodeP543() { + return cond35() + ? nodeP545() + : nodeP544(); + } + + private Endpoint nodeP544() { + return cond36() + ? result41() + : result114(); + } + + private Endpoint nodeP545() { + return cond36() + ? result42() + : result114(); + } + + private Endpoint nodeP546() { + return cond17() + ? nodeP547() + : result21(); + } + + private Endpoint nodeP547() { + return cond20() + ? nodeP548() + : result20(); + } + + private Endpoint nodeP548() { + return regionPrefix != null && regionPrefix.equals("beta") + ? nodeP551() + : nodeP549(); + } + + private Endpoint nodeP549() { + return hardwareType != null && hardwareType.equals("e") + ? result16() + : nodeP550(); + } + + private Endpoint nodeP550() { + return hardwareType != null && hardwareType.equals("o") + ? result18() + : result19(); + } + + private Endpoint nodeP551() { + return hardwareType != null && hardwareType.equals("e") + ? result14() + : nodeP552(); + } + + private Endpoint nodeP552() { + return hardwareType != null && hardwareType.equals("o") + ? result14() + : result19(); + } + + private boolean cond6() { + return (RulesFunctions.substringEquals(params.bucket(), 0, 6, true, "--x-s3")); + } + + private boolean cond7() { + return (RulesFunctions.substringEquals(params.bucket(), 0, 7, true, "--xa-s3")); + } + + private boolean cond8() { + partitionResult = RulesFunctions.awsPartition(region); + return partitionResult != null; + } + + private boolean cond9() { + accessPointSuffix = RulesFunctions.substring(params.bucket(), 0, 7, true); + return accessPointSuffix != null; + } + + private boolean cond11() { + regionPrefix = RulesFunctions.substring(params.bucket(), 8, 12, true); + return regionPrefix != null; + } + + private boolean cond12() { + outpostId_ssa_2 = RulesFunctions.substring(params.bucket(), 32, 49, true); + return outpostId_ssa_2 != null; + } + + private boolean cond13() { + hardwareType = RulesFunctions.substring(params.bucket(), 49, 50, true); + return hardwareType != null; + } + + private boolean cond15() { + return ("aws-cn".equals(partitionResult.name())); + } + + private boolean cond16() { + _s3e_ds = (params.useDualStack() ? ".dualstack" : ""); + return true; + } + + private boolean cond17() { + return (RulesFunctions.isValidHostLabelSingle(outpostId_ssa_2)); + } + + private boolean cond18() { + _s3e_fips = (params.useFips() ? "-fips" : ""); + return true; + } + + private boolean cond19() { + _s3e_auth = (Boolean.TRUE.equals(params.disableS3ExpressSessionAuth()) ? "sigv4" : "sigv4-s3express"); + return true; + } + + private boolean cond20() { + return (RulesFunctions.awsIsVirtualHostableS3Bucket(params.bucket(), false)); + } + + private boolean cond21() { + url = RulesFunctions.parseURL(params.endpoint()); + return url != null; + } + + private boolean cond22() { + return (Boolean.TRUE.equals(params.useS3ExpressControlEndpoint())); + } + + private boolean cond23() { + return (RulesFunctions.awsIsVirtualHostableS3Bucket(params.bucket(), true)); + } + + private boolean cond24() { + return ("http".equals(url.scheme())); + } + + private boolean cond25() { + return (RulesFunctions.isValidHostLabelSingle(region)); + } + + private boolean cond26() { + bucketArn = RulesFunctions.awsParseArn(params.bucket()); + return bucketArn != null; + } + + private boolean cond27() { + s3expressAvailabilityZoneId = RulesFunctions.listAccess(RulesFunctions.split(params.bucket(), "--", 0), -2); + return s3expressAvailabilityZoneId != null; + } + + private boolean cond28() { + return (RulesFunctions.substringEquals(params.bucket(), 0, 4, false, "arn:")); + } + + private boolean cond29() { + return (RulesFunctions.substringEquals(params.bucket(), 16, 18, true, "--")); + } + + private boolean cond30() { + return (url.isIp()); + } + + private boolean cond31() { + return (RulesFunctions.substringEquals(params.bucket(), 21, 23, true, "--")); + } + + private boolean cond32() { + return (RulesFunctions.substringEquals(params.bucket(), 27, 29, true, "--")); + } + + private boolean cond34() { + uri_encoded_bucket = RulesFunctions.uriEncode(params.bucket()); + return uri_encoded_bucket != null; + } + + private boolean cond35() { + return (RulesFunctions.isValidHostLabelMulti(region)); + } + + private boolean cond36() { + return (Boolean.TRUE.equals(params.useObjectLambdaEndpoint())); + } + + private boolean cond37() { + arnType = RulesFunctions.listAccess(bucketArn.resourceId(), 0); + return arnType != null; + } + + private boolean cond40() { + accessPointName_ssa_1 = RulesFunctions.listAccess(bucketArn.resourceId(), 1); + return accessPointName_ssa_1 != null; + } + + private boolean cond42() { + return ("".equals(bucketArn.region())); + } + + private boolean cond43() { + return (RulesFunctions.substringEquals(params.bucket(), 14, 16, true, "--")); + } + + private boolean cond47() { + return (RulesFunctions.substringEquals(params.bucket(), 19, 21, true, "--")); + } + + private boolean cond48() { + return ("s3-object-lambda".equals(bucketArn.service())); + } + + private boolean cond49() { + return (Boolean.TRUE.equals(params.disableAccessPoints())); + } + + private boolean cond50() { + return ("s3-outposts".equals(bucketArn.service())); + } + + private boolean cond51() { + bucketPartition = RulesFunctions.awsPartition(bucketArn.region()); + return bucketPartition != null; + } + + private boolean cond52() { + return (RulesFunctions.isValidHostLabelMulti(accessPointName_ssa_1)); + } + + private boolean cond53() { + return (RulesFunctions.substringEquals(params.bucket(), 26, 28, true, "--")); + } + + private boolean cond54() { + return (RulesFunctions.substringEquals(params.bucket(), 15, 17, true, "--")); + } + + private boolean cond55() { + return (RulesFunctions.listAccess(bucketArn.resourceId(), 4) != null); + } + + private boolean cond56() { + return (RulesFunctions.substringEquals(params.bucket(), 20, 22, true, "--")); + } + + private boolean cond59() { + outpostId_ssa_1 = RulesFunctions.listAccess(bucketArn.resourceId(), 1); + return outpostId_ssa_1 != null; + } + + private boolean cond60() { + return (!Boolean.FALSE.equals(params.useArnRegion())); + } + + private boolean cond61() { + return (RulesFunctions.isValidHostLabelSingle(outpostId_ssa_1)); + } + + private boolean cond62() { + outpostType = RulesFunctions.listAccess(bucketArn.resourceId(), 2); + return outpostType != null; + } + + private boolean cond63() { + return (RulesFunctions.stringEquals(region, bucketArn.region())); + } + + private boolean cond64() { + return (RulesFunctions.stringEquals(bucketPartition.name(), partitionResult.name())); + } + + private boolean cond66() { + return (RulesFunctions.isValidHostLabelMulti(bucketArn.region())); + } + + private boolean cond67() { + return (RulesFunctions.stringEquals(bucketArn.partition(), partitionResult.name())); + } + + private boolean cond68() { + return ("".equals(bucketArn.accountId())); + } + + private boolean cond69() { + return ("s3".equals(bucketArn.service())); + } + + private boolean cond70() { + return (RulesFunctions.isValidHostLabelSingle(bucketArn.accountId())); + } + + private boolean cond71() { + accessPointName_ssa_2 = RulesFunctions.listAccess(bucketArn.resourceId(), 3); + return accessPointName_ssa_2 != null; + } + + private boolean cond72() { + return (RulesFunctions.isValidHostLabelSingle(accessPointName_ssa_1)); + } + + private boolean cond74() { + return (RulesFunctions.isValidHostLabelSingle(accessPointName_ssa_2)); + } + + private Endpoint result0() { + throw SdkClientException.create("Accelerate cannot be used with FIPS"); + } + + private Endpoint result1() { + throw SdkClientException.create("Cannot set dual-stack in combination with a custom endpoint."); + } + + private Endpoint result2() { + throw SdkClientException.create("A custom endpoint cannot be combined with FIPS"); + } + + private Endpoint result3() { + throw SdkClientException.create("A custom endpoint cannot be combined with S3 Accelerate"); + } + + private Endpoint result4() { + throw SdkClientException.create("Partition does not support FIPS"); + } + + private Endpoint result5() { + throw SdkClientException.create("S3Express does not support S3 Accelerate."); + } + + private Endpoint result6() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromString(url.scheme() + "://" + url.authority() + "/" + uri_encoded_bucket + url.path())).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(DynamicEndpointAuthSchemeFactory.builder().disableDoubleEncoding(true).signingName("s3express").signingRegion(region).create(_s3e_auth))).build(); + } + + private Endpoint result7() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromString(url.scheme() + "://" + params.bucket() + "." + url.authority() + url.path())).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(DynamicEndpointAuthSchemeFactory.builder().disableDoubleEncoding(true).signingName("s3express").signingRegion(region).create(_s3e_auth))).build(); + } + + private Endpoint result8() { + throw SdkClientException.create("S3Express bucket name is not a valid virtual hostable name."); + } + + private Endpoint result9() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", "s3express-control" + _s3e_fips + _s3e_ds + "." + region + "." + partitionResult.dnsSuffix(), -1, "/" + uri_encoded_bucket)).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3express").signingRegion(region).build())).build(); + } + + private Endpoint result10() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", params.bucket() + ".s3express" + _s3e_fips + "-" + s3expressAvailabilityZoneId + _s3e_ds + "." + region + "." + partitionResult.dnsSuffix(), -1, "")).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(DynamicEndpointAuthSchemeFactory.builder().disableDoubleEncoding(true).signingName("s3express").signingRegion(region).create(_s3e_auth))).build(); + } + + private Endpoint result11() { + throw SdkClientException.create("Unrecognized S3Express bucket name format."); + } + + private Endpoint result12() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromString(url.scheme() + "://" + url.authority() + url.path())).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(DynamicEndpointAuthSchemeFactory.builder().disableDoubleEncoding(true).signingName("s3express").signingRegion(region).create(_s3e_auth))).build(); + } + + private Endpoint result13() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", "s3express-control" + _s3e_fips + _s3e_ds + "." + region + "." + partitionResult.dnsSuffix(), -1, "")).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3express").signingRegion(region).build())).build(); + } + + private Endpoint result14() { + throw SdkClientException.create("Expected a endpoint to be specified but no endpoint was found"); + } + + private Endpoint result15() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", params.bucket() + ".ec2." + url.authority(), -1, "")).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().disableDoubleEncoding(true).signingName("s3-outposts").signingRegionSet(Arrays.asList("*")).build(), SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3-outposts").signingRegion(region).build())).build(); + } + + private Endpoint result16() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", params.bucket() + ".ec2.s3-outposts." + region + "." + partitionResult.dnsSuffix(), -1, "")).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().disableDoubleEncoding(true).signingName("s3-outposts").signingRegionSet(Arrays.asList("*")).build(), SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3-outposts").signingRegion(region).build())).build(); + } + + private Endpoint result17() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", params.bucket() + ".op-" + outpostId_ssa_2 + "." + url.authority(), -1, "")).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().disableDoubleEncoding(true).signingName("s3-outposts").signingRegionSet(Arrays.asList("*")).build(), SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3-outposts").signingRegion(region).build())).build(); + } + + private Endpoint result18() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", params.bucket() + ".op-" + outpostId_ssa_2 + ".s3-outposts." + region + "." + partitionResult.dnsSuffix(), -1, "")).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().disableDoubleEncoding(true).signingName("s3-outposts").signingRegionSet(Arrays.asList("*")).build(), SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3-outposts").signingRegion(region).build())).build(); + } + + private Endpoint result19() { + throw SdkClientException.create("Unrecognized hardware type: \"Expected hardware type o or e but got " + hardwareType + "\""); + } + + private Endpoint result20() { + throw SdkClientException.create("Invalid Outposts Bucket alias - it must be a valid bucket name."); + } + + private Endpoint result21() { + throw SdkClientException.create("Invalid ARN: The outpost Id must only contain a-z, A-Z, 0-9 and `-`."); + } + + private Endpoint result22() { + throw SdkClientException.create("Custom endpoint `" + params.endpoint() + "` was not a valid URI"); + } + + private Endpoint result23() { + throw SdkClientException.create("S3 Accelerate cannot be used in this region"); + } + + private Endpoint result24() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", params.bucket() + ".s3-fips.dualstack.us-east-1." + partitionResult.dnsSuffix(), -1, "")).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion("us-east-1").build())).build(); + } + + private Endpoint result25() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", params.bucket() + ".s3-fips.dualstack." + region + "." + partitionResult.dnsSuffix(), -1, "")).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion(region).build())).build(); + } + + private Endpoint result26() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", params.bucket() + ".s3-fips.us-east-1." + partitionResult.dnsSuffix(), -1, "")).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion("us-east-1").build())).build(); + } + + private Endpoint result27() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", params.bucket() + ".s3-fips." + region + "." + partitionResult.dnsSuffix(), -1, "")).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion(region).build())).build(); + } + + private Endpoint result28() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", params.bucket() + ".s3-accelerate.dualstack.us-east-1." + partitionResult.dnsSuffix(), -1, "")).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion("us-east-1").build())).build(); + } + + private Endpoint result29() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", params.bucket() + ".s3-accelerate.dualstack." + partitionResult.dnsSuffix(), -1, "")).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion(region).build())).build(); + } + + private Endpoint result30() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", params.bucket() + ".s3.dualstack.us-east-1." + partitionResult.dnsSuffix(), -1, "")).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion("us-east-1").build())).build(); + } + + private Endpoint result31() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", params.bucket() + ".s3.dualstack." + region + "." + partitionResult.dnsSuffix(), -1, "")).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion(region).build())).build(); + } + + private Endpoint result32() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromString(url.scheme() + "://" + url.authority() + url.normalizedPath() + params.bucket())).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion("us-east-1").build())).build(); + } + + private Endpoint result33() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromString(url.scheme() + "://" + params.bucket() + "." + url.authority() + url.path())).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion("us-east-1").build())).build(); + } + + private Endpoint result34() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromString(url.scheme() + "://" + url.authority() + url.normalizedPath() + params.bucket())).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion(region).build())).build(); + } + + private Endpoint result35() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromString(url.scheme() + "://" + params.bucket() + "." + url.authority() + url.path())).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion(region).build())).build(); + } + + private Endpoint result36() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", params.bucket() + ".s3-accelerate." + partitionResult.dnsSuffix(), -1, "")).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion("us-east-1").build())).build(); + } + + private Endpoint result37() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", params.bucket() + ".s3-accelerate." + partitionResult.dnsSuffix(), -1, "")).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion(region).build())).build(); + } + + private Endpoint result38() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", params.bucket() + ".s3." + partitionResult.dnsSuffix(), -1, "")).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion("us-east-1").build())).build(); + } + + private Endpoint result39() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", params.bucket() + ".s3." + partitionResult.dnsSuffix(), -1, "")).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion(region).build())).build(); + } + + private Endpoint result40() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", params.bucket() + ".s3." + region + "." + partitionResult.dnsSuffix(), -1, "")).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion(region).build())).build(); + } + + private Endpoint result41() { + throw SdkClientException.create("Invalid region: region was not a valid DNS name."); + } + + private Endpoint result42() { + throw SdkClientException.create("S3 Object Lambda does not support Dual-stack"); + } + + private Endpoint result43() { + throw SdkClientException.create("S3 Object Lambda does not support S3 Accelerate"); + } + + private Endpoint result44() { + throw SdkClientException.create("Access points are not supported for this operation"); + } + + private Endpoint result45() { + throw SdkClientException.create("Invalid configuration: region from ARN `" + bucketArn.region() + "` does not match client region `" + region + "` and UseArnRegion is `false`"); + } + + private Endpoint result46() { + throw SdkClientException.create("Invalid ARN: Missing account id"); + } + + private Endpoint result47() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromString(url.scheme() + "://" + accessPointName_ssa_1 + "-" + bucketArn.accountId() + "." + url.authority() + url.path())).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3-object-lambda").signingRegion(bucketArn.region()).build())).build(); + } + + private Endpoint result48() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", accessPointName_ssa_1 + "-" + bucketArn.accountId() + ".s3-object-lambda-fips." + bucketArn.region() + "." + bucketPartition.dnsSuffix(), -1, "")).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3-object-lambda").signingRegion(bucketArn.region()).build())).build(); + } + + private Endpoint result49() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", accessPointName_ssa_1 + "-" + bucketArn.accountId() + ".s3-object-lambda." + bucketArn.region() + "." + bucketPartition.dnsSuffix(), -1, "")).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3-object-lambda").signingRegion(bucketArn.region()).build())).build(); + } + + private Endpoint result50() { + throw SdkClientException.create("Invalid ARN: The access point name may only contain a-z, A-Z, 0-9 and `-`. Found: `" + accessPointName_ssa_1 + "`"); + } + + private Endpoint result51() { + throw SdkClientException.create("Invalid ARN: The account id may only contain a-z, A-Z, 0-9 and `-`. Found: `" + bucketArn.accountId() + "`"); + } + + private Endpoint result52() { + throw SdkClientException.create("Invalid region in ARN: `" + bucketArn.region() + "` (invalid DNS name)"); + } + + private Endpoint result53() { + throw SdkClientException.create("Client was configured for partition `" + partitionResult.name() + "` but ARN (`" + params.bucket() + "`) has `" + bucketPartition.name() + "`"); + } + + private Endpoint result54() { + throw SdkClientException.create("Invalid ARN: The ARN may only contain a single resource component after `accesspoint`."); + } + + private Endpoint result55() { + throw SdkClientException.create("Invalid ARN: bucket ARN is missing a region"); + } + + private Endpoint result56() { + throw SdkClientException.create("Invalid ARN: Expected a resource of the format `accesspoint:` but no name was provided"); + } + + private Endpoint result57() { + throw SdkClientException.create("Invalid ARN: Object Lambda ARNs only support `accesspoint` arn types, but found: `" + arnType + "`"); + } + + private Endpoint result58() { + throw SdkClientException.create("Access Points do not support S3 Accelerate"); + } + + private Endpoint result59() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", accessPointName_ssa_1 + "-" + bucketArn.accountId() + ".s3-accesspoint-fips.dualstack." + bucketArn.region() + "." + bucketPartition.dnsSuffix(), -1, "")).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion(bucketArn.region()).build())).build(); + } + + private Endpoint result60() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", accessPointName_ssa_1 + "-" + bucketArn.accountId() + ".s3-accesspoint-fips." + bucketArn.region() + "." + bucketPartition.dnsSuffix(), -1, "")).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion(bucketArn.region()).build())).build(); + } + + private Endpoint result61() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", accessPointName_ssa_1 + "-" + bucketArn.accountId() + ".s3-accesspoint.dualstack." + bucketArn.region() + "." + bucketPartition.dnsSuffix(), -1, "")).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion(bucketArn.region()).build())).build(); + } + + private Endpoint result62() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromString(url.scheme() + "://" + accessPointName_ssa_1 + "-" + bucketArn.accountId() + "." + url.authority() + url.path())).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion(bucketArn.region()).build())).build(); + } + + private Endpoint result63() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", accessPointName_ssa_1 + "-" + bucketArn.accountId() + ".s3-accesspoint." + bucketArn.region() + "." + bucketPartition.dnsSuffix(), -1, "")).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion(bucketArn.region()).build())).build(); + } + + private Endpoint result64() { + throw SdkClientException.create("Invalid ARN: The ARN was not for the S3 service, found: " + bucketArn.service()); + } + + private Endpoint result65() { + throw SdkClientException.create("S3 MRAP does not support dual-stack"); + } + + private Endpoint result66() { + throw SdkClientException.create("S3 MRAP does not support FIPS"); + } + + private Endpoint result67() { + throw SdkClientException.create("S3 MRAP does not support S3 Accelerate"); + } + + private Endpoint result68() { + throw SdkClientException.create("Invalid configuration: Multi-Region Access Point ARNs are disabled."); + } + + private Endpoint result69() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", accessPointName_ssa_1 + ".accesspoint.s3-global." + partitionResult.dnsSuffix(), -1, "")).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegionSet(Arrays.asList("*")).build())).build(); + } + + private Endpoint result70() { + throw SdkClientException.create("Client was configured for partition `" + partitionResult.name() + "` but bucket referred to partition `" + bucketArn.partition() + "`"); + } + + private Endpoint result71() { + throw SdkClientException.create("Invalid Access Point Name"); + } + + private Endpoint result72() { + throw SdkClientException.create("S3 Outposts does not support Dual-stack"); + } + + private Endpoint result73() { + throw SdkClientException.create("S3 Outposts does not support FIPS"); + } + + private Endpoint result74() { + throw SdkClientException.create("S3 Outposts does not support S3 Accelerate"); + } + + private Endpoint result75() { + throw SdkClientException.create("Invalid Arn: Outpost Access Point ARN contains sub resources"); + } + + private Endpoint result76() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", accessPointName_ssa_2 + "-" + bucketArn.accountId() + "." + outpostId_ssa_1 + "." + url.authority(), -1, "")).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().disableDoubleEncoding(true).signingName("s3-outposts").signingRegionSet(Arrays.asList("*")).build(), SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3-outposts").signingRegion(bucketArn.region()).build())).build(); + } + + private Endpoint result77() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", accessPointName_ssa_2 + "-" + bucketArn.accountId() + "." + outpostId_ssa_1 + ".s3-outposts." + bucketArn.region() + "." + bucketPartition.dnsSuffix(), -1, "")).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4aAuthScheme.builder().disableDoubleEncoding(true).signingName("s3-outposts").signingRegionSet(Arrays.asList("*")).build(), SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3-outposts").signingRegion(bucketArn.region()).build())).build(); + } + + private Endpoint result78() { + throw SdkClientException.create("Invalid ARN: The access point name may only contain a-z, A-Z, 0-9 and `-`. Found: `" + accessPointName_ssa_2 + "`"); + } + + private Endpoint result79() { + throw SdkClientException.create("Expected an outpost type `accesspoint`, found " + outpostType); + } + + private Endpoint result80() { + throw SdkClientException.create("Invalid ARN: expected an access point name"); + } + + private Endpoint result81() { + throw SdkClientException.create("Invalid ARN: Expected a 4-component resource"); + } + + private Endpoint result82() { + throw SdkClientException.create("Invalid ARN: The outpost Id may only contain a-z, A-Z, 0-9 and `-`. Found: `" + outpostId_ssa_1 + "`"); + } + + private Endpoint result83() { + throw SdkClientException.create("Invalid ARN: The Outpost Id was not set"); + } + + private Endpoint result84() { + throw SdkClientException.create("Invalid ARN: Unrecognized format: " + params.bucket() + " (type: " + arnType + ")"); + } + + private Endpoint result85() { + throw SdkClientException.create("Invalid ARN: No ARN type specified"); + } + + private Endpoint result86() { + throw SdkClientException.create("Invalid ARN: `" + params.bucket() + "` was not a valid ARN"); + } + + private Endpoint result87() { + throw SdkClientException.create("Path-style addressing cannot be used with ARN buckets"); + } + + private Endpoint result88() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", "s3-fips.dualstack.us-east-1." + partitionResult.dnsSuffix(), -1, "/" + uri_encoded_bucket)).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion("us-east-1").build())).build(); + } + + private Endpoint result89() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", "s3-fips.dualstack." + region + "." + partitionResult.dnsSuffix(), -1, "/" + uri_encoded_bucket)).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion(region).build())).build(); + } + + private Endpoint result90() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", "s3-fips.us-east-1." + partitionResult.dnsSuffix(), -1, "/" + uri_encoded_bucket)).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion("us-east-1").build())).build(); + } + + private Endpoint result91() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", "s3-fips." + region + "." + partitionResult.dnsSuffix(), -1, "/" + uri_encoded_bucket)).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion(region).build())).build(); + } + + private Endpoint result92() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", "s3.dualstack.us-east-1." + partitionResult.dnsSuffix(), -1, "/" + uri_encoded_bucket)).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion("us-east-1").build())).build(); + } + + private Endpoint result93() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", "s3.dualstack." + region + "." + partitionResult.dnsSuffix(), -1, "/" + uri_encoded_bucket)).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion(region).build())).build(); + } + + private Endpoint result94() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromString(url.scheme() + "://" + url.authority() + url.normalizedPath() + uri_encoded_bucket)).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion("us-east-1").build())).build(); + } + + private Endpoint result95() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromString(url.scheme() + "://" + url.authority() + url.normalizedPath() + uri_encoded_bucket)).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion(region).build())).build(); + } + + private Endpoint result96() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", "s3." + partitionResult.dnsSuffix(), -1, "/" + uri_encoded_bucket)).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion("us-east-1").build())).build(); + } + + private Endpoint result97() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", "s3." + partitionResult.dnsSuffix(), -1, "/" + uri_encoded_bucket)).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion(region).build())).build(); + } + + private Endpoint result98() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", "s3." + region + "." + partitionResult.dnsSuffix(), -1, "/" + uri_encoded_bucket)).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion(region).build())).build(); + } + + private Endpoint result99() { + throw SdkClientException.create("Path-style addressing cannot be used with S3 Accelerate"); + } + + private Endpoint result100() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromString(url.scheme() + "://" + url.authority() + url.path())).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3-object-lambda").signingRegion(region).build())).build(); + } + + private Endpoint result101() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", "s3-object-lambda-fips." + region + "." + partitionResult.dnsSuffix(), -1, "")).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3-object-lambda").signingRegion(region).build())).build(); + } + + private Endpoint result102() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", "s3-object-lambda." + region + "." + partitionResult.dnsSuffix(), -1, "")).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3-object-lambda").signingRegion(region).build())).build(); + } + + private Endpoint result103() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", "s3-fips.dualstack.us-east-1." + partitionResult.dnsSuffix(), -1, "")).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion("us-east-1").build())).build(); + } + + private Endpoint result104() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", "s3-fips.dualstack." + region + "." + partitionResult.dnsSuffix(), -1, "")).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion(region).build())).build(); + } + + private Endpoint result105() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", "s3-fips.us-east-1." + partitionResult.dnsSuffix(), -1, "")).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion("us-east-1").build())).build(); + } + + private Endpoint result106() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", "s3-fips." + region + "." + partitionResult.dnsSuffix(), -1, "")).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion(region).build())).build(); + } + + private Endpoint result107() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", "s3.dualstack.us-east-1." + partitionResult.dnsSuffix(), -1, "")).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion("us-east-1").build())).build(); + } + + private Endpoint result108() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", "s3.dualstack." + region + "." + partitionResult.dnsSuffix(), -1, "")).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion(region).build())).build(); + } + + private Endpoint result109() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromString(url.scheme() + "://" + url.authority() + url.path())).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion("us-east-1").build())).build(); + } + + private Endpoint result110() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromString(url.scheme() + "://" + url.authority() + url.path())).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion(region).build())).build(); + } + + private Endpoint result111() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", "s3." + partitionResult.dnsSuffix(), -1, "")).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion("us-east-1").build())).build(); + } + + private Endpoint result112() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", "s3." + partitionResult.dnsSuffix(), -1, "")).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion(region).build())).build(); + } + + private Endpoint result113() { + return Endpoint.builder().endpointUrl(EndpointUrl.fromComponents("https", "s3." + region + "." + partitionResult.dnsSuffix(), -1, "")).putAttribute(AwsEndpointAttribute.AUTH_SCHEMES, Arrays.asList(SigV4AuthScheme.builder().disableDoubleEncoding(true).signingName("s3").signingRegion(region).build())).build(); + } + + private Endpoint result114() { + throw SdkClientException.create("A region must be set when sending requests to S3."); + } + } +} diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/compiledendpointrules/endpoints/internal/RulesFunctionsSubstringEqualsTest.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/compiledendpointrules/endpoints/internal/RulesFunctionsSubstringEqualsTest.java new file mode 100644 index 000000000000..2dfe2f2a18e2 --- /dev/null +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/compiledendpointrules/endpoints/internal/RulesFunctionsSubstringEqualsTest.java @@ -0,0 +1,232 @@ +/* + * 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.compiledendpointrules.endpoints.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * {@code RulesFunctions.substringEquals} is emitted by the BDD endpoint codegen peephole in place of + * {@code stringEquals(coalesce(substring(value, start, stop, reverse), ""), literal)}. It must be + * indistinguishable from that composition, otherwise a rule takes a different branch than the + * endpoint spec requires and we resolve a wrong endpoint. + * + *

The interesting case is non-ASCII input: the spec's {@code substring} returns null when *any* + * character in the whole input is outside the 7-bit ASCII range, which makes the comparison false + * even when the characters at the compared positions match exactly. + * + *

Why this lives here. {@code RulesFunctions} is a codegen template copied into every + * service module, not a class owned by any one service, so per + * {@code docs/guidelines/testing-guidelines.md} it belongs with "generated SDK common + * functionalities" in this module rather than under a single service. It is exercised through the + * {@code compiledendpointrules} test service, alongside {@code EndpointUrlConformanceTest} and + * {@code RuleUrlTest}, which cover other generated classes from the same template set. The + * substring windows below are the ones the peephole actually emits for the S3 BDD model. + */ +class RulesFunctionsSubstringEqualsTest { + + /** + * The composition that {@code substringEquals} replaces. Written out in full so the test is + * checking against the spec functions rather than against a restatement of the optimization. + */ + private static boolean reference(String value, int startIndex, int stopIndex, boolean reverse, String literal) { + return RulesFunctions.stringEquals( + RulesFunctions.coalesce(RulesFunctions.substring(value, startIndex, stopIndex, reverse), ""), + literal); + } + + @ParameterizedTest(name = "substringEquals({0}, {1}, {2}, {3}, {4})") + @MethodSource("nonEmptyLiteralCases") + void substringEquals_againstSpecComposition_agrees(String value, int startIndex, int stopIndex, boolean reverse, + String literal) { + assertThat(RulesFunctions.substringEquals(value, startIndex, stopIndex, reverse, literal)) + .as("substringEquals must agree with stringEquals(coalesce(substring(..), \"\"), literal)") + .isEqualTo(reference(value, startIndex, stopIndex, reverse, literal)); + } + + /** + * An empty literal is the sole case where the two are allowed to differ, because + * {@code coalesce(null, "")} makes the spec composition true whenever {@code substring} returns + * null. Asserting the direction of every divergence keeps that carve-out honest: if the helper + * ever diverged for a non-empty literal it would show up in + * {@link #substringEquals_againstSpecComposition_agrees}, and if a *new* kind of divergence + * appeared here it would show up as a failure rather than as a silently skipped case. + * + *

Codegen never emits this shape - see {@code BddPeepholeVisitorTest.emptyLiteralIsNotRewritten}. + */ + @ParameterizedTest(name = "substringEquals({0}, {1}, {2}, {3}, \"\")") + @MethodSource("emptyLiteralCases") + void substringEquals_withEmptyLiteral_divergesOnlyBySpecReturningTrue(String value, int startIndex, int stopIndex, + boolean reverse) { + boolean actual = RulesFunctions.substringEquals(value, startIndex, stopIndex, reverse, ""); + boolean expected = reference(value, startIndex, stopIndex, reverse, ""); + + assertThat(actual).as("substringEquals must never be true for an empty literal").isFalse(); + if (actual != expected) { + assertThat(expected) + .as("the only permitted divergence is the spec composition returning true") + .isTrue(); + } + } + + private static List nonEmptyLiteralCases() { + return cases(false); + } + + private static List emptyLiteralCases() { + List args = new ArrayList<>(); + for (Arguments a : cases(true)) { + Object[] g = a.get(); + // Drop the trailing literal; the test method supplies it. + args.add(Arguments.of(g[0], g[1], g[2], g[3])); + } + return args; + } + + private static List cases(boolean emptyLiteralOnly) { + // (startIndex, stopIndex) windows that codegen actually emits for the S3 BDD, plus a forward + // interior match which the S3 ruleset does not currently use, and a degenerate empty window. + int[][] windows = { + {0, 4}, {0, 6}, {0, 7}, {16, 18}, {14, 16}, {1, 3}, {3, 3} + }; + List values = Arrays.asList( + null, + "", + "a", + "ab", + "arn:", + "arn:aws:s3:::mybucket", + "mybucket--x-s3", + "mybucket--xa-s3", + "my-bucket-name", + "mybucket--abcd-ab1--x-s3", + // Non-ASCII: the compared window is pure ASCII and matches, but the spec still rejects + // the whole input, so every one of these must come out false. + "arn\u00e9:aws:s3:::b", + "arn:aws:s3:::b\u00fc", + "mybucket\u00fc--x-s3", + "\u00fcmybucket--x-s3", + "mybucket--x-s3\u00fc", + // Multi-byte beyond Latin-1, and a surrogate pair. + "mybucket\u4e2d--x-s3", + "mybucket\ud83d\ude00--x-s3" + ); + List literals = emptyLiteralOnly + ? Arrays.asList("") + : Arrays.asList("arn:", "--x-s3", "--xa-s3", "--", "rn", "zz"); + + List args = new ArrayList<>(); + for (int[] window : windows) { + for (String value : values) { + for (String literal : literals) { + for (boolean reverse : new boolean[] {false, true}) { + args.add(Arguments.of(value, window[0], window[1], reverse, literal)); + } + } + } + } + return args; + } + + /** + * Spot checks in the direction that matters, so a regression is legible rather than just a + * differential mismatch somewhere in the matrix above. + */ + @Test + void substringEquals_withNonAsciiInput_isFalseEvenWhenComparedWindowMatches() { + // Pure-ASCII directory bucket: this is an S3 Express bucket. + assertThat(RulesFunctions.substringEquals("mybucket--x-s3", 0, 6, true, "--x-s3")).isTrue(); + + // Same suffix, but a non-ASCII character elsewhere in the name. The spec's substring returns + // null, so this is NOT an S3 Express bucket and must not be routed as one. + assertThat(RulesFunctions.substringEquals("mybuck\u00e9t--x-s3", 0, 6, true, "--x-s3")).isFalse(); + assertThat(reference("mybuck\u00e9t--x-s3", 0, 6, true, "--x-s3")).isFalse(); + + // Likewise for the ARN prefix check. + assertThat(RulesFunctions.substringEquals("arn:aws:s3:::b", 0, 4, false, "arn:")).isTrue(); + assertThat(RulesFunctions.substringEquals("arn:aws:s3:::b\u00fc", 0, 4, false, "arn:")).isFalse(); + assertThat(reference("arn:aws:s3:::b\u00fc", 0, 4, false, "arn:")).isFalse(); + } + + @Test + void substringEquals_withNullOrTooShortInput_isFalse() { + assertThat(RulesFunctions.substringEquals(null, 0, 4, false, "arn:")).isFalse(); + assertThat(RulesFunctions.substringEquals("arn", 0, 4, false, "arn:")).isFalse(); + assertThat(RulesFunctions.substringEquals("", 0, 4, false, "arn:")).isFalse(); + } + + @Test + void substringEquals_withLiteralLengthNotMatchingWindow_isFalse() { + // Guards against a caller passing a literal that is not stopIndex - startIndex long. A + // shorter or longer literal can never equal the substring. + assertThat(RulesFunctions.substringEquals("arn:aws", 0, 4, false, "arn")).isFalse(); + assertThat(RulesFunctions.substringEquals("arn:aws", 0, 4, false, "arn:a")).isFalse(); + } + + /** + * The one case where {@code substringEquals} is deliberately not equivalent, and therefore the + * one case codegen must not rewrite. {@code startIndex == stopIndex} makes the spec's + * {@code substring} return null, the coalesce turns that into {@code ""}, and comparing {@code ""} + * to an empty literal is true - even for a null input. See + * {@code BddPeepholeVisitorTest.emptyLiteralIsNotRewritten}, which pins the codegen guard. + */ + @Test + void substringEquals_withDegenerateEmptyWindow_divergesFromSpecAndIsGuardedInCodegen() { + assertThat(reference(null, 3, 3, false, "")).isTrue(); + assertThat(reference("anything", 3, 3, false, "")).isTrue(); + + assertThat(RulesFunctions.substringEquals(null, 3, 3, false, "")).isFalse(); + assertThat(RulesFunctions.substringEquals("anything", 3, 3, false, "")).isFalse(); + } + + /** + * {@code isValidHostLabel(s, allowDots)} delegates to the two specialized helpers that optimized + * codegen calls directly, so the three must stay in agreement. Covers the delegation introduced + * to remove a duplicated copy of the multi-label loop. + */ + @Test + void isValidHostLabel_agreesWithSpecializedVariants() { + List labels = Arrays.asList( + null, "", ".", "a", "a.b", "a..b", "-a", "a-", "a-b", "a.b.c", "9a", "a_b", + "abc.def.ghi", ".leading", "trailing.", + // 63 chars (max) and 64 chars (over) + new String(new char[63]).replace('\0', 'a'), + new String(new char[64]).replace('\0', 'a'), + // over-length single chunk inside a multi-label name + "ok." + new String(new char[64]).replace('\0', 'a') + ); + + for (String label : labels) { + assertThat(RulesFunctions.isValidHostLabel(label, false)) + .as("isValidHostLabel(%s, false) must equal isValidHostLabelSingle", label) + .isEqualTo(RulesFunctions.isValidHostLabelSingle(label)); + assertThat(RulesFunctions.isValidHostLabel(label, true)) + .as("isValidHostLabel(%s, true) must equal isValidHostLabelMulti", label) + .isEqualTo(RulesFunctions.isValidHostLabelMulti(label)); + } + + // Sanity: the two variants genuinely differ on dots, so the above is not vacuous. + assertThat(RulesFunctions.isValidHostLabelSingle("a.b")).isFalse(); + assertThat(RulesFunctions.isValidHostLabelMulti("a.b")).isTrue(); + } +} From 91541fd68bbc0b91c499e9fdd9790c04e61e65f7 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Wed, 26 Aug 2026 17:21:31 -0700 Subject: [PATCH 4/7] perf(endpoints): Avoid eager map allocation when building an Endpoint (#7317) Endpoint's builder eagerly allocated two HashMaps (headers and attributes) for every endpoint, and handed them to the built Endpoint. Both escape into the result, so they cannot be optimized away by the JIT, and most endpoints need neither: headers are almost always absent and attributes are almost always a single AUTH_SCHEMES entry. Stage instead of allocate: - The header map is allocated only once a header is actually added. - The first attribute is held in two fields on the builder; a map is allocated only if a second distinct key arrives. - build() collapses whatever was staged into the smallest immutable map that fits: emptyMap, singletonMap, or an unmodifiable view. - Both maps are created with capacity 4 rather than the default 16, whose backing table was the largest single allocation on the path. No API change. The builder itself does not escape resolveEndpoint, so escape analysis already eliminates it; this removes the allocations that actually survive. Measured with JMH (allocation profiler, 2 forks, JDK 21, M-series), against an Endpoint-shaped holder built only from emptyMap/singletonMap as the theoretical floor: shape before after floor no attributes 120 B/op 24 B/op 24 B/op one attribute 232 B/op 64 B/op 64 B/op two attributes 264 B/op 232 B/op one attr + header 424 B/op 288 B/op The two shapes that dominate real endpoint rules now allocate exactly what a hand-written factory would. Time per construction drops 67% and 71% for those two shapes. Two-attribute construction costs ~3ns more because it stages an attribute and then promotes anyway; that path is S3 Express only and 3ns against a ~50us endpoint resolution. Behavior notes: - headers() and the attribute map are now unmodifiable. Mutating a map returned from a getter was never supported, and SdkHttpRequest already behaves this way. - Mutating a builder after build() no longer leaks into the Endpoint that was already built, for the no-attribute, single-attribute and header cases. Previously it leaked in every case. The two-or-more attribute case still shares state, unchanged from before. --- .../amazon/awssdk/endpoints/Endpoint.java | 72 +++++++++++-- .../amazon/awssdk/endpoints/EndpointTest.java | 102 ++++++++++++++++++ 2 files changed, 167 insertions(+), 7 deletions(-) diff --git a/core/endpoints-spi/src/main/java/software/amazon/awssdk/endpoints/Endpoint.java b/core/endpoints-spi/src/main/java/software/amazon/awssdk/endpoints/Endpoint.java index 0db92b5a7a2d..96829aa03924 100644 --- a/core/endpoints-spi/src/main/java/software/amazon/awssdk/endpoints/Endpoint.java +++ b/core/endpoints-spi/src/main/java/software/amazon/awssdk/endpoints/Endpoint.java @@ -17,6 +17,7 @@ import java.net.URI; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -28,14 +29,21 @@ */ @SdkPublicApi public final class Endpoint { + /** + * Initial capacity for the attribute and header maps. Endpoints carry very few of either, so the + * default capacity of 16 is extra overhead. + */ + private static final int ATTRIBUTE_MAP_CAPACITY = 4; + private static final int HEADER_MAP_CAPACITY = 4; + private final EndpointUrl endpointUrl; private final Map> headers; private final Map, Object> attributes; private Endpoint(BuilderImpl b) { this.endpointUrl = b.endpointUrl; - this.headers = b.headers; - this.attributes = b.attributes; + this.headers = b.headers == null ? Collections.emptyMap() : Collections.unmodifiableMap(b.headers); + this.attributes = b.buildAttributes(); } /** @@ -137,20 +145,67 @@ default Builder endpointUrl(EndpointUrl endpointUrl) { private static class BuilderImpl implements Builder { private EndpointUrl endpointUrl; - private final Map> headers = new HashMap<>(); - private final Map, Object> attributes = new HashMap<>(); + + /** + * Most endpoints declare no headers, so the map is allocated only once a header is added. + */ + private Map> headers; + + /** + * Endpoints almost always carry zero or one attribute (typically {@code AUTH_SCHEMES}), so the first + * entry is held in these two fields and {@link #attributes} is allocated only if a second distinct + * key arrives. This keeps the common cases free of a {@code HashMap} and its backing table. + */ + private EndpointAttributeKey firstAttributeKey; + private Object firstAttributeValue; + private Map, Object> attributes; private BuilderImpl() { } private BuilderImpl(Endpoint e) { this.endpointUrl = e.endpointUrl; - if (e.headers != null) { + if (!e.headers.isEmpty()) { + this.headers = new HashMap<>(Math.max(HEADER_MAP_CAPACITY, e.headers.size())); e.headers.forEach((n, v) -> { this.headers.put(n, new ArrayList<>(v)); }); } - this.attributes.putAll(e.attributes); + e.attributes.forEach(this::putAttributeUnchecked); + } + + /** + * Collapses the staged attributes into the smallest immutable map that can hold them. + */ + private Map, Object> buildAttributes() { + if (attributes != null) { + return Collections.unmodifiableMap(attributes); + } + if (firstAttributeKey != null) { + return Collections.singletonMap(firstAttributeKey, firstAttributeValue); + } + return Collections.emptyMap(); + } + + /** + * Stores an attribute without the generic key/value pairing, for use by callers that have already + * had that relationship checked (the {@link #putAttribute} overload and the copy constructor). + */ + private void putAttributeUnchecked(EndpointAttributeKey key, Object value) { + if (attributes != null) { + attributes.put(key, value); + } else if (firstAttributeKey == null || firstAttributeKey.equals(key)) { + firstAttributeKey = key; + firstAttributeValue = value; + } else { + // Sized for the realistic maximum rather than the default 16, whose backing table alone + // costs more than every other allocation on this path combined. + attributes = new HashMap<>(ATTRIBUTE_MAP_CAPACITY); + attributes.put(firstAttributeKey, firstAttributeValue); + attributes.put(key, value); + firstAttributeKey = null; + firstAttributeValue = null; + } } @SuppressWarnings("deprecation") @@ -168,6 +223,9 @@ public Builder endpointUrl(EndpointUrl endpointUrl) { @Override public Builder putHeader(String name, String value) { + if (this.headers == null) { + this.headers = new HashMap<>(HEADER_MAP_CAPACITY); + } List values = this.headers.computeIfAbsent(name, (n) -> new ArrayList<>()); values.add(value); return this; @@ -175,7 +233,7 @@ public Builder putHeader(String name, String value) { @Override public Builder putAttribute(EndpointAttributeKey key, T value) { - this.attributes.put(key, value); + putAttributeUnchecked(key, value); return this; } diff --git a/core/endpoints-spi/src/test/java/software/amazon/awssdk/endpoints/EndpointTest.java b/core/endpoints-spi/src/test/java/software/amazon/awssdk/endpoints/EndpointTest.java index 25eb1729e846..314c0f499fe8 100644 --- a/core/endpoints-spi/src/test/java/software/amazon/awssdk/endpoints/EndpointTest.java +++ b/core/endpoints-spi/src/test/java/software/amazon/awssdk/endpoints/EndpointTest.java @@ -16,6 +16,7 @@ package software.amazon.awssdk.endpoints; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.net.URI; import java.util.Arrays; @@ -133,4 +134,105 @@ public void endpointUrlAccessor_returnsCorrectComponents() { assertThat(endpointUrl.encodedPath()).isEqualTo("/bucket"); assertThat(endpointUrl.queryAndFragment()).isEmpty(); } + + @Test + public void build_noHeadersOrAttributes_returnsEmptyMaps() { + Endpoint endpoint = Endpoint.builder() + .endpointUrl(EndpointUrl.fromString("https://example.com")) + .build(); + + assertThat(endpoint.headers()).isEmpty(); + assertThat(endpoint.attribute(TEST_STRING_ATTR)).isNull(); + } + + @Test + public void headers_isUnmodifiable() { + Endpoint noHeaders = Endpoint.builder() + .endpointUrl(EndpointUrl.fromString("https://example.com")) + .build(); + Endpoint withHeaders = Endpoint.builder() + .endpointUrl(EndpointUrl.fromString("https://example.com")) + .putHeader("foo", "bar") + .build(); + + assertThatThrownBy(() -> noHeaders.headers().put("a", Arrays.asList("b"))) + .isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(() -> withHeaders.headers().put("a", Arrays.asList("b"))) + .isInstanceOf(UnsupportedOperationException.class); + } + + /** + * A single attribute is staged inline rather than in a map, so exercise the read path for one, two and + * three attributes as well as overwriting the staged entry. + */ + @Test + public void putAttribute_variousArities_allReadable() { + EndpointAttributeKey second = new EndpointAttributeKey<>("Second", String.class); + EndpointAttributeKey third = new EndpointAttributeKey<>("Third", String.class); + + Endpoint one = Endpoint.builder() + .endpointUrl(EndpointUrl.fromString("https://example.com")) + .putAttribute(TEST_STRING_ATTR, "a") + .build(); + assertThat(one.attribute(TEST_STRING_ATTR)).isEqualTo("a"); + assertThat(one.attribute(second)).isNull(); + + Endpoint two = Endpoint.builder() + .endpointUrl(EndpointUrl.fromString("https://example.com")) + .putAttribute(TEST_STRING_ATTR, "a") + .putAttribute(second, "b") + .build(); + assertThat(two.attribute(TEST_STRING_ATTR)).isEqualTo("a"); + assertThat(two.attribute(second)).isEqualTo("b"); + + Endpoint three = Endpoint.builder() + .endpointUrl(EndpointUrl.fromString("https://example.com")) + .putAttribute(TEST_STRING_ATTR, "a") + .putAttribute(second, "b") + .putAttribute(third, "c") + .build(); + assertThat(three.attribute(TEST_STRING_ATTR)).isEqualTo("a"); + assertThat(three.attribute(second)).isEqualTo("b"); + assertThat(three.attribute(third)).isEqualTo("c"); + } + + @Test + public void putAttribute_sameKeyTwice_lastValueWins() { + EndpointAttributeKey second = new EndpointAttributeKey<>("Second", String.class); + + Endpoint staged = Endpoint.builder() + .endpointUrl(EndpointUrl.fromString("https://example.com")) + .putAttribute(TEST_STRING_ATTR, "first") + .putAttribute(TEST_STRING_ATTR, "second") + .build(); + assertThat(staged.attribute(TEST_STRING_ATTR)).isEqualTo("second"); + + // Same key overwritten after the builder has been promoted to a map. + Endpoint promoted = Endpoint.builder() + .endpointUrl(EndpointUrl.fromString("https://example.com")) + .putAttribute(TEST_STRING_ATTR, "first") + .putAttribute(second, "other") + .putAttribute(TEST_STRING_ATTR, "second") + .build(); + assertThat(promoted.attribute(TEST_STRING_ATTR)).isEqualTo("second"); + assertThat(promoted.attribute(second)).isEqualTo("other"); + } + + @Test + public void toBuilder_roundTripsAllAttributeArities() { + EndpointAttributeKey second = new EndpointAttributeKey<>("Second", String.class); + + Endpoint none = Endpoint.builder() + .endpointUrl(EndpointUrl.fromString("https://example.com")) + .build(); + assertThat(none.toBuilder().build()).isEqualTo(none); + + Endpoint one = none.toBuilder().putAttribute(TEST_STRING_ATTR, "a").build(); + assertThat(one.toBuilder().build()).isEqualTo(one); + + Endpoint two = one.toBuilder().putAttribute(second, "b").build(); + assertThat(two.toBuilder().build()).isEqualTo(two); + assertThat(two.attribute(TEST_STRING_ATTR)).isEqualTo("a"); + assertThat(two.attribute(second)).isEqualTo("b"); + } } From 1e4216c3c05dd730ff54bd7acf5d9c31473de139 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Tue, 1 Sep 2026 08:34:44 -0700 Subject: [PATCH 5/7] [Endpoints BDD 5/5] Add a result cache to the BDD endpoint provider (#7322) * feat(endpoints): Add a result cache to the BDD endpoint provider Generate a single-entry (params -> endpoint) cache into each Default{Service}EndpointProvider produced by the BDD codegen path, so a client resolving the same endpoint repeatedly skips the BDD walk after the first call. Scoped to the BDD path only; the rules2 path that every shipped service uses is untouched, because no service model ships an endpoint-bdd-1.json yet and that keeps a caching defect away from customers while the approach is evaluated. The cache is a volatile field holding an immutable CacheEntry. Racing threads compute equivalent entries for equal params, so a lost write costs one re-resolution and needs no further synchronisation. Only successful resolutions are stored: a rule error or a no-match leaves the previous entry in place, so a bad call neither poisons the cache nor gets replayed from it. Cache-key comparison is generated per parameter from a codegen-time classification (EndpointCacheKeyClassification, computed by EndpointProviderCacheIndex), ordered cheapest check first with an early exit on mismatch: BOOLEAN - identity, which is complete for Boolean rather than merely fast, since autoboxing returns the TRUE and FALSE singletons CLIENT_STATIC_REF - identity (AWS::Region, clientContextParams) OPERATION_STATIC - identity (staticContextParams literals) SEMI_STABLE - identity then equals (SDK::Endpoint, AccountIdEndpointMode) IDENTITY_DERIVED - identity then equals (AWS::Auth::AccountId) REQUEST_DYNAMIC - identity then equals (contextParam, JMESPath) REQUEST_LIST - size-capped element-wise identity/equals Classifications are read from the BDD model's parameters, not the rule set's. The generated provider evaluates the BDD, nothing in codegen enforces that the two files agree, and a parameter absent from the key is the one defect here that returns an endpoint resolved for different inputs. Codegen fails outright rather than skipping a parameter it cannot place. Reference stability, which raises the hit rate but is not required for correctness since every string tier keeps an equals fallback: - StaticClientEndpointProvider sanitizes the client endpoint once at construction instead of rebuilding the URI per request, exposed through a new ClientEndpointProvider#sanitizedEndpointString() that AwsEndpointProviderUtils#endpointBuiltIn now delegates to. The transform has a single definition so the cached and recomputed forms cannot drift. - AccountIdEndpointMode#endpointModeValue() returns an interned literal from a field rather than name().toLowerCase(), and EndpointParamsKnowledgeIndex emits it. - EndpointResolverUtilsSpec hoists staticContextParams array values to static final unmodifiable lists. The last two also remove a per-request allocation on the rules2 path. Testing: - BddEndpointProviderCacheTest, 30 tests over the bddendpoints service: one no-stale-hit test per parameter, hit assertions via instance identity, unset transitions, equals fallbacks, the list size cap, errors never cached, and 16-thread concurrent resolution of two parameter sets. Mutation-checked: emptying the key fails 21 of 30, and dropping only clientStringParam fails exactly the test that names it. - EndpointProviderCacheIndexTest pins each parameter's tier, the comparison order, and that classification reads the BDD rather than the rule set. Tier assignment is invisible at runtime, so it is asserted here or not at all. - bddendpoints and the default-regional codegen models declare parameters their BDD graphs never read, which is how all seven tiers get covered without a BDD compiler: the node graph indexes conditions rather than naming parameters, and the cache key spans every declared parameter. - queryServiceModelsWithBddEndpoints now pairs the S3 BDD with the S3 rule set instead of the four-parameter default-regional one, so the 17 parameters in the golden file match the params class. * refactor(endpoints): Simplify the BDD cache key to one comparison form Replace the seven-tier cacheParamsMatch with a uniform Objects.equals chain, ordered into three coarse groups: booleans, then strings whose reference the SDK keeps stable, then everything else, each group in the model's declaration order. Benchmarking says the tiers were not earning their complexity. Against this form they bought nothing on the hit path, which is the only path a cache exists to improve, and about 0.2 ns on the miss shape that motivates ordering at all - ahead of a ~1400 ns resolution. They cost a classification pass over every operation, a seven-value enum, and three different emitted code shapes. Full data, including why hashing the params would be worse than comparing them and why the comparison stays a private static method in the provider rather than moving onto the params class, is in .kiro/reference/endpoint_cache_key_benchmark.md. Objects.equals is what makes one emitter sufficient: it tries identity before equals, so a parameter whose reference is stable settles on the identity check and one that arrives fresh falls through and still matches. That was the tiers' main trick, available for free. Ordering survives because it is nearly free to derive - a parameter's group follows from its declared type plus whether it is AWS::Region or a client context param, with no analysis of the service's operations - and it is worth 20 ns on a miss against a late-declared boolean. It cannot affect correctness, since the chain compares every parameter before returning true. List parameters route through a generated cacheListsMatch helper instead of Objects.equals, keeping every term in the chain a single boolean expression and keeping the comparison bounded. List.equals is unbounded, and resolution is typically indifferent to list length, so an unbounded key check can cost more than the resolution it avoids and turn the cache into a pessimisation for that request shape. Above the cap the provider reports a miss and resolves, which is what it would have done anyway. The helper is only emitted when the model declares a stringArray. Deletes EndpointCacheKeyClassification, EndpointProviderCacheIndex and EndpointProviderCacheIndexTest. Testing: - The classification unit test is replaced by two assertions on the generated source, which is a stronger place to make them: that the key compares every parameter the BDD declares, and that the three-group ordering holds. The first is the invariant that matters - a parameter missing from the key returns an endpoint resolved for a different value of it - and it now covers all three BDD test models. - Mutation-checked both levels. Dropping a parameter that exists only in the runtime model fails exactly one of the 30 BddEndpointProviderCacheTest cases, the one that names it; dropping parameters present in the codegen models fails the completeness assertion, the ordering assertion and both golden files. - codegen 707 pass, codegen-generated-classes-test 3677 pass, checkstyle clean. * perf(endpoints): Key the BDD cache on what the BDD actually reads Two changes to the generated cache key, both driven by the per-service measurements in .kiro/reference/endpoint_cache_service_shapes.md. 1. Exclude parameters no condition and no result reads. A parameter nothing reads cannot change the resolved endpoint, so comparing it can only turn a hit into a miss that resolves to the endpoint already cached. S3 is why this matters. It declares Key, Prefix and CopySource, reads none of them, and binds Key as a contextParam - so Key changes on essentially every object request. With Key in the key, S3's cache misses on almost every GetObject and pays 6.4 ns per request for nothing. Dropping the three unread parameters takes the key from 17 comparisons to 14, makes a hit 41% cheaper on fresh references, and converts the dominant miss into a hit. 2. Compare only element 0 of a stringArray read only at index 0. When every read of a list is getAttr(list, "[0]"), nothing past the first element reaches the endpoint, so the rest cannot change the answer. DynamoDB is why this matters. It reads ResourceArnList only through getAttr(ResourceArnList, "[0]"), and comparing a freshly built three-element ARN list measured 15.5 ns against a 28 ns regional resolution - over half the cost the cache exists to avoid, on a latency path that matters. Comparing element 0 makes it O(1). This also makes an absent list and an empty one the same key, which is correct rather than a concession: the runtime's listAccess returns null for both, so both take the same branch during resolution. It is strictly more permissive than comparing whole lists, so it can only turn misses into hits. Detection is conservative in the safe direction. BddParameterReferences walks the conditions and results with the same parser the generator uses, and any read that is not an index-0 access - isSet, a template interpolation, a non-zero index, passing the list to a function - marks the parameter as needing a full comparison. Erring that way costs comparison work; erring the other way would drop something from the key that can change the endpoint. Testing: - BddParameterReferences is not tested directly. Both behaviours are asserted on the generated source and on runtime behaviour, because those are what can be wrong in a way that matters; a unit test over the usage map would restate the implementation. - The codegen tests assert the key covers every referenced parameter for all three BDD models, omits the unreferenced ones, routes a whole-list parameter and an index-0-only parameter to their respective helpers, and that the provider really does read only element 0 - so the comparison and the thing it depends on cannot drift apart. - The runtime suite grows to 38 tests: changing element 0 invalidates while changing a later element, shortening the list, or going far past the size cap all hit; an unread parameter never invalidates; and the whole-list parameter keeps the previous element-wise coverage. - Mutation-checked three ways. Misclassifying whole-list reads as index-0-only fails the 4 whole-list tests; treating every parameter as unreferenced fails 22; never detecting index-0-only fails the 4 first-element tests. The suite discriminates in both directions. - Both test models needed their extra parameters wired into the node graph. They had been declared but unreferenced, so they would now be correctly excluded and the tests covering them would assert nothing. Each new condition is a node whose branches share a successor, which makes it genuinely evaluated without changing what any request resolves to. codegen 709 pass, codegen-generated-classes-test 3685 pass, checkstyle clean. * fix(endpoints): Let an isSet guard keep the first-element list comparison The rules language requires a null check before an indexed access, so a model that reads list[0] always reads isSet(list) as well. Counting that null check as a whole-value read meant the first-element comparison never fired on a real model: DynamoDB's ResourceArnList, the case it was added for, was still compared element by element. isSet observes only whether the parameter is present, so on its own it no longer disqualifies the parameter. Verified against the real DynamoDB BDD: the analysis now classifies ResourceArnList as FIRST_ELEMENT_ONLY and the other eight parameters as FULL. Presence does have to stay in the cache key, though, and that is a change from the previous commit. Because isSet tells an absent list apart from an empty one, the generated comparison now checks presence as well as element 0, rather than treating both as null: if (a == b) return true; if (a == null || b == null) return false; String firstA = a.isEmpty() ? null : a.get(0); String firstB = b.isEmpty() ? null : b.get(0); return Objects.equals(firstA, firstB); Collapsing absent and empty is sound only when the BDD's branches for the two converge. DynamoDB's do - traced through its graph, cond20 false and cond21 false both land on nodeP27, and likewise nodeP53 for the second occurrence - so it would have been correct there. It is a property of the graph rather than of the parameter, so relying on it would mean a future model could quietly invalidate the comparison. One extra reference check buys independence from that, and the comparison stays O(1). Test model changes: - Both BDD fixtures gained the isSet guard ahead of their index-0 access, so they match the shape a real model produces. Without it the fixtures were testing a case that cannot occur. - The whole-list parameters were previously read only via isSet, which now correctly qualifies them for the first-element comparison and left the whole-list path uncovered. Each now also reads index 1, the smallest realistic change that makes the rest of the list matter. - firstElementList_emptyAndUnset_areTheSameKey becomes ..._areDistinguished, matching the new semantics. - New codegen assertion that a list read past the head is compared in full, since the boundary between the two helpers is a correctness line rather than a preference. - Mutation-checked the new logic: treating isSet as a whole-value read - exactly the reported regression - fails three runtime tests, the codegen assertion, and the golden file. codegen 710 pass, codegen-generated-classes-test 3685 pass, checkstyle clean. * refactor(endpoints): Drop the public sanitizeEndpoint helper It was public only because of a package boundary: the transformation lived on StaticClientEndpointProvider in core.internal, and the caller - ClientEndpointProvider's default method - sits in core, so nothing weaker than public could reach it. That is a poor reason for a public member, even on an @SdkInternalApi class. The transformation now lives in the interface default, which is the implementation every provider that does not override it already uses, and StaticClientEndpointProvider's constructor calls ClientEndpointProvider.super.sanitizedEndpointString() to compute the value it caches. One definition, as before, with nothing public added. The class is now final. That is what makes the constructor call provably safe: the super call is non-virtual, but the default it invokes reads clientEndpoint() and isEndpointOverridden(), and a subclass overriding either could have observed partial construction. Nothing subclasses it and it is @SdkInternalApi, so sealing it costs nothing and removes the hazard rather than documenting it. Verified the two implementations still agree: for a matrix of endpoints covering query parameters, user info, explicit ports, fragments and plain hosts, the caching implementation and the interface default produce the same string, the not-overridden case still yields null, and the caching one still returns an identical reference across calls. sdk-core 1505 + 624 pass, aws-core 317, codegen 710, codegen-generated-classes-test 3685, checkstyle clean. * Cleanups * Additional cleanups/fixes from review pass * Add tests (missed adding in earlier commit) * Fix minor edge issues with split and loadPartitionData * Minor - rename method in AccoundIdEndpointMode to just value --- .../rules/EndpointParamsKnowledgeIndex.java | 6 +- .../poet/rules/EndpointResolverUtilsSpec.java | 61 ++- .../rules2/bdd/BddEndpointProviderSpec.java | 293 ++++++++++ .../rules2/bdd/BddParameterReferences.java | 180 +++++++ .../rules2/RulesFunctions.java.resource | 76 ++- .../awssdk/codegen/poet/ClientTestModels.java | 7 +- .../bdd/BddEndpointProviderSpecTest.java | 170 ++++++ .../query/endpoint-bdd-default-regional.json | 98 +++- .../endpoint-rule-set-default-regional.json | 32 +- ...esolver-utils-with-endpointsbasedauth.java | 2 +- ...point-resolver-utils-with-stringarray.java | 9 +- .../poet/rules/endpoint-resolver-utils.java | 2 +- .../bdd/endpoint-provider-bdd-class.java | 114 +++- .../bdd/endpoint-provider-bdd-s3-class.java | 37 ++ .../endpoints/AccountIdEndpointMode.java | 32 +- .../endpoints/AwsEndpointProviderUtils.java | 18 +- .../endpoints/AccountIdEndpointModeTest.java | 99 ++++ .../awssdk/core/ClientEndpointProvider.java | 15 + .../StaticClientEndpointProvider.java | 39 ++ .../core/ClientEndpointProviderTest.java | 107 ++++ .../bddendpoints/endpoint-bdd-1.json | 126 ++++- .../bddendpoints/endpoint-rule-set.json | 44 +- .../bddendpoints/service-2.json | 91 ++++ .../BddEndpointProviderCacheTest.java | 508 ++++++++++++++++++ .../RulesFunctionsAwsPartitionTest.java | 119 ++++ .../internal/RulesFunctionsSplitTest.java | 211 ++++++++ .../endpointproviders/no-aws-partitions.json | 19 + 27 files changed, 2465 insertions(+), 50 deletions(-) create mode 100644 codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/bdd/BddParameterReferences.java create mode 100644 core/aws-core/src/test/java/software/amazon/awssdk/awscore/endpoints/AccountIdEndpointModeTest.java create mode 100644 core/sdk-core/src/test/java/software/amazon/awssdk/core/ClientEndpointProviderTest.java create mode 100644 test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/bddendpoints/BddEndpointProviderCacheTest.java create mode 100644 test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/compiledendpointrules/endpoints/internal/RulesFunctionsAwsPartitionTest.java create mode 100644 test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/compiledendpointrules/endpoints/internal/RulesFunctionsSplitTest.java create mode 100644 test/codegen-generated-classes-test/src/test/resources/software/amazon/awssdk/services/endpointproviders/no-aws-partitions.json diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointParamsKnowledgeIndex.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointParamsKnowledgeIndex.java index 7d200aeae0b5..1fc3e90eb195 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointParamsKnowledgeIndex.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointParamsKnowledgeIndex.java @@ -191,7 +191,11 @@ public MethodSpec recordAccountIdEndpointModeMethod() { + ".ifPresent(m -> executionAttributes.getAttribute($T.BUSINESS_METRICS).addMetric(m))", BusinessMetricsUtils.class, SdkInternalExecutionAttribute.class); - builder.addStatement("return mode.name().toLowerCase()"); + // Use value() rather than name().toLowerCase() so that the returned String is an interned compile-time + // literal. That keeps the reference stable across calls and removes a per-request allocation. It also + // avoids name().toLowerCase()'s dependence on the default locale, which mangles the value under a + // Turkish locale. + builder.addStatement("return mode.value()"); return builder.build(); } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverUtilsSpec.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverUtilsSpec.java index 84677fcae0cd..cd49ec926a5e 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverUtilsSpec.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointResolverUtilsSpec.java @@ -22,11 +22,13 @@ import com.fasterxml.jackson.jr.stree.JrsString; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.FieldSpec; import com.squareup.javapoet.MethodSpec; import com.squareup.javapoet.ParameterizedTypeName; import com.squareup.javapoet.TypeName; import com.squareup.javapoet.TypeSpec; import com.squareup.javapoet.TypeVariableName; +import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Locale; @@ -34,6 +36,8 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.lang.model.element.Modifier; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.awscore.AwsExecutionAttribute; @@ -119,6 +123,7 @@ public TypeSpec poetSpec() { b.addMethod(setStaticContextParamsMethod()); addStaticContextParamMethods(b); + addStaticListFields(b); b.addMethod(authSchemeWithEndpointSignerPropertiesMethod()); @@ -342,9 +347,8 @@ private MethodSpec addStaticContextParamsMethod(OperationModel opModel) { 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); + String fieldName = staticListFieldName(opModel, n); + b.addStatement("params.$N($N)", setterName, fieldName); break; default: throw new RuntimeException("Don't know how to set parameter of type " + value.asToken()); @@ -358,6 +362,57 @@ private String staticContextParamsMethodName(OperationModel opModel) { return opModel.getMethodName() + "StaticContextParams"; } + /** + * Generates the name of the {@code static final List} field holding the static array value of + * {@code paramName} for {@code opModel}. + * + *

Format: {@code STATIC_LIST_{OPERATION}_{PARAM}} + */ + private static String staticListFieldName(OperationModel opModel, String paramName) { + return "STATIC_LIST_" + screamCase(opModel.getOperationName()) + "_" + screamCase(paramName); + } + + private static String screamCase(String word) { + return Stream.of(CodegenNamingUtils.splitOnWordBoundaries(word)) + .map(s -> s.toUpperCase(Locale.US)) + .collect(Collectors.joining("_")); + } + + /** + * Generates a {@code private static final List} field for every {@code staticContextParams} entry whose + * value is an array, so that {@code setStaticContextParams} hands the same list reference to the endpoint params + * builder on every call rather than constructing a new list each time. + */ + private void addStaticListFields(TypeSpec.Builder classBuilder) { + ParameterizedTypeName listOfString = ParameterizedTypeName.get(List.class, String.class); + + model.getOperations().forEach((opName, opModel) -> { + Map statics = opModel.getStaticContextParams(); + if (CollectionUtils.isNullOrEmpty(statics)) { + return; + } + statics.forEach((paramName, scp) -> { + TreeNode value = scp.getValue(); + if (value.asToken() != JsonToken.START_ARRAY) { + return; + } + JrsArray arrayValue = (JrsArray) value; + CodeBlock initializer; + if (arrayValue.size() == 0) { + initializer = CodeBlock.of("$T.emptyList()", Collections.class); + } else { + initializer = CodeBlock.of("$T.unmodifiableList($L)", Collections.class, + endpointRulesSpecUtils.treeNodeToLiteral(arrayValue)); + } + FieldSpec field = FieldSpec.builder(listOfString, staticListFieldName(opModel, paramName), + Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL) + .initializer(initializer) + .build(); + classBuilder.addField(field); + }); + }); + } + private boolean hasStaticContextParams(OperationModel opModel) { Map staticContextParams = opModel.getStaticContextParams(); return staticContextParams != null && !staticContextParams.isEmpty(); diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/bdd/BddEndpointProviderSpec.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/bdd/BddEndpointProviderSpec.java index 3fb181eaa637..614498045d0d 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/bdd/BddEndpointProviderSpec.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/bdd/BddEndpointProviderSpec.java @@ -30,6 +30,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.concurrent.CompletableFuture; import javax.lang.model.element.Modifier; import software.amazon.awssdk.annotations.SdkInternalApi; @@ -41,6 +42,7 @@ import software.amazon.awssdk.codegen.model.rules.endpoints.ConditionModel; import software.amazon.awssdk.codegen.model.rules.endpoints.ParameterModel; import software.amazon.awssdk.codegen.model.rules.endpoints.RuleModel; +import software.amazon.awssdk.codegen.model.service.ClientContextParam; import software.amazon.awssdk.codegen.model.service.EndpointBddModel; import software.amazon.awssdk.codegen.poet.ClassSpec; import software.amazon.awssdk.codegen.poet.PoetUtils; @@ -80,6 +82,20 @@ public class BddEndpointProviderSpec implements ClassSpec { */ private static final int NO_MATCH_RESULT = 100_000_000; + /** + * A {@code stringArray} cache key parameter longer than this reports a miss without comparing elements, so the cost + * of a cache check cannot grow with the size of the caller's list. + * + *

The cap is a cost bound, not a coverage target. Above it the provider resolves, which is what it would have + * done anyway, so the value trades hit rate against a bounded worst case and can never affect correctness. Four is + * arbitrary but deliberately conservative. + * + *

No shipped service needs this path today. DynamoDB's {@code ResourceArnList} is the only {@code stringArray} + * any shipped rule set declares, and it is read only at index 0, so it is compared by + * {@link #cacheFirstElementsMatchMethod()} with no cap at all. + */ + private static final int MAX_LIST_COMPARISON_SIZE = 4; + private final IntermediateModel intermediateModel; private final EndpointBddModel endpointBddModel; private final EndpointRulesSpecUtils endpointRulesSpecUtils; @@ -87,8 +103,10 @@ public class BddEndpointProviderSpec implements ClassSpec { private final RuleRuntimeTypeMirror typeMirror; private final Map registerInfoMap; private final ClassName evaluatorType; + private final ClassName cacheEntryType; private final List bddNodes; private final List conditionTypes; + private final Map paramUsage; public BddEndpointProviderSpec(IntermediateModel intermediateModel) { this.intermediateModel = intermediateModel; @@ -99,8 +117,10 @@ public BddEndpointProviderSpec(IntermediateModel intermediateModel) { this.knownEndpointAttributes = knownEndpointAttributes(intermediateModel); this.registerInfoMap = buildRegisterInfoMap(); this.evaluatorType = className().nestedClass("Evaluator"); + this.cacheEntryType = className().nestedClass("CacheEntry"); this.bddNodes = endpointBddModel.getDecodedNodes(); this.conditionTypes = analyzeConditions(); + this.paramUsage = BddParameterReferences.analyze(endpointBddModel); } @Override @@ -110,12 +130,273 @@ public TypeSpec poetSpec() { .addSuperinterface(endpointRulesSpecUtils.providerInterfaceName()) .addAnnotation(SdkInternalApi.class); + builder.addField(cacheField()); builder.addType(evaluatorClass()); + builder.addType(cacheEntryClass()); builder.addMethod(resolveEndpointMethod()); + builder.addMethod(cacheParamsMatchMethod()); + if (needsFullListHelper()) { + builder.addMethod(cacheListsMatchMethod()); + } + if (needsFirstElementHelper()) { + builder.addMethod(cacheFirstElementsMatchMethod()); + } return builder.build(); } + // ---- Single-entry result cache ---- + + /** + * Generates {@code private volatile CacheEntry cache;}. + * + *

One entry, holding the most recent successfully resolved {@code (params, endpoint)} pair. A single entry is + * enough because the overwhelmingly common shape is a client resolving the same endpoint repeatedly: same region, + * same flags, and for most services no request-derived parameters at all. A service whose endpoint genuinely varies + * per request simply misses every time and pays only the key check. + * + *

{@code volatile} is the whole of the synchronisation. Racing threads compute equivalent entries for equal + * params, so a lost write costs one re-resolution and nothing more; {@code CacheEntry} is immutable with final + * fields, so a thread that reads the reference sees fully initialised contents. + */ + private FieldSpec cacheField() { + return FieldSpec.builder(cacheEntryType, "cache") + .addModifiers(Modifier.PRIVATE, Modifier.VOLATILE) + .build(); + } + + /** + * Generates the immutable {@code CacheEntry} holding one {@code (params, endpoint)} snapshot. + * + *

The entry keeps the caller's params object rather than copying it, so it relies on that object and any + * collections it holds being effectively immutable after {@code build()}. Generated params do not copy list values + * ({@code this.resourceArnList = builder.resourceArnList;}), so the assumption is load-bearing rather than + * enforced. + * + *

It holds on every SDK path, because request objects are immutable and the params for a request are built and + * discarded within the call. It is an assumption only for a caller that invokes + * {@code EndpointProvider#resolveEndpoint} directly, retains a list it passed in, and mutates it afterwards: the + * mutation reaches the stored key, and a later call carrying the post-mutation contents can then hit an entry that + * was resolved for the pre-mutation contents. Snapshotting list parameters here would close that off, at the cost + * of an allocation on every miss. + */ + private TypeSpec cacheEntryClass() { + ClassName paramsClass = endpointRulesSpecUtils.parametersClassName(); + return TypeSpec.classBuilder(cacheEntryType) + .addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL) + .addField(paramsClass, "params", Modifier.FINAL) + .addField(Endpoint.class, "endpoint", Modifier.FINAL) + .addMethod(MethodSpec.constructorBuilder() + .addParameter(paramsClass, "params") + .addParameter(Endpoint.class, "endpoint") + .addStatement("this.params = params") + .addStatement("this.endpoint = endpoint") + .build()) + .build(); + } + + /** + * Generates {@code cacheParamsMatch(a, b)}: true when the two parameter objects are interchangeable as far as + * endpoint resolution is concerned. + * + *

One uniform {@link Objects#equals} term per parameter, joined with {@code &&} so the chain short-circuits on + * the first mismatch. {@code Objects.equals} tries identity before {@code equals}, which is what makes a single + * emitter sufficient: a parameter whose reference the SDK keeps stable settles on the identity check, and one that + * arrives as a fresh reference falls through to {@code equals} and still matches. + * + *

Parameter order comes from {@link #cacheKeyParameterOrder()}. Order is the only thing that varies between + * parameters, and it only affects how quickly a mismatch is found. + * + *

List-valued parameters are special cased to avoid slow comparisons for large lists. + * See {@link #cacheListsMatchMethod()}. + */ + private MethodSpec cacheParamsMatchMethod() { + ClassName paramsClass = endpointRulesSpecUtils.parametersClassName(); + Map parameters = endpointBddModel.getParameters(); + + MethodSpec.Builder b = MethodSpec.methodBuilder("cacheParamsMatch") + .addModifiers(Modifier.PRIVATE, Modifier.STATIC) + .returns(boolean.class) + .addParameter(paramsClass, "a") + .addParameter(paramsClass, "b"); + + CodeBlock.Builder chain = CodeBlock.builder().add("return "); + boolean first = true; + for (String paramName : cacheKeyParameterOrder()) { + String getter = endpointRulesSpecUtils.paramMethodName(paramName) + "()"; + if (!first) { + chain.add("\n && "); + } + if (!isListParam(parameters.get(paramName))) { + chain.add("$T.equals(a.$L, b.$L)", Objects.class, getter, getter); + } else if (paramUsage.get(paramName) == BddParameterReferences.Usage.FIRST_ELEMENT_ONLY) { + chain.add("cacheFirstElementsMatch(a.$L, b.$L)", getter, getter); + } else { + chain.add("cacheListsMatch(a.$L, b.$L)", getter, getter); + } + first = false; + } + if (first) { + // Either the model declares no parameters, or it reads none of them. Both mean one endpoint for every + // request, so any two parameter objects are interchangeable. + chain.add("true"); + } + b.addStatement(chain.build()); + return b.build(); + } + + /** + * Returns the parameter names in the order the generated cache key compares them: booleans, then strings whose + * reference the SDK keeps stable across requests, then everything else. Each group keeps the model's declaration + * order, so the result is deterministic across builds. + * + *

Ordering exists only to reach a mismatch sooner. It cannot change the outcome, because the chain compares + * every parameter before returning true. Booleans come first because they can never fall through to a real + * {@code equals}; reference-stable strings come next because they normally settle on the identity check; and the + * request-derived values that may have to compare characters come last. + */ + private List cacheKeyParameterOrder() { + Map parameters = endpointBddModel.getParameters(); + Map clientContextParams = intermediateModel.getClientContextParams(); + + List booleans = new ArrayList<>(); + List stableStrings = new ArrayList<>(); + List rest = new ArrayList<>(); + + parameters.forEach((name, model) -> { + if (paramUsage.get(name) == BddParameterReferences.Usage.UNREFERENCED) { + // Nothing reads it, so it cannot change the endpoint and must not force a miss. + return; + } + if (isBooleanParam(model)) { + booleans.add(name); + } else if (isReferenceStable(name, model, clientContextParams)) { + stableStrings.add(name); + } else { + rest.add(name); + } + }); + + List order = new ArrayList<>(booleans.size() + stableStrings.size() + rest.size()); + order.addAll(booleans); + order.addAll(stableStrings); + order.addAll(rest); + return order; + } + + /** + * Returns true for a string parameter the SDK hands to every request as the same reference: {@code AWS::Region}, + * which {@code Region.of} interns, and {@code clientContextParams}, which are read from the client's + * {@code AttributeMap}. + * + *

Only used to order the comparison. If one of these ever stops being reference-stable, the + * {@code Objects.equals} term still compares it correctly; the check simply costs an extra call. + */ + private static boolean isReferenceStable(String paramName, + ParameterModel model, + Map clientContextParams) { + if (model.getBuiltInEnum() == BuiltInParameter.AWS_REGION) { + return true; + } + if (clientContextParams == null) { + return false; + } + // Endpoint parameter names are unique case-insensitively, so a case-insensitive match is the same parameter. + for (String key : clientContextParams.keySet()) { + if (key.equalsIgnoreCase(paramName)) { + return true; + } + } + return false; + } + + /** + * Generates the {@code cacheListsMatch} helper, emitted only when the model declares a {@code stringArray} + * parameter. + * + *

{@code Objects.equals} would be correct here, but {@code List.equals} is unbounded: a request carrying a large + * list would walk every element on every cache check, which can cost more than the resolution the cache exists to + * avoid. The comparison is therefore capped at {@value #MAX_LIST_COMPARISON_SIZE} elements. A service handling + * longer lists simply misses and pays resolution, which is what it would have paid anyway. + */ + private MethodSpec cacheListsMatchMethod() { + TypeName listOfString = RuleRuntimeTypeMirror.LIST_OF_STRING.type(); + return MethodSpec.methodBuilder("cacheListsMatch") + .addModifiers(Modifier.PRIVATE, Modifier.STATIC) + .returns(boolean.class) + .addParameter(listOfString, "a") + .addParameter(listOfString, "b") + .addStatement("if (a == b) return true") + .addStatement("if (a == null || b == null) return false") + .addStatement("int size = a.size()") + .addStatement("if (size != b.size()) return false") + .addComment("Bounded so that a long list cannot make the cache check cost more than " + + "resolving.") + .addStatement("if (size > $L) return false", MAX_LIST_COMPARISON_SIZE) + .beginControlFlow("for (int i = 0; i < size; i++)") + .addStatement("if (!$T.equals(a.get(i), b.get(i))) return false", Objects.class) + .endControlFlow() + .addStatement("return true") + .build(); + } + + /** + * Generates the {@code cacheFirstElementsMatch} helper, emitted only when a {@code stringArray} parameter is read + * exclusively as {@code param[0]}. + * + *

When the rules can only see whether the list is present and what its first element is, comparing the rest is + * work that cannot change the answer. + */ + private MethodSpec cacheFirstElementsMatchMethod() { + TypeName listOfString = RuleRuntimeTypeMirror.LIST_OF_STRING.type(); + return MethodSpec.methodBuilder("cacheFirstElementsMatch") + .addModifiers(Modifier.PRIVATE, Modifier.STATIC) + .returns(boolean.class) + .addParameter(listOfString, "a") + .addParameter(listOfString, "b") + .addStatement("if (a == b) return true") + .addComment("isSet can tell an absent list from an empty one, so presence is part of the key.") + .addStatement("if (a == null || b == null) return false") + .addComment("Nothing past element 0 can reach the endpoint.") + .addStatement("$T firstA = a.isEmpty() ? null : a.get(0)", String.class) + .addStatement("$T firstB = b.isEmpty() ? null : b.get(0)", String.class) + .addStatement("return $T.equals(firstA, firstB)", Objects.class) + .build(); + } + + /** + * True when some list parameter in the cache key needs a whole-list comparison. + */ + private boolean needsFullListHelper() { + return listParamsInKeyWithUsage(BddParameterReferences.Usage.FULL); + } + + /** + * True when some list parameter in the cache key is read only at index 0. + */ + private boolean needsFirstElementHelper() { + return listParamsInKeyWithUsage(BddParameterReferences.Usage.FIRST_ELEMENT_ONLY); + } + + private boolean listParamsInKeyWithUsage(BddParameterReferences.Usage usage) { + Map parameters = endpointBddModel.getParameters(); + return cacheKeyParameterOrder().stream() + .filter(name -> isListParam(parameters.get(name))) + .anyMatch(name -> paramUsage.get(name) == usage); + } + + private static boolean isBooleanParam(ParameterModel model) { + return "boolean".equalsIgnoreCase(model.getType()); + } + + /** + * Shared with {@link BddParameterReferences}, which must agree with this class on what a list is: the usage it + * derives selects which comparison helper gets emitted for the parameter. + */ + static boolean isListParam(ParameterModel model) { + return "stringarray".equalsIgnoreCase(model.getType()); + } + private TypeSpec evaluatorClass() { TypeSpec.Builder builder = TypeSpec.classBuilder(evaluatorType) .addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL); @@ -386,6 +667,15 @@ private MethodSpec resolveEndpointMethod() { builder.addCode(validateRequiredParams()); + // Cache check. This sits after required-param validation so that invalid params fail the same way on a hit as + // on a miss. One volatile read into a local, so the entry cannot be replaced between the null check and the + // comparison. + builder.addComment("Single-entry result cache: reuse the last endpoint when the params still match."); + builder.addStatement("$T cached = this.cache", cacheEntryType); + builder.beginControlFlow("if (cached != null && cacheParamsMatch(endpointParams, cached.params))"); + builder.addStatement("return $T.completedFuture(cached.endpoint)", CompletableFuture.class); + builder.endControlFlow(); + builder.beginControlFlow("try"); // Allocate evaluator per call — lightweight (just fields, no maps), immediately young-gen collected. @@ -409,6 +699,9 @@ private MethodSpec resolveEndpointMethod() { CompletableFutureUtils.class, SdkClientException.class, "Rule engine did not reach an error or endpoint result") .endControlFlow(); + // Populate on success only. A rule error and a no-match both leave the previous entry in place, so a transient + // bad-params call cannot poison the cache and an error is never replayed from it. + builder.addStatement("this.cache = new $T(endpointParams, result)", cacheEntryType); builder.addStatement("return $T.completedFuture(result)", CompletableFuture.class); // Catch errors thrown from result methods diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/bdd/BddParameterReferences.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/bdd/BddParameterReferences.java new file mode 100644 index 000000000000..04439e4a718b --- /dev/null +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/bdd/BddParameterReferences.java @@ -0,0 +1,180 @@ +/* + * 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.rules2.bdd; + +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import software.amazon.awssdk.codegen.model.rules.endpoints.ConditionModel; +import software.amazon.awssdk.codegen.model.rules.endpoints.ParameterModel; +import software.amazon.awssdk.codegen.model.rules.endpoints.RuleModel; +import software.amazon.awssdk.codegen.model.service.EndpointBddModel; +import software.amazon.awssdk.codegen.poet.rules2.ExpressionParser; +import software.amazon.awssdk.codegen.poet.rules2.FunctionCallExpression; +import software.amazon.awssdk.codegen.poet.rules2.IndexedAccessExpression; +import software.amazon.awssdk.codegen.poet.rules2.MemberAccessExpression; +import software.amazon.awssdk.codegen.poet.rules2.RuleExpression; +import software.amazon.awssdk.codegen.poet.rules2.VariableReferenceExpression; +import software.amazon.awssdk.codegen.poet.rules2.WalkRuleExpressionVisitor; + +/** + * Works out how each endpoint parameter is used by a BDD, so that the generated result cache only compares what can + * actually change the resolved endpoint. + * + *

Analysis runs over the BDD's conditions and results using the same parser the generator uses, so the answer + * reflects what the generated provider reads rather than what the model happens to declare. + * + *

Two things fall out of it: + * + *

    + *
  • {@link Usage#UNREFERENCED} - a parameter no condition and no result reads. It cannot change the endpoint, so + * comparing it can only produce misses that resolve to the endpoint already cached. S3 is the motivating case: + * it declares {@code Key}, {@code Prefix} and {@code CopySource}, no rule reads any of them, and {@code Key} + * changes on essentially every object request - so including it means the cache almost never hits.
  • + *
  • {@link Usage#FIRST_ELEMENT_ONLY} - a {@code stringArray} whose every use is an index-0 access. Only the first + * element can reach the endpoint, so the generated comparison is optimized by comparing only that element instead + * of walking the list.
  • + *
+ */ +final class BddParameterReferences { + + /** + * How much of a parameter's value can influence the resolved endpoint. + */ + enum Usage { + /** No condition and no result reads it. It cannot be part of the cache key. */ + UNREFERENCED, + + /** + * A {@code stringArray} the rules can only observe through {@code isSet(param)} and {@code param[0]}. Whether + * the list is present, plus its first element, is therefore the whole of what can reach the endpoint, and + * nothing past element 0 can change the answer. + */ + FIRST_ELEMENT_ONLY, + + /** Read in a way that can depend on the entire value. */ + FULL + } + + private BddParameterReferences() { + } + + /** + * Returns the usage of every parameter the model declares, in declaration order. + */ + static Map analyze(EndpointBddModel model) { + Collector collector = new Collector(); + + for (ConditionModel condition : model.getConditions()) { + RuleModel synthetic = new RuleModel(); + synthetic.setType("error"); + synthetic.setError("synthetic"); + synthetic.setConditions(Collections.singletonList(condition)); + ExpressionParser.parseRuleSetExpression(synthetic).accept(collector); + } + for (RuleModel result : model.getResults()) { + ExpressionParser.parseRuleSetExpression(result).accept(collector); + } + + Map usage = new LinkedHashMap<>(); + model.getParameters().forEach((name, parameter) -> usage.put(name, usageOf(name, parameter, collector))); + return Collections.unmodifiableMap(usage); + } + + private static Usage usageOf(String name, ParameterModel parameter, Collector collector) { + if (!collector.referenced.contains(name)) { + return Usage.UNREFERENCED; + } + if (collector.wholeValue.contains(name)) { + return Usage.FULL; + } + // Only lists benefit, and only lists can be read element-wise. Anything else that somehow reached here is + // compared in full rather than guessed at. + return BddEndpointProviderSpec.isListParam(parameter) ? Usage.FIRST_ELEMENT_ONLY : Usage.FULL; + } + + /** + * Collects, for every name the expressions reference, whether any reference needs more than the first element. + * + *

Erring towards {@link Usage#FULL} is the safe direction: it costs comparison work, whereas erring the other + * way would drop something from the cache key that can change the endpoint. + */ + private static final class Collector extends WalkRuleExpressionVisitor { + private final Set referenced = new HashSet<>(); + private final Set wholeValue = new HashSet<>(); + + @Override + public Void visitIndexedAccessExpression(IndexedAccessExpression e) { + String subject = firstElementSubject(e); + if (subject != null) { + referenced.add(subject); + // Deliberately not descending. Descending would reach the variable reference underneath and record it + // as a whole-value read, which is the thing this case exists to avoid. + return null; + } + return super.visitIndexedAccessExpression(e); + } + + /** + * {@code isSet(param)} observes only whether the parameter is present, so on its own it does not force a + * whole-value comparison. + */ + @Override + public Void visitFunctionCallExpression(FunctionCallExpression e) { + if ("isSet".equals(e.name()) && e.arguments().size() == 1) { + RuleExpression argument = e.arguments().get(0); + if (argument instanceof VariableReferenceExpression) { + referenced.add(((VariableReferenceExpression) argument).variableName()); + return null; + } + } + return super.visitFunctionCallExpression(e); + } + + @Override + public Void visitVariableReferenceExpression(VariableReferenceExpression e) { + referenced.add(e.variableName()); + wholeValue.add(e.variableName()); + return null; + } + + /** + * Returns the parameter name when this is an index-0 read of a plain parameter, otherwise null. + * + *

Both spellings reach here: {@code "list[0]"} inside a template parses to an indexed access straight over + * the variable, while {@code getAttr(list, "[0]")} wraps it in a direct-index member access first. + */ + private static String firstElementSubject(IndexedAccessExpression e) { + if (e.index() != 0) { + return null; + } + RuleExpression source = e.source(); + if (source instanceof MemberAccessExpression) { + MemberAccessExpression member = (MemberAccessExpression) source; + if (!member.directIndex()) { + return null; + } + source = member.source(); + } + if (source instanceof VariableReferenceExpression) { + return ((VariableReferenceExpression) source).variableName(); + } + return null; + } + } +} diff --git a/codegen/src/main/resources/software/amazon/awssdk/codegen/rules2/RulesFunctions.java.resource b/codegen/src/main/resources/software/amazon/awssdk/codegen/rules2/RulesFunctions.java.resource index 69b3d79dacbb..93a7c28412c6 100644 --- a/codegen/src/main/resources/software/amazon/awssdk/codegen/rules2/RulesFunctions.java.resource +++ b/codegen/src/main/resources/software/amazon/awssdk/codegen/rules2/RulesFunctions.java.resource @@ -48,7 +48,44 @@ public class RulesFunctions { return args[args.length - 1]; } + /** + * Splits {@code value} around {@code delimiter} into at most {@code limit} parts, or into as many parts + * as there are when {@code limit} is 0. + * + *

None of the three arguments is optional. The rules language types {@code value} as a plain string, + * which its type checker only permits once the value is known to be set, and the specification requires a + * delimiter that is neither null nor empty and a limit that is not negative. A violation therefore means + * the rule set, or the code generated from it, is wrong - not that the request was unusual - so each is + * raised as an {@link SdkClientException} naming the offending argument. Generated providers surface that + * as a failed resolution rather than letting it escape as an unchecked exception. + * + *

The checks precede the {@code limit == 1} shortcut deliberately. That shortcut returns before either + * string is dereferenced, so validating after it would let {@code split(null, ",", 1)} return a list + * holding null, which reads downstream as an unset value and quietly redirects the rule rather than + * failing. + * + * @throws SdkClientException if {@code value} is null, if {@code delimiter} is null or empty, or if + * {@code limit} is negative. + */ public static List split(String value, String delimiter, int limit) { + if (value == null) { + throw SdkClientException.create( + "Cannot split a null value. The endpoint rules language requires a value to be set before it " + + "is split, so this is a defect in the rule set or in the provider generated from it."); + } + if (delimiter == null || delimiter.isEmpty()) { + // An empty delimiter is the dangerous one: indexOf("") matches at the current position forever, so + // an unlimited split would accumulate empty strings until the heap is gone. + throw SdkClientException.create( + "Cannot split on " + (delimiter == null ? "a null" : "an empty") + " delimiter. The endpoint " + + "rules specification requires a delimiter that is neither null nor empty."); + } + if (limit < 0) { + throw SdkClientException.create( + "Cannot split with a negative limit (" + limit + "). The endpoint rules specification requires " + + "a limit of 0, meaning unlimited, or greater."); + } + if (limit == 1) { return Collections.singletonList(value); } @@ -58,7 +95,9 @@ public class RulesFunctions { final int delimLen = delimiter.length(); // Unlimited split if limit == 0 final int maxSplits = (limit == 0) ? Integer.MAX_VALUE : limit - 1; - List result = new ArrayList<>(Math.min(limit, 8)); + // limit 0 means unlimited, so it carries no size hint; Math.min would turn it into a capacity of zero + // and make the list regrow from empty on the most common call shape. + List result = new ArrayList<>(limit == 0 ? 8 : Math.min(limit, 8)); int fromIndex = 0; int splits = 0; while (splits < maxSplits) { @@ -249,8 +288,11 @@ public class RulesFunctions { * *

{@link RulePartition} is immutable and is a pure function of the matched {@link Partition}, * of which there are only a handful, loaded once. The instances are therefore built up front in - * {@link #loadPartitionData()} and shared, so this method allocates nothing and holds no - * per-call state. + * {@link #loadPartitionData()} and shared, so this method allocates nothing on any path that + * resolves, and holds no per-call state. + * + * @throws SdkClientException if the region matches no partition and the loaded metadata declares no + * 'aws' partition to fall back to. */ public static RulePartition awsPartition(String regionName) { PartitionData data = PARTITION_DATA.value(); @@ -271,6 +313,19 @@ public class RulesFunctions { } // Couldn't find the region by name or pattern matching. Fallback to 'aws' partition. + // + // Only this path needs 'aws', so it is also the only path that can reject partition metadata for + // lacking it. Validating at load instead would refuse every region, including the ones the + // metadata does describe. Returning null is not an option either: generated code null-checks the + // result and would read null as "condition not satisfied", quietly taking a different rule branch. + if (data.awsRulePartition == null) { + throw SdkClientException.create( + "Region '" + regionName + "' matched no partition by name or by region pattern, and the " + + "partition metadata in use does not declare the 'aws' partition that is the fallback for " + + "an unmatched region. Check the partitions file supplied through the aws.partitionsFile " + + "system property, the AWS_PARTITIONS_FILE environment variable, or a " + + "software/amazon/awssdk/global/partitions.json resource on the classpath."); + } return data.awsRulePartition; } @@ -426,22 +481,17 @@ public class RulesFunctions { } } - // awsPartition falls back to the 'aws' partition for any region it cannot match. Fail loudly - // here rather than returning null from awsPartition, which generated code null-checks and - // would silently treat as "condition not satisfied", taking a different rule branch. - if (awsRulePartition == null) { - throw SdkClientException.create( - "Endpoint partition metadata is missing the 'aws' partition, which is required as the " - + "fallback for unmatched regions. This indicates a corrupt or incomplete partitions.json."); - } - + // Stays null when the metadata declares no 'aws' partition. That only matters for a region this + // data cannot match, so awsPartition raises it there rather than failing the load and taking the + // regions the metadata does describe down with it. return new PartitionData(entries, regionMap, awsRulePartition); } private static final class PartitionData { private final List partitions; private final Map regionMap; - // Fallback used when a region matches neither a known region name nor a partition pattern. + // Fallback used when a region matches neither a known region name nor a partition pattern. Null if + // the loaded metadata declares no 'aws' partition, which only fails the requests that need it. private final RulePartition awsRulePartition; private PartitionData(List partitions, Map regionMap, diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/ClientTestModels.java b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/ClientTestModels.java index 8137ad2213f5..d59eb4cf701a 100644 --- a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/ClientTestModels.java +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/ClientTestModels.java @@ -632,7 +632,7 @@ public static IntermediateModel batchManagerModels() { return new IntermediateModelBuilder(models).build(); } - + public static IntermediateModel presignedUrlExtensionModels() { File serviceModel = new File(ClientTestModels.class.getResource("client/c2j/presignedurl/service-2.json").getFile()); File customizationModel = new File(ClientTestModels.class.getResource("client/c2j/presignedurl/customization.config").getFile()); @@ -656,8 +656,11 @@ public static IntermediateModel queryServiceModelsWithBddEndpoints() { public static IntermediateModel queryServiceModelsWithBddEndpoints(boolean useS3ExpressSessionAuth) { File serviceModel = new File(ClientTestModels.class.getResource("client/c2j/query/service-2.json").getFile()); File waitersModel = new File(ClientTestModels.class.getResource("client/c2j/query/waiters-2.json").getFile()); + // The S3 rule set, not the default-regional one, because it declares the same 17 parameters as the S3 BDD. The + // generated params class comes from the rule set while the provider body comes from the BDD, so pairing the S3 + // BDD with a 4-parameter rule set would produce a provider referencing getters the params class does not have. File endpointRuleSetModel = - new File(ClientTestModels.class.getResource("client/c2j/query/endpoint-rule-set-default-regional.json").getFile()); + new File(ClientTestModels.class.getResource("client/c2j/s3-test/endpoint-rule-set.json").getFile()); File endpointTestsModel = new File(ClientTestModels.class.getResource("client/c2j/query/endpoint-tests.json").getFile()); File endpointBddModel = diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules2/bdd/BddEndpointProviderSpecTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules2/bdd/BddEndpointProviderSpecTest.java index 32e819a3f791..2ebf862b2461 100644 --- a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules2/bdd/BddEndpointProviderSpecTest.java +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules2/bdd/BddEndpointProviderSpecTest.java @@ -19,12 +19,23 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static software.amazon.awssdk.codegen.poet.PoetMatchers.generatesTo; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.Test; import software.amazon.awssdk.codegen.poet.ClientTestModels; public class BddEndpointProviderSpecTest { + /** + * The trailing nodes of {@code endpoint-bdd-default-regional.json} were appended by hand with {@code high == low}, + * so that each parameter is read by a condition without altering any resolved endpoint. That is the one shape a + * reduced BDD can never contain, which is why it shows up in the golden file as degenerate {@code nodeP} methods + * whose branches are identical. A future peephole pass that collapses {@code high == low} nodes would silently make + * those parameters unreferenced and void the cache-key coverage the tests below rely on. + */ @Test void endpointProviderClass_simpleBdd_generatesExpectedCode() { BddEndpointProviderSpec spec = new BddEndpointProviderSpec( @@ -146,6 +157,165 @@ void complementEdge_generatesNodeNWithSwappedBranches() { assertThat(generated).contains("Endpoint nodeN1()"); } + /** + * The invariant the result cache depends on: every parameter the BDD reads must appear in the generated + * key. A parameter left out is not a slow cache, it is a cache that returns an endpoint resolved for a different + * value of that parameter, and nothing else in the test suite would catch it. + * + *

Asserted against the generated source rather than an intermediate model, so it holds regardless of how the + * comparison is built. + */ + @Test + void cacheKeyComparesEveryReferencedParameter() { + // The simple BDD declares 10 parameters and reads 9; unusedParam is the one it does not read. + assertThat(cacheKeyGetterOrder( + new BddEndpointProviderSpec(ClientTestModels.queryServiceModelsWithSimpleBddEndpoints()))) + .containsExactlyInAnyOrder("useDualStack", "useFips", "region", "stringContextParam", "endpoint", + "staticStringParam", "operationContextParam", "arnList", + "customEndpointArray"); + + // The S3 BDD declares 17 and reads 14; Key, Prefix and CopySource are vestigial declarations. + assertThat(cacheKeyGetterOrder( + new BddEndpointProviderSpec(ClientTestModels.queryServiceModelsWithBddEndpoints()))) + .containsExactlyInAnyOrder("useFips", "useDualStack", "forcePathStyle", "accelerate", "useGlobalEndpoint", + "useObjectLambdaEndpoint", "disableAccessPoints", + "disableMultiRegionAccessPoints", "useArnRegion", + "useS3ExpressControlEndpoint", "disableS3ExpressSessionAuth", "region", + "bucket", "endpoint"); + + // The complement BDD declares and reads exactly two. + assertThat(cacheKeyGetterOrder( + new BddEndpointProviderSpec(ClientTestModels.queryServiceModelsWithComplementBddEndpoints()))) + .containsExactlyInAnyOrder("region", "endpoint"); + } + + /** + * A parameter no condition and no result reads cannot change the resolved endpoint, so comparing it could only turn + * hits into misses that resolve to the endpoint already cached. + * + *

This is what makes the cache worth having for S3, whose rule set declares {@code Key}, {@code Prefix} and + * {@code CopySource} and reads none of them. {@code Key} changes on essentially every object request, so including + * it would mean the cache almost never hits. + */ + @Test + void cacheKeyOmitsParametersTheBddNeverReads() { + assertThat(cacheKeyGetterOrder( + new BddEndpointProviderSpec(ClientTestModels.queryServiceModelsWithSimpleBddEndpoints()))) + .as("a parameter nothing reads must not force a cache miss") + .doesNotContain("unusedParam"); + + assertThat(cacheKeyGetterOrder( + new BddEndpointProviderSpec(ClientTestModels.queryServiceModelsWithBddEndpoints()))) + .as("S3 declares Key, Prefix and CopySource but reads none of them") + .doesNotContain("key", "prefix", "copySource") + .contains("bucket"); + } + + /** + * The generated key compares booleans first, then the strings whose reference the SDK keeps stable, then everything + * else. Ordering cannot change the result - the chain compares every parameter before returning true - it only + * decides how quickly a mismatch is found, so this is a performance property rather than a correctness one. It is + * pinned because the ordering is the entire reason the grouping exists; if it silently degraded to declaration + * order the code would still be correct and the benefit would be gone. + */ + @Test + void cacheKeyOrdersBooleansThenStableStringsThenTheRest() { + List order = cacheKeyGetterOrder( + new BddEndpointProviderSpec(ClientTestModels.queryServiceModelsWithSimpleBddEndpoints())); + + assertThat(order).containsExactly("useDualStack", "useFips", // booleans + "region", "stringContextParam", // reference-stable strings + "endpoint", "staticStringParam", // everything else, declaration order + "operationContextParam", "arnList", "customEndpointArray"); + } + + /** + * A list read as a whole needs a bounded comparison, so it routes through the emitted helper rather than + * {@code Objects.equals}, whose {@code List.equals} would walk every element however long the list is. + */ + @Test + void listReadAsAWholeUsesTheBoundedHelper() { + String generated = new BddEndpointProviderSpec( + ClientTestModels.queryServiceModelsWithSimpleBddEndpoints()).poetSpec().toString(); + + assertThat(generated).contains("cacheListsMatch(a.customEndpointArray(), b.customEndpointArray())"); + assertThat(generated).contains("if (size > 4) return false"); + } + + /** + * When the rules can only see whether a list is present and what its first element is, the rest of the list cannot + * reach the endpoint, so the key compares presence and element 0. This is the DynamoDB shape: it reads + * {@code ResourceArnList} through {@code isSet} and {@code getAttr(ResourceArnList, "[0]")} and nothing else, and + * comparing the whole list costs more than half of a regional resolution. + * + *

The {@code isSet} guard is not incidental. The rules language requires a null check before an indexed access, + * so every real model that reads {@code list[0]} also reads {@code isSet(list)}; if the null check disqualified the + * parameter this case would never fire in production. + * + *

Also asserts the generated provider really does read only element 0, so the two halves cannot drift apart: were + * a read past the head to appear, this comparison would silently become wrong. + */ + @Test + void listReadOnlyAtIndexZeroComparesPresenceAndTheFirstElement() { + String generated = new BddEndpointProviderSpec( + ClientTestModels.queryServiceModelsWithSimpleBddEndpoints()).poetSpec().toString(); + + assertThat(generated).contains("cacheFirstElementsMatch(a.arnList(), b.arnList())"); + assertThat(generated).doesNotContain("cacheListsMatch(a.arnList()"); + assertThat(generated) + .as("the first-element comparison is only valid while the provider reads nothing past element 0") + .contains("RulesFunctions.listAccess(params.arnList(), 0)") + .doesNotContain("RulesFunctions.listAccess(params.arnList(), 1)"); + assertThat(generated) + .as("isSet distinguishes an absent list from an empty one, so presence stays part of the key") + .contains("if (a == null || b == null) return false"); + } + + /** + * A read past the head disqualifies the first-element comparison, because elements beyond the first can then reach + * the endpoint. Pinned because the difference between the two helpers is a correctness boundary, not a preference. + */ + @Test + void listReadPastTheHeadIsComparedInFull() { + String generated = new BddEndpointProviderSpec( + ClientTestModels.queryServiceModelsWithSimpleBddEndpoints()).poetSpec().toString(); + + assertThat(generated).contains("cacheListsMatch(a.customEndpointArray(), b.customEndpointArray())"); + assertThat(generated).doesNotContain("cacheFirstElementsMatch(a.customEndpointArray()"); + assertThat(generated).contains("RulesFunctions.listAccess(params.customEndpointArray(), 1)"); + } + + /** + * Neither helper is worth emitting when nothing needs it, and the S3 BDD keeps no list parameter in its key. + */ + @Test + void listHelpersAreOmittedWhenNoListParameterIsInTheKey() { + String generated = new BddEndpointProviderSpec( + ClientTestModels.queryServiceModelsWithBddEndpoints()).poetSpec().toString(); + + assertThat(generated).doesNotContain("cacheListsMatch"); + assertThat(generated).doesNotContain("cacheFirstElementsMatch"); + } + + /** + * Returns the parameter getters referenced by the generated {@code cacheParamsMatch}, in the order they are + * compared. + */ + private static List cacheKeyGetterOrder(BddEndpointProviderSpec spec) { + String generated = spec.poetSpec().toString(); + int start = generated.indexOf("boolean cacheParamsMatch("); + assertThat(start).as("generated provider must contain cacheParamsMatch").isNotNegative(); + int end = generated.indexOf(";", start); + String body = generated.substring(start, end); + + List getters = new ArrayList<>(); + Matcher matcher = Pattern.compile("\\ba\\.(\\w+)\\(\\)").matcher(body); + while (matcher.find()) { + getters.add(matcher.group(1)); + } + return getters; + } + /** * {@code DynamicEndpointAuthSchemeFactory} is S3-specific, so a dynamically resolved auth scheme name in any other * service must fail codegen rather than emitting code that cannot compile. diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/query/endpoint-bdd-default-regional.json b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/query/endpoint-bdd-default-regional.json index 9f7c8fc3a7e4..a05255ae456e 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/query/endpoint-bdd-default-regional.json +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/query/endpoint-bdd-default-regional.json @@ -26,6 +26,36 @@ "required": false, "documentation": "Override the endpoint used to send this request", "type": "string" + }, + "stringContextParam": { + "required": false, + "documentation": "A client context parameter, so the cache key compares it early as reference-stable.", + "type": "string" + }, + "staticStringParam": { + "required": false, + "documentation": "Bound to a per-operation static literal.", + "type": "string" + }, + "operationContextParam": { + "required": false, + "documentation": "Bound to a request member, so it can change per request.", + "type": "string" + }, + "arnList": { + "required": false, + "documentation": "Read only as arnList[0], so the cache key compares just the first element.", + "type": "stringArray" + }, + "customEndpointArray": { + "required": false, + "documentation": "Read as a whole, so the cache key compares every element.", + "type": "stringArray" + }, + "unusedParam": { + "required": false, + "documentation": "Declared but read by no condition and no result, so the cache key must leave it out.", + "type": "string" } }, "conditions": [ @@ -116,6 +146,66 @@ ] } ] + }, + { + "fn": "isSet", + "argv": [ + { + "ref": "stringContextParam" + } + ] + }, + { + "fn": "isSet", + "argv": [ + { + "ref": "staticStringParam" + } + ] + }, + { + "fn": "isSet", + "argv": [ + { + "ref": "operationContextParam" + } + ] + }, + { + "fn": "isSet", + "argv": [ + { + "ref": "customEndpointArray" + } + ] + }, + { + "fn": "getAttr", + "argv": [ + { + "ref": "arnList" + }, + "[0]" + ], + "assign": "FirstArn" + }, + { + "fn": "isSet", + "argv": [ + { + "ref": "arnList" + } + ] + }, + { + "fn": "getAttr", + "argv": [ + { + "ref": "customEndpointArray" + }, + "[1]" + ], + "assign": "SecondEndpoint" } ], "results": [ @@ -206,7 +296,7 @@ "type": "error" } ], - "root": 15, - "nodeCount": 15, - "nodes": "/////wAAAAH/////AAAABAX14QIF9eEDAAAAAgX14QEAAAACAAAABgX14QQF9eEFAAAABQAAAAQF9eEFAAAABwX14QYF9eEHAAAABQAAAAYF9eEIAAAABAAAAAUAAAAHAAAAAwAAAAgF9eEMAAAABgX14QkF9eEKAAAABAAAAAoF9eELAAAAAwAAAAsF9eEMAAAAAgAAAAkAAAAMAAAAAQAAAA0F9eEMAAAAAAAAAAMAAAAO" -} \ No newline at end of file + "root": 22, + "nodeCount": 22, + "nodes": "/////wAAAAH/////AAAABAX14QIF9eEDAAAAAgX14QEAAAACAAAABgX14QQF9eEFAAAABQAAAAQF9eEFAAAABwX14QYF9eEHAAAABQAAAAYF9eEIAAAABAAAAAUAAAAHAAAAAwAAAAgF9eEMAAAABgX14QkF9eEKAAAABAAAAAoF9eELAAAAAwAAAAsF9eEMAAAAAgAAAAkAAAAMAAAAAQAAAA0F9eEMAAAAAAAAAAMAAAAOAAAACAAAABEAAAARAAAACQAAABIAAAASAAAACgAAABMAAAATAAAACwAAABQAAAAUAAAADAAAAA8AAAAPAAAADQAAABAAAAAQAAAADgAAABUAAAAV" +} diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/query/endpoint-rule-set-default-regional.json b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/query/endpoint-rule-set-default-regional.json index e0314767c0df..6c3b34e6e15f 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/query/endpoint-rule-set-default-regional.json +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/query/endpoint-rule-set-default-regional.json @@ -26,6 +26,36 @@ "required": false, "documentation": "Override the endpoint used to send this request", "type": "string" + }, + "stringContextParam": { + "required": false, + "documentation": "A client context parameter, so the cache key compares it early as reference-stable.", + "type": "string" + }, + "staticStringParam": { + "required": false, + "documentation": "Bound to a per-operation static literal.", + "type": "string" + }, + "operationContextParam": { + "required": false, + "documentation": "Bound to a request member, so it can change per request.", + "type": "string" + }, + "arnList": { + "required": false, + "documentation": "Read only as arnList[0], so the cache key compares just the first element.", + "type": "stringArray" + }, + "customEndpointArray": { + "required": false, + "documentation": "Read as a whole, so the cache key compares every element.", + "type": "stringArray" + }, + "unusedParam": { + "required": false, + "documentation": "Declared but read by no condition and no result, so the cache key must leave it out.", + "type": "string" } }, "rules": [ @@ -336,4 +366,4 @@ "type": "error" } ] -} \ No newline at end of file +} diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolver-utils-with-endpointsbasedauth.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolver-utils-with-endpointsbasedauth.java index ad3b2e674750..2bb0ab811240 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolver-utils-with-endpointsbasedauth.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolver-utils-with-endpointsbasedauth.java @@ -191,7 +191,7 @@ private static String recordAccountIdEndpointMode(ExecutionAttributes executionA AccountIdEndpointMode mode = executionAttributes.getAttribute(AwsExecutionAttribute.AWS_AUTH_ACCOUNT_ID_ENDPOINT_MODE); BusinessMetricsUtils.resolveAccountIdEndpointModeMetric(mode).ifPresent( m -> executionAttributes.getAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS).addMetric(m)); - return mode.name().toLowerCase(); + return mode.value(); } public static void setMetricValues(Endpoint endpoint, ExecutionAttributes executionAttributes) { diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolver-utils-with-stringarray.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolver-utils-with-stringarray.java index aaed43624739..a3ff124832a3 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolver-utils-with-stringarray.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolver-utils-with-stringarray.java @@ -1,6 +1,7 @@ package software.amazon.awssdk.services.samplesvc.endpoints.internal; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Optional; import software.amazon.awssdk.annotations.Generated; @@ -28,6 +29,10 @@ @Generated("software.amazon.awssdk:codegen") @SdkInternalApi public final class SampleSvcEndpointResolverUtils { + private static final List STATIC_LIST_EMPTY_STATIC_CONTEXT_OPERATION_STRING_ARRAY_PARAM = Collections.emptyList(); + + private static final List STATIC_LIST_STATIC_CONTEXT_OPERATION_STRING_ARRAY_PARAM = Collections.unmodifiableList(Arrays.asList("staticValue1")); + private SampleSvcEndpointResolverUtils() { } @@ -57,12 +62,12 @@ private static void setStaticContextParams(SampleSvcEndpointParams.Builder param private static void emptyStaticContextOperationStaticContextParams( SampleSvcEndpointParams.Builder params) { - params.stringArrayParam(Arrays.asList()); + params.stringArrayParam(STATIC_LIST_EMPTY_STATIC_CONTEXT_OPERATION_STRING_ARRAY_PARAM); } private static void staticContextOperationStaticContextParams( SampleSvcEndpointParams.Builder params) { - params.stringArrayParam(Arrays.asList("staticValue1")); + params.stringArrayParam(STATIC_LIST_STATIC_CONTEXT_OPERATION_STRING_ARRAY_PARAM); } public static SelectedAuthScheme authSchemeWithEndpointSignerProperties( diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolver-utils.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolver-utils.java index 01c7ba43831f..e7033a06876e 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolver-utils.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/endpoint-resolver-utils.java @@ -198,7 +198,7 @@ private static String recordAccountIdEndpointMode(ExecutionAttributes executionA AccountIdEndpointMode mode = executionAttributes.getAttribute(AwsExecutionAttribute.AWS_AUTH_ACCOUNT_ID_ENDPOINT_MODE); BusinessMetricsUtils.resolveAccountIdEndpointModeMetric(mode).ifPresent( m -> executionAttributes.getAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS).addMetric(m)); - return mode.name().toLowerCase(); + return mode.value(); } public static void setMetricValues(Endpoint endpoint, ExecutionAttributes executionAttributes) { diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/bdd/endpoint-provider-bdd-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/bdd/endpoint-provider-bdd-class.java index 39e5da5ba9ff..c0566ec44f4d 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/bdd/endpoint-provider-bdd-class.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/bdd/endpoint-provider-bdd-class.java @@ -1,5 +1,7 @@ package software.amazon.awssdk.services.query.endpoints.internal; +import java.util.List; +import java.util.Objects; import java.util.concurrent.CompletableFuture; import software.amazon.awssdk.annotations.Generated; import software.amazon.awssdk.annotations.SdkInternalApi; @@ -13,16 +15,24 @@ @Generated("software.amazon.awssdk:codegen") @SdkInternalApi public final class DefaultQueryEndpointProvider implements QueryEndpointProvider { + private volatile CacheEntry cache; + @Override public CompletableFuture resolveEndpoint(QueryEndpointParams endpointParams) { + // Single-entry result cache: reuse the last endpoint when the params still match. + CacheEntry cached = this.cache; + if (cached != null && cacheParamsMatch(endpointParams, cached.params)) { + return CompletableFuture.completedFuture(cached.endpoint); + } try { Evaluator evaluator = new Evaluator(); evaluator.params = endpointParams; evaluator.region = endpointParams.region() == null ? null : endpointParams.region().id(); - Endpoint result = evaluator.nodeP14(); + Endpoint result = evaluator.nodeP21(); if (result == null) { return CompletableFutureUtils.failedFuture(SdkClientException.create("Rule engine did not reach an error or endpoint result")); } + this.cache = new CacheEntry(endpointParams, result); return CompletableFuture.completedFuture(result); } catch (SdkClientException e) { String errorMsg = e.getMessage(); @@ -35,6 +45,41 @@ public CompletableFuture resolveEndpoint(QueryEndpointParams endpointP } } + private static boolean cacheParamsMatch(QueryEndpointParams a, QueryEndpointParams b) { + return Objects.equals(a.useDualStack(), b.useDualStack()) + && Objects.equals(a.useFips(), b.useFips()) + && Objects.equals(a.region(), b.region()) + && Objects.equals(a.stringContextParam(), b.stringContextParam()) + && Objects.equals(a.endpoint(), b.endpoint()) + && Objects.equals(a.staticStringParam(), b.staticStringParam()) + && Objects.equals(a.operationContextParam(), b.operationContextParam()) + && cacheFirstElementsMatch(a.arnList(), b.arnList()) + && cacheListsMatch(a.customEndpointArray(), b.customEndpointArray()); + } + + private static boolean cacheListsMatch(List a, List b) { + if (a == b) return true; + if (a == null || b == null) return false; + int size = a.size(); + if (size != b.size()) return false; + // Bounded so that a long list cannot make the cache check cost more than resolving. + if (size > 4) return false; + for (int i = 0; i < size; i++) { + if (!Objects.equals(a.get(i), b.get(i))) return false; + } + return true; + } + + private static boolean cacheFirstElementsMatch(List a, List b) { + if (a == b) return true; + // isSet can tell an absent list from an empty one, so presence is part of the key. + if (a == null || b == null) return false; + // Nothing past element 0 can reach the endpoint. + String firstA = a.isEmpty() ? null : a.get(0); + String firstB = b.isEmpty() ? null : b.get(0); + return Objects.equals(firstA, firstB); + } + private static final class Evaluator { QueryEndpointParams params; @@ -42,6 +87,10 @@ private static final class Evaluator { RulePartition partitionResult; + String firstArn; + + String secondEndpoint; + private Endpoint nodeP0() { return null; } @@ -130,6 +179,48 @@ private Endpoint nodeP14() { : nodeP13(); } + private Endpoint nodeP15() { + return params.stringContextParam() != null + ? nodeP16() + : nodeP16(); + } + + private Endpoint nodeP16() { + return params.staticStringParam() != null + ? nodeP17() + : nodeP17(); + } + + private Endpoint nodeP17() { + return params.operationContextParam() != null + ? nodeP18() + : nodeP18(); + } + + private Endpoint nodeP18() { + return params.customEndpointArray() != null + ? nodeP19() + : nodeP19(); + } + + private Endpoint nodeP19() { + return cond12() + ? nodeP14() + : nodeP14(); + } + + private Endpoint nodeP20() { + return params.arnList() != null + ? nodeP15() + : nodeP15(); + } + + private Endpoint nodeP21() { + return cond14() + ? nodeP20() + : nodeP20(); + } + private boolean cond3() { partitionResult = RulesFunctions.awsPartition(region); return partitionResult != null; @@ -147,6 +238,16 @@ private boolean cond7() { return ("aws-us-gov".equals(partitionResult.name())); } + private boolean cond12() { + firstArn = RulesFunctions.listAccess(params.arnList(), 0); + return firstArn != null; + } + + private boolean cond14() { + secondEndpoint = RulesFunctions.listAccess(params.customEndpointArray(), 1); + return secondEndpoint != null; + } + private Endpoint result0() { throw SdkClientException.create("Invalid Configuration: FIPS and custom endpoint are not supported"); } @@ -195,4 +296,15 @@ private Endpoint result11() { throw SdkClientException.create("Invalid Configuration: Missing Region"); } } + + private static final class CacheEntry { + final QueryEndpointParams params; + + final Endpoint endpoint; + + CacheEntry(QueryEndpointParams params, Endpoint endpoint) { + this.params = params; + this.endpoint = endpoint; + } + } } diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/bdd/endpoint-provider-bdd-s3-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/bdd/endpoint-provider-bdd-s3-class.java index bb661bf8c9db..7b55c6c57497 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/bdd/endpoint-provider-bdd-s3-class.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/bdd/endpoint-provider-bdd-s3-class.java @@ -1,6 +1,7 @@ package software.amazon.awssdk.services.query.endpoints.internal; import java.util.Arrays; +import java.util.Objects; import java.util.concurrent.CompletableFuture; import software.amazon.awssdk.annotations.Generated; import software.amazon.awssdk.annotations.SdkInternalApi; @@ -18,8 +19,15 @@ @Generated("software.amazon.awssdk:codegen") @SdkInternalApi public final class DefaultQueryEndpointProvider implements QueryEndpointProvider { + private volatile CacheEntry cache; + @Override public CompletableFuture resolveEndpoint(QueryEndpointParams endpointParams) { + // Single-entry result cache: reuse the last endpoint when the params still match. + CacheEntry cached = this.cache; + if (cached != null && cacheParamsMatch(endpointParams, cached.params)) { + return CompletableFuture.completedFuture(cached.endpoint); + } try { Evaluator evaluator = new Evaluator(); evaluator.params = endpointParams; @@ -28,6 +36,7 @@ public CompletableFuture resolveEndpoint(QueryEndpointParams endpointP if (result == null) { return CompletableFutureUtils.failedFuture(SdkClientException.create("Rule engine did not reach an error or endpoint result")); } + this.cache = new CacheEntry(endpointParams, result); return CompletableFuture.completedFuture(result); } catch (SdkClientException e) { String errorMsg = e.getMessage(); @@ -40,6 +49,23 @@ public CompletableFuture resolveEndpoint(QueryEndpointParams endpointP } } + private static boolean cacheParamsMatch(QueryEndpointParams a, QueryEndpointParams b) { + return Objects.equals(a.useFips(), b.useFips()) + && Objects.equals(a.useDualStack(), b.useDualStack()) + && Objects.equals(a.forcePathStyle(), b.forcePathStyle()) + && Objects.equals(a.accelerate(), b.accelerate()) + && Objects.equals(a.useGlobalEndpoint(), b.useGlobalEndpoint()) + && Objects.equals(a.useObjectLambdaEndpoint(), b.useObjectLambdaEndpoint()) + && Objects.equals(a.disableAccessPoints(), b.disableAccessPoints()) + && Objects.equals(a.disableMultiRegionAccessPoints(), b.disableMultiRegionAccessPoints()) + && Objects.equals(a.useArnRegion(), b.useArnRegion()) + && Objects.equals(a.useS3ExpressControlEndpoint(), b.useS3ExpressControlEndpoint()) + && Objects.equals(a.disableS3ExpressSessionAuth(), b.disableS3ExpressSessionAuth()) + && Objects.equals(a.region(), b.region()) + && Objects.equals(a.bucket(), b.bucket()) + && Objects.equals(a.endpoint(), b.endpoint()); + } + private static final class Evaluator { QueryEndpointParams params; @@ -4099,4 +4125,15 @@ private Endpoint result114() { throw SdkClientException.create("A region must be set when sending requests to S3."); } } + + private static final class CacheEntry { + final QueryEndpointParams params; + + final Endpoint endpoint; + + CacheEntry(QueryEndpointParams params, Endpoint endpoint) { + this.params = params; + this.endpoint = endpoint; + } + } } diff --git a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/endpoints/AccountIdEndpointMode.java b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/endpoints/AccountIdEndpointMode.java index 7e6f0050dc4e..89a0684c4b1d 100644 --- a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/endpoints/AccountIdEndpointMode.java +++ b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/endpoints/AccountIdEndpointMode.java @@ -26,19 +26,25 @@ public enum AccountIdEndpointMode { /** * Default value that indicates account ID values will be used in endpoint rules if available. */ - PREFERRED, + PREFERRED("preferred"), /** * When mode is disabled, any resolved account ID will not be used in endpoint construction and rules that * reference them will be bypassed. */ - DISABLED, + DISABLED("disabled"), /** * Required mode would be used in scenarios where endpoint resolution should return an error if no account ID is * available. */ - REQUIRED; + REQUIRED("required"); + + private final String value; + + AccountIdEndpointMode(String value) { + this.value = value; + } /** * Returns the appropriate AccountIdEndpointMode value after parsing the parameter. @@ -51,12 +57,26 @@ public static AccountIdEndpointMode fromValue(String s) { return null; } - for (AccountIdEndpointMode value : values()) { - if (value.name().equalsIgnoreCase(s)) { - return value; + for (AccountIdEndpointMode mode : values()) { + // Matched against value rather than name() so that the wire form has a single definition. Behaviour is + // unchanged: the two differ only in case, and the comparison is case-insensitive. + if (mode.value.equalsIgnoreCase(s)) { + return mode; } } throw new IllegalArgumentException("Unrecognized value for account id endpoint mode: " + s); } + + /** + * Returns the canonical lowercase string for this mode, as the endpoint rules engine expects to receive it in the + * {@code AWS::Auth::AccountIdEndpointMode} built-in. + *

+ * Unlike {@code name().toLowerCase()}, this returns the same interned {@link String} reference on every call instead + * of a fresh string per request, which removes an allocation from the request path. It is also independent of the + * default locale, which {@code name().toLowerCase()} is not. + */ + public String value() { + return value; + } } diff --git a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/endpoints/AwsEndpointProviderUtils.java b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/endpoints/AwsEndpointProviderUtils.java index b2ee07acbf8b..56ac41f02c69 100644 --- a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/endpoints/AwsEndpointProviderUtils.java +++ b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/endpoints/AwsEndpointProviderUtils.java @@ -15,8 +15,6 @@ package software.amazon.awssdk.awscore.endpoints; -import static software.amazon.awssdk.utils.FunctionalUtils.invokeSafely; - import java.net.URI; import software.amazon.awssdk.annotations.SdkProtectedApi; import software.amazon.awssdk.awscore.AwsExecutionAttribute; @@ -54,19 +52,19 @@ public static Boolean fipsEnabledBuiltIn(ExecutionAttributes executionAttributes } /** - * Returns the endpoint set on the client. Note that this strips off the query part of the URI because the endpoint - * rules library, e.g. {@code ParseURL} will return an exception if the URI it parses has query parameters. + * Returns the endpoint set on the client, sanitized for the rules engine. The rules engine (e.g. + * {@code ParseURL}) rejects URIs with query parameters, so we strip the query and user-info components. + *

+ * Delegates to {@link ClientEndpointProvider#sanitizedEndpointString()}, which returns a cached reference on + * {@link software.amazon.awssdk.core.internal.StaticClientEndpointProvider} reducing allocations and expensive + * URI.create calls. */ public static String endpointBuiltIn(ExecutionAttributes executionAttributes) { if (endpointIsOverridden(executionAttributes)) { executionAttributes.getOptionalAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS).ifPresent( metric -> metric.addMetric(BusinessMetricFeatureId.ENDPOINT_OVERRIDE.value())); - return invokeSafely(() -> { - URI endpointOverride = executionAttributes.getAttribute(SdkInternalExecutionAttribute.CLIENT_ENDPOINT_PROVIDER) - .clientEndpoint(); - return new URI(endpointOverride.getScheme(), null, endpointOverride.getHost(), endpointOverride.getPort(), - endpointOverride.getPath(), null, endpointOverride.getFragment()).toString(); - }); + return executionAttributes.getAttribute(SdkInternalExecutionAttribute.CLIENT_ENDPOINT_PROVIDER) + .sanitizedEndpointString(); } return null; } diff --git a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/endpoints/AccountIdEndpointModeTest.java b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/endpoints/AccountIdEndpointModeTest.java new file mode 100644 index 000000000000..c19b7a5e21d7 --- /dev/null +++ b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/endpoints/AccountIdEndpointModeTest.java @@ -0,0 +1,99 @@ +/* + * 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.awscore.endpoints; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.Locale; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +/** + * {@link AccountIdEndpointMode#value()} is what generated endpoint resolvers pass to the rules engine as the + * {@code AWS::Auth::AccountIdEndpointMode} built-in, so these tests pin the wire form itself rather than any caller. + */ +class AccountIdEndpointModeTest { + private final Locale defaultLocale = Locale.getDefault(); + + @AfterEach + void restoreLocale() { + Locale.setDefault(defaultLocale); + } + + @Test + void value_returnsTheLowercaseWireForm() { + assertThat(AccountIdEndpointMode.PREFERRED.value()).isEqualTo("preferred"); + assertThat(AccountIdEndpointMode.DISABLED.value()).isEqualTo("disabled"); + assertThat(AccountIdEndpointMode.REQUIRED.value()).isEqualTo("required"); + } + + /** + * The value is a compile-time literal, so the same reference comes back every time. That is what removes the + * per-request allocation the generated resolver used to pay for {@code name().toLowerCase()}. + */ + @ParameterizedTest + @EnumSource(AccountIdEndpointMode.class) + void value_returnsTheSameReferenceEachCall(AccountIdEndpointMode mode) { + assertThat(mode.value()).isSameAs(mode.value()); + } + + /** + * {@code fromValue} and {@code value} must describe the same mapping in both directions, otherwise a + * value the SDK emits is one it cannot read back. + */ + @ParameterizedTest + @EnumSource(AccountIdEndpointMode.class) + void fromValue_roundTripsValue(AccountIdEndpointMode mode) { + assertThat(AccountIdEndpointMode.fromValue(mode.value())).isSameAs(mode); + } + + /** + * The predecessor of this method was {@code name().toLowerCase()}, which uses the default locale. Under a Turkish + * locale that produces a dotless i (U+0131), so {@code DISABLED} became {@code dısabled} and was handed to the rules + * engine in that form. + */ + @ParameterizedTest + @EnumSource(AccountIdEndpointMode.class) + void value_isIndependentOfTheDefaultLocale(AccountIdEndpointMode mode) { + String underDefaultLocale = mode.value(); + + Locale.setDefault(new Locale("tr", "TR")); + + assertThat(mode.value()).isEqualTo(underDefaultLocale); + assertThat(mode.value()).doesNotContain("\u0131"); + } + + @Test + void fromValue_isCaseInsensitive() { + assertThat(AccountIdEndpointMode.fromValue("PREFERRED")).isSameAs(AccountIdEndpointMode.PREFERRED); + assertThat(AccountIdEndpointMode.fromValue("Disabled")).isSameAs(AccountIdEndpointMode.DISABLED); + } + + @Test + void fromValue_nullReturnsNull() { + assertThat(AccountIdEndpointMode.fromValue(null)).isNull(); + } + + @Test + void fromValue_unrecognizedThrows() { + assertThatThrownBy(() -> AccountIdEndpointMode.fromValue("nonsense")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("nonsense"); + } +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/ClientEndpointProvider.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/ClientEndpointProvider.java index 500dd446af4d..572830b0c152 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/ClientEndpointProvider.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/ClientEndpointProvider.java @@ -19,6 +19,7 @@ import software.amazon.awssdk.annotations.SdkProtectedApi; import software.amazon.awssdk.core.internal.StaticClientEndpointProvider; import software.amazon.awssdk.endpoints.EndpointProvider; +import software.amazon.awssdk.utils.FunctionalUtils; /** * Client endpoint providers are responsible for resolving client-level endpoints. {@link EndpointProvider}s are @@ -48,6 +49,20 @@ static ClientEndpointProvider create(URI uri, boolean isEndpointOverridden) { */ URI clientEndpoint(); + /** + * Returns the client endpoint as a string with the query and user-info components stripped, as the rules engine's + * {@code ParseURL} rejects a URI carrying query parameters. Returns {@code null} if the endpoint is not overridden. + */ + default String sanitizedEndpointString() { + if (!isEndpointOverridden()) { + return null; + } + URI endpoint = clientEndpoint(); + return FunctionalUtils.invokeSafely( + () -> new URI(endpoint.getScheme(), null, endpoint.getHost(), endpoint.getPort(), + endpoint.getPath(), null, endpoint.getFragment()).toString()); + } + /** * Returns true if this endpoint was specified as an override by the customer, or false if it was determined * automatically by the SDK. diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/StaticClientEndpointProvider.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/StaticClientEndpointProvider.java index 40a9cd38f2c3..43be721c9110 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/StaticClientEndpointProvider.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/StaticClientEndpointProvider.java @@ -18,6 +18,7 @@ import java.net.URI; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.core.ClientEndpointProvider; +import software.amazon.awssdk.utils.FunctionalUtils; import software.amazon.awssdk.utils.ToString; import software.amazon.awssdk.utils.Validate; @@ -31,10 +32,48 @@ public class StaticClientEndpointProvider implements ClientEndpointProvider { private final URI clientEndpoint; private final boolean isEndpointOverridden; + /** + * A sanitized form of {@link #clientEndpoint} with the query and user-info components stripped, formatted as a + * string. This is the value that endpoint rules receive via the {@code SDK::Endpoint} built-in. Computed once at + * construction so that every call to {@code endpointBuiltIn()} returns the same {@link String} reference. + *

+ * {@code null} when {@link #isEndpointOverridden} is {@code false}. + */ + private final String sanitizedEndpointString; + public StaticClientEndpointProvider(URI clientEndpoint, boolean isEndpointOverridden) { this.clientEndpoint = Validate.paramNotNull(clientEndpoint, "clientEndpoint"); this.isEndpointOverridden = isEndpointOverridden; Validate.paramNotNull(clientEndpoint.getScheme(), "The URI scheme of endpointOverride"); + this.sanitizedEndpointString = isEndpointOverridden ? sanitize(clientEndpoint) : null; + } + + /** + * {@inheritDoc} + *

+ * Returns the same {@link String} reference on every call, because the value is computed once at construction. That + * removes a URI construction and its string conversion from every request that resolves an endpoint against an + * overridden client endpoint. + *

+ * {@code final} so that the cached value cannot be shadowed by a subclass whose overridden accessors the constructor + * did not see. + */ + @Override + public final String sanitizedEndpointString() { + return sanitizedEndpointString; + } + + /** + * Repeats {@link ClientEndpointProvider#sanitizedEndpointString()}'s transformation over the constructor's argument, + * rather than calling that default from the constructor, so no virtual method runs before this class is fully + * initialised. The two must agree, and + * {@code ClientEndpointProviderTest.sanitizedEndpointString_cachedFormMatchesRecomputedForm} is what holds them + * together. + */ + private static String sanitize(URI endpoint) { + return FunctionalUtils.invokeSafely( + () -> new URI(endpoint.getScheme(), null, endpoint.getHost(), endpoint.getPort(), + endpoint.getPath(), null, endpoint.getFragment()).toString()); } @Override diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/ClientEndpointProviderTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/ClientEndpointProviderTest.java new file mode 100644 index 000000000000..43846d815357 --- /dev/null +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/ClientEndpointProviderTest.java @@ -0,0 +1,107 @@ +/* + * 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.core; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.URI; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +/** + * Covers {@link ClientEndpointProvider#sanitizedEndpointString()}'s default implementation, which is the value generated + * endpoint resolvers receive as the {@code SDK::Endpoint} built-in. + * + *

Every provider the SDK itself installs is a {@code StaticClientEndpointProvider}, which overrides the method with a + * value computed once at construction. The default therefore runs only for an external implementor, and it exists to be + * the single definition of the transformation that the overriding implementation caches. Both halves are asserted here: + * what the transformation does, and that the two forms agree. + */ +class ClientEndpointProviderTest { + /** + * A provider that leaves {@code sanitizedEndpointString()} to the interface, which is what an external implementor + * gets and what no SDK code path produces. + */ + private static ClientEndpointProvider defaultImplementation(URI uri, boolean isEndpointOverridden) { + return new ClientEndpointProvider() { + @Override + public URI clientEndpoint() { + return uri; + } + + @Override + public boolean isEndpointOverridden() { + return isEndpointOverridden; + } + }; + } + + @ParameterizedTest + @CsvSource({ + // Query parameters are rejected by the rules engine's ParseURL, which is why they are stripped. + "https://example.com/path?foo=bar, https://example.com/path", + "https://example.com?foo=bar, https://example.com", + "https://example.com/path?foo=bar&baz=qux, https://example.com/path", + // User-info is stripped for the same reason: it is not part of what the rules engine resolves against. + "https://user:pass@example.com/path, https://example.com/path", + "https://user@example.com, https://example.com", + "https://user:pass@example.com/path?foo=bar,https://example.com/path", + // Everything else survives untouched. + "https://example.com:8443/path, https://example.com:8443/path", + "http://example.com/path, http://example.com/path", + "https://example.com/path#frag, https://example.com/path#frag", + "https://example.com/a/b/c, https://example.com/a/b/c" + }) + void sanitizedEndpointString_stripsQueryAndUserInfo(String input, String expected) { + assertThat(defaultImplementation(URI.create(input), true).sanitizedEndpointString()).isEqualTo(expected); + } + + @Test + void sanitizedEndpointString_returnsNullWhenNotOverridden() { + URI uri = URI.create("https://example.com/path?foo=bar"); + + assertThat(defaultImplementation(uri, false).sanitizedEndpointString()).isNull(); + assertThat(ClientEndpointProvider.create(uri, false).sanitizedEndpointString()).isNull(); + } + + /** + * The claim that makes it safe for {@code StaticClientEndpointProvider} to compute this once at construction: the + * cached value and the recomputed one cannot disagree, because there is one definition of the transformation. + */ + @ParameterizedTest + @CsvSource({ + "https://example.com/path?foo=bar", + "https://user:pass@example.com/path?foo=bar", + "https://example.com:8443/path", + "https://example.com/path#frag", + "http://example.com" + }) + void sanitizedEndpointString_cachedFormMatchesRecomputedForm(String input) { + URI uri = URI.create(input); + + assertThat(ClientEndpointProvider.create(uri, true).sanitizedEndpointString()) + .isEqualTo(defaultImplementation(uri, true).sanitizedEndpointString()); + } + + @Test + void sanitizedEndpointString_overridingImplementationReturnsAStableReference() { + ClientEndpointProvider provider = ClientEndpointProvider.forEndpointOverride( + URI.create("https://example.com/path?foo=bar")); + + assertThat(provider.sanitizedEndpointString()).isSameAs(provider.sanitizedEndpointString()); + } +} diff --git a/test/codegen-generated-classes-test/src/main/resources/codegen-resources/bddendpoints/endpoint-bdd-1.json b/test/codegen-generated-classes-test/src/main/resources/codegen-resources/bddendpoints/endpoint-bdd-1.json index 157f1b1237d4..66e220ed535f 100644 --- a/test/codegen-generated-classes-test/src/main/resources/codegen-resources/bddendpoints/endpoint-bdd-1.json +++ b/test/codegen-generated-classes-test/src/main/resources/codegen-resources/bddendpoints/endpoint-bdd-1.json @@ -26,6 +26,48 @@ "required": false, "documentation": "Override the endpoint used to send this request", "type": "string" + }, + "AccountId": { + "builtIn": "AWS::Auth::AccountId", + "required": false, + "documentation": "The AWS account ID, read off the resolved identity.", + "type": "string" + }, + "AccountIdEndpointMode": { + "builtIn": "AWS::Auth::AccountIdEndpointMode", + "required": false, + "documentation": "Whether the account ID may be used in the endpoint.", + "type": "string" + }, + "clientStringParam": { + "required": false, + "documentation": "A client context parameter, so the cache key compares it early as reference-stable.", + "type": "string" + }, + "staticStringParam": { + "required": false, + "documentation": "Bound to a per-operation static literal.", + "type": "string" + }, + "requestStringParam": { + "required": false, + "documentation": "Bound to a request member, so it can change per request.", + "type": "string" + }, + "resourceArnList": { + "required": false, + "documentation": "Read only as resourceArnList[0], so the cache key compares just the first element. This is the DynamoDB shape.", + "type": "stringArray" + }, + "unusedStringParam": { + "required": false, + "documentation": "Declared but read by no condition and no result, so the cache key must leave it out.", + "type": "string" + }, + "wholeArnList": { + "required": false, + "documentation": "Read as a whole, so the cache key compares every element.", + "type": "stringArray" } }, "conditions": [ @@ -116,6 +158,82 @@ }, "aws-us-gov" ] + }, + { + "fn": "isSet", + "argv": [ + { + "ref": "AccountId" + } + ] + }, + { + "fn": "isSet", + "argv": [ + { + "ref": "AccountIdEndpointMode" + } + ] + }, + { + "fn": "isSet", + "argv": [ + { + "ref": "clientStringParam" + } + ] + }, + { + "fn": "isSet", + "argv": [ + { + "ref": "staticStringParam" + } + ] + }, + { + "fn": "isSet", + "argv": [ + { + "ref": "requestStringParam" + } + ] + }, + { + "fn": "getAttr", + "argv": [ + { + "ref": "resourceArnList" + }, + "[0]" + ], + "assign": "FirstArn" + }, + { + "fn": "isSet", + "argv": [ + { + "ref": "wholeArnList" + } + ] + }, + { + "fn": "isSet", + "argv": [ + { + "ref": "resourceArnList" + } + ] + }, + { + "fn": "getAttr", + "argv": [ + { + "ref": "wholeArnList" + }, + "[1]" + ], + "assign": "SecondArn" } ], "results": [ @@ -206,7 +324,7 @@ "type": "error" } ], - "root": 2, - "nodeCount": 14, - "nodes": "/////wAAAAH/////AAAAAAAAAA0AAAADAAAAAQAAAAQF9eEMAAAAAgAAAAUF9eEMAAAAAwAAAAgAAAAGAAAABAAAAAcF9eELAAAABQX14QkF9eEKAAAABAAAAAsAAAAJAAAABgAAAAoF9eEIAAAABwX14QYF9eEHAAAABQAAAAwF9eEFAAAABgX14QQF9eEFAAAAAwX14QEAAAAOAAAABAX14QIF9eED" -} \ No newline at end of file + "root": 23, + "nodeCount": 23, + "nodes": "/////wAAAAH/////AAAAAAAAAA0AAAADAAAAAQAAAAQF9eEMAAAAAgAAAAUF9eEMAAAAAwAAAAgAAAAGAAAABAAAAAcF9eELAAAABQX14QkF9eEKAAAABAAAAAsAAAAJAAAABgAAAAoF9eEIAAAABwX14QYF9eEHAAAABQAAAAwF9eEFAAAABgX14QQF9eEFAAAAAwX14QEAAAAOAAAABAX14QIF9eEDAAAACAAAABAAAAAQAAAACQAAABEAAAARAAAACgAAABIAAAASAAAACwAAABMAAAATAAAADAAAABQAAAAUAAAADQAAAAIAAAACAAAADgAAAA8AAAAPAAAADwAAABUAAAAVAAAAEAAAABYAAAAW" +} diff --git a/test/codegen-generated-classes-test/src/main/resources/codegen-resources/bddendpoints/endpoint-rule-set.json b/test/codegen-generated-classes-test/src/main/resources/codegen-resources/bddendpoints/endpoint-rule-set.json index c87dbf336b6d..74d707c19c06 100644 --- a/test/codegen-generated-classes-test/src/main/resources/codegen-resources/bddendpoints/endpoint-rule-set.json +++ b/test/codegen-generated-classes-test/src/main/resources/codegen-resources/bddendpoints/endpoint-rule-set.json @@ -26,6 +26,48 @@ "required": false, "documentation": "Override the endpoint used to send this request", "type": "string" + }, + "AccountId": { + "builtIn": "AWS::Auth::AccountId", + "required": false, + "documentation": "The AWS account ID, read off the resolved identity.", + "type": "string" + }, + "AccountIdEndpointMode": { + "builtIn": "AWS::Auth::AccountIdEndpointMode", + "required": false, + "documentation": "Whether the account ID may be used in the endpoint.", + "type": "string" + }, + "clientStringParam": { + "required": false, + "documentation": "A client context parameter, so the cache key compares it early as reference-stable.", + "type": "string" + }, + "staticStringParam": { + "required": false, + "documentation": "Bound to a per-operation static literal.", + "type": "string" + }, + "requestStringParam": { + "required": false, + "documentation": "Bound to a request member, so it can change per request.", + "type": "string" + }, + "resourceArnList": { + "required": false, + "documentation": "Read only as resourceArnList[0], so the cache key compares just the first element. This is the DynamoDB shape.", + "type": "stringArray" + }, + "unusedStringParam": { + "required": false, + "documentation": "Declared but read by no condition and no result, so the cache key must leave it out.", + "type": "string" + }, + "wholeArnList": { + "required": false, + "documentation": "Read as a whole, so the cache key compares every element.", + "type": "stringArray" } }, "rules": [ @@ -336,4 +378,4 @@ "type": "error" } ] -} \ No newline at end of file +} diff --git a/test/codegen-generated-classes-test/src/main/resources/codegen-resources/bddendpoints/service-2.json b/test/codegen-generated-classes-test/src/main/resources/codegen-resources/bddendpoints/service-2.json index c8ef99738ea8..0b1408801fca 100644 --- a/test/codegen-generated-classes-test/src/main/resources/codegen-resources/bddendpoints/service-2.json +++ b/test/codegen-generated-classes-test/src/main/resources/codegen-resources/bddendpoints/service-2.json @@ -15,6 +15,12 @@ "aws.auth#sigv4" ] }, + "clientContextParams": { + "clientStringParam": { + "documentation": "A client-level string context parameter.", + "type": "string" + } + }, "operations": { "TestOperation": { "name": "TestOperation", @@ -28,6 +34,58 @@ "output": { "shape": "TestOperationResponse" } + }, + "OperationWithStaticParam": { + "name": "OperationWithStaticParam", + "http": { + "method": "POST", + "requestUri": "/" + }, + "staticContextParams": { + "staticStringParam": { + "value": "static-value" + } + }, + "input": { + "shape": "TestOperationRequest" + }, + "output": { + "shape": "TestOperationResponse" + } + }, + "OperationWithContextParam": { + "name": "OperationWithContextParam", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { + "shape": "ContextParamInput" + }, + "output": { + "shape": "TestOperationResponse" + } + }, + "OperationWithListContextParam": { + "name": "OperationWithListContextParam", + "http": { + "method": "POST", + "requestUri": "/" + }, + "operationContextParams": { + "resourceArnList": { + "path": "Items[*].Arn" + }, + "wholeArnList": { + "path": "Items[*].Arn" + } + }, + "input": { + "shape": "ListContextParamInput" + }, + "output": { + "shape": "TestOperationResponse" + } } }, "shapes": { @@ -47,6 +105,39 @@ } } }, + "ContextParamInput": { + "type": "structure", + "members": { + "RequestMember": { + "shape": "String", + "contextParam": { + "name": "requestStringParam" + } + } + } + }, + "ListContextParamInput": { + "type": "structure", + "members": { + "Items": { + "shape": "ItemList" + } + } + }, + "ItemList": { + "type": "list", + "member": { + "shape": "Item" + } + }, + "Item": { + "type": "structure", + "members": { + "Arn": { + "shape": "String" + } + } + }, "String": { "type": "string" } diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/bddendpoints/BddEndpointProviderCacheTest.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/bddendpoints/BddEndpointProviderCacheTest.java new file mode 100644 index 000000000000..45887d41bc15 --- /dev/null +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/bddendpoints/BddEndpointProviderCacheTest.java @@ -0,0 +1,508 @@ +/* + * 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.bddendpoints; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletionException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.endpoints.Endpoint; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.bddendpoints.endpoints.BddEndpointsEndpointParams; +import software.amazon.awssdk.services.bddendpoints.endpoints.BddEndpointsEndpointProvider; + +/** + * Behavioural tests for the single-entry result cache generated into the BDD endpoint provider. + * + *

The suite is organised around the one invariant that matters: a cache hit must be indistinguishable from a fresh + * resolution. That splits into two obligations. + * + *

    + *
  1. No stale hit. Changing any single parameter must not return the endpoint resolved for the previous value. + * Every parameter the model declares gets its own test, because a parameter accidentally left out of the + * generated key check is the one defect here that produces a wrong endpoint rather than a slow one.
  2. + *
  3. Hits where they are due. Equal params must actually reuse the cached instance, otherwise the change is + * cost without benefit. Asserted with {@code isSameAs}, which is the only externally visible evidence that the + * cache was consulted.
  4. + *
+ * + *

The model over-declares parameters on purpose, to cover every {@code BddParameterReferences.Usage} value and every + * parameter kind: boolean, string, {@code stringArray}, built-in, client context, static context and request context. + * Several of them cannot change the resolved URL, which makes them the more interesting cases to test: a stale hit is + * detectable only by instance identity, not by comparing hosts. + * + *

The trailing nodes of {@code bddendpoints/endpoint-bdd-1.json} were appended by hand with {@code high == low}, so + * that each parameter is read by a condition without altering any resolved endpoint. A future peephole pass that + * collapses {@code high == low} nodes would silently make those parameters unreferenced and void the coverage above. + */ +class BddEndpointProviderCacheTest { + private static final Region REGION = Region.US_EAST_1; + private static final Region OTHER_REGION = Region.US_WEST_2; + + /** + * Mirrors {@code BddEndpointProviderSpec.MAX_LIST_COMPARISON_SIZE}. Not importable from here, so the cap-related + * tests below derive their sizes from this one constant. + */ + private static final int LIST_SIZE_CAP = 4; + + /** + * Returns params that resolve successfully, with every optional parameter left unset. + */ + private static BddEndpointsEndpointParams.Builder baseBuilder() { + return BddEndpointsEndpointParams.builder() + .region(REGION) + .useDualStack(false) + .useFips(false); + } + + private static BddEndpointsEndpointParams params(Consumer customizer) { + BddEndpointsEndpointParams.Builder builder = baseBuilder(); + customizer.accept(builder); + return builder.build(); + } + + private static BddEndpointsEndpointProvider provider() { + return BddEndpointsEndpointProvider.defaultProvider(); + } + + private static Endpoint resolve(BddEndpointsEndpointProvider provider, BddEndpointsEndpointParams params) { + return provider.resolveEndpoint(params).join(); + } + + /** + * Resolves {@code first}, then {@code second}, and asserts the second call did not reuse the first result. + * + *

Instance identity rather than URL comparison, so this works for the parameters that do not influence the + * resolved URL. Those are exactly the parameters where a missing key check would go unnoticed. + * + *

Identity is a valid miss signal only while every resolution constructs a new {@link Endpoint}. If codegen ever + * hoists constant endpoint results to {@code static final}, this helper and {@link #cacheIsPerProviderInstance()} + * start failing for that reason rather than because the cache regressed. + */ + private static void assertInvalidates(BddEndpointsEndpointParams first, BddEndpointsEndpointParams second) { + BddEndpointsEndpointProvider provider = provider(); + Endpoint firstEndpoint = resolve(provider, first); + Endpoint secondEndpoint = resolve(provider, second); + assertThat(secondEndpoint).isNotSameAs(firstEndpoint); + } + + private static void assertHits(BddEndpointsEndpointParams first, BddEndpointsEndpointParams second) { + BddEndpointsEndpointProvider provider = provider(); + Endpoint firstEndpoint = resolve(provider, first); + Endpoint secondEndpoint = resolve(provider, second); + assertThat(secondEndpoint).isSameAs(firstEndpoint); + } + + // ---- hits ---- + + @Test + void sameParamsInstance_reusesCachedEndpoint() { + BddEndpointsEndpointParams p = params(b -> { + }); + assertHits(p, p); + } + + @Test + void distinctButEqualParams_reusesCachedEndpoint() { + assertHits(params(b -> { + }), params(b -> { + })); + } + + @Test + void cacheIsPerProviderInstance() { + BddEndpointsEndpointParams p = params(b -> { + }); + Endpoint fromFirstProvider = resolve(provider(), p); + Endpoint fromSecondProvider = resolve(provider(), p); + assertThat(fromSecondProvider).isNotSameAs(fromFirstProvider); + assertThat(fromSecondProvider.endpointUrl().host()).isEqualTo(fromFirstProvider.endpointUrl().host()); + } + + /** + * The cached endpoint must be the one the params call for, not merely some previously resolved endpoint. Alternating + * between two parameter sets in a loop would pass even if the cache returned the wrong entry, so each round asserts + * the host as well. + */ + @Test + void alternatingParams_eachResolutionMatchesItsOwnParams() { + BddEndpointsEndpointProvider provider = provider(); + BddEndpointsEndpointParams plain = params(b -> { + }); + BddEndpointsEndpointParams fips = params(b -> b.useFips(true)); + + String plainHost = resolve(provider, plain).endpointUrl().host(); + String fipsHost = resolve(provider, fips).endpointUrl().host(); + assertThat(plainHost).doesNotContain("fips"); + assertThat(fipsHost).contains("fips"); + + for (int i = 0; i < 4; i++) { + assertThat(resolve(provider, plain).endpointUrl().host()).isEqualTo(plainHost); + assertThat(resolve(provider, fips).endpointUrl().host()).isEqualTo(fipsHost); + } + } + + // ---- no stale hit, one test per parameter ---- + + @Test + void useFipsChange_invalidates() { + assertInvalidates(params(b -> { + }), params(b -> b.useFips(true))); + } + + @Test + void useDualStackChange_invalidates() { + assertInvalidates(params(b -> { + }), params(b -> b.useDualStack(true))); + } + + @Test + void regionChange_invalidates() { + assertInvalidates(params(b -> { + }), params(b -> b.region(OTHER_REGION))); + } + + @Test + void clientStringParamChange_invalidates() { + assertInvalidates(params(b -> b.clientStringParam("first")), + params(b -> b.clientStringParam("second"))); + } + + @Test + void staticStringParamChange_invalidates() { + assertInvalidates(params(b -> b.staticStringParam("first")), + params(b -> b.staticStringParam("second"))); + } + + @Test + void endpointOverrideChange_invalidates() { + assertInvalidates(params(b -> b.endpoint("https://first.example.com")), + params(b -> b.endpoint("https://second.example.com"))); + } + + @Test + void accountIdEndpointModeChange_invalidates() { + assertInvalidates(params(b -> b.accountIdEndpointMode("preferred")), + params(b -> b.accountIdEndpointMode("disabled"))); + } + + @Test + void accountIdChange_invalidates() { + assertInvalidates(params(b -> b.accountId("111111111111")), + params(b -> b.accountId("222222222222"))); + } + + @Test + void requestStringParamChange_invalidates() { + assertInvalidates(params(b -> b.requestStringParam("first")), + params(b -> b.requestStringParam("second"))); + } + + // ---- lists read as a whole: wholeArnList, reached via isSet, so every element is part of the key ---- + + @Test + void wholeList_elementChange_invalidates() { + assertInvalidates(params(b -> b.wholeArnList(Arrays.asList("a", "b"))), + params(b -> b.wholeArnList(Arrays.asList("a", "c")))); + } + + @Test + void wholeList_lengthChange_invalidates() { + assertInvalidates(params(b -> b.wholeArnList(Arrays.asList("a", "b"))), + params(b -> b.wholeArnList(Collections.singletonList("a")))); + } + + @Test + void wholeList_orderChange_invalidates() { + assertInvalidates(params(b -> b.wholeArnList(Arrays.asList("a", "b"))), + params(b -> b.wholeArnList(Arrays.asList("b", "a")))); + } + + // ---- lists read only at index 0: resourceArnList, reached via getAttr(list, "[0]") ---- + // + // The BDD's only read of this list is its first element, so nothing past element 0 can reach the endpoint. The key + // therefore compares element 0 alone, which turns changes the endpoint cannot see into hits rather than misses. + // This is the DynamoDB shape, where comparing the whole list costs more than half a regional resolution. + + @Test + void firstElementList_firstElementChange_invalidates() { + assertInvalidates(params(b -> b.resourceArnList(Arrays.asList("a", "b"))), + params(b -> b.resourceArnList(Arrays.asList("z", "b")))); + } + + @Test + void firstElementList_laterElementChange_isAHit() { + assertHits(params(b -> b.resourceArnList(Arrays.asList("a", "b"))), + params(b -> b.resourceArnList(Arrays.asList("a", "c")))); + } + + @Test + void firstElementList_lengthChangeKeepingFirstElement_isAHit() { + assertHits(params(b -> b.resourceArnList(Arrays.asList("a", "b", "c"))), + params(b -> b.resourceArnList(Collections.singletonList("a")))); + } + + @Test + void firstElementList_orderChangeMovingFirstElement_invalidates() { + assertInvalidates(params(b -> b.resourceArnList(Arrays.asList("a", "b"))), + params(b -> b.resourceArnList(Arrays.asList("b", "a")))); + } + + /** + * {@code isSet} can tell an absent list from an empty one, so presence stays part of the key even though + * {@code listAccess} yields null for both. + * + *

Collapsing the two would be sound only for a BDD whose branches for absent and empty converge. DynamoDB's do - + * both reach the same node - but that is a property of the graph rather than of the parameter, so the generated + * comparison does not assume it. The cost is one extra reference check and a miss in this case. + */ + @Test + void firstElementList_emptyAndUnset_areDistinguished() { + assertInvalidates(params(b -> b.resourceArnList(Collections.emptyList())), params(b -> { + })); + } + + /** + * No size cap applies when only the first element is compared, so a list far past the cap still hits. That is the + * point: the comparison is O(1) rather than bounded-but-linear. + */ + @Test + void firstElementList_farPastTheSizeCap_stillHits() { + List long1 = new ArrayList<>(listOfSize(500)); + List long2 = new ArrayList<>(listOfSize(500)); + long2.set(499, "different-tail"); + assertHits(params(b -> b.resourceArnList(long1)), params(b -> b.resourceArnList(long2))); + } + + // ---- parameters the BDD never reads are not part of the key ---- + + /** + * {@code unusedStringParam} is declared by the model and read by no condition and no result, so it cannot change the + * resolved endpoint and must not evict the cached one. + * + *

This is the behaviour that makes the cache worth having for S3, whose rule set declares {@code Key}, + * {@code Prefix} and {@code CopySource} and reads none of them. {@code Key} changes on essentially every object + * request, so treating it as part of the key would mean the cache almost never hits. + */ + @Test + void parameterTheBddNeverReads_doesNotInvalidate() { + assertHits(params(b -> b.unusedStringParam("first")), + params(b -> b.unusedStringParam("second"))); + } + + @Test + void parameterTheBddNeverReads_settingItDoesNotInvalidate() { + assertHits(params(b -> { + }), params(b -> b.unusedStringParam("now-set"))); + } + + // ---- transitions to and from unset ---- + + @Test + void settingAPreviouslyUnsetParam_invalidates() { + assertInvalidates(params(b -> { + }), params(b -> b.requestStringParam("now-set"))); + } + + @Test + void clearingAPreviouslySetParam_invalidates() { + assertInvalidates(params(b -> b.requestStringParam("was-set")), params(b -> { + })); + } + + @Test + void settingAPreviouslyUnsetList_invalidates() { + assertInvalidates(params(b -> { + }), params(b -> b.wholeArnList(Collections.singletonList("a")))); + } + + @Test + void clearingAPreviouslySetList_invalidates() { + assertInvalidates(params(b -> b.wholeArnList(Collections.singletonList("a"))), params(b -> { + })); + } + + @Test + void emptyListAndUnsetList_areDistinguished() { + assertInvalidates(params(b -> b.wholeArnList(Collections.emptyList())), params(b -> { + })); + } + + // ---- equals fallback ---- + + /** + * A parameter compared with {@code Objects.equals} must hit on an equal value arriving as a fresh reference. Without + * the {@code equals} fallback, a request-derived string would miss on every call and the cache would never pay off + * for the services that need it most. + */ + @Test + void equalValueDifferentReference_hits() { + String value = "shared-value"; + String copy = new String(value); + assertThat(value).isNotSameAs(copy); + + assertHits(params(b -> b.requestStringParam(value)), params(b -> b.requestStringParam(copy))); + assertHits(params(b -> b.accountId(value)), params(b -> b.accountId(copy))); + + String url = "https://override.example.com"; + assertHits(params(b -> b.endpoint(url)), params(b -> b.endpoint(new String(url)))); + } + + @Test + void requestList_equalContentsDifferentListInstance_hits() { + assertHits(params(b -> b.wholeArnList(new ArrayList<>(Arrays.asList("a", "b")))), + params(b -> b.wholeArnList(new ArrayList<>(Arrays.asList("a", "b"))))); + } + + /** + * Element comparison also falls back to {@code equals}, so equal strings held by different references still hit. + */ + @Test + void requestList_equalElementsDifferentReferences_hits() { + assertHits(params(b -> b.wholeArnList(Collections.singletonList("element"))), + params(b -> b.wholeArnList(Collections.singletonList(new String("element"))))); + } + + // ---- list size cap ---- + + /** + * At the cap the element walk still runs, so equal lists hit. + */ + @Test + void requestList_atSizeCap_stillHits() { + assertHits(params(b -> b.wholeArnList(listOfSize(LIST_SIZE_CAP))), + params(b -> b.wholeArnList(listOfSize(LIST_SIZE_CAP)))); + } + + /** + * Past the cap the check bails out and reports a miss without walking the elements, which keeps the key check + * bounded. Equal lists therefore stop hitting; that is a deliberate cost ceiling, not a defect, and it is pinned + * here so that changing the cap is a conscious decision. + */ + @Test + void requestList_pastSizeCap_alwaysMisses() { + assertInvalidates(params(b -> b.wholeArnList(listOfSize(LIST_SIZE_CAP + 1))), + params(b -> b.wholeArnList(listOfSize(LIST_SIZE_CAP + 1)))); + } + + /** + * Even a miss must still resolve correctly, so an oversized list is not a functional break. + */ + @Test + void requestList_pastSizeCap_stillResolvesCorrectly() { + Endpoint endpoint = resolve(provider(), params(b -> b.wholeArnList(listOfSize(50)))); + assertThat(endpoint.endpointUrl().host()).isEqualTo("connect.us-east-1.amazonaws.com"); + } + + private static List listOfSize(int size) { + return IntStream.range(0, size).mapToObj(i -> "element-" + i).collect(Collectors.toList()); + } + + // ---- failures are never cached ---- + + /** + * A rule error must not be stored, and must not evict a good entry. Replaying a cached failure would turn one bad + * call into a permanently broken client. + */ + @Test + void ruleError_isNotCached_andLeavesEarlierEntryIntact() { + BddEndpointsEndpointProvider provider = provider(); + BddEndpointsEndpointParams good = params(b -> { + }); + Endpoint first = resolve(provider, good); + + // FIPS combined with an endpoint override is an error in this rule set. + BddEndpointsEndpointParams bad = params(b -> b.useFips(true).endpoint("https://override.example.com")); + assertThatThrownBy(() -> resolve(provider, bad)).isInstanceOf(CompletionException.class); + + assertThat(resolve(provider, good)).isSameAs(first); + // Still an error the second time, rather than a replayed success. + assertThatThrownBy(() -> resolve(provider, bad)).isInstanceOf(CompletionException.class); + } + + @Test + void missingRegion_isNotCached() { + BddEndpointsEndpointProvider provider = provider(); + BddEndpointsEndpointParams noRegion = BddEndpointsEndpointParams.builder() + .useDualStack(false) + .useFips(false) + .build(); + assertThatThrownBy(() -> resolve(provider, noRegion)).isInstanceOf(CompletionException.class); + assertThatThrownBy(() -> resolve(provider, noRegion)).isInstanceOf(CompletionException.class); + + assertThat(resolve(provider, params(b -> { + })).endpointUrl().host()).isEqualTo("connect.us-east-1.amazonaws.com"); + } + + // ---- concurrency ---- + + /** + * Concurrent resolution of two distinct parameter sets against one provider. The cache field is written without any + * lock, so threads race to overwrite it; every thread must still receive the endpoint its own params call for. A + * torn or misattributed entry shows up here as a host mismatch. + */ + @Test + void concurrentResolution_neverReturnsAnotherThreadsEndpoint() throws Exception { + BddEndpointsEndpointProvider provider = provider(); + BddEndpointsEndpointParams plain = params(b -> { + }); + BddEndpointsEndpointParams dualStack = params(b -> b.useDualStack(true)); + String plainHost = resolve(provider(), plain).endpointUrl().host(); + String dualStackHost = resolve(provider(), dualStack).endpointUrl().host(); + assertThat(plainHost).isNotEqualTo(dualStackHost); + + int threads = 16; + int iterations = 500; + ExecutorService executor = Executors.newFixedThreadPool(threads); + try { + CountDownLatch start = new CountDownLatch(1); + List> tasks = new ArrayList<>(); + for (int t = 0; t < threads; t++) { + boolean useDualStack = t % 2 == 0; + BddEndpointsEndpointParams params = useDualStack ? dualStack : plain; + String expectedHost = useDualStack ? dualStackHost : plainHost; + tasks.add(() -> { + start.await(); + for (int i = 0; i < iterations; i++) { + assertThat(resolve(provider, params).endpointUrl().host()).isEqualTo(expectedHost); + } + return null; + }); + } + List> futures = tasks.stream().map(executor::submit).collect(Collectors.toList()); + start.countDown(); + for (Future future : futures) { + future.get(60, TimeUnit.SECONDS); + } + } finally { + executor.shutdownNow(); + } + } +} diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/compiledendpointrules/endpoints/internal/RulesFunctionsAwsPartitionTest.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/compiledendpointrules/endpoints/internal/RulesFunctionsAwsPartitionTest.java new file mode 100644 index 000000000000..fa2468671c3f --- /dev/null +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/compiledendpointrules/endpoints/internal/RulesFunctionsAwsPartitionTest.java @@ -0,0 +1,119 @@ +/* + * 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.compiledendpointrules.endpoints.internal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.lang.reflect.Field; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.core.SdkSystemSetting; +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.testutils.EnvironmentVariableHelper; + +/** + * Covers {@code RulesFunctions.awsPartition}'s handling of partition metadata that declares no {@code aws} partition. + * + *

{@code aws} is the fallback for a region that matches neither a declared region name nor a partition's region + * pattern, so metadata without it is only incomplete for those regions. The regions it does describe must keep + * resolving, and only the unmatched ones may fail. Pinned here because the natural way to precompute the fallback - + * validating it while loading - fails every region instead, and reads as a tidier implementation. + * + *

Reachable in production through either partitions-file override, not through the shipped default: the default + * metadata is generated into {@code LazyPartitionsContainer} at build time from {@code partitions.json.resource} and + * always declares {@code aws}. + */ +class RulesFunctionsAwsPartitionTest { + private static final String NO_AWS_PARTITIONS_FILE = + RulesFunctionsAwsPartitionTest.class.getResource( + "/software/amazon/awssdk/services/endpointproviders/no-aws-partitions.json").getFile(); + + private final EnvironmentVariableHelper environment = new EnvironmentVariableHelper(); + + /** + * {@code RulesFunctions} caches the loaded metadata in a static, so the cache has to be cleared around any test + * that swaps the metadata out. Without this the result would depend on whether some earlier test in the same JVM + * had already resolved a partition. + */ + @BeforeEach + void clearCacheBefore() throws Exception { + resetPartitionDataCache(); + } + + @AfterEach + void clearCacheAfter() throws Exception { + environment.reset(); + resetPartitionDataCache(); + } + + @Test + void metadataWithoutAws_regionDeclaredByAnotherPartition_stillResolves() { + environment.set(SdkSystemSetting.AWS_PARTITIONS_FILE, NO_AWS_PARTITIONS_FILE); + + assertThat(RulesFunctions.awsPartition("cn-north-1").name()).isEqualTo("aws-cn"); + } + + @Test + void metadataWithoutAws_regionMatchingAnotherPartitionsPattern_stillResolves() { + environment.set(SdkSystemSetting.AWS_PARTITIONS_FILE, NO_AWS_PARTITIONS_FILE); + + // Not declared in the fixture's regions, so this can only resolve through the regionRegex. + assertThat(RulesFunctions.awsPartition("cn-northwest-1").name()).isEqualTo("aws-cn"); + } + + /** + * The one case the missing partition actually breaks. The message has to name the region and the override + * mechanisms, because at this point that is the only information available about why the fallback was needed. + */ + @Test + void metadataWithoutAws_unmatchedRegion_throwsNamingTheRegionAndTheCause() { + environment.set(SdkSystemSetting.AWS_PARTITIONS_FILE, NO_AWS_PARTITIONS_FILE); + + assertThatThrownBy(() -> RulesFunctions.awsPartition("us-east-1")) + .isInstanceOf(SdkClientException.class) + .hasMessageContaining("us-east-1") + .hasMessageContaining("'aws' partition") + .hasMessageContaining("aws.partitionsFile"); + } + + @Test + void defaultMetadata_unmatchedRegion_fallsBackToAws() { + assertThat(RulesFunctions.awsPartition("not-a-real-region-1").name()).isEqualTo("aws"); + } + + @Test + void defaultMetadata_knownRegionResolvesToItsOwnPartition() { + assertThat(RulesFunctions.awsPartition("us-west-2").name()).isEqualTo("aws"); + assertThat(RulesFunctions.awsPartition("cn-north-1").name()).isEqualTo("aws-cn"); + assertThat(RulesFunctions.awsPartition("us-gov-west-1").name()).isEqualTo("aws-us-gov"); + } + + private static void resetPartitionDataCache() throws Exception { + Field partitionDataField = RulesFunctions.class.getDeclaredField("PARTITION_DATA"); + partitionDataField.setAccessible(true); + Object lazyValue = partitionDataField.get(null); + + Field valueField = lazyValue.getClass().getDeclaredField("value"); + valueField.setAccessible(true); + valueField.set(lazyValue, null); + + Field initializedField = lazyValue.getClass().getDeclaredField("initialized"); + initializedField.setAccessible(true); + initializedField.setBoolean(lazyValue, false); + } +} diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/compiledendpointrules/endpoints/internal/RulesFunctionsSplitTest.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/compiledendpointrules/endpoints/internal/RulesFunctionsSplitTest.java new file mode 100644 index 000000000000..37d40593479c --- /dev/null +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/compiledendpointrules/endpoints/internal/RulesFunctionsSplitTest.java @@ -0,0 +1,211 @@ +/* + * 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.compiledendpointrules.endpoints.internal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.Arrays; +import java.util.Collections; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import software.amazon.awssdk.core.exception.SdkClientException; + +/** + * Covers {@code RulesFunctions.split}'s handling of arguments the endpoint rules specification forbids. + * + *

None of {@code value}, {@code delimiter} or {@code limit} is optional, so reaching this function with a bad one + * means the rule set or the generated provider is wrong. Each therefore has to fail as a named + * {@link SdkClientException} that a reader can act on, rather than as whatever the first dereference happens to + * produce. The four cases below previously produced four different outcomes, three of them unhelpful and one of them + * dangerous. + * + *

The valid cases are pinned alongside them, because the natural way to add the validation - after the + * {@code limit == 1} shortcut, where the arguments are first used - leaves two of the violations undetected. + */ +class RulesFunctionsSplitTest { + + // ---- value: the rules type checker requires it to be set ---- + + /** + * Covers every limit branch. {@code limit == 1} is the one that matters most: it returns before {@code value} is + * dereferenced, so a null used to come back as {@code [null]} with no error at all. Downstream that reads as an + * unset value, which sends the rule down a different branch instead of failing. + */ + @ParameterizedTest + @ValueSource(ints = {0, 1, 2, 5}) + void nullValue_throwsSdkClientException(int limit) { + assertThatThrownBy(() -> RulesFunctions.split(null, ",", limit)) + .isInstanceOf(SdkClientException.class) + .isNotInstanceOf(NullPointerException.class) + .hasMessageContaining("null value"); + } + + // ---- delimiter: "must not be null or empty" ---- + + /** + * At {@code limit == 1} the delimiter is never read, so a null one used to be accepted silently. + */ + @ParameterizedTest + @ValueSource(ints = {0, 1, 2, 5}) + void nullDelimiter_throwsSdkClientException(int limit) { + assertThatThrownBy(() -> RulesFunctions.split("a,b", null, limit)) + .isInstanceOf(SdkClientException.class) + .isNotInstanceOf(NullPointerException.class) + .hasMessageContaining("null delimiter"); + } + + /** + * An empty delimiter with a limit produced silent nonsense - {@code split("abc", "", 4)} returned + * {@code ["", "", "", "abc"]}. + */ + @ParameterizedTest + @ValueSource(ints = {2, 4, 5}) + void emptyDelimiterWithLimit_throwsSdkClientException(int limit) { + assertThatThrownBy(() -> RulesFunctions.split("abc", "", limit)) + .isInstanceOf(SdkClientException.class) + .hasMessageContaining("empty delimiter"); + } + + /** + * The case that made this worth fixing rather than tidying. {@code indexOf("")} matches at the current position + * every time, so with no limit the loop appended empty strings until the heap was exhausted - + * {@link OutOfMemoryError} on a 256 MB heap, which takes more than the request down with it. + * + *

The timeout is the assertion as much as the exception type is: a regression here hangs rather than fails. + */ + @Test + @Timeout(10) + void emptyDelimiterUnlimited_throwsInsteadOfExhaustingTheHeap() { + assertThatThrownBy(() -> RulesFunctions.split("abc", "", 0)) + .isInstanceOf(SdkClientException.class) + .hasMessageContaining("empty delimiter"); + } + + /** + * An empty value short-circuits before the delimiter is read, so this pins that validation still happens. + */ + @Test + void emptyValueWithBadDelimiter_stillThrows() { + assertThatThrownBy(() -> RulesFunctions.split("", null, 0)) + .isInstanceOf(SdkClientException.class) + .hasMessageContaining("null delimiter"); + assertThatThrownBy(() -> RulesFunctions.split("", "", 0)) + .isInstanceOf(SdkClientException.class) + .hasMessageContaining("empty delimiter"); + } + + // ---- limit: "must not be negative" ---- + + /** + * Previously an {@code IllegalArgumentException: Illegal Capacity: -1} from {@code ArrayList}'s constructor, which + * names neither split nor the limit. + */ + @ParameterizedTest + @ValueSource(ints = {-1, -2, -100, Integer.MIN_VALUE}) + void negativeLimit_throwsSdkClientExceptionNamingTheLimit(int limit) { + assertThatThrownBy(() -> RulesFunctions.split("a,b", ",", limit)) + .isInstanceOf(SdkClientException.class) + .isNotInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("negative limit") + .hasMessageContaining(String.valueOf(limit)); + } + + // ---- valid input keeps working ---- + + /** + * Stated by the specification: "For empty input strings, the function returns an array containing a single empty + * string." Not an empty array, which is what a reader tends to assume. + */ + @Test + void emptyValue_returnsSingleEmptyString() { + assertThat(RulesFunctions.split("", ",", 0)).containsExactly(""); + assertThat(RulesFunctions.split("", ",", 1)).containsExactly(""); + assertThat(RulesFunctions.split("", ",", 3)).containsExactly(""); + } + + @Test + void limitOne_returnsWholeValueUnsplit() { + assertThat(RulesFunctions.split("a,b,c", ",", 1)).containsExactly("a,b,c"); + } + + @Test + void limitZero_splitsOnEveryOccurrence() { + assertThat(RulesFunctions.split("a,b,c", ",", 0)).containsExactly("a", "b", "c"); + } + + @Test + void limitBoundsThePartsAndKeepsTheRemainderWhole() { + assertThat(RulesFunctions.split("a,b,c,d", ",", 2)).containsExactly("a", "b,c,d"); + assertThat(RulesFunctions.split("a,b,c,d", ",", 3)).containsExactly("a", "b", "c,d"); + assertThat(RulesFunctions.split("a,b,c,d", ",", 99)).containsExactly("a", "b", "c", "d"); + } + + @Test + void delimiterAbsent_returnsTheWholeValue() { + assertThat(RulesFunctions.split("abc", ",", 0)).containsExactly("abc"); + } + + /** + * The shape the S3 rules use: {@code split(Bucket, "--", 0)} feeding an index read, for S3 Express bucket names. + */ + @Test + void multiCharacterDelimiter_splitsOnTheWholeDelimiter() { + assertThat(RulesFunctions.split("mybucket--usw2-az1--x-s3", "--", 0)) + .containsExactly("mybucket", "usw2-az1", "x-s3"); + assertThat(RulesFunctions.split("a-b", "--", 0)).containsExactly("a-b"); + } + + @Test + void delimitersAtTheEdgesProduceEmptyParts() { + assertThat(RulesFunctions.split(",a,", ",", 0)).containsExactly("", "a", ""); + assertThat(RulesFunctions.split("a,,b", ",", 0)).containsExactly("a", "", "b"); + } + + @Test + void valueEqualToTheDelimiter_producesTwoEmptyParts() { + assertThat(RulesFunctions.split(",", ",", 0)).containsExactly("", ""); + } + + /** + * A long unlimited split, to exercise the growth path of the list now that {@code limit == 0} seeds a capacity + * instead of starting empty. + */ + @Test + void unlimitedSplitOfManyParts() { + int parts = 200; + StringBuilder value = new StringBuilder(); + for (int i = 0; i < parts; i++) { + if (i > 0) { + value.append(','); + } + value.append('p').append(i); + } + + assertThat(RulesFunctions.split(value.toString(), ",", 0)) + .hasSize(parts) + .startsWith("p0", "p1") + .endsWith("p" + (parts - 1)); + } + + @Test + void resultIsTheDocumentedOrderForAKnownCase() { + assertThat(RulesFunctions.split("a.b.c", ".", 0)).isEqualTo(Arrays.asList("a", "b", "c")); + assertThat(RulesFunctions.split("solo", ".", 0)).isEqualTo(Collections.singletonList("solo")); + } +} diff --git a/test/codegen-generated-classes-test/src/test/resources/software/amazon/awssdk/services/endpointproviders/no-aws-partitions.json b/test/codegen-generated-classes-test/src/test/resources/software/amazon/awssdk/services/endpointproviders/no-aws-partitions.json new file mode 100644 index 000000000000..20f0a977d26c --- /dev/null +++ b/test/codegen-generated-classes-test/src/test/resources/software/amazon/awssdk/services/endpointproviders/no-aws-partitions.json @@ -0,0 +1,19 @@ +{ + "partitions" : [ + { + "id": "aws-cn", + "outputs": { + "dnsSuffix": "amazonaws.com.cn", + "dualStackDnsSuffix": "api.amazonwebservices.com.cn", + "implicitGlobalRegion": "cn-northwest-1", + "name": "aws-cn", + "supportsDualStack": true, + "supportsFIPS": true + }, + "regionRegex": "^cn\\-\\w+\\-\\d+$", + "regions": { + "cn-north-1": {} + } + } + ] +} From 25e8fecc13d73ee6c120eed0e89aafab15d8636c Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Tue, 1 Sep 2026 11:52:19 -0700 Subject: [PATCH 6/7] Add BDD models for S3 and Dynamodb for release staging --- .../dynamodb/endpoint-bdd-1.json | 761 +++++ .../codegen-resources/endpoint-bdd-1.json | 2551 +++++++++++++++++ 2 files changed, 3312 insertions(+) create mode 100644 services/dynamodb/src/main/resources/codegen-resources/dynamodb/endpoint-bdd-1.json create mode 100644 services/s3/src/main/resources/codegen-resources/endpoint-bdd-1.json diff --git a/services/dynamodb/src/main/resources/codegen-resources/dynamodb/endpoint-bdd-1.json b/services/dynamodb/src/main/resources/codegen-resources/dynamodb/endpoint-bdd-1.json new file mode 100644 index 000000000000..3613e61fbc38 --- /dev/null +++ b/services/dynamodb/src/main/resources/codegen-resources/dynamodb/endpoint-bdd-1.json @@ -0,0 +1,761 @@ +{ + "version": "1.1", + "parameters": { + "Region": { + "builtIn": "AWS::Region", + "required": false, + "documentation": "The AWS region used to dispatch the request.", + "type": "string" + }, + "UseDualStack": { + "builtIn": "AWS::UseDualStack", + "required": true, + "default": false, + "documentation": "When true, use the dual-stack endpoint. If the configured endpoint does not support dual-stack, dispatching the request MAY return an error.", + "type": "boolean" + }, + "UseFIPS": { + "builtIn": "AWS::UseFIPS", + "required": true, + "default": false, + "documentation": "When true, send this request to the FIPS-compliant regional endpoint. If the configured endpoint does not have a FIPS compliant endpoint, dispatching the request will return an error.", + "type": "boolean" + }, + "Endpoint": { + "builtIn": "SDK::Endpoint", + "required": false, + "documentation": "Override the endpoint used to send this request", + "type": "string" + }, + "AccountId": { + "builtIn": "AWS::Auth::AccountId", + "required": false, + "documentation": "The AWS AccountId used for the request.", + "type": "string" + }, + "AccountIdEndpointMode": { + "builtIn": "AWS::Auth::AccountIdEndpointMode", + "required": false, + "documentation": "The AccountId Endpoint Mode.", + "type": "string" + }, + "ResourceArn": { + "required": false, + "documentation": "ResourceArn containing arn of resource", + "type": "string" + }, + "ResourceArnList": { + "required": false, + "documentation": "ResourceArnList containing list of resource arns", + "type": "stringArray" + }, + "IsSearchOperation": { + "required": false, + "documentation": "Set to true for SearchVectors to route to the Search FQDN", + "type": "boolean" + } + }, + "conditions": [ + { + "fn": "isSet", + "argv": [ + { + "ref": "Region" + } + ] + }, + { + "fn": "isSet", + "argv": [ + { + "ref": "Endpoint" + } + ] + }, + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseFIPS" + }, + true + ] + }, + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseDualStack" + }, + true + ] + }, + { + "fn": "aws.partition", + "argv": [ + { + "ref": "Region" + } + ], + "assign": "PartitionResult" + }, + { + "fn": "stringEquals", + "argv": [ + { + "ref": "Region" + }, + "local" + ] + }, + { + "fn": "booleanEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsFIPS" + ] + }, + true + ] + }, + { + "fn": "parseURL", + "argv": [ + { + "ref": "Endpoint" + } + ], + "assign": "parsedEndpoint" + }, + { + "fn": "stringEquals", + "argv": [ + "dynamodb.{Region}.{PartitionResult#dualStackDnsSuffix}", + { + "fn": "getAttr", + "argv": [ + { + "ref": "parsedEndpoint" + }, + "authority" + ] + } + ] + }, + { + "fn": "booleanEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "supportsDualStack" + ] + }, + true + ] + }, + { + "fn": "stringEquals", + "argv": [ + "search-dynamodb.{Region}.{PartitionResult#dualStackDnsSuffix}", + { + "fn": "getAttr", + "argv": [ + { + "ref": "parsedEndpoint" + }, + "authority" + ] + } + ] + }, + { + "fn": "isSet", + "argv": [ + { + "ref": "AccountIdEndpointMode" + } + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "name" + ] + }, + "aws" + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "ref": "AccountIdEndpointMode" + }, + "disabled" + ] + }, + { + "fn": "isSet", + "argv": [ + { + "ref": "ResourceArn" + } + ] + }, + { + "fn": "aws.parseArn", + "argv": [ + { + "ref": "ResourceArn" + } + ], + "assign": "ParsedArn_ssa_2" + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "ParsedArn_ssa_2" + }, + "region" + ] + }, + { + "ref": "Region" + } + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "ParsedArn_ssa_2" + }, + "service" + ] + }, + "dynamodb" + ] + }, + { + "fn": "isValidHostLabel", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "ParsedArn_ssa_2" + }, + "region" + ] + }, + false + ] + }, + { + "fn": "isValidHostLabel", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "ParsedArn_ssa_2" + }, + "accountId" + ] + }, + false + ] + }, + { + "fn": "isSet", + "argv": [ + { + "ref": "ResourceArnList" + } + ] + }, + { + "fn": "getAttr", + "argv": [ + { + "ref": "ResourceArnList" + }, + "[0]" + ], + "assign": "FirstArn" + }, + { + "fn": "aws.parseArn", + "argv": [ + { + "ref": "FirstArn" + } + ], + "assign": "ParsedArn_ssa_1" + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "ParsedArn_ssa_1" + }, + "region" + ] + }, + { + "ref": "Region" + } + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "ParsedArn_ssa_1" + }, + "service" + ] + }, + "dynamodb" + ] + }, + { + "fn": "isValidHostLabel", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "ParsedArn_ssa_1" + }, + "accountId" + ] + }, + false + ] + }, + { + "fn": "isValidHostLabel", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "ParsedArn_ssa_1" + }, + "region" + ] + }, + false + ] + }, + { + "fn": "isSet", + "argv": [ + { + "ref": "AccountId" + } + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "ref": "AccountIdEndpointMode" + }, + "required" + ] + }, + { + "fn": "isValidHostLabel", + "argv": [ + { + "ref": "AccountId" + }, + false + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "PartitionResult" + }, + "name" + ] + }, + "aws-us-gov" + ] + }, + { + "fn": "booleanEquals", + "argv": [ + { + "fn": "coalesce", + "argv": [ + { + "ref": "IsSearchOperation" + }, + false + ] + }, + true + ] + } + ], + "results": [ + { + "conditions": [], + "error": "Invalid Configuration: FIPS and custom endpoint are not supported", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid Configuration: Dualstack and custom endpoint are not supported", + "type": "error" + }, + { + "conditions": [], + "error": "Endpoint override is not supported for dual-stack endpoints. Please enable dual-stack functionality by enabling the configuration. For more details, see: https://docs.aws.amazon.com/sdkref/latest/guide/feature-endpoints.html", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": "{Endpoint}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "error": "Invalid Configuration: FIPS and local endpoint are not supported", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid Configuration: Dualstack and local endpoint are not supported", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": "http://localhost:8000", + "properties": { + "authSchemes": [ + { + "signingRegion": "us-east-1", + "name": "sigv4", + "signingName": "dynamodb" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "error": "Invalid Configuration: AccountIdEndpointMode is required and FIPS is enabled, but FIPS account endpoints are not supported", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": "https://search-dynamodb-fips.{Region}.{PartitionResult#dualStackDnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://dynamodb-fips.{Region}.{PartitionResult#dualStackDnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "error": "FIPS and DualStack are enabled, but this partition does not support one or both", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": "https://search-dynamodb.{Region}.{PartitionResult#dnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://dynamodb.{Region}.{PartitionResult#dnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://search-dynamodb-fips.{Region}.{PartitionResult#dnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://dynamodb-fips.{Region}.{PartitionResult#dnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "error": "FIPS is enabled but this partition does not support FIPS", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{ParsedArn_ssa_2#accountId}.search-ddb.{Region}.{PartitionResult#dualStackDnsSuffix}", + "properties": { + "metricValues": [ + "O" + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{ParsedArn_ssa_2#accountId}.ddb.{Region}.{PartitionResult#dualStackDnsSuffix}", + "properties": { + "metricValues": [ + "O" + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{ParsedArn_ssa_1#accountId}.search-ddb.{Region}.{PartitionResult#dualStackDnsSuffix}", + "properties": { + "metricValues": [ + "O" + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{ParsedArn_ssa_1#accountId}.ddb.{Region}.{PartitionResult#dualStackDnsSuffix}", + "properties": { + "metricValues": [ + "O" + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{AccountId}.search-ddb.{Region}.{PartitionResult#dualStackDnsSuffix}", + "properties": { + "metricValues": [ + "O" + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{AccountId}.ddb.{Region}.{PartitionResult#dualStackDnsSuffix}", + "properties": { + "metricValues": [ + "O" + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "error": "Credentials-sourced account ID parameter is invalid", + "type": "error" + }, + { + "conditions": [], + "error": "AccountIdEndpointMode is required but no AccountID was provided or able to be loaded", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid Configuration: AccountIdEndpointMode is required but account endpoints are not supported in this partition", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": "https://search-dynamodb.{Region}.{PartitionResult#dualStackDnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://dynamodb.{Region}.{PartitionResult#dualStackDnsSuffix}", + "properties": {}, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "error": "DualStack is enabled but this partition does not support DualStack", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{ParsedArn_ssa_2#accountId}.search-ddb.{Region}.{PartitionResult#dnsSuffix}", + "properties": { + "metricValues": [ + "O" + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{ParsedArn_ssa_2#accountId}.ddb.{Region}.{PartitionResult#dnsSuffix}", + "properties": { + "metricValues": [ + "O" + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{ParsedArn_ssa_1#accountId}.search-ddb.{Region}.{PartitionResult#dnsSuffix}", + "properties": { + "metricValues": [ + "O" + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{ParsedArn_ssa_1#accountId}.ddb.{Region}.{PartitionResult#dnsSuffix}", + "properties": { + "metricValues": [ + "O" + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{AccountId}.search-ddb.{Region}.{PartitionResult#dnsSuffix}", + "properties": { + "metricValues": [ + "O" + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{AccountId}.ddb.{Region}.{PartitionResult#dnsSuffix}", + "properties": { + "metricValues": [ + "O" + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "error": "Invalid Configuration: Missing Region", + "type": "error" + } + ], + "root": 2, + "nodeCount": 82, + "nodes": "/////wAAAAH/////AAAAAAAAAAYAAAADAAAAAQAAAAQF9eEjAAAAAgX14QEAAAAFAAAAAwX14QIF9eEEAAAAAQAAAE0AAAAHAAAAAgAAAD0AAAAIAAAAAwAAACIAAAAJAAAABAAAAAoF9eEjAAAABQX14QcAAAALAAAACwAAAAwAAABFAAAADAAAAA4AAAANAAAAHAX14RkAAABFAAAADQAAACEAAAAPAAAADgAAABAAAAAVAAAADwAAABEAAAAVAAAAEAAAABIAAAAVAAAAEQAAABMAAAAVAAAAEgAAABQAAAAVAAAAEwAAACAAAAAVAAAAFAAAABYAAAAcAAAAFQAAABcAAAAcAAAAFgAAABgAAAAcAAAAFwAAABkAAAAcAAAAGAAAABoAAAAcAAAAGQAAABsAAAAcAAAAGgAAAB8AAAAcAAAAGwAAAB0AAAAhAAAAHQAAAB4F9eEXAAAAHwX14SEF9eEiAAAAHwX14R8F9eEgAAAAHwX14R0F9eEeAAAAHAX14RgAAABFAAAABAAAACMF9eEjAAAABQX14QYAAAAkAAAACQAAACUF9eEcAAAACwAAACYAAAA8AAAADAAAACgAAAAnAAAAHAX14RkAAAA8AAAADQAAADsAAAApAAAADgAAACoAAAAvAAAADwAAACsAAAAvAAAAEAAAACwAAAAvAAAAEQAAAC0AAAAvAAAAEgAAAC4AAAAvAAAAEwAAADoAAAAvAAAAFAAAADAAAAA2AAAAFQAAADEAAAA2AAAAFgAAADIAAAA2AAAAFwAAADMAAAA2AAAAGAAAADQAAAA2AAAAGQAAADUAAAA2AAAAGgAAADkAAAA2AAAAGwAAADcAAAA7AAAAHQAAADgF9eEXAAAAHwX14RUF9eEWAAAAHwX14RMF9eEUAAAAHwX14REF9eESAAAAHAX14RgAAAA8AAAAHwX14RoF9eEbAAAAAwAAAEYAAAA+AAAABAAAAD8F9eEjAAAABQX14QUAAABAAAAABgAAAEEF9eEQAAAACwAAAEIAAABDAAAAHAX14QgAAABDAAAAHgAAAEUAAABEAAAAHwX14Q4F9eEPAAAAHwX14QwF9eENAAAABAAAAEcF9eEjAAAABQX14QUAAABIAAAABgAAAEkF9eELAAAACQAAAEoF9eELAAAACwAAAEsAAABMAAAAHAX14QgAAABMAAAAHwX14QkF9eEKAAAAAgX14QEAAABOAAAAAwX14QIAAABPAAAABAAAAFAF9eEEAAAABwAAAFEF9eEEAAAACAX14QMAAABSAAAACgX14QMF9eEE" +} \ No newline at end of file diff --git a/services/s3/src/main/resources/codegen-resources/endpoint-bdd-1.json b/services/s3/src/main/resources/codegen-resources/endpoint-bdd-1.json new file mode 100644 index 000000000000..3f427f9c104f --- /dev/null +++ b/services/s3/src/main/resources/codegen-resources/endpoint-bdd-1.json @@ -0,0 +1,2551 @@ +{ + "version": "1.1", + "parameters": { + "Bucket": { + "required": false, + "documentation": "The S3 bucket used to send the request. This is an optional parameter that will be set automatically for operations that are scoped to an S3 bucket.", + "type": "string" + }, + "Region": { + "builtIn": "AWS::Region", + "required": false, + "documentation": "The AWS region used to dispatch the request.", + "type": "string" + }, + "UseFIPS": { + "builtIn": "AWS::UseFIPS", + "required": true, + "default": false, + "documentation": "When true, send this request to the FIPS-compliant regional endpoint. If the configured endpoint does not have a FIPS compliant endpoint, dispatching the request will return an error.", + "type": "boolean" + }, + "UseDualStack": { + "builtIn": "AWS::UseDualStack", + "required": true, + "default": false, + "documentation": "When true, use the dual-stack endpoint. If the configured endpoint does not support dual-stack, dispatching the request MAY return an error.", + "type": "boolean" + }, + "Endpoint": { + "builtIn": "SDK::Endpoint", + "required": false, + "documentation": "Override the endpoint used to send this request", + "type": "string" + }, + "ForcePathStyle": { + "builtIn": "AWS::S3::ForcePathStyle", + "required": true, + "default": false, + "documentation": "When true, force a path-style endpoint to be used where the bucket name is part of the path.", + "type": "boolean" + }, + "Accelerate": { + "builtIn": "AWS::S3::Accelerate", + "required": true, + "default": false, + "documentation": "When true, use S3 Accelerate. NOTE: Not all regions support S3 accelerate.", + "type": "boolean" + }, + "UseGlobalEndpoint": { + "builtIn": "AWS::S3::UseGlobalEndpoint", + "required": true, + "default": false, + "documentation": "Whether the global endpoint should be used, rather then the regional endpoint for us-east-1.", + "type": "boolean" + }, + "UseObjectLambdaEndpoint": { + "required": false, + "documentation": "Internal parameter to use object lambda endpoint for an operation (eg: WriteGetObjectResponse)", + "type": "boolean" + }, + "Key": { + "required": false, + "documentation": "The S3 Key used to send the request. This is an optional parameter that will be set automatically for operations that are scoped to an S3 Key.", + "type": "string" + }, + "Prefix": { + "required": false, + "documentation": "The S3 Prefix used to send the request. This is an optional parameter that will be set automatically for operations that are scoped to an S3 Prefix.", + "type": "string" + }, + "CopySource": { + "required": false, + "documentation": "The Copy Source used for Copy Object request. This is an optional parameter that will be set automatically for operations that are scoped to Copy Source.", + "type": "string" + }, + "DisableAccessPoints": { + "required": false, + "documentation": "Internal parameter to disable Access Point Buckets", + "type": "boolean" + }, + "DisableMultiRegionAccessPoints": { + "builtIn": "AWS::S3::DisableMultiRegionAccessPoints", + "required": true, + "default": false, + "documentation": "Whether multi-region access points (MRAP) should be disabled.", + "type": "boolean" + }, + "UseArnRegion": { + "builtIn": "AWS::S3::UseArnRegion", + "required": false, + "documentation": "When an Access Point ARN is provided and this flag is enabled, the SDK MUST use the ARN's region when constructing the endpoint instead of the client's configured region.", + "type": "boolean" + }, + "UseS3ExpressControlEndpoint": { + "required": false, + "documentation": "Internal parameter to indicate whether S3Express operation should use control plane, (ex. CreateBucket)", + "type": "boolean" + }, + "DisableS3ExpressSessionAuth": { + "required": false, + "documentation": "Parameter to indicate whether S3Express session auth should be disabled", + "type": "boolean" + } + }, + "conditions": [ + { + "fn": "isSet", + "argv": [ + { + "ref": "Region" + } + ] + }, + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "Accelerate" + }, + true + ] + }, + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseFIPS" + }, + true + ] + }, + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseDualStack" + }, + true + ] + }, + { + "fn": "isSet", + "argv": [ + { + "ref": "Endpoint" + } + ] + }, + { + "fn": "isSet", + "argv": [ + { + "ref": "Bucket" + } + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "coalesce", + "argv": [ + { + "fn": "substring", + "argv": [ + { + "ref": "Bucket" + }, + 0, + 6, + true + ] + }, + "" + ] + }, + "--x-s3" + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "coalesce", + "argv": [ + { + "fn": "substring", + "argv": [ + { + "ref": "Bucket" + }, + 0, + 7, + true + ] + }, + "" + ] + }, + "--xa-s3" + ] + }, + { + "fn": "aws.partition", + "argv": [ + { + "ref": "Region" + } + ], + "assign": "partitionResult" + }, + { + "fn": "substring", + "argv": [ + { + "ref": "Bucket" + }, + 0, + 7, + true + ], + "assign": "accessPointSuffix" + }, + { + "fn": "stringEquals", + "argv": [ + { + "ref": "accessPointSuffix" + }, + "--op-s3" + ] + }, + { + "fn": "substring", + "argv": [ + { + "ref": "Bucket" + }, + 8, + 12, + true + ], + "assign": "regionPrefix" + }, + { + "fn": "substring", + "argv": [ + { + "ref": "Bucket" + }, + 32, + 49, + true + ], + "assign": "outpostId_ssa_2" + }, + { + "fn": "substring", + "argv": [ + { + "ref": "Bucket" + }, + 49, + 50, + true + ], + "assign": "hardwareType" + }, + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "ForcePathStyle" + }, + true + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "partitionResult" + }, + "name" + ] + }, + "aws-cn" + ] + }, + { + "fn": "ite", + "argv": [ + { + "ref": "UseDualStack" + }, + ".dualstack", + "" + ], + "assign": "_s3e_ds" + }, + { + "fn": "isValidHostLabel", + "argv": [ + { + "ref": "outpostId_ssa_2" + }, + false + ] + }, + { + "fn": "ite", + "argv": [ + { + "ref": "UseFIPS" + }, + "-fips", + "" + ], + "assign": "_s3e_fips" + }, + { + "fn": "ite", + "argv": [ + { + "fn": "coalesce", + "argv": [ + { + "ref": "DisableS3ExpressSessionAuth" + }, + false + ] + }, + "sigv4", + "sigv4-s3express" + ], + "assign": "_s3e_auth" + }, + { + "fn": "aws.isVirtualHostableS3Bucket", + "argv": [ + { + "ref": "Bucket" + }, + false + ] + }, + { + "fn": "parseURL", + "argv": [ + { + "ref": "Endpoint" + } + ], + "assign": "url" + }, + { + "fn": "booleanEquals", + "argv": [ + { + "fn": "coalesce", + "argv": [ + { + "ref": "UseS3ExpressControlEndpoint" + }, + false + ] + }, + true + ] + }, + { + "fn": "aws.isVirtualHostableS3Bucket", + "argv": [ + { + "ref": "Bucket" + }, + true + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "url" + }, + "scheme" + ] + }, + "http" + ] + }, + { + "fn": "isValidHostLabel", + "argv": [ + { + "ref": "Region" + }, + false + ] + }, + { + "fn": "aws.parseArn", + "argv": [ + { + "ref": "Bucket" + } + ], + "assign": "bucketArn" + }, + { + "fn": "getAttr", + "argv": [ + { + "fn": "split", + "argv": [ + { + "ref": "Bucket" + }, + "--", + 0 + ] + }, + "[-2]" + ], + "assign": "s3expressAvailabilityZoneId" + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "coalesce", + "argv": [ + { + "fn": "substring", + "argv": [ + { + "ref": "Bucket" + }, + 0, + 4, + false + ] + }, + "" + ] + }, + "arn:" + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "coalesce", + "argv": [ + { + "fn": "substring", + "argv": [ + { + "ref": "Bucket" + }, + 16, + 18, + true + ] + }, + "" + ] + }, + "--" + ] + }, + { + "fn": "booleanEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "url" + }, + "isIp" + ] + }, + true + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "coalesce", + "argv": [ + { + "fn": "substring", + "argv": [ + { + "ref": "Bucket" + }, + 21, + 23, + true + ] + }, + "" + ] + }, + "--" + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "coalesce", + "argv": [ + { + "fn": "substring", + "argv": [ + { + "ref": "Bucket" + }, + 27, + 29, + true + ] + }, + "" + ] + }, + "--" + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "ref": "regionPrefix" + }, + "beta" + ] + }, + { + "fn": "uriEncode", + "argv": [ + { + "ref": "Bucket" + } + ], + "assign": "uri_encoded_bucket" + }, + { + "fn": "isValidHostLabel", + "argv": [ + { + "ref": "Region" + }, + true + ] + }, + { + "fn": "booleanEquals", + "argv": [ + { + "fn": "coalesce", + "argv": [ + { + "ref": "UseObjectLambdaEndpoint" + }, + false + ] + }, + true + ] + }, + { + "fn": "getAttr", + "argv": [ + { + "ref": "bucketArn" + }, + "resourceId[0]" + ], + "assign": "arnType" + }, + { + "fn": "stringEquals", + "argv": [ + { + "ref": "arnType" + }, + "" + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "ref": "arnType" + }, + "accesspoint" + ] + }, + { + "fn": "getAttr", + "argv": [ + { + "ref": "bucketArn" + }, + "resourceId[1]" + ], + "assign": "accessPointName_ssa_1" + }, + { + "fn": "stringEquals", + "argv": [ + { + "ref": "accessPointName_ssa_1" + }, + "" + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "bucketArn" + }, + "region" + ] + }, + "" + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "coalesce", + "argv": [ + { + "fn": "substring", + "argv": [ + { + "ref": "Bucket" + }, + 14, + 16, + true + ] + }, + "" + ] + }, + "--" + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "ref": "hardwareType" + }, + "e" + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "ref": "hardwareType" + }, + "o" + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "ref": "Region" + }, + "aws-global" + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "coalesce", + "argv": [ + { + "fn": "substring", + "argv": [ + { + "ref": "Bucket" + }, + 19, + 21, + true + ] + }, + "" + ] + }, + "--" + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "bucketArn" + }, + "service" + ] + }, + "s3-object-lambda" + ] + }, + { + "fn": "booleanEquals", + "argv": [ + { + "fn": "coalesce", + "argv": [ + { + "ref": "DisableAccessPoints" + }, + false + ] + }, + true + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "bucketArn" + }, + "service" + ] + }, + "s3-outposts" + ] + }, + { + "fn": "aws.partition", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "bucketArn" + }, + "region" + ] + } + ], + "assign": "bucketPartition" + }, + { + "fn": "isValidHostLabel", + "argv": [ + { + "ref": "accessPointName_ssa_1" + }, + true + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "coalesce", + "argv": [ + { + "fn": "substring", + "argv": [ + { + "ref": "Bucket" + }, + 26, + 28, + true + ] + }, + "" + ] + }, + "--" + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "coalesce", + "argv": [ + { + "fn": "substring", + "argv": [ + { + "ref": "Bucket" + }, + 15, + 17, + true + ] + }, + "" + ] + }, + "--" + ] + }, + { + "fn": "getAttr", + "argv": [ + { + "ref": "bucketArn" + }, + "resourceId[4]" + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "coalesce", + "argv": [ + { + "fn": "substring", + "argv": [ + { + "ref": "Bucket" + }, + 20, + 22, + true + ] + }, + "" + ] + }, + "--" + ] + }, + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "UseGlobalEndpoint" + }, + true + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "ref": "Region" + }, + "us-east-1" + ] + }, + { + "fn": "getAttr", + "argv": [ + { + "ref": "bucketArn" + }, + "resourceId[1]" + ], + "assign": "outpostId_ssa_1" + }, + { + "fn": "booleanEquals", + "argv": [ + { + "fn": "coalesce", + "argv": [ + { + "ref": "UseArnRegion" + }, + true + ] + }, + true + ] + }, + { + "fn": "isValidHostLabel", + "argv": [ + { + "ref": "outpostId_ssa_1" + }, + false + ] + }, + { + "fn": "getAttr", + "argv": [ + { + "ref": "bucketArn" + }, + "resourceId[2]" + ], + "assign": "outpostType" + }, + { + "fn": "stringEquals", + "argv": [ + { + "ref": "Region" + }, + { + "fn": "getAttr", + "argv": [ + { + "ref": "bucketArn" + }, + "region" + ] + } + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "bucketPartition" + }, + "name" + ] + }, + { + "fn": "getAttr", + "argv": [ + { + "ref": "partitionResult" + }, + "name" + ] + } + ] + }, + { + "fn": "booleanEquals", + "argv": [ + { + "ref": "DisableMultiRegionAccessPoints" + }, + true + ] + }, + { + "fn": "isValidHostLabel", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "bucketArn" + }, + "region" + ] + }, + true + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "bucketArn" + }, + "partition" + ] + }, + { + "fn": "getAttr", + "argv": [ + { + "ref": "partitionResult" + }, + "name" + ] + } + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "bucketArn" + }, + "accountId" + ] + }, + "" + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "bucketArn" + }, + "service" + ] + }, + "s3" + ] + }, + { + "fn": "isValidHostLabel", + "argv": [ + { + "fn": "getAttr", + "argv": [ + { + "ref": "bucketArn" + }, + "accountId" + ] + }, + false + ] + }, + { + "fn": "getAttr", + "argv": [ + { + "ref": "bucketArn" + }, + "resourceId[3]" + ], + "assign": "accessPointName_ssa_2" + }, + { + "fn": "isValidHostLabel", + "argv": [ + { + "ref": "accessPointName_ssa_1" + }, + false + ] + }, + { + "fn": "stringEquals", + "argv": [ + { + "ref": "outpostType" + }, + "accesspoint" + ] + }, + { + "fn": "isValidHostLabel", + "argv": [ + { + "ref": "accessPointName_ssa_2" + }, + false + ] + } + ], + "results": [ + { + "conditions": [], + "error": "Accelerate cannot be used with FIPS", + "type": "error" + }, + { + "conditions": [], + "error": "Cannot set dual-stack in combination with a custom endpoint.", + "type": "error" + }, + { + "conditions": [], + "error": "A custom endpoint cannot be combined with FIPS", + "type": "error" + }, + { + "conditions": [], + "error": "A custom endpoint cannot be combined with S3 Accelerate", + "type": "error" + }, + { + "conditions": [], + "error": "Partition does not support FIPS", + "type": "error" + }, + { + "conditions": [], + "error": "S3Express does not support S3 Accelerate.", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": "{url#scheme}://{url#authority}/{uri_encoded_bucket}{url#path}", + "properties": { + "backend": "S3Express", + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "{_s3e_auth}", + "signingName": "s3express", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "{url#scheme}://{Bucket}.{url#authority}{url#path}", + "properties": { + "backend": "S3Express", + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "{_s3e_auth}", + "signingName": "s3express", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "error": "S3Express bucket name is not a valid virtual hostable name.", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3express-control{_s3e_fips}{_s3e_ds}.{Region}.{partitionResult#dnsSuffix}/{uri_encoded_bucket}", + "properties": { + "backend": "S3Express", + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3express", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{Bucket}.s3express{_s3e_fips}-{s3expressAvailabilityZoneId}{_s3e_ds}.{Region}.{partitionResult#dnsSuffix}", + "properties": { + "backend": "S3Express", + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "{_s3e_auth}", + "signingName": "s3express", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "error": "Unrecognized S3Express bucket name format.", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": "{url#scheme}://{url#authority}{url#path}", + "properties": { + "backend": "S3Express", + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "{_s3e_auth}", + "signingName": "s3express", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3express-control{_s3e_fips}{_s3e_ds}.{Region}.{partitionResult#dnsSuffix}", + "properties": { + "backend": "S3Express", + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3express", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "error": "Expected a endpoint to be specified but no endpoint was found", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{Bucket}.ec2.{url#authority}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4a", + "signingName": "s3-outposts", + "signingRegionSet": [ + "*" + ] + }, + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3-outposts", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{Bucket}.ec2.s3-outposts.{Region}.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4a", + "signingName": "s3-outposts", + "signingRegionSet": [ + "*" + ] + }, + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3-outposts", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{Bucket}.op-{outpostId_ssa_2}.{url#authority}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4a", + "signingName": "s3-outposts", + "signingRegionSet": [ + "*" + ] + }, + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3-outposts", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{Bucket}.op-{outpostId_ssa_2}.s3-outposts.{Region}.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4a", + "signingName": "s3-outposts", + "signingRegionSet": [ + "*" + ] + }, + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3-outposts", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "error": "Unrecognized hardware type: \"Expected hardware type o or e but got {hardwareType}\"", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid Outposts Bucket alias - it must be a valid bucket name.", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid ARN: The outpost Id must only contain a-z, A-Z, 0-9 and `-`.", + "type": "error" + }, + { + "conditions": [], + "error": "Custom endpoint `{Endpoint}` was not a valid URI", + "type": "error" + }, + { + "conditions": [], + "error": "S3 Accelerate cannot be used in this region", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{Bucket}.s3-fips.dualstack.us-east-1.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "us-east-1" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{Bucket}.s3-fips.dualstack.{Region}.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{Bucket}.s3-fips.us-east-1.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "us-east-1" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{Bucket}.s3-fips.{Region}.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{Bucket}.s3-accelerate.dualstack.us-east-1.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "us-east-1" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{Bucket}.s3-accelerate.dualstack.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{Bucket}.s3.dualstack.us-east-1.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "us-east-1" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{Bucket}.s3.dualstack.{Region}.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "{url#scheme}://{url#authority}{url#normalizedPath}{Bucket}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "us-east-1" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "{url#scheme}://{Bucket}.{url#authority}{url#path}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "us-east-1" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "{url#scheme}://{url#authority}{url#normalizedPath}{Bucket}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "{url#scheme}://{Bucket}.{url#authority}{url#path}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{Bucket}.s3-accelerate.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "us-east-1" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{Bucket}.s3-accelerate.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{Bucket}.s3.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "us-east-1" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{Bucket}.s3.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{Bucket}.s3.{Region}.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "error": "Invalid region: region was not a valid DNS name.", + "type": "error" + }, + { + "conditions": [], + "error": "S3 Object Lambda does not support Dual-stack", + "type": "error" + }, + { + "conditions": [], + "error": "S3 Object Lambda does not support S3 Accelerate", + "type": "error" + }, + { + "conditions": [], + "error": "Access points are not supported for this operation", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid configuration: region from ARN `{bucketArn#region}` does not match client region `{Region}` and UseArnRegion is `false`", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid ARN: Missing account id", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": "{url#scheme}://{accessPointName_ssa_1}-{bucketArn#accountId}.{url#authority}{url#path}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3-object-lambda", + "signingRegion": "{bucketArn#region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{accessPointName_ssa_1}-{bucketArn#accountId}.s3-object-lambda-fips.{bucketArn#region}.{bucketPartition#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3-object-lambda", + "signingRegion": "{bucketArn#region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{accessPointName_ssa_1}-{bucketArn#accountId}.s3-object-lambda.{bucketArn#region}.{bucketPartition#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3-object-lambda", + "signingRegion": "{bucketArn#region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "error": "Invalid ARN: The access point name may only contain a-z, A-Z, 0-9 and `-`. Found: `{accessPointName_ssa_1}`", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid ARN: The account id may only contain a-z, A-Z, 0-9 and `-`. Found: `{bucketArn#accountId}`", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid region in ARN: `{bucketArn#region}` (invalid DNS name)", + "type": "error" + }, + { + "conditions": [], + "error": "Client was configured for partition `{partitionResult#name}` but ARN (`{Bucket}`) has `{bucketPartition#name}`", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid ARN: The ARN may only contain a single resource component after `accesspoint`.", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid ARN: bucket ARN is missing a region", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid ARN: Expected a resource of the format `accesspoint:` but no name was provided", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid ARN: Object Lambda ARNs only support `accesspoint` arn types, but found: `{arnType}`", + "type": "error" + }, + { + "conditions": [], + "error": "Access Points do not support S3 Accelerate", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{accessPointName_ssa_1}-{bucketArn#accountId}.s3-accesspoint-fips.dualstack.{bucketArn#region}.{bucketPartition#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{bucketArn#region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{accessPointName_ssa_1}-{bucketArn#accountId}.s3-accesspoint-fips.{bucketArn#region}.{bucketPartition#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{bucketArn#region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{accessPointName_ssa_1}-{bucketArn#accountId}.s3-accesspoint.dualstack.{bucketArn#region}.{bucketPartition#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{bucketArn#region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "{url#scheme}://{accessPointName_ssa_1}-{bucketArn#accountId}.{url#authority}{url#path}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{bucketArn#region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{accessPointName_ssa_1}-{bucketArn#accountId}.s3-accesspoint.{bucketArn#region}.{bucketPartition#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{bucketArn#region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "error": "Invalid ARN: The ARN was not for the S3 service, found: {bucketArn#service}", + "type": "error" + }, + { + "conditions": [], + "error": "S3 MRAP does not support dual-stack", + "type": "error" + }, + { + "conditions": [], + "error": "S3 MRAP does not support FIPS", + "type": "error" + }, + { + "conditions": [], + "error": "S3 MRAP does not support S3 Accelerate", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid configuration: Multi-Region Access Point ARNs are disabled.", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{accessPointName_ssa_1}.accesspoint.s3-global.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4a", + "signingName": "s3", + "signingRegionSet": [ + "*" + ] + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "error": "Client was configured for partition `{partitionResult#name}` but bucket referred to partition `{bucketArn#partition}`", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid Access Point Name", + "type": "error" + }, + { + "conditions": [], + "error": "S3 Outposts does not support Dual-stack", + "type": "error" + }, + { + "conditions": [], + "error": "S3 Outposts does not support FIPS", + "type": "error" + }, + { + "conditions": [], + "error": "S3 Outposts does not support S3 Accelerate", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid Arn: Outpost Access Point ARN contains sub resources", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{accessPointName_ssa_2}-{bucketArn#accountId}.{outpostId_ssa_1}.{url#authority}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4a", + "signingName": "s3-outposts", + "signingRegionSet": [ + "*" + ] + }, + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3-outposts", + "signingRegion": "{bucketArn#region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://{accessPointName_ssa_2}-{bucketArn#accountId}.{outpostId_ssa_1}.s3-outposts.{bucketArn#region}.{bucketPartition#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4a", + "signingName": "s3-outposts", + "signingRegionSet": [ + "*" + ] + }, + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3-outposts", + "signingRegion": "{bucketArn#region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "error": "Invalid ARN: The access point name may only contain a-z, A-Z, 0-9 and `-`. Found: `{accessPointName_ssa_2}`", + "type": "error" + }, + { + "conditions": [], + "error": "Expected an outpost type `accesspoint`, found {outpostType}", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid ARN: expected an access point name", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid ARN: Expected a 4-component resource", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid ARN: The outpost Id may only contain a-z, A-Z, 0-9 and `-`. Found: `{outpostId_ssa_1}`", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid ARN: The Outpost Id was not set", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid ARN: Unrecognized format: {Bucket} (type: {arnType})", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid ARN: No ARN type specified", + "type": "error" + }, + { + "conditions": [], + "error": "Invalid ARN: `{Bucket}` was not a valid ARN", + "type": "error" + }, + { + "conditions": [], + "error": "Path-style addressing cannot be used with ARN buckets", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3-fips.dualstack.us-east-1.{partitionResult#dnsSuffix}/{uri_encoded_bucket}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "us-east-1" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3-fips.dualstack.{Region}.{partitionResult#dnsSuffix}/{uri_encoded_bucket}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3-fips.us-east-1.{partitionResult#dnsSuffix}/{uri_encoded_bucket}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "us-east-1" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3-fips.{Region}.{partitionResult#dnsSuffix}/{uri_encoded_bucket}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3.dualstack.us-east-1.{partitionResult#dnsSuffix}/{uri_encoded_bucket}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "us-east-1" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3.dualstack.{Region}.{partitionResult#dnsSuffix}/{uri_encoded_bucket}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "{url#scheme}://{url#authority}{url#normalizedPath}{uri_encoded_bucket}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "us-east-1" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "{url#scheme}://{url#authority}{url#normalizedPath}{uri_encoded_bucket}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3.{partitionResult#dnsSuffix}/{uri_encoded_bucket}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "us-east-1" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3.{partitionResult#dnsSuffix}/{uri_encoded_bucket}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3.{Region}.{partitionResult#dnsSuffix}/{uri_encoded_bucket}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "error": "Path-style addressing cannot be used with S3 Accelerate", + "type": "error" + }, + { + "conditions": [], + "endpoint": { + "url": "{url#scheme}://{url#authority}{url#path}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3-object-lambda", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3-object-lambda-fips.{Region}.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3-object-lambda", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3-object-lambda.{Region}.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3-object-lambda", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3-fips.dualstack.us-east-1.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "us-east-1" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3-fips.dualstack.{Region}.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3-fips.us-east-1.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "us-east-1" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3-fips.{Region}.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3.dualstack.us-east-1.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "us-east-1" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3.dualstack.{Region}.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "{url#scheme}://{url#authority}{url#path}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "us-east-1" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "{url#scheme}://{url#authority}{url#path}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "us-east-1" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "endpoint": { + "url": "https://s3.{Region}.{partitionResult#dnsSuffix}", + "properties": { + "authSchemes": [ + { + "disableDoubleEncoding": true, + "name": "sigv4", + "signingName": "s3", + "signingRegion": "{Region}" + } + ] + }, + "headers": {} + }, + "type": "endpoint" + }, + { + "conditions": [], + "error": "A region must be set when sending requests to S3.", + "type": "error" + } + ], + "root": 2, + "nodeCount": 553, + "nodes": "/////wAAAAH/////AAAAAAAAAAMF9eFzAAAAAQAAAagAAAAEAAAAAgAAARAAAAAFAAAAAwAAAOkAAAAGAAAABAAAAFUAAAAHAAAABQAAAA8AAAAIAAAACAAAAAkF9eFzAAAAEAAAAAoAAAANAAAAEgAAAAsAAAANAAAAEwAAAAwAAAANAAAAFgX14Q4AAAANAAAAIwAAAA4F9eEqAAAAJAX14WcAAAGzAAAABgAAAQ8AAAAQAAAABwAAAQ4AAAARAAAACAAAABMAAAASAAAADgAAAfUAAABqAAAACQAAABQAAAAYAAAACgAAABUAAAAYAAAACwAAABYAAAAYAAAADAAAABcAAAAYAAAADQAAAiMAAAAYAAAADgAAAE0AAAAZAAAAFAAAAEkAAAAaAAAAGgAAABsAAABOAAAAJQAAABwF9eFWAAAAJgX14VYAAAAdAAAAJwAAAC8AAAAeAAAAMAX14ToAAAAfAAAAMgAAACAF9eFVAAAAMwAAACEAAACIAAAANwX14UwAAAAiAAAAOwAAACMF9eFUAAAAPAAAACcAAAAkAAAAPQAAACUF9eFTAAAAPgAAACYAAACSAAAAPwAAACkF9eEuAAAAPQAAACgF9eFTAAAAPgAAACkAAACWAAAAQAAAACoF9eE2AAAAQgAAACsF9eE1AAAARgAAACwF9eE0AAAARwAAAC0F9eFRAAAASQAAAC4F9eFQAAAASgX14U4F9eFPAAAAKAAAADAF9eE5AAAAKQX14TkAAAAxAAAAKgAAALkAAAAyAAAAMAAAAD4AAAAzAAAAMQX14S0AAAA0AAAAMwAAADUAAAIOAAAAPAAAADgAAAA2AAAAPgX14TcAAAA3AAAAPwAAADkF9eEuAAAAPgX14TcAAAA5AAAAQAAAADoF9eE2AAAAQgAAADsF9eE1AAAARQAAADwF9eFBAAAARgAAAD0F9eE0AAAASAX14UAF9eEzAAAAMQX14S0AAAA/AAAAMwAAAEAAAAIOAAAAPAAAAEMAAABBAAAAPgX14TcAAABCAAAAPwAAAEQF9eEuAAAAPgX14TcAAABEAAAAQAAAAEUF9eE2AAAAQgAAAEYF9eE1AAAARAX14S8AAABHAAAARgAAAEgF9eE0AAAASAX14TIF9eEzAAAAGQAAAEoF9eEqAAAALgX14ScAAABLAAAAOQAAAEwF9eEpAAAAOgX14SgF9eEpAAAAGgX14VgAAABOAAAAHAX14VcAAABPAAAAIgAAAFIAAABQAAAAIwAAAFEAAAIhAAAAJAX14WcF9eFzAAAALgX14WEAAABTAAAAOQAAAFQF9eFjAAAAOgX14WIF9eFjAAAABQAAAGUAAABWAAAACAAAAFcF9eFzAAAAEAAAAFgAAABZAAAAEgAAAFsAAABZAAAAEwAAAFoAAABcAAAAFQAAAGEAAABfAAAAEwAAAF0AAABcAAAAFQAAAGIAAABfAAAAFQAAAGEAAABeAAAAFgX14Q4AAABfAAAAIwAAAGAF9eEqAAAAJAX14WcF9eEqAAAAFgX14Q0AAABiAAAAIwAAAGMF9eEqAAAAJAX14WUAAABkAAAALgX14W4F9eFvAAAABgAAANYAAABmAAAABwAAANAAAABnAAAACAAAAHcAAABoAAAADgAAAHYAAABpAAAAFQAAAGoF9eEXAAAAGgAAAGsAAAH2AAAAJQAAAGwF9eFWAAAAJgX14VYAAABtAAAAJwAAAHAAAABuAAAAMAX14ToAAABvAAAAMgAAAIgF9eFVAAAAKAAAAHEF9eE5AAAAKQX14TkAAAByAAAAKgAAAHMAAAH0AAAAMAX14TgAAAB0AAAANAAAAHUF9eFIAAAAQQX14UUF9eFIAAAAFQAAAfUF9eEXAAAACQAAAHgAAAB8AAAACgAAAHkAAAB8AAAACwAAAHoAAAB8AAAADAAAAHsAAAB8AAAADQAAAMoAAAB8AAAADgAAAMMAAAB9AAAAFAAAAL4AAAB+AAAAFQAAAH8F9eEXAAAAFwAAAIAAAACBAAAAGAAAAL0AAACBAAAAGgAAAIIAAADFAAAAJQAAAIMF9eFWAAAAJgX14VYAAACEAAAAJwAAAJ8AAACFAAAAMAX14ToAAACGAAAAMgAAAIcF9eFVAAAAMwAAAI0AAACIAAAANwX14UwAAACJAAAAOwAAAIoF9eFUAAAAPAX14VMAAACLAAAAPQAAAIwF9eFTAAAAPwX14VMF9eEuAAAANwX14UwAAACOAAAAOwAAAI8F9eFUAAAAPAAAAJQAAACQAAAAPQAAAJEF9eFTAAAAPgAAAJMAAACSAAAAPwAAAJYF9eEuAAAAPwAAAJkF9eEuAAAAPQAAAJUF9eFTAAAAPgAAAJkAAACWAAAAQAAAAJcF9eE2AAAAQgAAAJgF9eE1AAAARgX14VIF9eE0AAAAQAAAAJoF9eE2AAAAQgAAAJsF9eE1AAAARgAAAJwF9eE0AAAARwAAAJ0F9eFRAAAASQAAAJ4F9eFQAAAASgX14U0F9eFPAAAAKAAAAKAF9eE5AAAAKQX14TkAAAChAAAAKgAAALkAAACiAAAAMAAAAK4AAACjAAAAMQX14S0AAACkAAAAMwAAAKUAAAIOAAAAPAAAAKgAAACmAAAAPgX14TcAAACnAAAAPwAAAKkF9eEuAAAAPgX14TcAAACpAAAAQAAAAKoF9eE2AAAAQgAAAKsF9eE1AAAARQAAAKwF9eFBAAAARgAAAK0F9eE0AAAASAX14T8F9eEzAAAAMQX14S0AAACvAAAAMwAAALAAAAIOAAAAPAAAALMAAACxAAAAPgX14TcAAACyAAAAPwAAALQF9eEuAAAAPgX14TcAAAC0AAAAQAAAALUF9eE2AAAAQgAAALYF9eE1AAAARAX14S8AAAC3AAAARgAAALgF9eE0AAAASAX14TAF9eEzAAAAMAX14TgAAAC6AAAANAAAALsF9eFIAAAAQQX14UUAAAC8AAAAQwX14UYF9eFHAAAAGQX14SQF9eEqAAAAFQAAAL8F9eEXAAAAGQAAAMAF9eEqAAAAHgAAAMIAAADBAAAALgX14SIF9eEkAAAALgX14SEF9eEjAAAAFQAAAMQF9eEXAAAAGgX14VgAAADFAAAAHAX14VcAAADGAAAAIgAAAMkAAADHAAAAIwAAAMgAAAIhAAAAJAX14WUF9eFzAAAALgX14V8F9eFgAAAAEQAAAMsF9eEWAAAAFAAAAMwF9eEVAAAAFQAAAM0AAAImAAAAIQAAAM4AAAImAAAALAX14RAAAADPAAAALQX14RIF9eEUAAAACAAAANEAAADXAAAAEAAAANIAAADcAAAAEgAAANMAAADcAAAAEwAAANQAAADgAAAAFAAAANUAAADjAAAAFQAAAOcAAAGRAAAACAAAANoAAADXAAAAEwAAANgF9eEJAAAAFAAAANkAAADjAAAAFQAAAOcF9eEJAAAAEAAAANsAAADcAAAAEgAAAN8AAADcAAAAEwAAAN0AAADgAAAAFAAAAN4AAADjAAAAFQAAAOcF9eEMAAAAEwAAAOIAAADgAAAAFAAAAOEF9eEJAAAAFQX14QkF9eEMAAAAFAAAAOYAAADjAAAAFQAAAOQF9eEJAAAAHgAAAOUF9eEJAAAAIgX14QcF9eEJAAAAFQAAAOcAAAGfAAAAHgAAAOgF9eEIAAAAIgX14QcF9eEIAAAABAX14QIAAADqAAAABQAAAOsAAAHgAAAABgAAAQ8AAADsAAAABwAAAQ4AAADtAAAACAAAAO4AAAHrAAAACQAAAO8AAADzAAAACgAAAPAAAADzAAAACwAAAPEAAADzAAAADAAAAPIAAADzAAAADQAAAiMAAADzAAAADgAAAQoAAAD0AAAAFAAAAQgAAAD1AAAAGgAAAPYAAAELAAAAJQAAAPcF9eFWAAAAJgX14VYAAAD4AAAAJwAAAPkAAAIGAAAAKAAAAPoF9eE5AAAAKQX14TkAAAD7AAAAKgAAAhoAAAD8AAAAMAX14SsAAAD9AAAAMQX14S0AAAD+AAAAMwAAAP8AAAIOAAAAPAAAAQIAAAEAAAAAPgX14TcAAAEBAAAAPwAAAQMF9eEuAAAAPgX14TcAAAEDAAAAQAAAAQQF9eE2AAAAQgAAAQUF9eE1AAAARQAAAQYF9eFBAAAARgAAAQcF9eE0AAAASAX14T4F9eEzAAAAGQAAAQkF9eEqAAAALgX14R8F9eEgAAAAGgX14VgAAAELAAAAHAX14VcAAAEMAAAAIgAAAQ0AAAIgAAAALgX14V0F9eFeAAAACAAAAY0F9eEJAAAACAAAAZcF9eEJAAAAAwAAAVoAAAERAAAABAX14QMAAAESAAAABQAAARwAAAETAAAACAAAARQF9eFzAAAADwX14QUAAAEVAAAAEAAAARYAAAEZAAAAEgAAARcAAAEZAAAAEwAAARgAAAEZAAAAFgX14Q4AAAEZAAAAIwAAARoF9eEqAAAAJAX14WYAAAEbAAAALgX14WoF9eFrAAAABgAAAZUAAAEdAAAABwAAAYsAAAEeAAAACAAAAScAAAEfAAAADgAAAfUAAAEgAAAAGgAAASEAAAH2AAAAJQAAASIF9eFWAAAAJgX14VYAAAEjAAAAJwAAASQAAAEzAAAAKAAAASUF9eE5AAAAKQX14TkAAAEmAAAAKgAAAU8AAAH0AAAACQAAASgAAAEsAAAACgAAASkAAAEsAAAACwAAASoAAAEsAAAADAAAASsAAAEsAAAADQAAAYoAAAEsAAAADgAAAVMAAAEtAAAADwX14QUAAAEuAAAAFAAAAVEAAAEvAAAAGgAAATAAAAFVAAAAJQAAATEF9eFWAAAAJgX14VYAAAEyAAAAJwAAATUAAAEzAAAAMAX14ToAAAE0AAAAMgX14UoF9eFVAAAAKAAAATYF9eE5AAAAKQX14TkAAAE3AAAAKgAAAU8AAAE4AAAAMAAAAUQAAAE5AAAAMQX14S0AAAE6AAAAMwAAATsAAAIOAAAAPAAAAT4AAAE8AAAAPgX14TcAAAE9AAAAPwAAAT8F9eEuAAAAPgX14TcAAAE/AAAAQAAAAUAF9eE2AAAAQgAAAUEF9eE1AAAARQAAAUIF9eFBAAAARgAAAUMF9eE0AAAASAX14T0F9eEzAAAAMQX14S0AAAFFAAAAMwAAAUYAAAIOAAAAPAAAAUkAAAFHAAAAPgX14TcAAAFIAAAAPwAAAUoF9eEuAAAAPgX14TcAAAFKAAAAQAAAAUsF9eE2AAAAQgAAAUwF9eE1AAAARAX14S8AAAFNAAAARgAAAU4F9eE0AAAASAX14TEF9eEzAAAAMAX14TgAAAFQAAAANAX14UMF9eFIAAAAGQAAAVIF9eEqAAAALgX14RsF9eEcAAAADwX14QUAAAFUAAAAGgX14VgAAAFVAAAAHAX14VcAAAFWAAAAIgAAAVkAAAFXAAAAIwAAAVgAAAIhAAAAJAX14WYF9eFzAAAALgX14VsF9eFcAAAABAX14QIAAAFbAAAABQAAAWUAAAFcAAAACAAAAV0F9eFzAAAADwX14QUAAAFeAAAAEAAAAV8AAAFiAAAAEgAAAWAAAAFiAAAAEwAAAWEAAAFiAAAAFgX14Q4AAAFiAAAAIwAAAWMF9eEqAAAAJAX14SsAAAFkAAAALgX14WgF9eFpAAAABgAAAZUAAAFmAAAABwAAAYsAAAFnAAAACAAAAWgAAAHrAAAACQAAAWkAAAFtAAAACgAAAWoAAAFtAAAACwAAAWsAAAFtAAAADAAAAWwAAAFtAAAADQAAAYoAAAFtAAAADgAAAYUAAAFuAAAADwX14QUAAAFvAAAAFAAAAYMAAAFwAAAAGgAAAXEAAAGHAAAAJQAAAXIF9eFWAAAAJgX14VYAAAFzAAAAJwAAAXQAAAIGAAAAKAAAAXUF9eE5AAAAKQX14TkAAAF2AAAAKgAAAhoAAAF3AAAAMAX14SsAAAF4AAAAMQX14S0AAAF5AAAAMwAAAXoAAAIOAAAAPAAAAX0AAAF7AAAAPgX14TcAAAF8AAAAPwAAAX4F9eEuAAAAPgX14TcAAAF+AAAAQAAAAX8F9eE2AAAAQgAAAYAF9eE1AAAARQAAAYEF9eFBAAAARgAAAYIF9eE0AAAASAX14TwF9eEzAAAAGQAAAYQF9eEqAAAALgX14RkF9eEaAAAADwX14QUAAAGGAAAAGgX14VgAAAGHAAAAHAX14VcAAAGIAAAAIgAAAYkAAAIgAAAALgX14VkF9eFaAAAADwX14QUAAAIjAAAACAAAAYwF9eEJAAAADwX14QUAAAGNAAAAEAAAAY4AAAGaAAAAEgAAAY8AAAGaAAAAEwAAAZAAAAGaAAAAFAAAAZEF9eEJAAAAGwAAAZIF9eEMAAAAHQX14QsAAAGTAAAAHwX14QsAAAGUAAAAIAX14QsAAAGmAAAACAAAAZYF9eEJAAAADwX14QUAAAGXAAAAEAAAAZgAAAGaAAAAEgAAAZkAAAGaAAAAEwAAAZsAAAGaAAAAFAX14QwF9eEJAAAAFAAAAZ4AAAGcAAAAFgAAAZ0F9eEJAAAAIgX14QoF9eEJAAAAFgAAAaAAAAGfAAAAGwAAAaMF9eEMAAAAGwAAAaIAAAGhAAAAIgX14QoF9eEMAAAAIgX14QoAAAGjAAAAKwX14QsAAAGkAAAALwX14QsAAAGlAAAANQX14QsAAAGmAAAANgX14QsAAAGnAAAAOAX14QsF9eEMAAAAAgX14QEAAAGpAAAAAwAAAd4AAAGqAAAABAX14QQAAAGrAAAABQAAAbYAAAGsAAAACAAAAa0F9eFzAAAAEAAAAa4AAAGxAAAAEgAAAa8AAAGxAAAAEwAAAbAAAAGxAAAAFgX14Q4AAAGxAAAAIwAAAbIF9eEqAAAAJAX14SwAAAGzAAAALgX14XAAAAG0AAAAOQAAAbUF9eFyAAAAOgX14XEF9eFyAAAABgX14QYAAAG3AAAABwX14QYAAAG4AAAACAAAAcIAAAG5AAAADgAAAfUAAAG6AAAAGgAAAbsAAAH2AAAAJQAAAbwF9eFWAAAAJgX14VYAAAG9AAAAJwAAAb4AAAHRAAAAKAAAAb8F9eE5AAAAKQX14TkAAAHAAAAAKgAAAdcAAAHBAAAAMAX14SwAAAH0AAAACQAAAcMAAAHHAAAACgAAAcQAAAHHAAAACwAAAcUAAAHHAAAADAAAAcYAAAHHAAAADQAAAiMAAAHHAAAADgAAAdkAAAHIAAAADwAAAcwAAAHJAAAAFAAAAcoAAAHNAAAAGQAAAcsF9eEqAAAALgX14SUF9eEmAAAAFAAAAhwAAAHNAAAAGgAAAc4AAAHaAAAAJQAAAc8F9eFWAAAAJgX14VYAAAHQAAAAJwAAAdMAAAHRAAAAMAX14ToAAAHSAAAAMgX14UsF9eFVAAAAKAAAAdQF9eE5AAAAKQX14TkAAAHVAAAAKgAAAdcAAAHWAAAAMAX14SwAAAIMAAAAMAX14SwAAAHYAAAANAX14UQF9eFIAAAAGgX14VgAAAHaAAAAHAX14VcAAAHbAAAAIgX14WQAAAHcAAAAIwAAAd0AAAIhAAAAJAX14SwF9eFzAAAABAX14QIAAAHfAAAABQAAAegAAAHgAAAACAAAAeEF9eFzAAAAEAAAAeIAAAHlAAAAEgAAAeMAAAHlAAAAEwAAAeQAAAHlAAAAFgX14Q4AAAHlAAAAIwAAAeYF9eEqAAAAJAX14SsAAAHnAAAALgX14WwF9eFtAAAABgX14QYAAAHpAAAABwX14QYAAAHqAAAACAAAAfcAAAHrAAAADgAAAfUAAAHsAAAAGgAAAe0AAAH2AAAAJQAAAe4F9eFWAAAAJgX14VYAAAHvAAAAJwAAAfAAAAIGAAAAKAAAAfEF9eE5AAAAKQX14TkAAAHyAAAAKgAAAhoAAAHzAAAAMAX14SsAAAH0AAAAMQX14S0AAAIOAAAAGgX14VgAAAH2AAAAHAX14VcF9eFzAAAACQAAAfgAAAH8AAAACgAAAfkAAAH8AAAACwAAAfoAAAH8AAAADAAAAfsAAAH8AAAADQAAAiMAAAH8AAAADgAAAh0AAAH9AAAADwAAAgEAAAH+AAAAFAAAAf8AAAICAAAAGQAAAgAF9eEqAAAALgX14R0F9eEeAAAAFAAAAhwAAAICAAAAGgAAAgMAAAIeAAAAJQAAAgQF9eFWAAAAJgX14VYAAAIFAAAAJwAAAggAAAIGAAAAMAX14ToAAAIHAAAAMgX14UkF9eFVAAAAKAAAAgkF9eE5AAAAKQX14TkAAAIKAAAAKgAAAhoAAAILAAAAMAX14SsAAAIMAAAAMQX14S0AAAINAAAAMwAAAhEAAAIOAAAAPAX14TcAAAIPAAAAPgX14TcAAAIQAAAAPwX14TcF9eEuAAAAPAAAAhQAAAISAAAAPgX14TcAAAITAAAAPwAAAhUF9eEuAAAAPgX14TcAAAIVAAAAQAAAAhYF9eE2AAAAQgAAAhcF9eE1AAAARQAAAhgF9eFBAAAARgAAAhkF9eE0AAAASAX14TsF9eEzAAAAMAX14SsAAAIbAAAANAX14UIF9eFIAAAAGQX14RgF9eEqAAAAGgX14VgAAAIeAAAAHAX14VcAAAIfAAAAIgX14WQAAAIgAAAAIwAAAiIAAAIhAAAAJAX14SoF9eFzAAAAJAX14SsF9eFzAAAAEQAAAiQF9eEWAAAAFAAAAiUF9eEVAAAAIQAAAigAAAImAAAALAX14REAAAInAAAALQX14RMF9eEUAAAALAX14Q8AAAIpAAAALQX14Q8F9eEU" +} \ No newline at end of file From cf2ad62489ecf000f28e93663eef15e1405f4337 Mon Sep 17 00:00:00 2001 From: Alex Woods Date: Tue, 1 Sep 2026 15:51:31 -0700 Subject: [PATCH 7/7] feat(s3): Add DynamicEndpointAuthSchemeFactory for BDD endpoints BddResultCodeGeneratorVisitor emits a reference to software.amazon.awssdk.services.s3.endpoints.authscheme.DynamicEndpointAuth SchemeFactory whenever an endpoint result carries an auth scheme whose name is resolved at runtime, but the class itself was never carried into the BDD PR chain. Port it in from cd0c5a346a7. Endpoint rulesets normally declare the auth scheme name as a string literal, so codegen can emit a direct call to the matching concrete builder. S3 is the exception: its BDD ruleset merges two otherwise identical results whose auth scheme names differ, lifting the difference into a runtime conditional, so the concrete type cannot be selected at codegen time. This factory collects the shared properties up front and defers type selection to create(name). The gap was invisible on this branch because it ships no service BDD models, so nothing exercised the path. It surfaces as an S3 compile failure as soon as a service BDD model is added. Ported faithfully; the only additions are the @NotThreadSafe annotation and documentation of the null-property and null-name behaviour. Testing: adds DynamicEndpointAuthSchemeFactoryTest covering both scheme names, property pass-through, fluent and order-independent setters, unset and explicitly null properties, reuse across calls, and fail-fast on unsupported and null names. Verified the tests are non-vacuous by mutating the class twice (swapping the two scheme names, and returning a default instead of throwing); each mutation failed the suite. Full mvn install -pl :s3 passes, 1804 tests, including checkstyle and spotbugs. --- .../DynamicEndpointAuthSchemeFactory.java | 106 ++++++++++ .../DynamicEndpointAuthSchemeFactoryTest.java | 185 ++++++++++++++++++ 2 files changed, 291 insertions(+) create mode 100644 services/s3/src/main/java/software/amazon/awssdk/services/s3/endpoints/authscheme/DynamicEndpointAuthSchemeFactory.java create mode 100644 services/s3/src/test/java/software/amazon/awssdk/services/s3/endpoints/authscheme/DynamicEndpointAuthSchemeFactoryTest.java diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/endpoints/authscheme/DynamicEndpointAuthSchemeFactory.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/endpoints/authscheme/DynamicEndpointAuthSchemeFactory.java new file mode 100644 index 000000000000..dd822aaccc5f --- /dev/null +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/endpoints/authscheme/DynamicEndpointAuthSchemeFactory.java @@ -0,0 +1,106 @@ +/* + * 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.s3.endpoints.authscheme; + +import software.amazon.awssdk.annotations.NotThreadSafe; +import software.amazon.awssdk.annotations.SdkProtectedApi; +import software.amazon.awssdk.awscore.endpoints.authscheme.EndpointAuthScheme; +import software.amazon.awssdk.awscore.endpoints.authscheme.SigV4AuthScheme; +import software.amazon.awssdk.core.exception.SdkClientException; + +/** + * Builds an {@link EndpointAuthScheme} whose scheme name is only known at runtime. + * + *

Endpoint rulesets normally declare the auth scheme name as a string literal, which lets codegen emit a direct call to the + * matching concrete builder (for example {@code SigV4AuthScheme.builder()}). S3 is the one exception: its BDD-based ruleset + * merges two otherwise identical results whose auth scheme names differ, lifting the difference into a runtime conditional: + * + * {@snippet : + * _s3e_auth = ite(coalesce(DisableS3ExpressSessionAuth, false), "sigv4", "sigv4-s3express"); + * } + * + *

The merged result then refers to the name indirectly, so the concrete type cannot be selected at codegen time. This factory + * collects the auth scheme properties up front and defers the type selection to {@link #create(String)}. + * + *

Only {@code sigv4} and {@code sigv4-s3express} are supported, since those are the only names the S3 ruleset can produce + * here. The two schemes share the same property set ({@code signingName}, {@code signingRegion}, + * {@code disableDoubleEncoding}), so the properties are modelled directly rather than generically. Any other name is a + * programming error and fails fast. + * + *

Instances are mutable and are not safe for concurrent use. Generated endpoint providers create a fresh instance per + * resolution, so each instance is confined to a single resolution. + */ +@SdkProtectedApi +@NotThreadSafe +public final class DynamicEndpointAuthSchemeFactory { + private static final String SIGV4_NAME = "sigv4"; + private static final String S3EXPRESS_NAME = "sigv4-s3express"; + + private String signingName; + private String signingRegion; + private Boolean disableDoubleEncoding; + + private DynamicEndpointAuthSchemeFactory() { + } + + public static DynamicEndpointAuthSchemeFactory builder() { + return new DynamicEndpointAuthSchemeFactory(); + } + + public DynamicEndpointAuthSchemeFactory signingName(String signingName) { + this.signingName = signingName; + return this; + } + + public DynamicEndpointAuthSchemeFactory signingRegion(String signingRegion) { + this.signingRegion = signingRegion; + return this; + } + + public DynamicEndpointAuthSchemeFactory disableDoubleEncoding(Boolean disableDoubleEncoding) { + this.disableDoubleEncoding = disableDoubleEncoding; + return this; + } + + /** + * Create the endpoint auth scheme matching {@code name}, applying the properties collected on this factory. + * + *

Unset properties are passed through as {@code null}, which the concrete auth schemes treat as "not set" in the same way + * they do when codegen emits a direct builder call. + * + * @param name the auth scheme name; must be {@code sigv4} or {@code sigv4-s3express} + * @return the constructed endpoint auth scheme + * @throws SdkClientException if {@code name} is not a supported auth scheme name, including when it is {@code null} + */ + public EndpointAuthScheme create(String name) { + if (SIGV4_NAME.equals(name)) { + return SigV4AuthScheme.builder() + .signingName(signingName) + .signingRegion(signingRegion) + .disableDoubleEncoding(disableDoubleEncoding) + .build(); + } + if (S3EXPRESS_NAME.equals(name)) { + return S3ExpressEndpointAuthScheme.builder() + .signingName(signingName) + .signingRegion(signingRegion) + .disableDoubleEncoding(disableDoubleEncoding) + .build(); + } + throw SdkClientException.create("Unsupported dynamic endpoint auth scheme name: '" + name + "'. Expected '" + + SIGV4_NAME + "' or '" + S3EXPRESS_NAME + "'."); + } +} diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/endpoints/authscheme/DynamicEndpointAuthSchemeFactoryTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/endpoints/authscheme/DynamicEndpointAuthSchemeFactoryTest.java new file mode 100644 index 000000000000..6c4dcdd1390e --- /dev/null +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/endpoints/authscheme/DynamicEndpointAuthSchemeFactoryTest.java @@ -0,0 +1,185 @@ +/* + * 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.s3.endpoints.authscheme; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import software.amazon.awssdk.awscore.endpoints.authscheme.EndpointAuthScheme; +import software.amazon.awssdk.awscore.endpoints.authscheme.SigV4AuthScheme; +import software.amazon.awssdk.core.exception.SdkClientException; + +/** + * Tests for {@link DynamicEndpointAuthSchemeFactory}. + * + *

The generated S3 endpoint provider calls this factory as + * {@code DynamicEndpointAuthSchemeFactory.builder().disableDoubleEncoding(..).signingName(..).signingRegion(..).create(name)}, + * where {@code name} is resolved at runtime to either {@code sigv4} or {@code sigv4-s3express}. These tests pin that contract. + */ +class DynamicEndpointAuthSchemeFactoryTest { + + @Test + void create_sigv4_returnsSigV4AuthSchemeWithPropertiesApplied() { + EndpointAuthScheme scheme = DynamicEndpointAuthSchemeFactory.builder() + .disableDoubleEncoding(true) + .signingName("s3") + .signingRegion("us-west-2") + .create("sigv4"); + + assertThat(scheme).isInstanceOf(SigV4AuthScheme.class); + SigV4AuthScheme sigv4 = (SigV4AuthScheme) scheme; + assertThat(sigv4.name()).isEqualTo("sigv4"); + assertThat(sigv4.schemeId()).isEqualTo("aws.auth#sigv4"); + assertThat(sigv4.signingName()).isEqualTo("s3"); + assertThat(sigv4.signingRegion()).isEqualTo("us-west-2"); + assertThat(sigv4.disableDoubleEncoding()).isTrue(); + assertThat(sigv4.isDisableDoubleEncodingSet()).isTrue(); + } + + @Test + void create_s3Express_returnsS3ExpressAuthSchemeWithPropertiesApplied() { + EndpointAuthScheme scheme = DynamicEndpointAuthSchemeFactory.builder() + .disableDoubleEncoding(true) + .signingName("s3express") + .signingRegion("us-west-2") + .create("sigv4-s3express"); + + assertThat(scheme).isInstanceOf(S3ExpressEndpointAuthScheme.class); + S3ExpressEndpointAuthScheme s3Express = (S3ExpressEndpointAuthScheme) scheme; + assertThat(s3Express.name()).isEqualTo("sigv4-s3express"); + assertThat(s3Express.schemeId()).isEqualTo("aws.auth#sigv4-s3express"); + assertThat(s3Express.signingName()).isEqualTo("s3express"); + assertThat(s3Express.signingRegion()).isEqualTo("us-west-2"); + assertThat(s3Express.disableDoubleEncoding()).isTrue(); + assertThat(s3Express.isDisableDoubleEncodingSet()).isTrue(); + } + + /** + * The scheme name is the only thing that varies at runtime; the collected properties must be applied identically to + * whichever type is selected. This mirrors the merged S3 ruleset result, where both branches carry the same properties. + */ + @Test + void create_sameFactoryProperties_appliedIdenticallyToBothSchemes() { + DynamicEndpointAuthSchemeFactory factory = DynamicEndpointAuthSchemeFactory.builder() + .disableDoubleEncoding(false) + .signingName("s3express") + .signingRegion("eu-central-1"); + + SigV4AuthScheme sigv4 = (SigV4AuthScheme) factory.create("sigv4"); + S3ExpressEndpointAuthScheme s3Express = (S3ExpressEndpointAuthScheme) factory.create("sigv4-s3express"); + + assertThat(sigv4.signingName()).isEqualTo(s3Express.signingName()); + assertThat(sigv4.signingRegion()).isEqualTo(s3Express.signingRegion()); + assertThat(sigv4.disableDoubleEncoding()).isEqualTo(s3Express.disableDoubleEncoding()); + assertThat(sigv4.disableDoubleEncoding()).isFalse(); + } + + /** + * Codegen emits properties in ruleset order, which is not guaranteed to match the declaration order here, so every setter + * must be order independent and return the same instance. + */ + @Test + void setters_calledInAnyOrder_returnSameInstanceAndApplyAllProperties() { + DynamicEndpointAuthSchemeFactory factory = DynamicEndpointAuthSchemeFactory.builder(); + + assertThat(factory.signingRegion("us-east-1")).isSameAs(factory); + assertThat(factory.disableDoubleEncoding(true)).isSameAs(factory); + assertThat(factory.signingName("s3")).isSameAs(factory); + + SigV4AuthScheme scheme = (SigV4AuthScheme) factory.create("sigv4"); + assertThat(scheme.signingName()).isEqualTo("s3"); + assertThat(scheme.signingRegion()).isEqualTo("us-east-1"); + assertThat(scheme.disableDoubleEncoding()).isTrue(); + } + + /** + * Unset properties must behave exactly as they do when codegen emits a direct builder call with those properties omitted, + * that is, {@code null} rather than a defaulted value. + */ + @Test + void create_withNoPropertiesSet_leavesPropertiesUnset() { + SigV4AuthScheme sigv4 = (SigV4AuthScheme) DynamicEndpointAuthSchemeFactory.builder().create("sigv4"); + + assertThat(sigv4.signingName()).isNull(); + assertThat(sigv4.signingRegion()).isNull(); + assertThat(sigv4.isDisableDoubleEncodingSet()).isFalse(); + assertThat(sigv4.disableDoubleEncoding()).isFalse(); + + S3ExpressEndpointAuthScheme s3Express = + (S3ExpressEndpointAuthScheme) DynamicEndpointAuthSchemeFactory.builder().create("sigv4-s3express"); + + assertThat(s3Express.signingName()).isNull(); + assertThat(s3Express.signingRegion()).isNull(); + assertThat(s3Express.isDisableDoubleEncodingSet()).isFalse(); + assertThat(s3Express.disableDoubleEncoding()).isFalse(); + } + + @Test + void create_explicitNullDisableDoubleEncoding_leavesPropertyUnset() { + SigV4AuthScheme sigv4 = (SigV4AuthScheme) DynamicEndpointAuthSchemeFactory.builder() + .disableDoubleEncoding(null) + .create("sigv4"); + + assertThat(sigv4.isDisableDoubleEncodingSet()).isFalse(); + assertThat(sigv4.disableDoubleEncoding()).isFalse(); + } + + /** + * Anything other than the two names the S3 ruleset can produce is a programming error and must fail fast with a message + * naming the offending value, rather than silently resolving to the wrong signer. + */ + @ParameterizedTest + @ValueSource(strings = {"sigv4a", "sigv4-s3", "SIGV4", "Sigv4-S3Express", "", " ", "bearer"}) + void create_unsupportedName_throwsSdkClientException(String name) { + assertThatThrownBy(() -> DynamicEndpointAuthSchemeFactory.builder().create(name)) + .isInstanceOf(SdkClientException.class) + .hasMessageContaining("Unsupported dynamic endpoint auth scheme name") + .hasMessageContaining(name) + .hasMessageContaining("sigv4") + .hasMessageContaining("sigv4-s3express"); + } + + /** + * A null name must surface the same actionable error as any other unsupported value, not a NullPointerException. + */ + @Test + void create_nullName_throwsSdkClientExceptionNotNpe() { + assertThatThrownBy(() -> DynamicEndpointAuthSchemeFactory.builder().create(null)) + .isInstanceOf(SdkClientException.class) + .hasMessageContaining("Unsupported dynamic endpoint auth scheme name"); + } + + /** + * The factory is reusable: {@code create} must not consume or mutate the collected properties, since a single resolution can + * in principle build more than one scheme. + */ + @Test + void create_calledRepeatedly_returnsEqualButDistinctInstances() { + DynamicEndpointAuthSchemeFactory factory = DynamicEndpointAuthSchemeFactory.builder() + .signingName("s3express") + .signingRegion("us-west-2") + .disableDoubleEncoding(true); + + EndpointAuthScheme first = factory.create("sigv4-s3express"); + EndpointAuthScheme second = factory.create("sigv4-s3express"); + + assertThat(first).isNotSameAs(second); + assertThat(first).isEqualTo(second); + } +}