diff --git a/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinWriter.kt b/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinWriter.kt index e2a10c813..8c8e067c8 100644 --- a/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinWriter.kt +++ b/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinWriter.kt @@ -379,9 +379,24 @@ private fun String.stripAll(stripList: List): String { } // Remove whitespace from the beginning and end of each line of documentation -// Remove blank lines +// Remove leading, trailing, and consecutive blank lines private fun formatDocumentation(doc: String, lineSeparator: String = "\n") = doc .split('\n') // Break the doc into lines - .filter { it.isNotBlank() } // Remove empty lines + .dropWhile { it.isBlank() } // Drop leading blank lines + .dropLastWhile { it.isBlank() } // Drop trailing blank lines + .dropConsecutive { it.isBlank() } // Remove consecutive empty lines .joinToString(separator = lineSeparator) { it.trim() } // Trim line + +/** + * Filters out consecutive items matching the given [predicate]. + */ +private fun List.dropConsecutive(predicate: (T) -> Boolean) = + windowed(2, partialWindows = true) + .flatMap { window -> + if (predicate(window.first()) && window.first() == window.elementAtOrNull(1)) { + listOf() + } else { + listOf(window.first()) + } + } diff --git a/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/PaginatorGenerator.kt b/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/PaginatorGenerator.kt index bcf27b533..69961c744 100644 --- a/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/PaginatorGenerator.kt +++ b/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/PaginatorGenerator.kt @@ -15,6 +15,7 @@ import software.amazon.smithy.kotlin.codegen.core.KotlinWriter import software.amazon.smithy.kotlin.codegen.core.defaultName import software.amazon.smithy.kotlin.codegen.core.withBlock import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration +import software.amazon.smithy.kotlin.codegen.lang.KotlinTypes import software.amazon.smithy.kotlin.codegen.model.SymbolProperty import software.amazon.smithy.kotlin.codegen.model.expectShape import software.amazon.smithy.kotlin.codegen.model.hasTrait @@ -108,36 +109,37 @@ class PaginatorGenerator : KotlinIntegration { } val markerLiteral = paginationInfo.inputTokenMember.defaultName() + val docBody = """ + Paginate over [${outputSymbol.name}] results. + + When this operation is called, a [kotlinx.coroutines.Flow] is created. Flows are lazy (cold) so no service + calls are made until the flow is collected. This also means there is no guarantee that the request is valid + until then. Once you start collecting the flow, the SDK will lazily load response pages by making service + calls until there are no pages left or the flow is cancelled. If there are errors in your request, you will + see the failures only after you start collection. + """.trimIndent() + val docReturn = "@return A [kotlinx.coroutines.flow.Flow] that can collect [${outputSymbol.name}]" + writer.write("") - writer.dokka( - """ - Paginate over [${outputSymbol.name}] results. - When this operation is called, a [kotlinx.coroutines.Flow] is created. Flows are lazy (cold) so no service calls are - made until the flow is collected. This also means there is no guarantee that the request is valid until then. Once - you start collecting the flow, the SDK will lazily load response pages by making service calls until there are no - pages left or the flow is cancelled. If there are errors in your request, you will see the failures only after you start - collection. - @param initialRequest A [${inputSymbol.name}] to start pagination - @return A [kotlinx.coroutines.flow.Flow] that can collect [${outputSymbol.name}] - """.trimIndent() - ) writer - .addImport(ExternalTypes.KotlinxCoroutines.Flow) - .addImport(ExternalTypes.KotlinxCoroutines.FlowGenerator) - .addImport(serviceSymbol) - .addImport(inputSymbol) - .addImport(outputSymbol) - .addImport(cursorSymbol) + .dokka( + """ + $docBody + @param initialRequest A [${inputSymbol.name}] to start pagination + $docReturn + """.trimIndent() + ) .addImportReferences(cursorSymbol, SymbolReference.ContextOption.DECLARE) .withBlock( - "fun #T.#LPaginated(initialRequest: #T): Flow<#T> =", + "fun #T.#LPaginated(initialRequest: #T): #T<#T> =", "", serviceSymbol, operationShape.defaultName(), inputSymbol, - outputSymbol + ExternalTypes.KotlinxCoroutines.Flow, + outputSymbol, ) { - withBlock("flow {", "}") { + withBlock("#T {", "}", ExternalTypes.KotlinxCoroutines.FlowGenerator) { write("var cursor: #F = null", cursorSymbol) write("var isFirstPage: Boolean = true") write("") @@ -155,6 +157,28 @@ class PaginatorGenerator : KotlinIntegration { } } } + + writer.write("") + writer + .dokka( + """ + $docBody + @param block A builder block used for DSL-style invocation of the operation + $docReturn + """.trimIndent() + ) + .withBlock( + "fun #T.#LPaginated(block: #T.Builder.() -> #T): #T<#T> =", + "", + serviceSymbol, + operationShape.defaultName(), + inputSymbol, + KotlinTypes.Unit, + ExternalTypes.KotlinxCoroutines.Flow, + outputSymbol, + ) { + write("#LPaginated(#T.Builder().apply(block).build())", operationShape.defaultName(), inputSymbol) + } } // Generate a paginator that iterates over the model-specified item diff --git a/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/ClientConfigGeneratorTest.kt b/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/ClientConfigGeneratorTest.kt index c12fd8ed3..d1c9ee12e 100644 --- a/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/ClientConfigGeneratorTest.kt +++ b/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/ClientConfigGeneratorTest.kt @@ -74,7 +74,9 @@ class Config private constructor(builder: Builder): HttpClientConfig, Idempotenc /** * Configure events that will be logged. By default clients will not output * raw requests or responses. Use this setting to opt-in to additional debug logging. + * * This can be used to configure logging of requests, responses, retries, etc of SDK clients. + * * **NOTE**: Logging of raw requests or responses may leak sensitive information! It may also have * performance considerations when dumping the request/response body. This is primarily a tool for * debug purposes. diff --git a/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/PaginatorGeneratorTest.kt b/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/PaginatorGeneratorTest.kt index 327ec5cd4..bc1b05aa1 100644 --- a/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/PaginatorGeneratorTest.kt +++ b/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/PaginatorGeneratorTest.kt @@ -142,11 +142,12 @@ class PaginatorGeneratorTest { val expected = """ /** * Paginate over [ListFunctionsResponse] results. - * When this operation is called, a [kotlinx.coroutines.Flow] is created. Flows are lazy (cold) so no service calls are - * made until the flow is collected. This also means there is no guarantee that the request is valid until then. Once - * you start collecting the flow, the SDK will lazily load response pages by making service calls until there are no - * pages left or the flow is cancelled. If there are errors in your request, you will see the failures only after you start - * collection. + * + * When this operation is called, a [kotlinx.coroutines.Flow] is created. Flows are lazy (cold) so no service + * calls are made until the flow is collected. This also means there is no guarantee that the request is valid + * until then. Once you start collecting the flow, the SDK will lazily load response pages by making service + * calls until there are no pages left or the flow is cancelled. If there are errors in your request, you will + * see the failures only after you start collection. * @param initialRequest A [ListFunctionsRequest] to start pagination * @return A [kotlinx.coroutines.flow.Flow] that can collect [ListFunctionsResponse] */ @@ -165,6 +166,20 @@ class PaginatorGeneratorTest { emit(result) } } + + /** + * Paginate over [ListFunctionsResponse] results. + * + * When this operation is called, a [kotlinx.coroutines.Flow] is created. Flows are lazy (cold) so no service + * calls are made until the flow is collected. This also means there is no guarantee that the request is valid + * until then. Once you start collecting the flow, the SDK will lazily load response pages by making service + * calls until there are no pages left or the flow is cancelled. If there are errors in your request, you will + * see the failures only after you start collection. + * @param block A builder block used for DSL-style invocation of the operation + * @return A [kotlinx.coroutines.flow.Flow] that can collect [ListFunctionsResponse] + */ + fun TestClient.listFunctionsPaginated(block: ListFunctionsRequest.Builder.() -> Unit): Flow = + listFunctionsPaginated(ListFunctionsRequest.Builder().apply(block).build()) """.trimIndent() actual.shouldContainOnlyOnceWithDiff(expected) @@ -182,11 +197,12 @@ class PaginatorGeneratorTest { val expectedCode = """ /** * Paginate over [ListFunctionsResponse] results. - * When this operation is called, a [kotlinx.coroutines.Flow] is created. Flows are lazy (cold) so no service calls are - * made until the flow is collected. This also means there is no guarantee that the request is valid until then. Once - * you start collecting the flow, the SDK will lazily load response pages by making service calls until there are no - * pages left or the flow is cancelled. If there are errors in your request, you will see the failures only after you start - * collection. + * + * When this operation is called, a [kotlinx.coroutines.Flow] is created. Flows are lazy (cold) so no service + * calls are made until the flow is collected. This also means there is no guarantee that the request is valid + * until then. Once you start collecting the flow, the SDK will lazily load response pages by making service + * calls until there are no pages left or the flow is cancelled. If there are errors in your request, you will + * see the failures only after you start collection. * @param initialRequest A [ListFunctionsRequest] to start pagination * @return A [kotlinx.coroutines.flow.Flow] that can collect [ListFunctionsResponse] */ @@ -206,6 +222,20 @@ class PaginatorGeneratorTest { } } + /** + * Paginate over [ListFunctionsResponse] results. + * + * When this operation is called, a [kotlinx.coroutines.Flow] is created. Flows are lazy (cold) so no service + * calls are made until the flow is collected. This also means there is no guarantee that the request is valid + * until then. Once you start collecting the flow, the SDK will lazily load response pages by making service + * calls until there are no pages left or the flow is cancelled. If there are errors in your request, you will + * see the failures only after you start collection. + * @param block A builder block used for DSL-style invocation of the operation + * @return A [kotlinx.coroutines.flow.Flow] that can collect [ListFunctionsResponse] + */ + fun TestClient.listFunctionsPaginated(block: ListFunctionsRequest.Builder.() -> Unit): Flow = + listFunctionsPaginated(ListFunctionsRequest.Builder().apply(block).build()) + /** * This paginator transforms the flow returned by [listFunctionsPaginated] * to access the nested member [FunctionConfiguration]