Skip to content

Commit 9faf77f

Browse files
authored
fix!: overhaul client config property generation (#504)
1 parent f949a4b commit 9faf77f

File tree

11 files changed

+243
-74
lines changed

11 files changed

+243
-74
lines changed

smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinWriter.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,14 @@ fun <T : CodeWriter> T.declareSection(id: SectionId, context: Map<String, Any?>
121121
private fun <T : CodeWriter> T.removeContext(context: Map<String, Any?>): Unit =
122122
context.keys.forEach { key -> removeContext(key) }
123123

124+
/**
125+
* Convenience function to get a typed value out of the context or throw if the key doesn't exist
126+
* or the type is wrong
127+
*/
128+
inline fun <reified T> CodeWriter.getContextValue(key: String): T = checkNotNull(getContext(key) as? T) {
129+
"Expected `$key` in CodeWriter context"
130+
}
131+
124132
/**
125133
* Registers a [SectionWriter] given a [SectionId] to a specific writer. This will cause the
126134
* [SectionWriter.write] to be called at the point in which the section is declared via

smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/RuntimeTypes.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ object RuntimeTypes {
6464

6565
object Engine {
6666
val HttpClientEngineConfig = runtimeSymbol("HttpClientEngineConfig", KotlinDependency.HTTP, "engine")
67+
val HttpClientEngine = runtimeSymbol("HttpClientEngine", KotlinDependency.HTTP, "engine")
6768
}
6869
}
6970

@@ -84,6 +85,7 @@ object RuntimeTypes {
8485
}
8586

8687
object Retries {
88+
val RetryStrategy = runtimeSymbol("RetryStrategy", KotlinDependency.CORE, "retries")
8789
object Impl {
8890
val ExponentialBackoffWithJitter = runtimeSymbol("ExponentialBackoffWithJitter", KotlinDependency.CORE, "retries.impl")
8991
val ExponentialBackoffWithJitterOptions = runtimeSymbol("ExponentialBackoffWithJitterOptions", KotlinDependency.CORE, "retries.impl")

smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/ClientConfigGenerator.kt

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,17 @@ class ClientConfigGenerator(
8181
}
8282

8383
private fun renderCompanionObject() {
84-
if (builderReturnType != null) {
85-
ctx.writer.withBlock("companion object {", "}") {
86-
write("@JvmStatic")
87-
write("fun fluentBuilder(): FluentBuilder = BuilderImpl()")
88-
write("fun builder(): DslBuilder = BuilderImpl()")
89-
write("operator fun invoke(block: DslBuilder.() -> kotlin.Unit): #T = BuilderImpl().apply(block).build()", builderReturnType)
84+
ctx.writer.withBlock("companion object {", "}") {
85+
write("@JvmStatic")
86+
write("fun fluentBuilder(): FluentBuilder = BuilderImpl()")
87+
write("")
88+
if (builderReturnType != null) {
89+
write(
90+
"operator fun invoke(block: DslBuilder.() -> kotlin.Unit): #T = BuilderImpl().apply(block).build()",
91+
builderReturnType
92+
)
93+
} else {
94+
write("operator fun invoke(block: DslBuilder.() -> kotlin.Unit): #configClass.name:L = BuilderImpl().apply(block).build()")
9095
}
9196
}
9297
}
@@ -101,22 +106,38 @@ class ClientConfigGenerator(
101106
}
102107
ctx.writer.addImport(it.symbol)
103108
ctx.writer.addImportReferences(it.symbol, SymbolReference.ContextOption.USE)
109+
it.additionalImports.forEach { symbol ->
110+
ctx.writer.addImport(symbol)
111+
}
104112
}
105113
}
106114

107115
private fun renderImmutableProperties() {
108116
props.forEach { prop ->
109117
val override = if (prop.requiresOverride) "override " else ""
110118

111-
when {
112-
prop.constantValue != null -> {
113-
ctx.writer.write("${override}val #1L: #2T = #3L", prop.propertyName, prop.symbol, prop.constantValue)
119+
when (prop.propertyType) {
120+
is ClientConfigPropertyType.SymbolDefault -> {
121+
ctx.writer.write("${override}val #1L: #2P = builder.#1L", prop.propertyName, prop.symbol)
114122
}
115-
prop.required -> {
116-
ctx.writer.write("${override}val #1L: #2T = builder.#1L ?: throw ClientException(\"#1L must be set\")", prop.propertyName, prop.symbol)
123+
is ClientConfigPropertyType.ConstantValue -> {
124+
ctx.writer.write("${override}val #1L: #2T = #3L", prop.propertyName, prop.symbol, prop.propertyType.value)
117125
}
118-
else -> {
119-
ctx.writer.write("${override}val #1L: #2P = builder.#1L", prop.propertyName, prop.symbol)
126+
is ClientConfigPropertyType.Required -> {
127+
ctx.writer.write(
128+
"${override}val #1L: #2T = requireNotNull(builder.#1L) { #3S }",
129+
prop.propertyName,
130+
prop.symbol,
131+
prop.propertyType.message ?: "${prop.propertyName} is a required configuration property"
132+
)
133+
}
134+
is ClientConfigPropertyType.RequiredWithDefault -> {
135+
ctx.writer.write(
136+
"${override}val #1L: #2T = builder.#1L ?: #3L",
137+
prop.propertyName,
138+
prop.symbol,
139+
prop.propertyType.default
140+
)
120141
}
121142
}
122143
}
@@ -126,7 +147,7 @@ class ClientConfigGenerator(
126147
ctx.writer.write("")
127148
.withBlock("interface FluentBuilder {", "}") {
128149
props
129-
.filter { it.constantValue == null }
150+
.filter { it.propertyType !is ClientConfigPropertyType.ConstantValue }
130151
.forEach { prop ->
131152
// we want the type names sans nullability (?) for arguments
132153
write("fun #1L(#1L: #2L): FluentBuilder", prop.propertyName, prop.symbol.name)
@@ -139,14 +160,12 @@ class ClientConfigGenerator(
139160
ctx.writer.write("")
140161
.withBlock("interface DslBuilder {", "}") {
141162
props
142-
.filter { it.constantValue == null }
163+
.filter { it.propertyType !is ClientConfigPropertyType.ConstantValue }
143164
.forEach { prop ->
144165
prop.documentation?.let { ctx.writer.dokka(it) }
145166
write("var #L: #P", prop.propertyName, prop.symbol)
146167
write("")
147168
}
148-
write("")
149-
write("fun build(): #configClass.name:L")
150169
}
151170
}
152171

@@ -155,7 +174,7 @@ class ClientConfigGenerator(
155174
.withBlock("internal class BuilderImpl() : FluentBuilder, DslBuilder {", "}") {
156175
// override DSL properties
157176
props
158-
.filter { it.constantValue == null }
177+
.filter { it.propertyType !is ClientConfigPropertyType.ConstantValue }
159178
.forEach { prop ->
160179
write("override var #L: #D", prop.propertyName, prop.symbol)
161180
}
@@ -164,7 +183,7 @@ class ClientConfigGenerator(
164183
write("")
165184
write("override fun build(): #configClass.name:L = #configClass.name:L(this)")
166185
props
167-
.filter { it.constantValue == null }
186+
.filter { it.propertyType !is ClientConfigPropertyType.ConstantValue }
168187
.forEach { prop ->
169188
// we want the type names sans nullability (?) for arguments
170189
write("override fun #1L(#1L: #2L): FluentBuilder = apply { this.#1L = #1L }", prop.propertyName, prop.symbol.name)

smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/ClientConfigProperty.kt

Lines changed: 73 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
package software.amazon.smithy.kotlin.codegen.rendering
66

77
import software.amazon.smithy.codegen.core.Symbol
8-
import software.amazon.smithy.codegen.core.SymbolReference
98
import software.amazon.smithy.kotlin.codegen.core.*
109
import software.amazon.smithy.kotlin.codegen.model.boxed
1110
import software.amazon.smithy.kotlin.codegen.model.buildSymbol
@@ -59,14 +58,17 @@ class ClientConfigProperty private constructor(builder: Builder) {
5958
val baseClass: Symbol? = builder.baseClass
6059

6160
/**
62-
* If the property is not provided in the builder then a ClientException is thrown
61+
* The configuration property type. This controls how the property is constructed and rendered
6362
*/
64-
val required: Boolean = builder.required
63+
val propertyType: ClientConfigPropertyType = builder.propertyType
6564

6665
/**
67-
* Specifies that the value should be populated with a constant value that cannot be overridden in the builder.
66+
* Additional symbols that should be imported when this property is generated. This is useful for
67+
* example when the [symbol] type has is an interface and has a default or constant value that
68+
* implements that type. The default value symbol also needs imported.
6869
*/
69-
val constantValue: String? = builder.constantValue
70+
val additionalImports: List<Symbol> = builder.additionalImports
71+
7072
/**
7173
* Flag indicating if this property stems from some base class and needs an override modifier when rendered
7274
*/
@@ -90,7 +92,7 @@ class ClientConfigProperty private constructor(builder: Builder) {
9092
name: String,
9193
defaultValue: Int? = null,
9294
documentation: String? = null,
93-
baseClass: Symbol? = null
95+
baseClass: Symbol? = null,
9496
): ClientConfigProperty =
9597
builtInProperty(name, builtInSymbol("Int", defaultValue?.toString()), documentation, baseClass)
9698

@@ -107,7 +109,7 @@ class ClientConfigProperty private constructor(builder: Builder) {
107109
name: String,
108110
defaultValue: Boolean? = null,
109111
documentation: String? = null,
110-
baseClass: Symbol? = null
112+
baseClass: Symbol? = null,
111113
): ClientConfigProperty =
112114
builtInProperty(name, builtInSymbol("Boolean", defaultValue?.toString()), documentation, baseClass)
113115

@@ -124,7 +126,7 @@ class ClientConfigProperty private constructor(builder: Builder) {
124126
name: String,
125127
defaultValue: String? = null,
126128
documentation: String? = null,
127-
baseClass: Symbol? = null
129+
baseClass: Symbol? = null,
128130
): ClientConfigProperty =
129131
builtInProperty(name, builtInSymbol("String", defaultValue), documentation, baseClass)
130132
}
@@ -137,13 +139,49 @@ class ClientConfigProperty private constructor(builder: Builder) {
137139

138140
var baseClass: Symbol? = null
139141

140-
var required: Boolean = false
141-
var constantValue: String? = null
142+
var propertyType: ClientConfigPropertyType = ClientConfigPropertyType.SymbolDefault
143+
144+
var additionalImports: List<Symbol> = emptyList()
142145

143146
fun build(): ClientConfigProperty = ClientConfigProperty(this)
144147
}
145148
}
146149

150+
/**
151+
* Descriptor for how a configuration property is rendered when the configuration is built
152+
*/
153+
sealed class ClientConfigPropertyType {
154+
/**
155+
* A property type that uses the symbol type and builder symbol directly
156+
*/
157+
object SymbolDefault : ClientConfigPropertyType()
158+
159+
/**
160+
* Specifies that the value should be populated with a constant value that cannot be overridden in the builder.
161+
* These are effectively read-only properties that will show up in the configuration type but not the builder.
162+
*
163+
* @param value the value to assign to the property at construction time
164+
*/
165+
data class ConstantValue(val value: String) : ClientConfigPropertyType()
166+
167+
/**
168+
* A configuration property that is required to be set (i.e. not null).
169+
* If the property is not provided in the builder then an IllegalArgumentException is thrown
170+
*
171+
* @param message The exception message to throw if the property is null, if not set a message is generated
172+
* automatically based on the property name
173+
*/
174+
data class Required(val message: String? = null) : ClientConfigPropertyType()
175+
176+
/**
177+
* A configuration property that is required but has a default value. This has the same semantics of [Required]
178+
* but instead of an exception the default value will be used when not provided in the builder.
179+
*
180+
* @param default the value to assign if the corresponding builder property is null
181+
*/
182+
data class RequiredWithDefault(val default: String) : ClientConfigPropertyType()
183+
}
184+
147185
private fun builtInSymbol(symbolName: String, defaultValue: String?): Symbol {
148186
val builder = Symbol.builder()
149187
.name(symbolName)
@@ -156,7 +194,12 @@ private fun builtInSymbol(symbolName: String, defaultValue: String?): Symbol {
156194
return builder.build()
157195
}
158196

159-
private fun builtInProperty(name: String, symbol: Symbol, documentation: String?, baseClass: Symbol?): ClientConfigProperty =
197+
private fun builtInProperty(
198+
name: String,
199+
symbol: Symbol,
200+
documentation: String?,
201+
baseClass: Symbol?,
202+
): ClientConfigProperty =
160203
ClientConfigProperty {
161204
this.symbol = symbol
162205
this.name = name
@@ -180,13 +223,10 @@ object KotlinClientRuntimeConfigProperty {
180223
}
181224

182225
HttpClientEngine = ClientConfigProperty {
183-
symbol = buildSymbol {
184-
name = "HttpClientEngine"
185-
namespace(KotlinDependency.HTTP, "engine")
186-
}
226+
symbol = RuntimeTypes.Http.Engine.HttpClientEngine
187227
baseClass = httpClientConfigSymbol
188228
documentation = """
189-
Override the default HTTP client configuration (e.g. configure proxy behavior, concurrency, etc)
229+
Override the default HTTP client engine used to make SDK requests (e.g. configure proxy behavior, timeouts, concurrency, etc)
190230
""".trimIndent()
191231
}
192232

@@ -208,6 +248,13 @@ object KotlinClientRuntimeConfigProperty {
208248
}
209249

210250
RetryStrategy = ClientConfigProperty {
251+
symbol = RuntimeTypes.Core.Retries.RetryStrategy
252+
name = "retryStrategy"
253+
documentation = """
254+
The [RetryStrategy] implementation to use for service calls. All API calls will be wrapped by the
255+
strategy.
256+
""".trimIndent()
257+
211258
val retryStrategyBlock = """
212259
run {
213260
val strategyOptions = StandardRetryStrategyOptions.Default
@@ -216,24 +263,16 @@ object KotlinClientRuntimeConfigProperty {
216263
StandardRetryStrategy(strategyOptions, tokenBucket, delayer)
217264
}
218265
""".trimIndent()
219-
220-
symbol = buildSymbol {
221-
name = "RetryStrategy"
222-
namespace(KotlinDependency.CORE, "retries")
223-
nullable = false
224-
reference(RuntimeTypes.Core.Retries.Impl.StandardRetryStrategy, SymbolReference.ContextOption.USE)
225-
reference(RuntimeTypes.Core.Retries.Impl.StandardRetryStrategyOptions, SymbolReference.ContextOption.USE)
226-
reference(RuntimeTypes.Core.Retries.Impl.StandardRetryTokenBucket, SymbolReference.ContextOption.USE)
227-
reference(RuntimeTypes.Core.Retries.Impl.StandardRetryTokenBucketOptions, SymbolReference.ContextOption.USE)
228-
reference(RuntimeTypes.Core.Retries.Impl.ExponentialBackoffWithJitter, SymbolReference.ContextOption.USE)
229-
reference(RuntimeTypes.Core.Retries.Impl.ExponentialBackoffWithJitterOptions, SymbolReference.ContextOption.USE)
230-
}
231-
name = "retryStrategy"
232-
documentation = """
233-
The [RetryStrategy] implementation to use for service calls. All API calls will be wrapped by the
234-
strategy.
235-
""".trimIndent()
236-
constantValue = retryStrategyBlock
266+
propertyType = ClientConfigPropertyType.ConstantValue(retryStrategyBlock)
267+
268+
additionalImports = listOf(
269+
RuntimeTypes.Core.Retries.Impl.StandardRetryStrategy,
270+
RuntimeTypes.Core.Retries.Impl.StandardRetryStrategyOptions,
271+
RuntimeTypes.Core.Retries.Impl.StandardRetryTokenBucket,
272+
RuntimeTypes.Core.Retries.Impl.StandardRetryTokenBucketOptions,
273+
RuntimeTypes.Core.Retries.Impl.ExponentialBackoffWithJitter,
274+
RuntimeTypes.Core.Retries.Impl.ExponentialBackoffWithJitterOptions,
275+
)
237276
}
238277

239278
SdkLogMode = ClientConfigProperty {

smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/ServiceGenerator.kt

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,12 @@ class ServiceGenerator(private val ctx: RenderingContext<ServiceShape>) {
2323
/**
2424
* SectionId used when rendering the service interface companion object
2525
*/
26-
object ServiceInterfaceCompanionObject : SectionId
26+
object ServiceInterfaceCompanionObject : SectionId {
27+
/**
28+
* Context key for the service symbol
29+
*/
30+
const val ServiceSymbol = "ServiceSymbol"
31+
}
2732

2833
/**
2934
* SectionId used when rendering the service configuration object
@@ -58,7 +63,10 @@ class ServiceGenerator(private val ctx: RenderingContext<ServiceShape>) {
5863
.call {
5964
// allow integrations to add additional fields to companion object or configuration
6065
writer.write("")
61-
writer.declareSection(ServiceInterfaceCompanionObject) {
66+
writer.declareSection(
67+
ServiceInterfaceCompanionObject,
68+
context = mapOf(ServiceInterfaceCompanionObject.ServiceSymbol to serviceSymbol)
69+
) {
6270
renderCompanionObject()
6371
}
6472
writer.write("")
@@ -85,20 +93,25 @@ class ServiceGenerator(private val ctx: RenderingContext<ServiceShape>) {
8593
* e.g.
8694
* ```
8795
* companion object {
88-
* fun build(block: Configuration.() -> Unit = {}): LambdaClient {
89-
* val config = Configuration().apply(block)
96+
* operator fun invoke(block: Config.DslBuilder.() -> Unit = {}): LambdaClient {
97+
* val config = Config.BuilderImpl().apply(block).build()
9098
* return DefaultLambdaClient(config)
9199
* }
100+
*
101+
* operator fun invoke(config: Config): LambdaClient = DefaultLambdaClient(config)
92102
* }
93103
* ```
94104
*/
95105
private fun renderCompanionObject() {
96-
writer.openBlock("companion object {")
97-
.openBlock("operator fun invoke(block: Config.DslBuilder.() -> Unit = {}): ${serviceSymbol.name} {")
98-
.write("val config = Config.BuilderImpl().apply(block).build()")
99-
.write("return Default${serviceSymbol.name}(config)")
100-
.closeBlock("}")
101-
.closeBlock("}")
106+
writer.withBlock("companion object {", "}") {
107+
withBlock("operator fun invoke(block: Config.DslBuilder.() -> Unit = {}): ${serviceSymbol.name} {", "}") {
108+
write("val config = Config.BuilderImpl().apply(block).build()")
109+
write("return Default${serviceSymbol.name}(config)")
110+
}
111+
112+
write("")
113+
write("operator fun invoke(config: Config): ${serviceSymbol.name} = Default${serviceSymbol.name}(config)")
114+
}
102115
}
103116

104117
private fun importExternalSymbols() {

0 commit comments

Comments
 (0)