diff --git a/.gitignore b/.gitignore index 6722d7e3c11..200bbaf971a 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ local.properties # ignore generated files services/*/generated-src services/*/build.gradle.kts +services/*/API.md .kotest/ .kotlin/ *.klib diff --git a/build.gradle.kts b/build.gradle.kts index 87e685c1fab..95f727bc5d1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -67,7 +67,7 @@ allprojects { tasks.withType().configureEach { // each module can include their own top-level module documentation - // see https://kotlinlang.org/docs/kotlin-doc.html#module-and-package-documentation + // see https://kotlinlang.org/docs/dokka-module-and-package-docs.html if (project.file("API.md").exists()) { dokkaSourceSets.configureEach { includes.from(project.file("API.md")) diff --git a/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/ModuleDocumentationIntegration.kt b/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/ModuleDocumentationIntegration.kt new file mode 100644 index 00000000000..268da668c20 --- /dev/null +++ b/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/ModuleDocumentationIntegration.kt @@ -0,0 +1,126 @@ +package aws.sdk.kotlin.codegen + +import software.amazon.smithy.kotlin.codegen.KotlinSettings +import software.amazon.smithy.kotlin.codegen.core.CodegenContext +import software.amazon.smithy.kotlin.codegen.core.KotlinDelegator +import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration +import software.amazon.smithy.kotlin.codegen.model.expectShape +import software.amazon.smithy.kotlin.codegen.model.getTrait +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.shapes.ServiceShape +import software.amazon.smithy.model.traits.TitleTrait +import java.io.File + +/** + * Maps a service's SDK ID to its code examples + */ +private val CODE_EXAMPLES_SERVICES_MAP = mapOf( + "API Gateway" to "https://docs.aws.amazon.com/code-library/latest/ug/kotlin_1_api-gateway_code_examples.html", + "Auto Scaling" to "https://docs.aws.amazon.com/code-library/latest/ug/kotlin_1_auto-scaling_code_examples.html", + "Bedrock" to "https://docs.aws.amazon.com/code-library/latest/ug/kotlin_1_bedrock_code_examples.html", + "CloudWatch" to "https://docs.aws.amazon.com/code-library/latest/ug/kotlin_1_cloudwatch_code_examples.html", + "Comprehend" to "https://docs.aws.amazon.com/code-library/latest/ug/kotlin_1_comprehend_code_examples.html", + "DynamoDB" to "https://docs.aws.amazon.com/code-library/latest/ug/kotlin_1_dynamodb_code_examples.html", + "EC2" to "https://docs.aws.amazon.com/code-library/latest/ug/kotlin_1_ec2_code_examples.html", + "ECR" to "https://docs.aws.amazon.com/code-library/latest/ug/kotlin_1_ecr_code_examples.html", + "OpenSearch" to "https://docs.aws.amazon.com/code-library/latest/ug/kotlin_1_opensearch_code_examples.html", + "EventBridge" to "https://docs.aws.amazon.com/code-library/latest/ug/kotlin_1_eventbridge_code_examples.html", + "Glue" to "https://docs.aws.amazon.com/code-library/latest/ug/kotlin_1_glue_code_examples.html", + "IAM" to "https://docs.aws.amazon.com/code-library/latest/ug/kotlin_1_iam_code_examples.html", + "IoT" to "https://docs.aws.amazon.com/code-library/latest/ug/kotlin_1_iot_code_examples.html ", + "Keyspaces" to "https://docs.aws.amazon.com/code-library/latest/ug/kotlin_1_keyspaces_code_examples.html", + "KMS" to "https://docs.aws.amazon.com/code-library/latest/ug/kotlin_1_kms_code_examples.html", + "Lambda" to "https://docs.aws.amazon.com/code-library/latest/ug/kotlin_1_lambda_code_examples.html", + "MediaConvert" to "https://docs.aws.amazon.com/code-library/latest/ug/kotlin_1_mediaconvert_code_examples.html", + "Pinpoint" to "https://docs.aws.amazon.com/code-library/latest/ug/kotlin_1_pinpoint_code_examples.html", + "RDS" to "https://docs.aws.amazon.com/code-library/latest/ug/kotlin_1_rds_code_examples.html", + "Redshift" to "https://docs.aws.amazon.com/code-library/latest/ug/kotlin_1_redshift_code_examples.html", + "Rekognition" to "https://docs.aws.amazon.com/code-library/latest/ug/kotlin_1_rekognition_code_examples.html", +) + +/** + * Generates an `API.md` file that will be used as module documentation in our API ref docs. + * Some services have code example documentation we need to generate. Others have handwritten documentation. + * The integration renders both into the `API.md` file. + * + * See: https://kotlinlang.org/docs/dokka-module-and-package-docs.html + * + * See: https://github.com/awslabs/aws-sdk-kotlin/blob/0581f5c5eeaa14dcd8af4ea0dfc088b1057f5ba5/build.gradle.kts#L68-L75 + */ +class ModuleDocumentationIntegration( + private val codeExamples: Map = CODE_EXAMPLES_SERVICES_MAP, +) : KotlinIntegration { + override fun enabledForService(model: Model, settings: KotlinSettings): Boolean = + codeExamples.keys.contains( + model + .expectShape(settings.service) + .sdkId, + ) || + handWrittenDocsFile(settings).exists() + + override fun writeAdditionalFiles(ctx: CodegenContext, delegator: KotlinDelegator) { + delegator.fileManifest.writeFile( + "API.md", + generateModuleDocumentation(ctx), + ) + } + + internal fun generateModuleDocumentation( + ctx: CodegenContext, + ) = buildString { + val handWrittenDocsFile = handWrittenDocsFile(ctx.settings) + if (handWrittenDocsFile.exists()) { + append( + handWrittenDocsFile.readText(), + ) + appendLine() + } + if (codeExamples.keys.contains(ctx.settings.sdkId)) { + if (!handWrittenDocsFile.exists()) { + append( + boilerPlate(ctx), + ) + } + append( + codeExamplesDocs(ctx), + ) + } + } + + private fun boilerPlate(ctx: CodegenContext) = buildString { + // Title must be "Module" followed by the exact module name or dokka won't render it + appendLine("# Module ${ctx.settings.pkg.name.split(".").last()}") + appendLine() + ctx + .model + .expectShape(ctx.settings.service) + .getTrait() + ?.value + ?.let { + appendLine(it) + appendLine() + } + } + + private fun codeExamplesDocs(ctx: CodegenContext) = buildString { + val sdkId = ctx.settings.sdkId + val codeExampleLink = codeExamples[sdkId] + val title = ctx + .model + .expectShape(ctx.settings.service) + .getTrait() + ?.value + + appendLine("## Code Examples") + append("To see full code examples, see the ${title ?: sdkId} examples in the AWS code example library. ") + appendLine("See $codeExampleLink") + appendLine() + } +} + +private fun handWrittenDocsFile(settings: KotlinSettings): File { + val sdkRootDir = System.getProperty("user.dir") + val serviceDir = "$sdkRootDir/services/${settings.pkg.name.split(".").last()}" + + return File("$serviceDir/DOCS.md") +} diff --git a/codegen/aws-sdk-codegen/src/main/resources/META-INF/services/software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration b/codegen/aws-sdk-codegen/src/main/resources/META-INF/services/software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration index e5dfdcff080..195fc1f017c 100644 --- a/codegen/aws-sdk-codegen/src/main/resources/META-INF/services/software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration +++ b/codegen/aws-sdk-codegen/src/main/resources/META-INF/services/software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration @@ -48,3 +48,4 @@ aws.sdk.kotlin.codegen.smoketests.SmokeTestsDenyListIntegration aws.sdk.kotlin.codegen.smoketests.testing.SmokeTestSuccessHttpEngineIntegration aws.sdk.kotlin.codegen.smoketests.testing.SmokeTestFailHttpEngineIntegration aws.sdk.kotlin.codegen.customization.AwsQueryModeCustomization +aws.sdk.kotlin.codegen.ModuleDocumentationIntegration diff --git a/codegen/aws-sdk-codegen/src/test/kotlin/aws/sdk/kotlin/codegen/ModuleDocumentationIntegrationTest.kt b/codegen/aws-sdk-codegen/src/test/kotlin/aws/sdk/kotlin/codegen/ModuleDocumentationIntegrationTest.kt new file mode 100644 index 00000000000..d3911776bb7 --- /dev/null +++ b/codegen/aws-sdk-codegen/src/test/kotlin/aws/sdk/kotlin/codegen/ModuleDocumentationIntegrationTest.kt @@ -0,0 +1,67 @@ +package aws.sdk.kotlin.codegen + +import software.amazon.smithy.kotlin.codegen.test.newTestContext +import software.amazon.smithy.kotlin.codegen.test.shouldContainOnlyOnceWithDiff +import software.amazon.smithy.kotlin.codegen.test.toGenerationContext +import software.amazon.smithy.kotlin.codegen.test.toSmithyModel +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +private val model = """ + ${"$"}version: "2" + + namespace com.test + + use aws.api#service + + @service(sdkId: "Test") + @title("Test Service") + service Test { + version: "1.0.0", + operations: [] + } + """.toSmithyModel() + +val ctx = model.newTestContext("Test") + +class ModuleDocumentationIntegrationTest { + @Test + fun integrationIsAppliedCorrectly() { + assertFalse( + ModuleDocumentationIntegration().enabledForService(model, ctx.generationCtx.settings), + ) + assertTrue( + ModuleDocumentationIntegration( + codeExamples = mapOf("Test" to "https://example.com"), + ).enabledForService(model, ctx.generationCtx.settings), + ) + } + + @Test + fun rendersBoilerplate() = + ModuleDocumentationIntegration( + codeExamples = mapOf("Test" to "https://example.com"), + ) + .generateModuleDocumentation(ctx.toGenerationContext()) + .shouldContainOnlyOnceWithDiff( + """ + # Module test + + Test Service + """.trimIndent(), + ) + + @Test + fun rendersCodeExampleDocs() = + ModuleDocumentationIntegration( + codeExamples = mapOf("Test" to "https://example.com"), + ) + .generateModuleDocumentation(ctx.toGenerationContext()) + .shouldContainOnlyOnceWithDiff( + """ + ## Code Examples + To see full code examples, see the Test Service examples in the AWS code example library. See https://example.com + """.trimIndent(), + ) +} diff --git a/codegen/sdk/build.gradle.kts b/codegen/sdk/build.gradle.kts index 2ef66603b3b..575453a4dcc 100644 --- a/codegen/sdk/build.gradle.kts +++ b/codegen/sdk/build.gradle.kts @@ -180,6 +180,10 @@ val stageSdks = tasks.register("stageSdks") { from("$projectionOutputDir/build.gradle.kts") into(it.destinationDir) } + copy { + from("$projectionOutputDir/API.md") + into(it.destinationDir) + } } } } diff --git a/services/s3/API.md b/services/s3/DOCS.md similarity index 94% rename from services/s3/API.md rename to services/s3/DOCS.md index 14e04f78516..14fbd19362f 100644 --- a/services/s3/API.md +++ b/services/s3/DOCS.md @@ -35,7 +35,7 @@ See [aws.sdk.kotlin.services.s3.model.GetObjectResponse] ## Streaming Responses Streaming responses are scoped to a `block`. Instead of returning the response directly, you must pass a lambda which is given access to the response (and the underlying stream). -The result of the call is whatever the lambda returns. +The result of the call is whatever the lambda returns. See [aws.sdk.kotlin.services.s3.S3Client.getObject] @@ -58,4 +58,4 @@ println("wrote $contentSize bytes to $path") ``` -This scoped response simplifies lifetime management for both the caller and the runtime. +This scoped response simplifies lifetime management for both the caller and the runtime. \ No newline at end of file