codegen-kotlin: Support Java serialization of module classes (#721)

Motivation:
- Java serialization makes as much sense for module classes as it does for regular classes.
- Offer same Java serialization support as codegen-java.

Changes:
- Move generation of `appendProperty` method and serialization code
  into new method `generateCompanionRelatedCode`.
- Improve/fix generation of serialization code.

Result:
Java serialization is also supported for module classes.
This commit is contained in:
translatenix
2024-10-24 22:11:35 -07:00
committed by GitHub
parent 8b0118fec5
commit 8fa3acf32f
2 changed files with 113 additions and 76 deletions

View File

@@ -138,50 +138,86 @@ class KotlinCodeGenerator(
val pModuleClass = moduleSchema.moduleClass
val hasProperties = pModuleClass.properties.any { !it.value.isHidden }
val isGenerateClass = hasProperties || pModuleClass.isOpen || pModuleClass.isAbstract
val hasModuleProperties = pModuleClass.properties.any { !it.value.isHidden }
val isGenerateModuleClass =
hasModuleProperties || pModuleClass.isOpen || pModuleClass.isAbstract
fun generateCompanionRelatedCode(
builder: TypeSpec.Builder,
isModuleType: Boolean = false
): TypeSpec.Builder {
// ensure that at most one companion object is generated for this type
val companionObjectBuilder: Lazy<TypeSpec.Builder> = lazy {
TypeSpec.companionObjectBuilder()
}
// generate append method for module classes w/o parent class;
// reuse in subclasses and nested classes
val isGenerateAppendPropertyMethod =
isModuleType &&
// check if we inherit another module's append method
pModuleClass.superclass!!.info == PClassInfo.Module &&
// check if anyone is (potentially) going to use our append method
(pModuleClass.isOpen ||
pModuleClass.isAbstract ||
(isGenerateModuleClass && !builder.modifiers.contains(KModifier.DATA)) ||
builder.typeSpecs.any { !it.modifiers.contains(KModifier.DATA) })
if (isGenerateAppendPropertyMethod) {
val appendPropertyMethodModifier =
if (pModuleClass.isOpen || pModuleClass.isAbstract) {
// alternative is `@JvmStatic protected`
// (`protected` alone isn't sufficient as of Kotlin 1.6)
KModifier.PUBLIC
} else KModifier.PRIVATE
if (isGenerateModuleClass) {
companionObjectBuilder.value.addFunction(
appendPropertyMethod().addModifiers(appendPropertyMethodModifier).build()
)
} else { // kotlin object
builder.addFunction(
appendPropertyMethod().addModifiers(appendPropertyMethodModifier).build()
)
}
}
// generate serialization code
if (
options.implementSerializable &&
(!isModuleType || isGenerateModuleClass) &&
!builder.modifiers.contains(KModifier.ABSTRACT)
) {
builder.addSuperinterface(java.io.Serializable::class.java)
companionObjectBuilder.value.addProperty(
PropertySpec.builder(
"serialVersionUID",
Long::class.java,
KModifier.PRIVATE,
KModifier.CONST
)
.initializer("0L")
.build()
)
}
if (companionObjectBuilder.isInitialized()) {
builder.addType(companionObjectBuilder.value.build())
}
return builder
}
val moduleType =
if (isGenerateClass) {
if (isGenerateModuleClass) {
generateTypeSpec(pModuleClass, moduleSchema)
} else {
generateObjectSpec(pModuleClass)
}
for (pClass in moduleSchema.classes.values) {
moduleType.addType(generateTypeSpec(pClass, moduleSchema).ensureSerializable().build())
}
// generate append method for module classes w/o parent class; reuse in subclasses and nested
// classes
val isGenerateAppendPropertyMethod =
// check if we can inherit someone else's append method
pModuleClass.superclass!!.info == PClassInfo.Module &&
// check if anyone is (potentially) going to use our append method
(pModuleClass.isOpen ||
pModuleClass.isAbstract ||
(hasProperties && !moduleType.modifiers.contains(KModifier.DATA)) ||
moduleType.typeSpecs.any { !it.modifiers.contains(KModifier.DATA) })
if (isGenerateAppendPropertyMethod) {
val appendPropertyMethodModifier =
if (pModuleClass.isOpen || pModuleClass.isAbstract) {
// alternative is `@JvmStatic protected`
// (`protected` alone isn't sufficient as of Kotlin 1.6)
KModifier.PUBLIC
} else KModifier.PRIVATE
if (isGenerateClass) {
moduleType.addType(
TypeSpec.companionObjectBuilder()
.addFunction(
appendPropertyMethod().addModifiers(appendPropertyMethodModifier).build()
)
.build()
)
} else { // kotlin object
moduleType.addFunction(
appendPropertyMethod().addModifiers(appendPropertyMethodModifier).build()
)
}
moduleType.addType(
generateCompanionRelatedCode(generateTypeSpec(pClass, moduleSchema)).build()
)
}
val moduleName = moduleSchema.moduleName
@@ -206,7 +242,7 @@ class KotlinCodeGenerator(
}
}
fileSpec.addType(moduleType.build())
fileSpec.addType(generateCompanionRelatedCode(moduleType, isModuleType = true).build())
return fileSpec.build().toString()
}
@@ -530,44 +566,6 @@ class KotlinCodeGenerator(
else generateRegularClass()
}
private fun TypeSpec.Builder.ensureSerializable(): TypeSpec.Builder {
if (!options.implementSerializable || modifiers.contains(KModifier.ABSTRACT)) {
return this
}
if (!this.superinterfaces.containsKey(java.io.Serializable::class.java.asTypeName())) {
this.addSuperinterface(java.io.Serializable::class.java)
}
var useExistingCompanionBuilder = false
val companionBuilder =
this.typeSpecs
.find { it.isCompanion }
?.let {
useExistingCompanionBuilder = true
it.toBuilder(TypeSpec.Kind.OBJECT)
}
?: TypeSpec.companionObjectBuilder()
if (!companionBuilder.propertySpecs.any { it.name == "serialVersionUID" })
companionBuilder.addProperty(
PropertySpec.builder(
"serialVersionUID",
Long::class.java,
KModifier.PRIVATE,
KModifier.CONST
)
.initializer("0L")
.build()
)
if (!useExistingCompanionBuilder) {
this.addType(companionBuilder.build())
}
return this
}
private fun generateEnumTypeSpec(
typeAlias: TypeAlias,
stringLiterals: Set<String>

View File

@@ -1760,6 +1760,45 @@ class KotlinCodeGeneratorTest {
confirmSerDe(bigStruct)
}
@Test
fun `generates serializable module classes`() {
val kotlinCode =
generateKotlinCode(
"""
module Person
address: Address
class Address {
street: String
}
""",
implementSerializable = true
)
assertThat(kotlinCode)
.contains(
"""
|data class Person(
| val address: Address
|) : Serializable {
| data class Address(
| val street: String
| ) : Serializable {
| companion object {
| private const val serialVersionUID: Long = 0L
| }
| }
|
| companion object {
| private const val serialVersionUID: Long = 0L
| }
|}
"""
.trimMargin()
)
}
@Test
fun `encoded file paths`() {
val kotlinCode =