From 747b252106eb01dbc38f1da459d758df6bc2e0af Mon Sep 17 00:00:00 2001 From: Fredy Wijaya Date: Tue, 8 Apr 2025 23:36:49 -0500 Subject: [PATCH 1/2] 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); } ... } ``` --- options.md | 5 ++++ .../recordbuilder/core/RecordBuilder.java | 9 ++++++ .../InternalRecordBuilderProcessor.java | 27 +++++++++++++++++ .../test/RequiredComponents.java | 25 ++++++++++++++++ .../test/TestRequiredComponents.java | 30 +++++++++++++++++++ 5 files changed, 96 insertions(+) create mode 100644 record-builder-test/src/main/java/io/soabase/recordbuilder/test/RequiredComponents.java create mode 100644 record-builder-test/src/test/java/io/soabase/recordbuilder/test/TestRequiredComponents.java diff --git a/options.md b/options.md index d64cdaf2..e4de80bd 100644 --- a/options.md +++ b/options.md @@ -114,3 +114,8 @@ Special handling for collections. See the project test classes for usage. | `@RecordBuilder.Options(useUnmodifiableCollections = true/false)` | Adds special handling for collection record components. The default is `false`. | | `@RecordBuilder.Options(allowNullableCollections = true/false)` | Adds special null handling for record collectioncomponents. The default is `false`. | | `@RecordBuilder.Options(addSingleItemCollectionBuilders = true/false)` | Adds special handling for record collectioncomponents. The default is `false`. | + +## Required Components + +You can annotate record components with `@RecordBuilder.Required` to generate a builder with required components in the builder parameters. +See [RequiredComponent.java](record-builder-test/src/main/java/io/soabase/recordbuilder/test/RequiredComponents.java) for an example. 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 3f2d57e0..61e7ff5c 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 @@ -386,4 +386,13 @@ enum BuilderMode { */ Class source() default Object.class; } + + /** + * Apply to record components to specify required components for the generated builder. + */ + @Retention(RetentionPolicy.CLASS) + @Target(ElementType.FIELD) + @Inherited + @interface Required { + } } 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 0a9d8c7e..87a0d0ea 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 @@ -108,6 +108,10 @@ class InternalRecordBuilderProcessor { if ((metaData.builderMode() != BuilderMode.STAGED) && (metaData.builderMode() != BuilderMode.STAGED_REQUIRED_ONLY)) { addStaticDefaultBuilderMethod(); + if (record.getEnclosedElements().stream() + .anyMatch(element -> element.getAnnotation(RecordBuilder.Required.class) != null)) { + addStaticRequiredComponentsBuilderMethod(record); + } } addStaticCopyBuilderMethod(); if (metaData.enableWither()) { @@ -793,6 +797,29 @@ private void addStaticDefaultBuilderMethod() { builder.addMethod(methodSpec); } + private void addStaticRequiredComponentsBuilderMethod(TypeElement record) { + /* + * Adds the builder method with all required components similar to: + * + * public static MyRecordBuilder builder(int p1, int p2) { return new MyRecordBuilder().pi(pi).p2(p2); } + */ + var requiredParameters = record.getEnclosedElements().stream() + .filter(element -> element.getAnnotation(RecordBuilder.Required.class) != null) + .map(element -> ParameterSpec + .builder(TypeName.get(element.asType()), element.getSimpleName().toString()).build()) + .toList(); + var codeBuilder = CodeBlock.builder().add("return new $T()", builderClassType.typeName()); + IntStream.range(0, requiredParameters.size()).forEach(index -> { + codeBuilder.add(".$L($L)", requiredParameters.get(index).name, requiredParameters.get(index).name); + }); + var methodSpec = MethodSpec.methodBuilder(metaData.builderMethodName()).addParameters(requiredParameters) + .addJavadoc("Return a new builder with all fields set to the values taken from the given parameters\n") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC).addAnnotation(generatedRecordBuilderAnnotation) + .addTypeVariables(typeVariables).returns(builderClassType.typeName()).addStatement(codeBuilder.build()) + .build(); + builder.addMethod(methodSpec); + } + private void addStaticStagedBuilderMethod(String builderMethodName) { /* * Adds the staged builder method similar to: diff --git a/record-builder-test/src/main/java/io/soabase/recordbuilder/test/RequiredComponents.java b/record-builder-test/src/main/java/io/soabase/recordbuilder/test/RequiredComponents.java new file mode 100644 index 00000000..f29cc741 --- /dev/null +++ b/record-builder-test/src/main/java/io/soabase/recordbuilder/test/RequiredComponents.java @@ -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; + +import io.soabase.recordbuilder.core.RecordBuilder; + +import java.util.Optional; + +@RecordBuilder +public record RequiredComponents(@RecordBuilder.Required int a, @RecordBuilder.Required String b, float c, + Optional d) { +} diff --git a/record-builder-test/src/test/java/io/soabase/recordbuilder/test/TestRequiredComponents.java b/record-builder-test/src/test/java/io/soabase/recordbuilder/test/TestRequiredComponents.java new file mode 100644 index 00000000..49280558 --- /dev/null +++ b/record-builder-test/src/test/java/io/soabase/recordbuilder/test/TestRequiredComponents.java @@ -0,0 +1,30 @@ +/* + * 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; + +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class TestRequiredComponents { + @Test + void testRequiredComponents() { + var obj = RequiredComponentsBuilder.builder(100, "hello").build(); + assertEquals(new RequiredComponents(100, "hello", 0.0f, Optional.empty()), obj); + } +} From 1afbdb6e6712ab0439fc3df643ce941a3c994f0a Mon Sep 17 00:00:00 2001 From: Fredy Wijaya Date: Mon, 21 Apr 2025 11:55:01 -0500 Subject: [PATCH 2/2] Do not generate no-arg builder() --- .../processor/InternalRecordBuilderProcessor.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 87a0d0ea..358f366e 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 @@ -107,10 +107,11 @@ class InternalRecordBuilderProcessor { } if ((metaData.builderMode() != BuilderMode.STAGED) && (metaData.builderMode() != BuilderMode.STAGED_REQUIRED_ONLY)) { - addStaticDefaultBuilderMethod(); if (record.getEnclosedElements().stream() .anyMatch(element -> element.getAnnotation(RecordBuilder.Required.class) != null)) { addStaticRequiredComponentsBuilderMethod(record); + } else { + addStaticDefaultBuilderMethod(); } } addStaticCopyBuilderMethod();