Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ local.properties
# ignore generated files
services/*/generated-src
services/*/build.gradle.kts
services/*/API.md
.kotest/
.kotlin/
*.klib
Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ allprojects {

tasks.withType<org.jetbrains.dokka.gradle.DokkaTaskPartial>().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"))
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, String> = CODE_EXAMPLES_SERVICES_MAP,
) : KotlinIntegration {
override fun enabledForService(model: Model, settings: KotlinSettings): Boolean =
codeExamples.keys.contains(
model
.expectShape<ServiceShape>(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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

naming: the function and the value have the same name, handWrittenDocsFile, which can be hard to read. Consider renaming function to getHandWrittenDocsFile

Also nit/style: you can check if the file exists inside getHandWrittenDocsFile and return null if it doesn't.

That would simplify this code section to just: getHandWrittenDocsFile(ctx)?.let { append(it.readText()) }

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<ServiceShape>(ctx.settings.service)
.getTrait<TitleTrait>()
?.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<ServiceShape>(ctx.settings.service)
.getTrait<TitleTrait>()
?.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")
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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(),
)
}
4 changes: 4 additions & 0 deletions codegen/sdk/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions services/s3/API.md → services/s3/DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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.
Loading