diff --git a/client/client-api/src/main/java/software/amazon/smithy/java/client/core/ClientTransportFactory.java b/client/client-api/src/main/java/software/amazon/smithy/java/client/core/ClientTransportFactory.java index c019828ca..bd0cb0ac9 100644 --- a/client/client-api/src/main/java/software/amazon/smithy/java/client/core/ClientTransportFactory.java +++ b/client/client-api/src/main/java/software/amazon/smithy/java/client/core/ClientTransportFactory.java @@ -6,7 +6,6 @@ package software.amazon.smithy.java.client.core; import java.util.ArrayList; -import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.ServiceLoader; @@ -52,15 +51,17 @@ default byte priority() { *

Transports must be able to be instantiated without any arguments for use in dynamic clients. */ default ClientTransport createTransport() { - return createTransport(Document.of(Collections.emptyMap())); + return createTransport(Document.EMPTY_MAP, Document.EMPTY_MAP); } /** - * Create a {@link ClientTransport} with a user-provided configuration. + * Create a {@link ClientTransport} with transport-specific settings and the full plugin settings. * - *

Configurations are typically specified in the configuration of the client-codegen plugin. + *

The {@code pluginSettings} document contains the full codegen plugin configuration, allowing + * transports to read shared settings (e.g., {@code httpConfig}) that are not transport-specific. + * Transport-specific settings should take precedence over shared settings. */ - ClientTransport createTransport(Document settings); + ClientTransport createTransport(Document settings, Document pluginSettings); /** * Loads all {@link ClientTransportFactory} implementations and sorts them by priority. diff --git a/client/client-http/src/main/java/software/amazon/smithy/java/client/http/HttpTransportConfig.java b/client/client-http/src/main/java/software/amazon/smithy/java/client/http/HttpTransportConfig.java new file mode 100644 index 000000000..2fe1eb364 --- /dev/null +++ b/client/client-http/src/main/java/software/amazon/smithy/java/client/http/HttpTransportConfig.java @@ -0,0 +1,99 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.client.http; + +import java.time.Duration; +import software.amazon.smithy.java.core.serde.document.Document; +import software.amazon.smithy.java.http.api.HttpVersion; + +/** + * Common HTTP transport configuration shared across transport implementations. + * + *

Subclass this to add transport-specific settings. + */ +public class HttpTransportConfig { + + private Duration requestTimeout; + private Duration connectTimeout; + private HttpVersion httpVersion; + + /** + * Per-request timeout. Null means no timeout (use client default). + * + *

Defined in the document data as milliseconds. + * + * @return request timeout. + */ + public Duration requestTimeout() { + return requestTimeout; + } + + public HttpTransportConfig requestTimeout(Duration requestTimeout) { + this.requestTimeout = requestTimeout; + return this; + } + + /** + * TCP connect timeout. Null means use transport default. + */ + public Duration connectTimeout() { + return connectTimeout; + } + + public HttpTransportConfig connectTimeout(Duration connectTimeout) { + this.connectTimeout = connectTimeout; + return this; + } + + /** + * HTTP version preference. Null means use transport default. + * + *

Defined in the document data as "http/1.0", "http/1.1", or "h2". + */ + public HttpVersion httpVersion() { + return httpVersion; + } + + public HttpTransportConfig httpVersion(HttpVersion httpVersion) { + this.httpVersion = httpVersion; + return this; + } + + /** + * Populate fields from a Document config. + * + *

{@code
+     * {
+     *   "requestTimeoutMs": 5000,
+     *   "connectTimeoutMs": 3000,
+     *   "httpVersion": "HTTP/2.0"
+     * }
+     * }
+ * + * @param doc configuration document + * @return this config for chaining + */ + public HttpTransportConfig fromDocument(Document doc) { + var config = doc.asStringMap(); + + var timeout = config.get("requestTimeoutMs"); + if (timeout != null) { + this.requestTimeout = Duration.ofMillis(timeout.asLong()); + } + + var connTimeout = config.get("connectTimeoutMs"); + if (connTimeout != null) { + this.connectTimeout = Duration.ofMillis(connTimeout.asLong()); + } + + var version = config.get("httpVersion"); + if (version != null) { + this.httpVersion = HttpVersion.from(version.asString()); + } + + return this; + } +} diff --git a/client/client-http/src/main/java/software/amazon/smithy/java/client/http/JavaHttpClientTransport.java b/client/client-http/src/main/java/software/amazon/smithy/java/client/http/JavaHttpClientTransport.java index 4713a7087..52be2b7af 100644 --- a/client/client-http/src/main/java/software/amazon/smithy/java/client/http/JavaHttpClientTransport.java +++ b/client/client-http/src/main/java/software/amazon/smithy/java/client/http/JavaHttpClientTransport.java @@ -39,6 +39,7 @@ public class JavaHttpClientTransport implements ClientTransport runtimeTraits; private final Selector runtimeTraitsSelector; + private final ObjectNode httpConfig; private final Map> generatedSymbols = new HashMap<>(); private JavaCodegenSettings(Builder builder) { @@ -103,6 +106,7 @@ private JavaCodegenSettings(Builder builder) { this.useExternalTypes = builder.useExternalTypes; this.runtimeTraits = Collections.unmodifiableList(builder.runtimeTraits); this.runtimeTraitsSelector = builder.runtimeTraitsSelector; + this.httpConfig = builder.httpConfig; } /** @@ -128,7 +132,8 @@ public static JavaCodegenSettings fromNode(ObjectNode settingsNode) { .getStringMember(EDITION, builder::edition) .getBooleanMember(USE_EXTERNAL_TYPES, builder::useExternalTypes) .getArrayMember(RUNTIME_TRAITS, n -> n.expectStringNode().expectShapeId(), builder::runtimeTraits) - .getStringMember(RUNTIME_TRAITS_SELECTOR, builder::runtimeTraitsSelector); + .getStringMember(RUNTIME_TRAITS_SELECTOR, builder::runtimeTraitsSelector) + .getObjectMember(HTTP_CONFIG, builder::httpConfig); builder.sourceLocation(settingsNode.getSourceLocation().getFilename()); @@ -203,6 +208,10 @@ public Selector runtimeTraitsSelector() { return runtimeTraitsSelector; } + public ObjectNode httpConfig() { + return httpConfig; + } + @SmithyInternalApi public void addSymbol(Symbol symbol) { var symbols = generatedSymbols.computeIfAbsent(symbol.getNamespace(), k -> new HashSet<>()); @@ -262,6 +271,7 @@ public static final class Builder { private final List runtimeTraits = new ArrayList<>(); private Selector runtimeTraitsSelector; private boolean useExternalTypes; + private ObjectNode httpConfig; public Builder service(String string) { this.service = ShapeId.from(string); @@ -366,6 +376,11 @@ public Builder runtimeTraitsSelector(String selector) { return this; } + public Builder httpConfig(ObjectNode httpConfig) { + this.httpConfig = httpConfig; + return this; + } + public JavaCodegenSettings build() { return new JavaCodegenSettings(this); } diff --git a/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/client/generators/ClientInterfaceGenerator.java b/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/client/generators/ClientInterfaceGenerator.java index 418f8ec91..a27f43bd2 100644 --- a/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/client/generators/ClientInterfaceGenerator.java +++ b/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/client/generators/ClientInterfaceGenerator.java @@ -133,7 +133,8 @@ final class Builder extends ${client:T}.Builder<${interface:T}, Builder>${?setti ${?hasDefaults}${defaultPlugins:C|} ${/hasDefaults}${?hasDefaultProtocol}${defaultProtocol:C|} ${/hasDefaultProtocol}${?hasTransportSettings}${transportSettings:C|} - ${/hasTransportSettings}${?defaultSchemes} + ${/hasTransportSettings}${?hasHttpConfig}${httpConfig:C|} + ${/hasHttpConfig}${?defaultSchemes} ${defaultAuth:C|}${/defaultSchemes} private Builder() { @@ -151,7 +152,7 @@ private Builder() { configBuilder().protocol(${protocolFactory:C}.createProtocol(protocolSettings, protocolTrait)); } ${/hasDefaultProtocol}${?hasDefaultTransport}if (configBuilder().transport() == null) { - configBuilder().transport(${transportFactory:C}.createTransport(${?hasTransportSettings}transportSettings${/hasTransportSettings})); + configBuilder().transport(${transportFactory:C}.createTransport(${?hasTransportSettings}transportSettings${/hasTransportSettings}${^hasTransportSettings}${document:T}.EMPTY_MAP${/hasTransportSettings}, ${?hasHttpConfig}httpConfig${/hasHttpConfig}${^hasHttpConfig}${document:T}.EMPTY_MAP${/hasHttpConfig})); }${/hasDefaultTransport} ${?hasBdd}${loadBddInfo:C|}${/hasBdd} return new ${impl:T}(this); @@ -199,6 +200,10 @@ final class RequestOverrideBuilder extends ${requestOverride:T}.OverrideBuilder< writer.putContext( "transportSettings", new TransportSettingsGenerator(writer, settings.transportSettings())); + var hasHttpConfig = settings.httpConfig() != null && !settings.httpConfig().isEmpty(); + writer.putContext("hasHttpConfig", hasHttpConfig); + writer.putContext("httpConfig", new HttpConfigGenerator(writer, settings.httpConfig())); + writer.putContext("document", Document.class); writer.putContext( "operations", new OperationMethodGenerator( @@ -249,6 +254,20 @@ public void run() { } } + private record HttpConfigGenerator(JavaWriter writer, ObjectNode settings) implements Runnable { + @Override + public void run() { + if (settings == null) { + return; + } + writer.pushState(); + writer.putContext("document", Document.class); + writer.putContext("nodeWriter", new NodeDocumentWriter(writer, settings)); + writer.write("private static final ${document:T} httpConfig = ${nodeWriter:C};"); + writer.popState(); + } + } + private record NodeDocumentWriter(JavaWriter writer, ObjectNode node) implements NodeVisitor, Runnable { @Override diff --git a/core/src/main/java/software/amazon/smithy/java/core/serde/document/Document.java b/core/src/main/java/software/amazon/smithy/java/core/serde/document/Document.java index 04b7eccd8..b0e04836e 100644 --- a/core/src/main/java/software/amazon/smithy/java/core/serde/document/Document.java +++ b/core/src/main/java/software/amazon/smithy/java/core/serde/document/Document.java @@ -10,6 +10,7 @@ import java.nio.ByteBuffer; import java.time.Instant; import java.util.ArrayList; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -68,6 +69,11 @@ * itself was serialized or deserialized directly. */ public interface Document extends SerializableShape { + /** + * An empty map document. + */ + Document EMPTY_MAP = of(Collections.emptyMap()); + /** * Get the Smithy data model type for the underlying contents of the document. *