Compare commits
16 Commits
record-bui
...
record-bui
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1dd00b2c65 | ||
|
|
3b34b5dee3 | ||
|
|
7248bad2bd | ||
|
|
7e494d8753 | ||
|
|
3954499d4b | ||
|
|
13959dee2a | ||
|
|
af759c0570 | ||
|
|
9a7d73e78c | ||
|
|
3b8c3ff9e3 | ||
|
|
9943667af1 | ||
|
|
b0c8f10711 | ||
|
|
0d3c2f37c1 | ||
|
|
eabcb2f179 | ||
|
|
5fef81191d | ||
|
|
8dbec027e4 | ||
|
|
ef09d68b78 |
28
.github/workflows/maven_java15.yml
vendored
28
.github/workflows/maven_java15.yml
vendored
@@ -1,28 +0,0 @@
|
||||
# This workflow will build a Java project with Maven
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven
|
||||
|
||||
name: Maven Build - Java 15
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 15
|
||||
- name: Create Maven Directory
|
||||
run: mkdir -p .mvn/
|
||||
- name: Create Maven JVM file
|
||||
run: echo "--enable-preview" > .mvn/jvm.config
|
||||
- name: Build with Maven
|
||||
run: mvn -P java15 -B package --file pom.xml
|
||||
355
README.md
355
README.md
@@ -22,7 +22,6 @@ _Details:_
|
||||
- [Generation Via Includes](#generation-via-includes)
|
||||
- [Usage](#usage)
|
||||
- [Customizing](customizing.md) (e.g. add immutable collections, etc.)
|
||||
- [Java 15 Versions](#java-15-versions)
|
||||
|
||||
## RecordBuilder Example
|
||||
|
||||
@@ -80,6 +79,10 @@ NameAndAge r5 = r4.with(b -> {
|
||||
b.name("whatever"));
|
||||
}
|
||||
});
|
||||
|
||||
// or, if you cannot add the "With" interface to your record...
|
||||
NameAndAge r6 = NameAndAgeBuilder.from(r5).with(b -> b.age(200).name("whatever"));
|
||||
NameAndAge r7 = NameAndAgeBuilder.from(r5).withName("boop");
|
||||
```
|
||||
|
||||
_Hat tip to [Benji Weber](https://benjiweber.co.uk/blog/2020/09/19/fun-with-java-records/) for the Withers idea._
|
||||
@@ -92,145 +95,162 @@ The full builder class is defined as:
|
||||
|
||||
```java
|
||||
public class NameAndAgeBuilder {
|
||||
private String name;
|
||||
private String name;
|
||||
|
||||
private int age;
|
||||
private int age;
|
||||
|
||||
private NameAndAgeBuilder() {
|
||||
}
|
||||
private NameAndAgeBuilder() {
|
||||
}
|
||||
|
||||
private NameAndAgeBuilder(String name, int age) {
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
private NameAndAgeBuilder(String name, int age) {
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static constructor/builder. Can be used instead of new NameAndAge(...)
|
||||
*/
|
||||
public static NameAndAge NameAndAge(String name, int age) {
|
||||
return new NameAndAge(name, age);
|
||||
}
|
||||
/**
|
||||
* Static constructor/builder. Can be used instead of new NameAndAge(...)
|
||||
*/
|
||||
public static NameAndAge NameAndAge(String name, int age) {
|
||||
return new NameAndAge(name, age);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new builder with all fields set to default Java values
|
||||
*/
|
||||
public static NameAndAgeBuilder builder() {
|
||||
return new NameAndAgeBuilder();
|
||||
}
|
||||
/**
|
||||
* Return a new builder with all fields set to default Java values
|
||||
*/
|
||||
public static NameAndAgeBuilder builder() {
|
||||
return new NameAndAgeBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new builder with all fields set to the values taken from the given record instance
|
||||
*/
|
||||
public static NameAndAgeBuilder builder(NameAndAge from) {
|
||||
return new NameAndAgeBuilder(from.name(), from.age());
|
||||
}
|
||||
/**
|
||||
* Return a new builder with all fields set to the values taken from the given record instance
|
||||
*/
|
||||
public static NameAndAgeBuilder builder(NameAndAge from) {
|
||||
return new NameAndAgeBuilder(from.name(), from.age());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new record instance with all fields set to the current values in this builder
|
||||
*/
|
||||
public NameAndAge build() {
|
||||
return new NameAndAge(name, age);
|
||||
}
|
||||
/**
|
||||
* Return a "with"er for an existing record instance
|
||||
*/
|
||||
public static NameAndAgeBuilder.With from(NameAndAge from) {
|
||||
return new NameAndAgeBuilder.With() {
|
||||
@Override
|
||||
public String name() {
|
||||
return from.name();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a new value for the {@code name} record component in the builder
|
||||
*/
|
||||
public NameAndAgeBuilder name(String name) {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
@Override
|
||||
public int age() {
|
||||
return from.age();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a stream of the record components as map entries keyed with the component name and the value as the component value
|
||||
*/
|
||||
public static Stream<Map.Entry<String, Object>> stream(NameAndAge record) {
|
||||
return Stream.of(Map.entry("name", record.name()),
|
||||
Map.entry("age", record.age()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new record instance with all fields set to the current values in this builder
|
||||
*/
|
||||
public NameAndAge build() {
|
||||
return new NameAndAge(name, age);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "NameAndAgeBuilder[name=" + name + ", age=" + age + "]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(name, age);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
return (this == o) || ((o instanceof NameAndAgeBuilder r)
|
||||
&& Objects.equals(name, r.name)
|
||||
&& (age == r.age));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a new value for the {@code name} record component in the builder
|
||||
*/
|
||||
public NameAndAgeBuilder name(String name) {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current value for the {@code name} record component in the builder
|
||||
*/
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a new value for the {@code age} record component in the builder
|
||||
*/
|
||||
public NameAndAgeBuilder age(int age) {
|
||||
this.age = age;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current value for the {@code age} record component in the builder
|
||||
*/
|
||||
public int age() {
|
||||
return age;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add withers to {@code NameAndAge}
|
||||
*/
|
||||
public interface With {
|
||||
/**
|
||||
* Return the current value for the {@code name} record component in the builder
|
||||
*/
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a new value for the {@code age} record component in the builder
|
||||
*/
|
||||
public NameAndAgeBuilder age(int age) {
|
||||
this.age = age;
|
||||
return this;
|
||||
}
|
||||
String name();
|
||||
|
||||
/**
|
||||
* Return the current value for the {@code age} record component in the builder
|
||||
*/
|
||||
public int age() {
|
||||
return age;
|
||||
int age();
|
||||
|
||||
/**
|
||||
* Return a new record builder using the current values
|
||||
*/
|
||||
default NameAndAgeBuilder with() {
|
||||
return new NameAndAgeBuilder(name(), age());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a stream of the record components as map entries keyed with the component name and the value as the component value
|
||||
* Return a new record built from the builder passed to the given consumer
|
||||
*/
|
||||
public static Stream<Map.Entry<String, Object>> stream(NameAndAge record) {
|
||||
return Stream.of(Map.entry("name", record.name()),
|
||||
Map.entry("age", record.age()));
|
||||
default NameAndAge with(Consumer<NameAndAgeBuilder> consumer) {
|
||||
NameAndAgeBuilder builder = with();
|
||||
consumer.accept(builder);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "NameAndAgeBuilder[name=" + name + ", age=" + age + "]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(name, age);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
return (this == o) || ((o instanceof NameAndAgeBuilder b)
|
||||
&& Objects.equals(name, b.name)
|
||||
&& (age == b.age));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add withers to {@code NameAndAge}
|
||||
* Return a new instance of {@code NameAndAge} with a new value for {@code name}
|
||||
*/
|
||||
public interface With {
|
||||
/**
|
||||
* Return the current value for the {@code name} record component in the builder
|
||||
*/
|
||||
String name();
|
||||
|
||||
/**
|
||||
* Return the current value for the {@code age} record component in the builder
|
||||
*/
|
||||
int age();
|
||||
|
||||
/**
|
||||
* Return a new record builder using the current values
|
||||
*/
|
||||
default NameAndAgeBuilder with() {
|
||||
return new NameAndAgeBuilder(name(), age());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new record built from the builder passed to the given consumer
|
||||
*/
|
||||
default NameAndAge with(Consumer<NameAndAgeBuilder> consumer) {
|
||||
NameAndAgeBuilder builder = with();
|
||||
consumer.accept(builder);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new instance of {@code NameAndAge} with a new value for {@code name}
|
||||
*/
|
||||
default NameAndAge withName(String name) {
|
||||
return new NameAndAge(name, age());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new instance of {@code NameAndAge} with a new value for {@code age}
|
||||
*/
|
||||
default NameAndAge withAge(int age) {
|
||||
return new NameAndAge(name(), age);
|
||||
}
|
||||
default NameAndAge withName(String name) {
|
||||
return new NameAndAge(name, age());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new instance of {@code NameAndAge} with a new value for {@code age}
|
||||
*/
|
||||
default NameAndAge withAge(int age) {
|
||||
return new NameAndAge(name(), age);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -298,41 +318,15 @@ annotation. Use `packagePattern` to change this (see Javadoc for details).
|
||||
|
||||
### Maven
|
||||
|
||||
1) Add the dependency that contains the `@RecordBuilder` annotation.
|
||||
Add a dependency that contains the discoverable annotation processor:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder-core</artifactId>
|
||||
<version>set-version-here</version>
|
||||
<artifactId>record-builder-processor</artifactId>
|
||||
<version>${record.builder.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
```
|
||||
|
||||
2) Enable the annotation processing for the Maven Compiler Plugin:
|
||||
|
||||
```xml
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>set-version-here</version>
|
||||
<configuration>
|
||||
<annotationProcessorPaths>
|
||||
<annotationProcessorPath>
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder-processor</artifactId>
|
||||
<version>set-version-here</version>
|
||||
</annotationProcessorPath>
|
||||
</annotationProcessorPaths>
|
||||
<annotationProcessors>
|
||||
<annotationProcessor>io.soabase.recordbuilder.processor.RecordBuilderProcessor</annotationProcessor>
|
||||
</annotationProcessors>
|
||||
|
||||
|
||||
... any other options here ...
|
||||
</configuration>
|
||||
</plugin>
|
||||
```
|
||||
|
||||
### Gradle
|
||||
@@ -355,76 +349,3 @@ Depending on your IDE you are likely to need to enable Annotation Processing in
|
||||
RecordBuilder can be customized to your needs and you can even create your
|
||||
own custom RecordBuilder annotations. See [Customizing RecordBuilder](customizing.md)
|
||||
for details.
|
||||
|
||||
## Java 15 Versions
|
||||
|
||||
Artifacts compiled wth Java 15 are available. These versions have `-java15` appended.
|
||||
|
||||
Note: records are a preview feature only in Java 15. You'll need take a number of steps in order to try RecordBuilder:
|
||||
|
||||
- Install and make active Java 15 or later
|
||||
- Make sure your development tool is using Java 15 or later and is configured to enable preview features (for Maven I've documented how to do this here: [https://stackoverflow.com/a/59363152/2048051](https://stackoverflow.com/a/59363152/2048051))
|
||||
- Bear in mind that this is not yet meant for production and there are numerous bugs in the tools and JDKs.
|
||||
|
||||
Note: I've seen some very odd compilation bugs with the current Java 15 and Maven. If you get internal Javac errors I suggest rebuilding with `mvn clean package` and/or `mvn clean install`.
|
||||
|
||||
You will need to enable preview in your build tools:
|
||||
|
||||
### Maven
|
||||
|
||||
```xml
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder-core</artifactId>
|
||||
<version>record-builder-version-java15</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>maven-compiler-version</version>
|
||||
<configuration>
|
||||
<annotationProcessorPaths>
|
||||
<annotationProcessorPath>
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder-processor</artifactId>
|
||||
<version>record-builder-version-java15</version>
|
||||
</annotationProcessorPath>
|
||||
</annotationProcessorPaths>
|
||||
<annotationProcessors>
|
||||
<annotationProcessor>io.soabase.recordbuilder.processor.RecordBuilderProcessor</annotationProcessor>
|
||||
</annotationProcessors>
|
||||
|
||||
|
||||
<!-- "release" and "enable-preview" are required while records are preview features -->
|
||||
<release>15</release>
|
||||
<compilerArgs>
|
||||
<arg>--enable-preview</arg>
|
||||
</compilerArgs>
|
||||
|
||||
... any other options here ...
|
||||
</configuration>
|
||||
</plugin>
|
||||
```
|
||||
|
||||
Create a file in your project's root named `.mvn/jvm.config`. The file should have 1 line with the value: `--enable-preview`. (see: https://stackoverflow.com/questions/58023240)
|
||||
|
||||
### Gradle
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
annotationProcessor 'io.soabase.record-builder:record-builder-processor:$record-builder-version-java15'
|
||||
compileOnly 'io.soabase.record-builder:record-builder-core:$record-builder-version-java15'
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
options.fork = true
|
||||
options.forkOptions.jvmArgs += '--enable-preview'
|
||||
options.compilerArgs += '--enable-preview'
|
||||
}
|
||||
tasks.withType(Test) {
|
||||
jvmArgs += "--enable-preview"
|
||||
}
|
||||
```
|
||||
|
||||
22
java15.sh
22
java15.sh
@@ -1,22 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Copyright 2019 Jordan Zimmerman
|
||||
#
|
||||
# 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
|
||||
#
|
||||
# http://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.
|
||||
#
|
||||
|
||||
|
||||
jenv local 15
|
||||
javahome
|
||||
mkdir -p .mvn/
|
||||
echo "--enable-preview" > .mvn/jvm.config
|
||||
21
java16.sh
21
java16.sh
@@ -1,21 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Copyright 2019 Jordan Zimmerman
|
||||
#
|
||||
# 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
|
||||
#
|
||||
# http://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.
|
||||
#
|
||||
|
||||
|
||||
jenv local 16
|
||||
javahome
|
||||
rm -fr .mvn
|
||||
33
pom.xml
33
pom.xml
@@ -5,7 +5,7 @@
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<version>27-java15</version>
|
||||
<version>31</version>
|
||||
|
||||
<modules>
|
||||
<module>record-builder-core</module>
|
||||
@@ -19,8 +19,6 @@
|
||||
<project.build.resourceEncoding>UTF-8</project.build.resourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
|
||||
<enable-preview />
|
||||
|
||||
<jdk-version>16</jdk-version>
|
||||
|
||||
<maven-compiler-plugin-version>3.8.1</maven-compiler-plugin-version>
|
||||
@@ -33,7 +31,6 @@
|
||||
<maven-clean-plugin-version>3.1.0</maven-clean-plugin-version>
|
||||
<maven-shade-plugin-version>3.2.1</maven-shade-plugin-version>
|
||||
<maven-release-plugin-version>2.5.3</maven-release-plugin-version>
|
||||
<maven-surefire-plugin-version>3.0.0-M5</maven-surefire-plugin-version>
|
||||
<maven-jar-plugin-version>3.2.0</maven-jar-plugin-version>
|
||||
|
||||
<license-file-path>src/etc/header.txt</license-file-path>
|
||||
@@ -80,7 +77,7 @@
|
||||
<url>https://github.com/randgalt/record-builder</url>
|
||||
<connection>scm:git:https://github.com/randgalt/record-builder.git</connection>
|
||||
<developerConnection>scm:git:git@github.com:randgalt/record-builder.git</developerConnection>
|
||||
<tag>record-builder-27-java15</tag>
|
||||
<tag>record-builder-31</tag>
|
||||
</scm>
|
||||
|
||||
<issueManagement>
|
||||
@@ -115,6 +112,12 @@
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder-processor</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder-validator</artifactId>
|
||||
@@ -156,9 +159,6 @@
|
||||
<version>${maven-compiler-plugin-version}</version>
|
||||
<configuration>
|
||||
<release>${jdk-version}</release>
|
||||
<compilerArgs>
|
||||
<arg>${enable-preview}</arg>
|
||||
</compilerArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
@@ -308,15 +308,6 @@
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>${maven-surefire-plugin-version}</version>
|
||||
<configuration>
|
||||
<argLine>${enable-preview}</argLine>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
@@ -388,13 +379,5 @@
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
|
||||
<profile>
|
||||
<id>java15</id>
|
||||
<properties>
|
||||
<jdk-version>15</jdk-version>
|
||||
<enable-preview>--enable-preview</enable-preview>
|
||||
</properties>
|
||||
</profile>
|
||||
</profiles>
|
||||
</project>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder</artifactId>
|
||||
<version>27-java15</version>
|
||||
<version>31</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
||||
@@ -89,6 +89,11 @@ public @interface RecordBuilder {
|
||||
*/
|
||||
String buildMethodName() default "build";
|
||||
|
||||
/**
|
||||
* The name to use for the from-to-wither method
|
||||
*/
|
||||
String fromMethodName() default "from";
|
||||
|
||||
/**
|
||||
* The name to use for the method that returns the record components as a stream
|
||||
*/
|
||||
@@ -177,6 +182,11 @@ public @interface RecordBuilder {
|
||||
* The prefix for adder methods when {@link #addSingleItemCollectionBuilders()} is enabled
|
||||
*/
|
||||
String singleItemBuilderPrefix() default "add";
|
||||
|
||||
/**
|
||||
* When enabled, adds functional methods to the nested "With" class (such as {@code map()} and {@code accept()}).
|
||||
*/
|
||||
boolean addFunctionalMethodsToWith() default false;
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.CLASS)
|
||||
|
||||
@@ -20,7 +20,8 @@ import java.lang.annotation.*;
|
||||
@RecordBuilder.Template(options = @RecordBuilder.Options(
|
||||
interpretNotNulls = true,
|
||||
useImmutableCollections = true,
|
||||
addSingleItemCollectionBuilders = true
|
||||
addSingleItemCollectionBuilders = true,
|
||||
addFunctionalMethodsToWith = true
|
||||
))
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@Target(ElementType.TYPE)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder</artifactId>
|
||||
<version>27-java15</version>
|
||||
<version>31</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ class InternalRecordBuilderProcessor {
|
||||
private static final TypeName optionalLongType = TypeName.get(OptionalLong.class);
|
||||
private static final TypeName optionalDoubleType = TypeName.get(OptionalDouble.class);
|
||||
private static final TypeName validatorTypeName = ClassName.get("io.soabase.recordbuilder.validator", "RecordBuilderValidator");
|
||||
private static final TypeVariableName rType = TypeVariableName.get("R");
|
||||
private final ProcessingEnvironment processingEnv;
|
||||
|
||||
InternalRecordBuilderProcessor(ProcessingEnvironment processingEnv, TypeElement record, RecordBuilder.Options metaData, Optional<String> packageNameOpt) {
|
||||
@@ -78,6 +79,7 @@ class InternalRecordBuilderProcessor {
|
||||
}
|
||||
addStaticDefaultBuilderMethod();
|
||||
addStaticCopyBuilderMethod();
|
||||
addStaticFromWithMethod();
|
||||
addStaticComponentsMethod();
|
||||
addBuildMethod();
|
||||
addToStringMethod();
|
||||
@@ -145,10 +147,16 @@ class InternalRecordBuilderProcessor {
|
||||
.addJavadoc("Add withers to {@code $L}\n", recordClassType.name())
|
||||
.addModifiers(Modifier.PUBLIC)
|
||||
.addTypeVariables(typeVariables);
|
||||
recordComponents.forEach(component -> addWithGetterMethod(classBuilder, component));
|
||||
recordComponents.forEach(component -> addNestedGetterMethod(classBuilder, component));
|
||||
addWithBuilderMethod(classBuilder);
|
||||
addWithSuppliedBuilderMethod(classBuilder);
|
||||
IntStream.range(0, recordComponents.size()).forEach(index -> add1WithMethod(classBuilder, recordComponents.get(index), index));
|
||||
if (metaData.addFunctionalMethodsToWith()) {
|
||||
classBuilder.addType(buildFunctionalInterface("Function", true))
|
||||
.addType(buildFunctionalInterface("Consumer", false))
|
||||
.addMethod(buildFunctionalHandler("Function", "map", true))
|
||||
.addMethod(buildFunctionalHandler("Consumer", "accept", false));
|
||||
}
|
||||
builder.addType(classBuilder.build());
|
||||
}
|
||||
|
||||
@@ -236,7 +244,6 @@ class InternalRecordBuilderProcessor {
|
||||
codeBlockBuilder.add(";$]");
|
||||
|
||||
var methodName = getWithMethodName(component, metaData.withClassMethodPrefix());
|
||||
var singleItemsMetaData = collectionBuilderUtils.singleItemsMetaData(component, STANDARD_FOR_SETTER);
|
||||
var parameterSpecBuilder = ParameterSpec.builder(component.typeName(), component.name());
|
||||
addConstructorAnnotations(component, parameterSpecBuilder);
|
||||
var methodSpec = MethodSpec.methodBuilder(methodName)
|
||||
@@ -483,6 +490,67 @@ class InternalRecordBuilderProcessor {
|
||||
return codeBuilder.build();
|
||||
}
|
||||
|
||||
private void addStaticFromWithMethod() {
|
||||
/*
|
||||
Adds static method that returns a "with"er view of an existing record.
|
||||
|
||||
public static With from(MyRecord from) {
|
||||
return new MyRecordBuilder.With() {
|
||||
@Override
|
||||
public String p1() {
|
||||
return from.p1();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String p2() {
|
||||
return from.p2();
|
||||
}
|
||||
};
|
||||
}
|
||||
*/
|
||||
var witherClassNameBuilder = CodeBlock.builder()
|
||||
.add("$L.$L", builderClassType.name(), metaData.withClassName());
|
||||
if (!typeVariables.isEmpty()) {
|
||||
witherClassNameBuilder.add("<");
|
||||
IntStream.range(0, typeVariables.size()).forEach(index -> {
|
||||
if (index > 0) {
|
||||
witherClassNameBuilder.add(", ");
|
||||
}
|
||||
witherClassNameBuilder.add(typeVariables.get(index).name);
|
||||
});
|
||||
witherClassNameBuilder.add(">");
|
||||
}
|
||||
var witherClassName = witherClassNameBuilder.build().toString();
|
||||
var codeBuilder = CodeBlock.builder()
|
||||
.add("return new $L", witherClassName)
|
||||
.add("() {\n").indent();
|
||||
IntStream.range(0, recordComponents.size()).forEach(index -> {
|
||||
var component = recordComponents.get(index);
|
||||
if (index > 0) {
|
||||
codeBuilder.add("\n");
|
||||
}
|
||||
codeBuilder.add("@Override\n")
|
||||
.add("public $T $L() {\n", component.typeName(), component.name())
|
||||
.indent()
|
||||
.addStatement("return from.$L()", component.name())
|
||||
.unindent()
|
||||
.add("}\n");
|
||||
});
|
||||
codeBuilder.unindent().addStatement("}");
|
||||
|
||||
var withType = ClassName.get("", witherClassName);
|
||||
var methodSpec = MethodSpec.methodBuilder("from")//metaData.copyMethodName())
|
||||
.addJavadoc("Return a \"with\"er for an existing record instance\n")
|
||||
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
|
||||
.addAnnotation(generatedRecordBuilderAnnotation)
|
||||
.addTypeVariables(typeVariables)
|
||||
.addParameter(recordClassType.typeName(), metaData.fromMethodName())
|
||||
.returns(withType)
|
||||
.addCode(codeBuilder.build())
|
||||
.build();
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void addStaticCopyBuilderMethod() {
|
||||
/*
|
||||
Adds a copy builder method that pre-fills the builder with existing values similar to:
|
||||
@@ -536,8 +604,8 @@ class InternalRecordBuilderProcessor {
|
||||
Adds a static method that converts a record instance into a stream of its component parts
|
||||
|
||||
public static Stream<Map.Entry<String, Object>> stream(MyRecord record) {
|
||||
return Stream.of(Map.entry("p1", record.p1()),
|
||||
Map.entry("p2", record.p2()));
|
||||
return Stream.of(new AbstractMap.SimpleImmutableEntry<>("p1", record.p1()),
|
||||
new AbstractMap.SimpleImmutableEntry<>("p2", record.p2()));
|
||||
}
|
||||
*/
|
||||
var codeBuilder = CodeBlock.builder().add("return $T.of(", Stream.class);
|
||||
@@ -546,7 +614,7 @@ class InternalRecordBuilderProcessor {
|
||||
codeBuilder.add(",\n ");
|
||||
}
|
||||
var name = recordComponents.get(index).name();
|
||||
codeBuilder.add("$T.entry($S, record.$L())", Map.class, name, name);
|
||||
codeBuilder.add("new $T<>($S, record.$L())", AbstractMap.SimpleImmutableEntry.class, name, name);
|
||||
});
|
||||
codeBuilder.add(")");
|
||||
var mapEntryTypeVariables = ParameterizedTypeName.get(Map.Entry.class, String.class, Object.class);
|
||||
@@ -596,7 +664,7 @@ class InternalRecordBuilderProcessor {
|
||||
return (component.typeName() instanceof ParameterizedTypeName parameterizedTypeName) && parameterizedTypeName.rawType.equals(optionalType);
|
||||
}
|
||||
|
||||
private void addWithGetterMethod(TypeSpec.Builder classBuilder, RecordClassType component) {
|
||||
private void addNestedGetterMethod(TypeSpec.Builder classBuilder, RecordClassType component) {
|
||||
/*
|
||||
For a single record component, add a getter similar to:
|
||||
|
||||
@@ -807,5 +875,70 @@ class InternalRecordBuilderProcessor {
|
||||
methodSpec.addStatement("return this").addParameter(parameterSpecBuilder.build());
|
||||
builder.addMethod(methodSpec.build());
|
||||
}
|
||||
|
||||
private List<TypeVariableName> typeVariablesWithReturn() {
|
||||
var variables = new ArrayList<TypeVariableName>();
|
||||
variables.add(rType);
|
||||
variables.addAll(typeVariables);
|
||||
return variables;
|
||||
}
|
||||
|
||||
private MethodSpec buildFunctionalHandler(String className, String methodName, boolean isMap) {
|
||||
/*
|
||||
Build a Functional handler ala:
|
||||
|
||||
default <R> R map(Function<R, T> proc) {
|
||||
return proc.apply(p());
|
||||
}
|
||||
*/
|
||||
var localTypeVariables = isMap ? typeVariablesWithReturn() : typeVariables;
|
||||
var typeName = localTypeVariables.isEmpty() ? ClassName.get("", className) : ParameterizedTypeName.get(ClassName.get("", className), localTypeVariables.toArray(TypeName[]::new));
|
||||
var methodBuilder = MethodSpec.methodBuilder(methodName)
|
||||
.addAnnotation(generatedRecordBuilderAnnotation)
|
||||
.addModifiers(Modifier.PUBLIC, Modifier.DEFAULT)
|
||||
.addParameter(typeName, "proc");
|
||||
var codeBlockBuilder = CodeBlock.builder();
|
||||
if (isMap) {
|
||||
methodBuilder.addJavadoc("Map record components into a new object");
|
||||
methodBuilder.addTypeVariable(rType);
|
||||
methodBuilder.returns(rType);
|
||||
codeBlockBuilder.add("return ");
|
||||
} else {
|
||||
methodBuilder.addJavadoc("Perform an operation on record components");
|
||||
}
|
||||
codeBlockBuilder.add("proc.apply(");
|
||||
addComponentCallsAsArguments(-1, codeBlockBuilder);
|
||||
codeBlockBuilder.add(");");
|
||||
methodBuilder.addCode(codeBlockBuilder.build());
|
||||
return methodBuilder.build();
|
||||
}
|
||||
|
||||
private TypeSpec buildFunctionalInterface(String className, boolean isMap) {
|
||||
/*
|
||||
Build a Functional interface ala:
|
||||
|
||||
@FunctionalInterface
|
||||
interface Function<R, T> {
|
||||
R apply(T a);
|
||||
}
|
||||
*/
|
||||
var localTypeVariables = isMap ? typeVariablesWithReturn() : typeVariables;
|
||||
var methodBuilder = MethodSpec.methodBuilder("apply").addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT);
|
||||
recordComponents.forEach(component -> {
|
||||
var parameterSpecBuilder = ParameterSpec.builder(component.typeName(), component.name());
|
||||
addConstructorAnnotations(component, parameterSpecBuilder);
|
||||
methodBuilder.addParameter(parameterSpecBuilder.build());
|
||||
});
|
||||
if (isMap) {
|
||||
methodBuilder.returns(rType);
|
||||
}
|
||||
return TypeSpec.interfaceBuilder(className)
|
||||
.addAnnotation(generatedRecordBuilderAnnotation)
|
||||
.addAnnotation(FunctionalInterface.class)
|
||||
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
|
||||
.addTypeVariables(localTypeVariables)
|
||||
.addMethod(methodBuilder.build())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -76,8 +76,7 @@ public class RecordBuilderProcessor
|
||||
var typeElement = (TypeElement) element;
|
||||
processRecordInterface(typeElement, element.getAnnotation(RecordInterface.class).addRecordBuilder(), getMetaData(typeElement), Optional.empty(), false);
|
||||
} else if (annotationClass.equals(RECORD_BUILDER_INCLUDE) || annotationClass.equals(RECORD_INTERFACE_INCLUDE)) {
|
||||
var metaData = RecordBuilderOptions.build(processingEnv.getOptions());
|
||||
processIncludes(element, metaData, annotationClass);
|
||||
processIncludes(element, getMetaData(element), annotationClass);
|
||||
} else {
|
||||
var recordBuilderTemplate = annotation.getAnnotation(RecordBuilder.Template.class);
|
||||
if (recordBuilderTemplate != null) {
|
||||
@@ -90,8 +89,8 @@ public class RecordBuilderProcessor
|
||||
}
|
||||
}
|
||||
|
||||
private RecordBuilder.Options getMetaData(TypeElement typeElement) {
|
||||
var recordSpecificMetaData = typeElement.getAnnotation(RecordBuilder.Options.class);
|
||||
private RecordBuilder.Options getMetaData(Element element) {
|
||||
var recordSpecificMetaData = element.getAnnotation(RecordBuilder.Options.class);
|
||||
return (recordSpecificMetaData != null) ? recordSpecificMetaData : RecordBuilderOptions.build(processingEnv.getOptions());
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder</artifactId>
|
||||
<version>27-java15</version>
|
||||
<version>31</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
<dependency>
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder-core</artifactId>
|
||||
<artifactId>record-builder-processor</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -51,31 +51,6 @@
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<annotationProcessorPaths>
|
||||
<annotationProcessorPath>
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder-processor</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</annotationProcessorPath>
|
||||
</annotationProcessorPaths>
|
||||
<annotationProcessors>
|
||||
<annotationProcessor>io.soabase.recordbuilder.processor.RecordBuilderProcessor</annotationProcessor>
|
||||
</annotationProcessors>
|
||||
<release>${jdk-version}</release>
|
||||
<compilerArgs>
|
||||
<arg>${enable-preview}</arg>
|
||||
</compilerArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-deploy-plugin</artifactId>
|
||||
|
||||
@@ -17,12 +17,20 @@ package io.soabase.recordbuilder.test;
|
||||
|
||||
import io.soabase.recordbuilder.core.RecordBuilder;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@RecordBuilder
|
||||
@RecordBuilder.Options(useImmutableCollections = true)
|
||||
public record CollectionRecord<T, X extends Point>(List<T> l, Set<T> s, Map<T, X> m, Collection<X> c) implements CollectionRecordBuilder.With<T, X> {
|
||||
@RecordBuilder.Options(useImmutableCollections = true, addFunctionalMethodsToWith = true)
|
||||
public record CollectionRecord<T, X extends Point>(List<T> l, Set<T> s, Map<T, X> m,
|
||||
Collection<X> c) implements CollectionRecordBuilder.With<T, X> {
|
||||
public static void main(String[] args) {
|
||||
var r = new CollectionRecord<>(List.of("hey"), Set.of("there"), Map.of("one", new Point(10, 20)), Set.of(new Point(30, 40)));
|
||||
Instant now = r.map((l1, s1, m1, c1) -> Instant.now());
|
||||
r.accept((l1, s1, m1, c1) -> {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright 2019 Jordan Zimmerman
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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 io.soabase.recordbuilder.test;
|
||||
|
||||
import io.soabase.recordbuilder.core.RecordBuilder;
|
||||
|
||||
@RecordBuilder.Options(prefixEnclosingClassNames = false)
|
||||
@RecordBuilder.Include(IncludeWithOption.Hey.class)
|
||||
public class IncludeWithOption {
|
||||
public static record Hey(String s){}
|
||||
}
|
||||
@@ -27,7 +27,8 @@ import java.util.Set;
|
||||
@RecordBuilder.Options(
|
||||
addSingleItemCollectionBuilders = true,
|
||||
singleItemBuilderPrefix = "add1",
|
||||
useImmutableCollections = true
|
||||
useImmutableCollections = true,
|
||||
addFunctionalMethodsToWith = true
|
||||
)
|
||||
public record SingleItems<T>(List<String> strings, Set<List<T>> sets, Map<Instant, T> map, Collection<T> collection) implements SingleItemsBuilder.With<T> {
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright 2019 Jordan Zimmerman
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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 io.soabase.recordbuilder.test;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class TestIncludes {
|
||||
@Test
|
||||
void testOptionsOnInclude() {
|
||||
// assert it's not prefixed with the enclosing class name
|
||||
IncludeWithOption.Hey hey = io.soabase.recordbuilder.test.HeyBuilder.builder().s("this is s").build();
|
||||
Assertions.assertEquals("this is s", hey.s());
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,9 @@
|
||||
*/
|
||||
package io.soabase.recordbuilder.test;
|
||||
|
||||
import java.util.AbstractMap.SimpleImmutableEntry;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -48,4 +51,28 @@ public class TestRecordInterface
|
||||
Assertions.assertEquals(generic.i(), 101);
|
||||
Assertions.assertEquals(generic.s(), now);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuilderStreamWithValues()
|
||||
{
|
||||
var stream = SimpleRecordBuilder.stream(SimpleRecordBuilder.builder()
|
||||
.i(19)
|
||||
.s("value")
|
||||
.build())
|
||||
.toList();
|
||||
Assertions.assertEquals(stream, List.of(
|
||||
Map.entry("i", 19),
|
||||
Map.entry("s", "value")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuilderStreamWithNulls()
|
||||
{
|
||||
var stream = SimpleRecordBuilder.stream(SimpleRecordBuilder.builder()
|
||||
.build())
|
||||
.toList();
|
||||
Assertions.assertEquals(stream, List.of(
|
||||
new SimpleImmutableEntry<>("i", 0),
|
||||
new SimpleImmutableEntry<>("s", null)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,17 @@ import org.junit.jupiter.api.Test;
|
||||
import java.util.List;
|
||||
|
||||
class TestWithers {
|
||||
@Test
|
||||
void testFromWithers() {
|
||||
var r1 = new SimpleGenericRecord<>(10, List.of("1", "2", "3"));
|
||||
var r2 = SimpleGenericRecordBuilder.from(r1).withS(List.of("4", "5"));
|
||||
var r3 = SimpleGenericRecordBuilder.from(r1).with(b -> b.i(20).s(List.of("6", "7")));
|
||||
Assertions.assertEquals(List.of("1", "2", "3"), r1.s());
|
||||
Assertions.assertEquals(List.of("4", "5"), r2.s());
|
||||
Assertions.assertEquals(List.of("6", "7"), r3.s());
|
||||
Assertions.assertEquals(20, r3.i());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWithers() {
|
||||
var r1 = new SimpleGenericRecord<>(10, List.of("1", "2", "3"));
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder</artifactId>
|
||||
<version>27-java15</version>
|
||||
<version>31</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user