Allow renaming Java/Kotlin classes/packages during code generation (#499)

Adds a `rename` field to the Java/Kotlin code generators that allows renaming packages and classes during codegen.

* Add `--rename` flag to CLIs
* Add `rename` property to Gradle API
This commit is contained in:
Vladimir Matveev
2024-06-12 15:43:43 -07:00
committed by GitHub
parent b03530ed1f
commit d7a1778199
25 changed files with 1099 additions and 157 deletions

View File

@@ -36,8 +36,23 @@ data class CliKotlinCodeGeneratorOptions(
val generateSpringBootConfig: Boolean = false,
/** Whether to make generated classes implement [java.io.Serializable] */
val implementSerializable: Boolean = false
val implementSerializable: Boolean = false,
/**
* A rename mapping for class names.
*
* When you need to have Kotlin class or package names different from the default names derived
* from Pkl module names, you can define a rename mapping, where the key is a prefix of the
* original Pkl module name, and the value is the desired replacement.
*/
val renames: Map<String, String> = emptyMap()
) {
fun toKotlinCodegenOptions(): KotlinCodegenOptions =
KotlinCodegenOptions(indent, generateKdoc, generateSpringBootConfig, implementSerializable)
KotlinCodegenOptions(
indent,
generateKdoc,
generateSpringBootConfig,
implementSerializable,
renames
)
}

View File

@@ -20,6 +20,7 @@ import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import java.io.StringWriter
import java.net.URI
import java.util.*
import org.pkl.commons.NameMapper
import org.pkl.core.*
import org.pkl.core.util.CodeGeneratorUtils
import org.pkl.core.util.IoUtils
@@ -35,7 +36,15 @@ data class KotlinCodegenOptions(
val generateSpringBootConfig: Boolean = false,
/** Whether to make generated classes implement [java.io.Serializable] */
val implementSerializable: Boolean = false
val implementSerializable: Boolean = false,
/**
* A mapping from Pkl module name prefixes to their replacements.
*
* Can be used when the class or package name in the generated source code should be different
* from the corresponding name derived from the Pkl module declaration .
*/
val renames: Map<String, String> = emptyMap(),
)
class KotlinCodeGeneratorException(message: String) : RuntimeException(message)
@@ -106,10 +115,17 @@ class KotlinCodeGenerator(
.toString()
}
val kotlinFileName: String
private val kotlinFileName: String
get() = buildString {
val (packageName, className) = nameMapper.map(moduleSchema.moduleName)
val dirPath = packageName.split('.').joinToString("/", transform = IoUtils::encodePath)
val fileName = IoUtils.encodePath(className)
append("kot")
append("lin/${relativeOutputPathFor(moduleSchema.moduleName)}")
append("lin/")
if (dirPath.isNotEmpty()) {
append("$dirPath/")
}
append("$fileName.kt")
}
val kotlinFile: String
@@ -169,9 +185,8 @@ class KotlinCodeGenerator(
}
val moduleName = moduleSchema.moduleName
val index = moduleName.lastIndexOf(".")
val packageName = if (index == -1) "" else moduleName.substring(0, index)
val moduleTypeName = moduleName.substring(index + 1).replaceFirstChar { it.titlecaseChar() }
val (packageName, moduleTypeName) = nameMapper.map(moduleName)
val fileSpec = FileSpec.builder(packageName, moduleTypeName).indent(options.indent)
@@ -195,17 +210,6 @@ class KotlinCodeGenerator(
return fileSpec.build().toString()
}
private fun relativeOutputPathFor(moduleName: String): String {
val nameParts = moduleName.split(".").map(IoUtils::encodePath)
val dirPath = nameParts.dropLast(1).joinToString("/")
val fileName = nameParts.last().replaceFirstChar { it.titlecaseChar() }
return if (dirPath.isEmpty()) {
"$fileName.kt"
} else {
"$dirPath/$fileName.kt"
}
}
private fun generateObjectSpec(pClass: PClass): TypeSpec.Builder {
val builder = TypeSpec.objectBuilder(pClass.toKotlinPoetName())
val docComment = pClass.docComment
@@ -636,9 +640,7 @@ class KotlinCodeGenerator(
.endControlFlow()
private fun PClass.toKotlinPoetName(): ClassName {
val index = moduleName.lastIndexOf(".")
val packageName = if (index == -1) "" else moduleName.substring(0, index)
val moduleTypeName = moduleName.substring(index + 1).replaceFirstChar { it.titlecaseChar() }
val (packageName, moduleTypeName) = nameMapper.map(moduleName)
return if (isModuleClass) {
ClassName(packageName, moduleTypeName)
} else {
@@ -647,8 +649,7 @@ class KotlinCodeGenerator(
}
private fun TypeAlias.toKotlinPoetName(): ClassName {
val index = moduleName.lastIndexOf(".")
val packageName = if (index == -1) "" else moduleName.substring(0, index)
val (packageName, moduleTypeName) = nameMapper.map(moduleName)
return when {
aliasedType is PType.Alias -> {
@@ -663,7 +664,6 @@ class KotlinCodeGenerator(
)
}
// Kotlin type generated for [this] is a nested enum class
val moduleTypeName = moduleName.substring(index + 1).replaceFirstChar { it.titlecaseChar() }
ClassName(packageName, moduleTypeName, simpleName)
}
else -> {
@@ -786,4 +786,6 @@ class KotlinCodeGenerator(
private fun List<PType>.toKotlinPoet(): Array<TypeName> =
map { it.toKotlinPoetName() }.toTypedArray()
private val nameMapper = NameMapper(options.renames)
}

View File

@@ -17,6 +17,7 @@
package org.pkl.codegen.kotlin
import com.github.ajalt.clikt.parameters.options.associate
import com.github.ajalt.clikt.parameters.options.default
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option
@@ -81,6 +82,21 @@ class PklKotlinCodegenCommand :
)
.flag()
private val renames: Map<String, String> by
option(
names = arrayOf("--rename"),
metavar = "<old_name=new_name>",
help =
"""
Replace a prefix in the names of the generated Kotlin classes (repeatable).
By default, the names of generated classes are derived from the Pkl module names.
With this option, you can override the modify the default names, renaming entire
classes or just their packages.
"""
.trimIndent()
)
.associate()
override fun run() {
val options =
CliKotlinCodeGeneratorOptions(
@@ -89,7 +105,8 @@ class PklKotlinCodegenCommand :
indent = indent,
generateKdoc = generateKdoc,
generateSpringBootConfig = generateSpringboot,
implementSerializable = implementSerializable
implementSerializable = implementSerializable,
renames = renames
)
CliKotlinCodeGenerator(options).run()
}

View File

@@ -152,6 +152,119 @@ class CliKotlinCodeGeneratorTest {
)
}
@Test
fun `custom package names`(@TempDir tempDir: Path) {
val module1 =
PklModule(
"org.foo.Module1",
"""
module org.foo.Module1
class Person {
name: String
}
"""
)
val module2 =
PklModule(
"org.bar.Module2",
"""
module org.bar.Module2
import "../../org/foo/Module1.pkl"
class Group {
owner: Module1.Person
name: String
}
"""
)
val module3 =
PklModule(
"org.baz.Module3",
"""
module org.baz.Module3
import "../../org/bar/Module2.pkl"
class Supergroup {
owner: Module2.Group
}
"""
)
val module1PklFile = module1.writeToDisk(tempDir.resolve("org/foo/Module1.pkl"))
val module2PklFile = module2.writeToDisk(tempDir.resolve("org/bar/Module2.pkl"))
val module3PklFile = module3.writeToDisk(tempDir.resolve("org/baz/Module3.pkl"))
val outputDir = tempDir.resolve("output")
val generator =
CliKotlinCodeGenerator(
CliKotlinCodeGeneratorOptions(
CliBaseOptions(listOf(module1PklFile, module2PklFile, module3PklFile).map { it.toUri() }),
outputDir,
renames = mapOf("org.foo" to "com.foo.x", "org.baz" to "com.baz.a.b")
)
)
generator.run()
val module1KotlinFile = outputDir.resolve("kotlin/com/foo/x/Module1.kt")
module1KotlinFile.readString().let {
assertContains("package com.foo.x", it)
assertContains("object Module1 {", it)
assertContains(
"""
| data class Person(
| val name: String
| )
""",
it
)
}
val module2KotlinFile = outputDir.resolve("kotlin/org/bar/Module2.kt")
module2KotlinFile.readString().let {
assertContains("package org.bar", it)
assertContains("import com.foo.x.Module1", it)
assertContains("object Module2 {", it)
assertContains(
"""
| data class Group(
| val owner: Module1.Person,
| val name: String
| )
""",
it
)
}
val module3KotlinFile = outputDir.resolve("kotlin/com/baz/a/b/Module3.kt")
module3KotlinFile.readString().let {
assertContains("package com.baz.a.b", it)
assertContains("import org.bar.Module2", it)
assertContains("object Module3 {", it)
assertContains(
"""
| data class Supergroup(
| val owner: Module2.Group
| )
""",
it
)
}
}
private fun assertContains(part: String, code: String) {
val trimmedPart = part.trim().trimMargin()
if (!code.contains(trimmedPart)) {

View File

@@ -56,6 +56,7 @@ object InMemoryKotlinCompiler {
val (importLines, remainder) =
sourceFiles.entries
.filter { (filename, _) -> filename.endsWith(".kt") }
.flatMap { (_, text) -> text.lines() }
.partition { it.startsWith("import") }
val importBlock = importLines.sorted().distinct()

View File

@@ -32,6 +32,8 @@ import org.pkl.core.util.IoUtils
class KotlinCodeGeneratorTest {
companion object {
const val MAPPER_PREFIX = "resources/META-INF/org/pkl/config/java/mapper/classes"
// according to:
// https://github.com/JetBrains/kotlin/blob/master/core/descriptors/
// src/org/jetbrains/kotlin/renderer/KeywordStringsGenerated.java
@@ -153,9 +155,13 @@ class KotlinCodeGeneratorTest {
private fun compileKotlinCode(kotlinCode: String): Map<String, KClass<*>> =
InMemoryKotlinCompiler.compile(mapOf("my/Mod.kt" to kotlinCode))
private fun assertCompilesSuccessfully(sourceText: String) = compileKotlinCode(sourceText)
private fun assertCompilesSuccessfully(sourceText: String) {
assertThatCode { compileKotlinCode(sourceText) }.doesNotThrowAnyException()
}
}
@TempDir lateinit var tempDir: Path
@Test
fun testEquals() {
val ctor = simpleClass.constructors.first()
@@ -1206,7 +1212,7 @@ class KotlinCodeGeneratorTest {
}
@Test
fun `import module`(@TempDir tempDir: Path) {
fun `import module`() {
val library =
PklModule(
"library",
@@ -1235,7 +1241,7 @@ class KotlinCodeGeneratorTest {
.trimIndent()
)
val kotlinSourceFiles = generateKotlinFiles(tempDir, library, client)
val kotlinSourceFiles = generateFiles(library, client)
val kotlinClientCode =
kotlinSourceFiles.entries.find { (fileName, _) -> fileName.endsWith("Client.kt") }!!.value
@@ -1253,7 +1259,7 @@ class KotlinCodeGeneratorTest {
}
@Test
fun `extend module`(@TempDir tempDir: Path) {
fun `extend module`() {
val base =
PklModule(
"base",
@@ -1282,7 +1288,7 @@ class KotlinCodeGeneratorTest {
.trimIndent()
)
val kotlinSourceFiles = generateKotlinFiles(tempDir, base, derived)
val kotlinSourceFiles = generateFiles(base, derived)
val kotlinDerivedCode =
kotlinSourceFiles.entries.find { (filename, _) -> filename.endsWith("Derived.kt") }!!.value
@@ -1317,7 +1323,7 @@ class KotlinCodeGeneratorTest {
}
@Test
fun `extend module that only contains type aliases`(@TempDir tempDir: Path) {
fun `extend module that only contains type aliases`() {
val moduleOne =
PklModule(
"base",
@@ -1342,7 +1348,7 @@ class KotlinCodeGeneratorTest {
.trimIndent()
)
val kotlinSourceFiles = generateKotlinFiles(tempDir, moduleOne, moduleTwo)
val kotlinSourceFiles = generateFiles(moduleOne, moduleTwo)
val kotlinDerivedCode =
kotlinSourceFiles.entries.find { (filename, _) -> filename.endsWith("Derived.kt") }!!.value
@@ -1359,7 +1365,7 @@ class KotlinCodeGeneratorTest {
}
@Test
fun `generated properties files`(@TempDir tempDir: Path) {
fun `generated properties files`() {
val pklModule =
PklModule(
"Mod.pkl",
@@ -1380,17 +1386,17 @@ class KotlinCodeGeneratorTest {
"""
.trimIndent()
)
val generated = generateFiles(tempDir, pklModule)
val generated = generateFiles(pklModule)
val expectedPropertyFile =
"resources/META-INF/org/pkl/config/java/mapper/classes/org.pkl.Mod.properties"
assertThat(generated).containsKey(expectedPropertyFile)
val propertyFileContents = generated[expectedPropertyFile]!!
assertThat(propertyFileContents)
.contains("org.pkl.config.java.mapper.org.pkl.Mod\\#ModuleClass=org.pkl.Mod")
assertThat(propertyFileContents)
.contains("org.pkl.config.java.mapper.org.pkl.Mod\\#Foo=org.pkl.Mod\$Foo")
assertThat(propertyFileContents)
.contains("org.pkl.config.java.mapper.org.pkl.Mod\\#Bar=org.pkl.Mod\$Bar")
.contains(
"org.pkl.config.java.mapper.org.pkl.Mod\\#ModuleClass=org.pkl.Mod",
"org.pkl.config.java.mapper.org.pkl.Mod\\#Foo=org.pkl.Mod\$Foo",
"org.pkl.config.java.mapper.org.pkl.Mod\\#Bar=org.pkl.Mod\$Bar"
)
}
@Test
@@ -1502,45 +1508,253 @@ class KotlinCodeGeneratorTest {
}
@Test
fun `encoded file paths`(@TempDir path: Path) {
fun `encoded file paths`() {
val kotlinCode =
generateKotlinFiles(
path,
generateFiles(
PklModule(
"FooBar.pkl",
"""
module `Foo*Bar`
someProp: String
"""
module `Foo*Bar`
someProp: String
"""
.trimIndent()
)
)
assertThat(kotlinCode).containsKey("kotlin/Foo(2a)Bar.kt")
}
private fun generateFiles(tempDir: Path, vararg pklModules: PklModule): Map<String, String> {
val pklFiles = pklModules.map { it.writeToDisk(tempDir.resolve("pkl/${it.name}.pkl")) }
val evaluator = Evaluator.preconfigured()
return pklFiles.fold(mapOf()) { acc, pklFile ->
val pklSchema = evaluator.evaluateSchema(ModuleSource.path(pklFile))
acc + KotlinCodeGenerator(pklSchema, KotlinCodegenOptions()).output
}
@Test
fun `override names in a standalone module`() {
val files =
KotlinCodegenOptions(
renames = mapOf("a.b.c" to "x.y.z", "d.e.f.AnotherModule" to "u.v.w.RenamedModule")
)
.generateFiles(
"MyModule.pkl" to
"""
module a.b.c.MyModule
foo: String = "abc"
"""
.trimIndent(),
"AnotherModule.pkl" to
"""
module d.e.f.AnotherModule
bar: Int = 123
"""
.trimIndent()
)
.toMutableMap()
files.validateContents(
"kotlin/x/y/z/MyModule.kt" to listOf("package x.y.z", "data class MyModule("),
"$MAPPER_PREFIX/a.b.c.MyModule.properties" to
listOf("org.pkl.config.java.mapper.a.b.c.MyModule\\#ModuleClass=x.y.z.MyModule"),
// ---
"kotlin/u/v/w/RenamedModule.kt" to listOf("package u.v.w", "data class RenamedModule("),
"$MAPPER_PREFIX/d.e.f.AnotherModule.properties" to
listOf("org.pkl.config.java.mapper.d.e.f.AnotherModule\\#ModuleClass=u.v.w.RenamedModule"),
)
}
private fun generateKotlinFiles(
tempDir: Path,
@Test
fun `override names based on the longest prefix`() {
val files =
KotlinCodegenOptions(
renames = mapOf("com.foo.bar." to "x.", "com.foo." to "y.", "com." to "z.", "" to "w.")
)
.generateFiles(
"com/foo/bar/Module1" to
"""
module com.foo.bar.Module1
bar: String
"""
.trimIndent(),
"com/Module2" to
"""
module com.Module2
com: String
"""
.trimIndent(),
"org/baz/Module3" to
"""
module org.baz.Module3
baz: String
"""
.trimIndent()
)
.toMutableMap()
files.validateContents(
"kotlin/x/Module1.kt" to listOf("package x", "data class Module1("),
"$MAPPER_PREFIX/com.foo.bar.Module1.properties" to
listOf("org.pkl.config.java.mapper.com.foo.bar.Module1\\#ModuleClass=x.Module1"),
// ---
"kotlin/z/Module2.kt" to listOf("package z", "data class Module2("),
"$MAPPER_PREFIX/com.Module2.properties" to
listOf("org.pkl.config.java.mapper.com.Module2\\#ModuleClass=z.Module2"),
// ---
"kotlin/w/org/baz/Module3.kt" to listOf("package w.org.baz", "data class Module3("),
"$MAPPER_PREFIX/org.baz.Module3.properties" to
listOf("org.pkl.config.java.mapper.org.baz.Module3\\#ModuleClass=w.org.baz.Module3")
)
}
@Test
fun `override names in multiple modules using each other`() {
val files =
KotlinCodegenOptions(
renames =
mapOf(
"org.foo" to "com.foo.x",
"org.bar.Module2" to "org.bar.RenamedModule",
"org.baz" to "com.baz.a.b"
)
)
.generateFiles(
"org/foo/Module1" to
"""
module org.foo.Module1
class Person {
name: String
}
"""
.trimIndent(),
"org/bar/Module2" to
"""
module org.bar.Module2
import "../../org/foo/Module1.pkl"
class Group {
owner: Module1.Person
name: String
}
"""
.trimIndent(),
"org/baz/Module3" to
"""
module org.baz.Module3
import "../../org/bar/Module2.pkl"
class Supergroup {
owner: Module2.Group
}
"""
.trimIndent()
)
files.validateContents(
"kotlin/com/foo/x/Module1.kt" to
listOf("package com.foo.x", "object Module1 {", "data class Person("),
"$MAPPER_PREFIX/org.foo.Module1.properties" to
listOf(
"org.pkl.config.java.mapper.org.foo.Module1\\#ModuleClass=com.foo.x.Module1",
"org.pkl.config.java.mapper.org.foo.Module1\\#Person=com.foo.x.Module1${'$'}Person",
),
// ---
"kotlin/org/bar/RenamedModule.kt" to
listOf(
"package org.bar",
"import com.foo.x.Module1",
"object RenamedModule {",
"val owner: Module1.Person"
),
"$MAPPER_PREFIX/org.bar.Module2.properties" to
listOf(
"org.pkl.config.java.mapper.org.bar.Module2\\#ModuleClass=org.bar.RenamedModule",
"org.pkl.config.java.mapper.org.bar.Module2\\#Group=org.bar.RenamedModule${'$'}Group",
),
// ---
"kotlin/com/baz/a/b/Module3.kt" to
listOf(
"package com.baz.a.b",
"import org.bar.RenamedModule",
"object Module3 {",
"val owner: RenamedModule.Group"
),
"$MAPPER_PREFIX/org.baz.Module3.properties" to
listOf(
"org.pkl.config.java.mapper.org.baz.Module3\\#ModuleClass=com.baz.a.b.Module3",
"org.pkl.config.java.mapper.org.baz.Module3\\#Supergroup=com.baz.a.b.Module3${'$'}Supergroup",
),
)
}
@Test
fun `do not capitalize names of renamed classes`() {
val files =
KotlinCodegenOptions(
renames = mapOf("a.b.c.MyModule" to "x.y.z.renamed_module", "d.e.f." to "u.v.w.")
)
.generateFiles(
"MyModule.pkl" to
"""
module a.b.c.MyModule
foo: String = "abc"
"""
.trimIndent(),
"lower_module.pkl" to
"""
module d.e.f.lower_module
bar: Int = 123
"""
.trimIndent()
)
files.validateContents(
"kotlin/x/y/z/renamed_module.kt" to listOf("package x.y.z", "data class renamed_module("),
"$MAPPER_PREFIX/a.b.c.MyModule.properties" to
listOf("org.pkl.config.java.mapper.a.b.c.MyModule\\#ModuleClass=x.y.z.renamed_module"),
// ---
"kotlin/u/v/w/Lower_module.kt" to listOf("package u.v.w", "data class Lower_module("),
"$MAPPER_PREFIX/d.e.f.lower_module.properties" to
listOf("org.pkl.config.java.mapper.d.e.f.lower_module\\#ModuleClass=u.v.w.Lower_module"),
)
}
private fun Map<String, String>.validateContents(
vararg assertions: kotlin.Pair<String, List<String>>
) {
val files = toMutableMap()
for ((fileName, lines) in assertions) {
assertThat(files).containsKey(fileName)
assertThat(files.remove(fileName)).describedAs("Contents of $fileName").contains(lines)
}
assertThat(files).isEmpty()
}
private fun KotlinCodegenOptions.generateFiles(
vararg pklModules: PklModule
): Map<String, String> {
val pklFiles = pklModules.map { it.writeToDisk(tempDir.resolve("pkl/${it.name}.pkl")) }
val evaluator = Evaluator.preconfigured()
return pklFiles.fold(mapOf()) { acc, pklFile ->
val pklSchema = evaluator.evaluateSchema(ModuleSource.path(pklFile))
val generator = KotlinCodeGenerator(pklSchema, KotlinCodegenOptions())
acc + arrayOf(generator.kotlinFileName to generator.kotlinFile)
val generator = KotlinCodeGenerator(pklSchema, this)
acc + generator.output
}
}
private fun KotlinCodegenOptions.generateFiles(
vararg pklModules: kotlin.Pair<String, String>
): Map<String, String> =
generateFiles(*pklModules.map { (name, text) -> PklModule(name, text) }.toTypedArray())
private fun generateFiles(vararg pklModules: PklModule): Map<String, String> =
KotlinCodegenOptions().generateFiles(*pklModules)
private fun instantiateOtherAndPropertyTypes(): kotlin.Pair<Any, Any> {
val otherCtor = propertyTypesClasses.getValue("Other").constructors.first()
val other = otherCtor.call("pigeon")