Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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,121 @@
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

/**
* Maps a services SKD ID to its code examples
Copy link
Member

Choose a reason for hiding this comment

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

nit/grammar: service's
typo: SDK

*/
private val currentCodeExamplesServices = mapOf(
Copy link
Member

Choose a reason for hiding this comment

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

naming: top-level values should be in SCREAMING_SNAKE_CASE. Also "current" is implied.

Something like CODE_EXAMPLES_SERVICES_MAP

"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",
)

/**
* Maps a services SKD ID to its handwritten module documentation file in the `resources` dir.
Copy link
Member

Choose a reason for hiding this comment

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

"service's SDK"

* The module documentation files MUST be markdown files.
*/
private val currentHandWrittenServices = mapOf(
Copy link
Member

Choose a reason for hiding this comment

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

Same comment here. "current" is implied. HAND_WRITTEN_SERVICES_MAP

"S3" to "S3.md",
)

/**
* Generates an `API.md` file that will be used as module documentation in our API ref docs.
* Some services have code examples we need to link to. 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> = currentCodeExamplesServices,
private val handWritten: Map<String, String> = currentHandWrittenServices,
) : KotlinIntegration {
override fun enabledForService(model: Model, settings: KotlinSettings): Boolean =
model.expectShape<ServiceShape>(settings.service).sdkId.let {
codeExamples.keys.contains(it) || handWritten.keys.contains(it)
}

override fun writeAdditionalFiles(ctx: CodegenContext, delegator: KotlinDelegator) {
delegator.fileManifest.writeFile(
"API.md",
generateModuleDocumentation(ctx, ctx.settings.sdkId),
)
}

internal fun generateModuleDocumentation(
ctx: CodegenContext,
sdkId: String,
) = buildString {
append(
generateBoilerPlate(ctx),
)
if (codeExamples.keys.contains(sdkId)) {
append(
generateCodeExamplesDocs(sdkId),
)
appendLine()
}
if (handWritten.keys.contains(sdkId)) {
append(
generateHandWrittenDocs(sdkId),
)
}
Copy link
Member

Choose a reason for hiding this comment

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

I think we want our hand-written content to appear above links to the code examples.

}

private fun generateBoilerPlate(ctx: CodegenContext) = buildString {
// Title must me "Module" followed by the exact module name or dokka won't render it
Copy link
Member

Choose a reason for hiding this comment

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

typo: "me" -> "be"

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 generateCodeExamplesDocs(sdkId: String) = buildString {
appendLine("## Code Examples")
appendLine("To see full code examples, see the $sdkId examples in the AWS Code Library. See ${codeExamples[sdkId]}")
Copy link
Member

@lauzadis lauzadis Mar 12, 2025

Choose a reason for hiding this comment

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

"AWS Code Library" -> "AWS code example library"
https://docs.aws.amazon.com/code-library/latest/ug/what-is-code-library.html

Copy link
Member

Choose a reason for hiding this comment

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

"see the $sdkId examples"

I think we should be using the value from the TitleTrait rather than the sdkId

TitleTrait: Provides a human-readable proper noun title to services and resources.

appendLine()
}

private fun generateHandWrittenDocs(sdkId: String): String = object {}
.javaClass
.classLoader
.getResourceAsStream("aws/sdk/kotlin/codegen/moduledocumentation/${handWritten[sdkId]}")
Copy link
Member

Choose a reason for hiding this comment

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

I didn't think we'd need to make a new directory for this, can't we keep the handwritten service docs in /services/?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can keep the handwritten docs in the service dir. It's a little awkward to have handwritten and code generated documentation combined into the same file in my opinion.

Codegen changes would modify the handwritten API.md and the changes would be committed, etc. It seems cleaner to code generate everything.

Copy link
Member

@lauzadis lauzadis Mar 13, 2025

Choose a reason for hiding this comment

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

I mean keeping them in /services/ but with a different file name. Renaming services/s3/API.md to services/s3/s3.md

Copy link
Member

Choose a reason for hiding this comment

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

Or some other consistent name, as long as it's not API.md, because you're right, that will cause a clash

?.bufferedReader()
?.readText()
?: throw Exception("Unable to read from file ${handWritten[sdkId]}")
Copy link
Member

Choose a reason for hiding this comment

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

Throw a more specific exception here

}
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
@@ -1,7 +1,3 @@
# Module s3

Amazon Simple Storage Service

## Binary Data

Binary data (streams) are represented as an [aws.smithy.kotlin.runtime.content.ByteStream].
Expand Down Expand Up @@ -35,7 +31,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 Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
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("A 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),
)
assertTrue(
ModuleDocumentationIntegration(
handWritten = mapOf("Test" to "example.md"),
).enabledForService(model, ctx.generationCtx.settings),
)
assertTrue(
ModuleDocumentationIntegration(
codeExamples = mapOf("Test" to "https://example.com"),
handWritten = mapOf("Test" to "test.md"),
).enabledForService(model, ctx.generationCtx.settings),
)
}

@Test
fun rendersBoilerplate() =
ModuleDocumentationIntegration()
.generateModuleDocumentation(
ctx.toGenerationContext(),
"Test",
)
.shouldContainOnlyOnceWithDiff(
"""
# Module test

A test service
""".trimIndent(),
)

@Test
fun rendersCodeExampleDocs() =
ModuleDocumentationIntegration(
codeExamples = mapOf("Test" to "https://example.com"),
)
.generateModuleDocumentation(
ctx.toGenerationContext(),
"Test",
)
.shouldContainOnlyOnceWithDiff(
"""
## Code Examples
To see full code examples, see the Test examples in the AWS Code Library. See https://example.com
""".trimIndent(),
)

@Test
fun rendersHandWrittenDocs() =
ModuleDocumentationIntegration(
handWritten = mapOf("Test" to "test.md"),
)
.generateModuleDocumentation(
ctx.toGenerationContext(),
"Test",
)
.shouldContainOnlyOnceWithDiff(
"""
## Subtitle

Lorem Ipsum
""".trimIndent(),
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## Subtitle

Lorem Ipsum
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
Loading