Compare commits
18 Commits
jordanz/en
...
record-bui
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0abbf3ecd0 | ||
|
|
1e8a88b81e | ||
|
|
b53392f910 | ||
|
|
00bb0940bc | ||
|
|
e0abb07481 | ||
|
|
8845622540 | ||
|
|
f8e56a40f3 | ||
|
|
5a07cf23f7 | ||
|
|
78ba53f96d | ||
|
|
0153f65121 | ||
|
|
c1e771bbbe | ||
|
|
c9ba514773 | ||
|
|
491ed4f6e0 | ||
|
|
16751508cc | ||
|
|
ee7f81c7b8 | ||
|
|
015287608b | ||
|
|
467bcc9041 | ||
|
|
666bc334ad |
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: Randgalt
|
||||
24
.github/workflows/maven.yml
vendored
Normal file
24
.github/workflows/maven.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# 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: Java CI with Maven
|
||||
|
||||
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: 16.0.0-ea
|
||||
- name: Build with Maven
|
||||
run: mvn -B package --file pom.xml
|
||||
@@ -1 +0,0 @@
|
||||
--enable-preview
|
||||
@@ -1,3 +0,0 @@
|
||||
language: java
|
||||
jdk:
|
||||
- openjdk15
|
||||
51
README.md
51
README.md
@@ -1,13 +1,13 @@
|
||||
[](https://travis-ci.org/Randgalt/record-builder)
|
||||
[](https://github.com/Randgalt/record-builder/actions)
|
||||
[](https://search.maven.org/search?q=g:io.soabase.record-builder%20a:record-builder)
|
||||
|
||||
# RecordBuilder - Early Access
|
||||
# RecordBuilder
|
||||
|
||||
## What is RecordBuilder
|
||||
|
||||
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:
|
||||
Java 16 introduces [Records](https://openjdk.java.net/jeps/395). While this version of records is fantastic,
|
||||
it's currently missing some important features normally found in 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
|
||||
@@ -203,8 +203,7 @@ public class NameAndAgeBuilder {
|
||||
* Return a new record built from the builder passed to the given consumer
|
||||
*/
|
||||
default NameAndAge with(Consumer<NameAndAgeBuilder> consumer) {
|
||||
NameAndAge r = _downcast(this);
|
||||
NameAndAgeBuilder builder = NameAndAgeBuilder.builder(r);
|
||||
NameAndAgeBuilder builder = with();
|
||||
consumer.accept(builder);
|
||||
return builder.build();
|
||||
}
|
||||
@@ -257,6 +256,8 @@ Notes:
|
||||
- ...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)`
|
||||
- If your interface is a JavaBean (e.g. `getThing()`, `isThing()`) the "get" and "is" prefixes are
|
||||
stripped and forwarding methods are added.
|
||||
|
||||
## Generation Via Includes
|
||||
|
||||
@@ -287,18 +288,19 @@ annotation. Use `packagePattern` to change this (see Javadoc for details).
|
||||
|
||||
### Maven
|
||||
|
||||
1\. Add the dependency that contains the `@RecordBuilder` annotation.
|
||||
1) Add the dependency that contains the `@RecordBuilder` annotation.
|
||||
|
||||
```
|
||||
<dependency>
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder-core</artifactId>
|
||||
<version>set-version-here</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
```
|
||||
|
||||
2\. Enable the annotation processing for the Maven Compiler Plugin:
|
||||
2) Enable the annotation processing for the Maven Compiler Plugin:
|
||||
|
||||
```
|
||||
<plugin>
|
||||
@@ -318,21 +320,11 @@ annotation. Use `packagePattern` to change this (see Javadoc for details).
|
||||
</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>
|
||||
```
|
||||
|
||||
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:
|
||||
@@ -340,16 +332,7 @@ 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"
|
||||
compileOnly 'io.soabase.record-builder:record-builder-core:$version-goes-here'
|
||||
}
|
||||
```
|
||||
|
||||
@@ -357,16 +340,6 @@ tasks.withType(Test) {
|
||||
|
||||
Depending on your IDE you are likely to need to enable Annotation Processing in your IDE settings.
|
||||
|
||||
## Enable Preview
|
||||
|
||||
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 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`.
|
||||
|
||||
## Customizing
|
||||
|
||||
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:
|
||||
|
||||
12
pom.xml
12
pom.xml
@@ -5,7 +5,7 @@
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<version>1.14.ea-SNAPSHOT</version>
|
||||
<version>1.16</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>15</jdk-version>
|
||||
<jdk-version>16</jdk-version>
|
||||
|
||||
<maven-compiler-plugin-version>3.8.1</maven-compiler-plugin-version>
|
||||
<maven-source-plugin-version>3.2.0</maven-source-plugin-version>
|
||||
@@ -71,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.16</tag>
|
||||
</scm>
|
||||
|
||||
<issueManagement>
|
||||
@@ -123,9 +123,6 @@
|
||||
<version>${maven-compiler-plugin-version}</version>
|
||||
<configuration>
|
||||
<release>${jdk-version}</release>
|
||||
<compilerArgs>
|
||||
<arg>--enable-preview</arg>
|
||||
</compilerArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
@@ -277,9 +274,6 @@
|
||||
<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.14.ea-SNAPSHOT</version>
|
||||
<version>1.16</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder</artifactId>
|
||||
<version>1.14.ea-SNAPSHOT</version>
|
||||
<version>1.16</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
||||
@@ -134,15 +134,13 @@ class InternalRecordBuilderProcessor
|
||||
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);
|
||||
MyRecordBuilder builder = with();
|
||||
consumer.accept(builder);
|
||||
return builder.build();
|
||||
}
|
||||
*/
|
||||
var codeBlockBuilder = CodeBlock.builder()
|
||||
.add("$T $L = $L(this);\n", recordClassType.typeName(), uniqueVarName, metaData.downCastMethodName())
|
||||
.add("$T builder = $L.$L($L);\n", builderClassType.typeName(), builderClassType.name(), metaData.copyMethodName(), uniqueVarName)
|
||||
.add("$T builder = with();\n", builderClassType.typeName())
|
||||
.add("consumer.accept(builder);\n")
|
||||
.add("return builder.build();\n");
|
||||
var consumerType = ParameterizedTypeName.get(ClassName.get(Consumer.class), builderClassType.typeName());
|
||||
@@ -164,7 +162,7 @@ class InternalRecordBuilderProcessor
|
||||
Adds a method that returns a pre-filled copy builder similar to:
|
||||
|
||||
default MyRecordBuilder with() {
|
||||
MyRecord r = (MyRecord)(Object)this;
|
||||
MyRecord r = _downcast(this);
|
||||
return MyRecordBuilder.builder(r);
|
||||
}
|
||||
*/
|
||||
@@ -201,7 +199,7 @@ class InternalRecordBuilderProcessor
|
||||
Adds a with method for the component similar to:
|
||||
|
||||
default MyRecord withName(String name) {
|
||||
MyRecord r = (MyRecord)(Object)this;
|
||||
MyRecord r = _downcast(this);
|
||||
return new MyRecord(name, r.age());
|
||||
}
|
||||
*/
|
||||
@@ -529,7 +527,7 @@ class InternalRecordBuilderProcessor
|
||||
.add("}\n")
|
||||
.add("catch (ClassCastException dummy) {\n")
|
||||
.indent()
|
||||
.add("throw new RuntimeException($S);\n", builderClassType.name() + "." + metaData.withClassName() + " can only be implemented for " + recordClassType.name())
|
||||
.add("throw new RuntimeException($S);\n", builderClassType.name() + "." + metaData.withClassName() + " can only be implemented by " + recordClassType.name())
|
||||
.unindent()
|
||||
.add("}");
|
||||
var methodSpec = MethodSpec.methodBuilder(metaData.downCastMethodName())
|
||||
|
||||
@@ -48,12 +48,17 @@ class InternalRecordInterfaceProcessor {
|
||||
private final ProcessingEnvironment processingEnv;
|
||||
private final String packageName;
|
||||
private final TypeSpec recordType;
|
||||
private final List<ExecutableElement> recordComponents;
|
||||
private final List<Component> recordComponents;
|
||||
private final TypeElement iface;
|
||||
private final ClassType recordClassType;
|
||||
private final List<String> alternateMethods;
|
||||
|
||||
private static final String FAKE_METHOD_NAME = "__FAKE__";
|
||||
|
||||
private static final Set<String> javaBeanPrefixes = Set.of("get", "is");
|
||||
|
||||
private record Component(ExecutableElement element, Optional<String> alternateName){}
|
||||
|
||||
InternalRecordInterfaceProcessor(ProcessingEnvironment processingEnv, TypeElement iface, boolean addRecordBuilder, RecordBuilderMetaData metaData, Optional<String> packageNameOpt) {
|
||||
this.processingEnv = processingEnv;
|
||||
packageName = packageNameOpt.orElseGet(() -> ElementUtils.getPackageName(iface));
|
||||
@@ -79,6 +84,8 @@ class InternalRecordInterfaceProcessor {
|
||||
builder.addSuperinterface(builderClassType.typeName());
|
||||
}
|
||||
|
||||
alternateMethods = buildAlternateMethods(recordComponents);
|
||||
|
||||
recordType = builder.build();
|
||||
}
|
||||
|
||||
@@ -126,22 +133,43 @@ class InternalRecordInterfaceProcessor {
|
||||
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 + " {}";
|
||||
|
||||
StringBuilder fixedRecord = new StringBuilder(declaration).append(argumentList).append(' ').append(implementsSection).append(" {");
|
||||
alternateMethods.forEach(method -> fixedRecord.append('\n').append(method));
|
||||
fixedRecord.append('}');
|
||||
return fixedRecord.toString();
|
||||
}
|
||||
|
||||
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()));
|
||||
recordComponents.forEach(component -> {
|
||||
String name = component.alternateName.orElseGet(() -> component.element.getSimpleName().toString());
|
||||
ParameterSpec parameterSpec = ParameterSpec.builder(ClassName.get(component.element.getReturnType()), name).build();
|
||||
builder.addTypeVariables(component.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<>();
|
||||
private List<String> buildAlternateMethods(List<Component> recordComponents) {
|
||||
return recordComponents.stream()
|
||||
.filter(component -> component.alternateName.isPresent())
|
||||
.map(component -> {
|
||||
var method = MethodSpec.methodBuilder(component.element.getSimpleName().toString())
|
||||
.addAnnotation(Override.class)
|
||||
.addAnnotation(generatedRecordInterfaceAnnotation)
|
||||
.returns(ClassName.get(component.element.getReturnType()))
|
||||
.addModifiers(Modifier.PUBLIC)
|
||||
.addCode("return $L();", component.alternateName.get())
|
||||
.build();
|
||||
return method.toString();
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private List<Component> getRecordComponents(TypeElement iface) {
|
||||
List<Component> components = new ArrayList<>();
|
||||
try {
|
||||
getRecordComponents(iface, components, new HashSet<>(), new HashSet<>());
|
||||
if (components.isEmpty()) {
|
||||
@@ -153,15 +181,15 @@ class InternalRecordInterfaceProcessor {
|
||||
}
|
||||
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) {
|
||||
private void getRecordComponents(TypeElement iface, Collection<Component> components, Set<String> visitedSet, Set<String> usedNames) {
|
||||
if (!visitedSet.add(iface.getQualifiedName().toString())) {
|
||||
return;
|
||||
}
|
||||
@@ -184,10 +212,22 @@ class InternalRecordInterfaceProcessor {
|
||||
}
|
||||
})
|
||||
.filter(element -> usedNames.add(element.getSimpleName().toString()))
|
||||
.map(element -> new Component(element, stripBeanPrefix(element.getSimpleName().toString())))
|
||||
.collect(Collectors.toCollection(() -> components));
|
||||
iface.getInterfaces().forEach(parentIface -> {
|
||||
TypeElement parentIfaceElement = (TypeElement) processingEnv.getTypeUtils().asElement(parentIface);
|
||||
getRecordComponents(parentIfaceElement, components, visitedSet, usedNames);
|
||||
});
|
||||
}
|
||||
|
||||
private Optional<String> stripBeanPrefix(String name)
|
||||
{
|
||||
return javaBeanPrefixes.stream()
|
||||
.filter(prefix -> name.startsWith(prefix) && (name.length() > prefix.length()))
|
||||
.findFirst()
|
||||
.map(prefix -> {
|
||||
var stripped = name.substring(prefix.length());
|
||||
return Character.toLowerCase(stripped.charAt(0)) + stripped.substring(1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder</artifactId>
|
||||
<version>1.14.ea-SNAPSHOT</version>
|
||||
<version>1.16</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<dependency>
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder-core</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -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.time.Instant;
|
||||
|
||||
@RecordInterface
|
||||
public interface BeanStyle {
|
||||
String getName();
|
||||
|
||||
Instant getDate();
|
||||
|
||||
boolean isSomething();
|
||||
}
|
||||
Reference in New Issue
Block a user