Compare commits

...

6 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
11 changed files with 129 additions and 16 deletions

View File

@@ -5,7 +5,7 @@
<groupId>io.soabase.record-builder</groupId>
<artifactId>record-builder</artifactId>
<packaging>pom</packaging>
<version>29</version>
<version>31</version>
<modules>
<module>record-builder-core</module>
@@ -77,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-29</tag>
<tag>record-builder-31</tag>
</scm>
<issueManagement>

View File

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

View File

@@ -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)

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>29</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) {
@@ -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());
}
@@ -237,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)
@@ -598,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);
@@ -608,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);
@@ -658,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:
@@ -869,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

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

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

@@ -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

@@ -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

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