Compare commits
37 Commits
record-bui
...
record-bui
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c4baa015f | ||
|
|
81b7b93a5b | ||
|
|
c39983e342 | ||
|
|
400caa2943 | ||
|
|
a2edd7299f | ||
|
|
6661c2ae0e | ||
|
|
74c8480b43 | ||
|
|
8dbdb43391 | ||
|
|
44064d656e | ||
|
|
791eb02faf | ||
|
|
aa3aefa39c | ||
|
|
de6946030f | ||
|
|
091c663520 | ||
|
|
6a45a2cbd9 | ||
|
|
c47e290363 | ||
|
|
d7abf4c60d | ||
|
|
6fbb0d0330 | ||
|
|
e235fc078f | ||
|
|
cf8f277018 | ||
|
|
e8e74cce1a | ||
|
|
3b425d4dce | ||
|
|
395f0879ac | ||
|
|
8969a17053 | ||
|
|
5ef0662d99 | ||
|
|
c6de55d5ad | ||
|
|
d19a0c2dc5 | ||
|
|
feb334e6ea | ||
|
|
f812e173e3 | ||
|
|
d50303ca09 | ||
|
|
13319e643b | ||
|
|
83802125aa | ||
|
|
b1e343f733 | ||
|
|
75fce6bb84 | ||
|
|
c676436f5e | ||
|
|
b861d0f4b1 | ||
|
|
933098b07d | ||
|
|
8560666ec6 |
3
.travis.yml
Normal file
3
.travis.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
language: java
|
||||
jdk:
|
||||
- openjdk15
|
||||
186
README.md
186
README.md
@@ -1,10 +1,26 @@
|
||||
[](https://travis-ci.org/Randgalt/record-builder)
|
||||
[](https://search.maven.org/search?q=g:io.soabase.record-builder%20a:record-builder)
|
||||
|
||||
# RecordBuilder - Early Access
|
||||
|
||||
## What is RecordBuilder
|
||||
|
||||
Java 14 is introducing [Records](https://cr.openjdk.java.net/~briangoetz/amber/datum.html) as a preview feature. Since Java 9, features in Java are being released in stages. While the Java 14 version of records is fantastic, it's currently missing an important feature for data classes: a builder. This project is an annotation processor that creates companion builder classes for Java records.
|
||||
Java 15 introduced [Records](https://cr.openjdk.java.net/~briangoetz/amber/datum.html) as a preview feature. Since Java 9,
|
||||
features in Java are being released in stages. While the Java 15 version of records is fantastic, it's currently missing important features
|
||||
for data classes: a builder and "with"ers. This project is an annotation processor that creates:
|
||||
|
||||
- a companion builder class for Java records
|
||||
- an interface that adds "with" copy methods
|
||||
- an annotation that generates a Java record from an Interface template
|
||||
|
||||
## Example
|
||||
_Details:_
|
||||
|
||||
- [RecordBuilder Details](#RecordBuilder-Example)
|
||||
- [Wither Details](#Wither-Example)
|
||||
- [RecordBuilder Full Definition](#Builder-Class-Definition)
|
||||
- [Record From Interface Details](#RecordInterface-Example)
|
||||
|
||||
## RecordBuilder Example
|
||||
|
||||
```java
|
||||
@RecordBuilder
|
||||
@@ -27,6 +43,31 @@ setAge(builder);
|
||||
var n3 = builder.build();
|
||||
```
|
||||
|
||||
## Wither Example
|
||||
|
||||
```java
|
||||
@RecordBuilder
|
||||
public record NameAndAge(String name, int age) implements NameAndAgeBuilder.With {}
|
||||
```
|
||||
|
||||
In addition to creating a builder, your record is enhanced by "wither" methods ala:
|
||||
|
||||
```java
|
||||
var r1 = new NameAndAge("foo", 123);
|
||||
var r2 = r1.withName("bar");
|
||||
var r3 = r2.withAge(456);
|
||||
|
||||
// access the builder as well
|
||||
var r4 = r3.with().age(101).name("baz").build();
|
||||
|
||||
// alternate method of accessing the builder (note: no need to call "build()")
|
||||
var r5 = r3.with(b -> b.age(200).name("whatever"));
|
||||
```
|
||||
|
||||
_Hat tip to [Benji Weber](https://benjiweber.co.uk/blog/2020/09/19/fun-with-java-records/) for the Withers idea._
|
||||
|
||||
## Builder Class Definition
|
||||
|
||||
The full builder class is defined as:
|
||||
|
||||
```java
|
||||
@@ -65,7 +106,7 @@ public class NameAndAgeBuilder {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a new value for this record component in the builder
|
||||
* Set a new value for the {@code name} record component in the builder
|
||||
*/
|
||||
public NameAndAgeBuilder name(String name) {
|
||||
this.name = name;
|
||||
@@ -73,13 +114,35 @@ public class NameAndAgeBuilder {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a new value for this record component in the builder
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(new AbstractMap.SimpleEntry<>("name", record.name()),
|
||||
new AbstractMap.SimpleEntry<>("age", record.age()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "NameAndAgeBuilder[name=" + name + ", age=" + age + "]";
|
||||
@@ -96,9 +159,67 @@ public class NameAndAgeBuilder {
|
||||
&& Objects.equals(name, b.name)
|
||||
&& (age == b.age));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add withers to {@code NameAndAge}
|
||||
*/
|
||||
public interface With {
|
||||
/**
|
||||
* Return a new record builder using the current values
|
||||
*/
|
||||
default NameAndAgeBuilder with() {
|
||||
var r = (NameAndAge)(Object)this;
|
||||
return NameAndAgeBuilder.builder(r);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new instance of {@code NameAndAge} with a new value for {@code name}
|
||||
*/
|
||||
default NameAndAge withName(String name) {
|
||||
var r = (NameAndAge)(Object)this;
|
||||
return new NameAndAge(name, r.age());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new instance of {@code NameAndAge} with a new value for {@code age}
|
||||
*/
|
||||
default NameAndAge withAge(int age) {
|
||||
var r = (NameAndAge)(Object)this;
|
||||
return new NameAndAge(r.name(), age);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## RecordInterface Example
|
||||
|
||||
```java
|
||||
@RecordInterface
|
||||
public interface NameAndAge {
|
||||
String name();
|
||||
int age();
|
||||
}
|
||||
```
|
||||
|
||||
This will generate a record ala:
|
||||
|
||||
```java
|
||||
@RecordBuilder
|
||||
public record NameAndAgeRecord(String name, int age) implements NameAndAge {}
|
||||
```
|
||||
|
||||
Note that the generated record is annotated with `@RecordBuilder` so a record
|
||||
builder is generated for the new record as well.
|
||||
|
||||
Notes:
|
||||
|
||||
- Non static methods in the interface...
|
||||
- ...cannot have arguments
|
||||
- ...must return a value
|
||||
- ...cannot have type parameters
|
||||
- Methods with default implementations are used in the generation unless they are annotated with `@IgnoreDefaultMethod`
|
||||
- If you do not want a record builder generated, annotate your interface as `@RecordInterface(addRecordBuilder = false)`
|
||||
|
||||
## Usage
|
||||
|
||||
### Maven
|
||||
@@ -135,7 +256,7 @@ public class NameAndAgeBuilder {
|
||||
|
||||
|
||||
<!-- "release" and "enable-preview" are required while records are preview features -->
|
||||
<release>14</release>
|
||||
<release>15</release>
|
||||
<compilerArgs>
|
||||
<arg>--enable-preview</arg>
|
||||
</compilerArgs>
|
||||
@@ -145,6 +266,30 @@ public class NameAndAgeBuilder {
|
||||
</plugin>
|
||||
```
|
||||
|
||||
3\. Enable Preview for Maven
|
||||
|
||||
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
|
||||
|
||||
Add the following to your build.gradle file:
|
||||
|
||||
```
|
||||
dependencies {
|
||||
annotationProcessor 'io.soabase.record-builder:record-builder-processor:$version-goes-here'
|
||||
implementation 'io.soabase.record-builder:record-builder-core:$version-goes-here'
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
options.fork = true
|
||||
options.forkOptions.jvmArgs += '--enable-preview'
|
||||
options.compilerArgs += '--enable-preview'
|
||||
}
|
||||
tasks.withType(Test) {
|
||||
jvmArgs += "--enable-preview"
|
||||
}
|
||||
```
|
||||
|
||||
### IDE
|
||||
|
||||
Depending on your IDE you are likely to need to enable Annotation Processing in your IDE settings.
|
||||
@@ -153,15 +298,30 @@ Depending on your IDE you are likely to need to enable Annotation Processing in
|
||||
|
||||
Note: records are a preview feature only. You'll need take a number of steps in order to try RecordBuilder:
|
||||
|
||||
- Install and make active Java 14 or later
|
||||
- Make sure your development tool is using Java 14 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))
|
||||
- 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 14 and Maven. If you get internal Javac errors I suggest rebuilding with `mvn clean package` and/or `mvn clean install`.
|
||||
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`.
|
||||
|
||||
## TODOs
|
||||
## Customizing
|
||||
|
||||
- Document how to integrate with Gradle
|
||||
- Keep up with changes
|
||||
- Testing
|
||||
- Etc.
|
||||
The names of the generated methods, etc. are determined by [RecordBuilderMetaData](https://github.com/Randgalt/record-builder/blob/master/record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilderMetaData.java). If you want to use your own meta data instance:
|
||||
|
||||
- Create a class that implements RecordBuilderMetaData
|
||||
- When compiling, make sure that the compiled class is in the processor path
|
||||
- Add a "metaDataClass" compiler option with the class name. E.g. `javac ... -AmetaDataClass=foo.bar.MyMetaData`
|
||||
|
||||
Alternatively, you can provide values for each individual meta data (or combinations):
|
||||
|
||||
- `javac ... -Asuffix=foo`
|
||||
- `javac ... -AinterfaceSuffix=foo`
|
||||
- `javac ... -AcopyMethodName=foo`
|
||||
- `javac ... -AbuilderMethodName=foo`
|
||||
- `javac ... -AbuildMethodName=foo`
|
||||
- `javac ... -AcomponentsMethodName=foo`
|
||||
- `javac ... -AwithClassName=foo`
|
||||
- `javac ... -AwithClassMethodPrefix=foo`
|
||||
- `javac ... -AfileComment=foo`
|
||||
- `javac ... -AfileIndent=foo`
|
||||
- `javac ... -AprefixEnclosingClassNames=foo`
|
||||
|
||||
21
pom.xml
21
pom.xml
@@ -5,7 +5,7 @@
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<version>1.4.ea-SNAPSHOT</version>
|
||||
<version>1.9.ea</version>
|
||||
|
||||
<modules>
|
||||
<module>record-builder-core</module>
|
||||
@@ -18,7 +18,7 @@
|
||||
<project.build.resourceEncoding>UTF-8</project.build.resourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
|
||||
<jdk-version>14</jdk-version>
|
||||
<jdk-version>15</jdk-version>
|
||||
|
||||
<maven-compiler-plugin-version>3.8.1</maven-compiler-plugin-version>
|
||||
<maven-source-plugin-version>3.2.0</maven-source-plugin-version>
|
||||
@@ -30,8 +30,9 @@
|
||||
<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>
|
||||
|
||||
<javapoet-version>1.11.0</javapoet-version>
|
||||
<javapoet-version>1.12.1</javapoet-version>
|
||||
<junit-jupiter-version>5.5.2</junit-jupiter-version>
|
||||
<asm-version>7.2</asm-version>
|
||||
</properties>
|
||||
@@ -70,7 +71,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>HEAD</tag>
|
||||
<tag>record-builder-1.9.ea</tag>
|
||||
</scm>
|
||||
|
||||
<issueManagement>
|
||||
@@ -179,6 +180,9 @@
|
||||
<exclude>**/io/soabase/com/google/**</exclude>
|
||||
<exclude>**/com/company/**</exclude>
|
||||
<exclude>**/META-INF/services/**</exclude>
|
||||
<exclude>**/jvm.config</exclude>
|
||||
<exclude>**/.java-version</exclude>
|
||||
<exclude>**/.travis.yml</exclude>
|
||||
</excludes>
|
||||
<strictCheck>true</strictCheck>
|
||||
</configuration>
|
||||
@@ -268,6 +272,15 @@
|
||||
<tagNameFormat>record-builder-@{project.version}</tagNameFormat>
|
||||
</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>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder</artifactId>
|
||||
<version>1.4.ea-SNAPSHOT</version>
|
||||
<version>1.9.ea</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* 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.core;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@Target(ElementType.METHOD)
|
||||
public @interface IgnoreDefaultMethod {
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright 2016 Jordan Zimmerman
|
||||
* 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.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright 2016 Jordan Zimmerman
|
||||
* 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.
|
||||
@@ -41,6 +41,16 @@ public interface RecordBuilderMetaData {
|
||||
return "Builder";
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by {@code RecordInterface}. The generated record will have the same name as the annotated interface
|
||||
* plus this suffix. E.g. if the interface name is "Foo", the record will be named "FooRecord".
|
||||
*
|
||||
* @return suffix
|
||||
*/
|
||||
default String interfaceSuffix() {
|
||||
return "Record";
|
||||
}
|
||||
|
||||
/**
|
||||
* The name to use for the copy builder
|
||||
*
|
||||
@@ -68,13 +78,40 @@ public interface RecordBuilderMetaData {
|
||||
return "build";
|
||||
}
|
||||
|
||||
/**
|
||||
* The name to use for the method that returns the record components as a stream
|
||||
*
|
||||
* @return build method
|
||||
*/
|
||||
default String componentsMethodName() {
|
||||
return "stream";
|
||||
}
|
||||
|
||||
/**
|
||||
* The name to use for the nested With class
|
||||
*
|
||||
* @return with class name
|
||||
*/
|
||||
default String withClassName() {
|
||||
return "With";
|
||||
}
|
||||
|
||||
/**
|
||||
* The prefix to use for the methods in the With class
|
||||
*
|
||||
* @return prefix
|
||||
*/
|
||||
default String withClassMethodPrefix() {
|
||||
return "with";
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the comment to place at the top of generated files. Return null or an empty string for no comment.
|
||||
*
|
||||
* @return comment or empty
|
||||
*/
|
||||
default String fileComment() {
|
||||
return "Auto generated by RecordBuilder: https://github.com/Randgalt/record-builder";
|
||||
return "Auto generated by io.soabase.recordbuilder.core.RecordBuilder: https://github.com/Randgalt/record-builder";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 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.core;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@Target(ElementType.TYPE)
|
||||
public @interface RecordInterface {
|
||||
boolean addRecordBuilder() default true;
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder</artifactId>
|
||||
<version>1.4.ea-SNAPSHOT</version>
|
||||
<version>1.9.ea</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -24,30 +24,12 @@
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<proc>none</proc>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<!--plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<configuration>
|
||||
<artifactSet>
|
||||
<includes>
|
||||
<include>com.squareup:javapoet</include>
|
||||
<include>io.soabase.record-builder:record-builder-processor</include>
|
||||
</includes>
|
||||
</artifactSet>
|
||||
<relocations>
|
||||
<relocation>
|
||||
<pattern>com.squareup.javapoet</pattern>
|
||||
<shadedPattern>io.soabase.com.squareup.javapoet</shadedPattern>
|
||||
</relocation>
|
||||
</relocations>
|
||||
</configuration>
|
||||
</plugin-->
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright 2016 Jordan Zimmerman
|
||||
* 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.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright 2016 Jordan Zimmerman
|
||||
* 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.
|
||||
@@ -19,6 +19,7 @@ import com.squareup.javapoet.ClassName;
|
||||
import com.squareup.javapoet.ParameterizedTypeName;
|
||||
import com.squareup.javapoet.TypeName;
|
||||
import com.squareup.javapoet.TypeVariableName;
|
||||
import io.soabase.recordbuilder.core.RecordBuilderMetaData;
|
||||
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.element.RecordComponentElement;
|
||||
@@ -60,6 +61,28 @@ public class ElementUtils {
|
||||
return new ClassType(TypeName.get(recordComponent.asType()), recordComponent.getSimpleName().toString());
|
||||
}
|
||||
|
||||
public static String getWithMethodName(ClassType component, String prefix) {
|
||||
var name = component.name();
|
||||
if (name.length() == 1) {
|
||||
return prefix + name.toUpperCase();
|
||||
}
|
||||
return prefix + Character.toUpperCase(name.charAt(0)) + name.substring(1);
|
||||
}
|
||||
|
||||
public static String getBuilderName(TypeElement element, RecordBuilderMetaData metaData, ClassType classType, String suffix) {
|
||||
// generate the class name
|
||||
var baseName = classType.name() + suffix;
|
||||
return metaData.prefixEnclosingClassNames() ? (getBuilderNamePrefix(element.getEnclosingElement()) + baseName) : baseName;
|
||||
}
|
||||
|
||||
private static String getBuilderNamePrefix(Element element) {
|
||||
// prefix enclosing class names if nested in a class
|
||||
if (element instanceof TypeElement) {
|
||||
return getBuilderNamePrefix(element.getEnclosingElement()) + element.getSimpleName().toString();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private ElementUtils() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,515 @@
|
||||
/**
|
||||
* 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.processor;
|
||||
|
||||
import com.squareup.javapoet.ClassName;
|
||||
import com.squareup.javapoet.CodeBlock;
|
||||
import com.squareup.javapoet.FieldSpec;
|
||||
import com.squareup.javapoet.MethodSpec;
|
||||
import com.squareup.javapoet.ParameterSpec;
|
||||
import com.squareup.javapoet.ParameterizedTypeName;
|
||||
import com.squareup.javapoet.TypeName;
|
||||
import com.squareup.javapoet.TypeSpec;
|
||||
import com.squareup.javapoet.TypeVariableName;
|
||||
import io.soabase.recordbuilder.core.RecordBuilderMetaData;
|
||||
|
||||
import javax.lang.model.element.Modifier;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
|
||||
import java.util.AbstractMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static io.soabase.recordbuilder.processor.ElementUtils.getBuilderName;
|
||||
import static io.soabase.recordbuilder.processor.ElementUtils.getWithMethodName;
|
||||
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.generatedRecordBuilderAnnotation;
|
||||
|
||||
class InternalRecordBuilderProcessor
|
||||
{
|
||||
private final RecordBuilderMetaData metaData;
|
||||
private final ClassType recordClassType;
|
||||
private final String packageName;
|
||||
private final ClassType builderClassType;
|
||||
private final List<TypeVariableName> typeVariables;
|
||||
private final List<ClassType> recordComponents;
|
||||
private final TypeSpec builderType;
|
||||
private final TypeSpec.Builder builder;
|
||||
|
||||
InternalRecordBuilderProcessor(TypeElement record, RecordBuilderMetaData metaData)
|
||||
{
|
||||
this.metaData = metaData;
|
||||
recordClassType = ElementUtils.getClassType(record, record.getTypeParameters());
|
||||
packageName = ElementUtils.getPackageName(record);
|
||||
builderClassType = ElementUtils.getClassType(packageName, getBuilderName(record, metaData, recordClassType, metaData.suffix()), record.getTypeParameters());
|
||||
typeVariables = record.getTypeParameters().stream().map(TypeVariableName::get).collect(Collectors.toList());
|
||||
recordComponents = record.getRecordComponents().stream().map(ElementUtils::getClassType).collect(Collectors.toList());
|
||||
|
||||
builder = TypeSpec.classBuilder(builderClassType.name())
|
||||
.addModifiers(Modifier.PUBLIC)
|
||||
.addAnnotation(generatedRecordBuilderAnnotation)
|
||||
.addTypeVariables(typeVariables);
|
||||
addWithNestedClass();
|
||||
addDefaultConstructor();
|
||||
addAllArgsConstructor();
|
||||
addStaticDefaultBuilderMethod();
|
||||
addStaticCopyBuilderMethod();
|
||||
addStaticComponentsMethod();
|
||||
addBuildMethod();
|
||||
addToStringMethod();
|
||||
addHashCodeMethod();
|
||||
addEqualsMethod();
|
||||
recordComponents.forEach(component -> {
|
||||
add1Field(component);
|
||||
add1SetterMethod(component);
|
||||
add1GetterMethod(component);
|
||||
});
|
||||
builderType = builder.build();
|
||||
}
|
||||
|
||||
String packageName()
|
||||
{
|
||||
return packageName;
|
||||
}
|
||||
|
||||
ClassType builderClassType()
|
||||
{
|
||||
return builderClassType;
|
||||
}
|
||||
|
||||
TypeSpec builderType()
|
||||
{
|
||||
return builderType;
|
||||
}
|
||||
|
||||
private void addWithNestedClass()
|
||||
{
|
||||
/*
|
||||
Adds a nested interface that adds withers similar to:
|
||||
|
||||
public class MyRecordBuilder {
|
||||
public interface With {
|
||||
// with methods
|
||||
}
|
||||
}
|
||||
*/
|
||||
var classBuilder = TypeSpec.interfaceBuilder(metaData.withClassName())
|
||||
.addAnnotation(generatedRecordBuilderAnnotation)
|
||||
.addJavadoc("Add withers to {@code $L}\n", recordClassType.name())
|
||||
.addModifiers(Modifier.PUBLIC)
|
||||
.addTypeVariables(typeVariables);
|
||||
addWithBuilderMethod(classBuilder);
|
||||
addWithSuppliedBuilderMethod(classBuilder);
|
||||
IntStream.range(0, recordComponents.size()).forEach(index -> add1WithMethod(classBuilder, recordComponents.get(index), index));
|
||||
builder.addType(classBuilder.build());
|
||||
}
|
||||
|
||||
private void addWithSuppliedBuilderMethod(TypeSpec.Builder classBuilder)
|
||||
{
|
||||
/*
|
||||
Adds a method that returns a pre-filled copy builder similar to:
|
||||
|
||||
default MyRecord with(Consumer<MyRecordBuilder> consumer) {
|
||||
MyRecord r = (MyRecord)(Object)this;
|
||||
MyRecordBuilder builder MyRecordBuilder.builder(r);
|
||||
consumer.accept(builder);
|
||||
return builder.build();
|
||||
}
|
||||
*/
|
||||
var codeBlockBuilder = CodeBlock.builder()
|
||||
.add("var r = ($T)(Object)this;\n", recordClassType.typeName())
|
||||
.add("$T builder = $L.$L(r);\n", builderClassType.typeName(), builderClassType.name(), metaData.copyMethodName())
|
||||
.add("consumer.accept(builder);\n")
|
||||
.add("return builder.build();\n");
|
||||
var consumerType = ParameterizedTypeName.get(ClassName.get(Consumer.class), builderClassType.typeName());
|
||||
var parameter = ParameterSpec.builder(consumerType, "consumer").build();
|
||||
var methodSpec = MethodSpec.methodBuilder(metaData.withClassMethodPrefix())
|
||||
.addAnnotation(generatedRecordBuilderAnnotation)
|
||||
.addJavadoc("Return a new record built from the builder passed to the given consumer")
|
||||
.addModifiers(Modifier.PUBLIC, Modifier.DEFAULT)
|
||||
.addParameter(parameter)
|
||||
.returns(recordClassType.typeName())
|
||||
.addCode(codeBlockBuilder.build())
|
||||
.build();
|
||||
classBuilder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void addWithBuilderMethod(TypeSpec.Builder classBuilder)
|
||||
{
|
||||
/*
|
||||
Adds a method that returns a pre-filled copy builder similar to:
|
||||
|
||||
default MyRecordBuilder with() {
|
||||
MyRecord r = (MyRecord)(Object)this;
|
||||
return MyRecordBuilder.builder(r);
|
||||
}
|
||||
*/
|
||||
var codeBlockBuilder = CodeBlock.builder()
|
||||
.add("var r = ($T)(Object)this;\n", recordClassType.typeName())
|
||||
.add("return $L.$L(r);", builderClassType.name(), metaData.copyMethodName());
|
||||
var methodSpec = MethodSpec.methodBuilder(metaData.withClassMethodPrefix())
|
||||
.addAnnotation(generatedRecordBuilderAnnotation)
|
||||
.addJavadoc("Return a new record builder using the current values")
|
||||
.addModifiers(Modifier.PUBLIC, Modifier.DEFAULT)
|
||||
.returns(builderClassType.typeName())
|
||||
.addCode(codeBlockBuilder.build())
|
||||
.build();
|
||||
classBuilder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void add1WithMethod(TypeSpec.Builder classBuilder, ClassType component, int index)
|
||||
{
|
||||
/*
|
||||
Adds a with method for the component similar to:
|
||||
|
||||
default MyRecord withName(String name) {
|
||||
MyRecord r = (MyRecord)(Object)this;
|
||||
return new MyRecord(name, r.age());
|
||||
}
|
||||
*/
|
||||
var codeBlockBuilder = CodeBlock.builder()
|
||||
.add("var r = ($T)(Object)this;\n", recordClassType.typeName())
|
||||
.add("return new $T(", recordClassType.typeName());
|
||||
IntStream.range(0, recordComponents.size()).forEach(parameterIndex -> {
|
||||
if (parameterIndex > 0) {
|
||||
codeBlockBuilder.add(", ");
|
||||
}
|
||||
ClassType parameterComponent = recordComponents.get(parameterIndex);
|
||||
if (parameterIndex == index) {
|
||||
codeBlockBuilder.add(parameterComponent.name());
|
||||
}
|
||||
else {
|
||||
codeBlockBuilder.add("r.$L()", parameterComponent.name());
|
||||
}
|
||||
});
|
||||
codeBlockBuilder.add(");");
|
||||
|
||||
var methodName = getWithMethodName(component, metaData.withClassMethodPrefix());
|
||||
var parameterSpec = ParameterSpec.builder(component.typeName(), component.name()).build();
|
||||
var methodSpec = MethodSpec.methodBuilder(methodName)
|
||||
.addAnnotation(generatedRecordBuilderAnnotation)
|
||||
.addJavadoc("Return a new instance of {@code $L} with a new value for {@code $L}\n", recordClassType.name(), component.name())
|
||||
.addModifiers(Modifier.PUBLIC, Modifier.DEFAULT)
|
||||
.addParameter(parameterSpec)
|
||||
.addCode(codeBlockBuilder.build())
|
||||
.returns(recordClassType.typeName())
|
||||
.build();
|
||||
classBuilder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void addDefaultConstructor()
|
||||
{
|
||||
/*
|
||||
Adds a default constructor similar to:
|
||||
|
||||
private MyRecordBuilder() {
|
||||
}
|
||||
*/
|
||||
var constructor = MethodSpec.constructorBuilder()
|
||||
.addModifiers(Modifier.PRIVATE)
|
||||
.addAnnotation(generatedRecordBuilderAnnotation)
|
||||
.build();
|
||||
builder.addMethod(constructor);
|
||||
}
|
||||
|
||||
private void addAllArgsConstructor()
|
||||
{
|
||||
/*
|
||||
Adds an all-args constructor similar to:
|
||||
|
||||
private MyRecordBuilder(int p1, T p2, ...) {
|
||||
this.p1 = p1;
|
||||
this.p2 = p2;
|
||||
...
|
||||
}
|
||||
*/
|
||||
var constructorBuilder = MethodSpec.constructorBuilder()
|
||||
.addModifiers(Modifier.PRIVATE)
|
||||
.addAnnotation(generatedRecordBuilderAnnotation);
|
||||
recordComponents.forEach(component -> {
|
||||
constructorBuilder.addParameter(component.typeName(), component.name());
|
||||
var codeBuilder = CodeBlock.builder().add("this.$L = $L", component.name(), component.name());
|
||||
constructorBuilder.addStatement(codeBuilder.build());
|
||||
});
|
||||
builder.addMethod(constructorBuilder.build());
|
||||
}
|
||||
|
||||
private void addToStringMethod()
|
||||
{
|
||||
/*
|
||||
add a toString() method similar to:
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "MyRecord[p1=blah, p2=blah]";
|
||||
}
|
||||
*/
|
||||
var codeBuilder = CodeBlock.builder().add("return \"$L[", builderClassType.name());
|
||||
IntStream.range(0, recordComponents.size()).forEach(index -> {
|
||||
if (index > 0) {
|
||||
codeBuilder.add(", ");
|
||||
}
|
||||
String name = recordComponents.get(index).name();
|
||||
codeBuilder.add("$L=\" + $L + \"", name, name);
|
||||
});
|
||||
codeBuilder.add("]\"");
|
||||
|
||||
var methodSpec = MethodSpec.methodBuilder("toString")
|
||||
.addModifiers(Modifier.PUBLIC)
|
||||
.addAnnotation(generatedRecordBuilderAnnotation)
|
||||
.addAnnotation(Override.class)
|
||||
.returns(String.class)
|
||||
.addStatement(codeBuilder.build())
|
||||
.build();
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void addHashCodeMethod()
|
||||
{
|
||||
/*
|
||||
add a hashCode() method similar to:
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(p1, p2);
|
||||
}
|
||||
*/
|
||||
var codeBuilder = CodeBlock.builder().add("return $T.hash(", Objects.class);
|
||||
IntStream.range(0, recordComponents.size()).forEach(index -> {
|
||||
if (index > 0) {
|
||||
codeBuilder.add(", ");
|
||||
}
|
||||
codeBuilder.add("$L", recordComponents.get(index).name());
|
||||
});
|
||||
codeBuilder.add(")");
|
||||
|
||||
var methodSpec = MethodSpec.methodBuilder("hashCode")
|
||||
.addModifiers(Modifier.PUBLIC)
|
||||
.addAnnotation(generatedRecordBuilderAnnotation)
|
||||
.addAnnotation(Override.class)
|
||||
.returns(TypeName.INT)
|
||||
.addStatement(codeBuilder.build())
|
||||
.build();
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void addEqualsMethod()
|
||||
{
|
||||
/*
|
||||
add an equals() method similar to:
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) || ((o instanceof MyRecordBuilder b)
|
||||
&& Objects.equals(p1, b.p1)
|
||||
&& Objects.equals(p2, b.p2));
|
||||
}
|
||||
*/
|
||||
var codeBuilder = CodeBlock.builder();
|
||||
codeBuilder.add("return (this == o) || (");
|
||||
codeBuilder.add("(o instanceof $L b)", builderClassType.name());
|
||||
recordComponents.forEach(recordComponent -> {
|
||||
String name = recordComponent.name();
|
||||
if (recordComponent.typeName().isPrimitive()) {
|
||||
codeBuilder.add("\n&& ($L == b.$L)", name, name);
|
||||
}
|
||||
else {
|
||||
codeBuilder.add("\n&& $T.equals($L, b.$L)", Objects.class, name, name);
|
||||
}
|
||||
});
|
||||
codeBuilder.add(")");
|
||||
|
||||
var methodSpec = MethodSpec.methodBuilder("equals")
|
||||
.addParameter(Object.class, "o")
|
||||
.addModifiers(Modifier.PUBLIC)
|
||||
.addAnnotation(generatedRecordBuilderAnnotation)
|
||||
.addAnnotation(Override.class)
|
||||
.returns(TypeName.BOOLEAN)
|
||||
.addStatement(codeBuilder.build())
|
||||
.build();
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void addBuildMethod()
|
||||
{
|
||||
/*
|
||||
Adds the build method that generates the record similar to:
|
||||
|
||||
public MyRecord build() {
|
||||
return new MyRecord(p1, p2, ...);
|
||||
}
|
||||
*/
|
||||
var codeBuilder = CodeBlock.builder().add("return new $T(", recordClassType.typeName());
|
||||
IntStream.range(0, recordComponents.size()).forEach(index -> {
|
||||
if (index > 0) {
|
||||
codeBuilder.add(", ");
|
||||
}
|
||||
codeBuilder.add("$L", recordComponents.get(index).name());
|
||||
});
|
||||
codeBuilder.add(")");
|
||||
|
||||
var methodSpec = MethodSpec.methodBuilder(metaData.buildMethodName())
|
||||
.addJavadoc("Return a new record instance with all fields set to the current values in this builder\n")
|
||||
.addModifiers(Modifier.PUBLIC)
|
||||
.addAnnotation(generatedRecordBuilderAnnotation)
|
||||
.returns(recordClassType.typeName())
|
||||
.addStatement(codeBuilder.build())
|
||||
.build();
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void addStaticCopyBuilderMethod()
|
||||
{
|
||||
/*
|
||||
Adds a copy builder method that pre-fills the builder with existing values similar to:
|
||||
|
||||
public static MyRecordBuilder builder(MyRecord from) {
|
||||
return new MyRecordBuilder(from.p1(), from.p2(), ...);
|
||||
}
|
||||
*/
|
||||
var codeBuilder = CodeBlock.builder().add("return new $T(", builderClassType.typeName());
|
||||
IntStream.range(0, recordComponents.size()).forEach(index -> {
|
||||
if (index > 0) {
|
||||
codeBuilder.add(", ");
|
||||
}
|
||||
codeBuilder.add("from.$L()", recordComponents.get(index).name());
|
||||
});
|
||||
codeBuilder.add(")");
|
||||
|
||||
var methodSpec = MethodSpec.methodBuilder(metaData.copyMethodName())
|
||||
.addJavadoc("Return a new builder with all fields set to the values taken from the given record instance\n")
|
||||
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
|
||||
.addAnnotation(generatedRecordBuilderAnnotation)
|
||||
.addTypeVariables(typeVariables)
|
||||
.addParameter(recordClassType.typeName(), "from")
|
||||
.returns(builderClassType.typeName())
|
||||
.addStatement(codeBuilder.build())
|
||||
.build();
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void addStaticDefaultBuilderMethod()
|
||||
{
|
||||
/*
|
||||
Adds a the default builder method similar to:
|
||||
|
||||
public static MyRecordBuilder builder() {
|
||||
return new MyRecordBuilder();
|
||||
}
|
||||
*/
|
||||
var methodSpec = MethodSpec.methodBuilder(metaData.builderMethodName())
|
||||
.addJavadoc("Return a new builder with all fields set to default Java values\n")
|
||||
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
|
||||
.addAnnotation(generatedRecordBuilderAnnotation)
|
||||
.addTypeVariables(typeVariables)
|
||||
.returns(builderClassType.typeName())
|
||||
.addStatement("return new $T()", builderClassType.typeName())
|
||||
.build();
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void addStaticComponentsMethod()
|
||||
{
|
||||
/*
|
||||
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(
|
||||
new AbstractMap.SimpleEntry<>("p1", record.p1()),
|
||||
new AbstractMap.SimpleEntry<>("p2", record.p2())
|
||||
);
|
||||
}
|
||||
*/
|
||||
var codeBuilder = CodeBlock.builder().add("return $T.of(", Stream.class);
|
||||
IntStream.range(0, recordComponents.size()).forEach(index -> {
|
||||
if (index > 0) {
|
||||
codeBuilder.add(",\n ");
|
||||
}
|
||||
var name = recordComponents.get(index).name();
|
||||
codeBuilder.add("new $T<>($S, record.$L())", AbstractMap.SimpleEntry.class, name, name);
|
||||
});
|
||||
codeBuilder.add(")");
|
||||
var mapEntryTypeVariables = ParameterizedTypeName.get(Map.Entry.class, String.class, Object.class);
|
||||
var mapEntryType = ParameterizedTypeName.get(ClassName.get(Stream.class), mapEntryTypeVariables);
|
||||
var methodSpec = MethodSpec.methodBuilder(metaData.componentsMethodName())
|
||||
.addJavadoc("Return a stream of the record components as map entries keyed with the component name and the value as the component value\n")
|
||||
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
|
||||
.addParameter(recordClassType.typeName(), "record")
|
||||
.addAnnotation(generatedRecordBuilderAnnotation)
|
||||
.addTypeVariables(typeVariables)
|
||||
.returns(mapEntryType)
|
||||
.addStatement(codeBuilder.build())
|
||||
.build();
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void add1Field(ClassType component)
|
||||
{
|
||||
/*
|
||||
For a single record component, add a field similar to:
|
||||
|
||||
private T p;
|
||||
*/
|
||||
var fieldSpec = FieldSpec.builder(component.typeName(), component.name(), Modifier.PRIVATE).build();
|
||||
builder.addField(fieldSpec);
|
||||
}
|
||||
|
||||
private void add1GetterMethod(ClassType component)
|
||||
{
|
||||
/*
|
||||
For a single record component, add a getter similar to:
|
||||
|
||||
public T p() {
|
||||
return p;
|
||||
}
|
||||
*/
|
||||
var methodSpec = MethodSpec.methodBuilder(component.name())
|
||||
.addJavadoc("Return the current value for the {@code $L} record component in the builder\n", component.name())
|
||||
.addModifiers(Modifier.PUBLIC)
|
||||
.addAnnotation(generatedRecordBuilderAnnotation)
|
||||
.returns(component.typeName())
|
||||
.addStatement("return $L", component.name())
|
||||
.build();
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void add1SetterMethod(ClassType component)
|
||||
{
|
||||
/*
|
||||
For a single record component, add a setter similar to:
|
||||
|
||||
public MyRecordBuilder p(T p) {
|
||||
this.p = p;
|
||||
return this;
|
||||
}
|
||||
*/
|
||||
var parameterSpec = ParameterSpec.builder(component.typeName(), component.name()).build();
|
||||
var methodSpec = MethodSpec.methodBuilder(component.name())
|
||||
.addJavadoc("Set a new value for the {@code $L} record component in the builder\n", component.name())
|
||||
.addModifiers(Modifier.PUBLIC)
|
||||
.addAnnotation(generatedRecordBuilderAnnotation)
|
||||
.addParameter(parameterSpec)
|
||||
.returns(builderClassType.typeName())
|
||||
.addStatement("this.$L = $L", component.name(), component.name())
|
||||
.addStatement("return this")
|
||||
.build();
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* 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.processor;
|
||||
|
||||
import com.squareup.javapoet.*;
|
||||
import io.soabase.recordbuilder.core.RecordBuilder;
|
||||
import io.soabase.recordbuilder.core.RecordBuilderMetaData;
|
||||
import io.soabase.recordbuilder.core.RecordInterface;
|
||||
import io.soabase.recordbuilder.core.IgnoreDefaultMethod;
|
||||
|
||||
import javax.annotation.processing.ProcessingEnvironment;
|
||||
import javax.lang.model.element.ElementKind;
|
||||
import javax.lang.model.element.ExecutableElement;
|
||||
import javax.lang.model.element.Modifier;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import javax.lang.model.type.TypeKind;
|
||||
import javax.tools.Diagnostic;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static io.soabase.recordbuilder.processor.ElementUtils.getBuilderName;
|
||||
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.generatedRecordInterfaceAnnotation;
|
||||
|
||||
class InternalRecordInterfaceProcessor {
|
||||
private final ProcessingEnvironment processingEnv;
|
||||
private final String packageName;
|
||||
private final TypeSpec recordType;
|
||||
private final List<ExecutableElement> recordComponents;
|
||||
private final TypeElement iface;
|
||||
private final ClassType recordClassType;
|
||||
|
||||
private static final String FAKE_METHOD_NAME = "__FAKE__";
|
||||
|
||||
InternalRecordInterfaceProcessor(ProcessingEnvironment processingEnv, TypeElement iface, RecordInterface recordInterface, RecordBuilderMetaData metaData) {
|
||||
this.processingEnv = processingEnv;
|
||||
packageName = ElementUtils.getPackageName(iface);
|
||||
recordComponents = getRecordComponents(iface);
|
||||
this.iface = iface;
|
||||
|
||||
ClassType ifaceClassType = ElementUtils.getClassType(iface, iface.getTypeParameters());
|
||||
recordClassType = ElementUtils.getClassType(packageName, getBuilderName(iface, metaData, ifaceClassType, metaData.interfaceSuffix()), iface.getTypeParameters());
|
||||
List<TypeVariableName> typeVariables = iface.getTypeParameters().stream().map(TypeVariableName::get).collect(Collectors.toList());
|
||||
|
||||
MethodSpec methodSpec = generateArgumentList();
|
||||
|
||||
TypeSpec.Builder builder = TypeSpec.classBuilder(recordClassType.name())
|
||||
.addSuperinterface(iface.asType())
|
||||
.addMethod(methodSpec)
|
||||
.addModifiers(Modifier.PUBLIC)
|
||||
.addAnnotation(generatedRecordInterfaceAnnotation)
|
||||
.addTypeVariables(typeVariables);
|
||||
|
||||
if (recordInterface.addRecordBuilder()) {
|
||||
builder.addAnnotation(RecordBuilder.class);
|
||||
}
|
||||
|
||||
recordType = builder.build();
|
||||
}
|
||||
|
||||
boolean isValid()
|
||||
{
|
||||
return !recordComponents.isEmpty();
|
||||
}
|
||||
|
||||
TypeSpec recordType() {
|
||||
return recordType;
|
||||
}
|
||||
|
||||
String packageName() {
|
||||
return packageName;
|
||||
}
|
||||
|
||||
ClassType recordClassType() {
|
||||
return recordClassType;
|
||||
}
|
||||
|
||||
String toRecord(String classSource)
|
||||
{
|
||||
// javapoet does yet support records - so a class was created and we can reshape it
|
||||
// The class will look something like this:
|
||||
/*
|
||||
// Auto generated by io.soabase.recordbuilder.core.RecordBuilder: https://github.com/Randgalt/record-builder
|
||||
package io.soabase.recordbuilder.test;
|
||||
|
||||
import io.soabase.recordbuilder.core.RecordBuilder;
|
||||
import javax.annotation.processing.Generated;
|
||||
|
||||
@Generated("io.soabase.recordbuilder.core.RecordInterface")
|
||||
@RecordBuilder
|
||||
public class MyRecord implements MyInterface {
|
||||
void __FAKE__(String name, int age) {
|
||||
}
|
||||
}
|
||||
*/
|
||||
Pattern pattern = Pattern.compile("(.*)(implements.*)(\\{)(.*" + FAKE_METHOD_NAME + ")(\\(.*\\))(.*)", Pattern.MULTILINE | Pattern.DOTALL);
|
||||
Matcher matcher = pattern.matcher(classSource);
|
||||
if (!matcher.find() || matcher.groupCount() != 6) {
|
||||
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Internal error generating record. Group count: " + matcher.groupCount(), iface);
|
||||
}
|
||||
|
||||
String declaration = matcher.group(1).trim().replace("class", "record");
|
||||
String implementsSection = matcher.group(2).trim();
|
||||
String argumentList = matcher.group(5).trim();
|
||||
return declaration + argumentList + " " + implementsSection + " {}";
|
||||
}
|
||||
|
||||
private MethodSpec generateArgumentList()
|
||||
{
|
||||
MethodSpec.Builder builder = MethodSpec.methodBuilder(FAKE_METHOD_NAME);
|
||||
recordComponents.forEach(element -> {
|
||||
ParameterSpec parameterSpec = ParameterSpec.builder(ClassName.get(element.getReturnType()), element.getSimpleName().toString()).build();
|
||||
builder.addTypeVariables(element.getTypeParameters().stream().map(TypeVariableName::get).collect(Collectors.toList()));
|
||||
builder.addParameter(parameterSpec);
|
||||
});
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private List<ExecutableElement> getRecordComponents(TypeElement iface) {
|
||||
List<ExecutableElement> components = new ArrayList<>();
|
||||
try {
|
||||
getRecordComponents(iface, components, new HashSet<>(), new HashSet<>());
|
||||
if (components.isEmpty()) {
|
||||
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Annotated interface has no component methods", iface);
|
||||
}
|
||||
} catch (IllegalInterface e) {
|
||||
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, e.getMessage(), iface);
|
||||
components = Collections.emptyList();
|
||||
}
|
||||
return components;
|
||||
}
|
||||
|
||||
private static class IllegalInterface extends RuntimeException
|
||||
{
|
||||
public IllegalInterface(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
private void getRecordComponents(TypeElement iface, Collection<? super ExecutableElement> components, Set<String> visitedSet, Set<String> usedNames) {
|
||||
if (!visitedSet.add(iface.getQualifiedName().toString())) {
|
||||
return;
|
||||
}
|
||||
|
||||
iface.getEnclosedElements().stream()
|
||||
.filter(element -> (element.getKind() == ElementKind.METHOD) && !(element.getModifiers().contains(Modifier.STATIC)))
|
||||
.map(element -> ((ExecutableElement) element))
|
||||
.filter(element -> {
|
||||
if (element.isDefault()) {
|
||||
return element.getAnnotation(IgnoreDefaultMethod.class) == null;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.peek(element -> {
|
||||
if (!element.getParameters().isEmpty() || element.getReturnType().getKind() == TypeKind.VOID) {
|
||||
throw new IllegalInterface(String.format("Non-static, non-default methods must take no arguments and must return a value. Bad method: %s.%s()", iface.getSimpleName(), element.getSimpleName()));
|
||||
}
|
||||
if (!element.getTypeParameters().isEmpty()) {
|
||||
throw new IllegalInterface(String.format("Interface methods cannot have type parameters. Bad method: %s.%s()", iface.getSimpleName(), element.getSimpleName()));
|
||||
}
|
||||
})
|
||||
.filter(element -> usedNames.add(element.getSimpleName().toString()))
|
||||
.collect(Collectors.toCollection(() -> components));
|
||||
iface.getInterfaces().forEach(parentIface -> {
|
||||
TypeElement parentIfaceElement = (TypeElement) processingEnv.getTypeUtils().asElement(parentIface);
|
||||
getRecordComponents(parentIfaceElement, components, visitedSet, usedNames);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* 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.processor;
|
||||
|
||||
import io.soabase.recordbuilder.core.RecordBuilderMetaData;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class OptionBasedRecordBuilderMetaData implements RecordBuilderMetaData {
|
||||
/**
|
||||
* @see #suffix()
|
||||
*/
|
||||
public static final String OPTION_SUFFIX = "suffix";
|
||||
|
||||
/**
|
||||
* @see #interfaceSuffix()
|
||||
*/
|
||||
public static final String OPTION_INTERFACE_SUFFIX = "interfaceSuffix";
|
||||
|
||||
/**
|
||||
* @see #copyMethodName()
|
||||
*/
|
||||
public static final String OPTION_COPY_METHOD_NAME = "copyMethodName";
|
||||
|
||||
/**
|
||||
* @see #builderMethodName()
|
||||
*/
|
||||
public static final String OPTION_BUILDER_METHOD_NAME = "builderMethodName";
|
||||
|
||||
/**
|
||||
* @see #buildMethodName()
|
||||
*/
|
||||
public static final String OPTION_BUILD_METHOD_NAME = "buildMethodName";
|
||||
|
||||
/**
|
||||
* @see #componentsMethodName()
|
||||
*/
|
||||
public static final String OPTION_COMPONENTS_METHOD_NAME = "componentsMethodName";
|
||||
|
||||
/**
|
||||
* @see #fileComment()
|
||||
*/
|
||||
public static final String OPTION_FILE_COMMENT = "fileComment";
|
||||
|
||||
/**
|
||||
* @see #fileIndent()
|
||||
*/
|
||||
public static final String OPTION_FILE_INDENT = "fileIndent";
|
||||
|
||||
/**
|
||||
* @see #prefixEnclosingClassNames()
|
||||
*/
|
||||
public static final String OPTION_PREFIX_ENCLOSING_CLASS_NAMES = "prefixEnclosingClassNames";
|
||||
|
||||
/**
|
||||
* @see #withClassName()
|
||||
*/
|
||||
public static final String OPTION_WITH_CLASS_NAME = "withClassName";
|
||||
|
||||
/**
|
||||
* @see #withClassMethodPrefix()
|
||||
*/
|
||||
public static final String OPTION_WITH_CLASS_METHOD_PREFIX = "withClassMethodPrefix";
|
||||
|
||||
private final String suffix;
|
||||
private final String interfaceSuffix;
|
||||
private final String copyMethodName;
|
||||
private final String builderMethodName;
|
||||
private final String buildMethodName;
|
||||
private final String componentsMethodName;
|
||||
private final String withClassName;
|
||||
private final String withClassMethodPrefix;
|
||||
private final String fileComment;
|
||||
private final String fileIndent;
|
||||
private final boolean prefixEnclosingClassNames;
|
||||
|
||||
public OptionBasedRecordBuilderMetaData(Map<String, String> options) {
|
||||
suffix = options.getOrDefault(OPTION_SUFFIX, DEFAULT.suffix());
|
||||
interfaceSuffix = options.getOrDefault(OPTION_INTERFACE_SUFFIX, DEFAULT.interfaceSuffix());
|
||||
builderMethodName = options.getOrDefault(OPTION_BUILDER_METHOD_NAME, DEFAULT.builderMethodName());
|
||||
copyMethodName = options.getOrDefault(OPTION_COPY_METHOD_NAME, DEFAULT.copyMethodName());
|
||||
buildMethodName = options.getOrDefault(OPTION_BUILD_METHOD_NAME, DEFAULT.buildMethodName());
|
||||
componentsMethodName = options.getOrDefault(OPTION_COMPONENTS_METHOD_NAME, DEFAULT.componentsMethodName());
|
||||
withClassName = options.getOrDefault(OPTION_WITH_CLASS_NAME, DEFAULT.withClassName());
|
||||
withClassMethodPrefix = options.getOrDefault(OPTION_WITH_CLASS_METHOD_PREFIX, DEFAULT.withClassMethodPrefix());
|
||||
fileComment = options.getOrDefault(OPTION_FILE_COMMENT, DEFAULT.fileComment());
|
||||
fileIndent = options.getOrDefault(OPTION_FILE_INDENT, DEFAULT.fileIndent());
|
||||
String prefixenclosingclassnamesopt = options.getOrDefault(OPTION_PREFIX_ENCLOSING_CLASS_NAMES, String.valueOf(DEFAULT.prefixEnclosingClassNames()));
|
||||
if (prefixenclosingclassnamesopt == null) {
|
||||
prefixEnclosingClassNames = true;
|
||||
} else {
|
||||
prefixEnclosingClassNames = Boolean.parseBoolean(prefixenclosingclassnamesopt);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String suffix() {
|
||||
return suffix;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String copyMethodName() {
|
||||
return copyMethodName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String builderMethodName() {
|
||||
return builderMethodName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String buildMethodName() {
|
||||
return buildMethodName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String componentsMethodName() {
|
||||
return componentsMethodName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String withClassName() {
|
||||
return withClassName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String withClassMethodPrefix() {
|
||||
return withClassMethodPrefix;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String fileComment() {
|
||||
return fileComment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String fileIndent() {
|
||||
return fileIndent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean prefixEnclosingClassNames() {
|
||||
return prefixEnclosingClassNames;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String interfaceSuffix() {
|
||||
return interfaceSuffix;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright 2016 Jordan Zimmerman
|
||||
* 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.
|
||||
@@ -17,23 +17,34 @@ package io.soabase.recordbuilder.processor;
|
||||
|
||||
import io.soabase.recordbuilder.core.RecordBuilderMetaData;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javax.annotation.processing.ProcessingEnvironment;
|
||||
|
||||
class RecordBuilderMetaDataLoader {
|
||||
private final RecordBuilderMetaData metaData;
|
||||
|
||||
RecordBuilderMetaDataLoader(String metaDataClassName, Consumer<String> logger) {
|
||||
RecordBuilderMetaData localMetaData = null;
|
||||
RecordBuilderMetaDataLoader(ProcessingEnvironment processingEnv, Consumer<String> logger) {
|
||||
Map<String, String> options = processingEnv.getOptions();
|
||||
String metaDataClassName = options.get(RecordBuilderMetaData.JAVAC_OPTION_NAME);
|
||||
if ((metaDataClassName != null) && !metaDataClassName.isEmpty()) {
|
||||
RecordBuilderMetaData loadedMetaData = null;
|
||||
try {
|
||||
Class<?> clazz = Class.forName(metaDataClassName);
|
||||
localMetaData = (RecordBuilderMetaData) clazz.getDeclaredConstructor().newInstance();
|
||||
logger.accept("Found meta data: " + localMetaData.getClass());
|
||||
loadedMetaData = (RecordBuilderMetaData) clazz.getDeclaredConstructor().newInstance();
|
||||
logger.accept("Found meta data: " + clazz);
|
||||
} catch (InvocationTargetException e) {
|
||||
// log the thrown exception instead of the invocation target exception
|
||||
logger.accept("Could not load meta data: " + metaDataClassName + " - " + e.getCause());
|
||||
} catch (Exception e) {
|
||||
logger.accept("Could not load meta data: " + metaDataClassName + " - " + e.getMessage());
|
||||
logger.accept("Could not load meta data: " + metaDataClassName + " - " + e);
|
||||
}
|
||||
metaData = (loadedMetaData != null) ? loadedMetaData : RecordBuilderMetaData.DEFAULT;
|
||||
} else {
|
||||
metaData = new OptionBasedRecordBuilderMetaData(options);
|
||||
}
|
||||
metaData = (localMetaData != null) ? localMetaData : RecordBuilderMetaData.DEFAULT;
|
||||
}
|
||||
|
||||
RecordBuilderMetaData getMetaData() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright 2016 Jordan Zimmerman
|
||||
* 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.
|
||||
@@ -15,316 +15,91 @@
|
||||
*/
|
||||
package io.soabase.recordbuilder.processor;
|
||||
|
||||
import com.squareup.javapoet.*;
|
||||
import com.squareup.javapoet.AnnotationSpec;
|
||||
import com.squareup.javapoet.JavaFile;
|
||||
import com.squareup.javapoet.TypeSpec;
|
||||
import io.soabase.recordbuilder.core.RecordBuilderMetaData;
|
||||
import io.soabase.recordbuilder.core.RecordInterface;
|
||||
|
||||
import javax.annotation.processing.*;
|
||||
import javax.lang.model.SourceVersion;
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.element.ElementKind;
|
||||
import javax.lang.model.element.Modifier;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import javax.tools.Diagnostic;
|
||||
import javax.tools.JavaFileObject;
|
||||
import java.io.IOException;
|
||||
import java.io.Writer;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.function.Function;
|
||||
|
||||
@SupportedAnnotationTypes("io.soabase.recordbuilder.core.RecordBuilder")
|
||||
@SupportedSourceVersion(SourceVersion.RELEASE_14)
|
||||
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.RECORD_BUILDER;
|
||||
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.RECORD_INTERFACE;
|
||||
|
||||
@SupportedAnnotationTypes({RECORD_BUILDER, RECORD_INTERFACE})
|
||||
public class RecordBuilderProcessor extends AbstractProcessor {
|
||||
public static final String RECORD_BUILDER = "io.soabase.recordbuilder.core.RecordBuilder";
|
||||
public static final String RECORD_INTERFACE = "io.soabase.recordbuilder.core.RecordInterface";
|
||||
|
||||
static final AnnotationSpec generatedRecordBuilderAnnotation = AnnotationSpec.builder(Generated.class).addMember("value", "$S", RECORD_BUILDER).build();
|
||||
static final AnnotationSpec generatedRecordInterfaceAnnotation = AnnotationSpec.builder(Generated.class).addMember("value", "$S", RECORD_INTERFACE).build();
|
||||
|
||||
@Override
|
||||
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
|
||||
annotations.forEach(annotation -> roundEnv.getElementsAnnotatedWith(annotation).forEach(this::process));
|
||||
annotations.forEach(annotation -> roundEnv.getElementsAnnotatedWith(annotation)
|
||||
.forEach(element -> process(annotation, element))
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SourceVersion getSupportedSourceVersion() {
|
||||
// we don't directly return RELEASE_14 as that may
|
||||
// not exist in prior releases
|
||||
// if we're running on an older release, returning latest()
|
||||
// is fine as we won't encounter any records anyway
|
||||
return SourceVersion.latest();
|
||||
}
|
||||
|
||||
private void process(Element element) {
|
||||
var messager = processingEnv.getMessager();
|
||||
if (element.getKind() != ElementKind.RECORD) {
|
||||
messager.printMessage(Diagnostic.Kind.ERROR, "RecordBuilder only valid for records.", element);
|
||||
private void process(TypeElement annotation, Element element) {
|
||||
var metaData = new RecordBuilderMetaDataLoader(processingEnv, s -> processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, s)).getMetaData();
|
||||
|
||||
if (annotation.getQualifiedName().toString().equals(RECORD_BUILDER)) {
|
||||
processRecordBuilder((TypeElement) element, metaData);
|
||||
} else if (annotation.getQualifiedName().toString().equals(RECORD_INTERFACE)) {
|
||||
processRecordInterface((TypeElement) element, element.getAnnotation(RecordInterface.class), metaData);
|
||||
} else {
|
||||
throw new RuntimeException("Unknown annotation: " + annotation);
|
||||
}
|
||||
}
|
||||
|
||||
private void processRecordInterface(TypeElement element, RecordInterface recordInterface, RecordBuilderMetaData metaData) {
|
||||
if (!element.getKind().isInterface()) {
|
||||
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "RecordInterface only valid for interfaces.", element);
|
||||
return;
|
||||
}
|
||||
var metaData = new RecordBuilderMetaDataLoader(processingEnv.getOptions().get(RecordBuilderMetaData.JAVAC_OPTION_NAME), s -> messager.printMessage(Diagnostic.Kind.NOTE, s)).getMetaData();
|
||||
process((TypeElement) element, metaData);
|
||||
}
|
||||
|
||||
private void process(TypeElement record, RecordBuilderMetaData metaData) {
|
||||
var recordClassType = ElementUtils.getClassType(record, record.getTypeParameters());
|
||||
var packageName = ElementUtils.getPackageName(record);
|
||||
var builderClassType = ElementUtils.getClassType(packageName, getBuilderName(record, metaData, recordClassType), record.getTypeParameters());
|
||||
var typeVariables = record.getTypeParameters().stream().map(TypeVariableName::get).collect(Collectors.toList());
|
||||
var recordComponents = record.getRecordComponents().stream().map(ElementUtils::getClassType).collect(Collectors.toList());
|
||||
|
||||
var builder = TypeSpec.classBuilder(builderClassType.name()).addModifiers(Modifier.PUBLIC);
|
||||
builder.addTypeVariables(typeVariables);
|
||||
|
||||
addDefaultConstructor(builder);
|
||||
addAllArgsConstructor(builder, recordComponents);
|
||||
addStaticDefaultBuilderMethod(builder, builderClassType, typeVariables, metaData);
|
||||
addStaticCopyMethod(builder, builderClassType, recordClassType, recordComponents, typeVariables, metaData);
|
||||
addBuildMethod(builder, recordClassType, recordComponents, metaData);
|
||||
addToStringMethod(builder, builderClassType, recordComponents);
|
||||
addHashCodeMethod(builder, recordComponents);
|
||||
addEqualsMethod(builder, builderClassType, recordComponents);
|
||||
recordComponents.forEach(component -> {
|
||||
add1Field(builder, component);
|
||||
add1SetterMethod(builder, component, builderClassType);
|
||||
});
|
||||
|
||||
writeJavaFile(record, packageName, builderClassType, builder, metaData);
|
||||
}
|
||||
|
||||
private String getBuilderName(TypeElement record, RecordBuilderMetaData metaData, ClassType recordClassType) {
|
||||
// generate the record builder class name
|
||||
var baseName = recordClassType.name() + metaData.suffix();
|
||||
return metaData.prefixEnclosingClassNames() ? (getBuilderNamePrefix(record.getEnclosingElement()) + baseName) : baseName;
|
||||
}
|
||||
|
||||
private String getBuilderNamePrefix(Element element) {
|
||||
// prefix enclosing class names if this record is nested in a class
|
||||
if (element instanceof TypeElement) {
|
||||
return getBuilderNamePrefix(element.getEnclosingElement()) + element.getSimpleName().toString();
|
||||
var internalProcessor = new InternalRecordInterfaceProcessor(processingEnv, element, recordInterface, metaData);
|
||||
if (!internalProcessor.isValid()) {
|
||||
return;
|
||||
}
|
||||
return "";
|
||||
writeRecordInterfaceJavaFile(element, internalProcessor.packageName(), internalProcessor.recordClassType(), internalProcessor.recordType(), metaData, internalProcessor::toRecord);
|
||||
}
|
||||
|
||||
private void addDefaultConstructor(TypeSpec.Builder builder) {
|
||||
/*
|
||||
Adds a default constructor similar to:
|
||||
|
||||
private MyRecordBuilder() {
|
||||
}
|
||||
*/
|
||||
var constructorBuilder = MethodSpec.constructorBuilder().addModifiers(Modifier.PRIVATE);
|
||||
builder.addMethod(constructorBuilder.build());
|
||||
private void processRecordBuilder(TypeElement record, RecordBuilderMetaData metaData) {
|
||||
// we use string based name comparison for the element kind,
|
||||
// as the ElementKind.RECORD enum doesn't exist on JRE releases
|
||||
// older than Java 14, and we don't want to throw unexpected
|
||||
// NoSuchFieldErrors
|
||||
if (!"RECORD".equals(record.getKind().name())) {
|
||||
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "RecordBuilder only valid for records.", record);
|
||||
return;
|
||||
}
|
||||
var internalProcessor = new InternalRecordBuilderProcessor(record, metaData);
|
||||
writeRecordBuilderJavaFile(record, internalProcessor.packageName(), internalProcessor.builderClassType(), internalProcessor.builderType(), metaData);
|
||||
}
|
||||
|
||||
private void addAllArgsConstructor(TypeSpec.Builder builder, List<ClassType> recordComponents) {
|
||||
/*
|
||||
Adds an all-args constructor similar to:
|
||||
|
||||
private MyRecordBuilder(int p1, T p2, ...) {
|
||||
this.p1 = p1;
|
||||
this.p2 = p2;
|
||||
...
|
||||
}
|
||||
*/
|
||||
var constructorBuilder = MethodSpec.constructorBuilder().addModifiers(Modifier.PRIVATE);
|
||||
recordComponents.forEach(component -> {
|
||||
constructorBuilder.addParameter(component.typeName(), component.name());
|
||||
var codeBuilder = CodeBlock.builder().add("this.$L = $L", component.name(), component.name());
|
||||
constructorBuilder.addStatement(codeBuilder.build());
|
||||
});
|
||||
builder.addMethod(constructorBuilder.build());
|
||||
}
|
||||
|
||||
private void addToStringMethod(TypeSpec.Builder builder, ClassType builderClassType, List<ClassType> recordComponents) {
|
||||
/*
|
||||
add a toString() method similar to:
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "MyRecord[p1=blah, p2=blah]";
|
||||
}
|
||||
*/
|
||||
var codeBuilder = CodeBlock.builder().add("return \"$L[", builderClassType.name());
|
||||
IntStream.range(0, recordComponents.size()).forEach(index -> {
|
||||
if (index > 0) {
|
||||
codeBuilder.add(", ");
|
||||
}
|
||||
String name = recordComponents.get(index).name();
|
||||
codeBuilder.add("$L=\" + $L + \"", name, name);
|
||||
});
|
||||
codeBuilder.add("]\"");
|
||||
|
||||
var methodSpec = MethodSpec.methodBuilder("toString")
|
||||
.addModifiers(Modifier.PUBLIC)
|
||||
.addAnnotation(Override.class)
|
||||
.returns(String.class)
|
||||
.addStatement(codeBuilder.build())
|
||||
.build();
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void addHashCodeMethod(TypeSpec.Builder builder, List<ClassType> recordComponents) {
|
||||
/*
|
||||
add an hashCode() method similar to:
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(p1, p2);
|
||||
}
|
||||
*/
|
||||
var codeBuilder = CodeBlock.builder().add("return $T.hash(", Objects.class);
|
||||
IntStream.range(0, recordComponents.size()).forEach(index -> {
|
||||
if (index > 0) {
|
||||
codeBuilder.add(", ");
|
||||
}
|
||||
codeBuilder.add("$L", recordComponents.get(index).name());
|
||||
});
|
||||
codeBuilder.add(")");
|
||||
|
||||
var methodSpec = MethodSpec.methodBuilder("hashCode")
|
||||
.addModifiers(Modifier.PUBLIC)
|
||||
.addAnnotation(Override.class)
|
||||
.returns(TypeName.INT)
|
||||
.addStatement(codeBuilder.build())
|
||||
.build();
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void addEqualsMethod(TypeSpec.Builder builder, ClassType builderClassType, List<ClassType> recordComponents) {
|
||||
/*
|
||||
add an equals() method similar to:
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
return (o instanceof MyRecordBuilder b)
|
||||
&& Objects.equals(p1, b.p1)
|
||||
&& Objects.equals(p2, b.p2);
|
||||
}
|
||||
*/
|
||||
var codeBuilder = CodeBlock.builder();
|
||||
codeBuilder.add("return (this == o) || (");
|
||||
codeBuilder.add("(o instanceof $L b)", builderClassType.name());
|
||||
recordComponents.forEach(recordComponent -> {
|
||||
String name = recordComponent.name();
|
||||
if (recordComponent.typeName().isPrimitive()) {
|
||||
codeBuilder.add("\n&& ($L == b.$L)", name, name);
|
||||
} else {
|
||||
codeBuilder.add("\n&& $T.equals($L, b.$L)", Objects.class, name, name);
|
||||
}
|
||||
});
|
||||
codeBuilder.add(")");
|
||||
|
||||
var methodSpec = MethodSpec.methodBuilder("equals")
|
||||
.addParameter(Object.class, "o")
|
||||
.addModifiers(Modifier.PUBLIC)
|
||||
.addAnnotation(Override.class)
|
||||
.returns(TypeName.BOOLEAN)
|
||||
.addStatement(codeBuilder.build())
|
||||
.build();
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void addBuildMethod(TypeSpec.Builder builder, ClassType recordClassType, List<ClassType> recordComponents, RecordBuilderMetaData metaData) {
|
||||
/*
|
||||
Adds the build method that generates the record similar to:
|
||||
|
||||
public MyRecord build() {
|
||||
return new MyRecord(p1, p2, ...);
|
||||
}
|
||||
*/
|
||||
var codeBuilder = CodeBlock.builder().add("return new $T(", recordClassType.typeName());
|
||||
IntStream.range(0, recordComponents.size()).forEach(index -> {
|
||||
if (index > 0) {
|
||||
codeBuilder.add(", ");
|
||||
}
|
||||
codeBuilder.add("$L", recordComponents.get(index).name());
|
||||
});
|
||||
codeBuilder.add(")");
|
||||
|
||||
var methodSpec = MethodSpec.methodBuilder(metaData.buildMethodName())
|
||||
.addJavadoc("Return a new record instance with all fields set to the current values in this builder\n")
|
||||
.addModifiers(Modifier.PUBLIC)
|
||||
.returns(recordClassType.typeName())
|
||||
.addStatement(codeBuilder.build())
|
||||
.build();
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void addStaticCopyMethod(TypeSpec.Builder builder, ClassType builderClassType, ClassType recordClassType, List<ClassType> recordComponents, List<TypeVariableName> typeVariables, RecordBuilderMetaData metaData) {
|
||||
/*
|
||||
Adds a copy builder method that pre-fills the builder with existing values similar to:
|
||||
|
||||
public static MyRecordBuilder builder(MyRecord from) {
|
||||
return new MyRecordBuilder(from.p1(), from.p2(), ...);
|
||||
}
|
||||
*/
|
||||
var codeBuilder = CodeBlock.builder().add("return new $T(", builderClassType.typeName());
|
||||
IntStream.range(0, recordComponents.size()).forEach(index -> {
|
||||
if (index > 0) {
|
||||
codeBuilder.add(", ");
|
||||
}
|
||||
codeBuilder.add("from.$L()", recordComponents.get(index).name());
|
||||
});
|
||||
codeBuilder.add(")");
|
||||
|
||||
var methodSpec = MethodSpec.methodBuilder(metaData.copyMethodName())
|
||||
.addJavadoc("Return a new builder with all fields set to the values taken from the given record instance\n")
|
||||
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
|
||||
.addTypeVariables(typeVariables)
|
||||
.addParameter(recordClassType.typeName(), "from")
|
||||
.returns(builderClassType.typeName())
|
||||
.addStatement(codeBuilder.build())
|
||||
.build();
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void addStaticDefaultBuilderMethod(TypeSpec.Builder builder, ClassType builderClassType, List<TypeVariableName> typeVariables, RecordBuilderMetaData metaData) {
|
||||
/*
|
||||
Adds a the default builder method similar to:
|
||||
|
||||
public static MyRecordBuilder builder() {
|
||||
return new MyRecordBuilder();
|
||||
}
|
||||
*/
|
||||
var methodSpec = MethodSpec.methodBuilder(metaData.builderMethodName())
|
||||
.addJavadoc("Return a new builder with all fields set to default Java values\n")
|
||||
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
|
||||
.addTypeVariables(typeVariables)
|
||||
.returns(builderClassType.typeName())
|
||||
.addStatement("return new $T()", builderClassType.typeName())
|
||||
.build();
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void add1Field(TypeSpec.Builder builder, ClassType component) {
|
||||
/*
|
||||
For a single record component, add a field similar to:
|
||||
|
||||
private T p;
|
||||
*/
|
||||
var fieldSpec = FieldSpec.builder(component.typeName(), component.name(), Modifier.PRIVATE).build();
|
||||
builder.addField(fieldSpec);
|
||||
}
|
||||
|
||||
private void add1SetterMethod(TypeSpec.Builder builder, ClassType component, ClassType builderClassType) {
|
||||
/*
|
||||
For a single record component, add a setter similar to:
|
||||
|
||||
public MyRecordBuilder p(T p) {
|
||||
this.p = p;
|
||||
return this;
|
||||
}
|
||||
*/
|
||||
var parameterSpec = ParameterSpec.builder(component.typeName(), component.name()).build();
|
||||
var methodSpec = MethodSpec.methodBuilder(component.name())
|
||||
.addJavadoc("Set a new value for this record component in the builder\n")
|
||||
.addModifiers(Modifier.PUBLIC)
|
||||
.addParameter(parameterSpec)
|
||||
.returns(builderClassType.typeName())
|
||||
.addStatement("this.$L = $L", component.name(), component.name())
|
||||
.addStatement("return this")
|
||||
.build();
|
||||
builder.addMethod(methodSpec);
|
||||
}
|
||||
|
||||
private void writeJavaFile(TypeElement record, String packageName, ClassType builderClassType, TypeSpec.Builder builder, RecordBuilderMetaData metaData) {
|
||||
private void writeRecordBuilderJavaFile(TypeElement record, String packageName, ClassType builderClassType, TypeSpec builderType, RecordBuilderMetaData metaData) {
|
||||
// produces the Java file
|
||||
var javaFileBuilder = JavaFile.builder(packageName, builder.build())
|
||||
.indent(metaData.fileIndent());
|
||||
var comment = metaData.fileComment();
|
||||
if ((comment != null) && !comment.isEmpty()) {
|
||||
javaFileBuilder.addFileComment(comment);
|
||||
}
|
||||
JavaFile javaFile = javaFileBuilder.build();
|
||||
JavaFile javaFile = javaFileBuilder(packageName, builderType, metaData);
|
||||
Filer filer = processingEnv.getFiler();
|
||||
try
|
||||
{
|
||||
@@ -337,12 +112,49 @@ public class RecordBuilderProcessor extends AbstractProcessor {
|
||||
}
|
||||
catch ( IOException e )
|
||||
{
|
||||
String message = "Could not create source file";
|
||||
if ( e.getMessage() != null )
|
||||
{
|
||||
message = message + ": " + e.getMessage();
|
||||
}
|
||||
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, message, record);
|
||||
handleWriteError(record, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeRecordInterfaceJavaFile(TypeElement element, String packageName, ClassType classType, TypeSpec type, RecordBuilderMetaData metaData, Function<String, String> toRecordProc) {
|
||||
JavaFile javaFile = javaFileBuilder(packageName, type, metaData);
|
||||
|
||||
String classSourceCode = javaFile.toString();
|
||||
int generatedIndex = classSourceCode.indexOf("@Generated");
|
||||
String recordSourceCode = toRecordProc.apply(classSourceCode);
|
||||
|
||||
Filer filer = processingEnv.getFiler();
|
||||
try
|
||||
{
|
||||
String fullyQualifiedName = packageName.isEmpty() ? classType.name() : (packageName + "." + classType.name());
|
||||
JavaFileObject sourceFile = filer.createSourceFile(fullyQualifiedName);
|
||||
try ( Writer writer = sourceFile.openWriter() )
|
||||
{
|
||||
writer.write(recordSourceCode);
|
||||
}
|
||||
}
|
||||
catch ( IOException e )
|
||||
{
|
||||
handleWriteError(element, e);
|
||||
}
|
||||
}
|
||||
|
||||
private JavaFile javaFileBuilder(String packageName, TypeSpec type, RecordBuilderMetaData metaData) {
|
||||
var javaFileBuilder = JavaFile.builder(packageName, type)
|
||||
.skipJavaLangImports(true)
|
||||
.indent(metaData.fileIndent());
|
||||
var comment = metaData.fileComment();
|
||||
if ((comment != null) && !comment.isEmpty()) {
|
||||
javaFileBuilder.addFileComment(comment);
|
||||
}
|
||||
return javaFileBuilder.build();
|
||||
}
|
||||
|
||||
private void handleWriteError(TypeElement element, IOException e) {
|
||||
String message = "Could not create source file";
|
||||
if (e.getMessage() != null) {
|
||||
message = message + ": " + e.getMessage();
|
||||
}
|
||||
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, message, element);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder</artifactId>
|
||||
<version>1.4.ea-SNAPSHOT</version>
|
||||
<version>1.9.ea</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -24,6 +24,11 @@
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 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.IgnoreDefaultMethod;
|
||||
import io.soabase.recordbuilder.core.RecordInterface;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@RecordInterface
|
||||
public interface HasDefaults {
|
||||
Instant time();
|
||||
|
||||
default Instant tomorrow() {
|
||||
return Instant.now().plusMillis(TimeUnit.DAYS.toMillis(1));
|
||||
}
|
||||
|
||||
@IgnoreDefaultMethod
|
||||
default void complexMethod(String s1, String s2) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
@@ -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 io.soabase.recordbuilder.core.RecordInterface;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
|
||||
@RecordInterface(addRecordBuilder = false)
|
||||
public interface NoBuilder {
|
||||
BigInteger big();
|
||||
|
||||
BigDecimal decimal();
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* 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.RecordInterface;
|
||||
|
||||
@RecordInterface
|
||||
public interface Person {
|
||||
String name();
|
||||
|
||||
int age();
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright 2016 Jordan Zimmerman
|
||||
* 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.
|
||||
@@ -18,5 +18,5 @@ package io.soabase.recordbuilder.test;
|
||||
import io.soabase.recordbuilder.core.RecordBuilder;
|
||||
|
||||
@RecordBuilder
|
||||
public record SimpleGenericRecord<T>(int i, T s) {
|
||||
public record SimpleGenericRecord<T>(int i, T s) implements SimpleGenericRecordBuilder.With<T> {
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright 2016 Jordan Zimmerman
|
||||
* 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.
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 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.RecordInterface;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@RecordInterface
|
||||
public interface SpecializedPerson<T, U> extends Person {
|
||||
List<T> features();
|
||||
|
||||
Map<Supplier<T>, Function<U, Function<T, U>>> complex();
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright 2016 Jordan Zimmerman
|
||||
* 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.
|
||||
@@ -15,13 +15,20 @@
|
||||
*/
|
||||
package io.soabase.recordbuilder.test;
|
||||
|
||||
import io.soabase.recordbuilder.test.SimpleRecordBuilder;
|
||||
|
||||
public class Usage {
|
||||
public static void main(String[] args) {
|
||||
var hey = SimpleRecordBuilder.builder().i(10).s("hey").build();
|
||||
System.out.println(hey);
|
||||
var hey2 = SimpleRecordBuilder.builder(hey).i(100).build();
|
||||
System.out.println(hey2);
|
||||
|
||||
var person = new PersonRecord("me", 42);
|
||||
outputPerson(person);
|
||||
var aged = PersonRecordBuilder.builder(person).age(100).build();
|
||||
outputPerson(aged);
|
||||
}
|
||||
|
||||
private static void outputPerson(Person p) {
|
||||
System.out.println(p.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
class TestWithers {
|
||||
@Test
|
||||
void testWithers() {
|
||||
var r1 = new SimpleGenericRecord<>(10, List.of("1", "2", "3"));
|
||||
var r2 = r1.withS(List.of("4", "5"));
|
||||
var r3 = r2.withI(20);
|
||||
Assertions.assertEquals(10, r1.i());
|
||||
Assertions.assertEquals(List.of("1", "2", "3"), r1.s());
|
||||
Assertions.assertEquals(10, r2.i());
|
||||
Assertions.assertEquals(List.of("4", "5"), r2.s());
|
||||
Assertions.assertEquals(20, r3.i());
|
||||
Assertions.assertEquals(List.of("4", "5"), r2.s());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWitherBuilder() {
|
||||
var r1 = new SimpleGenericRecord<>(10, "ten");
|
||||
var r2 = r1.with().i(20).s("twenty").build();
|
||||
var r3 = r2.with().s("changed");
|
||||
Assertions.assertEquals(10, r1.i());
|
||||
Assertions.assertEquals("ten", r1.s());
|
||||
Assertions.assertEquals(20, r2.i());
|
||||
Assertions.assertEquals("twenty", r2.s());
|
||||
Assertions.assertEquals(20, r3.i());
|
||||
Assertions.assertEquals("changed", r3.s());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWitherBuilderConsumer() {
|
||||
var r1 = new SimpleGenericRecord<>(10, "ten");
|
||||
var r2 = r1.with(r -> r.i(15));
|
||||
var r3 = r1.with(r -> r.s("twenty").i(20));
|
||||
Assertions.assertEquals(10, r1.i());
|
||||
Assertions.assertEquals("ten", r1.s());
|
||||
Assertions.assertEquals(15, r2.i());
|
||||
Assertions.assertEquals("ten", r2.s());
|
||||
Assertions.assertEquals(20, r3.i());
|
||||
Assertions.assertEquals("twenty", r3.s());
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright 2016 Jordan Zimmerman
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user