Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .idea/codeStyles/Project.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions buildSrc/src/main/kotlin/pklKotlinTest.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ val buildInfo = project.extensions.getByType<BuildInfo>()
val libs = the<LibrariesForLibs>()

dependencies {
testImplementation(platform(libs.junitBom))
testImplementation(libs.assertj)
testImplementation(libs.junitApi)
testImplementation(libs.junitParams)
Expand Down
30 changes: 28 additions & 2 deletions docs/modules/java-binding/pages/codegen.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ The benefits of code generation are:
* The entire configuration tree can be code-completed in Java IDEs.
* Any drift between Java code and Pkl configuration structure is caught at compile time.

The generated classes are immutable and have component-wise implementations of `equals()`, `hashCode()`, and `toString()`.
The generated classes are immutable and have component-wise implementations of `equals()`, `hashCode()`, and `toString()`,
or can optionally be produced as Java Records, in case of `generateRecords=true`, delivering the same benefits.

== Installation

Expand Down Expand Up @@ -157,7 +158,8 @@ See xref:pkl-gradle:index.adoc#java-code-gen[Java Code Generation] in the Gradle
=== Java Library

The Java library offers two APIs: a high-level API that corresponds to the CLI, and a lower-level API that provides additional features and control.
The entry points for these APIs are `org.pkl.codegen.java.CliJavaCodeGenerator` and `org.pkl.codegen.java.JavaCodeGenerator`, respectively.
The entry points for these APIs are `org.pkl.codegen.java.CliJavaCodeGenerator`, and `org.pkl.codegen.java.JavaCodeGenerator` or
`org.pkl.codegen.java.JavaRecordCodeGenerator`, respectively.
For more information, refer to the Javadoc documentation.

=== CLI
Expand All @@ -177,6 +179,30 @@ Default: (flag not set) +
Flag that indicates to generate private final fields and public getter methods instead of public final fields.
====

.--generate-records
[%collapsible]
====
Default: (flag not set) +
Flag that indicates to generate Java records and the related interfaces. +
Overrides Java class generation option `--generate-getters`.
====

.--use-withers
[%collapsible]
====
Default: (flag not set) +
Flag that indicates whether to generate JEP 468 like `withers` for Java records. +
Works in conjunction with Java records class generation option `-generate-records`.
====

.--use-lombok-builders
[%collapsible]
====
Default: (flag not set) +
Flag that indicates whether to generate Lombok Builders for Java records. +
Works in conjunction with Java records class generation option `-generate-records`.
====

.--generate-javadoc
[%collapsible]
====
Expand Down
28 changes: 28 additions & 0 deletions docs/modules/pkl-gradle/pages/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,34 @@ Example: `generateGetters = true` +
Whether to generate private final fields and public getter methods rather than public final fields.
====

.generateRecords: Property<Boolean>
[%collapsible]
====
Default: `false` +
Example: `generateRecords = true` +
Whether to generate Java records, the related interfaces, and JEP 468 like `withers`.
If set to `true`, overrides Java class generation option `generateGetters`.

====

.useWithers: Property<Boolean>
[%collapsible]
====
Default: `false` +
Example: `useWithers = true` +
Whether to generate JEP 468 like withers for records.

====

.useLombokBuilders: Property<Boolean>
[%collapsible]
====
Default: `false` +
Example: `useLombokBuilders = true` +
Whether to generate Lombok Builders for records.

====

.paramsAnnotation: Property<String>
[%collapsible]
====
Expand Down
15 changes: 9 additions & 6 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jimfs = "1.+"
# (at least not without additional configuration; tested with 3.25.1 and 3.27.1)
jline = "3.23.0"
jmh = "1.+"
jmhPlugin = "0.7.2"
jmhPlugin = "0.7.3"
jsr305 = "3.+"
junit = "5.+"
junitPlatform = "1.+"
Expand Down Expand Up @@ -71,10 +71,13 @@ jlineReader = { group = "org.jline", name = "jline-reader", version.ref = "jline
jlineTerminal = { group = "org.jline", name = "jline-terminal", version.ref = "jline" }
jlineTerminalJansi = { group = "org.jline", name = "jline-terminal-jansi", version.ref = "jline" }
jsr305 = { group = "com.google.code.findbugs", name = "jsr305", version.ref = "jsr305" }
junitApi = { group = "org.junit.jupiter", name = "junit-jupiter-api", version.ref = "junit" }
junitEngine = { group = "org.junit.jupiter", name = "junit-jupiter-engine", version.ref = "junit" }
junitParams = { group = "org.junit.jupiter", name = "junit-jupiter-params", version.ref = "junit" }
junitLauncher = { group = "org.junit.platform", name = "junit-platform-launcher", version.ref = "junitPlatform" }

junitBom = { group = "org.junit", name = "junit-bom", version.ref = "junit" }
junitApi = { module = "org.junit.jupiter:junit-jupiter-api" }
junitEngine = { module = "org.junit.jupiter:junit-jupiter-engine" }
junitParams = { module = "org.junit.jupiter:junit-jupiter-params" }
junitLauncher = { module = "org.junit.platform:junit-platform-launcher" }

kotlinCompilerEmbeddable = { group = "org.jetbrains.kotlin", name = "kotlin-compiler-embeddable", version.ref = "kotlin" }
kotlinPlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" }
kotlinPoet = { group = "com.squareup", name = "kotlinpoet", version.ref = "kotlinPoet" }
Expand All @@ -94,7 +97,7 @@ shadowPlugin = { group = "com.gradleup.shadow", name = "com.gradleup.shadow.grad
slf4jApi = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" }
slf4jSimple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" }
snakeYaml = { group = "org.snakeyaml", name = "snakeyaml-engine", version.ref = "snakeYaml" }
spotlessPlugin = { group = "com.diffplug.spotless", name = "spotless-plugin-gradle", version.ref = "spotlessPlugin"}
spotlessPlugin = { group = "com.diffplug.spotless", name = "spotless-plugin-gradle", version.ref = "spotlessPlugin" }
svm = { group = "org.graalvm.nativeimage", name = "svm", version.ref = "graalVm" }
truffleApi = { group = "org.graalvm.truffle", name = "truffle-api", version.ref = "graalVm" }
truffleDslProcessor = { group = "org.graalvm.truffle", name = "truffle-dsl-processor", version.ref = "graalVm" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,31 @@ class CliJavaCodeGenerator(private val options: CliJavaCodeGeneratorOptions) :
val builder = evaluatorBuilder()
try {
builder.build().use { evaluator ->
if (options.generateRecords) {
if (options.useWithers) {
options.outputDir.resolve(JavaRecordCodeGenerator.commonCodePackageFile).apply {
createParentDirectories()
.writeString(
JavaRecordCodeGenerator.generateCommonCode(options.toJavaCodeGeneratorOptions())
)
}
}

if (options.useLombokBuilders) {
TODO("not implemented in this Pkl version yet")
}
}

for (moduleUri in options.base.normalizedSourceModules) {
val schema = evaluator.evaluateSchema(ModuleSource.uri(moduleUri))
val codeGenerator = JavaCodeGenerator(schema, options.toJavaCodeGeneratorOptions())

val output =
if (options.generateRecords)
JavaRecordCodeGenerator(schema, options.toJavaCodeGeneratorOptions()).output
else JavaCodeGenerator(schema, options.toJavaCodeGeneratorOptions()).output

try {
for ((fileName, fileContents) in codeGenerator.output) {
for ((fileName, fileContents) in output) {
val outputFile = options.outputDir.resolve(fileName)
try {
outputFile.createParentDirectories().writeString(fileContents)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,21 +77,37 @@ data class CliJavaCodeGeneratorOptions(
* Pkl module name, and the value is the desired replacement.
*/
val renames: Map<String, String> = emptyMap(),

/**
* Whether to generate Java records and the related interfaces.
*
* This overrides any Java class generation related options!
*/
val generateRecords: Boolean = false,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should just flip this around and have a flag called generatePojos which defaults to false.

By default, the code generator should be generating records whenever it can (all Pkl users are on Java 17).
This flag would just be for folks that are migrating and don't want to suffer a breaking change.

Copy link
Author

@protobufel2 protobufel2 Feb 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding flipping generateRecords to the default true, it'd be very distractive, incompatible change by breaking all regenerated Java object models' usage, necessitating the corresponding boolean flip in Gradle extension and any such Java generation via using pkl-core or pkl-config-java by the affected users

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's okay; for users that want minimal breakage, they can add generatePojos = true to retain the current code generator output. For most users, this should be a matter of adding a line to their build.gradle.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we follow the industry practice of deprecation, keep things as compatible as possible for one, two releases before make it the breaking change? I guess, until Pkl goes 1.0.0 we could've followed Kotlin's model of making such breaking changes after a couple of minor releases, especially when it doesn't change the Pkl itself?

Copy link
Member

@bioball bioball Mar 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The downside of that is: it is harder for new users that are adopting Pkl (the code generator has less sensible defaults).

We usually try to minimize breaking changes, but this one feels quite tolerable to me. I'm also okay with already making the generatePojos option already deprecated, and eventually removed. Once we ship 1.x, we will be stricter about breaking changes.


/** Whether to generate JEP 468 like withers for records. */
val useWithers: Boolean = false,

/** Whether to generate Lombok Builders for records. */
val useLombokBuilders: Boolean = false,
) {
@Suppress("DeprecatedCallableAddReplaceWith")
@Deprecated("deprecated without replacement")
fun toJavaCodegenOptions() = toJavaCodeGeneratorOptions()

internal fun toJavaCodeGeneratorOptions() =
JavaCodeGeneratorOptions(
indent,
addGeneratedAnnotation,
generateGetters,
generateJavadoc,
generateSpringBootConfig,
paramsAnnotation,
nonNullAnnotation,
implementSerializable,
renames,
indent = indent,
addGeneratedAnnotation = addGeneratedAnnotation,
generateGetters = generateGetters,
generateJavadoc = generateJavadoc,
generateSpringBootConfig = generateSpringBootConfig,
paramsAnnotation = paramsAnnotation,
nonNullAnnotation = nonNullAnnotation,
implementSerializable = implementSerializable,
renames = renames,
generateRecords = generateRecords,
useWithers = useWithers,
useLombokBuilders = useLombokBuilders,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import kotlin.AssertionError
import kotlin.Boolean
import kotlin.Int
import kotlin.Long
import kotlin.RuntimeException
import kotlin.String
import kotlin.Suppress
import kotlin.Unit
Expand All @@ -38,64 +37,6 @@ import org.pkl.core.*
import org.pkl.core.util.CodeGeneratorUtils
import org.pkl.core.util.IoUtils

class JavaCodeGeneratorException(message: String) : RuntimeException(message)

@kotlin.Deprecated("renamed to JavaCodeGeneratorOptions", ReplaceWith("JavaCodeGeneratorOptions"))
typealias JavaCodegenOptions = JavaCodeGeneratorOptions

data class JavaCodeGeneratorOptions(
/** The characters to use for indenting generated Java code. */
val indent: String = " ",

/** Adds the `org.pkl.config.java.Generated` annotation to the classes to be generated. */
val addGeneratedAnnotation: Boolean = false,

/**
* Whether to generate public getter methods and protected final fields instead of public final
* fields.
*/
val generateGetters: Boolean = false,

/** Whether to preserve Pkl doc comments by generating corresponding Javadoc comments. */
val generateJavadoc: Boolean = false,

/** Whether to generate config classes for use with Spring Boot. */
val generateSpringBootConfig: Boolean = false,

/**
* Fully qualified name of the annotation type to use for annotating constructor parameters with
* their name.
*
* The specified annotation type must have a `value` parameter of type [java.lang.String] or the
* generated code may not compile.
*
* If set to `null`, constructor parameters are not annotated. The default value is `null` if
* [generateSpringBootConfig] is `true` and `"org.pkl.config.java.mapper.Named"` otherwise.
*/
val paramsAnnotation: String? =
if (generateSpringBootConfig) null else "org.pkl.config.java.mapper.Named",

/**
* Fully qualified name of the annotation type to use for annotating non-null types.
*
* The specified annotation type must have a [java.lang.annotation.Target] of
* [java.lang.annotation.ElementType.TYPE_USE] or the generated code may not compile. If set to
* `null`, [org.pkl.config.java.mapper.NonNull] will be used.
*/
val nonNullAnnotation: String? = null,

/** Whether to generate classes that implement [java.io.Serializable]. */
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. */
class JavaCodeGenerator(
private val schema: ModuleSchema,
Expand Down Expand Up @@ -906,61 +847,3 @@ class JavaCodeGenerator(

private val nameMapper = NameMapper(codegenOptions.renames)
}

internal val javaReservedWords =
setOf(
"_", // java 9+
"abstract",
"assert",
"boolean",
"break",
"byte",
"case",
"catch",
"char",
"class",
"const",
"continue",
"default",
"double",
"do",
"else",
"enum",
"extends",
"false",
"final",
"finally",
"float",
"for",
"goto",
"if",
"implements",
"import",
"instanceof",
"int",
"interface",
"long",
"native",
"new",
"null",
"package",
"private",
"protected",
"public",
"return",
"short",
"static",
"strictfp",
"super",
"switch",
"synchronized",
"this",
"throw",
"throws",
"transient",
"true",
"try",
"void",
"volatile",
"while",
)
Loading