Skip to content

Commit 747b252

Browse files
committed
Add support for a builder with required components
When the record components are annotated with `@RecordBuilder.Required`, the builder method will take the required record components as its parameters. This is particularly useful to ensure that the builder is always populated with the required fields at compile-time instead of relying on a runtime validation. Example: ``` public record MyRecord(@required int a, @required int b, int c) @generated("io.soabase.recordbuilder.core.RecordBuilder") public class MyRecordBuilder { ... @generated("io.soabase.recordbuilder.core.RecordBuilder") public static MyRecordBuilder builder(int a, int b) { return new MyRecordBuilder().a(a).b(b); } ... } ```
1 parent de842e7 commit 747b252

5 files changed

Lines changed: 96 additions & 0 deletions

File tree

options.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,8 @@ Special handling for collections. See the project test classes for usage.
114114
| `@RecordBuilder.Options(useUnmodifiableCollections = true/false)` | Adds special handling for collection record components. The default is `false`. |
115115
| `@RecordBuilder.Options(allowNullableCollections = true/false)` | Adds special null handling for record collectioncomponents. The default is `false`. |
116116
| `@RecordBuilder.Options(addSingleItemCollectionBuilders = true/false)` | Adds special handling for record collectioncomponents. The default is `false`. |
117+
118+
## Required Components
119+
120+
You can annotate record components with `@RecordBuilder.Required` to generate a builder with required components in the builder parameters.
121+
See [RequiredComponent.java](record-builder-test/src/main/java/io/soabase/recordbuilder/test/RequiredComponents.java) for an example.

record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilder.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,4 +386,13 @@ enum BuilderMode {
386386
*/
387387
Class<?> source() default Object.class;
388388
}
389+
390+
/**
391+
* Apply to record components to specify required components for the generated builder.
392+
*/
393+
@Retention(RetentionPolicy.CLASS)
394+
@Target(ElementType.FIELD)
395+
@Inherited
396+
@interface Required {
397+
}
389398
}

record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/InternalRecordBuilderProcessor.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ class InternalRecordBuilderProcessor {
108108
if ((metaData.builderMode() != BuilderMode.STAGED)
109109
&& (metaData.builderMode() != BuilderMode.STAGED_REQUIRED_ONLY)) {
110110
addStaticDefaultBuilderMethod();
111+
if (record.getEnclosedElements().stream()
112+
.anyMatch(element -> element.getAnnotation(RecordBuilder.Required.class) != null)) {
113+
addStaticRequiredComponentsBuilderMethod(record);
114+
}
111115
}
112116
addStaticCopyBuilderMethod();
113117
if (metaData.enableWither()) {
@@ -793,6 +797,29 @@ private void addStaticDefaultBuilderMethod() {
793797
builder.addMethod(methodSpec);
794798
}
795799

800+
private void addStaticRequiredComponentsBuilderMethod(TypeElement record) {
801+
/*
802+
* Adds the builder method with all required components similar to:
803+
*
804+
* public static MyRecordBuilder builder(int p1, int p2) { return new MyRecordBuilder().pi(pi).p2(p2); }
805+
*/
806+
var requiredParameters = record.getEnclosedElements().stream()
807+
.filter(element -> element.getAnnotation(RecordBuilder.Required.class) != null)
808+
.map(element -> ParameterSpec
809+
.builder(TypeName.get(element.asType()), element.getSimpleName().toString()).build())
810+
.toList();
811+
var codeBuilder = CodeBlock.builder().add("return new $T()", builderClassType.typeName());
812+
IntStream.range(0, requiredParameters.size()).forEach(index -> {
813+
codeBuilder.add(".$L($L)", requiredParameters.get(index).name, requiredParameters.get(index).name);
814+
});
815+
var methodSpec = MethodSpec.methodBuilder(metaData.builderMethodName()).addParameters(requiredParameters)
816+
.addJavadoc("Return a new builder with all fields set to the values taken from the given parameters\n")
817+
.addModifiers(Modifier.PUBLIC, Modifier.STATIC).addAnnotation(generatedRecordBuilderAnnotation)
818+
.addTypeVariables(typeVariables).returns(builderClassType.typeName()).addStatement(codeBuilder.build())
819+
.build();
820+
builder.addMethod(methodSpec);
821+
}
822+
796823
private void addStaticStagedBuilderMethod(String builderMethodName) {
797824
/*
798825
* Adds the staged builder method similar to:
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright 2019 The original author or authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.soabase.recordbuilder.test;
17+
18+
import io.soabase.recordbuilder.core.RecordBuilder;
19+
20+
import java.util.Optional;
21+
22+
@RecordBuilder
23+
public record RequiredComponents(@RecordBuilder.Required int a, @RecordBuilder.Required String b, float c,
24+
Optional<Integer> d) {
25+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2019 The original author or authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.soabase.recordbuilder.test;
17+
18+
import org.junit.jupiter.api.Test;
19+
20+
import java.util.Optional;
21+
22+
import static org.junit.jupiter.api.Assertions.assertEquals;
23+
24+
public class TestRequiredComponents {
25+
@Test
26+
void testRequiredComponents() {
27+
var obj = RequiredComponentsBuilder.builder(100, "hello").build();
28+
assertEquals(new RequiredComponents(100, "hello", 0.0f, Optional.empty()), obj);
29+
}
30+
}

0 commit comments

Comments
 (0)