Compare commits

...

16 Commits

Author SHA1 Message Date
Jordan Zimmerman
1dd00b2c65 [maven-release-plugin] prepare release record-builder-31 2022-01-25 11:06:01 +00:00
Jordan Zimmerman
3b34b5dee3 [maven-release-plugin] prepare for next development iteration 2022-01-25 10:59:50 +00:00
Jordan Zimmerman
7248bad2bd [maven-release-plugin] prepare release record-builder-30 2022-01-25 10:59:45 +00:00
Jordan Zimmerman
7e494d8753 Optional functional methods for With
When enabled, some functional methods are added to the `With` nested
class.

E.g.

```java
@RecordBuilder
record MyRecord<T>(String name, T value, int qty) implements MyRecordBuilder.With<T> {}

...

MyRecord<Thing> r = ...

var other = r.map((name, value, qty) -> new Other(...));
```
2022-01-22 09:17:15 +00:00
Madis Pärn
3954499d4b Allow components stream method to work with null fields (#85)
Co-authored-by: Madis Parn <madis.parn@topia.com>
2022-01-22 07:56:39 +00:00
Jordan Zimmerman
13959dee2a [maven-release-plugin] prepare for next development iteration 2021-11-03 08:51:54 +00:00
Jordan Zimmerman
af759c0570 [maven-release-plugin] prepare release record-builder-29 2021-11-03 08:51:47 +00:00
Jordan Zimmerman
9a7d73e78c Support options on includes 2021-11-03 08:44:51 +00:00
Jordan Zimmerman
3b8c3ff9e3 Remove Java 15 support
Java 15 has been EOL for a while now. No need to continue supporting
it.

Closes #78
2021-10-21 19:00:46 +01:00
Jordan Zimmerman
9943667af1 Add support for static from() that returns a Wither
Useful for when you can't add the With interface to your record.
Static method takes a record as an argument and returns a With
instance.
2021-10-19 20:24:10 +01:00
Stefan Kuhn
b0c8f10711 fix: build 2021-10-19 19:42:04 +01:00
Stefan Kuhn
0d3c2f37c1 chore: update test to use annotation processor discovery 2021-10-19 19:42:04 +01:00
Stefan Kuhn
eabcb2f179 doc: update maven usage, so that the default annotation processors discovery process is not disabled 2021-10-19 19:42:04 +01:00
Jordan Zimmerman
5fef81191d [maven-release-plugin] prepare for next development iteration 2021-10-07 10:14:58 +01:00
Jordan Zimmerman
8dbec027e4 [maven-release-plugin] prepare release record-builder-28 2021-10-07 10:14:53 +01:00
Jordan Zimmerman
ef09d68b78 [maven-release-plugin] prepare for next development iteration 2021-10-07 10:11:59 +01:00
19 changed files with 407 additions and 357 deletions

View File

@@ -1,28 +0,0 @@
# This workflow will build a Java project with Maven
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven
name: Maven Build - Java 15
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK
uses: actions/setup-java@v1
with:
java-version: 15
- name: Create Maven Directory
run: mkdir -p .mvn/
- name: Create Maven JVM file
run: echo "--enable-preview" > .mvn/jvm.config
- name: Build with Maven
run: mvn -P java15 -B package --file pom.xml

355
README.md
View File

@@ -22,7 +22,6 @@ _Details:_
- [Generation Via Includes](#generation-via-includes)
- [Usage](#usage)
- [Customizing](customizing.md) (e.g. add immutable collections, etc.)
- [Java 15 Versions](#java-15-versions)
## RecordBuilder Example
@@ -80,6 +79,10 @@ NameAndAge r5 = r4.with(b -> {
b.name("whatever"));
}
});
// or, if you cannot add the "With" interface to your record...
NameAndAge r6 = NameAndAgeBuilder.from(r5).with(b -> b.age(200).name("whatever"));
NameAndAge r7 = NameAndAgeBuilder.from(r5).withName("boop");
```
_Hat tip to [Benji Weber](https://benjiweber.co.uk/blog/2020/09/19/fun-with-java-records/) for the Withers idea._
@@ -92,145 +95,162 @@ The full builder class is defined as:
```java
public class NameAndAgeBuilder {
private String name;
private String name;
private int age;
private int age;
private NameAndAgeBuilder() {
}
private NameAndAgeBuilder() {
}
private NameAndAgeBuilder(String name, int age) {
this.name = name;
this.age = age;
}
private NameAndAgeBuilder(String name, int age) {
this.name = name;
this.age = age;
}
/**
* Static constructor/builder. Can be used instead of new NameAndAge(...)
*/
public static NameAndAge NameAndAge(String name, int age) {
return new NameAndAge(name, age);
}
/**
* Static constructor/builder. Can be used instead of new NameAndAge(...)
*/
public static NameAndAge NameAndAge(String name, int age) {
return new NameAndAge(name, age);
}
/**
* Return a new builder with all fields set to default Java values
*/
public static NameAndAgeBuilder builder() {
return new NameAndAgeBuilder();
}
/**
* Return a new builder with all fields set to default Java values
*/
public static NameAndAgeBuilder builder() {
return new NameAndAgeBuilder();
}
/**
* Return a new builder with all fields set to the values taken from the given record instance
*/
public static NameAndAgeBuilder builder(NameAndAge from) {
return new NameAndAgeBuilder(from.name(), from.age());
}
/**
* Return a new builder with all fields set to the values taken from the given record instance
*/
public static NameAndAgeBuilder builder(NameAndAge from) {
return new NameAndAgeBuilder(from.name(), from.age());
}
/**
* Return a new record instance with all fields set to the current values in this builder
*/
public NameAndAge build() {
return new NameAndAge(name, age);
}
/**
* Return a "with"er for an existing record instance
*/
public static NameAndAgeBuilder.With from(NameAndAge from) {
return new NameAndAgeBuilder.With() {
@Override
public String name() {
return from.name();
}
/**
* Set a new value for the {@code name} record component in the builder
*/
public NameAndAgeBuilder name(String name) {
this.name = name;
return this;
}
@Override
public int age() {
return from.age();
}
};
}
/**
* Return a stream of the record components as map entries keyed with the component name and the value as the component value
*/
public static Stream<Map.Entry<String, Object>> stream(NameAndAge record) {
return Stream.of(Map.entry("name", record.name()),
Map.entry("age", record.age()));
}
/**
* Return a new record instance with all fields set to the current values in this builder
*/
public NameAndAge build() {
return new NameAndAge(name, age);
}
@Override
public String toString() {
return "NameAndAgeBuilder[name=" + name + ", age=" + age + "]";
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public boolean equals(Object o) {
return (this == o) || ((o instanceof NameAndAgeBuilder r)
&& Objects.equals(name, r.name)
&& (age == r.age));
}
/**
* Set a new value for the {@code name} record component in the builder
*/
public NameAndAgeBuilder name(String name) {
this.name = name;
return this;
}
/**
* Return the current value for the {@code name} record component in the builder
*/
public String name() {
return name;
}
/**
* Set a new value for the {@code age} record component in the builder
*/
public NameAndAgeBuilder age(int age) {
this.age = age;
return this;
}
/**
* Return the current value for the {@code age} record component in the builder
*/
public int age() {
return age;
}
/**
* Add withers to {@code NameAndAge}
*/
public interface With {
/**
* Return the current value for the {@code name} record component in the builder
*/
public String name() {
return name;
}
/**
* Set a new value for the {@code age} record component in the builder
*/
public NameAndAgeBuilder age(int age) {
this.age = age;
return this;
}
String name();
/**
* Return the current value for the {@code age} record component in the builder
*/
public int age() {
return age;
int age();
/**
* Return a new record builder using the current values
*/
default NameAndAgeBuilder with() {
return new NameAndAgeBuilder(name(), age());
}
/**
* Return a stream of the record components as map entries keyed with the component name and the value as the component value
* Return a new record built from the builder passed to the given consumer
*/
public static Stream<Map.Entry<String, Object>> stream(NameAndAge record) {
return Stream.of(Map.entry("name", record.name()),
Map.entry("age", record.age()));
default NameAndAge with(Consumer<NameAndAgeBuilder> consumer) {
NameAndAgeBuilder builder = with();
consumer.accept(builder);
return builder.build();
}
@Override
public String toString() {
return "NameAndAgeBuilder[name=" + name + ", age=" + age + "]";
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public boolean equals(Object o) {
return (this == o) || ((o instanceof NameAndAgeBuilder b)
&& Objects.equals(name, b.name)
&& (age == b.age));
}
/**
* Add withers to {@code NameAndAge}
* Return a new instance of {@code NameAndAge} with a new value for {@code name}
*/
public interface With {
/**
* Return the current value for the {@code name} record component in the builder
*/
String name();
/**
* Return the current value for the {@code age} record component in the builder
*/
int age();
/**
* Return a new record builder using the current values
*/
default NameAndAgeBuilder with() {
return new NameAndAgeBuilder(name(), age());
}
/**
* Return a new record built from the builder passed to the given consumer
*/
default NameAndAge with(Consumer<NameAndAgeBuilder> consumer) {
NameAndAgeBuilder builder = with();
consumer.accept(builder);
return builder.build();
}
/**
* Return a new instance of {@code NameAndAge} with a new value for {@code name}
*/
default NameAndAge withName(String name) {
return new NameAndAge(name, age());
}
/**
* Return a new instance of {@code NameAndAge} with a new value for {@code age}
*/
default NameAndAge withAge(int age) {
return new NameAndAge(name(), age);
}
default NameAndAge withName(String name) {
return new NameAndAge(name, age());
}
/**
* Return a new instance of {@code NameAndAge} with a new value for {@code age}
*/
default NameAndAge withAge(int age) {
return new NameAndAge(name(), age);
}
}
}
```
@@ -298,41 +318,15 @@ annotation. Use `packagePattern` to change this (see Javadoc for details).
### Maven
1) Add the dependency that contains the `@RecordBuilder` annotation.
Add a dependency that contains the discoverable annotation processor:
```xml
<dependency>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-core</artifactId>
<version>set-version-here</version>
<artifactId>record-builder-processor</artifactId>
<version>${record.builder.version}</version>
<scope>provided</scope>
</dependency>
```
2) Enable the annotation processing for the Maven Compiler Plugin:
```xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>set-version-here</version>
<configuration>
<annotationProcessorPaths>
<annotationProcessorPath>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-processor</artifactId>
<version>set-version-here</version>
</annotationProcessorPath>
</annotationProcessorPaths>
<annotationProcessors>
<annotationProcessor>io.soabase.recordbuilder.processor.RecordBuilderProcessor</annotationProcessor>
</annotationProcessors>
... any other options here ...
</configuration>
</plugin>
```
### Gradle
@@ -355,76 +349,3 @@ Depending on your IDE you are likely to need to enable Annotation Processing in
RecordBuilder can be customized to your needs and you can even create your
own custom RecordBuilder annotations. See [Customizing RecordBuilder](customizing.md)
for details.
## Java 15 Versions
Artifacts compiled wth Java 15 are available. These versions have `-java15` appended.
Note: records are a preview feature only in Java 15. You'll need take a number of steps in order to try RecordBuilder:
- Install and make active Java 15 or later
- Make sure your development tool is using Java 15 or later and is configured to enable preview features (for Maven I've documented how to do this here: [https://stackoverflow.com/a/59363152/2048051](https://stackoverflow.com/a/59363152/2048051))
- Bear in mind that this is not yet meant for production and there are numerous bugs in the tools and JDKs.
Note: I've seen some very odd compilation bugs with the current Java 15 and Maven. If you get internal Javac errors I suggest rebuilding with `mvn clean package` and/or `mvn clean install`.
You will need to enable preview in your build tools:
### Maven
```xml
<dependencies>
<dependency>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-core</artifactId>
<version>record-builder-version-java15</version>
</dependency>
</dependencies>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>maven-compiler-version</version>
<configuration>
<annotationProcessorPaths>
<annotationProcessorPath>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-processor</artifactId>
<version>record-builder-version-java15</version>
</annotationProcessorPath>
</annotationProcessorPaths>
<annotationProcessors>
<annotationProcessor>io.soabase.recordbuilder.processor.RecordBuilderProcessor</annotationProcessor>
</annotationProcessors>
<!-- "release" and "enable-preview" are required while records are preview features -->
<release>15</release>
<compilerArgs>
<arg>--enable-preview</arg>
</compilerArgs>
... any other options here ...
</configuration>
</plugin>
```
Create a file in your project's root named `.mvn/jvm.config`. The file should have 1 line with the value: `--enable-preview`. (see: https://stackoverflow.com/questions/58023240)
### Gradle
```groovy
dependencies {
annotationProcessor 'io.soabase.record-builder:record-builder-processor:$record-builder-version-java15'
compileOnly 'io.soabase.record-builder:record-builder-core:$record-builder-version-java15'
}
tasks.withType(JavaCompile) {
options.fork = true
options.forkOptions.jvmArgs += '--enable-preview'
options.compilerArgs += '--enable-preview'
}
tasks.withType(Test) {
jvmArgs += "--enable-preview"
}
```

View File

@@ -1,22 +0,0 @@
#!/usr/bin/env bash
#
# Copyright 2019 Jordan Zimmerman
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
jenv local 15
javahome
mkdir -p .mvn/
echo "--enable-preview" > .mvn/jvm.config

View File

@@ -1,21 +0,0 @@
#!/usr/bin/env bash
#
# Copyright 2019 Jordan Zimmerman
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
jenv local 16
javahome
rm -fr .mvn

33
pom.xml
View File

@@ -5,7 +5,7 @@
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId>
<packaging>pom</packaging>
<version>27-java15</version>
<version>31</version>
<modules>
<module>record-builder-core</module>
@@ -19,8 +19,6 @@
<project.build.resourceEncoding>UTF-8</project.build.resourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<enable-preview />
<jdk-version>16</jdk-version>
<maven-compiler-plugin-version>3.8.1</maven-compiler-plugin-version>
@@ -33,7 +31,6 @@
<maven-clean-plugin-version>3.1.0</maven-clean-plugin-version>
<maven-shade-plugin-version>3.2.1</maven-shade-plugin-version>
<maven-release-plugin-version>2.5.3</maven-release-plugin-version>
<maven-surefire-plugin-version>3.0.0-M5</maven-surefire-plugin-version>
<maven-jar-plugin-version>3.2.0</maven-jar-plugin-version>
<license-file-path>src/etc/header.txt</license-file-path>
@@ -80,7 +77,7 @@
<url>https://github.com/randgalt/record-builder</url>
<connection>scm:git:https://github.com/randgalt/record-builder.git</connection>
<developerConnection>scm:git:git@github.com:randgalt/record-builder.git</developerConnection>
<tag>record-builder-27-java15</tag>
<tag>record-builder-31</tag>
</scm>
<issueManagement>
@@ -115,6 +112,12 @@
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-processor</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-validator</artifactId>
@@ -156,9 +159,6 @@
<version>${maven-compiler-plugin-version}</version>
<configuration>
<release>${jdk-version}</release>
<compilerArgs>
<arg>${enable-preview}</arg>
</compilerArgs>
</configuration>
</plugin>
@@ -308,15 +308,6 @@
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-plugin-version}</version>
<configuration>
<argLine>${enable-preview}</argLine>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
@@ -388,13 +379,5 @@
</plugins>
</build>
</profile>
<profile>
<id>java15</id>
<properties>
<jdk-version>15</jdk-version>
<enable-preview>--enable-preview</enable-preview>
</properties>
</profile>
</profiles>
</project>

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId>
<version>27-java15</version>
<version>31</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -89,6 +89,11 @@ public @interface RecordBuilder {
*/
String buildMethodName() default "build";
/**
* The name to use for the from-to-wither method
*/
String fromMethodName() default "from";
/**
* The name to use for the method that returns the record components as a stream
*/
@@ -177,6 +182,11 @@ public @interface RecordBuilder {
* The prefix for adder methods when {@link #addSingleItemCollectionBuilders()} is enabled
*/
String singleItemBuilderPrefix() default "add";
/**
* When enabled, adds functional methods to the nested "With" class (such as {@code map()} and {@code accept()}).
*/
boolean addFunctionalMethodsToWith() default false;
}
@Retention(RetentionPolicy.CLASS)

View File

@@ -20,7 +20,8 @@ import java.lang.annotation.*;
@RecordBuilder.Template(options = @RecordBuilder.Options(
interpretNotNulls = true,
useImmutableCollections = true,
addSingleItemCollectionBuilders = true
addSingleItemCollectionBuilders = true,
addFunctionalMethodsToWith = true
))
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId>
<version>27-java15</version>
<version>31</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -51,6 +51,7 @@ class InternalRecordBuilderProcessor {
private static final TypeName optionalLongType = TypeName.get(OptionalLong.class);
private static final TypeName optionalDoubleType = TypeName.get(OptionalDouble.class);
private static final TypeName validatorTypeName = ClassName.get("io.soabase.recordbuilder.validator", "RecordBuilderValidator");
private static final TypeVariableName rType = TypeVariableName.get("R");
private final ProcessingEnvironment processingEnv;
InternalRecordBuilderProcessor(ProcessingEnvironment processingEnv, TypeElement record, RecordBuilder.Options metaData, Optional<String> packageNameOpt) {
@@ -78,6 +79,7 @@ class InternalRecordBuilderProcessor {
}
addStaticDefaultBuilderMethod();
addStaticCopyBuilderMethod();
addStaticFromWithMethod();
addStaticComponentsMethod();
addBuildMethod();
addToStringMethod();
@@ -145,10 +147,16 @@ class InternalRecordBuilderProcessor {
.addJavadoc("Add withers to {@code $L}\n", recordClassType.name())
.addModifiers(Modifier.PUBLIC)
.addTypeVariables(typeVariables);
recordComponents.forEach(component -> addWithGetterMethod(classBuilder, component));
recordComponents.forEach(component -> addNestedGetterMethod(classBuilder, component));
addWithBuilderMethod(classBuilder);
addWithSuppliedBuilderMethod(classBuilder);
IntStream.range(0, recordComponents.size()).forEach(index -> add1WithMethod(classBuilder, recordComponents.get(index), index));
if (metaData.addFunctionalMethodsToWith()) {
classBuilder.addType(buildFunctionalInterface("Function", true))
.addType(buildFunctionalInterface("Consumer", false))
.addMethod(buildFunctionalHandler("Function", "map", true))
.addMethod(buildFunctionalHandler("Consumer", "accept", false));
}
builder.addType(classBuilder.build());
}
@@ -236,7 +244,6 @@ class InternalRecordBuilderProcessor {
codeBlockBuilder.add(";$]");
var methodName = getWithMethodName(component, metaData.withClassMethodPrefix());
var singleItemsMetaData = collectionBuilderUtils.singleItemsMetaData(component, STANDARD_FOR_SETTER);
var parameterSpecBuilder = ParameterSpec.builder(component.typeName(), component.name());
addConstructorAnnotations(component, parameterSpecBuilder);
var methodSpec = MethodSpec.methodBuilder(methodName)
@@ -483,6 +490,67 @@ class InternalRecordBuilderProcessor {
return codeBuilder.build();
}
private void addStaticFromWithMethod() {
/*
Adds static method that returns a "with"er view of an existing record.
public static With from(MyRecord from) {
return new MyRecordBuilder.With() {
@Override
public String p1() {
return from.p1();
}
@Override
public String p2() {
return from.p2();
}
};
}
*/
var witherClassNameBuilder = CodeBlock.builder()
.add("$L.$L", builderClassType.name(), metaData.withClassName());
if (!typeVariables.isEmpty()) {
witherClassNameBuilder.add("<");
IntStream.range(0, typeVariables.size()).forEach(index -> {
if (index > 0) {
witherClassNameBuilder.add(", ");
}
witherClassNameBuilder.add(typeVariables.get(index).name);
});
witherClassNameBuilder.add(">");
}
var witherClassName = witherClassNameBuilder.build().toString();
var codeBuilder = CodeBlock.builder()
.add("return new $L", witherClassName)
.add("() {\n").indent();
IntStream.range(0, recordComponents.size()).forEach(index -> {
var component = recordComponents.get(index);
if (index > 0) {
codeBuilder.add("\n");
}
codeBuilder.add("@Override\n")
.add("public $T $L() {\n", component.typeName(), component.name())
.indent()
.addStatement("return from.$L()", component.name())
.unindent()
.add("}\n");
});
codeBuilder.unindent().addStatement("}");
var withType = ClassName.get("", witherClassName);
var methodSpec = MethodSpec.methodBuilder("from")//metaData.copyMethodName())
.addJavadoc("Return a \"with\"er for an existing record instance\n")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addAnnotation(generatedRecordBuilderAnnotation)
.addTypeVariables(typeVariables)
.addParameter(recordClassType.typeName(), metaData.fromMethodName())
.returns(withType)
.addCode(codeBuilder.build())
.build();
builder.addMethod(methodSpec);
}
private void addStaticCopyBuilderMethod() {
/*
Adds a copy builder method that pre-fills the builder with existing values similar to:
@@ -536,8 +604,8 @@ class InternalRecordBuilderProcessor {
Adds a static method that converts a record instance into a stream of its component parts
public static Stream<Map.Entry<String, Object>> stream(MyRecord record) {
return Stream.of(Map.entry("p1", record.p1()),
Map.entry("p2", record.p2()));
return Stream.of(new AbstractMap.SimpleImmutableEntry<>("p1", record.p1()),
new AbstractMap.SimpleImmutableEntry<>("p2", record.p2()));
}
*/
var codeBuilder = CodeBlock.builder().add("return $T.of(", Stream.class);
@@ -546,7 +614,7 @@ class InternalRecordBuilderProcessor {
codeBuilder.add(",\n ");
}
var name = recordComponents.get(index).name();
codeBuilder.add("$T.entry($S, record.$L())", Map.class, name, name);
codeBuilder.add("new $T<>($S, record.$L())", AbstractMap.SimpleImmutableEntry.class, name, name);
});
codeBuilder.add(")");
var mapEntryTypeVariables = ParameterizedTypeName.get(Map.Entry.class, String.class, Object.class);
@@ -596,7 +664,7 @@ class InternalRecordBuilderProcessor {
return (component.typeName() instanceof ParameterizedTypeName parameterizedTypeName) && parameterizedTypeName.rawType.equals(optionalType);
}
private void addWithGetterMethod(TypeSpec.Builder classBuilder, RecordClassType component) {
private void addNestedGetterMethod(TypeSpec.Builder classBuilder, RecordClassType component) {
/*
For a single record component, add a getter similar to:
@@ -807,5 +875,70 @@ class InternalRecordBuilderProcessor {
methodSpec.addStatement("return this").addParameter(parameterSpecBuilder.build());
builder.addMethod(methodSpec.build());
}
private List<TypeVariableName> typeVariablesWithReturn() {
var variables = new ArrayList<TypeVariableName>();
variables.add(rType);
variables.addAll(typeVariables);
return variables;
}
private MethodSpec buildFunctionalHandler(String className, String methodName, boolean isMap) {
/*
Build a Functional handler ala:
default <R> R map(Function<R, T> proc) {
return proc.apply(p());
}
*/
var localTypeVariables = isMap ? typeVariablesWithReturn() : typeVariables;
var typeName = localTypeVariables.isEmpty() ? ClassName.get("", className) : ParameterizedTypeName.get(ClassName.get("", className), localTypeVariables.toArray(TypeName[]::new));
var methodBuilder = MethodSpec.methodBuilder(methodName)
.addAnnotation(generatedRecordBuilderAnnotation)
.addModifiers(Modifier.PUBLIC, Modifier.DEFAULT)
.addParameter(typeName, "proc");
var codeBlockBuilder = CodeBlock.builder();
if (isMap) {
methodBuilder.addJavadoc("Map record components into a new object");
methodBuilder.addTypeVariable(rType);
methodBuilder.returns(rType);
codeBlockBuilder.add("return ");
} else {
methodBuilder.addJavadoc("Perform an operation on record components");
}
codeBlockBuilder.add("proc.apply(");
addComponentCallsAsArguments(-1, codeBlockBuilder);
codeBlockBuilder.add(");");
methodBuilder.addCode(codeBlockBuilder.build());
return methodBuilder.build();
}
private TypeSpec buildFunctionalInterface(String className, boolean isMap) {
/*
Build a Functional interface ala:
@FunctionalInterface
interface Function<R, T> {
R apply(T a);
}
*/
var localTypeVariables = isMap ? typeVariablesWithReturn() : typeVariables;
var methodBuilder = MethodSpec.methodBuilder("apply").addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT);
recordComponents.forEach(component -> {
var parameterSpecBuilder = ParameterSpec.builder(component.typeName(), component.name());
addConstructorAnnotations(component, parameterSpecBuilder);
methodBuilder.addParameter(parameterSpecBuilder.build());
});
if (isMap) {
methodBuilder.returns(rType);
}
return TypeSpec.interfaceBuilder(className)
.addAnnotation(generatedRecordBuilderAnnotation)
.addAnnotation(FunctionalInterface.class)
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addTypeVariables(localTypeVariables)
.addMethod(methodBuilder.build())
.build();
}
}

View File

@@ -76,8 +76,7 @@ public class RecordBuilderProcessor
var typeElement = (TypeElement) element;
processRecordInterface(typeElement, element.getAnnotation(RecordInterface.class).addRecordBuilder(), getMetaData(typeElement), Optional.empty(), false);
} else if (annotationClass.equals(RECORD_BUILDER_INCLUDE) || annotationClass.equals(RECORD_INTERFACE_INCLUDE)) {
var metaData = RecordBuilderOptions.build(processingEnv.getOptions());
processIncludes(element, metaData, annotationClass);
processIncludes(element, getMetaData(element), annotationClass);
} else {
var recordBuilderTemplate = annotation.getAnnotation(RecordBuilder.Template.class);
if (recordBuilderTemplate != null) {
@@ -90,8 +89,8 @@ public class RecordBuilderProcessor
}
}
private RecordBuilder.Options getMetaData(TypeElement typeElement) {
var recordSpecificMetaData = typeElement.getAnnotation(RecordBuilder.Options.class);
private RecordBuilder.Options getMetaData(Element element) {
var recordSpecificMetaData = element.getAnnotation(RecordBuilder.Options.class);
return (recordSpecificMetaData != null) ? recordSpecificMetaData : RecordBuilderOptions.build(processingEnv.getOptions());
}

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId>
<version>27-java15</version>
<version>31</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -21,7 +21,7 @@
<dependency>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-core</artifactId>
<artifactId>record-builder-processor</artifactId>
<scope>provided</scope>
</dependency>
@@ -51,31 +51,6 @@
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<annotationProcessorPath>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder-processor</artifactId>
<version>${project.version}</version>
</annotationProcessorPath>
</annotationProcessorPaths>
<annotationProcessors>
<annotationProcessor>io.soabase.recordbuilder.processor.RecordBuilderProcessor</annotationProcessor>
</annotationProcessors>
<release>${jdk-version}</release>
<compilerArgs>
<arg>${enable-preview}</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>

View File

@@ -17,12 +17,20 @@ package io.soabase.recordbuilder.test;
import io.soabase.recordbuilder.core.RecordBuilder;
import java.time.Instant;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
@RecordBuilder
@RecordBuilder.Options(useImmutableCollections = true)
public record CollectionRecord<T, X extends Point>(List<T> l, Set<T> s, Map<T, X> m, Collection<X> c) implements CollectionRecordBuilder.With<T, X> {
@RecordBuilder.Options(useImmutableCollections = true, addFunctionalMethodsToWith = true)
public record CollectionRecord<T, X extends Point>(List<T> l, Set<T> s, Map<T, X> m,
Collection<X> c) implements CollectionRecordBuilder.With<T, X> {
public static void main(String[] args) {
var r = new CollectionRecord<>(List.of("hey"), Set.of("there"), Map.of("one", new Point(10, 20)), Set.of(new Point(30, 40)));
Instant now = r.map((l1, s1, m1, c1) -> Instant.now());
r.accept((l1, s1, m1, c1) -> {
});
}
}

View File

@@ -0,0 +1,24 @@
/**
* Copyright 2019 Jordan Zimmerman
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.soabase.recordbuilder.test;
import io.soabase.recordbuilder.core.RecordBuilder;
@RecordBuilder.Options(prefixEnclosingClassNames = false)
@RecordBuilder.Include(IncludeWithOption.Hey.class)
public class IncludeWithOption {
public static record Hey(String s){}
}

View File

@@ -27,7 +27,8 @@ import java.util.Set;
@RecordBuilder.Options(
addSingleItemCollectionBuilders = true,
singleItemBuilderPrefix = "add1",
useImmutableCollections = true
useImmutableCollections = true,
addFunctionalMethodsToWith = true
)
public record SingleItems<T>(List<String> strings, Set<List<T>> sets, Map<Instant, T> map, Collection<T> collection) implements SingleItemsBuilder.With<T> {
}

View File

@@ -0,0 +1,28 @@
/**
* Copyright 2019 Jordan Zimmerman
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.soabase.recordbuilder.test;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
class TestIncludes {
@Test
void testOptionsOnInclude() {
// assert it's not prefixed with the enclosing class name
IncludeWithOption.Hey hey = io.soabase.recordbuilder.test.HeyBuilder.builder().s("this is s").build();
Assertions.assertEquals("this is s", hey.s());
}
}

View File

@@ -15,6 +15,9 @@
*/
package io.soabase.recordbuilder.test;
import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -48,4 +51,28 @@ public class TestRecordInterface
Assertions.assertEquals(generic.i(), 101);
Assertions.assertEquals(generic.s(), now);
}
@Test
public void testBuilderStreamWithValues()
{
var stream = SimpleRecordBuilder.stream(SimpleRecordBuilder.builder()
.i(19)
.s("value")
.build())
.toList();
Assertions.assertEquals(stream, List.of(
Map.entry("i", 19),
Map.entry("s", "value")));
}
@Test
public void testBuilderStreamWithNulls()
{
var stream = SimpleRecordBuilder.stream(SimpleRecordBuilder.builder()
.build())
.toList();
Assertions.assertEquals(stream, List.of(
new SimpleImmutableEntry<>("i", 0),
new SimpleImmutableEntry<>("s", null)));
}
}

View File

@@ -21,6 +21,17 @@ import org.junit.jupiter.api.Test;
import java.util.List;
class TestWithers {
@Test
void testFromWithers() {
var r1 = new SimpleGenericRecord<>(10, List.of("1", "2", "3"));
var r2 = SimpleGenericRecordBuilder.from(r1).withS(List.of("4", "5"));
var r3 = SimpleGenericRecordBuilder.from(r1).with(b -> b.i(20).s(List.of("6", "7")));
Assertions.assertEquals(List.of("1", "2", "3"), r1.s());
Assertions.assertEquals(List.of("4", "5"), r2.s());
Assertions.assertEquals(List.of("6", "7"), r3.s());
Assertions.assertEquals(20, r3.i());
}
@Test
void testWithers() {
var r1 = new SimpleGenericRecord<>(10, List.of("1", "2", "3"));

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId>
<version>27-java15</version>
<version>31</version>
</parent>
<modelVersion>4.0.0</modelVersion>