Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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.

14 changes: 12 additions & 2 deletions docs/modules/java-binding/pages/codegen.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,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 @@ -118,7 +119,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 @@ -141,6 +143,14 @@ 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, the related interfaces, and JEP 468 like `withers`.
Overrides Java class generation option `--generate-getters`.
====

.--generate-javadoc
[%collapsible]
====
Expand Down
10 changes: 10 additions & 0 deletions docs/modules/pkl-gradle/pages/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,16 @@ 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`.

====

.paramsAnnotation: Property<String>
[%collapsible]
====
Expand Down
157 changes: 157 additions & 0 deletions pkl-codegen-java/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# Java Code Generation Specification:

## Preamble

1. The goal of the current spec is to change **only** Pkl classes production, leaving the rest of the generation intact
2. Each custom immutable Java class with the one-property withers to be replaced by:
1. each Pkl abstract class would be generated as the corresponding Java interface, such that:
1. each its declared public property would become the `interface`'s abstract method of the same `type` and `name
2. the `interface` would implement the Pkl abstract class's superclass, if present
2. each Pkl class, including modules, would be generated as the corresponding Java record, such that:
1. `record`'s components are identical to the current custom Java class
2. `record` would implement its Pkl superclass corresponding Java interface
3. `record` would in addition implement the common generic `Wither` interface (in line with https://openjdk.org/jeps/468 which is not available yet)
4. `record` would have its special `Memento` public static inner class generated as described below
3. each Pkl `open` class would in addition have its default interface generated like in the case of a Pkl abstract class
3. The following would be generated as the singleton common constructs for all:
```java

import java.util.function.Consumer;

public interface Wither<R extends Record, S> {
R with(Consumer<S> setter);
}

```
4. The record `R`, its `Memento` would be generated as follows:
```java

record R(String p1, String p2, String p3) implements Wither<R, R.Memento>, Serializable {

@Override
public R with(final Consumer<Memento> setter) {
final var memento = new Memento(this);
setter.accept(memento);
return memento.build();
}

public static final class Memento {
public String p1;
public String p2;
public String p3;

private Memento(final R r) {
p1 = r.p1;
p2 = r.p2;
p3 = r.p3;
}

private R build() {
return new R(p1, p2, p3);
}
}
}

```
5. The usage in the Java consumer code would be as follows:
```java

class Scratch {

public static void main(String[] args) {
final R r1 = new R("a", "b", "c");
final R r2 = r1.with(it -> it.p1 = "a2").with(it -> {
it.p2 = "b2";
it.p3 = "c2";
});

System.out.println(r1); // R[p1=a, p2=b, p3=c]
System.out.println(r2); // R[p1=a2, p2=b2, p3=c2]
}
}


```
6. Given Pkl is single inheritance and doesn't support creation or extension of generic classes, the above generation scheme should be sufficient and adequate.
7. As an extension API, the generation offers the option to expose empty base interface(-s) to be extended by the Java consumer as follows:
1. one base interface implemented by all generated records as follows:
1. `IPklBase` interface code would have to be implemented elsewhere in the Java consumer code, otherwise causing the compilation error
```java
record R(/* component list of <Type name> */) implements Wither<R, R.Memento>, IPklBase {
// ...
}
```
2. Most likely, `IPklBase` would have the default methods, thus effectively extending the functionality of all classes
2. a base interface per record type, to be implemented elsewhere in the Java consumer code similar to above:
```java
record R(/* component list of <Type name> */) implements Wither<R, R.Memento>, IR {
// ...
}
```
8. Serialization would be delegated to the Record API as follows:
1. if requested in options, each generated Java record would in addition implement a Java `java.io.Serializable`

> [!IMPORTANT]
> All the annotation, name handling regarding Java reserved words, and such would be handled as currently.

<details>

<summary>See a complete example of Java code generation</summary>

```java

package com.apple.pkl.code.gen.java.example;

import java.io.Serializable;
import java.util.function.Consumer;

class Demo implements Serializable {

public static void main(final String[] args) {
final R r1 = new R("a", "b", "c");
final R r2 = r1.with(it -> it.p1 = "a2").with(it -> {
it.p2 = "b2";
it.p3 = "c2";
});

System.out.println(r1);
System.out.println(r2);
}
}

//TODO: include as-is once
interface Wither<R extends Record, S> {
R with(Consumer<S> setter);
}

//TODO: include per Pkl class
record R(String p1, String p2, String p3) implements Wither<R, R.Memento>, Serializable {

@Override
public R with(final Consumer<Memento> setter) {
final var memento = new Memento(this);
setter.accept(memento);
return memento.build();
}

public static final class Memento {

public String p1;
public String p2;
public String p3;

private Memento(final R r) {
p1 = r.p1;
p2 = r.p2;
p3 = r.p3;
}

private R build() {
return new R(p1, p2, p3);
}
}
}

```

</details>
2 changes: 1 addition & 1 deletion pkl-codegen-java/pkl-codegen-java.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2025 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,25 @@ class CliJavaCodeGenerator(private val options: CliJavaCodeGeneratorOptions) :
val builder = evaluatorBuilder()
try {
builder.build().use { evaluator ->
if (options.generateRecords) {
options.outputDir.resolve(JavaRecordCodeGenerator.commonCodePackageFile).apply {
createParentDirectories()
.writeString(
JavaRecordCodeGenerator.generateCommonCode(options.toJavaCodeGeneratorOptions())
)
}
}

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 @@ -74,6 +74,13 @@ data class CliJavaCodeGeneratorOptions(
* Pkl module name, and the value is the desired replacement.
*/
val renames: Map<String, String> = emptyMap(),

/**
* Whether to generate Java records, the related interfaces, and JEP 468 like withers.
*
* This overrides any Java class generation related options!
*/
val generateRecords: Boolean = false,
Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.

) {
@Suppress("DeprecatedCallableAddReplaceWith")
@Deprecated("deprecated without replacement")
Expand All @@ -89,5 +96,6 @@ data class CliJavaCodeGeneratorOptions(
nonNullAnnotation,
implementSerializable,
renames,
generateRecords,
)
}
Loading