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

@@ -54,7 +54,16 @@ data class CliJavaCodeGeneratorOptions(
val nonNullAnnotation: String? = null,
/** 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 Java 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 toJavaCodegenOptions() =
JavaCodegenOptions(
@@ -64,6 +73,7 @@ data class CliJavaCodeGeneratorOptions(
generateSpringBootConfig,
paramsAnnotation,
nonNullAnnotation,
implementSerializable
implementSerializable,
renames
)
}

View File

@@ -34,8 +34,10 @@ import kotlin.apply
import kotlin.let
import kotlin.takeIf
import kotlin.to
import org.pkl.commons.NameMapper
import org.pkl.core.*
import org.pkl.core.util.CodeGeneratorUtils
import org.pkl.core.util.IoUtils
class JavaCodeGeneratorException(message: String) : RuntimeException(message)
@@ -68,7 +70,15 @@ data class JavaCodegenOptions(
val nonNullAnnotation: String? = null,
/** 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()
)
/** Entrypoint for the Java code generator API. */
@@ -123,7 +133,8 @@ class JavaCodeGenerator(
}
private val propertyFileName: String
get() = "resources/META-INF/org/pkl/config/java/mapper/classes/${schema.moduleName}.properties"
get() =
"resources/META-INF/org/pkl/config/java/mapper/classes/${IoUtils.encodePath(schema.moduleName)}.properties"
private val propertiesFile: String
get() {
@@ -150,8 +161,16 @@ class JavaCodeGenerator(
return AnnotationSpec.builder(className).build()
}
val javaFileName: String
get() = relativeOutputPathFor(schema.moduleName)
private val javaFileName: String
get() {
val (packageName, className) = nameMapper.map(schema.moduleName)
val dirPath = packageName.replace('.', '/')
return if (dirPath.isEmpty()) {
"java/$className.java"
} else {
"java/$dirPath/$className.java"
}
}
val javaFile: String
get() {
@@ -183,9 +202,7 @@ class JavaCodeGenerator(
moduleClass.addMethod(appendPropertyMethod().addModifiers(modifier).build())
}
val moduleName = schema.moduleName
val index = moduleName.lastIndexOf(".")
val packageName = if (index == -1) "" else moduleName.substring(0, index)
val (packageName, _) = nameMapper.map(schema.moduleName)
return JavaFile.builder(packageName, moduleClass.build())
.indent(codegenOptions.indent)
@@ -193,20 +210,7 @@ class JavaCodeGenerator(
.toString()
}
private fun relativeOutputPathFor(moduleName: String): String {
val moduleNameParts = moduleName.split(".")
val dirPath = moduleNameParts.dropLast(1).joinToString("/")
val fileName = moduleNameParts.last().replaceFirstChar { it.titlecaseChar() }
return if (dirPath.isEmpty()) {
"java/$fileName.java"
} else {
"java/$dirPath/$fileName.java"
}
}
@Suppress("NAME_SHADOWING")
private fun generateTypeSpec(pClass: PClass, schema: ModuleSchema): TypeSpec.Builder {
val isModuleClass = pClass == schema.moduleClass
val javaPoetClassName = pClass.toJavaPoetName()
val superclass =
@@ -687,9 +691,7 @@ class JavaCodeGenerator(
.endControlFlow()
private fun PClass.toJavaPoetName(): ClassName {
val index = moduleName.lastIndexOf(".")
val packageName = if (index == -1) "" else moduleName.substring(0, index)
val moduleClassName = moduleName.substring(index + 1).replaceFirstChar { it.titlecaseChar() }
val (packageName, moduleClassName) = nameMapper.map(moduleName)
return if (isModuleClass) {
ClassName.get(packageName, moduleClassName)
} else {
@@ -699,9 +701,7 @@ class JavaCodeGenerator(
// generated type is a nested enum class
private fun TypeAlias.toJavaPoetName(): ClassName {
val index = moduleName.lastIndexOf(".")
val packageName = if (index == -1) "" else moduleName.substring(0, index)
val moduleClassName = moduleName.substring(index + 1).replaceFirstChar { it.titlecaseChar() }
val (packageName, moduleClassName) = nameMapper.map(moduleName)
return ClassName.get(packageName, moduleClassName, simpleName)
}
@@ -872,6 +872,8 @@ class JavaCodeGenerator(
} else key
}
}
private val nameMapper = NameMapper(codegenOptions.renames)
}
internal val javaReservedWords =

View File

@@ -17,6 +17,7 @@
package org.pkl.codegen.java
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
@@ -108,6 +109,21 @@ class PklJavaCodegenCommand :
)
.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 Java 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 =
CliJavaCodeGeneratorOptions(
@@ -119,7 +135,8 @@ class PklJavaCodegenCommand :
generateSpringBootConfig = generateSpringboot,
paramsAnnotation = paramsAnnotation,
nonNullAnnotation = nonNullAnnotation,
implementSerializable = implementSerializable
implementSerializable = implementSerializable,
renames = renames
)
CliJavaCodeGenerator(options).run()
}

View File

@@ -173,6 +173,115 @@ class CliJavaCodeGeneratorTest {
)
}
@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 =
CliJavaCodeGenerator(
CliJavaCodeGeneratorOptions(
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 module1JavaFile = outputDir.resolve("java/com/foo/x/Module1.java")
module1JavaFile.readString().let {
assertContains("package com.foo.x;", it)
assertContains("public final class Module1 {", it)
assertContains(
"""
| public static final class Person {
| public final @NonNull String name;
""",
it
)
}
val module2JavaFile = outputDir.resolve("java/org/bar/Module2.java")
module2JavaFile.readString().let {
assertContains("package org.bar;", it)
assertContains("import com.foo.x.Module1;", it)
assertContains("public final class Module2 {", it)
assertContains(
"""
| public static final class Group {
| public final Module1. @NonNull Person owner;
""",
it
)
}
val module3JavaFile = outputDir.resolve("java/com/baz/a/b/Module3.java")
module3JavaFile.readString().let {
assertContains("package com.baz.a.b;", it)
assertContains("import org.bar.Module2;", it)
assertContains("public final class Module3 {", it)
assertContains(
"""
| public static final class Supergroup {
| public final Module2. @NonNull Group owner;
""",
it
)
}
}
private fun assertContains(part: String, code: String) {
val trimmedPart = part.trim().trimMargin()
if (!code.contains(trimmedPart)) {

View File

@@ -30,7 +30,9 @@ object InMemoryJavaCompiler {
val fileManager =
InMemoryFileManager(compiler.getStandardFileManager(diagnosticsCollector, null, null))
val sourceObjects =
sourceFiles.map { (filename, contents) -> ReadableSourceFileObject(filename, contents) }
sourceFiles
.filter { (filename, _) -> filename.endsWith(".java") }
.map { (filename, contents) -> ReadableSourceFileObject(filename, contents) }
val task = compiler.getTask(null, fileManager, diagnosticsCollector, null, null, sourceObjects)
val result = task.call()
if (!result) {

View File

@@ -33,6 +33,8 @@ import org.pkl.core.util.IoUtils
class JavaCodeGeneratorTest {
companion object {
const val MAPPER_PREFIX = "resources/META-INF/org/pkl/config/java/mapper/classes"
private val simpleClass by lazy {
compileJavaCode(
generateJavaCode(
@@ -101,7 +103,8 @@ class JavaCodeGeneratorTest {
generateJavadoc: Boolean = false,
generateSpringBootConfig: Boolean = false,
nonNullAnnotation: String? = null,
implementSerializable: Boolean = false
implementSerializable: Boolean = false,
renames: Map<String, String> = emptyMap()
): String {
val module = Evaluator.preconfigured().evaluateSchema(text(pklCode))
val generator =
@@ -113,6 +116,7 @@ class JavaCodeGeneratorTest {
generateSpringBootConfig = generateSpringBootConfig,
nonNullAnnotation = nonNullAnnotation,
implementSerializable = implementSerializable,
renames = renames
)
)
return generator.javaFile
@@ -127,6 +131,8 @@ class JavaCodeGeneratorTest {
}
}
@TempDir lateinit var tempDir: Path
@Test
fun testEquals() {
val ctor = simpleClass.constructors.first()
@@ -1420,7 +1426,7 @@ class JavaCodeGeneratorTest {
}
@Test
fun `import module`(@TempDir tempDir: Path) {
fun `import module`() {
val library =
PklModule(
"library",
@@ -1449,7 +1455,7 @@ class JavaCodeGeneratorTest {
.trimIndent()
)
val javaSourceFiles = generateJavaFiles(tempDir, library, client)
val javaSourceFiles = generateFiles(library, client)
val javaClientCode =
javaSourceFiles.entries.find { (fileName, _) -> fileName.endsWith("Client.java") }!!.value
@@ -1467,7 +1473,7 @@ class JavaCodeGeneratorTest {
}
@Test
fun `extend module`(@TempDir tempDir: Path) {
fun `extend module`() {
val base =
PklModule(
"base",
@@ -1496,7 +1502,7 @@ class JavaCodeGeneratorTest {
.trimIndent()
)
val javaSourceFiles = generateJavaFiles(tempDir, base, derived)
val javaSourceFiles = generateFiles(base, derived)
val javaDerivedCode =
javaSourceFiles.entries.find { (filename, _) -> filename.endsWith("Derived.java") }!!.value
@@ -1520,7 +1526,7 @@ class JavaCodeGeneratorTest {
}
@Test
fun `extend module that only contains type aliases`(@TempDir tempDir: Path) {
fun `extend module that only contains type aliases`() {
val base =
PklModule(
"base",
@@ -1545,7 +1551,7 @@ class JavaCodeGeneratorTest {
.trimIndent()
)
val javaSourceFiles = generateJavaFiles(tempDir, base, derived)
val javaSourceFiles = generateFiles(base, derived)
val javaDerivedCode =
javaSourceFiles.entries.find { (filename, _) -> filename.endsWith("Derived.java") }!!.value
@@ -1561,7 +1567,7 @@ class JavaCodeGeneratorTest {
}
@Test
fun `generated properties files`(@TempDir tempDir: Path) {
fun `generated properties files`() {
val pklModule =
PklModule(
"Mod.pkl",
@@ -1582,7 +1588,7 @@ class JavaCodeGeneratorTest {
"""
.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)
@@ -1596,7 +1602,7 @@ class JavaCodeGeneratorTest {
}
@Test
fun `generated properties files with normalized java name`(@TempDir tempDir: Path) {
fun `generated properties files with normalized java name`() {
val pklModule =
PklModule(
"mod.pkl",
@@ -1617,7 +1623,7 @@ class JavaCodeGeneratorTest {
"""
.trimIndent()
)
val generated = generateFiles(tempDir, pklModule)
val generated = generateFiles(pklModule)
val expectedPropertyFile =
"resources/META-INF/org/pkl/config/java/mapper/classes/my.mod.properties"
assertThat(generated).containsKey(expectedPropertyFile)
@@ -1824,24 +1830,235 @@ class JavaCodeGeneratorTest {
assertCompilesSuccessfully(javaCode)
}
private fun generateFiles(tempDir: Path, vararg pklModules: PklModule): Map<String, String> {
@Test
fun `override names in a standalone module`() {
val files =
JavaCodegenOptions(
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(
"java/x/y/z/MyModule.java" to listOf("package x.y.z;", "public final 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"),
// ---
"java/u/v/w/RenamedModule.java" to
listOf("package u.v.w;", "public final 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"),
)
}
@Test
fun `override names based on the longest prefix`() {
val files =
JavaCodegenOptions(
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()
)
files.validateContents(
"java/x/Module1.java" to listOf("package x;", "public final class Module1 {"),
"$MAPPER_PREFIX/com.foo.bar.Module1.properties" to
listOf("org.pkl.config.java.mapper.com.foo.bar.Module1\\#ModuleClass=x.Module1"),
// ---
"java/z/Module2.java" to listOf("package z;", "public final class Module2 {"),
"$MAPPER_PREFIX/com.Module2.properties" to
listOf("org.pkl.config.java.mapper.com.Module2\\#ModuleClass=z.Module2"),
// ---
"java/w/org/baz/Module3.java" to listOf("package w.org.baz;", "public final 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 =
JavaCodegenOptions(
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(
"java/com/foo/x/Module1.java" to listOf("package com.foo.x;", "public final class Module1 {"),
"$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",
),
// ---
"java/org/bar/RenamedModule.java" to
listOf(
"package org.bar;",
"import com.foo.x.Module1;",
"public final class RenamedModule {",
"public final Module1. @NonNull Person owner;"
),
"$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",
),
// ---
"java/com/baz/a/b/Module3.java" to
listOf(
"package com.baz.a.b;",
"import org.bar.RenamedModule;",
"public final class Module3 {",
"public final RenamedModule. @NonNull Group owner;"
),
"$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 =
JavaCodegenOptions(
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(
"java/x/y/z/renamed_module.java" to
listOf("package x.y.z;", "public final 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"),
// ---
"java/u/v/w/Lower_module.java" to
listOf("package u.v.w;", "public final 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 JavaCodegenOptions.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(path(pklFile))
acc + JavaCodeGenerator(pklSchema, JavaCodegenOptions()).output
val generator = JavaCodeGenerator(pklSchema, this)
acc + generator.output
}
}
private fun generateJavaFiles(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(path(pklFile))
val generator = JavaCodeGenerator(pklSchema, JavaCodegenOptions())
acc + arrayOf(generator.javaFileName to generator.javaFile)
}
}
private fun JavaCodegenOptions.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> =
JavaCodegenOptions().generateFiles(*pklModules)
private fun instantiateOtherAndPropertyTypes(): kotlin.Pair<Any, Any> {
val otherCtor = propertyTypesClasses.getValue("my.Mod\$Other").constructors.first()