|
| 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 |
0 commit comments