Skip to content

Commit 72155f7

Browse files
authored
fix: correctly codegen paginators for types which require fully-qualified names (#1249)
1 parent 462967b commit 72155f7

File tree

3 files changed

+144
-13
lines changed

3 files changed

+144
-13
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"id": "13a47747-197a-4b88-bc3b-393af5f5127a",
3+
"type": "bugfix",
4+
"description": "Correctly generate paginators for item type names which collide with other used types (e.g., an item type `com.foo.Flow` which conflicts with `kotlinx.coroutines.flow.Flow`)"
5+
}

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

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,7 @@ import software.amazon.smithy.codegen.core.CodegenException
88
import software.amazon.smithy.codegen.core.Symbol
99
import software.amazon.smithy.codegen.core.SymbolReference
1010
import software.amazon.smithy.kotlin.codegen.KotlinSettings
11-
import software.amazon.smithy.kotlin.codegen.core.CodegenContext
12-
import software.amazon.smithy.kotlin.codegen.core.ExternalTypes
13-
import software.amazon.smithy.kotlin.codegen.core.KotlinDelegator
14-
import software.amazon.smithy.kotlin.codegen.core.KotlinWriter
15-
import software.amazon.smithy.kotlin.codegen.core.defaultName
16-
import software.amazon.smithy.kotlin.codegen.core.withBlock
11+
import software.amazon.smithy.kotlin.codegen.core.*
1712
import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration
1813
import software.amazon.smithy.kotlin.codegen.lang.KotlinTypes
1914
import software.amazon.smithy.kotlin.codegen.model.*
@@ -54,7 +49,7 @@ class PaginatorGenerator : KotlinIntegration {
5449
paginatedOperations.forEach { paginatedOperation ->
5550
val paginationInfo = paginatedIndex.getPaginationInfo(service, paginatedOperation).getOrNull()
5651
?: throw CodegenException("Unexpectedly unable to get PaginationInfo from $service $paginatedOperation")
57-
val paginationItemInfo = getItemDescriptorOrNull(paginationInfo, ctx)
52+
val paginationItemInfo = getItemDescriptorOrNull(paginationInfo, ctx, writer)
5853

5954
renderPaginatorForOperation(ctx, writer, paginatedOperation, paginationInfo, paginationItemInfo)
6055
}
@@ -264,7 +259,11 @@ private data class ItemDescriptor(
264259
/**
265260
* Return an [ItemDescriptor] if model supplies, otherwise null
266261
*/
267-
private fun getItemDescriptorOrNull(paginationInfo: PaginationInfo, ctx: CodegenContext): ItemDescriptor? {
262+
private fun getItemDescriptorOrNull(
263+
paginationInfo: PaginationInfo,
264+
ctx: CodegenContext,
265+
writer: KotlinWriter,
266+
): ItemDescriptor? {
268267
val itemMemberId = paginationInfo.itemsMemberPath?.lastOrNull()?.target ?: return null
269268

270269
val itemLiteral = paginationInfo.itemsMemberPath!!.last()!!.defaultName()
@@ -273,15 +272,18 @@ private fun getItemDescriptorOrNull(paginationInfo: PaginationInfo, ctx: Codegen
273272
val isSparse = itemMember.isSparse
274273
val (collectionLiteral, targetMember) = when (itemMember) {
275274
is MapShape -> {
276-
val symbol = ctx.symbolProvider.toSymbol(itemMember)
277-
val entryExpression = symbol.expectProperty(SymbolProperty.ENTRY_EXPRESSION) as String
278-
entryExpression to itemMember
275+
val keySymbol = ctx.symbolProvider.toSymbol(itemMember.key)
276+
val valueSymbol = ctx.symbolProvider.toSymbol(itemMember.value)
277+
val valueSuffix = if (isSparse || valueSymbol.isNullable) "?" else ""
278+
val elementExpression = writer.format("Map.Entry<#T, #T#L>", keySymbol, valueSymbol, valueSuffix)
279+
elementExpression to itemMember
279280
}
280281
is CollectionShape -> {
281282
val target = ctx.model.expectShape(itemMember.member.target)
282283
val symbol = ctx.symbolProvider.toSymbol(target)
283-
val literal = symbol.name + if (symbol.isNullable || isSparse) "?" else ""
284-
literal to target
284+
val suffix = if (isSparse || symbol.isNullable) "?" else ""
285+
val elementExpression = writer.format("#T#L", symbol, suffix)
286+
elementExpression to target
285287
}
286288
else -> error("Unexpected shape type ${itemMember.type}")
287289
}

codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/PaginatorGeneratorTest.kt

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,130 @@ class PaginatorGeneratorTest {
263263
actual.shouldContainOnlyOnceWithDiff(expectedImports)
264264
}
265265

266+
@Test
267+
fun testRenderPaginatorWithItemRequiringFullName() {
268+
val testModelWithItems = """
269+
namespace com.test
270+
271+
use aws.protocols#restJson1
272+
273+
service FlowService {
274+
operations: [ListFlows]
275+
}
276+
277+
@paginated(
278+
inputToken: "Marker",
279+
outputToken: "NextMarker",
280+
pageSize: "MaxItems",
281+
items: "Flows"
282+
)
283+
@readonly
284+
@http(method: "GET", uri: "/flows", code: 200)
285+
operation ListFlows {
286+
input: ListFlowsRequest,
287+
output: ListFlowsResponse
288+
}
289+
290+
structure ListFlowsRequest {
291+
@httpQuery("FlowVersion")
292+
FlowVersion: String,
293+
@httpQuery("Marker")
294+
Marker: String,
295+
@httpQuery("MasterRegion")
296+
MasterRegion: String,
297+
@httpQuery("MaxItems")
298+
MaxItems: Integer
299+
}
300+
301+
structure ListFlowsResponse {
302+
Flows: FlowList,
303+
NextMarker: String
304+
}
305+
306+
list FlowList {
307+
member: Flow
308+
}
309+
310+
structure Flow {
311+
Name: String
312+
}
313+
""".toSmithyModel()
314+
val testContextWithItems = testModelWithItems.newTestContext("FlowService", "com.test")
315+
316+
val codegenContextWithItems = object : CodegenContext {
317+
override val model: Model = testContextWithItems.generationCtx.model
318+
override val symbolProvider: SymbolProvider = testContextWithItems.generationCtx.symbolProvider
319+
override val settings: KotlinSettings = testContextWithItems.generationCtx.settings
320+
override val protocolGenerator: ProtocolGenerator = testContextWithItems.generator
321+
override val integrations: List<KotlinIntegration> = testContextWithItems.generationCtx.integrations
322+
}
323+
324+
val unit = PaginatorGenerator()
325+
unit.writeAdditionalFiles(codegenContextWithItems, testContextWithItems.generationCtx.delegator)
326+
327+
testContextWithItems.generationCtx.delegator.flushWriters()
328+
val testManifest = testContextWithItems.generationCtx.delegator.fileManifest as MockManifest
329+
val actual = testManifest.expectFileString("src/main/kotlin/com/test/paginators/Paginators.kt")
330+
331+
val expectedCode = """
332+
/**
333+
* Paginate over [ListFlowsResponse] results.
334+
*
335+
* When this operation is called, a [kotlinx.coroutines.Flow] is created. Flows are lazy (cold) so no service
336+
* calls are made until the flow is collected. This also means there is no guarantee that the request is valid
337+
* until then. Once you start collecting the flow, the SDK will lazily load response pages by making service
338+
* calls until there are no pages left or the flow is cancelled. If there are errors in your request, you will
339+
* see the failures only after you start collection.
340+
* @param initialRequest A [ListFlowsRequest] to start pagination
341+
* @return A [kotlinx.coroutines.flow.Flow] that can collect [ListFlowsResponse]
342+
*/
343+
public fun TestClient.listFlowsPaginated(initialRequest: ListFlowsRequest = ListFlowsRequest { }): kotlinx.coroutines.flow.Flow<ListFlowsResponse> =
344+
flow {
345+
var cursor: kotlin.String? = initialRequest.marker
346+
var hasNextPage: Boolean = true
347+
348+
while (hasNextPage) {
349+
val req = initialRequest.copy {
350+
this.marker = cursor
351+
}
352+
val result = [email protected](req)
353+
cursor = result.nextMarker
354+
hasNextPage = cursor?.isNotEmpty() == true
355+
emit(result)
356+
}
357+
}
358+
359+
/**
360+
* Paginate over [ListFlowsResponse] results.
361+
*
362+
* When this operation is called, a [kotlinx.coroutines.Flow] is created. Flows are lazy (cold) so no service
363+
* calls are made until the flow is collected. This also means there is no guarantee that the request is valid
364+
* until then. Once you start collecting the flow, the SDK will lazily load response pages by making service
365+
* calls until there are no pages left or the flow is cancelled. If there are errors in your request, you will
366+
* see the failures only after you start collection.
367+
* @param block A builder block used for DSL-style invocation of the operation
368+
* @return A [kotlinx.coroutines.flow.Flow] that can collect [ListFlowsResponse]
369+
*/
370+
public fun TestClient.listFlowsPaginated(block: ListFlowsRequest.Builder.() -> Unit): kotlinx.coroutines.flow.Flow<ListFlowsResponse> =
371+
listFlowsPaginated(ListFlowsRequest.Builder().apply(block).build())
372+
373+
/**
374+
* This paginator transforms the flow returned by [listFlowsPaginated]
375+
* to access the nested member [Flow]
376+
* @return A [kotlinx.coroutines.flow.Flow] that can collect [Flow]
377+
*/
378+
@JvmName("listFlowsResponseFlow")
379+
public fun kotlinx.coroutines.flow.Flow<ListFlowsResponse>.flows(): kotlinx.coroutines.flow.Flow<Flow> =
380+
transform() { response ->
381+
response.flows?.forEach {
382+
emit(it)
383+
}
384+
}
385+
""".trimIndent()
386+
387+
actual.shouldContainOnlyOnceWithDiff(expectedCode)
388+
}
389+
266390
@Test
267391
fun testRenderPaginatorWithSparseItem() {
268392
val testModelWithItems = """

0 commit comments

Comments
 (0)