From efd1a6b0d458725ad51382ecedbee90cb05dc67c Mon Sep 17 00:00:00 2001 From: Mads Baardsgaard Date: Mon, 21 Mar 2022 10:08:00 +0100 Subject: [PATCH] Add flag to add concrete setters for optionals (#94) * Add flag to add concrete setters for optionals * Add another test with concrete optionals disabled --- .../recordbuilder/core/RecordBuilder.java | 5 ++ .../InternalRecordBuilderProcessor.java | 58 +++++++++++------- .../recordbuilder/processor/OptionalType.java | 61 +++++++++++++++++++ .../test/RecordWithOptional.java | 2 +- .../test/RecordWithOptional2.java | 26 ++++++++ .../recordbuilder/test/TestOptional.java | 32 ++++++++++ 6 files changed, 160 insertions(+), 24 deletions(-) create mode 100644 record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/OptionalType.java create mode 100644 record-builder-test/src/main/java/io/soabase/recordbuilder/test/RecordWithOptional2.java 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 08f06ce8..668804b7 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 @@ -144,6 +144,11 @@ */ boolean emptyDefaultForOptional() default true; + /** + * Add non-optional setter methods for optional record components. + */ + boolean addConcreteSettersForOptional() default false; + /** * Add not-null checks for record components annotated with any annotation named either "NotNull", * "NoNull", or "NonNull" (see {@link #interpretNotNullsPattern()} for the actual regex matching pattern). 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 506cfeae..0098751f 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 @@ -47,10 +47,6 @@ class InternalRecordBuilderProcessor { private final CollectionBuilderUtils collectionBuilderUtils; private static final TypeName overrideType = TypeName.get(Override.class); - private static final TypeName optionalType = TypeName.get(Optional.class); - private static final TypeName optionalIntType = TypeName.get(OptionalInt.class); - 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; @@ -99,6 +95,9 @@ class InternalRecordBuilderProcessor { if (metaData.enableGetters()) { add1GetterMethod(component); } + if (metaData.addConcreteSettersForOptional()) { + add1ConcreteOptionalSetterMethod(component); + } var collectionMetaData = collectionBuilderUtils.singleItemsMetaData(component, EXCLUDE_WILDCARD_TYPES); collectionMetaData.ifPresent(meta -> add1CollectionBuilders(meta, component)); }); @@ -708,31 +707,17 @@ private void add1Field(ClassType component) { */ var fieldSpecBuilder = FieldSpec.builder(component.typeName(), component.name(), Modifier.PRIVATE); if (metaData.emptyDefaultForOptional()) { - TypeName thisOptionalType = null; - if (isOptional(component)) { - thisOptionalType = optionalType; - } else if (component.typeName().equals(optionalIntType)) { - thisOptionalType = optionalIntType; - } else if (component.typeName().equals(optionalLongType)) { - thisOptionalType = optionalLongType; - } else if (component.typeName().equals(optionalDoubleType)) { - thisOptionalType = optionalDoubleType; - } - if (thisOptionalType != null) { - var codeBlock = CodeBlock.builder().add("$T.empty()", thisOptionalType).build(); + Optional thisOptionalType = OptionalType.fromClassType(component); + if (thisOptionalType.isPresent()) { + var codeBlock = CodeBlock.builder() + .add("$T.empty()", thisOptionalType.get().typeName()) + .build(); fieldSpecBuilder.initializer(codeBlock); } } builder.addField(fieldSpecBuilder.build()); } - private boolean isOptional(ClassType component) { - if (component.typeName().equals(optionalType)) { - return true; - } - return (component.typeName() instanceof ParameterizedTypeName parameterizedTypeName) && parameterizedTypeName.rawType.equals(optionalType); - } - private void addNestedGetterMethod(TypeSpec.Builder classBuilder, RecordClassType component, String methodName) { /* For a single record component, add a getter similar to: @@ -944,6 +929,33 @@ public MyRecordBuilder p(T p) { builder.addMethod(methodSpec.build()); } + private void add1ConcreteOptionalSetterMethod(RecordClassType component) { + /* + For a single optional record component, add a concrete setter similar to: + + public MyRecordBuilder p(T p) { + this.p = p; + return this; + } + */ + var optionalType = OptionalType.fromClassType(component); + if (optionalType.isEmpty()) { + return; + } + var type = optionalType.get(); + var methodSpec = MethodSpec.methodBuilder(prefixedName(component, false)) + .addModifiers(Modifier.PUBLIC) + .addAnnotation(generatedRecordBuilderAnnotation) + .returns(builderClassType.typeName()); + + var parameterSpecBuilder = ParameterSpec.builder(type.valueType(), component.name()); + methodSpec.addJavadoc("Set a new value for the {@code $L} record component in the builder\n", component.name()) + .addStatement("this.$L = $T.of($L)", component.name(), type.typeName(), component.name()); + addConstructorAnnotations(component, parameterSpecBuilder); + methodSpec.addStatement("return this").addParameter(parameterSpecBuilder.build()); + builder.addMethod(methodSpec.build()); + } + private List typeVariablesWithReturn() { var variables = new ArrayList(); variables.add(rType); diff --git a/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/OptionalType.java b/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/OptionalType.java new file mode 100644 index 00000000..30ff1387 --- /dev/null +++ b/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/OptionalType.java @@ -0,0 +1,61 @@ +/** + * 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.processor; + +import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeName; + +public record OptionalType(TypeName typeName, TypeName valueType) { + + private static final TypeName optionalType = TypeName.get(Optional.class); + private static final TypeName optionalIntType = TypeName.get(OptionalInt.class); + private static final TypeName optionalLongType = TypeName.get(OptionalLong.class); + private static final TypeName optionalDoubleType = TypeName.get(OptionalDouble.class); + + private static boolean isOptional(ClassType component) { + if (component.typeName().equals(optionalType)) { + return true; + } + return (component.typeName() instanceof ParameterizedTypeName parameterizedTypeName) + && parameterizedTypeName.rawType.equals(optionalType); + } + + static Optional fromClassType(final ClassType component) { + if (isOptional(component)) { + if (!(component.typeName() instanceof ParameterizedTypeName parameterizedType)) { + return Optional.of(new OptionalType(optionalType, TypeName.get(Object.class))); + } + final TypeName containingType = parameterizedType.typeArguments.isEmpty() + ? TypeName.get(Object.class) + : parameterizedType.typeArguments.get(0); + return Optional.of(new OptionalType(optionalType, containingType)); + } + if (component.typeName().equals(optionalIntType)) { + return Optional.of(new OptionalType(optionalIntType, TypeName.get(int.class))); + } + if (component.typeName().equals(optionalLongType)) { + return Optional.of(new OptionalType(optionalLongType, TypeName.get(long.class))); + } + if (component.typeName().equals(optionalDoubleType)) { + return Optional.of(new OptionalType(optionalDoubleType, TypeName.get(double.class))); + } + return Optional.empty(); + } +} diff --git a/record-builder-test/src/main/java/io/soabase/recordbuilder/test/RecordWithOptional.java b/record-builder-test/src/main/java/io/soabase/recordbuilder/test/RecordWithOptional.java index da6b6637..41918679 100644 --- a/record-builder-test/src/main/java/io/soabase/recordbuilder/test/RecordWithOptional.java +++ b/record-builder-test/src/main/java/io/soabase/recordbuilder/test/RecordWithOptional.java @@ -22,6 +22,6 @@ import java.util.OptionalInt; import java.util.OptionalLong; -@RecordBuilder.Options(emptyDefaultForOptional = true) +@RecordBuilder.Options(emptyDefaultForOptional = true, addConcreteSettersForOptional = true) @RecordBuilder public record RecordWithOptional(Optional value, Optional raw, OptionalInt i, OptionalLong l, OptionalDouble d) {} diff --git a/record-builder-test/src/main/java/io/soabase/recordbuilder/test/RecordWithOptional2.java b/record-builder-test/src/main/java/io/soabase/recordbuilder/test/RecordWithOptional2.java new file mode 100644 index 00000000..28e1205a --- /dev/null +++ b/record-builder-test/src/main/java/io/soabase/recordbuilder/test/RecordWithOptional2.java @@ -0,0 +1,26 @@ +/** + * 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.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; +import io.soabase.recordbuilder.core.RecordBuilder; + +@RecordBuilder.Options(emptyDefaultForOptional = true) +@RecordBuilder +public record RecordWithOptional2(Optional value, Optional raw, OptionalInt i, OptionalLong l, OptionalDouble d) {} diff --git a/record-builder-test/src/test/java/io/soabase/recordbuilder/test/TestOptional.java b/record-builder-test/src/test/java/io/soabase/recordbuilder/test/TestOptional.java index 9cf1f8a7..7e2bc377 100644 --- a/record-builder-test/src/test/java/io/soabase/recordbuilder/test/TestOptional.java +++ b/record-builder-test/src/test/java/io/soabase/recordbuilder/test/TestOptional.java @@ -33,4 +33,36 @@ var record = RecordWithOptionalBuilder.builder(); Assertions.assertEquals(OptionalLong.empty(), record.l()); Assertions.assertEquals(OptionalDouble.empty(), record.d()); } + + @Test + void testRawSetters() { + var record = RecordWithOptionalBuilder.builder() + .value("value") + .raw("rawValue") + .i(42) + .l(424242L) + .d(42.42) + .build(); + Assertions.assertEquals(Optional.of("value"), record.value()); + Assertions.assertEquals(Optional.of("rawValue"), record.raw()); + Assertions.assertEquals(OptionalInt.of(42), record.i()); + Assertions.assertEquals(OptionalLong.of(424242L), record.l()); + Assertions.assertEquals(OptionalDouble.of(42.42), record.d()); + } + + @Test + void testOptionalSetters() { + var record = RecordWithOptional2Builder.builder() + .value(Optional.of("value")) + .raw(Optional.of("rawValue")) + .i(OptionalInt.of(42)) + .l(OptionalLong.of(424242L)) + .d(OptionalDouble.of(42.42)) + .build(); + Assertions.assertEquals(Optional.of("value"), record.value()); + Assertions.assertEquals(Optional.of("rawValue"), record.raw()); + Assertions.assertEquals(OptionalInt.of(42), record.i()); + Assertions.assertEquals(OptionalLong.of(424242L), record.l()); + Assertions.assertEquals(OptionalDouble.of(42.42), record.d()); + } }