Skip to content

Commit 692f148

Browse files
authored
refactor(codegen): restructure operation serialization and deserialization (#579)
1 parent d589844 commit 692f148

File tree

29 files changed

+1336
-538
lines changed

29 files changed

+1336
-538
lines changed

smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/CodegenVisitor.kt

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -124,20 +124,18 @@ class CodegenVisitor(context: PluginContext) : ShapeVisitor.Default<Unit>() {
124124
writers
125125
)
126126

127-
LOGGER.info("[${service.id}] Generating serde for protocol $protocol")
128-
generateSerializers(ctx)
129-
generateDeserializers(ctx)
130-
131127
LOGGER.info("[${service.id}] Generating unit tests for protocol $protocol")
132128
generateProtocolUnitTests(ctx)
133129

134130
LOGGER.info("[${service.id}] Generating service client for protocol $protocol")
135131
generateProtocolClient(ctx)
136132
}
137133

134+
writers.finalize()
135+
138136
if (settings.build.generateDefaultBuildFiles) {
139137
val dependencies = writers.dependencies
140-
.map { it.properties["dependency"] as KotlinDependency }
138+
.mapNotNull { it.properties["dependency"] as? KotlinDependency }
141139
.distinct()
142140
writeGradleBuild(settings, fileManifest, dependencies)
143141
}

smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/KotlinSettings.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,12 @@ data class KotlinSettings(
4141
/**
4242
* Configuration elements specific to the service's package namespace, version, and description.
4343
*/
44-
data class PackageSettings(val name: String, val version: String, val description: String? = null)
44+
data class PackageSettings(val name: String, val version: String, val description: String? = null) {
45+
/**
46+
* Derive a subpackage namespace from the root package name
47+
*/
48+
fun subpackage(subpackageName: String): String = "$name.$subpackageName"
49+
}
4550

4651
/**
4752
* Get the corresponding [ServiceShape] from a model.

smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinDelegator.kt

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,10 @@
55
package software.amazon.smithy.kotlin.codegen.core
66

77
import software.amazon.smithy.build.FileManifest
8-
import software.amazon.smithy.codegen.core.Symbol
9-
import software.amazon.smithy.codegen.core.SymbolDependency
10-
import software.amazon.smithy.codegen.core.SymbolProvider
11-
import software.amazon.smithy.codegen.core.SymbolReference
8+
import software.amazon.smithy.codegen.core.*
129
import software.amazon.smithy.kotlin.codegen.KotlinSettings
1310
import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration
11+
import software.amazon.smithy.kotlin.codegen.model.SymbolProperty
1412
import software.amazon.smithy.kotlin.codegen.utils.namespaceToPath
1513
import software.amazon.smithy.model.Model
1614
import software.amazon.smithy.model.shapes.Shape
@@ -34,6 +32,30 @@ class KotlinDelegator(
3432
// Tracks dependencies for source not provided by codegen that may reside in the service source tree.
3533
val runtimeDependencies: MutableList<SymbolDependency> = mutableListOf()
3634

35+
/**
36+
* Finalize renders any generated dependencies that were used and should be called just before [flushWriters]
37+
*/
38+
fun finalize() {
39+
// render generated "dependencies"
40+
val writtenDependencies = mutableSetOf<String>()
41+
42+
// since generated dependencies can further mutate the set of generated dependencies this has
43+
// to be unwound until we have nothing left to process
44+
while (unprocessedDependencies(writtenDependencies).isNotEmpty()) {
45+
unprocessedDependencies(writtenDependencies).forEach { generated ->
46+
writtenDependencies.add(generated.fullName)
47+
val writer = checkoutWriter(generated.definitionFile, generated.namespace)
48+
writer.apply(generated.renderer)
49+
}
50+
}
51+
}
52+
53+
private fun unprocessedDependencies(writtenDependencies: Set<String>) =
54+
dependencies
55+
.mapNotNull { it.properties[SymbolProperty.GENERATED_DEPENDENCY] as? GeneratedDependency }
56+
.filterNot { writtenDependencies.contains(it.fullName) }
57+
.distinctBy { it.fullName }
58+
3759
/**
3860
* Writes all pending writers to disk and then clears them out.
3961
*/
@@ -155,3 +177,37 @@ class KotlinDelegator(
155177
return writer
156178
}
157179
}
180+
181+
/**
182+
* A pseudo dependency on a snippet of code. A generated dependency is usually a symbol that is required
183+
* by some other piece of code and must be generated.
184+
*
185+
* GeneratedDependency should not be instantiated directly, rather, it should be constructed by setting
186+
* the [software.amazon.smithy.kotlin.codegen.model.SymbolBuilder.renderBy] field when creating a [Symbol]
187+
*
188+
* Generated dependencies are created in the definition file of the [Symbol] they generate. They are
189+
* deduplicated by their fully qualified name in [KotlinDelegator] during codegen when writers are finalized.
190+
*/
191+
internal data class GeneratedDependency(
192+
val name: String,
193+
val namespace: String,
194+
val definitionFile: String,
195+
val renderer: SymbolRenderer
196+
) : SymbolDependencyContainer {
197+
/**
198+
* Fully qualified name
199+
*/
200+
val fullName: String
201+
get() = "$namespace.$name"
202+
203+
override fun getDependencies(): List<SymbolDependency> {
204+
val symbolDep = SymbolDependency.builder()
205+
.dependencyType("generated")
206+
.version("n/a")
207+
.packageName(fullName)
208+
.putProperty(SymbolProperty.GENERATED_DEPENDENCY, this)
209+
.build()
210+
211+
return listOf(symbolDep)
212+
}
213+
}

smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinWriter.kt

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ import software.amazon.smithy.model.traits.EnumDefinition
2525
import software.amazon.smithy.utils.CodeWriter
2626
import java.util.function.BiFunction
2727

28+
/**
29+
* A function that renders a symbol to the given writer
30+
*/
31+
typealias SymbolRenderer = (KotlinWriter) -> Unit
32+
2833
/**
2934
* Provides capability of writing Kotlin source code. An instance of a KotlinWriter corresponds to a file emitted
3035
* from codegen.
@@ -45,33 +50,39 @@ class KotlinWriter(
4550
expressionStart = '#'
4651

4752
// type name: `Foo`
48-
putFormatter('T', KotlinSymbolFormatter { symbol -> fullyQualifiedSymbols.contains(symbol.toFullyQualifiedSymbolName()) })
53+
putFormatter('T', KotlinSymbolFormatter(this) { symbol -> fullyQualifiedSymbols.contains(symbol.toFullyQualifiedSymbolName()) })
4954
// fully qualified type: `aws.sdk.kotlin.model.Foo`
50-
putFormatter('Q', KotlinSymbolFormatter { true })
55+
putFormatter('Q', KotlinSymbolFormatter(this) { true })
5156

5257
// like `T` but with nullability information: `aws.sdk.kotlin.model.Foo?`. This is mostly useful
5358
// when formatting properties
54-
putFormatter('P', KotlinPropertyFormatter())
59+
putFormatter('P', KotlinPropertyFormatter(this))
5560

5661
// like `P` but fully qualified
57-
putFormatter('F', KotlinPropertyFormatter(fullyQualifiedNames = true))
62+
putFormatter('F', KotlinPropertyFormatter(this, fullyQualifiedNames = true))
5863

5964
// like `P` but with default set (if applicable): `aws.sdk.kotlin.model.Foo = 1`
60-
putFormatter('D', KotlinPropertyFormatter(setDefault = true))
65+
putFormatter('D', KotlinPropertyFormatter(this, setDefault = true))
6166

6267
// like `D` but fully qualified
63-
putFormatter('E', KotlinPropertyFormatter(setDefault = true, fullyQualifiedNames = true))
68+
putFormatter('E', KotlinPropertyFormatter(this, setDefault = true, fullyQualifiedNames = true))
6469

6570
// Pass a function receiving a [KotlinWriter] to generate an inline value
6671
putFormatter('W', InlineKotlinWriterFormatter(this))
6772
}
6873

74+
/**
75+
* Import [symbol] into the current file. If the symbol resides in the same package as the current writer
76+
* then only the dependencies will be processed and no import statement generated.
77+
*
78+
* @param symbol the symbol to generate an import statement for
79+
* @param alias an alias name to give to the imported symbol (e.g. `import foo.bar.baz as Quux`)
80+
*/
6981
fun addImport(symbol: Symbol, alias: String = symbol.name): KotlinWriter {
7082
// don't import built-in symbols
7183
if (symbol.isBuiltIn) return this
7284

73-
// always add dependencies
74-
dependencies.addAll(symbol.dependencies)
85+
addDepsRecursively(symbol)
7586

7687
// only add imports for symbols in a different namespace
7788
if (symbol.namespace.isNotEmpty() && symbol.namespace != fullPackageName) {
@@ -108,6 +119,12 @@ class KotlinWriter(
108119
}
109120
}
110121

122+
private fun addDepsRecursively(symbol: Symbol) {
123+
// always add dependencies
124+
dependencies.addAll(symbol.dependencies)
125+
symbol.references.forEach { addDepsRecursively(it.symbol) }
126+
}
127+
111128
/**
112129
* Directly add an import
113130
*/
@@ -251,13 +268,17 @@ fun KotlinWriter.addImport(vararg imports: Iterable<Symbol>): KotlinWriter {
251268

252269
/**
253270
* Implements Kotlin symbol formatting for the `#T` and `#Q` formatter(s)
271+
* NOTE: That the symbol will automatically be imported.
254272
*/
255273
private class KotlinSymbolFormatter(
274+
private val writer: KotlinWriter,
256275
private val fullyQualifiedPredicate: (Symbol) -> Boolean = { false },
257276
) : BiFunction<Any, String, String> {
258277
override fun apply(type: Any, indent: String): String {
259278
when (type) {
260279
is Symbol -> {
280+
// writer will omit unnecessary same package imports and dedupe
281+
writer.addImport(type)
261282
return if (fullyQualifiedPredicate(type)) type.fullName else type.name
262283
}
263284
else -> throw CodegenException("Invalid type provided for #T. Expected a Symbol, but found `$type`")
@@ -269,6 +290,7 @@ private class KotlinSymbolFormatter(
269290
* Implements Kotlin symbol formatting for the `#D` and `#P` formatter(s)
270291
*/
271292
class KotlinPropertyFormatter(
293+
private val writer: KotlinWriter,
272294
// set defaults
273295
private val setDefault: Boolean = false,
274296
// format with nullability `?`
@@ -279,6 +301,7 @@ class KotlinPropertyFormatter(
279301
override fun apply(type: Any, indent: String): String {
280302
when (type) {
281303
is Symbol -> {
304+
writer.addImport(type)
282305
var formatted = if (fullyQualifiedNames) type.fullName else type.name
283306
if (includeNullability && type.isBoxed) {
284307
formatted += "?"

smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/model/SymbolBuilder.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ package software.amazon.smithy.kotlin.codegen.model
77
import software.amazon.smithy.codegen.core.Symbol
88
import software.amazon.smithy.codegen.core.SymbolDependencyContainer
99
import software.amazon.smithy.codegen.core.SymbolReference
10+
import software.amazon.smithy.kotlin.codegen.core.GeneratedDependency
1011
import software.amazon.smithy.kotlin.codegen.core.KotlinDependency
12+
import software.amazon.smithy.kotlin.codegen.core.SymbolRenderer
1113

1214
@DslMarker
1315
annotation class SymbolDsl
@@ -26,6 +28,11 @@ open class SymbolBuilder {
2628
var declarationFile: String? = null
2729
var defaultValue: String? = null
2830

31+
/**
32+
* Register the function (dependency) responsible for rendering this symbol.
33+
*/
34+
var renderBy: SymbolRenderer? = null
35+
2936
val dependencies: MutableSet<SymbolDependencyContainer> = mutableSetOf()
3037
val references: MutableList<SymbolReference> = mutableListOf()
3138

@@ -76,6 +83,15 @@ open class SymbolBuilder {
7683
declarationFile?.let { builder.declarationFile(it) }
7784
definitionFile?.let { builder.definitionFile(it) }
7885
defaultValue?.let { builder.defaultValue(it) }
86+
87+
if (renderBy != null) {
88+
checkNotNull(definitionFile) { "a rendered dependency must declare a definition file!" }
89+
checkNotNull(namespace) { "a rendered dependency must declare a namespace" }
90+
// abuse dependencies to get the delegator to eventually render this
91+
val generatedDep = GeneratedDependency(name!!, namespace!!, definitionFile!!, renderBy!!)
92+
dependency(generatedDep)
93+
}
94+
7995
dependencies.forEach { builder.addDependency(it) }
8096
references.forEach { builder.addReference(it) }
8197

smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/model/SymbolExt.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ object SymbolProperty {
3232

3333
// Entry type for Maps
3434
const val ENTRY_EXPRESSION: String = "entryExpression"
35+
36+
// Pseudo dependency on a snippet of code
37+
const val GENERATED_DEPENDENCY: String = "generatedDependency"
3538
}
3639

3740
/**

smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/model/knowledge/SerdeIndex.kt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,22 @@ class SerdeIndex(private val model: Model) : KnowledgeIndex {
4747
return walkNestedShapesRequiringSerde(model, topLevelMembers)
4848
}
4949

50+
/**
51+
* Find and return the set of shapes reachable from the given shape that would require a "document" serializer.
52+
* @return The set of shapes that require a serializer implementation
53+
*/
54+
fun requiresDocumentSerializer(shape: Shape): Set<Shape> =
55+
when (shape) {
56+
is OperationShape -> requiresDocumentSerializer(listOf(shape))
57+
else -> {
58+
val topLevelMembers = shape.members()
59+
.map { model.expectShape(it.target) }
60+
.filter { it.isStructureShape || it.isUnionShape || it is CollectionShape || it.isMapShape }
61+
.toSet()
62+
walkNestedShapesRequiringSerde(model, topLevelMembers)
63+
}
64+
}
65+
5066
/**
5167
* Find and return the set of shapes that are not operation outputs but do require a deserializer
5268
*
@@ -83,6 +99,22 @@ class SerdeIndex(private val model: Model) : KnowledgeIndex {
8399

84100
return walkNestedShapesRequiringSerde(model, topLevelMembers)
85101
}
102+
103+
/**
104+
* Find and return the set of shapes reachable from the given shape that would require a "document" deserializer.
105+
* @return The set of shapes that require a deserializer implementation
106+
*/
107+
fun requiresDocumentDeserializer(shape: Shape): Set<Shape> =
108+
when (shape) {
109+
is OperationShape -> requiresDocumentDeserializer(listOf(shape))
110+
else -> {
111+
val topLevelMembers = shape.members()
112+
.map { model.expectShape(it.target) }
113+
.filter { it.isStructureShape || it.isUnionShape || it is CollectionShape || it.isMapShape }
114+
.toMutableSet()
115+
walkNestedShapesRequiringSerde(model, topLevelMembers)
116+
}
117+
}
86118
}
87119

88120
private fun walkNestedShapesRequiringSerde(model: Model, shapes: Set<Shape>): Set<Shape> {

0 commit comments

Comments
 (0)