diff --git a/record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilder.java b/record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilder.java index 14e462c..b448001 100644 --- a/record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilder.java +++ b/record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilder.java @@ -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) diff --git a/record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilderFull.java b/record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilderFull.java index 6eddb7e..df03d36 100644 --- a/record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilderFull.java +++ b/record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilderFull.java @@ -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) diff --git a/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/InternalRecordBuilderProcessor.java b/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/InternalRecordBuilderProcessor.java index c20aca6..9cb18ac 100644 --- a/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/InternalRecordBuilderProcessor.java +++ b/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/InternalRecordBuilderProcessor.java @@ -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 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 typeVariablesWithReturn() { + var variables = new ArrayList(); + variables.add(rType); + variables.addAll(typeVariables); + return variables; + } + + private MethodSpec buildFunctionalHandler(String className, String methodName, boolean isMap) { + /* + Build a Functional handler ala: + + default R map(Function 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 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(); + } } diff --git a/record-builder-test/src/main/java/io/soabase/recordbuilder/test/CollectionRecord.java b/record-builder-test/src/main/java/io/soabase/recordbuilder/test/CollectionRecord.java index 001b62b..e01e454 100644 --- a/record-builder-test/src/main/java/io/soabase/recordbuilder/test/CollectionRecord.java +++ b/record-builder-test/src/main/java/io/soabase/recordbuilder/test/CollectionRecord.java @@ -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(List l, Set s, Map m, Collection c) implements CollectionRecordBuilder.With { +@RecordBuilder.Options(useImmutableCollections = true, addFunctionalMethodsToWith = true) +public record CollectionRecord(List l, Set s, Map m, + Collection c) implements CollectionRecordBuilder.With { + 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) -> { + }); + } } diff --git a/record-builder-test/src/main/java/io/soabase/recordbuilder/test/SingleItems.java b/record-builder-test/src/main/java/io/soabase/recordbuilder/test/SingleItems.java index 3dbd551..63a6943 100644 --- a/record-builder-test/src/main/java/io/soabase/recordbuilder/test/SingleItems.java +++ b/record-builder-test/src/main/java/io/soabase/recordbuilder/test/SingleItems.java @@ -27,7 +27,8 @@ import java.util.Set; @RecordBuilder.Options( addSingleItemCollectionBuilders = true, singleItemBuilderPrefix = "add1", - useImmutableCollections = true + useImmutableCollections = true, + addFunctionalMethodsToWith = true ) public record SingleItems(List strings, Set> sets, Map map, Collection collection) implements SingleItemsBuilder.With { }