From 8fa3acf32ffa50b36349d201257519d0dec567ca Mon Sep 17 00:00:00 2001 From: translatenix <119817707+translatenix@users.noreply.github.com> Date: Thu, 24 Oct 2024 22:11:35 -0700 Subject: [PATCH] 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. --- .../pkl/codegen/kotlin/KotlinCodeGenerator.kt | 150 +++++++++--------- .../codegen/kotlin/KotlinCodeGeneratorTest.kt | 39 +++++ 2 files changed, 113 insertions(+), 76 deletions(-) diff --git a/pkl-codegen-kotlin/src/main/kotlin/org/pkl/codegen/kotlin/KotlinCodeGenerator.kt b/pkl-codegen-kotlin/src/main/kotlin/org/pkl/codegen/kotlin/KotlinCodeGenerator.kt index a270ec5f..423d881b 100644 --- a/pkl-codegen-kotlin/src/main/kotlin/org/pkl/codegen/kotlin/KotlinCodeGenerator.kt +++ b/pkl-codegen-kotlin/src/main/kotlin/org/pkl/codegen/kotlin/KotlinCodeGenerator.kt @@ -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 = 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 diff --git a/pkl-codegen-kotlin/src/test/kotlin/org/pkl/codegen/kotlin/KotlinCodeGeneratorTest.kt b/pkl-codegen-kotlin/src/test/kotlin/org/pkl/codegen/kotlin/KotlinCodeGeneratorTest.kt index 71a2a484..f6a194c7 100644 --- a/pkl-codegen-kotlin/src/test/kotlin/org/pkl/codegen/kotlin/KotlinCodeGeneratorTest.kt +++ b/pkl-codegen-kotlin/src/test/kotlin/org/pkl/codegen/kotlin/KotlinCodeGeneratorTest.kt @@ -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 =