-
Notifications
You must be signed in to change notification settings - Fork 368
Java class and record generators and friends side-by-side #970
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
3a3916e
baec91e
c4374b1
b9d9af6
986a98a
305b251
e594176
e75aa3c
f5a883d
329257a
fe09e8c
0930f75
145eea4
8c855b6
edc5d82
0f2d0bf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| 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> { | ||
protobufel2 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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); | ||
| } | ||
| } | ||
| } | ||
protobufel2 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| ``` | ||
| 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> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 By default, the code generator should be generating records whenever it can (all Pkl users are on Java 17).
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Regarding flipping
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| ) { | ||
| @Suppress("DeprecatedCallableAddReplaceWith") | ||
| @Deprecated("deprecated without replacement") | ||
|
|
@@ -89,5 +96,6 @@ data class CliJavaCodeGeneratorOptions( | |
| nonNullAnnotation, | ||
| implementSerializable, | ||
| renames, | ||
| generateRecords, | ||
| ) | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.