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

@@ -164,13 +164,6 @@ This annotation is required to have `java.lang.annotation.ElementType.TYPE_USE`
or it may generate code that does not compile.
====
.--implement-serializable
[%collapsible]
====
Default: (flag not set) +
Whether to make generated classes implement `java.io.Serializable`.
====
Common code generator options:
include::{partialsdir}/cli-codegen-options.adoc[]

View File

@@ -21,3 +21,57 @@ Relative paths are resolved against the working directory.
Default: (not set) +
Flag that indicates to generate config classes for use with Spring Boot.
====
.--implement-serializable
[%collapsible]
====
Default: (not set) +
Whether to make generated classes implement `java.io.Serializable`.
====
.--rename
[%collapsible]
====
Default: (none) +
Example: `foo.=com.example.foo.` +
Allows to change default class and package names (derived from Pkl module names) in the generated code.
When you need the generated class or package names to be different from the default names derived from the Pkl module names, you can define a rename mapping, where the key is the original Pkl module name prefix, and the value is its replacement.
When you do, the generated code's `package` declarations, class names, as well as file locations, will be modified according to this mapping.
The prefixes are replaced literally, which means that dots at the end are important.
If you want to rename packages only, in most cases, you must ensure that you have an ending dot on both sides of a mapping (except for an empty mapping, if you use it), otherwise you may get unexpected results:
----
// Assuming the following arguments:
--rename com.foo.=x // Dot on the left only
--rename org.bar=y. // Dot on the right only
--rename net.baz=z // No dots
// The following renames will be made:
"com.foo.bar" -> "xbar" // Target prefix merged into the suffix
"org.bar.baz" -> "y..baz" // Double dot, invalid name
"net.baz.qux" -> "z.qux" // Looks okay, but...
"net.bazqux" -> "zqux" // ...may cut the name in the middle.
----
When computing the appropriate target name, the longest matching prefix is used:
----
// Assuming the following arguments:
--rename com.foo.Main=w.Main
--rename com.foo.=x.
--rename com.=y.
--rename =z.
// The following renames will be made:
com.foo.Main -> w.Main
com.foo.bar -> x.bar
com.baz.qux -> y.baz.qux
org.foo.bar -> z.org.foo.bar
----
Repeat this option to define multiple mappings.
Keys can be arbitrary strings, including an empty string.
Values must be valid dot-separated fully qualified class name prefixes, possibly terminated by a dot.
====

View File

@@ -36,4 +36,54 @@ Example: `generateSpringBootConfig = true` +
Whether to generate config classes for use with Spring Boot.
====
.packageMapping: MapProperty<String, String>
[%collapsible]
====
Default: `[:]` +
Example: `packageMapping = ["foo.": "com.example.foo.", "bar.Config": "com.example.bar.Config"]` +
Allows to change default class and package names (derived from Pkl module names) in the generated code.
When you need the generated class or package names to be different from the default names derived from the Pkl module names, you can define a rename mapping, where the key is the original Pkl module name prefix, and the value is its replacement.
When you do, the generated code's `package` declarations, class names, as well as file locations, will be modified according to this mapping.
The prefixes are replaced literally, which means that dots at the end are important.
If you want to rename packages only, in most cases, you must ensure that you have an ending dot on both sides of a mapping (except for an empty mapping, if you use it), otherwise you may get unexpected results:
....
// Assuming the following mapping configuration:
packageMapping = [
"com.foo.": "x", // Dot on the left only
"org.bar": "y.", // Dot on the right only
"net.baz": "z" // No dots
]
// The following renames will be made:
"com.foo.bar" -> "xbar" // Target prefix merged into the suffix
"org.bar.baz" -> "y..baz" // Double dot, invalid name
"net.baz.qux" -> "z.qux" // Looks okay, but...
"net.bazqux" -> "zqux" // ...may cut the name in the middle.
....
When computing the appropriate target name, the longest matching prefix is used:
....
// Assuming the following mapping configuration:
packageMapping = [
"com.foo.Main": "w.Main",
"com.foo.": "x.",
"com.": "y.",
"": "z."
]
// The following renames will be made:
com.foo.Main -> w.Main
com.foo.bar -> x.bar
com.baz.qux -> y.baz.qux
org.foo.bar -> z.org.foo.bar
....
Keys in this mapping can be arbitrary strings, including an empty string.
Values must be valid dot-separated fully qualifed class name prefixes, possibly terminated by a dot.
====
// TODO: fixme (implementSerializable)

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()

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`
module `Foo*Bar`
someProp: String
"""
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")

View File

@@ -0,0 +1,73 @@
/**
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.commons
/**
* A helper class for translating names of Pkl modules to different names of classes and/or objects
* in the target language of a code generation execution.
*
* The `mapping` parameter is expected to contain valid prefixes of Pkl module names, with an
* optional dot at the end, and values should be valid class names in the language for which code
* generation is performed.
*
* If the rename patterns do not explicitly rename the class, the class name is capitalized.
*
* When computing the appropriate target name, the longest matching prefix is used.
*
* Prefix replacements are literal, and therefore dots are important. When renaming packages, in
* most cases, you must ensure that you have an ending dot on both sides of a mapping (except for
* the empty mapping, if you use it), otherwise you may get unexpected results:
* ```kotlin
* val mapper = NameMapper(
* mapOf(
* "com.foo." to "x", // Dot on the left only
* "org.bar" to "y.", // Dot on the right only
* "net.baz" to "z" // No dots
* )
* )
*
* assertThat(mapper.map("com.foo.bar")).isEqualTo("" to "xbar") // Target prefix merged into the suffix
* assertThat(mapper.map("org.bar.baz")).isEqualTo("y." to "Baz") // Double dot, invalid package name
* assertThat(mapper.map("net.baz.qux")).isEqualTo("z" to "Qux") // Looks okay, but...
* assertThat(mapper.map("net.bazqux")).isEqualTo("" to "zqux") // ...may cut the package name in the middle.
* ```
*/
class NameMapper(mapping: Map<String, String>) {
private val sortedMapping = mapping.toList().sortedBy { -it.first.length }
private fun doMap(sourceName: String): Pair<String, Boolean> {
for ((sourcePrefix, targetPrefix) in sortedMapping) {
if (sourceName.startsWith(sourcePrefix)) {
val rest = sourceName.substring(sourcePrefix.length)
val mapped = targetPrefix + rest
val wasClassRenamed =
!targetPrefix.endsWith('.') && (sourcePrefix.length - 1) >= sourceName.lastIndexOf('.')
return mapped to wasClassRenamed
}
}
return sourceName to false
}
fun map(sourceName: String): Pair<String, String> {
val (mappedName, wasClassRenamed) = doMap(sourceName)
val packageName = mappedName.substringBeforeLast(".", "")
val mappedClassName = mappedName.substringAfterLast(".")
val className =
if (wasClassRenamed) mappedClassName
else mappedClassName.replaceFirstChar { it.titlecaseChar() }
return packageName to className
}
}

View File

@@ -0,0 +1,59 @@
/**
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.commons
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
class NameMapperTest {
@Test
fun `empty prefixes everything`() {
val mapper = NameMapper(mapOf("" to "bar."))
assertThat(mapper.map("foo.bar.Baz")).isEqualTo("bar.foo.bar" to "Baz")
assertThat(mapper.map("Baz")).isEqualTo("bar" to "Baz")
}
@Test
fun `longest prefix wins`() {
val mapper = NameMapper(mapOf("bar." to "com.bar.", "bar.baz." to "foo.bar."))
assertThat(mapper.map("bar.baz.Buzzy")).isEqualTo("foo.bar" to "Buzzy")
}
@Test
fun `implicit uppercase classname`() {
val mapper = NameMapper(mapOf("foo." to "bar."))
assertThat(mapper.map("foo.bar.baz")).isEqualTo("bar.bar" to "Baz")
assertThat(mapper.map("foo.bar")).isEqualTo("bar" to "Bar")
assertThat(mapper.map("baz")).isEqualTo("" to "Baz")
assertThat(mapper.map("baz")).isEqualTo("" to "Baz")
}
@Test
fun `no implicit uppercased classname if explicitly renamed`() {
val mapper =
NameMapper(
mapOf(
"foo.bar" to "bar.bar",
"foo.c" to "foo.z",
"com.foo." to "x",
)
)
assertThat(mapper.map("foo.bar")).isEqualTo("bar" to "bar")
assertThat(mapper.map("foo.bar")).isEqualTo("bar" to "bar")
assertThat(mapper.map("foo.cow")).isEqualTo("foo" to "zow")
assertThat(mapper.map("com.foo.bar")).isEqualTo("" to "xbar")
}
}

View File

@@ -26,7 +26,11 @@ dependencies {
//
// To debug shaded code in IntelliJ, temporarily remove the conditional.
if (System.getProperty("idea.sync.active") == null) {
runtimeOnly(project(":pkl-tools", "fatJar"))
runtimeOnly(projects.pklTools) {
attributes {
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.SHADOWED))
}
}
}
testImplementation(projects.pklCommonsTest)

View File

@@ -412,6 +412,7 @@ public class PklPlugin implements Plugin<Project> {
task.getOutputDir().set(spec.getOutputDir());
task.getGenerateSpringBootConfig().set(spec.getGenerateSpringBootConfig());
task.getImplementSerializable().set(spec.getImplementSerializable());
task.getPackageMapping().set(spec.getPackageMapping());
}
private <T extends BasePklTask, S extends BasePklSpec> void configureBaseTask(T task, S spec) {

View File

@@ -16,6 +16,7 @@
package org.pkl.gradle.spec;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.provider.MapProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.SourceSet;
@@ -30,4 +31,6 @@ public interface CodeGenSpec extends ModulesSpec {
Property<Boolean> getGenerateSpringBootConfig();
Property<Boolean> getImplementSerializable();
MapProperty<String, String> getPackageMapping();
}

View File

@@ -16,6 +16,7 @@
package org.pkl.gradle.task;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.provider.MapProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.OutputDirectory;
@@ -32,4 +33,7 @@ public abstract class CodeGenTask extends ModulesTask {
@Input
public abstract Property<Boolean> getImplementSerializable();
@Input
public abstract MapProperty<String, String> getPackageMapping();
}

View File

@@ -52,7 +52,8 @@ public abstract class JavaCodeGenTask extends CodeGenTask {
getGenerateSpringBootConfig().get(),
getParamsAnnotation().getOrNull(),
getNonNullAnnotation().getOrNull(),
getImplementSerializable().get()))
getImplementSerializable().get(),
getPackageMapping().get()))
.run();
}
}

View File

@@ -37,7 +37,8 @@ public abstract class KotlinCodeGenTask extends CodeGenTask {
getIndent().get(),
getGenerateKdoc().get(),
getGenerateSpringBootConfig().get(),
getImplementSerializable().get()))
getImplementSerializable().get(),
getPackageMapping().get()))
.run();
}
}

View File

@@ -13,7 +13,7 @@ class JavaCodeGeneratorsTest : AbstractTest() {
runTask("configClasses")
val baseDir = testProjectDir.resolve("build/generated/java/org")
val baseDir = testProjectDir.resolve("build/generated/java/foo/bar")
val moduleFile = baseDir.resolve("Mod.java")
assertThat(baseDir.listDirectoryEntries().count()).isEqualTo(1)
@@ -36,7 +36,7 @@ class JavaCodeGeneratorsTest : AbstractTest() {
| public static final class Person {
| public final @Nonnull String name;
|
| public final @Nonnull List<@Nonnull Address> addresses;
| public final @Nonnull List<Address> addresses;
"""
)
@@ -53,28 +53,14 @@ class JavaCodeGeneratorsTest : AbstractTest() {
@Test
fun `compile generated code`() {
writeBuildFile()
writeFile("mod.pkl", """
module org.mod
class Person {
name: String
addresses: List<Address?>
}
class Address {
street: String
zip: Int
}
other: Any = 42
""".trimIndent())
writePklFile()
runTask("compileJava")
val classesDir = testProjectDir.resolve("build/classes/java/main")
val moduleClassFile = classesDir.resolve("org/Mod.class")
val personClassFile = classesDir.resolve("org/Mod\$Person.class")
val addressClassFile = classesDir.resolve("org/Mod\$Address.class")
val moduleClassFile = classesDir.resolve("foo/bar/Mod.class")
val personClassFile = classesDir.resolve("foo/bar/Mod\$Person.class")
val addressClassFile = classesDir.resolve("foo/bar/Mod\$Address.class")
assertThat(moduleClassFile).exists()
assertThat(personClassFile).exists()
assertThat(addressClassFile).exists()
@@ -127,6 +113,9 @@ class JavaCodeGeneratorsTest : AbstractTest() {
paramsAnnotation = "javax.inject.Named"
nonNullAnnotation = "javax.annotation.Nonnull"
settingsModule = "pkl:settings"
packageMapping = [
'org': 'foo.bar'
]
}
}
}
@@ -134,18 +123,6 @@ class JavaCodeGeneratorsTest : AbstractTest() {
)
}
private fun writeGradlePropertiesFile() {
writeFile("gradle.properties", """
systemProp.http.proxyHost=proxy.config.pcp.local
systemProp.http.proxyPort=3128
systemProp.http.nonProxyHosts=localhost|*.apple.com
systemProp.https.proxyHost=proxy.config.pcp.local
systemProp.https.proxyPort=3128
systemProp.https.nonProxyHosts=localhost|*.apple.com
""")
}
private fun writePklFile() {
writeFile(
"mod.pkl", """
@@ -153,7 +130,7 @@ class JavaCodeGeneratorsTest : AbstractTest() {
class Person {
name: String
addresses: List<Address>
addresses: List<Address?>
}
class Address {

View File

@@ -13,7 +13,7 @@ class KotlinCodeGeneratorsTest : AbstractTest() {
runTask("configClasses")
val baseDir = testProjectDir.resolve("build/generated/kotlin/org")
val baseDir = testProjectDir.resolve("build/generated/kotlin/foo/bar")
val kotlinFile = baseDir.resolve("Mod.kt")
assertThat(baseDir.listDirectoryEntries().count()).isEqualTo(1)
@@ -58,9 +58,9 @@ class KotlinCodeGeneratorsTest : AbstractTest() {
runTask("compileKotlin")
val classesDir = testProjectDir.resolve("build/classes/kotlin/main")
val moduleClassFile = classesDir.resolve("org/Mod.class")
val personClassFile = classesDir.resolve("org/Mod\$Person.class")
val addressClassFile = classesDir.resolve("org/Mod\$Address.class")
val moduleClassFile = classesDir.resolve("foo/bar/Mod.class")
val personClassFile = classesDir.resolve("foo/bar/Mod\$Person.class")
val addressClassFile = classesDir.resolve("foo/bar/Mod\$Address.class")
assertThat(moduleClassFile).exists()
assertThat(personClassFile).exists()
assertThat(addressClassFile).exists()
@@ -125,6 +125,9 @@ class KotlinCodeGeneratorsTest : AbstractTest() {
sourceModules = ["mod.pkl"]
outputDir = file("build/generated")
settingsModule = "pkl:settings"
packageMapping = [
'org.': 'foo.bar.'
]
}
}
}