diff --git a/pkl-codegen-kotlin/src/main/kotlin/org/pkl/codegen/kotlin/CliKotlinCodeGeneratorOptions.kt b/pkl-codegen-kotlin/src/main/kotlin/org/pkl/codegen/kotlin/CliKotlinCodeGeneratorOptions.kt index 2800fdea..bf56f90d 100644 --- a/pkl-codegen-kotlin/src/main/kotlin/org/pkl/codegen/kotlin/CliKotlinCodeGeneratorOptions.kt +++ b/pkl-codegen-kotlin/src/main/kotlin/org/pkl/codegen/kotlin/CliKotlinCodeGeneratorOptions.kt @@ -29,6 +29,9 @@ data class CliKotlinCodeGeneratorOptions( /** The characters to use for indenting generated source code. */ val indent: String = " ", + /** Kotlin package to use for generated code; if none is provided, the root package is used. */ + val kotlinPackage: String = "", + /** Whether to generate Kdoc based on doc comments for Pkl modules, classes, and properties. */ val generateKdoc: Boolean = false, @@ -39,5 +42,11 @@ data class CliKotlinCodeGeneratorOptions( val implementSerializable: Boolean = false ) { fun toKotlinCodegenOptions(): KotlinCodegenOptions = - KotlinCodegenOptions(indent, generateKdoc, generateSpringBootConfig, implementSerializable) + KotlinCodegenOptions( + indent = indent, + generateKdoc = generateKdoc, + generateSpringBootConfig = generateSpringBootConfig, + implementSerializable = implementSerializable, + kotlinPackage = kotlinPackage, + ) } 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 f2699e7e..b67c291d 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 @@ -27,6 +27,9 @@ data class KotlinCodegenOptions( /** The characters to use for indenting generated Kotlin code. */ val indent: String = " ", + /** Kotlin package to use for generated code; if none is provided, the root package is used. */ + val kotlinPackage: String = "", + /** Whether to generate KDoc based on doc comments for Pkl modules, classes, and properties. */ val generateKdoc: Boolean = false, @@ -111,6 +114,9 @@ class KotlinCodeGenerator( append("lin/${relativeOutputPathFor(moduleSchema.moduleName)}") } + val kotlinPackage: String? + get() = options.kotlinPackage.ifEmpty { null } + val kotlinFile: String get() { if (moduleSchema.moduleUri.scheme == "pkl") { @@ -123,6 +129,7 @@ class KotlinCodeGenerator( val hasProperties = pModuleClass.properties.any { !it.value.isHidden } val isGenerateClass = hasProperties || pModuleClass.isOpen || pModuleClass.isAbstract + val packagePrefix = kotlinPackage?.let { "$it." } ?: "" val moduleType = if (isGenerateClass) { generateTypeSpec(pModuleClass, moduleSchema) @@ -172,7 +179,9 @@ class KotlinCodeGenerator( val packageName = if (index == -1) "" else moduleName.substring(0, index) val moduleTypeName = moduleName.substring(index + 1).replaceFirstChar { it.titlecaseChar() } - val fileSpec = FileSpec.builder(packageName, moduleTypeName).indent(options.indent) + val packagePath = + if (packagePrefix.isNotBlank()) "$packagePrefix.$packageName" else packageName + val fileSpec = FileSpec.builder(packagePath, moduleTypeName).indent(options.indent) for (typeAlias in moduleSchema.typeAliases.values) { if (typeAlias.aliasedType is PType.Alias) { @@ -638,10 +647,14 @@ class KotlinCodeGenerator( val index = moduleName.lastIndexOf(".") val packageName = if (index == -1) "" else moduleName.substring(0, index) val moduleTypeName = moduleName.substring(index + 1).replaceFirstChar { it.titlecaseChar() } + val packagePrefix = kotlinPackage?.let { "$it." } ?: "" + val renderedPackage = + if (packagePrefix.isNotBlank()) "$packagePrefix.$packageName" else packageName + return if (isModuleClass) { - ClassName(packageName, moduleTypeName) + ClassName(renderedPackage, moduleTypeName) } else { - ClassName(packageName, moduleTypeName, simpleName) + ClassName(renderedPackage, moduleTypeName, simpleName) } } 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 e8fd0319..5aa0d6aa 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 @@ -133,7 +133,8 @@ class KotlinCodeGeneratorTest { pklCode: String, generateKdoc: Boolean = false, generateSpringBootConfig: Boolean = false, - implementSerializable: Boolean = false + implementSerializable: Boolean = false, + kotlinPackage: String? = null, ): String { val module = Evaluator.preconfigured().evaluateSchema(ModuleSource.text(pklCode)) @@ -144,7 +145,8 @@ class KotlinCodeGeneratorTest { KotlinCodegenOptions( generateKdoc = generateKdoc, generateSpringBootConfig = generateSpringBootConfig, - implementSerializable = implementSerializable + implementSerializable = implementSerializable, + kotlinPackage = kotlinPackage ?: "", ) ) return generator.kotlinFile @@ -553,6 +555,92 @@ class KotlinCodeGeneratorTest { assertCompilesSuccessfully(kotlinCode) } + @Test + fun `custom kotlin package prefix`() { + val kotlinCode = + generateKotlinCode( + """ + module my.mod + + class Person { + name: String + age: Int + hobbies: List + friends: Map + sibling: Person? + } + """, + kotlinPackage = "cool.pkg.path", + ) + + assertEqualTo( + """ + package cool.pkg.path.my + + import kotlin.Long + import kotlin.String + import kotlin.collections.List + import kotlin.collections.Map + + object Mod { + data class Person( + val name: String, + val age: Long, + val hobbies: List, + val friends: Map, + val sibling: Person? + ) + } + """, + kotlinCode + ) + + assertCompilesSuccessfully(kotlinCode) + } + + @Test + fun `empty kotlin package prefix`() { + val kotlinCode = + generateKotlinCode( + """ + module my.mod + + class Person { + name: String + age: Int + hobbies: List + friends: Map + sibling: Person? + } + """, + kotlinPackage = "", + ) + + assertEqualTo( + """ + package my + + import kotlin.Long + import kotlin.String + import kotlin.collections.List + import kotlin.collections.Map + + object Mod { + data class Person( + val name: String, + val age: Long, + val hobbies: List, + val friends: Map, + val sibling: Person? + ) + } + """, + kotlinCode + ) + + assertCompilesSuccessfully(kotlinCode) + } + @Test fun `recursive types`() { val kotlinCode = diff --git a/pkl-gradle/src/main/java/org/pkl/gradle/PklPlugin.java b/pkl-gradle/src/main/java/org/pkl/gradle/PklPlugin.java index 23741970..10ffdf66 100644 --- a/pkl-gradle/src/main/java/org/pkl/gradle/PklPlugin.java +++ b/pkl-gradle/src/main/java/org/pkl/gradle/PklPlugin.java @@ -196,12 +196,15 @@ public class PklPlugin implements Plugin { configureCodeGenSpec(spec); spec.getGenerateKdoc().convention(false); + spec.getKotlinPackage().convention(""); createModulesTask(KotlinCodeGenTask.class, spec) .configure( task -> { configureCodeGenTask(task, spec); task.getGenerateKdoc().set(spec.getGenerateKdoc()); + task.getIndent().set(spec.getIndent()); + task.getKotlinPackage().set(spec.getKotlinPackage()); }); }); diff --git a/pkl-gradle/src/main/java/org/pkl/gradle/spec/KotlinCodeGenSpec.java b/pkl-gradle/src/main/java/org/pkl/gradle/spec/KotlinCodeGenSpec.java index 62b8faa9..50e7d56f 100644 --- a/pkl-gradle/src/main/java/org/pkl/gradle/spec/KotlinCodeGenSpec.java +++ b/pkl-gradle/src/main/java/org/pkl/gradle/spec/KotlinCodeGenSpec.java @@ -19,5 +19,9 @@ import org.gradle.api.provider.Property; /** Configuration options for Kotlin code generators. Documented in user manual. */ public interface KotlinCodeGenSpec extends CodeGenSpec { + Property getIndent(); + + Property getKotlinPackage(); + Property getGenerateKdoc(); } diff --git a/pkl-gradle/src/main/java/org/pkl/gradle/task/KotlinCodeGenTask.java b/pkl-gradle/src/main/java/org/pkl/gradle/task/KotlinCodeGenTask.java index 240cbe6e..7ed4175e 100644 --- a/pkl-gradle/src/main/java/org/pkl/gradle/task/KotlinCodeGenTask.java +++ b/pkl-gradle/src/main/java/org/pkl/gradle/task/KotlinCodeGenTask.java @@ -25,6 +25,9 @@ public abstract class KotlinCodeGenTask extends CodeGenTask { @Input public abstract Property getGenerateKdoc(); + @Input + public abstract Property getKotlinPackage(); + @Override protected void doRunTask() { //noinspection ResultOfMethodCallIgnored @@ -35,6 +38,7 @@ public abstract class KotlinCodeGenTask extends CodeGenTask { getCliBaseOptions(), getProject().file(getOutputDir()).toPath(), getIndent().get(), + getKotlinPackage().get(), getGenerateKdoc().get(), getGenerateSpringBootConfig().get(), getImplementSerializable().get())) diff --git a/pkl-gradle/src/test/kotlin/org/pkl/gradle/KotlinCodeGeneratorsTest.kt b/pkl-gradle/src/test/kotlin/org/pkl/gradle/KotlinCodeGeneratorsTest.kt index 3e563ff6..93248b4d 100644 --- a/pkl-gradle/src/test/kotlin/org/pkl/gradle/KotlinCodeGeneratorsTest.kt +++ b/pkl-gradle/src/test/kotlin/org/pkl/gradle/KotlinCodeGeneratorsTest.kt @@ -65,7 +65,22 @@ class KotlinCodeGeneratorsTest : AbstractTest() { assertThat(personClassFile).exists() assertThat(addressClassFile).exists() } - + + @Test + fun `compile generated code with custom kotlin package`() { + writeBuildFile(kotlinPackage = "my.cool.pkl.pkg") + writePklFile() + runTask("compileKotlin") + + val classesDir = testProjectDir.resolve("build/classes/kotlin/main") + val moduleClassFile = classesDir.resolve("my/cool/pkl/pkg/org/Mod.class") + val personClassFile = classesDir.resolve("my/cool/pkl/pkg/org/Mod\$Person.class") + val addressClassFile = classesDir.resolve("my/cool/pkl/pkg/org/Mod\$Address.class") + assertThat(moduleClassFile).exists() + assertThat(personClassFile).exists() + assertThat(addressClassFile).exists() + } + @Test fun `no source modules`() { writeFile( @@ -88,7 +103,7 @@ class KotlinCodeGeneratorsTest : AbstractTest() { assertThat(result.output).contains("No source modules specified.") } - private fun writeBuildFile() { + private fun writeBuildFile(kotlinPackage: String? = null) { val kotlinVersion = "1.6.0" writeFile( @@ -125,6 +140,7 @@ class KotlinCodeGeneratorsTest : AbstractTest() { sourceModules = ["mod.pkl"] outputDir = file("build/generated") settingsModule = "pkl:settings" + ${kotlinPackage?.let { "kotlinPackage = \"$it\"" } ?: ""} } } }