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(...));
```
This commit is contained in:
@@ -182,6 +182,11 @@ public @interface RecordBuilder {
|
||||
* The prefix for adder methods when {@link #addSingleItemCollectionBuilders()} is enabled
|
||||
*/
|
||||
String singleItemBuilderPrefix() default "add";
|
||||
|
||||
/**
|
||||
* When enabled, adds functional methods to the nested "With" class (such as {@code map()} and {@code accept()}).
|
||||
*/
|
||||
boolean addFunctionalMethodsToWith() default false;
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.CLASS)
|
||||
|
||||
@@ -20,7 +20,8 @@ import java.lang.annotation.*;
|
||||
@RecordBuilder.Template(options = @RecordBuilder.Options(
|
||||
interpretNotNulls = true,
|
||||
useImmutableCollections = true,
|
||||
addSingleItemCollectionBuilders = true
|
||||
addSingleItemCollectionBuilders = true,
|
||||
addFunctionalMethodsToWith = true
|
||||
))
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@Target(ElementType.TYPE)
|
||||
|
||||
@@ -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) {
|
||||
@@ -146,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());
|
||||
}
|
||||
|
||||
@@ -657,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:
|
||||
|
||||
@@ -868,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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) -> {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user