Skip to content

Commit

Permalink
Support staged builder
Browse files Browse the repository at this point in the history
Closes #159

New option to have standard builder, staged builder or both
  • Loading branch information
Randgalt committed Jan 4, 2024
1 parent 66458a7 commit 60e9f35
Show file tree
Hide file tree
Showing 8 changed files with 319 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,8 @@
*/
package io.soabase.recordbuilder.core;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.lang.model.element.Modifier;
import java.lang.annotation.*;

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
Expand Down Expand Up @@ -311,6 +306,21 @@
* Makes the generated builder's constructors public
*/
boolean publicBuilderConstructors() default false;

/**
* Whether to add standard builder, staged builder or both
*/
BuilderMode builderMode() default BuilderMode.STANDARD;

/**
* The name to use for the staged builder if present
*/
String stagedBuilderMethodName() default "stagedBuilder";

/**
* The suffix to use for the staged builder interfaces if present
*/
String stagedBuilderMethodSuffix() default "Stage";
}

@Retention(RetentionPolicy.CLASS)
Expand All @@ -321,4 +331,8 @@

boolean asRecordInterface() default false;
}

enum BuilderMode {
STANDARD, STAGED, STANDARD_AND_STAGED,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,15 @@ public static ClassType getClassType(ClassName builderClassName,
return new ClassType(ParameterizedTypeName.get(builderClassName, typeNames), builderClassName.simpleName());
}

public static ClassType getClassTypeFromNames(ClassName builderClassName,
List<? extends TypeVariableName> typeVariableNames) {
if (typeVariableNames.isEmpty()) {
return new ClassType(builderClassName, builderClassName.simpleName());
}
TypeName[] typeNames = typeVariableNames.toArray(TypeName[]::new);
return new ClassType(ParameterizedTypeName.get(builderClassName, typeNames), builderClassName.simpleName());
}

public static RecordClassType getRecordClassType(ProcessingEnvironment processingEnv,
RecordComponentElement recordComponent, List<? extends AnnotationMirror> accessorAnnotations,
List<? extends AnnotationMirror> canonicalConstructorAnnotations) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

import com.squareup.javapoet.*;
import io.soabase.recordbuilder.core.RecordBuilder;
import io.soabase.recordbuilder.core.RecordBuilder.BuilderMode;
import io.soabase.recordbuilder.processor.CollectionBuilderUtils.SingleItemsMetaData;

import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.*;
Expand All @@ -31,8 +33,7 @@

import static io.soabase.recordbuilder.processor.CollectionBuilderUtils.SingleItemsMetaDataMode.EXCLUDE_WILDCARD_TYPES;
import static io.soabase.recordbuilder.processor.CollectionBuilderUtils.SingleItemsMetaDataMode.STANDARD_FOR_SETTER;
import static io.soabase.recordbuilder.processor.ElementUtils.getBuilderName;
import static io.soabase.recordbuilder.processor.ElementUtils.getWithMethodName;
import static io.soabase.recordbuilder.processor.ElementUtils.*;
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.generatedRecordBuilderAnnotation;
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.recordBuilderGeneratedAnnotation;

Expand Down Expand Up @@ -85,14 +86,21 @@ class InternalRecordBuilderProcessor {
if (!metaData.beanClassName().isEmpty()) {
addBeanNestedClass();
}
if (metaData.builderMode() != BuilderMode.STANDARD) {
addStagedBuilderClasses();
addStaticStagedBuilderMethod((metaData.builderMode() == BuilderMode.STANDARD_AND_STAGED)
? metaData.stagedBuilderMethodName() : metaData.builderMethodName());
}
addDefaultConstructor();
if (metaData.addStaticBuilder()) {
addStaticBuilder();
}
if (recordComponents.size() > 0) {
addAllArgsConstructor();
}
addStaticDefaultBuilderMethod();
if (metaData.builderMode() != BuilderMode.STAGED) {
addStaticDefaultBuilderMethod();
}
addStaticCopyBuilderMethod();
if (metaData.enableWither()) {
addStaticFromWithMethod();
Expand Down Expand Up @@ -136,7 +144,7 @@ private void addVisibility(boolean builderIsInRecordPackage, Set<Modifier> modif
if (modifiers.contains(Modifier.PUBLIC) || modifiers.contains(Modifier.PRIVATE)
|| modifiers.contains(Modifier.PROTECTED)) {
builder.addModifiers(Modifier.PUBLIC); // builders are top level classes - can only be public or
// package-private
// package-private
}
// is package-private
} else {
Expand All @@ -162,6 +170,76 @@ private List<RecordClassType> buildRecordComponents(TypeElement record) {
}).collect(Collectors.toList());
}

public void addStagedBuilderClasses() {
if (recordComponents.size() < 2) {
return;
}

IntStream.range(0, recordComponents.size()).forEach(index -> {
Optional<RecordClassType> nextComponent = ((index + 1) < recordComponents.size())
? Optional.of(recordComponents.get(index + 1)) : Optional.empty();
add1StagedBuilderClass(recordComponents.get(index), nextComponent);
});

/*
* Adds the final builder stage that has the "build" methods similar to:
*
* public class BuilderStage { PersonBuilder builder();
*
* default Person build() { return builder().build(); }
*/
var classBuilder = TypeSpec.interfaceBuilder(stagedBuilderName(builderClassType))
.addAnnotation(generatedRecordBuilderAnnotation)
.addJavadoc("Add final staged builder to {@code $L}\n", recordClassType.name())
.addModifiers(Modifier.PUBLIC).addTypeVariables(typeVariables);
if (metaData.addClassRetainedGenerated()) {
classBuilder.addAnnotation(recordBuilderGeneratedAnnotation);
}

MethodSpec buildMethod = buildMethod().addModifiers(Modifier.PUBLIC, Modifier.DEFAULT)
.addStatement("return builder().build()").build();
classBuilder.addMethod(buildMethod);

var builderMethod = MethodSpec.methodBuilder(metaData.builderMethodName())
.addJavadoc("Return a new builder with all fields set to the current values in this builder\n")
.addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT).addAnnotation(generatedRecordBuilderAnnotation)
.returns(builderClassType.typeName()).build();
classBuilder.addMethod(builderMethod);

builder.addType(classBuilder.build());
}

public void add1StagedBuilderClass(RecordClassType component, Optional<RecordClassType> nextComponent) {
/*
* Adds a nested interface similar to:
*
* public class NameStage { AgeStage name(String name); }
*/
var classBuilder = TypeSpec.interfaceBuilder(stagedBuilderName(component))
.addAnnotation(generatedRecordBuilderAnnotation)
.addJavadoc("Add staged builder to {@code $L} for component {@code $L}\n", recordClassType.name(),
component.name())
.addModifiers(Modifier.PUBLIC).addTypeVariables(typeVariables);
if (metaData.addClassRetainedGenerated()) {
classBuilder.addAnnotation(recordBuilderGeneratedAnnotation);
}

var returnType = nextComponent.map(this::stagedBuilderType)
.orElseGet(() -> stagedBuilderType(builderClassType));
var methodSpec = MethodSpec.methodBuilder(prefixedName(component, false))
.addAnnotation(generatedRecordBuilderAnnotation).returns(returnType.typeName())
.addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT);

methodSpec.addJavadoc("Set a new value for the {@code $L} record component in the builder\n", component.name());
var parameterSpecBuilder = ParameterSpec.builder(component.typeName(), component.name());
addConstructorAnnotations(component, parameterSpecBuilder);
methodSpec.addParameter(parameterSpecBuilder.build());

classBuilder.addMethod(methodSpec.build());

builder.addType(classBuilder.build());
}

private void addWithNestedClass() {
/*
* Adds a nested interface that adds withers similar to:
Expand Down Expand Up @@ -470,11 +548,15 @@ private void addBuildMethod() {
* public MyRecord build() { return new MyRecord(p1, p2, ...); }
*/
CodeBlock codeBlock = buildCodeBlock();
var methodSpec = MethodSpec.methodBuilder(metaData.buildMethodName())
MethodSpec methodSpec = buildMethod().addCode(codeBlock).build();
builder.addMethod(methodSpec);
}

private MethodSpec.Builder buildMethod() {
return MethodSpec.methodBuilder(metaData.buildMethodName())
.addJavadoc("Return a new record instance with all fields set to the current values in this builder\n")
.addModifiers(Modifier.PUBLIC).addAnnotation(generatedRecordBuilderAnnotation)
.returns(recordClassType.typeName()).addCode(codeBlock).build();
builder.addMethod(methodSpec);
.returns(recordClassType.typeName());
}

private CodeBlock buildCodeBlock() {
Expand Down Expand Up @@ -603,7 +685,7 @@ private void addStaticCopyBuilderMethod() {

private void addStaticDefaultBuilderMethod() {
/*
* Adds a the default builder method similar to:
* Adds the default builder method similar to:
*
* public static MyRecordBuilder builder() { return new MyRecordBuilder(); }
*/
Expand All @@ -615,6 +697,35 @@ private void addStaticDefaultBuilderMethod() {
builder.addMethod(methodSpec);
}

private void addStaticStagedBuilderMethod(String builderMethodName) {
if (recordComponents.size() < 2) {
return;
}

/*
* Adds the staged builder method similar to:
*
* public static NameStage stagedBuilder() { return name -> age -> () -> new PersonBuilder(name, age).build(); }
*/
CodeBlock.Builder codeBlock = CodeBlock.builder().add("return ");
recordComponents.forEach(recordComponent -> codeBlock.add("$L -> ", recordComponent.name()));
codeBlock.add("() -> new $T(", builderClassType.typeName());
IntStream.range(0, recordComponents.size()).forEach(index -> {
if (index > 0) {
codeBlock.add(", ");
}
codeBlock.add("$L", recordComponents.get(index).name());
});
codeBlock.addStatement(")");

var methodSpec = MethodSpec.methodBuilder(builderMethodName)
.addJavadoc("Return the first stage of a staged builder\n")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC).addAnnotation(generatedRecordBuilderAnnotation)
.addTypeVariables(typeVariables).returns(stagedBuilderType(recordComponents.get(0)).typeName())
.addCode(codeBlock.build()).build();
builder.addMethod(methodSpec);
}

private void addStaticComponentsMethod() {
/*
* Adds a static method that converts a record instance into a stream of its component parts
Expand Down Expand Up @@ -701,15 +812,15 @@ private String capitalize(String s) {
return (s.length() < 2) ? s.toUpperCase(Locale.ROOT) : (Character.toUpperCase(s.charAt(0)) + s.substring(1));
}

private void add1CollectionBuilders(CollectionBuilderUtils.SingleItemsMetaData meta, RecordClassType component) {
private void add1CollectionBuilders(SingleItemsMetaData meta, RecordClassType component) {
if (collectionBuilderUtils.isList(component) || collectionBuilderUtils.isSet(component)) {
add1ListBuilder(meta, component);
} else if (collectionBuilderUtils.isMap(component)) {
add1MapBuilder(meta, component);
}
}

private void add1MapBuilder(CollectionBuilderUtils.SingleItemsMetaData meta, RecordClassType component) {
private void add1MapBuilder(SingleItemsMetaData meta, RecordClassType component) {
/*
* For a single map record component, add a methods similar to:
*
Expand Down Expand Up @@ -757,7 +868,7 @@ private void add1MapBuilder(CollectionBuilderUtils.SingleItemsMetaData meta, Rec
}
}

private void add1ListBuilder(CollectionBuilderUtils.SingleItemsMetaData meta, RecordClassType component) {
private void add1ListBuilder(SingleItemsMetaData meta, RecordClassType component) {
/*
* For a single list or set record component, add methods similar to:
*
Expand Down Expand Up @@ -955,4 +1066,12 @@ private String prefixedName(RecordClassType component, boolean isGetter) {
}
return prefixer.apply(metaData.setterPrefix(), component.name());
}

private String stagedBuilderName(ClassType component) {
return capitalize(component.name()) + metaData.stagedBuilderMethodSuffix();
}

private ClassType stagedBuilderType(ClassType component) {
return getClassTypeFromNames(ClassName.get("", stagedBuilderName(component)), typeVariables);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright 2019 The original author or authors
*
* 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.staged;

import io.soabase.recordbuilder.core.RecordBuilder;

@RecordBuilder
@RecordBuilder.Options(builderMode = RecordBuilder.BuilderMode.STANDARD_AND_STAGED)
public record CombinedGenericStaged<T extends GenericStaged<SimpleStaged, U>, U>(String name, T aT, U theUThing) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright 2019 The original author or authors
*
* 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.staged;

import io.soabase.recordbuilder.core.RecordBuilder;

import java.time.Instant;

@RecordBuilder
@RecordBuilder.Options(builderMode = RecordBuilder.BuilderMode.STANDARD_AND_STAGED)
public record CombinedSimpleStaged(int i, String s, Instant instant) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright 2019 The original author or authors
*
* 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.staged;

import io.soabase.recordbuilder.core.RecordBuilder;

@RecordBuilder
@RecordBuilder.Options(builderMode = RecordBuilder.BuilderMode.STAGED)
public record GenericStaged<T extends SimpleStaged, U>(String name, T aT, U theUThing) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright 2019 The original author or authors
*
* 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.staged;

import io.soabase.recordbuilder.core.RecordBuilder;

import java.time.Instant;

@RecordBuilder
@RecordBuilder.Options(builderMode = RecordBuilder.BuilderMode.STAGED)
public record SimpleStaged(int i, String s, Instant instant) {
}
Loading

0 comments on commit 60e9f35

Please sign in to comment.