Skip to content

Commit 54b6038

Browse files
authored
feat: add codegen support for extra metadata (#846)
1 parent 0184a16 commit 54b6038

File tree

9 files changed

+258
-15
lines changed

9 files changed

+258
-15
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package aws.sdk.kotlin.runtime.http.interceptors
6+
7+
import aws.sdk.kotlin.runtime.InternalSdkApi
8+
import aws.sdk.kotlin.runtime.http.operation.CustomUserAgentMetadata
9+
import aws.smithy.kotlin.runtime.client.RequestInterceptorContext
10+
import aws.smithy.kotlin.runtime.http.interceptors.HttpInterceptor
11+
12+
/**
13+
* Adds metadata to the user agent sent in requests.
14+
* @param metadata A map of keys/values to add to existing metadata.
15+
*/
16+
@InternalSdkApi
17+
public class AddUserAgentMetadataInterceptor(private val metadata: Map<String, String>) : HttpInterceptor {
18+
override fun readBeforeExecution(context: RequestInterceptorContext<Any>) {
19+
val existing = context.executionContext.getOrNull(CustomUserAgentMetadata.ContextKey)
20+
21+
val new = existing ?: CustomUserAgentMetadata()
22+
metadata.forEach { (k, v) -> new.add(k, v) }
23+
24+
context.executionContext[CustomUserAgentMetadata.ContextKey] = new
25+
}
26+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package aws.sdk.kotlin.runtime.http.interceptors
6+
7+
import aws.sdk.kotlin.runtime.http.operation.CustomUserAgentMetadata
8+
import aws.smithy.kotlin.runtime.client.RequestInterceptorContext
9+
import aws.smithy.kotlin.runtime.operation.ExecutionContext
10+
import aws.smithy.kotlin.runtime.util.get
11+
import kotlin.test.Test
12+
import kotlin.test.assertEquals
13+
14+
class AddUserAgentMetadataInterceptorTest {
15+
@Test
16+
fun testAddNew() {
17+
val ctx = interceptorContext()
18+
19+
val interceptor = AddUserAgentMetadataInterceptor(metadata)
20+
interceptor.readBeforeExecution(ctx)
21+
22+
val actual = ctx.executionContext[CustomUserAgentMetadata.ContextKey].extras
23+
assertEquals("bar", actual["foo"])
24+
}
25+
26+
@Test
27+
fun testMerge() {
28+
val existingMetadata = mapOf("foo" to "This value will be replaced", "baz" to "qux")
29+
val executionContext = ExecutionContext().apply {
30+
set(CustomUserAgentMetadata.ContextKey, CustomUserAgentMetadata(extras = existingMetadata))
31+
}
32+
val ctx = interceptorContext(executionContext)
33+
34+
val interceptor = AddUserAgentMetadataInterceptor(metadata)
35+
interceptor.readBeforeExecution(ctx)
36+
37+
val actual = ctx.executionContext[CustomUserAgentMetadata.ContextKey].extras
38+
assertEquals("bar", actual["foo"])
39+
assertEquals("qux", actual["baz"])
40+
}
41+
}
42+
43+
private val metadata = mapOf("foo" to "bar")
44+
45+
private fun interceptorContext(executionContext: ExecutionContext = ExecutionContext()) =
46+
object : RequestInterceptorContext<Any> {
47+
override val request: Any = Unit
48+
override val executionContext: ExecutionContext = executionContext
49+
}

codegen/sdk/build.gradle.kts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -307,13 +307,14 @@ fun forwardProperty(name: String) {
307307
val generateSmithyProjections = tasks.named<SmithyBuild>("generateSmithyProjections") {
308308
doFirst {
309309
forwardProperty("aws.partitions_file")
310+
forwardProperty("aws.user_agent.add_metadata")
310311
}
311312
}
312313

313314
val stageSdks = tasks.register("stageSdks") {
314315
group = "codegen"
315316
description = "relocate generated SDK(s) from build directory to services/ dir"
316-
dependsOn(tasks.named("generateSmithyProjections"))
317+
dependsOn(generateSmithyProjections)
317318
doLast {
318319
println("discoveredServices = ${discoveredServices.joinToString { it.sdkId }}")
319320
discoveredServices.forEach {
@@ -335,7 +336,7 @@ tasks.register("bootstrap") {
335336
group = "codegen"
336337
description = "Generate AWS SDK's and register them with the build"
337338

338-
dependsOn(tasks.named("generateSmithyProjections"))
339+
dependsOn(generateSmithyProjections)
339340
finalizedBy(stageSdks)
340341
}
341342

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package aws.sdk.kotlin.codegen
6+
7+
import software.amazon.smithy.kotlin.codegen.KotlinSettings
8+
import software.amazon.smithy.kotlin.codegen.core.KotlinWriter
9+
import software.amazon.smithy.kotlin.codegen.core.withBlock
10+
import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration
11+
import software.amazon.smithy.kotlin.codegen.rendering.protocol.ProtocolGenerator
12+
import software.amazon.smithy.kotlin.codegen.rendering.protocol.ProtocolMiddleware
13+
import software.amazon.smithy.model.Model
14+
import software.amazon.smithy.model.node.Node
15+
import software.amazon.smithy.model.shapes.OperationShape
16+
import java.io.File
17+
18+
internal const val ADD_USER_AGENT_METADATA_ENV_VAR = "ADD_USER_AGENT_METADATA"
19+
internal const val ADD_USER_AGENT_METADATA_SYS_PROP = "aws.user_agent.add_metadata"
20+
21+
/**
22+
* Adds additional metadata to the user agent sent in requests. This additional metadata is loaded from a JSON file
23+
* during the **:codegen:sdk:bootstrap** task. The file name is detected by one of the following means:
24+
*
25+
* * The `ADD_USER_AGENT_METADATA` environment variable
26+
* * The `aws.user_agent.add_metadata` project property. This may be set via:
27+
* * The Gradle command line argument `-Paws.user_agent.add_metadata=<path>` –or–
28+
* * A line in the **local.properties** file
29+
*
30+
* If neither option is configured, no additional metadata is added to the user agent.
31+
*/
32+
class AddUserAgentMetadataIntegration : KotlinIntegration {
33+
private val metadataFile: String? by lazy {
34+
System.getenv(ADD_USER_AGENT_METADATA_ENV_VAR) ?: System.getProperty(ADD_USER_AGENT_METADATA_SYS_PROP)
35+
}
36+
37+
private val metadata by lazy {
38+
val json = metadataFile?.let { File(it).readText() } ?: "{}" // Default to empty JSON object (no extra metadata)
39+
val node = Node.parse(json).expectObjectNode()
40+
node.members.entries.associate { (k, v) -> k.value to v.expectStringNode().value }
41+
}
42+
43+
override fun customizeMiddleware(
44+
ctx: ProtocolGenerator.GenerationContext,
45+
resolved: List<ProtocolMiddleware>,
46+
): List<ProtocolMiddleware> = resolved + addUserAgentMetadataMiddleware
47+
48+
override fun enabledForService(model: Model, settings: KotlinSettings): Boolean = metadataFile != null
49+
50+
private val addUserAgentMetadataMiddleware = object : ProtocolMiddleware {
51+
override val name: String = "AddUserAgentMetadata"
52+
53+
override fun render(ctx: ProtocolGenerator.GenerationContext, op: OperationShape, writer: KotlinWriter) {
54+
writer.write(
55+
"op.interceptors.add(#T(extraMetadata))",
56+
AwsRuntimeTypes.Http.Interceptors.AddUserAgentMetadataInterceptor,
57+
)
58+
}
59+
60+
override fun renderProperties(writer: KotlinWriter) {
61+
writer.withBlock("private val extraMetadata: Map<String, String> = mapOf(", ")") {
62+
metadata.forEach { (k, v) -> writer.write("#S to #S,", k, v) }
63+
}
64+
}
65+
}
66+
}

codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/AwsRuntimeTypes.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,14 @@ object AwsRuntimeTypes {
5050
}
5151

5252
object Http : RuntimeTypePackage(AwsKotlinDependency.AWS_HTTP) {
53+
object Interceptors : RuntimeTypePackage(AwsKotlinDependency.AWS_HTTP, "interceptors") {
54+
val AddUserAgentMetadataInterceptor = symbol("AddUserAgentMetadataInterceptor")
55+
}
56+
5357
object Retries {
5458
val AwsDefaultRetryPolicy = symbol("AwsDefaultRetryPolicy", "retries")
5559
}
60+
5661
object Middleware {
5762
val AwsRetryHeaderMiddleware = symbol("AwsRetryHeaderMiddleware", "middleware")
5863
}

codegen/smithy-aws-kotlin-codegen/src/main/resources/META-INF/services/software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
aws.sdk.kotlin.codegen.SdkProtocolGeneratorSupplier
2+
aws.sdk.kotlin.codegen.AddUserAgentMetadataIntegration
23
aws.sdk.kotlin.codegen.AwsRetryHeaderIntegration
34
aws.sdk.kotlin.codegen.customization.s3.S3GeneratorSupplier
45
aws.sdk.kotlin.codegen.GradleGenerator
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package aws.sdk.kotlin.codegen
6+
7+
import aws.sdk.kotlin.codegen.testutil.lines
8+
import software.amazon.smithy.kotlin.codegen.test.*
9+
import java.nio.file.Files
10+
import kotlin.io.path.deleteIfExists
11+
import kotlin.io.path.writeText
12+
import kotlin.test.Test
13+
import kotlin.test.assertFalse
14+
import kotlin.test.assertTrue
15+
16+
class AddUserAgentMetadataIntegrationTest {
17+
private val model = """
18+
@http(method: "GET", uri: "/foo")
19+
operation Foo { }
20+
"""
21+
.trimIndent()
22+
.prependNamespaceAndService(operations = listOf("Foo"))
23+
.toSmithyModel()
24+
private val settings = model.defaultSettings()
25+
26+
@Test
27+
fun testDisabled() {
28+
assertFalse(AddUserAgentMetadataIntegration().enabledForService(model, settings))
29+
}
30+
31+
@Test
32+
fun testEnabledBySystemProperty() {
33+
withSysProp {
34+
assertTrue(AddUserAgentMetadataIntegration().enabledForService(model, settings))
35+
}
36+
}
37+
38+
@Test
39+
fun testRenderProperty() {
40+
withSysProp {
41+
val ctx = model.newTestContext(integrations = listOf(AddUserAgentMetadataIntegration()))
42+
val generator = MockHttpProtocolGenerator()
43+
generator.generateProtocolClient(ctx.generationCtx)
44+
45+
ctx.generationCtx.delegator.finalize()
46+
ctx.generationCtx.delegator.flushWriters()
47+
48+
val actual = ctx.manifest.expectFileString("/src/main/kotlin/com/test/DefaultTestClient.kt")
49+
val expected = """
50+
private val extraMetadata: Map<String, String> = mapOf(
51+
"foo" to "bar",
52+
)
53+
""".replaceIndent(" ")
54+
actual.shouldContainOnlyOnceWithDiff(expected)
55+
}
56+
}
57+
58+
@Test
59+
fun testRenderInterceptor() {
60+
withSysProp {
61+
val ctx = model.newTestContext(integrations = listOf(AddUserAgentMetadataIntegration()))
62+
val generator = MockHttpProtocolGenerator()
63+
generator.generateProtocolClient(ctx.generationCtx)
64+
65+
ctx.generationCtx.delegator.finalize()
66+
ctx.generationCtx.delegator.flushWriters()
67+
68+
val actual = ctx.manifest.expectFileString("/src/main/kotlin/com/test/DefaultTestClient.kt")
69+
70+
val fooMethod = actual.lines(" override suspend fun foo(input: FooRequest): FooResponse {", " }")
71+
val expectedInterceptor = "op.interceptors.add(AddUserAgentMetadataInterceptor(extraMetadata))"
72+
fooMethod.shouldContainOnlyOnceWithDiff(expectedInterceptor)
73+
}
74+
}
75+
}
76+
77+
private inline fun withSysProp(block: () -> Unit) {
78+
val path = Files.createTempFile(null, null)
79+
try {
80+
path.writeText(""" { "foo" : "bar" } """)
81+
System.setProperty(ADD_USER_AGENT_METADATA_SYS_PROP, path.toString())
82+
block()
83+
} finally {
84+
System.clearProperty(ADD_USER_AGENT_METADATA_SYS_PROP)
85+
path.deleteIfExists()
86+
}
87+
}

codegen/smithy-aws-kotlin-codegen/src/test/kotlin/aws/sdk/kotlin/codegen/customization/s3/ContinueIntegrationTest.kt

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package aws.sdk.kotlin.codegen.customization.s3
22

3+
import aws.sdk.kotlin.codegen.testutil.lines
34
import org.junit.jupiter.api.Test
45
import software.amazon.smithy.codegen.core.SymbolProvider
56
import software.amazon.smithy.kotlin.codegen.KotlinSettings
@@ -120,16 +121,3 @@ object FooMiddleware : ProtocolMiddleware {
120121
override fun render(ctx: ProtocolGenerator.GenerationContext, op: OperationShape, writer: KotlinWriter) =
121122
fail("Unexpected call to `FooMiddleware.render")
122123
}
123-
124-
private fun String.lines(fromLine: String, toLine: String): String {
125-
val allLines = lines()
126-
127-
val fromIdx = allLines.indexOf(fromLine)
128-
assertNotEquals(-1, fromIdx, """Could not find from line "$fromLine" in all lines""")
129-
130-
val toIdxOffset = allLines.drop(fromIdx + 1).indexOf(toLine)
131-
assertNotEquals(-1, toIdxOffset, """Could not find to line "$toLine" in all lines""")
132-
133-
val toIdx = toIdxOffset + fromIdx + 1
134-
return allLines.subList(fromIdx, toIdx + 1).joinToString("\n")
135-
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package aws.sdk.kotlin.codegen.testutil
6+
7+
import kotlin.test.assertNotEquals
8+
9+
public fun String.lines(fromLine: String, toLine: String): String {
10+
val allLines = lines()
11+
12+
val fromIdx = allLines.indexOf(fromLine)
13+
assertNotEquals(-1, fromIdx, """Could not find from line "$fromLine" in all lines""")
14+
15+
val toIdxOffset = allLines.drop(fromIdx + 1).indexOf(toLine)
16+
assertNotEquals(-1, toIdxOffset, """Could not find to line "$toLine" in all lines""")
17+
18+
val toIdx = toIdxOffset + fromIdx + 1
19+
return allLines.subList(fromIdx, toIdx + 1).joinToString("\n")
20+
}

0 commit comments

Comments
 (0)