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 b448001..7d3b3b5 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 @@ -187,6 +187,38 @@ public @interface RecordBuilder { * When enabled, adds functional methods to the nested "With" class (such as {@code map()} and {@code accept()}). */ boolean addFunctionalMethodsToWith() default false; + + /** + * If set, all builder setter methods will be prefixed with this string. Camel-casing will + * still be enforced, so if this option is set to "set" a field named "myField" will get + * a corresponding setter named "setMyField". + */ + String setterPrefix() default ""; + + /** + * If set, all builder getter methods will be prefixed with this string. Camel-casing will + * still be enforced, so if this option is set to "get", a field named "myField" will get + * a corresponding getter named "getMyField". + */ + String getterPrefix() default ""; + + /** + * If set, all boolean builder getter methods will be prefixed with this string. + * Camel-casing will still be enforced, so if this option is set to "is", a field named + * "myField" will get a corresponding getter named "isMyField". + */ + String booleanPrefix() default ""; + + /** + * If set, the Builder will contain an internal interface with this name. This interface + * contains getters for all the fields in the Record prefixed with the value supplied in + * {@link this.getterPrefix} and {@link this.booleanPrefix}. This interface can be + * implemented by the original Record to have proper bean-style prefixed getters. + * + * Please note that unless either of the aforementioned prefixes are set, + * this option does nothing. + */ + String beanClassName() default ""; } @Retention(RetentionPolicy.CLASS) 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 9cb18ac..aff7b09 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 @@ -21,6 +21,7 @@ import io.soabase.recordbuilder.core.RecordBuilder; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.*; import java.util.*; +import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -72,6 +73,9 @@ class InternalRecordBuilderProcessor { .addTypeVariables(typeVariables); addVisibility(recordActualPackage.equals(packageName), record.getModifiers()); addWithNestedClass(); + if (!metaData.beanClassName().isEmpty()) { + addBeanNestedClass(); + } addDefaultConstructor(); addStaticBuilder(); if (recordComponents.size() > 0) { @@ -147,7 +151,7 @@ class InternalRecordBuilderProcessor { .addJavadoc("Add withers to {@code $L}\n", recordClassType.name()) .addModifiers(Modifier.PUBLIC) .addTypeVariables(typeVariables); - recordComponents.forEach(component -> addNestedGetterMethod(classBuilder, component)); + recordComponents.forEach(component -> addNestedGetterMethod(classBuilder, component, prefixedName(component, true))); addWithBuilderMethod(classBuilder); addWithSuppliedBuilderMethod(classBuilder); IntStream.range(0, recordComponents.size()).forEach(index -> add1WithMethod(classBuilder, recordComponents.get(index), index)); @@ -160,6 +164,31 @@ class InternalRecordBuilderProcessor { builder.addType(classBuilder.build()); } + private void addBeanNestedClass() { + /* + Adds a nested interface that adds getters similar to: + + public class MyRecordBuilder { + public interface Bean { + // getter methods + } + } + */ + var classBuilder = TypeSpec.interfaceBuilder(metaData.beanClassName()) + .addAnnotation(generatedRecordBuilderAnnotation) + .addJavadoc("Add getters to {@code $L}\n", recordClassType.name()) + .addModifiers(Modifier.PUBLIC) + .addTypeVariables(typeVariables); + recordComponents.forEach(component -> { + if (prefixedName(component, true).equals(component.name())) { + return; + } + addNestedGetterMethod(classBuilder, component, component.name()); + add1PrefixedGetterMethod(classBuilder, component); + }); + builder.addType(classBuilder.build()); + } + private void addWithSuppliedBuilderMethod(TypeSpec.Builder classBuilder) { /* Adds a method that returns a pre-filled copy builder similar to: @@ -257,6 +286,28 @@ class InternalRecordBuilderProcessor { classBuilder.addMethod(methodSpec); } + private void add1PrefixedGetterMethod(TypeSpec.Builder classBuilder, RecordClassType component) { + /* + Adds a get method for the component similar to: + + default MyRecord getName() { + return name(); + } + */ + var codeBlockBuilder = CodeBlock.builder(); + codeBlockBuilder.add("$[return $L()$];", component.name()); + + var methodName = prefixedName(component, true); + var methodSpec = MethodSpec.methodBuilder(methodName) + .addAnnotation(generatedRecordBuilderAnnotation) + .addJavadoc("Returns the value of {@code $L}\n", component.name()) + .addModifiers(Modifier.PUBLIC, Modifier.DEFAULT) + .addCode(codeBlockBuilder.build()) + .returns(component.typeName()) + .build(); + classBuilder.addMethod(methodSpec); + } + private void addComponentCallsAsArguments(int index, CodeBlock.Builder codeBlockBuilder) { IntStream.range(0, recordComponents.size()).forEach(parameterIndex -> { if (parameterIndex > 0) { @@ -266,7 +317,7 @@ class InternalRecordBuilderProcessor { if (parameterIndex == index) { collectionBuilderUtils.add(codeBlockBuilder, parameterComponent); } else { - codeBlockBuilder.add("$L()", parameterComponent.name()); + codeBlockBuilder.add("$L()", prefixedName(parameterComponent, true)); } }); } @@ -530,7 +581,7 @@ class InternalRecordBuilderProcessor { codeBuilder.add("\n"); } codeBuilder.add("@Override\n") - .add("public $T $L() {\n", component.typeName(), component.name()) + .add("public $T $L() {\n", component.typeName(), prefixedName(component, true)) .indent() .addStatement("return from.$L()", component.name()) .unindent() @@ -664,13 +715,13 @@ class InternalRecordBuilderProcessor { return (component.typeName() instanceof ParameterizedTypeName parameterizedTypeName) && parameterizedTypeName.rawType.equals(optionalType); } - private void addNestedGetterMethod(TypeSpec.Builder classBuilder, RecordClassType component) { + private void addNestedGetterMethod(TypeSpec.Builder classBuilder, RecordClassType component, String methodName) { /* For a single record component, add a getter similar to: T p(); */ - var methodSpecBuilder = MethodSpec.methodBuilder(component.name()) + var methodSpecBuilder = MethodSpec.methodBuilder(methodName) .addJavadoc("Return the current value for the {@code $L} record component in the builder\n", component.name()) .addModifiers(Modifier.ABSTRACT, Modifier.PUBLIC) .addAnnotation(generatedRecordBuilderAnnotation) @@ -834,7 +885,7 @@ class InternalRecordBuilderProcessor { return p; } */ - var methodSpecBuilder = MethodSpec.methodBuilder(component.name()) + var methodSpecBuilder = MethodSpec.methodBuilder(prefixedName(component, true)) .addJavadoc("Return the current value for the {@code $L} record component in the builder\n", component.name()) .addModifiers(Modifier.PUBLIC) .addAnnotation(generatedRecordBuilderAnnotation) @@ -853,8 +904,7 @@ class InternalRecordBuilderProcessor { return this; } */ - - var methodSpec = MethodSpec.methodBuilder(component.name()) + var methodSpec = MethodSpec.methodBuilder(prefixedName(component, false)) .addModifiers(Modifier.PUBLIC) .addAnnotation(generatedRecordBuilderAnnotation) .returns(builderClassType.typeName()); @@ -940,5 +990,18 @@ class InternalRecordBuilderProcessor { .addMethod(methodBuilder.build()) .build(); } + + private String prefixedName(RecordClassType component, boolean isGetter) { + BiFunction prefixer = (p, s) -> p.isEmpty() + ? s : p + Character.toUpperCase(s.charAt(0)) + s.substring(1); + boolean isBool = component.typeName().toString().toLowerCase(Locale.ROOT).equals("boolean"); + if (isGetter) { + if (isBool) { + return prefixer.apply(metaData.booleanPrefix(), component.name()); + } + return prefixer.apply(metaData.getterPrefix(), component.name()); + } + return prefixer.apply(metaData.setterPrefix(), component.name()); + } } diff --git a/record-builder-test/src/main/java/io/soabase/recordbuilder/test/CustomMethodNames.java b/record-builder-test/src/main/java/io/soabase/recordbuilder/test/CustomMethodNames.java new file mode 100644 index 0000000..ba43471 --- /dev/null +++ b/record-builder-test/src/main/java/io/soabase/recordbuilder/test/CustomMethodNames.java @@ -0,0 +1,29 @@ +/** + * Copyright 2019 Jordan Zimmerman + * + * 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 java.util.List; +import io.soabase.recordbuilder.core.RecordBuilder; +import io.soabase.recordbuilder.test.CustomMethodNamesBuilder.Bean; + +@RecordBuilder +@RecordBuilder.Options( + setterPrefix = "set", getterPrefix = "get", booleanPrefix = "is", beanClassName = "Bean") +public record CustomMethodNames( + int theValue, + List theList, + boolean theBoolean) implements Bean { +} diff --git a/record-builder-test/src/test/java/io/soabase/recordbuilder/test/TestCustomMethodNames.java b/record-builder-test/src/test/java/io/soabase/recordbuilder/test/TestCustomMethodNames.java new file mode 100644 index 0000000..b3cbe91 --- /dev/null +++ b/record-builder-test/src/test/java/io/soabase/recordbuilder/test/TestCustomMethodNames.java @@ -0,0 +1,57 @@ +/** + * Copyright 2019 Jordan Zimmerman + * + * 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 java.util.List; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class TestCustomMethodNames { + + @Test + public void builderGetsCustomSetterAndGetterNames() { + var obj = CustomMethodNamesBuilder.builder() + .setTheValue(1) + .setTheList(List.of(2)) + .setTheBoolean(true); + assertEquals(1, obj.getTheValue()); + assertEquals(List.of(2), obj.getTheList()); + assertTrue(obj.isTheBoolean()); + assertEquals(new CustomMethodNames(1, List.of(2), true), obj.build()); + } + + @Test + public void withBuilderGetsCustomSetterAndGetterNames() { + var obj = CustomMethodNamesBuilder.from(CustomMethodNamesBuilder.builder() + .setTheValue(1) + .setTheList(List.of(2)) + .setTheBoolean(true) + .build()); + assertEquals(1, obj.getTheValue()); + assertEquals(List.of(2), obj.getTheList()); + assertTrue(obj.isTheBoolean()); + } + + @Test + public void recordHasPrefixedGetters() { + var obj = new CustomMethodNames(1, List.of(2), true); + assertEquals(1, obj.getTheValue()); + assertEquals(List.of(2), obj.getTheList()); + assertTrue(obj.isTheBoolean()); + } +}