Skip to content

Commit 4c54b2a

Browse files
authored
feat: link api ref docs to examples (#1550)
1 parent 4d071f3 commit 4c54b2a

File tree

7 files changed

+202
-3
lines changed

7 files changed

+202
-3
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ local.properties
1616
# ignore generated files
1717
services/*/generated-src
1818
services/*/build.gradle.kts
19+
services/*/API.md
1920
.kotest/
2021
.kotlin/
2122
*.klib

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ allprojects {
6767

6868
tasks.withType<org.jetbrains.dokka.gradle.DokkaTaskPartial>().configureEach {
6969
// each module can include their own top-level module documentation
70-
// see https://kotlinlang.org/docs/kotlin-doc.html#module-and-package-documentation
70+
// see https://kotlinlang.org/docs/dokka-module-and-package-docs.html
7171
if (project.file("API.md").exists()) {
7272
dokkaSourceSets.configureEach {
7373
includes.from(project.file("API.md"))
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package aws.sdk.kotlin.codegen
2+
3+
import software.amazon.smithy.kotlin.codegen.KotlinSettings
4+
import software.amazon.smithy.kotlin.codegen.core.CodegenContext
5+
import software.amazon.smithy.kotlin.codegen.core.KotlinDelegator
6+
import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration
7+
import software.amazon.smithy.kotlin.codegen.model.expectShape
8+
import software.amazon.smithy.kotlin.codegen.model.getTrait
9+
import software.amazon.smithy.model.Model
10+
import software.amazon.smithy.model.shapes.ServiceShape
11+
import software.amazon.smithy.model.traits.TitleTrait
12+
import java.io.File
13+
14+
/**
15+
* Maps a service's SDK ID to its code examples
16+
*/
17+
private val CODE_EXAMPLES_SERVICES_MAP = mapOf(
18+
"API Gateway" to "https://docs.aws.amazon.com/code-library/latest/ug/kotlin_1_api-gateway_code_examples.html",
19+
"Auto Scaling" to "https://docs.aws.amazon.com/code-library/latest/ug/kotlin_1_auto-scaling_code_examples.html",
20+
"Bedrock" to "https://docs.aws.amazon.com/code-library/latest/ug/kotlin_1_bedrock_code_examples.html",
21+
"CloudWatch" to "https://docs.aws.amazon.com/code-library/latest/ug/kotlin_1_cloudwatch_code_examples.html",
22+
"Comprehend" to "https://docs.aws.amazon.com/code-library/latest/ug/kotlin_1_comprehend_code_examples.html",
23+
"DynamoDB" to "https://docs.aws.amazon.com/code-library/latest/ug/kotlin_1_dynamodb_code_examples.html",
24+
"EC2" to "https://docs.aws.amazon.com/code-library/latest/ug/kotlin_1_ec2_code_examples.html",
25+
"ECR" to "https://docs.aws.amazon.com/code-library/latest/ug/kotlin_1_ecr_code_examples.html",
26+
"OpenSearch" to "https://docs.aws.amazon.com/code-library/latest/ug/kotlin_1_opensearch_code_examples.html",
27+
"EventBridge" to "https://docs.aws.amazon.com/code-library/latest/ug/kotlin_1_eventbridge_code_examples.html",
28+
"Glue" to "https://docs.aws.amazon.com/code-library/latest/ug/kotlin_1_glue_code_examples.html",
29+
"IAM" to "https://docs.aws.amazon.com/code-library/latest/ug/kotlin_1_iam_code_examples.html",
30+
"IoT" to "https://docs.aws.amazon.com/code-library/latest/ug/kotlin_1_iot_code_examples.html ",
31+
"Keyspaces" to "https://docs.aws.amazon.com/code-library/latest/ug/kotlin_1_keyspaces_code_examples.html",
32+
"KMS" to "https://docs.aws.amazon.com/code-library/latest/ug/kotlin_1_kms_code_examples.html",
33+
"Lambda" to "https://docs.aws.amazon.com/code-library/latest/ug/kotlin_1_lambda_code_examples.html",
34+
"MediaConvert" to "https://docs.aws.amazon.com/code-library/latest/ug/kotlin_1_mediaconvert_code_examples.html",
35+
"Pinpoint" to "https://docs.aws.amazon.com/code-library/latest/ug/kotlin_1_pinpoint_code_examples.html",
36+
"RDS" to "https://docs.aws.amazon.com/code-library/latest/ug/kotlin_1_rds_code_examples.html",
37+
"Redshift" to "https://docs.aws.amazon.com/code-library/latest/ug/kotlin_1_redshift_code_examples.html",
38+
"Rekognition" to "https://docs.aws.amazon.com/code-library/latest/ug/kotlin_1_rekognition_code_examples.html",
39+
)
40+
41+
/**
42+
* Generates an `API.md` file that will be used as module documentation in our API ref docs.
43+
* Some services have code example documentation we need to generate. Others have handwritten documentation.
44+
* The integration renders both into the `API.md` file.
45+
*
46+
* See: https://kotlinlang.org/docs/dokka-module-and-package-docs.html
47+
*
48+
* See: https://github.com/awslabs/aws-sdk-kotlin/blob/0581f5c5eeaa14dcd8af4ea0dfc088b1057f5ba5/build.gradle.kts#L68-L75
49+
*/
50+
class ModuleDocumentationIntegration(
51+
private val codeExamples: Map<String, String> = CODE_EXAMPLES_SERVICES_MAP,
52+
) : KotlinIntegration {
53+
override fun enabledForService(model: Model, settings: KotlinSettings): Boolean =
54+
codeExamples.keys.contains(
55+
model
56+
.expectShape<ServiceShape>(settings.service)
57+
.sdkId,
58+
) ||
59+
handWrittenDocsFile(settings).exists()
60+
61+
override fun writeAdditionalFiles(ctx: CodegenContext, delegator: KotlinDelegator) {
62+
delegator.fileManifest.writeFile(
63+
"API.md",
64+
generateModuleDocumentation(ctx),
65+
)
66+
}
67+
68+
internal fun generateModuleDocumentation(
69+
ctx: CodegenContext,
70+
) = buildString {
71+
val handWrittenDocsFile = handWrittenDocsFile(ctx.settings)
72+
if (handWrittenDocsFile.exists()) {
73+
append(
74+
handWrittenDocsFile.readText(),
75+
)
76+
appendLine()
77+
}
78+
if (codeExamples.keys.contains(ctx.settings.sdkId)) {
79+
if (!handWrittenDocsFile.exists()) {
80+
append(
81+
boilerPlate(ctx),
82+
)
83+
}
84+
append(
85+
codeExamplesDocs(ctx),
86+
)
87+
}
88+
}
89+
90+
private fun boilerPlate(ctx: CodegenContext) = buildString {
91+
// Title must be "Module" followed by the exact module name or dokka won't render it
92+
appendLine("# Module ${ctx.settings.pkg.name.split(".").last()}")
93+
appendLine()
94+
ctx
95+
.model
96+
.expectShape<ServiceShape>(ctx.settings.service)
97+
.getTrait<TitleTrait>()
98+
?.value
99+
?.let {
100+
appendLine(it)
101+
appendLine()
102+
}
103+
}
104+
105+
private fun codeExamplesDocs(ctx: CodegenContext) = buildString {
106+
val sdkId = ctx.settings.sdkId
107+
val codeExampleLink = codeExamples[sdkId]
108+
val title = ctx
109+
.model
110+
.expectShape<ServiceShape>(ctx.settings.service)
111+
.getTrait<TitleTrait>()
112+
?.value
113+
114+
appendLine("## Code Examples")
115+
append("To see full code examples, see the ${title ?: sdkId} examples in the AWS code example library. ")
116+
appendLine("See $codeExampleLink")
117+
appendLine()
118+
}
119+
}
120+
121+
private fun handWrittenDocsFile(settings: KotlinSettings): File {
122+
val sdkRootDir = System.getProperty("user.dir")
123+
val serviceDir = "$sdkRootDir/services/${settings.pkg.name.split(".").last()}"
124+
125+
return File("$serviceDir/DOCS.md")
126+
}

codegen/aws-sdk-codegen/src/main/resources/META-INF/services/software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,4 @@ aws.sdk.kotlin.codegen.smoketests.SmokeTestsDenyListIntegration
4848
aws.sdk.kotlin.codegen.smoketests.testing.SmokeTestSuccessHttpEngineIntegration
4949
aws.sdk.kotlin.codegen.smoketests.testing.SmokeTestFailHttpEngineIntegration
5050
aws.sdk.kotlin.codegen.customization.AwsQueryModeCustomization
51+
aws.sdk.kotlin.codegen.ModuleDocumentationIntegration
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package aws.sdk.kotlin.codegen
2+
3+
import software.amazon.smithy.kotlin.codegen.test.newTestContext
4+
import software.amazon.smithy.kotlin.codegen.test.shouldContainOnlyOnceWithDiff
5+
import software.amazon.smithy.kotlin.codegen.test.toGenerationContext
6+
import software.amazon.smithy.kotlin.codegen.test.toSmithyModel
7+
import kotlin.test.Test
8+
import kotlin.test.assertFalse
9+
import kotlin.test.assertTrue
10+
11+
private val model = """
12+
${"$"}version: "2"
13+
14+
namespace com.test
15+
16+
use aws.api#service
17+
18+
@service(sdkId: "Test")
19+
@title("Test Service")
20+
service Test {
21+
version: "1.0.0",
22+
operations: []
23+
}
24+
""".toSmithyModel()
25+
26+
val ctx = model.newTestContext("Test")
27+
28+
class ModuleDocumentationIntegrationTest {
29+
@Test
30+
fun integrationIsAppliedCorrectly() {
31+
assertFalse(
32+
ModuleDocumentationIntegration().enabledForService(model, ctx.generationCtx.settings),
33+
)
34+
assertTrue(
35+
ModuleDocumentationIntegration(
36+
codeExamples = mapOf("Test" to "https://example.com"),
37+
).enabledForService(model, ctx.generationCtx.settings),
38+
)
39+
}
40+
41+
@Test
42+
fun rendersBoilerplate() =
43+
ModuleDocumentationIntegration(
44+
codeExamples = mapOf("Test" to "https://example.com"),
45+
)
46+
.generateModuleDocumentation(ctx.toGenerationContext())
47+
.shouldContainOnlyOnceWithDiff(
48+
"""
49+
# Module test
50+
51+
Test Service
52+
""".trimIndent(),
53+
)
54+
55+
@Test
56+
fun rendersCodeExampleDocs() =
57+
ModuleDocumentationIntegration(
58+
codeExamples = mapOf("Test" to "https://example.com"),
59+
)
60+
.generateModuleDocumentation(ctx.toGenerationContext())
61+
.shouldContainOnlyOnceWithDiff(
62+
"""
63+
## Code Examples
64+
To see full code examples, see the Test Service examples in the AWS code example library. See https://example.com
65+
""".trimIndent(),
66+
)
67+
}

codegen/sdk/build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,10 @@ val stageSdks = tasks.register("stageSdks") {
180180
from("$projectionOutputDir/build.gradle.kts")
181181
into(it.destinationDir)
182182
}
183+
copy {
184+
from("$projectionOutputDir/API.md")
185+
into(it.destinationDir)
186+
}
183187
}
184188
}
185189
}

services/s3/API.md renamed to services/s3/DOCS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ See [aws.sdk.kotlin.services.s3.model.GetObjectResponse]
3535
## Streaming Responses
3636

3737
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).
38-
The result of the call is whatever the lambda returns.
38+
The result of the call is whatever the lambda returns.
3939

4040

4141
See [aws.sdk.kotlin.services.s3.S3Client.getObject]
@@ -58,4 +58,4 @@ println("wrote $contentSize bytes to $path")
5858
```
5959

6060

61-
This scoped response simplifies lifetime management for both the caller and the runtime.
61+
This scoped response simplifies lifetime management for both the caller and the runtime.

0 commit comments

Comments
 (0)