Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: apply clock skew interceptor to clients created via invoke #1067

Merged
merged 2 commits into from
Apr 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changes/5498a35b-f57f-4c0c-bbf7-43e07830be01.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"id": "5498a35b-f57f-4c0c-bbf7-43e07830be01",
"type": "bugfix",
"description": "Add config finalization to service clients via new abstract factory class; apply clock skew interceptor to clients created via `invoke`",
"issues": [
"awslabs/aws-sdk-kotlin#1211"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ import software.amazon.smithy.kotlin.codegen.rendering.ServiceClientGenerator
*/
class ClockSkew : KotlinIntegration {
override val sectionWriters: List<SectionWriterBinding>
get() = listOf(SectionWriterBinding(ServiceClientGenerator.Sections.FinalizeConfig, clockSkewSectionWriter))
get() = listOf(
SectionWriterBinding(
ServiceClientGenerator.Sections.CompanionObject.FinalizeConfig,
clockSkewSectionWriter,
),
)

private val clockSkewSectionWriter = AppendingSectionWriter { writer ->
val interceptorSymbol = RuntimeTypes.AwsProtocolCore.ClockSkewInterceptor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ object RuntimeTypes {
object SmithyClient : RuntimeTypePackage(KotlinDependency.SMITHY_CLIENT) {
val SdkClient = symbol("SdkClient")
val AbstractSdkClientBuilder = symbol("AbstractSdkClientBuilder")
val AbstractSdkClientFactory = symbol("AbstractSdkClientFactory")
val LogMode = symbol("LogMode")
val RetryClientConfig = symbol("RetryClientConfig")
val RetryStrategyClientConfig = symbol("RetryStrategyClientConfig")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ class ServiceClientGenerator(private val ctx: RenderingContext<ServiceShape>) {
* [SectionId] used when rendering the service interface companion object
*/
object CompanionObject : SectionId {

/**
* [SectionId] used when rendering the finalizeConfig block of a service client companion object
*/
object FinalizeConfig : SectionId

/**
* Context key for the service symbol
*/
Expand All @@ -46,6 +52,11 @@ class ServiceClientGenerator(private val ctx: RenderingContext<ServiceShape>) {
* Context key for the SDK ID
*/
val SdkId: SectionKey<String> = SectionKey("SdkId")

/**
* [SectionId] used when rendering the supertype(s) of the companion object
*/
object SuperTypes : SectionId
}

/**
Expand All @@ -57,11 +68,6 @@ class ServiceClientGenerator(private val ctx: RenderingContext<ServiceShape>) {
*/
val RenderingContext: SectionKey<RenderingContext<ServiceShape>> = SectionKey("RenderingContext")
}

/**
* [SectionId] used when rendering the finalizeConfig block of a service client
*/
object FinalizeConfig : SectionId
}

init {
Expand All @@ -88,38 +94,28 @@ class ServiceClientGenerator(private val ctx: RenderingContext<ServiceShape>) {

writer.renderDocumentation(service)
writer.renderAnnotations(service)
writer.openBlock(
writer.withBlock(
"#L interface ${serviceSymbol.name} : #T {",
"}",
ctx.settings.api.visibility,
RuntimeTypes.SmithyClient.SdkClient,
)
.call {
// allow access to client's Config
writer.dokka("${serviceSymbol.name}'s configuration")
writer.write("public override val config: Config")
}
.call {
// allow integrations to add additional fields to companion object or configuration
writer.write("")
writer.declareSection(
Sections.CompanionObject,
context = mapOf(
Sections.CompanionObject.ServiceSymbol to serviceSymbol,
Sections.CompanionObject.SdkId to ctx.settings.sdkId,
),
) {
renderCompanionObject()
}
writer.write("")
renderServiceBuilder()
) {
// allow access to client's Config
dokka("${serviceSymbol.name}'s configuration")
write("public override val config: Config")

writer.write("")
renderServiceConfig()
}
.call {
operations.forEach { renderOperation(operationsIndex, it) }
}
.closeBlock("}")
// allow integrations to add additional fields to companion object or configuration
write("")
renderCompanionObject()

write("")
renderServiceBuilder()

write("")
renderServiceConfig()

operations.forEach { renderOperation(operationsIndex, it) }
}
.write("")

if (ctx.protocolGenerator != null) { // returns default impl, which only exists if there's a protocol generator
Expand Down Expand Up @@ -172,15 +168,39 @@ class ServiceClientGenerator(private val ctx: RenderingContext<ServiceShape>) {
private fun renderCompanionObject() {
// don't render a companion object which is used for building a service client unless we have a protocol generator
if (ctx.protocolGenerator == null) return
writer.withBlock(
"public companion object : #T<Config, Config.Builder, #T, Builder> {",
"}",
RuntimeTypes.SmithyClient.SdkClientFactory,
serviceSymbol,
) {
write("@#T", KotlinTypes.Jvm.JvmStatic)
write("override fun builder(): Builder = Builder()")
}

writer
.writeInline("public companion object : ")
.declareSection(
Sections.CompanionObject.SuperTypes,
context = mapOf(
Sections.CompanionObject.ServiceSymbol to serviceSymbol,
),
) {
writeInline(
"#T<Config, Config.Builder, #T, Builder>()",
RuntimeTypes.SmithyClient.AbstractSdkClientFactory,
serviceSymbol,
)
}
.withBlock(" {", "}") {
declareSection(
Sections.CompanionObject,
context = mapOf(
Sections.CompanionObject.ServiceSymbol to serviceSymbol,
Sections.CompanionObject.SdkId to ctx.settings.sdkId,
),
) {
write("@#T", KotlinTypes.Jvm.JvmStatic)
write("override fun builder(): Builder = Builder()")
write("")
withBlock("override fun finalizeConfig(builder: Builder) {", "}") {
declareSection(Sections.CompanionObject.FinalizeConfig) {
write("super.finalizeConfig(builder)")
}
}
}
}
}

private fun renderOperation(opIndex: OperationIndex, op: OperationShape) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,13 @@ class ServiceClientGeneratorTest {
@Test
fun `it renders a companion object with default client factory if protocol generator`() {
val expected = """
public companion object : SdkClientFactory<Config, Config.Builder, TestClient, Builder> {
public companion object : AbstractSdkClientFactory<Config, Config.Builder, TestClient, Builder>() {
@JvmStatic
override fun builder(): Builder = Builder()

override fun finalizeConfig(builder: Builder) {
super.finalizeConfig(builder)
}
}
""".formatForTest()
commonWithProtocolTestContents.shouldContainOnlyOnceWithDiff(expected)
Expand All @@ -66,7 +70,7 @@ class ServiceClientGeneratorTest {
@Test
fun `it renders a companion object without default client factory if no protocol generator`() {
val expected = """
public companion object : SdkClientFactory
public companion object : AbstractSdkClientFactory
""".formatForTest()
commonTestContents.shouldNotContain(expected)
}
Expand All @@ -91,9 +95,7 @@ class ServiceClientGeneratorTest {
val writer = KotlinWriter(TestModelDefault.NAMESPACE)
val service = model.expectShape<ServiceShape>(TestModelDefault.SERVICE_SHAPE_ID)
writer.registerSectionWriter(ServiceClientGenerator.Sections.CompanionObject) { codeWriter, _ ->
codeWriter.openBlock("public companion object {")
.write("public fun foo(): Int = 1")
.closeBlock("}")
codeWriter.write("public fun foo(): Int = 1")
}

writer.registerSectionWriter(ServiceClientGenerator.Sections.ServiceConfig) { codeWriter, _ ->
Expand All @@ -103,13 +105,17 @@ class ServiceClientGeneratorTest {
}

val settings = KotlinSettings(service.id, KotlinSettings.PackageSettings("test", "0.0"), sdkId = service.id.name)
val renderingCtx = RenderingContext(writer, service, model, provider, settings)

// Without a protocol generator the companion object won't be generated so use a fake protocol here.
val protocol = MockHttpProtocolGenerator(model)
val renderingCtx = RenderingContext(writer, service, model, provider, settings, protocol)

val generator = ServiceClientGenerator(renderingCtx)
generator.render()
val contents = writer.toString()

val expectedCompanionOverride = """
public companion object {
public companion object : AbstractSdkClientFactory<Config, Config.Builder, TestClient, Builder>() {
public fun foo(): Int = 1
}
""".formatForTest()
Expand Down
6 changes: 6 additions & 0 deletions runtime/smithy-client/api/smithy-client.api
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ public abstract class aws/smithy/kotlin/runtime/client/AbstractSdkClientBuilder
protected abstract fun newClient (Laws/smithy/kotlin/runtime/client/SdkClientConfig;)Laws/smithy/kotlin/runtime/client/SdkClient;
}

public abstract class aws/smithy/kotlin/runtime/client/AbstractSdkClientFactory : aws/smithy/kotlin/runtime/client/SdkClientFactory {
public fun <init> ()V
protected fun finalizeConfig (Laws/smithy/kotlin/runtime/client/SdkClient$Builder;)V
public fun invoke (Lkotlin/jvm/functions/Function1;)Laws/smithy/kotlin/runtime/client/SdkClient;
}

public abstract interface class aws/smithy/kotlin/runtime/client/IdempotencyTokenConfig {
public abstract fun getIdempotencyTokenProvider ()Laws/smithy/kotlin/runtime/client/IdempotencyTokenProvider;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package aws.smithy.kotlin.runtime.client

public abstract class AbstractSdkClientFactory<
TConfig : SdkClientConfig,
TConfigBuilder : SdkClientConfig.Builder<TConfig>,
TClient : SdkClient,
TClientBuilder : SdkClient.Builder<TConfig, TConfigBuilder, TClient>,
> : SdkClientFactory<TConfig, TConfigBuilder, TClient, TClientBuilder> {

/**
* Inject any client-specific config
*/
protected open fun finalizeConfig(builder: TClientBuilder) { }

public override operator fun invoke(block: TConfigBuilder.() -> Unit): TClient = builder().apply {
config.apply(block)
finalizeConfig(this)
}.build()
Comment on lines +19 to +22
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invoking an abstract class looks a little odd since they aren't meant to be instantiated. I don't have any suggestion here, it just stands out to me

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't quite invoking an abstract class. It's an operator defined on an abstract class which will only be callable on a concrete instance. No one's ever going to call AbstractSdkClientFactory.invoke { … } or AbstractSdkClientFactory().invoke { … }—both are compile-time errors.

This simply enables classes which inherit AbstractSdkClientFactory (namely, service client companion objects) to be invoked (e.g., S3Client.invoke { … }).

}
Loading