Skip to content

Commit d0a628d

Browse files
committed
wrap low-level client access in mapper
1 parent 1f2b867 commit d0a628d

File tree

8 files changed

+87
-22
lines changed

8 files changed

+87
-22
lines changed

hll/dynamodb-mapper/dynamodb-mapper-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/model/MapperPkg.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public object MapperPkg {
1212
public object Hl {
1313
public val Base: String = "aws.sdk.kotlin.hll.dynamodbmapper"
1414
public val Annotations: String = "$Base.annotations"
15+
public val Internal: String = "$Base.internal"
1516
public val Items: String = "$Base.items"
1617
public val Model: String = "$Base.model"
1718
public val Ops: String = "$Base.operations"

hll/dynamodb-mapper/dynamodb-mapper-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/model/MapperTypes.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ public object MapperTypes {
2222
public val ManualPagination: TypeRef = TypeRef(MapperPkg.Hl.Annotations, "ManualPagination")
2323
}
2424

25+
public object Internal {
26+
public val withWrappedClient: TypeRef = TypeRef(MapperPkg.Hl.Internal, "withWrappedClient")
27+
}
28+
2529
public object Items {
2630
public fun itemSchema(typeVar: String): TypeRef =
2731
TypeRef(MapperPkg.Hl.Items, "ItemSchema", genericArgs = listOf(TypeVar(typeVar)))

hll/dynamodb-mapper/dynamodb-mapper-ops-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/operations/rendering/OperationRenderer.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,12 @@ internal class OperationRenderer(
7070
}
7171
write("schema) },")
7272

73-
write("lowLevelInvoke = spec.mapper.client::#L,", operation.methodName)
73+
withBlock("lowLevelInvoke = { lowLevelReq ->", "},") {
74+
withBlock("spec.mapper.client.#T { client ->", "}", MapperTypes.Internal.withWrappedClient) {
75+
write("client.#L(lowLevelReq)", operation.methodName)
76+
}
77+
}
78+
7479
write("deserialize = #L::convert,", operation.response.lowLevelName)
7580
write("interceptors = spec.mapper.config.interceptors,")
7681
}

hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/DynamoDbMapper.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,4 +112,4 @@ public interface DynamoDbMapper {
112112
public fun DynamoDbMapper(
113113
client: DynamoDbClient,
114114
config: DynamoDbMapper.Config.Builder.() -> Unit = { },
115-
): DynamoDbMapper = DynamoDbMapperImpl.wrapping(client, DynamoDbMapper.Config(config))
115+
): DynamoDbMapper = DynamoDbMapperImpl(client, DynamoDbMapper.Config(config))

hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/internal/DynamoDbMapperImpl.kt

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,6 @@ internal data class DynamoDbMapperImpl(
1919
override val client: DynamoDbClient,
2020
override val config: DynamoDbMapper.Config,
2121
) : DynamoDbMapper {
22-
internal companion object {
23-
/**
24-
* Wraps a low-level [DynamoDbClient] to add additional features before instantiating a new
25-
* [DynamoDbMapperImpl].
26-
*/
27-
fun wrapping(client: DynamoDbClient, config: DynamoDbMapper.Config): DynamoDbMapperImpl {
28-
val wrappedClient = client.withConfig { interceptors += DdbMapperMetricInterceptor }
29-
return DynamoDbMapperImpl(wrappedClient, config)
30-
}
31-
}
3222
override fun <T, PK> getTable(name: String, schema: ItemSchema.PartitionKey<T, PK>) =
3323
tableImpl(this, name, schema)
3424

@@ -54,9 +44,12 @@ internal class MapperConfigBuilderImpl : DynamoDbMapper.Config.Builder {
5444
/**
5545
* An interceptor that emits the DynamoDB Mapper business metric
5646
*/
57-
private object DdbMapperMetricInterceptor : HttpInterceptor {
47+
private object BusinessMetricInterceptor : HttpInterceptor {
5848
override suspend fun modifyBeforeSerialization(context: RequestInterceptorContext<Any>): Any {
5949
context.executionContext.emitBusinessMetric(AwsBusinessMetric.DDB_MAPPER)
6050
return context.request
6151
}
6252
}
53+
54+
internal inline fun <T> DynamoDbClient.withWrappedClient(block: (DynamoDbClient) -> T): T =
55+
withConfig { interceptors += BusinessMetricInterceptor }.use(block)

hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/DynamoDbMapperTest.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,11 @@ class DynamoDbMapperTest : DdbLocalTest() {
5151
fun testBusinessMetricEmission() = runTest {
5252
val interceptor = MetricCapturingInterceptor()
5353

54-
val ddb = super.ddb.withConfig { interceptors += interceptor }
54+
val ddb = lowLevelAccess { withConfig { interceptors += interceptor } }
5555
interceptor.assertEmpty()
5656

5757
// No metric for low-level client
58-
ddb.scan { tableName = TABLE_NAME }
58+
lowLevelAccess { scan { tableName = TABLE_NAME } }
5959
interceptor.assertMetric(AwsBusinessMetric.DDB_MAPPER, exists = false)
6060
interceptor.reset()
6161

@@ -67,12 +67,12 @@ class DynamoDbMapperTest : DdbLocalTest() {
6767
interceptor.reset()
6868

6969
// Still no metric for low-level client (i.e., LL wasn't modified by HL)
70-
ddb.scan { tableName = TABLE_NAME }
70+
lowLevelAccess { scan { tableName = TABLE_NAME } }
7171
interceptor.assertMetric(AwsBusinessMetric.DDB_MAPPER, exists = false)
7272
interceptor.reset()
7373

7474
// Original client can be closed, mapper is unaffected
75-
ddb.close()
75+
lowLevelAccess { close() }
7676
table.scanPaginated { }.collect()
7777
interceptor.assertMetric(AwsBusinessMetric.DDB_MAPPER)
7878
}

hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/operations/PutItemTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class PutItemTest : DdbLocalTest() {
4343

4444
table.putItem { item = Item(id = "foo", value = 42) }
4545

46-
val resp = ddb.getItem(TABLE_NAME, "id" to "foo")
46+
val resp = lowLevelAccess { getItem(TABLE_NAME, "id" to "foo") }
4747

4848
val item = assertNotNull(resp.item)
4949
assertEquals("foo", item["id"]?.asSOrNull())

hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/testutils/DdbLocalTest.kt

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,19 @@ import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbMapper
88
import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema
99
import aws.sdk.kotlin.hll.dynamodbmapper.model.Item
1010
import aws.sdk.kotlin.runtime.auth.credentials.StaticCredentialsProvider
11+
import aws.sdk.kotlin.runtime.http.interceptors.AwsBusinessMetric
1112
import aws.sdk.kotlin.services.dynamodb.DynamoDbClient
1213
import aws.sdk.kotlin.services.dynamodb.deleteTable
1314
import aws.sdk.kotlin.services.dynamodb.waiters.waitUntilTableNotExists
15+
import aws.smithy.kotlin.runtime.client.ProtocolRequestInterceptorContext
16+
import aws.smithy.kotlin.runtime.http.interceptors.HttpInterceptor
17+
import aws.smithy.kotlin.runtime.http.request.HttpRequest
1418
import aws.smithy.kotlin.runtime.net.Host
1519
import aws.smithy.kotlin.runtime.net.Scheme
1620
import aws.smithy.kotlin.runtime.net.url.Url
1721
import io.kotest.core.spec.style.AnnotationSpec
1822
import kotlinx.coroutines.runBlocking
23+
import kotlin.test.assertContains
1924
import kotlin.test.assertEquals
2025
import kotlin.test.assertNotNull
2126

@@ -41,6 +46,9 @@ abstract class DdbLocalTest : AnnotationSpec() {
4146
}
4247
}
4348

49+
private val requests = mutableListOf<HttpRequest>()
50+
private val requestInterceptor = RequestCapturingInterceptor(this@DdbLocalTest.requests)
51+
4452
private val ddbHolder = lazy {
4553
DynamoDbClient {
4654
endpointUrl = Url {
@@ -55,15 +63,20 @@ abstract class DdbLocalTest : AnnotationSpec() {
5563
accessKeyId = "DUMMY"
5664
secretAccessKey = "DUMMY"
5765
}
66+
67+
interceptors += requestInterceptor
5868
}
5969
}
6070

6171
/**
6272
* An instance of a low-level [DynamoDbClient] utilizing the DynamoDB Local instance which may be used for setting
6373
* up or verifying various mapper tests. If this is the first time accessing the value, the client will be
6474
* initialized.
75+
*
76+
* **Important**: This low-level client should only be accessed via [lowLevelAccess] to ensure that User-Agent
77+
* header verification succeeds.
6578
*/
66-
val ddb by ddbHolder
79+
private val ddb by ddbHolder
6780

6881
private val tempTables = mutableListOf<String>()
6982

@@ -95,9 +108,11 @@ abstract class DdbLocalTest : AnnotationSpec() {
95108
lsis: Map<String, ItemSchema<*>>,
96109
items: List<Item>,
97110
) {
98-
ddb.createTable(name, schema, gsis, lsis)
99-
tempTables += name
100-
ddb.putItems(name, items)
111+
lowLevelAccess {
112+
createTable(name, schema, gsis, lsis)
113+
tempTables += name
114+
putItems(name, items)
115+
}
101116
}
102117

103118
/**
@@ -109,6 +124,43 @@ abstract class DdbLocalTest : AnnotationSpec() {
109124
config: DynamoDbMapper.Config.Builder.() -> Unit = { },
110125
) = DynamoDbMapper(ddb ?: this.ddb, config)
111126

127+
@BeforeEach
128+
fun initializeTest() {
129+
requestInterceptor.enabled = true
130+
}
131+
132+
/**
133+
* Executes requests on a low-level [DynamoDbClient] and _does not_ log any requests executed in [block]. (This
134+
* skips verifying that low-level requests contain the [AwsBusinessMetric.DDB_MAPPER] metric.)
135+
*/
136+
protected suspend fun <T> lowLevelAccess(block: suspend DynamoDbClient.() -> T): T {
137+
requestInterceptor.enabled = false
138+
return block(ddb).also { requestInterceptor.enabled = true }
139+
}
140+
141+
@AfterEach
142+
fun postVerify() {
143+
requests.forEach { req ->
144+
val uaString = requireNotNull(req.headers["User-Agent"]) {
145+
"Missing User-Agent header for request $req"
146+
}
147+
148+
val components = uaString.split(" ")
149+
150+
val metricsComponent = requireNotNull(components.find { it.startsWith("m/") }) {
151+
"""User-Agent header "$uaString" doesn't contain business metrics for request $req"""
152+
}
153+
154+
val metrics = metricsComponent.removePrefix("m/").split(",")
155+
156+
assertContains(
157+
metrics,
158+
AwsBusinessMetric.DDB_MAPPER.identifier,
159+
"""Mapper business metric not present in User-Agent header "$uaString" for request $req""",
160+
)
161+
}
162+
}
163+
112164
@AfterAll
113165
fun cleanUp() {
114166
if (ddbHolder.isInitialized()) {
@@ -123,3 +175,13 @@ abstract class DdbLocalTest : AnnotationSpec() {
123175
}
124176
}
125177
}
178+
179+
private class RequestCapturingInterceptor(val requests: MutableList<HttpRequest>) : HttpInterceptor {
180+
var enabled = true
181+
182+
override fun readBeforeTransmit(context: ProtocolRequestInterceptorContext<Any, HttpRequest>) {
183+
if (enabled) {
184+
requests += context.protocolRequest
185+
}
186+
}
187+
}

0 commit comments

Comments
 (0)