diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/CodeGeneratorVisitor.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/CodeGeneratorVisitor.java index 624173bde3a1..34d6a64a3154 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/CodeGeneratorVisitor.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/CodeGeneratorVisitor.java @@ -28,7 +28,6 @@ import software.amazon.awssdk.awscore.endpoints.authscheme.SigV4aAuthScheme; import software.amazon.awssdk.codegen.model.config.customization.KeyTypePair; import software.amazon.awssdk.endpoints.Endpoint; -import software.amazon.awssdk.endpoints.EndpointUrl; public class CodeGeneratorVisitor extends WalkRuleExpressionVisitor { private static final Logger log = LoggerFactory.getLogger(CodeGeneratorVisitor.class); @@ -329,9 +328,9 @@ private String callParams(String ruleId) { @Override public Void visitEndpointExpression(EndpointExpression e) { builder.add("return $T.endpoint(", typeMirror.rulesResult().type()); - builder.add("$T.builder().endpointUrl($T.fromString(", Endpoint.class, EndpointUrl.class); - e.url().accept(this); - builder.add("))"); + builder.add("$T.builder().endpointUrl(", Endpoint.class); + EndpointUrlCodeEmitter.emit(e.url(), builder, this); + builder.add(")"); e.headers().accept(this); e.properties().accept(this); builder.add(".build()"); diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/EndpointUrlCodeEmitter.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/EndpointUrlCodeEmitter.java new file mode 100644 index 000000000000..957c0a72e933 --- /dev/null +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules2/EndpointUrlCodeEmitter.java @@ -0,0 +1,366 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.codegen.poet.rules2; + +import com.squareup.javapoet.CodeBlock; +import java.util.ArrayList; +import java.util.List; +import software.amazon.awssdk.endpoints.EndpointUrl; + +/** + * Emits the optimal {@code EndpointUrl} construction code for a URL expression. + * + *
This class encapsulates both the analysis of the URL expression structure (determining whether + * it can be statically decomposed) and the code emission. When the URL starts with a literal scheme + * prefix ({@code https://} or {@code http://}) and the components (scheme, host, port, path) can be + * identified at codegen time then we eliminate runtime parsing and use {@code EndpointUrl.fromComponents()}. + * Otherwise, falls back to {@code EndpointUrl.fromString()} for runtime parsing. + * + *
The endpoint URL spec guarantees that URLs contain only scheme, host, optional port, and optional + * 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 { + + private static final String HTTPS_SCHEME_PREFIX = "https://"; + private static final String HTTP_SCHEME_PREFIX = "http://"; + + private EndpointUrlCodeEmitter() { + } + + /** + * Emit the optimal EndpointUrl construction code for the given URL expression. + * + *
Writes to {@code builder} either: + *
StringConcatExpressions are created by our codegen pre-parser: {@link ExpressionParser}. + * It splits on "{...}" boundaries, producing alternating literal strings and variable/member-access references. The + * result is a StringConcatExpression whose expressions() list interleaves: + *
Typical service endpoints follow something like: "https://sts.{Region}.{PartitionResult#dnsSuffix}" which
+ * parses to something like:
+ * {@snippet :
+ * StringConcatExpression([
+ * LiteralStringExpression("https://sts."),
+ * VariableReferenceExpression("Region"),
+ * LiteralStringExpression("."),
+ * MemberAccessExpression(source=VarRef("PartitionResult"), name="dnsSuffix")
+ * ])
+ * }
+ *
+ */
+ private static void emitFromStringConcat(StringConcatExpression concatExpr,
+ CodeBlock.Builder builder,
+ CodeGeneratorVisitor codegenVisitor) {
+ List If the list is empty, emits an empty string literal. Otherwise, wraps them in a
+ * {@link StringConcatExpression} and delegates to the code generator's existing concat
+ * emission logic.
+ */
+ private static void emitConcatExpression(List This method mutates {@code hostParts} and {@code pathParts} as a side effect, appending
+ * the relevant portions of the literal to each list. On a {@code NOT_PARSEABLE} result, the
+ * lists may have been partially modified — callers must not reuse the lists after a failure.
+ */
+ private static ScanResult scanLiteral(String literal,
+ List Verifies that URL expressions are correctly decomposed into {@code EndpointUrl.fromComponents()}
+ * calls when possible, and fall back to {@code EndpointUrl.fromString()} otherwise. This is the standard representation when a user provides a custom endpoint URL via
+ * client configuration. The endpoint rule decomposes the user-provided URL into its parts
+ * (scheme, authority, path) and reassembles them with possible modifications (e.g., host prefix).
+ * Since the scheme is a dynamic expression (not a literal "https://" or "http://"), this
+ * cannot be pre-parsed and must fall back to runtime parsing.
+ */
+ @Test
+ void emit_customEndpointOverride_emitsFromString() {
+ StringConcatExpression urlExpr = StringConcatExpression.builder()
+ .addExpression(memberAccess("url", "scheme"))
+ .addExpression(literal("://"))
+ .addExpression(memberAccess("url", "authority"))
+ .addExpression(memberAccess("url", "path"))
+ .build();
+
+ String code = emitUrl(urlExpr);
+
+ assertThat(code).contains("EndpointUrl.fromString(");
+ assertThat(code).contains("url.scheme()");
+ assertThat(code).contains("url.authority()");
+ assertThat(code).contains("url.path()");
+ assertThat(code).doesNotContain("fromComponents");
+ }
+}
diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-class.java
index 2edf6b89329c..d01b466c69f1 100644
--- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-class.java
+++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-class.java
@@ -66,8 +66,9 @@ private static RuleResult endpointRule1(QueryEndpointParams params, String regio
if (parsedArn != null) {
return RuleResult.endpoint(Endpoint
.builder()
- .endpointUrl(EndpointUrl.fromString("https://" + params.endpointId() + ".query."
- + partitionResult.dualStackDnsSuffix()))
+ .endpointUrl(
+ EndpointUrl.fromComponents("https",
+ params.endpointId() + ".query." + partitionResult.dualStackDnsSuffix(), -1, ""))
.putAttribute(
AwsEndpointAttribute.AUTH_SCHEMES,
Arrays.asList(SigV4aAuthScheme.builder().signingName("query")
@@ -87,7 +88,9 @@ private static RuleResult endpointRule2(QueryEndpointParams params, RulePartitio
if (params.useFipsEndpoint() == null && params.useDualStackEndpoint() != null && params.useDualStackEndpoint()) {
return RuleResult.endpoint(Endpoint
.builder()
- .endpointUrl(EndpointUrl.fromString("https://" + params.endpointId() + ".query." + partitionResult.dualStackDnsSuffix()))
+ .endpointUrl(
+ EndpointUrl.fromComponents("https", params.endpointId() + ".query." + partitionResult.dualStackDnsSuffix(),
+ -1, ""))
.putAttribute(
AwsEndpointAttribute.AUTH_SCHEMES,
Arrays.asList(SigV4aAuthScheme.builder().signingName("query")
@@ -95,7 +98,7 @@ private static RuleResult endpointRule2(QueryEndpointParams params, RulePartitio
}
return RuleResult.endpoint(Endpoint
.builder()
- .endpointUrl(EndpointUrl.fromString("https://" + params.endpointId() + ".query." + partitionResult.dnsSuffix()))
+ .endpointUrl(EndpointUrl.fromComponents("https", params.endpointId() + ".query." + partitionResult.dnsSuffix(), -1, ""))
.putAttribute(
AwsEndpointAttribute.AUTH_SCHEMES,
Arrays.asList(SigV4aAuthScheme.builder().signingName("query").signingRegionSet(Arrays.asList("*"))
@@ -109,7 +112,7 @@ private static RuleResult endpointRule6(QueryEndpointParams params, String regio
if (params.useFipsEndpoint() != null && params.useFipsEndpoint() && params.useDualStackEndpoint() == null) {
return RuleResult.endpoint(Endpoint
.builder()
- .endpointUrl(EndpointUrl.fromString("https://query-fips." + region + "." + partitionResult.dnsSuffix()))
+ .endpointUrl(EndpointUrl.fromComponents("https", "query-fips." + region + "." + partitionResult.dnsSuffix(), -1, ""))
.putAttribute(
AwsEndpointAttribute.AUTH_SCHEMES,
Arrays.asList(SigV4aAuthScheme.builder().signingName("query")
@@ -118,7 +121,8 @@ private static RuleResult endpointRule6(QueryEndpointParams params, String regio
if (params.useDualStackEndpoint() != null && params.useDualStackEndpoint() && params.useFipsEndpoint() == null) {
return RuleResult.endpoint(Endpoint
.builder()
- .endpointUrl(EndpointUrl.fromString("https://query." + region + "." + partitionResult.dualStackDnsSuffix()))
+ .endpointUrl(
+ EndpointUrl.fromComponents("https", "query." + region + "." + partitionResult.dualStackDnsSuffix(), -1, ""))
.putAttribute(
AwsEndpointAttribute.AUTH_SCHEMES,
Arrays.asList(SigV4aAuthScheme.builder().signingName("query")
@@ -129,14 +133,16 @@ private static RuleResult endpointRule6(QueryEndpointParams params, String regio
&& params.useFipsEndpoint()) {
return RuleResult.endpoint(Endpoint
.builder()
- .endpointUrl(EndpointUrl.fromString("https://query-fips." + region + "." + partitionResult.dualStackDnsSuffix()))
+ .endpointUrl(
+ EndpointUrl.fromComponents("https", "query-fips." + region + "." + partitionResult.dualStackDnsSuffix(), -1,
+ ""))
.putAttribute(
AwsEndpointAttribute.AUTH_SCHEMES,
Arrays.asList(SigV4aAuthScheme.builder().signingName("query")
.signingRegionSet(Arrays.asList("*")).build())).build());
}
return RuleResult.endpoint(Endpoint.builder()
- .endpointUrl(EndpointUrl.fromString("https://query." + region + "." + partitionResult.dnsSuffix())).build());
+ .endpointUrl(EndpointUrl.fromComponents("https", "query." + region + "." + partitionResult.dnsSuffix(), -1, "")).build());
}
return RuleResult.carryOn();
}
diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-know-prop-override-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-know-prop-override-class.java
index 2edf6b89329c..d01b466c69f1 100644
--- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-know-prop-override-class.java
+++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-know-prop-override-class.java
@@ -66,8 +66,9 @@ private static RuleResult endpointRule1(QueryEndpointParams params, String regio
if (parsedArn != null) {
return RuleResult.endpoint(Endpoint
.builder()
- .endpointUrl(EndpointUrl.fromString("https://" + params.endpointId() + ".query."
- + partitionResult.dualStackDnsSuffix()))
+ .endpointUrl(
+ EndpointUrl.fromComponents("https",
+ params.endpointId() + ".query." + partitionResult.dualStackDnsSuffix(), -1, ""))
.putAttribute(
AwsEndpointAttribute.AUTH_SCHEMES,
Arrays.asList(SigV4aAuthScheme.builder().signingName("query")
@@ -87,7 +88,9 @@ private static RuleResult endpointRule2(QueryEndpointParams params, RulePartitio
if (params.useFipsEndpoint() == null && params.useDualStackEndpoint() != null && params.useDualStackEndpoint()) {
return RuleResult.endpoint(Endpoint
.builder()
- .endpointUrl(EndpointUrl.fromString("https://" + params.endpointId() + ".query." + partitionResult.dualStackDnsSuffix()))
+ .endpointUrl(
+ EndpointUrl.fromComponents("https", params.endpointId() + ".query." + partitionResult.dualStackDnsSuffix(),
+ -1, ""))
.putAttribute(
AwsEndpointAttribute.AUTH_SCHEMES,
Arrays.asList(SigV4aAuthScheme.builder().signingName("query")
@@ -95,7 +98,7 @@ private static RuleResult endpointRule2(QueryEndpointParams params, RulePartitio
}
return RuleResult.endpoint(Endpoint
.builder()
- .endpointUrl(EndpointUrl.fromString("https://" + params.endpointId() + ".query." + partitionResult.dnsSuffix()))
+ .endpointUrl(EndpointUrl.fromComponents("https", params.endpointId() + ".query." + partitionResult.dnsSuffix(), -1, ""))
.putAttribute(
AwsEndpointAttribute.AUTH_SCHEMES,
Arrays.asList(SigV4aAuthScheme.builder().signingName("query").signingRegionSet(Arrays.asList("*"))
@@ -109,7 +112,7 @@ private static RuleResult endpointRule6(QueryEndpointParams params, String regio
if (params.useFipsEndpoint() != null && params.useFipsEndpoint() && params.useDualStackEndpoint() == null) {
return RuleResult.endpoint(Endpoint
.builder()
- .endpointUrl(EndpointUrl.fromString("https://query-fips." + region + "." + partitionResult.dnsSuffix()))
+ .endpointUrl(EndpointUrl.fromComponents("https", "query-fips." + region + "." + partitionResult.dnsSuffix(), -1, ""))
.putAttribute(
AwsEndpointAttribute.AUTH_SCHEMES,
Arrays.asList(SigV4aAuthScheme.builder().signingName("query")
@@ -118,7 +121,8 @@ private static RuleResult endpointRule6(QueryEndpointParams params, String regio
if (params.useDualStackEndpoint() != null && params.useDualStackEndpoint() && params.useFipsEndpoint() == null) {
return RuleResult.endpoint(Endpoint
.builder()
- .endpointUrl(EndpointUrl.fromString("https://query." + region + "." + partitionResult.dualStackDnsSuffix()))
+ .endpointUrl(
+ EndpointUrl.fromComponents("https", "query." + region + "." + partitionResult.dualStackDnsSuffix(), -1, ""))
.putAttribute(
AwsEndpointAttribute.AUTH_SCHEMES,
Arrays.asList(SigV4aAuthScheme.builder().signingName("query")
@@ -129,14 +133,16 @@ private static RuleResult endpointRule6(QueryEndpointParams params, String regio
&& params.useFipsEndpoint()) {
return RuleResult.endpoint(Endpoint
.builder()
- .endpointUrl(EndpointUrl.fromString("https://query-fips." + region + "." + partitionResult.dualStackDnsSuffix()))
+ .endpointUrl(
+ EndpointUrl.fromComponents("https", "query-fips." + region + "." + partitionResult.dualStackDnsSuffix(), -1,
+ ""))
.putAttribute(
AwsEndpointAttribute.AUTH_SCHEMES,
Arrays.asList(SigV4aAuthScheme.builder().signingName("query")
.signingRegionSet(Arrays.asList("*")).build())).build());
}
return RuleResult.endpoint(Endpoint.builder()
- .endpointUrl(EndpointUrl.fromString("https://query." + region + "." + partitionResult.dnsSuffix())).build());
+ .endpointUrl(EndpointUrl.fromComponents("https", "query." + region + "." + partitionResult.dnsSuffix(), -1, "")).build());
}
return RuleResult.carryOn();
}
diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-metric-values-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-metric-values-class.java
index a086d3466d4e..cadeb0b86890 100644
--- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-metric-values-class.java
+++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-metric-values-class.java
@@ -69,20 +69,22 @@ private static RuleResult endpointRule2(QueryEndpointParams params, RulePartitio
}
if (params.useFipsEndpoint() == null && params.useDualStackEndpoint() != null && params.useDualStackEndpoint()) {
return RuleResult.endpoint(Endpoint
- .builder()
- .endpointUrl(EndpointUrl.fromString("https://" + params.endpointId() + ".query." + partitionResult.dualStackDnsSuffix()))
- .putAttribute(
- AwsEndpointAttribute.AUTH_SCHEMES,
- Arrays.asList(SigV4aAuthScheme.builder().signingName("query")
- .signingRegionSet(Arrays.asList("*")).build())).build());
+ .builder()
+ .endpointUrl(
+ EndpointUrl.fromComponents("https", params.endpointId() + ".query." + partitionResult.dualStackDnsSuffix(),
+ -1, ""))
+ .putAttribute(
+ AwsEndpointAttribute.AUTH_SCHEMES,
+ Arrays.asList(SigV4aAuthScheme.builder().signingName("query")
+ .signingRegionSet(Arrays.asList("*")).build())).build());
}
return RuleResult.endpoint(Endpoint
- .builder()
- .endpointUrl(EndpointUrl.fromString("https://" + params.endpointId() + ".query." + partitionResult.dnsSuffix()))
- .putAttribute(
- AwsEndpointAttribute.AUTH_SCHEMES,
- Arrays.asList(SigV4aAuthScheme.builder().signingName("query").signingRegionSet(Arrays.asList("*"))
- .build())).putAttribute(AwsEndpointAttribute.METRIC_VALUES, Arrays.asList("1", "2")).build());
+ .builder()
+ .endpointUrl(EndpointUrl.fromComponents("https", params.endpointId() + ".query." + partitionResult.dnsSuffix(), -1, ""))
+ .putAttribute(
+ AwsEndpointAttribute.AUTH_SCHEMES,
+ Arrays.asList(SigV4aAuthScheme.builder().signingName("query").signingRegionSet(Arrays.asList("*"))
+ .build())).putAttribute(AwsEndpointAttribute.METRIC_VALUES, Arrays.asList("1", "2")).build());
}
return RuleResult.carryOn();
}
@@ -91,35 +93,38 @@ private static RuleResult endpointRule6(QueryEndpointParams params, String regio
if (RulesFunctions.isValidHostLabel(region, false)) {
if (params.useFipsEndpoint() != null && params.useFipsEndpoint() && params.useDualStackEndpoint() == null) {
return RuleResult.endpoint(Endpoint
- .builder()
- .endpointUrl(EndpointUrl.fromString("https://query-fips." + region + "." + partitionResult.dnsSuffix()))
- .putAttribute(
- AwsEndpointAttribute.AUTH_SCHEMES,
- Arrays.asList(SigV4aAuthScheme.builder().signingName("query")
- .signingRegionSet(Arrays.asList("*")).build())).build());
+ .builder()
+ .endpointUrl(EndpointUrl.fromComponents("https", "query-fips." + region + "." + partitionResult.dnsSuffix(), -1, ""))
+ .putAttribute(
+ AwsEndpointAttribute.AUTH_SCHEMES,
+ Arrays.asList(SigV4aAuthScheme.builder().signingName("query")
+ .signingRegionSet(Arrays.asList("*")).build())).build());
}
if (params.useDualStackEndpoint() != null && params.useDualStackEndpoint() && params.useFipsEndpoint() == null) {
return RuleResult.endpoint(Endpoint
- .builder()
- .endpointUrl(EndpointUrl.fromString("https://query." + region + "." + partitionResult.dualStackDnsSuffix()))
- .putAttribute(
- AwsEndpointAttribute.AUTH_SCHEMES,
- Arrays.asList(SigV4aAuthScheme.builder().signingName("query")
- .signingRegionSet(Arrays.asList("*")).build(),
- SigV4AuthScheme.builder().signingName("query").signingRegion(region).build())).build());
+ .builder()
+ .endpointUrl(
+ EndpointUrl.fromComponents("https", "query." + region + "." + partitionResult.dualStackDnsSuffix(), -1, ""))
+ .putAttribute(
+ AwsEndpointAttribute.AUTH_SCHEMES,
+ Arrays.asList(SigV4aAuthScheme.builder().signingName("query")
+ .signingRegionSet(Arrays.asList("*")).build(),
+ SigV4AuthScheme.builder().signingName("query").signingRegion(region).build())).build());
}
if (params.useDualStackEndpoint() != null && params.useFipsEndpoint() != null && params.useDualStackEndpoint()
- && params.useFipsEndpoint()) {
+ && params.useFipsEndpoint()) {
return RuleResult.endpoint(Endpoint
- .builder()
- .endpointUrl(EndpointUrl.fromString("https://query-fips." + region + "." + partitionResult.dualStackDnsSuffix()))
- .putAttribute(
- AwsEndpointAttribute.AUTH_SCHEMES,
- Arrays.asList(SigV4aAuthScheme.builder().signingName("query")
- .signingRegionSet(Arrays.asList("*")).build())).build());
+ .builder()
+ .endpointUrl(
+ EndpointUrl.fromComponents("https", "query-fips." + region + "." + partitionResult.dualStackDnsSuffix(), -1,
+ ""))
+ .putAttribute(
+ AwsEndpointAttribute.AUTH_SCHEMES,
+ Arrays.asList(SigV4aAuthScheme.builder().signingName("query")
+ .signingRegionSet(Arrays.asList("*")).build())).build());
}
return RuleResult.endpoint(Endpoint.builder()
- .endpointUrl(EndpointUrl.fromString("https://query." + region + "." + partitionResult.dnsSuffix())).build());
+ .endpointUrl(EndpointUrl.fromComponents("https", "query." + region + "." + partitionResult.dnsSuffix(), -1, "")).build());
}
return RuleResult.carryOn();
}
diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-uri-cache-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-uri-cache-class.java
index 2edf6b89329c..d01b466c69f1 100644
--- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-uri-cache-class.java
+++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/rules2/endpoint-provider-uri-cache-class.java
@@ -66,8 +66,9 @@ private static RuleResult endpointRule1(QueryEndpointParams params, String regio
if (parsedArn != null) {
return RuleResult.endpoint(Endpoint
.builder()
- .endpointUrl(EndpointUrl.fromString("https://" + params.endpointId() + ".query."
- + partitionResult.dualStackDnsSuffix()))
+ .endpointUrl(
+ EndpointUrl.fromComponents("https",
+ params.endpointId() + ".query." + partitionResult.dualStackDnsSuffix(), -1, ""))
.putAttribute(
AwsEndpointAttribute.AUTH_SCHEMES,
Arrays.asList(SigV4aAuthScheme.builder().signingName("query")
@@ -87,7 +88,9 @@ private static RuleResult endpointRule2(QueryEndpointParams params, RulePartitio
if (params.useFipsEndpoint() == null && params.useDualStackEndpoint() != null && params.useDualStackEndpoint()) {
return RuleResult.endpoint(Endpoint
.builder()
- .endpointUrl(EndpointUrl.fromString("https://" + params.endpointId() + ".query." + partitionResult.dualStackDnsSuffix()))
+ .endpointUrl(
+ EndpointUrl.fromComponents("https", params.endpointId() + ".query." + partitionResult.dualStackDnsSuffix(),
+ -1, ""))
.putAttribute(
AwsEndpointAttribute.AUTH_SCHEMES,
Arrays.asList(SigV4aAuthScheme.builder().signingName("query")
@@ -95,7 +98,7 @@ private static RuleResult endpointRule2(QueryEndpointParams params, RulePartitio
}
return RuleResult.endpoint(Endpoint
.builder()
- .endpointUrl(EndpointUrl.fromString("https://" + params.endpointId() + ".query." + partitionResult.dnsSuffix()))
+ .endpointUrl(EndpointUrl.fromComponents("https", params.endpointId() + ".query." + partitionResult.dnsSuffix(), -1, ""))
.putAttribute(
AwsEndpointAttribute.AUTH_SCHEMES,
Arrays.asList(SigV4aAuthScheme.builder().signingName("query").signingRegionSet(Arrays.asList("*"))
@@ -109,7 +112,7 @@ private static RuleResult endpointRule6(QueryEndpointParams params, String regio
if (params.useFipsEndpoint() != null && params.useFipsEndpoint() && params.useDualStackEndpoint() == null) {
return RuleResult.endpoint(Endpoint
.builder()
- .endpointUrl(EndpointUrl.fromString("https://query-fips." + region + "." + partitionResult.dnsSuffix()))
+ .endpointUrl(EndpointUrl.fromComponents("https", "query-fips." + region + "." + partitionResult.dnsSuffix(), -1, ""))
.putAttribute(
AwsEndpointAttribute.AUTH_SCHEMES,
Arrays.asList(SigV4aAuthScheme.builder().signingName("query")
@@ -118,7 +121,8 @@ private static RuleResult endpointRule6(QueryEndpointParams params, String regio
if (params.useDualStackEndpoint() != null && params.useDualStackEndpoint() && params.useFipsEndpoint() == null) {
return RuleResult.endpoint(Endpoint
.builder()
- .endpointUrl(EndpointUrl.fromString("https://query." + region + "." + partitionResult.dualStackDnsSuffix()))
+ .endpointUrl(
+ EndpointUrl.fromComponents("https", "query." + region + "." + partitionResult.dualStackDnsSuffix(), -1, ""))
.putAttribute(
AwsEndpointAttribute.AUTH_SCHEMES,
Arrays.asList(SigV4aAuthScheme.builder().signingName("query")
@@ -129,14 +133,16 @@ private static RuleResult endpointRule6(QueryEndpointParams params, String regio
&& params.useFipsEndpoint()) {
return RuleResult.endpoint(Endpoint
.builder()
- .endpointUrl(EndpointUrl.fromString("https://query-fips." + region + "." + partitionResult.dualStackDnsSuffix()))
+ .endpointUrl(
+ EndpointUrl.fromComponents("https", "query-fips." + region + "." + partitionResult.dualStackDnsSuffix(), -1,
+ ""))
.putAttribute(
AwsEndpointAttribute.AUTH_SCHEMES,
Arrays.asList(SigV4aAuthScheme.builder().signingName("query")
.signingRegionSet(Arrays.asList("*")).build())).build());
}
return RuleResult.endpoint(Endpoint.builder()
- .endpointUrl(EndpointUrl.fromString("https://query." + region + "." + partitionResult.dnsSuffix())).build());
+ .endpointUrl(EndpointUrl.fromComponents("https", "query." + region + "." + partitionResult.dnsSuffix(), -1, "")).build());
}
return RuleResult.carryOn();
}
diff --git a/core/endpoints-spi/src/main/java/software/amazon/awssdk/endpoints/EndpointUrl.java b/core/endpoints-spi/src/main/java/software/amazon/awssdk/endpoints/EndpointUrl.java
index b6b9594beb45..43a8ac97ea00 100644
--- a/core/endpoints-spi/src/main/java/software/amazon/awssdk/endpoints/EndpointUrl.java
+++ b/core/endpoints-spi/src/main/java/software/amazon/awssdk/endpoints/EndpointUrl.java
@@ -154,6 +154,11 @@ public static EndpointUrl fromString(String url) {
* The {@code rawUrl} field is {@code null} in this case, so {@link #toUri()} reconstructs
* the URI from components. No query or fragment is included.
*
+ * Equivalence contract: For any valid endpoint URL string {@code s} with no query or fragment,
+ * {@code fromComponents(scheme, host, port, path)} MUST produce an {@code EndpointUrl} that equals
+ * {@code fromString(scheme + "://" + host + (port >= 0 ? ":" + port : "") + path)}.
+ * The codegen relies on this equivalence — see {@code EndpointUrlCodeEmitter}.
+ *
* @param scheme the URL scheme (e.g., "https")
* @param host the hostname (e.g., "s3.us-east-1.amazonaws.com")
* @param port the port number, or -1 if not specified
diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/compiledendpointrules/endpoints/internal/EndpointUrlConformanceTest.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/compiledendpointrules/endpoints/internal/EndpointUrlConformanceTest.java
new file mode 100644
index 000000000000..2c4212607a29
--- /dev/null
+++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/compiledendpointrules/endpoints/internal/EndpointUrlConformanceTest.java
@@ -0,0 +1,136 @@
+/*
+ * 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.net.URI;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import software.amazon.awssdk.endpoints.Endpoint;
+import software.amazon.awssdk.endpoints.EndpointUrl;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.compiledendpointrules.endpoints.CompiledEndpointRulesEndpointParams;
+import software.amazon.awssdk.services.compiledendpointrules.endpoints.CompiledEndpointRulesEndpointProvider;
+
+/**
+ * Conformance test verifying that the build-time decomposed EndpointUrl (via {@code EndpointUrl.fromComponents()})
+ * produces results equivalent to runtime parsing (via {@code EndpointUrl.fromString()}).
+
+ * This guards against drift between the build-time ({@code EndpointUrlCodeEmitter}) and runtime
+ * ({@code EndpointUrl.fromString()}) URL decomposition logic.
+ */
+class EndpointUrlConformanceTest {
+
+ private static final CompiledEndpointRulesEndpointProvider PROVIDER =
+ CompiledEndpointRulesEndpointProvider.defaultProvider();
+
+ /**
+ * Test cases covering the standard region-based resolution path (uses fromComponents in generated code)
+ * and the custom endpoint override path (uses fromString in generated code).
+ */
+ static List