Skip to content

Commit 36e612c

Browse files
authored
refactor: provide structure to client configuration (#778)
* Proposes a new convention doc for client configuration that describes how new configurations should be added and in general how they are used by the SDK. This isn't a normal design doc but a convention/guidelines doc. * Define new builder interfaces for specific domain configurations (e.g. `HttpClientConfig.Builder`, `TracingClientConfig.Builder`, etc). Previously these configuration interfaces were inherited on the generated `Config` type but not on the configuration builder. This is pre-requisite for runtime plugins to work because without it every `Config.Builder` generated shares nothing with any other client. This prevents any kind of common code (e.g. like a plugin) to configure a service client without knowing the concrete type of client. * Rename `ServiceGenerator` to `ServiceClientGenerator` * Refactor `ClientConfigGenerator` into an abstract base class that makes less assumptions about the type of configuration being generated. * Generate a builder for service clients * Introduce new runtime types that generated service client `companion` objects inherit from. This moves the DSL `operator invoke` function into the runtime. * Introduce an abstract base class for service client builders. This will allow us to inject code into all clients instantiated (e.g. load plugins using SPI is one use case). * Moved a few things around (e.g. everything in core `config` package moved to the `client` subpackage). * Refactors `RuntimeTypes` to reduce boilerplate * Refactor our section generator abstraction to be type safe when getting section keys
1 parent fdefd8d commit 36e612c

File tree

42 files changed

+1730
-965
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1730
-965
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"id": "cbe1ef3e-6a6b-467d-b15b-a50eb17c4dbb",
3+
"type": "misc",
4+
"description": "Refactor the way service client configuration is generated"
5+
}

docs/design/README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,12 @@ Start here for an overview:
1010

1111
## Detailed sub-designs
1212

13+
* [Client Configuration](client-configuration.md)
1314
* [Document type](document-type.md)
1415
* [Domain class types in Kotlin SDK](domain-class-types-in-kotlin-sdk.md)
1516
* [Binary request/response streams](binary-streaming.md)
1617
* [Event streams](event-streams.md)
17-
* [Marshaling/serde](marshalling-serde.md) (coming soon)
1818
* [Modeled errors](modeled-errors.md)
19-
* [Nullable properties in SDK domain types](nullable-properties-in-sdk-domain-types.md) (coming soon)
2019
* [Pagination](paginators.md)
2120
* [Retries](retries.md)
2221
* [Tracing](tracing.md)
Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
# Client Configuration
2+
3+
* **Type**: Convention
4+
* **Author(s)**: Aaron Todd
5+
6+
# Abstract
7+
8+
This document describes the structure and conventions used for service client configuration.
9+
10+
# Conventions
11+
12+
This section describes general conventions and guidelines to be used for service client configuration. See the section
13+
below for a concrete example of these conventions in practice.
14+
15+
* All generated service client config builders implement `SdkClientConfig.Builder`
16+
* Domain specific configuration (e.g. HTTP, tracing, etc) should be used as a "mixin" rather than form a hierarchy
17+
* e.g. `HttpClientConfig` should not inherit from `SdkClientConfig`
18+
* Configuration interfaces should live in a subpackage `config` of whatever root package they belong to.
19+
* Configuration interfaces should have a nested `Builder` interface (e.g. `FooConfig.Builder`)
20+
21+
## Example Domain Specific Configuration
22+
23+
This section walks through what a new configuration interface and builder should look like in the runtime.
24+
25+
```kotlin
26+
27+
/**
28+
* Description (1)
29+
*/
30+
public interface FooConfig { // 2
31+
/**
32+
* Property use description // 3
33+
*/
34+
public val fooConfigProp: String // 4
35+
36+
public interface Builder { // 5
37+
38+
/**
39+
* Property configuration description // 6
40+
*/
41+
public var fooConfigProp: String? // 7
42+
}
43+
}
44+
```
45+
46+
1. A detailed description of what the configuration is used for and what might be found in it.
47+
2. Configuration interface should be `public` and generally be `Xyz` with the suffix `Config` (e.g. `XyzConfig`).
48+
3. A detailed description of how the property is used, which _may_ differ from the description of how to configure it.
49+
* This may delegate to the builder property docs. e.g. `Controls the how the foo does blah. See [Builder.fooConfigProp] for more details.`.
50+
4. Configuration fields must be read-only (`val`) and should be immutable. Their type may differ from the builder type, e.g. `List` vs `MutableList` or be non-null where the configuration field is nullable.
51+
* These differences are handled in codegen by setting default values or mapping the builder type as required.
52+
5. The configuration builder interface should be public and nested inside the configuration interface it is meant to build.
53+
6. A detailed description of how the property is configured and how it is used.
54+
7. Builder fields should generally be `var` and/or mutable. Their type may be different from the immutable configuration property.
55+
56+
## Example Service Client
57+
58+
This section walks through an example service client configuration class, describing each of its components at a high level.
59+
60+
```kotlin
61+
public interface BazClient : SdkClient { // 1
62+
63+
override val serviceName: String
64+
get() = "baz"
65+
66+
public override val config: Config // 2
67+
68+
public companion object : SdkClientFactory<Config, Config.Builder, BazClient, Builder>() { // 3
69+
@JvmStatic
70+
override fun builder(): Builder = Builder()
71+
}
72+
73+
public class Builder internal constructor() : AbstractSdkClientBuilder<Config, Config.Builder, BazClient>() { // 4
74+
override val config: Config.Builder = Config.Builder()
75+
override fun newClient(config: Config): BazClient = DefaultBazClient(config) // 5
76+
}
77+
78+
public class Config private constructor(builder: Builder) : SdkClientConfig, HttpClientConfig { // 6
79+
override val sdkLogMode: SdkLogMode = builder.sdkLogMode // 7
80+
override val httpClientEngine: HttpClientEngine? = builder.httpClientEngine
81+
val bazSpecificConfig: String? = builder.bazSpecificConfig
82+
83+
public companion object { // 8
84+
public inline operator fun invoke(block: Builder.() -> kotlin.Unit): Config = Builder().apply(block).build()
85+
}
86+
87+
public class Builder : SdkClientConfig.Builder<Config>, HttpClientConfig.Builder { // 9
88+
/**
89+
* Description
90+
*/
91+
override var httpClientEngine: HttpClientEngine? = null // 10
92+
93+
/**
94+
* Description
95+
*/
96+
override var sdkLogMode: SdkLogMode = SdkLogMode.Default
97+
98+
/**
99+
* Description
100+
*/
101+
var bazSpecificConfig: String? = null
102+
103+
override fun build(): Config = Config(this)
104+
}
105+
}
106+
}
107+
```
108+
109+
NOTE: The example client interface above has intentionally left off KDoc comments for many of the fields for brevity.
110+
NOTE: See the Appendix for definitions of some of these inherited interfaces (e.g. `SdkClientFactory`).
111+
112+
1. All generated service clients inherit from `SdkClient`
113+
2. The `config` property (from `SdkClient`) may be overridden with a more specific configuration than what the inherited interface specifies (`SdkClientConfig`)
114+
3. A companion object is generated that inherits default behavior from the runtime. See [client creation patterns](#client-creation-patterns).
115+
4. A builder for creating a service client is generated. It inherits behavior from `AbstractSdkClientBuilder`
116+
5. The internal concrete (generated) implementation of `BazClient` is provided.
117+
6. The immutable service client configuration container type.
118+
* This will inherit from `SdkClientConfig` and `N` "mixin" configurations. Mixins are pulled in based on the model/customizations.
119+
7. Configuration properties are set from the builder, possibly providing defaults when not set or mapping to different types.
120+
8. The configuration companion object provides a convenience DSL like experience for instantiation, e.g. `val config = BazClient.Config { ... }`
121+
9. The configuration builder inherits from `SdkClientConfig.Builder` and `N` "mixin" configuration builders.
122+
10. Default values are set for the configuration builder properties
123+
124+
## Client Creation Patterns
125+
126+
This section describes in more detail how service clients are created and configured using the `BazClient` described
127+
in the [example service client](#example-service-client) section. These behaviors are a combination of runtime types and
128+
codegen.
129+
130+
```kotlin
131+
// explicit using DSL syntax inherited from companion `SdkClientFactory`
132+
val c1 = BazClient { // this: BazClient.Config.Builder
133+
sdkLogMode = SdkLogMode.LogRequest
134+
}
135+
136+
// use of a builder explicitly, this could be passed around for example
137+
val c2 = BazClient.builder().apply { // this: BazClient.Builder
138+
config.sdkLogMode = SdkLogMode.LogRequest
139+
}.build()
140+
141+
// "vended" using common code
142+
val c3 = ClientVendingMachine.getClient(BazClient)
143+
144+
145+
private object ClientVendingMachine {
146+
fun <
147+
TConfig: SdkClientConfig,
148+
TConfigBuilder: SdkClientConfig.Builder<TConfig>,
149+
TClient: SdkClient,
150+
TClientBuilder: SdkClient.Builder<TConfig, TConfigBuilder, TClient>
151+
> getClient(factory: SdkClientFactory<TConfig, TConfigBuilder, TClient, TClientBuilder>): TClient {
152+
val builder = factory.builder()
153+
// setting various configuration
154+
when(val config = builder.config) {
155+
is BazClient.Config.Builder -> {
156+
// access to configuration that may be specific to BazClient
157+
config.bazSpecificConfig = "specific"
158+
}
159+
is HttpClientConfig.Builder -> {
160+
// access to http related configuration
161+
config.httpClientEngine
162+
}
163+
else -> {
164+
// always available from generic bounds:
165+
config.sdkLogMode
166+
}
167+
}
168+
return builder.build()
169+
}
170+
}
171+
```
172+
173+
1. `c1` is created using DSL like syntax inherited from [SdkClientFactory](#sdkclientfactory)
174+
2. `c2` is created by instantiating a service client builder explicitly using `BazClient.builder()`.
175+
This comes from the companion object and is a static method (defined in [SdkClientFactory](#sdkclientfactory))
176+
3. `c3` is an example of what's possible. It is created using common code that inspects the builder type for specific
177+
configuration (builder) mixins. This type of pattern could be used to centralize configuring a service client without
178+
knowing the concrete type.
179+
180+
# Appendices
181+
182+
## Appendix: FAQ
183+
184+
**Why builders**?
185+
186+
Builders are useful for dealing with evolution as well as the ability to insert additional logic into the creation of
187+
some entity at the time it is built. The API of most builders should be "DSL" like in nature (properties over functions).
188+
When possible the SDK will offer a DSL like experience for creating instances of types.
189+
190+
**Why no hierarchy of config builder classes to inherit from**?
191+
192+
The config interface (e.g. `FooConfig`) is an immutable collection of properties to be
193+
consumed by something else (usually a service client). The configuration builder interface (`FooConfig.Builder`)
194+
describes how to configure the properties. The builder interfaces (e.g. `FooConfig.Builder`) generally have no
195+
concrete implementation because the SDK will codegen a specific implementation by describing the configuration
196+
properties (including the default values that should be used when not given).
197+
198+
## Appendix: Runtime Types
199+
200+
This section provides concrete examples of runtime types referenced in this document. These types may differ or evolve
201+
from when this document was written without loss of understanding of conventions and guidelines described herein.
202+
203+
### SdkClient
204+
205+
```kotlin
206+
/**
207+
* Common interface all generated service clients implement
208+
*/
209+
public interface SdkClient : Closeable {
210+
/**
211+
* The name of the service client
212+
*/
213+
public val serviceName: String
214+
215+
/**
216+
* The client's configuration
217+
*/
218+
public val config: SdkClientConfig
219+
220+
public interface Builder<
221+
TConfig : SdkClientConfig,
222+
TConfigBuilder : SdkClientConfig.Builder<TConfig>,
223+
out TClient : SdkClient,
224+
> : Buildable<TClient> {
225+
226+
/**
227+
* The configuration builder for this client
228+
*/
229+
public val config: TConfigBuilder
230+
}
231+
}
232+
```
233+
234+
### SdkClientConfig
235+
236+
```kotlin
237+
/**
238+
* Common configuration options for any generated SDK client
239+
*/
240+
public interface SdkClientConfig {
241+
/**
242+
* Controls the events that will be logged by the SDK, see [Builder.sdkLogMode].
243+
*/
244+
public val sdkLogMode: SdkLogMode
245+
get() = SdkLogMode.Default
246+
247+
public interface Builder<TConfig : SdkClientConfig> : Buildable<TConfig> {
248+
/**
249+
* Configure events that will be logged. By default, clients will not output
250+
* raw requests or responses. Use this setting to opt in to additional debug logging.
251+
*
252+
* This can be used to configure logging of requests, responses, retries, etc of SDK clients.
253+
*
254+
* **NOTE**: Logging of raw requests or responses may leak sensitive information! It may also have
255+
* performance considerations when dumping the request/response body. This is primarily a tool for
256+
* debug purposes.
257+
*/
258+
public var sdkLogMode: SdkLogMode
259+
}
260+
}
261+
```
262+
263+
### SdkClientFactory
264+
265+
```kotlin
266+
/**
267+
* Interface all generated [SdkClient] companion objects inherit from.
268+
*/
269+
public interface SdkClientFactory<
270+
TConfig : SdkClientConfig,
271+
TConfigBuilder : SdkClientConfig.Builder<TConfig>,
272+
TClient : SdkClient,
273+
out TClientBuilder : SdkClient.Builder<TConfig, TConfigBuilder, TClient>,
274+
> {
275+
/**
276+
* Return a [TClientBuilder] that can create a new [TClient] instance
277+
*/
278+
public fun builder(): TClientBuilder
279+
280+
/**
281+
* Configure a new [TClient] with [block].
282+
*
283+
* Example
284+
* ```
285+
* val client = FooClient { ... }
286+
* ```
287+
*/
288+
public operator fun invoke(block: TConfigBuilder.() -> Unit): TClient = builder().apply {
289+
config.apply(block)
290+
}.build()
291+
}
292+
```
293+
294+
295+
### AbstractSdkClientBuilder
296+
297+
Abstract base class that all service client builder implementations inherit from. This allows the runtime to add
298+
additional logic over time before or after a client is instantiated (e.g. this could be used to load and apply plugins
299+
using SPI on the JVM).
300+
301+
```kotlin
302+
/**
303+
* Abstract base class all [SdkClient] builders should inherit from
304+
*/
305+
@InternalApi
306+
public abstract class AbstractSdkClientBuilder<
307+
TConfig : SdkClientConfig,
308+
TConfigBuilder : SdkClientConfig.Builder<TConfig>,
309+
out TClient : SdkClient,
310+
> : SdkClient.Builder<TConfig, TConfigBuilder, TClient> {
311+
312+
final override fun build(): TClient {
313+
return newClient(config.build())
314+
}
315+
316+
/**
317+
* Return a new [TClient] instance with the given [config]
318+
*/
319+
protected abstract fun newClient(config: TConfig): TClient
320+
}
321+
```
322+
323+
## Appendix: Additional References
324+
325+
* [Kotlin Smithy SDK](kotlin-smithy-sdk.md)
326+
327+
# Revision history
328+
329+
* 01/12/2023 - Created

runtime/protocol/http/common/src/aws/smithy/kotlin/runtime/http/config/HttpClientConfig.kt

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,46 @@
66
package aws.smithy.kotlin.runtime.http.config
77

88
import aws.smithy.kotlin.runtime.http.engine.HttpClientEngine
9+
import aws.smithy.kotlin.runtime.http.interceptors.HttpInterceptor
910

1011
/**
1112
* The user-accessible configuration properties for the SDKs internal HTTP client facility.
1213
*/
1314
public interface HttpClientConfig {
1415
/**
15-
* Allows for overriding the default HTTP client engine.
16+
* Explicit HTTP engine to use when making SDK requests, when not set a default engine will be created and managed
17+
* on behalf of the caller.
18+
*
19+
* **NOTE**: The caller is responsible for managing the lifetime of the engine when set. The SDK
20+
* client will not close it when the client is closed.
1621
*/
1722
public val httpClientEngine: HttpClientEngine?
23+
24+
/**
25+
* Interceptors that will be executed for each SDK operation.
26+
* An [aws.smithy.kotlin.runtime.client.Interceptor] has access to read and modify
27+
* the request and response objects as they are processed by the SDK.
28+
* Interceptors are executed in the order they are configured and are always later than any added automatically by
29+
* the SDK.
30+
*/
31+
public val interceptors: List<HttpInterceptor>
32+
33+
public interface Builder {
34+
/**
35+
* Override the default HTTP client engine used to make SDK requests (e.g. configure proxy behavior, timeouts,
36+
* concurrency, etc).
37+
*
38+
* **NOTE**: The caller is responsible for managing the lifetime of the engine when set. The SDK
39+
* client will not close it when the client is closed.
40+
*/
41+
public var httpClientEngine: HttpClientEngine?
42+
43+
/**
44+
* Add an [aws.smithy.kotlin.runtime.client.Interceptor] that will have access to read and modify
45+
* the request and response objects as they are processed by the SDK.
46+
* Interceptors added using this method are executed in the order they are configured and are always
47+
* later than any added automatically by the SDK.
48+
*/
49+
public var interceptors: MutableList<HttpInterceptor>
50+
}
1851
}

0 commit comments

Comments
 (0)