Skip to content

Commit 516da06

Browse files
authored
fix: correctly codegen paginators for items in sparse lists (#1072)
1 parent f2412eb commit 516da06

File tree

3 files changed

+145
-5
lines changed

3 files changed

+145
-5
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"id": "e43c0830-b8d0-43d0-a8bb-1e2c93b8ac7c",
3+
"type": "bugfix",
4+
"description": "Correctly codegen paginators for items in sparse lists"
5+
}

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

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,7 @@ import software.amazon.smithy.kotlin.codegen.core.defaultName
1616
import software.amazon.smithy.kotlin.codegen.core.withBlock
1717
import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration
1818
import software.amazon.smithy.kotlin.codegen.lang.KotlinTypes
19-
import software.amazon.smithy.kotlin.codegen.model.SymbolProperty
20-
import software.amazon.smithy.kotlin.codegen.model.expectShape
21-
import software.amazon.smithy.kotlin.codegen.model.hasAllOptionalMembers
22-
import software.amazon.smithy.kotlin.codegen.model.hasTrait
19+
import software.amazon.smithy.kotlin.codegen.model.*
2320
import software.amazon.smithy.kotlin.codegen.model.traits.PaginationTruncationMember
2421
import software.amazon.smithy.kotlin.codegen.utils.getOrNull
2522
import software.amazon.smithy.model.Model
@@ -267,6 +264,7 @@ private fun getItemDescriptorOrNull(paginationInfo: PaginationInfo, ctx: Codegen
267264
val itemLiteral = paginationInfo.itemsMemberPath!!.last()!!.defaultName()
268265
val itemPathLiteral = paginationInfo.itemsMemberPath.joinToString(separator = "?.") { it.defaultName() }
269266
val itemMember = ctx.model.expectShape(itemMemberId)
267+
val isSparse = itemMember.isSparse
270268
val (collectionLiteral, targetMember) = when (itemMember) {
271269
is MapShape ->
272270
ctx.symbolProvider.toSymbol(itemMember)
@@ -279,7 +277,7 @@ private fun getItemDescriptorOrNull(paginationInfo: PaginationInfo, ctx: Codegen
279277
}
280278

281279
return ItemDescriptor(
282-
collectionLiteral,
280+
collectionLiteral + if (isSparse) "?" else "",
283281
targetMember,
284282
itemLiteral,
285283
itemPathLiteral,

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

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

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

0 commit comments

Comments
 (0)