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/emitters/tasks/EndpointProviderTasks.java b/codegen/src/main/java/software/amazon/awssdk/codegen/emitters/tasks/EndpointProviderTasks.java index b623091220c1..74991d12b16b 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 @@ -34,6 +34,7 @@ import software.amazon.awssdk.codegen.poet.rules.EndpointProviderTestSpec; import software.amazon.awssdk.codegen.poet.rules.EndpointResolverUtilsSpec; import software.amazon.awssdk.codegen.poet.rules.EndpointRulesClientTestSpec; +import software.amazon.awssdk.codegen.poet.rules.bdd.BddEndpointProviderSpec; public final class EndpointProviderTasks extends BaseGeneratorTasks { private final GeneratorTaskParams generatorTaskParams; @@ -48,7 +49,11 @@ protected List createTasks() throws Exception { List tasks = new ArrayList<>(); tasks.add(generateInterface()); tasks.add(generateParams()); - tasks.add(generateDefaultProvider2()); + if (generatorTaskParams.getModel().getEndpointBddModel() != null) { + tasks.add(generateDefaultProviderBdd()); + } else { + tasks.add(generateDefaultProvider2()); + } tasks.add(new RulesEngineRuntimeGeneratorTask(generatorTaskParams)); if (shouldGenerateJmesPathRuntime()) { tasks.add(new JmesPathRuntimeGeneratorTask(generatorTaskParams)); @@ -79,6 +84,14 @@ private GeneratorTask generateDefaultProvider2() { return new PoetGeneratorTask(endpointRulesInternalDir(), model.getFileHeader(), new EndpointProviderSpec(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/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/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/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/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/rules/EndpointUrlCodeEmitter.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointUrlCodeEmitter.java index 3b77ecc6fc3e..3fe67f951a2b 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointUrlCodeEmitter.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/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/rules/ExpressionParser.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/ExpressionParser.java index 6c58d2150464..a73aca7fd0a0 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/ExpressionParser.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/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/rules/RuleRuntimeTypeMirror.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/RuleRuntimeTypeMirror.java index 252e78770b40..f4676ce03d3b 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/RuleRuntimeTypeMirror.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/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) @@ -192,10 +213,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/rules/StringConcatExpression.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/StringConcatExpression.java index 72713c0c22f6..811216f600af 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/StringConcatExpression.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/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/rules/Tokenizer.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/Tokenizer.java index 2d43a356223a..0d772717e2db 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/Tokenizer.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/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/java/software/amazon/awssdk/codegen/poet/rules/bdd/AssignTypeInferringVisitor.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/bdd/AssignTypeInferringVisitor.java new file mode 100644 index 000000000000..e16f8dfb5756 --- /dev/null +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/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.rules.bdd; + +import java.util.Map; +import software.amazon.awssdk.codegen.poet.rules.BooleanAndExpression; +import software.amazon.awssdk.codegen.poet.rules.BooleanNotExpression; +import software.amazon.awssdk.codegen.poet.rules.EndpointExpression; +import software.amazon.awssdk.codegen.poet.rules.ErrorExpression; +import software.amazon.awssdk.codegen.poet.rules.FunctionCallExpression; +import software.amazon.awssdk.codegen.poet.rules.HeadersExpression; +import software.amazon.awssdk.codegen.poet.rules.IndexedAccessExpression; +import software.amazon.awssdk.codegen.poet.rules.LetExpression; +import software.amazon.awssdk.codegen.poet.rules.ListExpression; +import software.amazon.awssdk.codegen.poet.rules.LiteralBooleanExpression; +import software.amazon.awssdk.codegen.poet.rules.LiteralIntegerExpression; +import software.amazon.awssdk.codegen.poet.rules.LiteralStringExpression; +import software.amazon.awssdk.codegen.poet.rules.MemberAccessExpression; +import software.amazon.awssdk.codegen.poet.rules.MethodCallExpression; +import software.amazon.awssdk.codegen.poet.rules.PropertiesExpression; +import software.amazon.awssdk.codegen.poet.rules.RuleExpression; +import software.amazon.awssdk.codegen.poet.rules.RuleExpressionVisitor; +import software.amazon.awssdk.codegen.poet.rules.RuleFunctionMirror; +import software.amazon.awssdk.codegen.poet.rules.RuleRuntimeTypeMirror; +import software.amazon.awssdk.codegen.poet.rules.RuleSetExpression; +import software.amazon.awssdk.codegen.poet.rules.RuleType; +import software.amazon.awssdk.codegen.poet.rules.StringConcatExpression; +import software.amazon.awssdk.codegen.poet.rules.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/rules/bdd/BddEndpointProviderSpec.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/bdd/BddEndpointProviderSpec.java new file mode 100644 index 000000000000..b09b18c71484 --- /dev/null +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/bdd/BddEndpointProviderSpec.java @@ -0,0 +1,992 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.codegen.poet.rules.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.Objects; +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.ClientContextParam; +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.rules.ExpressionParser; +import software.amazon.awssdk.codegen.poet.rules.PrepareForCodegenVisitor; +import software.amazon.awssdk.codegen.poet.rules.RuleExpression; +import software.amazon.awssdk.codegen.poet.rules.RuleRuntimeTypeMirror; +import software.amazon.awssdk.codegen.poet.rules.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). The shared + * RulesFunctions helpers it calls are stateless. + */ +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; + + /** + * 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; + private final Map knownEndpointAttributes; + 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; + 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.cacheEntryType = className().nestedClass("CacheEntry"); + this.bddNodes = endpointBddModel.getDecodedNodes(); + this.conditionTypes = analyzeConditions(); + this.paramUsage = BddParameterReferences.analyze(endpointBddModel); + } + + @Override + public TypeSpec poetSpec() { + TypeSpec.Builder builder = PoetUtils.createClassBuilder(className()) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .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); + + // 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 BddPeepholeVisitor()) + .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(); + // 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()); + 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()); + + // 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. + 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(); + // 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 + 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/rules/bdd/BddParameterReferences.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/bdd/BddParameterReferences.java new file mode 100644 index 000000000000..a718f5def3d2 --- /dev/null +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/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.rules.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.rules.ExpressionParser; +import software.amazon.awssdk.codegen.poet.rules.FunctionCallExpression; +import software.amazon.awssdk.codegen.poet.rules.IndexedAccessExpression; +import software.amazon.awssdk.codegen.poet.rules.MemberAccessExpression; +import software.amazon.awssdk.codegen.poet.rules.RuleExpression; +import software.amazon.awssdk.codegen.poet.rules.VariableReferenceExpression; +import software.amazon.awssdk.codegen.poet.rules.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/java/software/amazon/awssdk/codegen/poet/rules/bdd/BddPeepholeVisitor.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/bdd/BddPeepholeVisitor.java new file mode 100644 index 000000000000..a7c33b5e6321 --- /dev/null +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/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.rules.bdd; + +import java.util.List; +import software.amazon.awssdk.codegen.poet.rules.FunctionCallExpression; +import software.amazon.awssdk.codegen.poet.rules.LiteralBooleanExpression; +import software.amazon.awssdk.codegen.poet.rules.LiteralIntegerExpression; +import software.amazon.awssdk.codegen.poet.rules.LiteralStringExpression; +import software.amazon.awssdk.codegen.poet.rules.RewriteRuleExpressionVisitor; +import software.amazon.awssdk.codegen.poet.rules.RuleExpression; +import software.amazon.awssdk.codegen.poet.rules.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.rules.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.rules.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/rules/bdd/BddResultCodeGeneratorVisitor.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/bdd/BddResultCodeGeneratorVisitor.java new file mode 100644 index 000000000000..963fb6c92415 --- /dev/null +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/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.rules.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.BooleanAndExpression; +import software.amazon.awssdk.codegen.poet.rules.BooleanNotExpression; +import software.amazon.awssdk.codegen.poet.rules.EndpointExpression; +import software.amazon.awssdk.codegen.poet.rules.EndpointRulesSpecUtils; +import software.amazon.awssdk.codegen.poet.rules.EndpointUrlCodeEmitter; +import software.amazon.awssdk.codegen.poet.rules.ErrorExpression; +import software.amazon.awssdk.codegen.poet.rules.FunctionCallExpression; +import software.amazon.awssdk.codegen.poet.rules.HeadersExpression; +import software.amazon.awssdk.codegen.poet.rules.IndexedAccessExpression; +import software.amazon.awssdk.codegen.poet.rules.LetExpression; +import software.amazon.awssdk.codegen.poet.rules.ListExpression; +import software.amazon.awssdk.codegen.poet.rules.LiteralBooleanExpression; +import software.amazon.awssdk.codegen.poet.rules.LiteralIntegerExpression; +import software.amazon.awssdk.codegen.poet.rules.LiteralStringExpression; +import software.amazon.awssdk.codegen.poet.rules.MemberAccessExpression; +import software.amazon.awssdk.codegen.poet.rules.MethodCallExpression; +import software.amazon.awssdk.codegen.poet.rules.PropertiesExpression; +import software.amazon.awssdk.codegen.poet.rules.RuleExpression; +import software.amazon.awssdk.codegen.poet.rules.RuleExpressionVisitor; +import software.amazon.awssdk.codegen.poet.rules.RuleFunctionMirror; +import software.amazon.awssdk.codegen.poet.rules.RuleRuntimeTypeMirror; +import software.amazon.awssdk.codegen.poet.rules.RuleSetExpression; +import software.amazon.awssdk.codegen.poet.rules.RuleType; +import software.amazon.awssdk.codegen.poet.rules.StringConcatExpression; +import software.amazon.awssdk.codegen.poet.rules.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/rules/bdd/ConditionFnCodeGeneratorVisitor.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/bdd/ConditionFnCodeGeneratorVisitor.java new file mode 100644 index 000000000000..e5b8f6419b1a --- /dev/null +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/bdd/ConditionFnCodeGeneratorVisitor.java @@ -0,0 +1,399 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.codegen.poet.rules.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.BooleanAndExpression; +import software.amazon.awssdk.codegen.poet.rules.BooleanNotExpression; +import software.amazon.awssdk.codegen.poet.rules.EndpointExpression; +import software.amazon.awssdk.codegen.poet.rules.EndpointRulesSpecUtils; +import software.amazon.awssdk.codegen.poet.rules.ErrorExpression; +import software.amazon.awssdk.codegen.poet.rules.FunctionCallExpression; +import software.amazon.awssdk.codegen.poet.rules.HeadersExpression; +import software.amazon.awssdk.codegen.poet.rules.IndexedAccessExpression; +import software.amazon.awssdk.codegen.poet.rules.LetExpression; +import software.amazon.awssdk.codegen.poet.rules.ListExpression; +import software.amazon.awssdk.codegen.poet.rules.LiteralBooleanExpression; +import software.amazon.awssdk.codegen.poet.rules.LiteralIntegerExpression; +import software.amazon.awssdk.codegen.poet.rules.LiteralStringExpression; +import software.amazon.awssdk.codegen.poet.rules.MemberAccessExpression; +import software.amazon.awssdk.codegen.poet.rules.MethodCallExpression; +import software.amazon.awssdk.codegen.poet.rules.PropertiesExpression; +import software.amazon.awssdk.codegen.poet.rules.RuleExpression; +import software.amazon.awssdk.codegen.poet.rules.RuleExpressionVisitor; +import software.amazon.awssdk.codegen.poet.rules.RuleFunctionMirror; +import software.amazon.awssdk.codegen.poet.rules.RuleRuntimeTypeMirror; +import software.amazon.awssdk.codegen.poet.rules.RuleSetExpression; +import software.amazon.awssdk.codegen.poet.rules.RuleType; +import software.amazon.awssdk.codegen.poet.rules.StringConcatExpression; +import software.amazon.awssdk.codegen.poet.rules.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; + } + + // 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(); + 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(); + } + + /** + * 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); + 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 + // 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) + // 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/rules/bdd/RegistryInfo.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/bdd/RegistryInfo.java new file mode 100644 index 000000000000..8246d1071335 --- /dev/null +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/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.rules.bdd; + +import software.amazon.awssdk.codegen.poet.rules.RuleSetExpression; +import software.amazon.awssdk.codegen.poet.rules.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/main/resources/software/amazon/awssdk/codegen/rules/RulesFunctions.java.resource b/codegen/src/main/resources/software/amazon/awssdk/codegen/rules/RulesFunctions.java.resource index d1944d9ed01e..93a7c28412c6 100644 --- a/codegen/src/main/resources/software/amazon/awssdk/codegen/rules/RulesFunctions.java.resource +++ b/codegen/src/main/resources/software/amazon/awssdk/codegen/rules/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; @@ -17,12 +18,106 @@ 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; + @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]; + } + + /** + * 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); + } + 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; + // 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) { + 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; @@ -33,11 +128,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) { @@ -49,6 +141,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 { @@ -71,31 +209,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}$ @@ -125,34 +239,94 @@ 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 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(); - 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; + } + + // 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; } } // Couldn't find the region by name or pattern matching. Fallback to 'aws' partition. - if (matchedPartition == null) { - matchedPartition = AWS_PARTITION.value(); + // + // 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 RulePartition.from(matchedPartition.id(), matchedPartition.outputs()); + return data.awsRulePartition; } public static RuleArn awsParseArn(String value) { @@ -170,6 +344,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; } @@ -285,25 +465,55 @@ 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; - return partitionData; + 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; + } + } + + // 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 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. 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, + 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/ClientTestModels.java b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/ClientTestModels.java index d0083c3386e1..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 @@ -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; @@ -631,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()); @@ -644,6 +645,90 @@ 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()); + // 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/s3-test/endpoint-rule-set.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 +773,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/rules/TokenizerTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules/TokenizerTest.java index 61a1a620d147..e4ffc8aa2301 100644 --- a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules/TokenizerTest.java +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules/TokenizerTest.java @@ -16,6 +16,7 @@ package software.amazon.awssdk.codegen.poet.rules; 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/rules/bdd/BddEndpointProviderSpecTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules/bdd/BddEndpointProviderSpecTest.java new file mode 100644 index 000000000000..5bf5ca7d6779 --- /dev/null +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules/bdd/BddEndpointProviderSpecTest.java @@ -0,0 +1,332 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.codegen.poet.rules.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 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( + ClientTestModels.queryServiceModelsWithSimpleBddEndpoints()); + 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 s3Bdd_neverEmitsSpecViolatingForms() { + BddEndpointProviderSpec spec = new BddEndpointProviderSpec( + ClientTestModels.queryServiceModelsWithBddEndpoints()); + 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;"); + } + + /** + * 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()"); + } + + /** + * 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. + */ + @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/java/software/amazon/awssdk/codegen/poet/rules/bdd/BddPeepholeVisitorTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules/bdd/BddPeepholeVisitorTest.java new file mode 100644 index 000000000000..9a694c36babc --- /dev/null +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules/bdd/BddPeepholeVisitorTest.java @@ -0,0 +1,163 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.codegen.poet.rules.bdd; + +import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.codegen.poet.rules.FunctionCallExpression; +import software.amazon.awssdk.codegen.poet.rules.LiteralBooleanExpression; +import software.amazon.awssdk.codegen.poet.rules.LiteralIntegerExpression; +import software.amazon.awssdk.codegen.poet.rules.LiteralStringExpression; +import software.amazon.awssdk.codegen.poet.rules.RuleExpression; +import software.amazon.awssdk.codegen.poet.rules.RuleRuntimeTypeMirror; +import software.amazon.awssdk.codegen.poet.rules.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.rules.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/rules/bdd/ConditionFnCodeGeneratorVisitorTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules/bdd/ConditionFnCodeGeneratorVisitorTest.java new file mode 100644 index 000000000000..1d1b0eca0b94 --- /dev/null +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/rules/bdd/ConditionFnCodeGeneratorVisitorTest.java @@ -0,0 +1,163 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.codegen.poet.rules.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.rules.FunctionCallExpression; +import software.amazon.awssdk.codegen.poet.rules.LetExpression; +import software.amazon.awssdk.codegen.poet.rules.LiteralBooleanExpression; +import software.amazon.awssdk.codegen.poet.rules.LiteralStringExpression; +import software.amazon.awssdk.codegen.poet.rules.RuleExpression; +import software.amazon.awssdk.codegen.poet.rules.RuleRuntimeTypeMirror; +import software.amazon.awssdk.codegen.poet.rules.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/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..a05255ae456e --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/query/endpoint-bdd-default-regional.json @@ -0,0 +1,302 @@ +{ + "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" + }, + "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": [ + { + "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" + ] + } + ] + }, + { + "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": [ + { + "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": 22, + "nodeCount": 22, + "nodes": "/////wAAAAH/////AAAABAX14QIF9eEDAAAAAgX14QEAAAACAAAABgX14QQF9eEFAAAABQAAAAQF9eEFAAAABwX14QYF9eEHAAAABQAAAAYF9eEIAAAABAAAAAUAAAAHAAAAAwAAAAgF9eEMAAAABgX14QkF9eEKAAAABAAAAAoF9eELAAAAAwAAAAsF9eEMAAAAAgAAAAkAAAAMAAAAAQAAAA0F9eEMAAAAAAAAAAMAAAAOAAAACAAAABEAAAARAAAACQAAABIAAAASAAAACgAAABMAAAATAAAACwAAABQAAAAUAAAADAAAAA8AAAAPAAAADQAAABAAAAAQAAAADgAAABUAAAAV" +} 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..6c3b34e6e15f --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/query/endpoint-rule-set-default-regional.json @@ -0,0 +1,369 @@ +{ + "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" + }, + "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": [ + { + "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" + } + ] +} diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/bdd/endpoint-provider-bdd-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/bdd/endpoint-provider-bdd-class.java new file mode 100644 index 000000000000..c0566ec44f4d --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/bdd/endpoint-provider-bdd-class.java @@ -0,0 +1,310 @@ +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; +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 { + 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.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(); + 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 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; + + String region; + + RulePartition partitionResult; + + String firstArn; + + String secondEndpoint; + + 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 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; + } + + private boolean cond5() { + return (partitionResult.supportsFIPS()); + } + + private boolean cond6() { + return (partitionResult.supportsDualStack()); + } + + 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"); + } + + 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"); + } + } + + 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/rules/bdd/endpoint-provider-bdd-s3-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/bdd/endpoint-provider-bdd-s3-class.java new file mode 100644 index 000000000000..7b55c6c57497 --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules/bdd/endpoint-provider-bdd-s3-class.java @@ -0,0 +1,4139 @@ +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; +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 { + 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.nodeP1(); + 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(); + 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 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; + + 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."); + } + } + + 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/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/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/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/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"); + } } 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/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/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/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 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); + } +} 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..66e220ed535f --- /dev/null +++ b/test/codegen-generated-classes-test/src/main/resources/codegen-resources/bddendpoints/endpoint-bdd-1.json @@ -0,0 +1,330 @@ +{ + "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 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": [ + { + "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" + ] + }, + { + "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": [ + { + "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": 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 new file mode 100644 index 000000000000..74d707c19c06 --- /dev/null +++ b/test/codegen-generated-classes-test/src/main/resources/codegen-resources/bddendpoints/endpoint-rule-set.json @@ -0,0 +1,381 @@ +{ + "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" + }, + "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": [ + { + "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" + } + ] +} 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..0b1408801fca --- /dev/null +++ b/test/codegen-generated-classes-test/src/main/resources/codegen-resources/bddendpoints/service-2.json @@ -0,0 +1,145 @@ +{ + "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" + ] + }, + "clientContextParams": { + "clientStringParam": { + "documentation": "A client-level string context parameter.", + "type": "string" + } + }, + "operations": { + "TestOperation": { + "name": "TestOperation", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { + "shape": "TestOperationRequest" + }, + "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": { + "TestOperationRequest": { + "type": "structure", + "members": { + "StringMember": { + "shape": "String" + } + } + }, + "TestOperationResponse": { + "type": "structure", + "members": { + "StringMember": { + "shape": "String" + } + } + }, + "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/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(); + } +} 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": {} + } + } + ] +} 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(); + } + } }