Skip to content

Commit 5457cfc

Browse files
authored
feat: generate operation samples (#1007)
1 parent 79e39bc commit 5457cfc

File tree

9 files changed

+480
-46
lines changed

9 files changed

+480
-46
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"id": "295410ec-cc32-4a9c-a30a-aaeda9996c0f",
3+
"type": "feature",
4+
"description": "Generate KDoc samples from modeled examples"
5+
}

codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/ShapeValueGenerator.kt

Lines changed: 62 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -25,24 +25,44 @@ class ShapeValueGenerator(
2525
) {
2626

2727
/**
28-
* Writes generation of a shape value type declaration for the given the parameters.
28+
* Renders a shape value declaration for the given parameters inline
29+
* with the current writer.
2930
*
3031
* @param writer writer to write generated code with.
3132
* @param shape the shape that will be declared.
3233
* @param params parameters to fill the generated shape declaration.
3334
*/
34-
fun writeShapeValueInline(writer: KotlinWriter, shape: Shape, params: Node) {
35-
val nodeVisitor = ShapeValueNodeVisitor(writer, this, shape)
36-
when (shape.type) {
37-
ShapeType.STRUCTURE -> {
38-
if (params.isNullNode) {
39-
params.accept(nodeVisitor)
40-
} else {
41-
classDeclaration(writer, shape.asStructureShape().get()) {
42-
params.accept(nodeVisitor)
43-
}
35+
fun instantiateShapeInline(writer: KotlinWriter, shape: Shape, params: Node) {
36+
if (shape.isStructureShape) {
37+
if (params.isNullNode) {
38+
writeShapeValuesInline(writer, shape, params)
39+
} else {
40+
classDeclaration(writer, shape.asStructureShape().get()) {
41+
writeShapeValuesInline(writer, shape, params)
4442
}
4543
}
44+
} else {
45+
writeShapeValuesInline(writer, shape, params)
46+
}
47+
}
48+
49+
/**
50+
* Renders the mapping of the shape fields to the given parameters
51+
*
52+
* @param writer writer to write generated code with.
53+
* @param shape the shape that will be declared.
54+
* @param params parameters to fill the generated shape declaration.
55+
*/
56+
fun writeShapeValues(writer: KotlinWriter, shape: Shape, params: Node) {
57+
writer.ensureNewline()
58+
writeShapeValuesInline(writer, shape, params)
59+
writer.ensureNewline()
60+
}
61+
62+
private fun writeShapeValuesInline(writer: KotlinWriter, shape: Shape, params: Node) {
63+
val nodeVisitor = ShapeValueNodeVisitor(writer, this, shape)
64+
when (shape.type) {
65+
ShapeType.STRUCTURE -> params.accept(nodeVisitor)
4666
ShapeType.MAP -> mapDeclaration(writer, shape.asMapShape().get()) {
4767
params.accept(nodeVisitor)
4868
}
@@ -58,11 +78,12 @@ class ShapeValueGenerator(
5878
private fun classDeclaration(writer: KotlinWriter, shape: StructureShape, block: () -> Unit) {
5979
val symbol = symbolProvider.toSymbol(shape)
6080
// invoke the generated DSL builder for the class
61-
writer.writeInline("#L {\n", symbol.name)
81+
writer.writeInline("#L {", symbol.name)
82+
.ensureNewline()
6283
.indent()
6384
.call { block() }
6485
.dedent()
65-
.write("")
86+
.ensureNewline()
6687
.write("}")
6788
}
6889

@@ -72,11 +93,12 @@ class ShapeValueGenerator(
7293

7394
val collectionGeneratorFunction = symbolProvider.toSymbol(shape).expectProperty(SymbolProperty.IMMUTABLE_COLLECTION_FUNCTION)
7495

75-
writer.writeInline("$collectionGeneratorFunction(\n")
96+
writer.writeInline("$collectionGeneratorFunction(")
97+
.ensureNewline()
7698
.indent()
7799
.call { block() }
78100
.dedent()
79-
.write("")
101+
.ensureNewline()
80102
.write(")")
81103

82104
writer.popState()
@@ -92,11 +114,12 @@ class ShapeValueGenerator(
92114
collectionSymbol.references.forEach {
93115
writer.addImport(it.symbol)
94116
}
95-
writer.writeInline("$generatorFn(\n")
117+
writer.writeInline("$generatorFn(")
118+
.ensureNewline()
96119
.indent()
97120
.call { block() }
98121
.dedent()
99-
.write("")
122+
.ensureNewline()
100123
.write(")")
101124

102125
writer.popState()
@@ -142,7 +165,8 @@ class ShapeValueGenerator(
142165
override fun objectNode(node: ObjectNode) {
143166
if (currShape.type == ShapeType.DOCUMENT) {
144167
writer
145-
.writeInline("#T {\n", RuntimeTypes.Core.Content.buildDocument)
168+
.writeInline("#T {", RuntimeTypes.Core.Content.buildDocument)
169+
.ensureNewline()
146170
.indent()
147171
}
148172

@@ -157,9 +181,9 @@ class ShapeValueGenerator(
157181
memberShape = generator.model.expectShape(member.target)
158182
val memberName = generator.symbolProvider.toMemberName(member)
159183
writer.writeInline("#L = ", memberName)
160-
generator.writeShapeValueInline(writer, memberShape, valueNode)
184+
generator.instantiateShapeInline(writer, memberShape, valueNode)
161185
if (i < node.members.size - 1) {
162-
writer.write("")
186+
writer.ensureNewline()
163187
}
164188
}
165189
is MapShape -> {
@@ -169,16 +193,17 @@ class ShapeValueGenerator(
169193
if (valueNode is NullNode) {
170194
writer.write("null")
171195
} else {
172-
generator.writeShapeValueInline(writer, memberShape, valueNode)
196+
generator.instantiateShapeInline(writer, memberShape, valueNode)
173197
if (i < node.members.size - 1) {
174-
writer.writeInline(",\n")
198+
writer.writeInline(",")
199+
.ensureNewline()
175200
}
176201
}
177202
}
178203
is DocumentShape -> {
179204
writer.writeInline("#S to ", keyNode.value)
180-
generator.writeShapeValueInline(writer, currShape, valueNode)
181-
writer.writeInline("\n")
205+
generator.instantiateShapeInline(writer, currShape, valueNode)
206+
writer.ensureNewline()
182207
}
183208
is UnionShape -> {
184209
val member = currShape.getMember(keyNode.value).orElseThrow {
@@ -189,8 +214,8 @@ class ShapeValueGenerator(
189214
val memberName = generator.symbolProvider.toMemberName(member)
190215
val variantName = memberName.replaceFirstChar { c -> c.uppercaseChar() }
191216
writer.writeInline("${currSymbol.name}.$variantName(")
192-
generator.writeShapeValueInline(writer, memberShape, valueNode)
193-
writer.write(")")
217+
generator.instantiateShapeInline(writer, memberShape, valueNode)
218+
writer.writeInline(")")
194219
}
195220
else -> throw CodegenException("unexpected shape type " + currShape.type)
196221
}
@@ -238,8 +263,10 @@ class ShapeValueGenerator(
238263
writer.withInlineBlock("#T(", ")", RuntimeTypes.Core.Content.Document) {
239264
writer.withInlineBlock("listOf(", ")") {
240265
node.elements.forEach {
241-
generator.writeShapeValueInline(writer, currShape, it)
242-
writer.writeInline(",\n")
266+
generator.instantiateShapeInline(writer, currShape, it)
267+
writer.unwrite(writer.newline)
268+
writer.writeInline(",")
269+
.ensureNewline()
243270
}
244271
}
245272
}
@@ -249,9 +276,14 @@ class ShapeValueGenerator(
249276
val memberShape = generator.model.expectShape((currShape as CollectionShape).member.target)
250277
var i = 0
251278
node.elements.forEach { element ->
252-
generator.writeShapeValueInline(writer, memberShape, element)
279+
generator.instantiateShapeInline(writer, memberShape, element)
280+
writer.unwrite(writer.newline)
253281
if (i < node.elements.size - 1) {
254-
writer.writeInline(",\n")
282+
writer.pushState()
283+
writer.indentText = ""
284+
writer.writeInlineWithNoFormatting(",")
285+
writer.ensureNewline()
286+
writer.popState()
255287
}
256288
i++
257289
}

codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpProtocolUnitTestRequestGenerator.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ open class HttpProtocolUnitTestRequestGenerator protected constructor(builder: B
101101
writer.writeInline("\nval input = ")
102102
.indent()
103103
.call {
104-
ShapeValueGenerator(model, symbolProvider).writeShapeValueInline(writer, inputShape, test.params)
104+
ShapeValueGenerator(model, symbolProvider).instantiateShapeInline(writer, inputShape, test.params)
105105
}
106106
.dedent()
107107
.write("")

codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpProtocolUnitTestResponseGenerator.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ open class HttpProtocolUnitTestResponseGenerator protected constructor(builder:
8080
.call {
8181
outputShape?.let {
8282
writer.writeInline("\nresponse = ")
83-
ShapeValueGenerator(model, symbolProvider).writeShapeValueInline(writer, it, test.params)
83+
ShapeValueGenerator(model, symbolProvider).instantiateShapeInline(writer, it, test.params)
8484
}
8585
}
8686
.write("")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package software.amazon.smithy.kotlin.codegen.rendering.samples
6+
7+
import software.amazon.smithy.kotlin.codegen.KotlinSettings
8+
import software.amazon.smithy.kotlin.codegen.core.*
9+
import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration
10+
import software.amazon.smithy.kotlin.codegen.model.expectTrait
11+
import software.amazon.smithy.kotlin.codegen.model.getTrait
12+
import software.amazon.smithy.kotlin.codegen.model.hasAllOptionalMembers
13+
import software.amazon.smithy.kotlin.codegen.model.hasTrait
14+
import software.amazon.smithy.kotlin.codegen.rendering.ShapeValueGenerator
15+
import software.amazon.smithy.model.Model
16+
import software.amazon.smithy.model.SourceLocation
17+
import software.amazon.smithy.model.knowledge.TopDownIndex
18+
import software.amazon.smithy.model.shapes.OperationShape
19+
import software.amazon.smithy.model.traits.DocumentationTrait
20+
import software.amazon.smithy.model.traits.ExamplesTrait
21+
import software.amazon.smithy.model.traits.ExamplesTrait.Example
22+
import software.amazon.smithy.model.transform.ModelTransformer
23+
import java.util.*
24+
import kotlin.jvm.optionals.getOrDefault
25+
26+
/**
27+
* [KotlinIntegration] that renders [KDoc samples](https://kotlinlang.org/docs/kotlin-doc.html#sample-identifier)
28+
* and pre-processes the documentation to insert references to the generated sample identifiers for operations
29+
* that have [examples](https://smithy.io/2.0/spec/documentation-traits.html#smithy-api-examples-trait)
30+
*/
31+
class KDocSamplesGenerator : KotlinIntegration {
32+
override fun enabledForService(model: Model, settings: KotlinSettings): Boolean {
33+
val topDownIndex = TopDownIndex.of(model)
34+
val operations = topDownIndex.getContainedOperations(settings.service)
35+
return operations.any { it.hasTrait<ExamplesTrait>() }
36+
}
37+
38+
/**
39+
* This should run _after_ [software.amazon.smithy.kotlin.codegen.lang.DocumentationPreprocessor]
40+
*/
41+
override fun preprocessModel(model: Model, settings: KotlinSettings): Model {
42+
val transformer = ModelTransformer.create()
43+
return transformer.mapShapes(model) { shape ->
44+
when {
45+
shape is OperationShape && shape.hasTrait<ExamplesTrait>() -> {
46+
val examplesTrait = shape.expectTrait<ExamplesTrait>()
47+
val filtered = examplesTrait.examples.filterNot { it.error.isPresent }
48+
val kdocSampleIdentifiers = filtered.indices.joinToString(separator = "\n") { idx ->
49+
val identifier = sampleIdentifier(settings, shape, idx)
50+
"@sample $identifier"
51+
}
52+
53+
val existingDocs = shape.getTrait<DocumentationTrait>()
54+
val updatedDocs = buildString {
55+
if (existingDocs != null) {
56+
append(existingDocs.value)
57+
append("\n\n")
58+
}
59+
append(kdocSampleIdentifiers)
60+
}
61+
val sourceLocation = existingDocs?.sourceLocation ?: SourceLocation.NONE
62+
val newOrUpdatedDocTrait = DocumentationTrait(updatedDocs, sourceLocation)
63+
shape.toBuilder()
64+
.addTrait(newOrUpdatedDocTrait)
65+
.build()
66+
}
67+
else -> shape
68+
}
69+
}
70+
}
71+
72+
private fun sampleIdentifier(settings: KotlinSettings, op: OperationShape, index: Int): String =
73+
listOf(
74+
samplePackage(settings),
75+
sampleClassName(op),
76+
sampleFunctionName(index),
77+
).joinToString(separator = ".")
78+
79+
private fun sampleFunctionName(index: Int): String = "sample" + if (index > 0) "${index + 1}" else ""
80+
private fun sampleClassName(op: OperationShape): String = op.id.name
81+
82+
private fun samplePackage(settings: KotlinSettings): String = settings.pkg.subpackage("samples")
83+
84+
override fun writeAdditionalFiles(ctx: CodegenContext, delegator: KotlinDelegator) {
85+
val topDownIndex = TopDownIndex.of(ctx.model)
86+
val operations = topDownIndex.getContainedOperations(ctx.settings.service)
87+
operations.filter { it.hasTrait<ExamplesTrait>() }
88+
.forEach { op ->
89+
val examples = op.expectTrait<ExamplesTrait>()
90+
91+
val writer = KotlinWriter(samplePackage(ctx.settings))
92+
writer.withBlock("class #L {", "}", sampleClassName(op)) {
93+
examples
94+
.examples
95+
// unclear what benefit error examples provide, omit for now
96+
.filterNot { it.error.isPresent }
97+
.forEachIndexed { idx, example ->
98+
write("")
99+
write("@Sample")
100+
withBlock("fun #L() {", "}", sampleFunctionName(idx)) {
101+
example
102+
.documentation
103+
.getOrDefault(example.title)
104+
.breakLongLines()
105+
.forEach { line ->
106+
write("// #L", line)
107+
}
108+
109+
renderNormalExample(ctx, writer, op, example)
110+
}
111+
}
112+
}
113+
val contents = writer.toString()
114+
delegator.fileManifest.writeFile("src/samples/${op.id.name}.kt", contents)
115+
}
116+
}
117+
118+
private fun renderNormalExample(ctx: CodegenContext, writer: KotlinWriter, op: OperationShape, example: Example) {
119+
val clientName = clientName(ctx.settings.sdkId).replaceFirstChar { it.lowercase(Locale.getDefault()) }
120+
val respPrefix = if (example.output.isPresent) "val resp = " else ""
121+
122+
val input = ctx.model.expectShape(op.inputShape)
123+
if (input.hasAllOptionalMembers && example.input.isEmpty) {
124+
writer.write("#L#LClient.#L()", respPrefix, clientName, op.defaultName())
125+
} else {
126+
writer.withBlock("#L#LClient.#L {", "}", respPrefix, clientName, op.defaultName()) {
127+
ShapeValueGenerator(ctx.model, ctx.symbolProvider).writeShapeValues(writer, input, example.input)
128+
}
129+
}
130+
}
131+
}
132+
133+
private val wordsPattern = Regex("""\w+[.,]?|".*?"[.,]?|\(.*\)[.,]?""")
134+
internal fun String.breakLongLines(maxLineLengthChars: Int = 100): List<String> {
135+
val words = wordsPattern.findAll(this).map(MatchResult::value)
136+
val lines = mutableListOf<String>()
137+
val wordsOnLine = mutableListOf<String>()
138+
var lineLength = 0
139+
140+
words.forEach { word ->
141+
if (word.length + lineLength < maxLineLengthChars) {
142+
if (wordsOnLine.isNotEmpty()) lineLength++
143+
lineLength += word.length
144+
wordsOnLine.add(word)
145+
} else {
146+
lines.add(wordsOnLine.joinToString(separator = " "))
147+
lineLength = 0
148+
wordsOnLine.clear()
149+
wordsOnLine.add(word)
150+
}
151+
}
152+
153+
if (wordsOnLine.isNotEmpty()) {
154+
lines.add(wordsOnLine.joinToString(separator = " "))
155+
}
156+
return lines
157+
}

codegen/smithy-kotlin-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
@@ -1,5 +1,6 @@
11
software.amazon.smithy.kotlin.codegen.lang.BuiltinPreprocessor
22
software.amazon.smithy.kotlin.codegen.lang.DocumentationPreprocessor
3+
software.amazon.smithy.kotlin.codegen.rendering.samples.KDocSamplesGenerator
34
software.amazon.smithy.kotlin.codegen.model.SetRefactorPreprocessor
45
software.amazon.smithy.kotlin.codegen.rendering.PaginatorGenerator
56
software.amazon.smithy.kotlin.codegen.rendering.waiters.ServiceWaitersGenerator

0 commit comments

Comments
 (0)