diff --git a/options.md b/options.md index 8825c40a..015ae135 100644 --- a/options.md +++ b/options.md @@ -54,19 +54,20 @@ The names used for generated methods, classes, etc. can be changed via the follo ## Miscellaneous -| option | details | -|----------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `@RecordBuilder.Options(inheritComponentAnnotations = true/false)` | If true, any annotations (if applicable) on record components are copied to the builder methods. The default is `true`. | -| `@RecordBuilder.Options(publicBuilderConstructors = true/false)` | Makes the generated builder's constructors public. The default is `false`. | -| `@RecordBuilder.Options(builderClassModifiers = {}})` | Any additional `javax.lang.model.element.Modifier` you wish to apply to the builder. | -| `@RecordBuilder.Options(beanClassName = "Foo")` | If set, the Builder will contain an internal interface with this name. | -| `@RecordBuilder.Options(addClassRetainedGenerated = true/false)` | If true, generated classes are annotated with `RecordBuilderGenerated`. The default is `false`. | -| `@RecordBuilder.Options(addStaticBuilder = true/false)` | If true, a functional-style builder is added so that record instances can be instantiated without `new()`. The default is `true`. | -| `@RecordBuilder.Options(inheritComponentAnnotations = true/false)` | If true, any annotations (if applicable) on record components are copied to the builder methods. The default is `true`. | -| `@RecordBuilder.Options(addConcreteSettersForOptional = true/false)` | Add non-optional setter methods for optional record components. The default is `false`. | -| `@RecordBuilder.Options(useValidationApi = true/false)` | Pass built records through the Java Validation API if it's available in the classpath. The default is `false`. | -| `@RecordBuilder.Options(builderMode = BuilderMode.XXX)` | Whether to add standard builder, staged builder or both. The default is `BuilderMode.STANDARD`. | -| `@RecordBuilder.Options(onceOnlyAssignment = true/false)` | If true, attributes can be set/assigned only 1 time. Attempts to reassign/reset attributes will throw `java.lang.IllegalStateException`. The default is `false`. | +| option | details | +|----------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `@RecordBuilder.Options(inheritComponentAnnotations = true/false)` | If true, any annotations (if applicable) on record components are copied to the builder methods. The default is `true`. | +| `@RecordBuilder.Options(publicBuilderConstructors = true/false)` | Makes the generated builder's constructors public. The default is `false`. | +| `@RecordBuilder.Options(parameterizedBuilder = true/false)` | Parameterizes the generated builder on the builder type itself. Builder setters will return the type parameter. This is useful when extending the builder. The default is `false`. | | +| `@RecordBuilder.Options(builderClassModifiers = {}})` | Any additional `javax.lang.model.element.Modifier` you wish to apply to the builder. | +| `@RecordBuilder.Options(beanClassName = "Foo")` | If set, the Builder will contain an internal interface with this name. | +| `@RecordBuilder.Options(addClassRetainedGenerated = true/false)` | If true, generated classes are annotated with `RecordBuilderGenerated`. The default is `false`. | +| `@RecordBuilder.Options(addStaticBuilder = true/false)` | If true, a functional-style builder is added so that record instances can be instantiated without `new()`. The default is `true`. | +| `@RecordBuilder.Options(inheritComponentAnnotations = true/false)` | If true, any annotations (if applicable) on record components are copied to the builder methods. The default is `true`. | +| `@RecordBuilder.Options(addConcreteSettersForOptional = true/false)` | Add non-optional setter methods for optional record components. The default is `false`. | +| `@RecordBuilder.Options(useValidationApi = true/false)` | Pass built records through the Java Validation API if it's available in the classpath. The default is `false`. | +| `@RecordBuilder.Options(builderMode = BuilderMode.XXX)` | Whether to add standard builder, staged builder or both. The default is `BuilderMode.STANDARD`. | +| `@RecordBuilder.Options(onceOnlyAssignment = true/false)` | If true, attributes can be set/assigned only 1 time. Attempts to reassign/reset attributes will throw `java.lang.IllegalStateException`. The default is `false`. | ### Staged Builders 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 c604ba3e..bc0a8f79 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 @@ -307,6 +307,12 @@ */ boolean publicBuilderConstructors() default false; + /** + * Parameterizes the generated builder on the builder type itself. Builder setters will return the type + * parameter. This is useful when extending the builder. + */ + boolean parameterizedBuilder() default false; + /** * Whether to add standard builder, staged builder or both */ diff --git a/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/ElementUtils.java b/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/ElementUtils.java index cb6fcec9..fcedfe28 100644 --- a/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/ElementUtils.java +++ b/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/ElementUtils.java @@ -108,7 +108,7 @@ public static ClassType getClassType(ClassName builderClassName, if (typeParameters.isEmpty()) { return new ClassType(builderClassName, builderClassName.simpleName()); } - TypeName[] typeNames = typeParameters.stream().map(TypeVariableName::get).toArray(TypeName[]::new); + TypeName[] typeNames = toTypeVariableNames(typeParameters).toArray(TypeName[]::new); return new ClassType(ParameterizedTypeName.get(builderClassName, typeNames), builderClassName.simpleName()); } @@ -121,6 +121,10 @@ public static ClassType getClassTypeFromNames(ClassName builderClassName, return new ClassType(ParameterizedTypeName.get(builderClassName, typeNames), builderClassName.simpleName()); } + public static List toTypeVariableNames(List typeParameters) { + return typeParameters.stream().map(TypeVariableName::get).toList(); + } + public static RecordClassType getRecordClassType(ProcessingEnvironment processingEnv, RecordComponentElement recordComponent, List accessorAnnotations, List canonicalConstructorAnnotations) { 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 f47ebaf1..8d6034c7 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 @@ -42,9 +42,12 @@ class InternalRecordBuilderProcessor { private final ClassType recordClassType; private final String packageName; private final ClassType builderClassType; - private final List typeVariables; + private final ClassType builderClassTypeWildcard; + private final List recordTypeVariables; + private final List builderTypeVariables; private final List recordComponents; private final TypeSpec builderType; + private final TypeName setterReturnType; private final TypeSpec.Builder builder; private final String uniqueVarName; private final Pattern notNullPattern; @@ -66,9 +69,28 @@ class InternalRecordBuilderProcessor { this.metaData = metaData; recordClassType = ElementUtils.getClassType(record, record.getTypeParameters()); packageName = packageNameOpt.orElse(recordActualPackage); - builderClassType = ElementUtils.getClassType(packageName, - getBuilderName(record, metaData, recordClassType, metaData.suffix()), record.getTypeParameters()); - typeVariables = record.getTypeParameters().stream().map(TypeVariableName::get).collect(Collectors.toList()); + + String builderName = getBuilderName(record, metaData, recordClassType, metaData.suffix()); + recordTypeVariables = toTypeVariableNames(record.getTypeParameters()); + List builderTypeParams = new ArrayList<>(recordTypeVariables); + List builderTypeParamsWildcard = new ArrayList<>(recordTypeVariables); + var self = TypeVariableName.get("SELF"); + ClassName builderClassName = ClassName.get(packageName, builderName); + if (metaData.parameterizedBuilder()) { + List selfBounds = new ArrayList<>(recordTypeVariables); + selfBounds.add(self); + builderTypeParams.add(TypeVariableName.get("SELF", + ParameterizedTypeName.get(builderClassName, selfBounds.toArray(new TypeName[0])))); + builderTypeParamsWildcard.add(TypeVariableName.get("?")); + } + builderTypeVariables = builderTypeParams; + builderClassType = ElementUtils.getClassTypeFromNames(builderClassName, builderTypeParams); + builderClassTypeWildcard = ElementUtils.getClassTypeFromNames(builderClassName, builderTypeParamsWildcard); + if (metaData.parameterizedBuilder()) { + setterReturnType = self; + } else { + setterReturnType = builderClassType.typeName(); + } recordComponents = buildRecordComponents(record); uniqueVarName = getUniqueVarName(); notNullPattern = Pattern.compile(metaData.interpretNotNullsPattern()); @@ -77,7 +99,7 @@ class InternalRecordBuilderProcessor { initializers = InitializerUtil.detectInitializers(processingEnv, record); builder = TypeSpec.classBuilder(builderClassType.name()).addAnnotation(generatedRecordBuilderAnnotation) - .addModifiers(metaData.builderClassModifiers()).addTypeVariables(typeVariables); + .addModifiers(metaData.builderClassModifiers()).addTypeVariables(builderTypeVariables); if (metaData.addClassRetainedGenerated()) { builder.addAnnotation(recordBuilderGeneratedAnnotation); } @@ -216,7 +238,7 @@ private void addStagedBuilderClasses() { var classBuilder = TypeSpec.interfaceBuilder(stagedBuilderName(builderClassType)) .addAnnotation(generatedRecordBuilderAnnotation) .addJavadoc("Add final staged builder to {@code $L}\n", recordClassType.name()) - .addModifiers(Modifier.PUBLIC).addTypeVariables(typeVariables); + .addModifiers(Modifier.PUBLIC).addTypeVariables(recordTypeVariables); if (metaData.addClassRetainedGenerated()) { classBuilder.addAnnotation(recordBuilderGeneratedAnnotation); } @@ -228,7 +250,7 @@ private void addStagedBuilderClasses() { 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(); + .returns(builderClassTypeWildcard.typeName()).build(); classBuilder.addMethod(builderMethod); builder.addType(classBuilder.build()); @@ -244,7 +266,7 @@ private void add1StagedBuilderClass(RecordClassType component, Optional { if (prefixedName(component, true).equals(component.name())) { return; @@ -319,9 +341,10 @@ private void addWithSuppliedBuilderMethod(TypeSpec.Builder classBuilder) { * default MyRecord with(Consumer consumer) { MyRecordBuilder builder = with(); * consumer.accept(builder); return builder.build(); } */ - var codeBlockBuilder = CodeBlock.builder().add("$T builder = with();\n", builderClassType.typeName()) + var codeBlockBuilder = CodeBlock.builder().add("$T builder = with();\n", builderClassTypeWildcard.typeName()) .add("consumer.accept(builder);\n").add("return builder.$L();\n", metaData.buildMethodName()); - var consumerType = ParameterizedTypeName.get(ClassName.get(Consumer.class), builderClassType.typeName()); + var consumerType = ParameterizedTypeName.get(ClassName.get(Consumer.class), + builderClassTypeWildcard.typeName()); var parameter = ParameterSpec.builder(consumerType, "consumer").build(); var methodSpec = MethodSpec.methodBuilder(metaData.withClassMethodPrefix()) .addAnnotation(generatedRecordBuilderAnnotation) @@ -338,13 +361,13 @@ private void addWithBuilderMethod(TypeSpec.Builder classBuilder) { * default MyRecordBuilder with() { return MyRecordBuilder.builder(r); } */ var codeBlockBuilder = CodeBlock.builder().add("return new $L$L(", builderClassType.name(), - typeVariables.isEmpty() ? "" : "<>"); + builderTypeVariables.isEmpty() ? "" : "<>"); addComponentCallsAsArguments(-1, codeBlockBuilder, false); codeBlockBuilder.add(");"); var methodSpec = MethodSpec.methodBuilder(metaData.withClassMethodPrefix()) .addAnnotation(generatedRecordBuilderAnnotation) .addJavadoc("Return a new record builder using the current values") - .addModifiers(Modifier.PUBLIC, Modifier.DEFAULT).returns(builderClassType.typeName()) + .addModifiers(Modifier.PUBLIC, Modifier.DEFAULT).returns(builderClassTypeWildcard.typeName()) .addCode(codeBlockBuilder.build()).build(); classBuilder.addMethod(methodSpec); } @@ -442,7 +465,7 @@ private void addStaticBuilder() { CodeBlock codeBlock = buildCodeBlock(); var builder = MethodSpec.methodBuilder(recordClassType.name()) .addJavadoc("Static constructor/builder. Can be used instead of new $L(...)\n", recordClassType.name()) - .addTypeVariables(typeVariables).addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addTypeVariables(builderTypeVariables).addModifiers(Modifier.PUBLIC, Modifier.STATIC) .addAnnotation(generatedRecordBuilderAnnotation).returns(recordClassType.typeName()).addCode(codeBlock); recordComponents.forEach(component -> { var parameterSpecBuilder = ParameterSpec.builder(component.typeName(), component.name()); @@ -544,10 +567,10 @@ private void addEqualsMethod() { */ var codeBuilder = CodeBlock.builder(); codeBuilder.add("return (this == o) || ("); - if (typeVariables.isEmpty()) { + if (builderTypeVariables.isEmpty()) { codeBuilder.add("(o instanceof $L $L)", builderClassType.name(), uniqueVarName); } else { - String wildcardList = typeVariables.stream().map(__ -> "?").collect(Collectors.joining(",")); + String wildcardList = builderTypeVariables.stream().map(__ -> "?").collect(Collectors.joining(",")); codeBuilder.add("(o instanceof $L<$L> $L)", builderClassType.name(), wildcardList, uniqueVarName); } recordComponents.forEach(recordComponent -> { @@ -622,10 +645,10 @@ private CodeBlock buildCodeBlock() { private TypeName buildWithTypeName() { ClassName rawTypeName = ClassName.get(packageName, builderClassType.name() + "." + metaData.withClassName()); - if (typeVariables.isEmpty()) { + if (recordTypeVariables.isEmpty()) { return rawTypeName; } - return ParameterizedTypeName.get(rawTypeName, typeVariables.toArray(new TypeName[] {})); + return ParameterizedTypeName.get(rawTypeName, recordTypeVariables.toArray(new TypeName[] {})); } private void addFromWithClass() { @@ -641,7 +664,7 @@ private void addFromWithClass() { var fromWithClassBuilder = TypeSpec.classBuilder(metaData.fromWithClassName()) .addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL) - .addAnnotation(generatedRecordBuilderAnnotation).addTypeVariables(typeVariables) + .addAnnotation(generatedRecordBuilderAnnotation).addTypeVariables(recordTypeVariables) .addSuperinterface(buildWithTypeName()); if (metaData.addClassRetainedGenerated()) { fromWithClassBuilder.addAnnotation(recordBuilderGeneratedAnnotation); @@ -676,9 +699,10 @@ private void addStaticFromWithMethod() { var methodSpec = MethodSpec.methodBuilder(metaData.fromMethodName()) .addJavadoc("Return a \"with\"er for an existing record instance\n") .addModifiers(Modifier.PUBLIC, Modifier.STATIC).addAnnotation(generatedRecordBuilderAnnotation) - .addTypeVariables(typeVariables).addParameter(recordClassType.typeName(), metaData.fromMethodName()) - .returns(buildWithTypeName()).addStatement("return new $L$L(from)", metaData.fromWithClassName(), - typeVariables.isEmpty() ? "" : "<>") + .addTypeVariables(recordTypeVariables) + .addParameter(recordClassType.typeName(), metaData.fromMethodName()).returns(buildWithTypeName()) + .addStatement("return new $L$L(from)", metaData.fromWithClassName(), + recordTypeVariables.isEmpty() ? "" : "<>") .build(); builder.addMethod(methodSpec); } @@ -703,7 +727,7 @@ private void addStaticCopyBuilderMethod() { .addJavadoc( "Return a new builder with all fields set to the values taken from the given record instance\n") .addModifiers(Modifier.PUBLIC, Modifier.STATIC).addAnnotation(generatedRecordBuilderAnnotation) - .addTypeVariables(typeVariables).addParameter(recordClassType.typeName(), "from") + .addTypeVariables(builderTypeVariables).addParameter(recordClassType.typeName(), "from") .returns(builderClassType.typeName()).addStatement(codeBuilder.build()).build(); builder.addMethod(methodSpec); } @@ -717,7 +741,7 @@ private void addStaticDefaultBuilderMethod() { var methodSpec = MethodSpec.methodBuilder(metaData.builderMethodName()) .addJavadoc("Return a new builder with all fields set to default Java values\n") .addModifiers(Modifier.PUBLIC, Modifier.STATIC).addAnnotation(generatedRecordBuilderAnnotation) - .addTypeVariables(typeVariables).returns(builderClassType.typeName()) + .addTypeVariables(builderTypeVariables).returns(builderClassType.typeName()) .addStatement("return new $T()", builderClassType.typeName()).build(); builder.addMethod(methodSpec); } @@ -760,7 +784,7 @@ private void addStaticStagedBuilderMethod(String builderMethodName) { 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()) + .addTypeVariables(builderTypeVariables).returns(stagedBuilderType(recordComponents.get(0)).typeName()) .addCode(codeBlock.build()).build(); builder.addMethod(methodSpec); } @@ -787,8 +811,8 @@ private void addStaticComponentsMethod() { var methodSpec = MethodSpec.methodBuilder(metaData.componentsMethodName()).addJavadoc( "Return a stream of the record components as map entries keyed with the component name and the value as the component value\n") .addModifiers(Modifier.PUBLIC, Modifier.STATIC).addParameter(recordClassType.typeName(), "record") - .addAnnotation(generatedRecordBuilderAnnotation).addTypeVariables(typeVariables).returns(mapEntryType) - .addStatement(codeBuilder.build()).build(); + .addAnnotation(generatedRecordBuilderAnnotation).addTypeVariables(recordTypeVariables) + .returns(mapEntryType).addStatement(codeBuilder.build()).build(); builder.addMethod(methodSpec); } @@ -991,7 +1015,7 @@ private void add1SetterMethod(RecordClassType component, int componentIndex) { * public MyRecordBuilder p(T p) { this.p = p; return this; } */ var methodSpec = MethodSpec.methodBuilder(prefixedName(component, false)).addModifiers(Modifier.PUBLIC) - .addAnnotation(generatedRecordBuilderAnnotation).returns(builderClassType.typeName()); + .addAnnotation(generatedRecordBuilderAnnotation).returns(setterReturnType); if (metaData.onceOnlyAssignment()) { var onceOnlyCheck = CodeBlock.builder() @@ -1019,7 +1043,12 @@ private void add1SetterMethod(RecordClassType component, int componentIndex) { }); addConstructorAnnotations(component, parameterSpecBuilder); - methodSpec.addStatement("return this").addParameter(parameterSpecBuilder.build()); + if (metaData.parameterizedBuilder()) { + methodSpec.addStatement("return (SELF) this"); + } else { + methodSpec.addStatement("return this"); + } + methodSpec.addParameter(parameterSpecBuilder.build()); builder.addMethod(methodSpec.build()); } @@ -1058,7 +1087,7 @@ private String getOptionalStatement(OptionalType type) { private List typeVariablesWithReturn() { var variables = new ArrayList(); variables.add(rType); - variables.addAll(typeVariables); + variables.addAll(builderTypeVariables); return variables; } @@ -1068,7 +1097,7 @@ private MethodSpec buildFunctionalHandler(String className, String methodName, b * * default R map(Function proc) { return proc.apply(p()); } */ - var localTypeVariables = isMap ? typeVariablesWithReturn() : typeVariables; + var localTypeVariables = isMap ? typeVariablesWithReturn() : builderTypeVariables; var typeName = localTypeVariables.isEmpty() ? ClassName.get("", className) : ParameterizedTypeName.get(ClassName.get("", className), localTypeVariables.toArray(TypeName[]::new)); var methodBuilder = MethodSpec.methodBuilder(methodName).addAnnotation(generatedRecordBuilderAnnotation) @@ -1095,7 +1124,7 @@ private TypeSpec buildFunctionalInterface(String className, boolean isMap) { * * @FunctionalInterface interface Function { R apply(T a); } */ - var localTypeVariables = isMap ? typeVariablesWithReturn() : typeVariables; + var localTypeVariables = isMap ? typeVariablesWithReturn() : builderTypeVariables; var methodBuilder = MethodSpec.methodBuilder("apply").addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT); recordComponents.forEach(component -> { var parameterSpecBuilder = ParameterSpec.builder(component.typeName(), component.name()); @@ -1128,6 +1157,6 @@ private String stagedBuilderName(ClassType component) { } private ClassType stagedBuilderType(ClassType component) { - return getClassTypeFromNames(ClassName.get("", stagedBuilderName(component)), typeVariables); + return getClassTypeFromNames(ClassName.get("", stagedBuilderName(component)), recordTypeVariables); } } diff --git a/record-builder-test/src/main/java/io/soabase/recordbuilder/test/ParameterizedBuilder.java b/record-builder-test/src/main/java/io/soabase/recordbuilder/test/ParameterizedBuilder.java new file mode 100644 index 00000000..2fba5a68 --- /dev/null +++ b/record-builder-test/src/main/java/io/soabase/recordbuilder/test/ParameterizedBuilder.java @@ -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; + +import io.soabase.recordbuilder.core.RecordBuilder; + +@RecordBuilder.Options(parameterizedBuilder = true, publicBuilderConstructors = true, builderMode = RecordBuilder.BuilderMode.STANDARD_AND_STAGED) +@RecordBuilder +public record ParameterizedBuilder(String foo) { +} diff --git a/record-builder-test/src/main/java/io/soabase/recordbuilder/test/ParameterizedGenericBuilder.java b/record-builder-test/src/main/java/io/soabase/recordbuilder/test/ParameterizedGenericBuilder.java new file mode 100644 index 00000000..23edc1e2 --- /dev/null +++ b/record-builder-test/src/main/java/io/soabase/recordbuilder/test/ParameterizedGenericBuilder.java @@ -0,0 +1,24 @@ +/* + * 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; + +@RecordBuilder.Options(parameterizedBuilder = true, publicBuilderConstructors = true, builderMode = RecordBuilder.BuilderMode.STANDARD_AND_STAGED) +@RecordBuilder +public record ParameterizedGenericBuilder(String foo, T bar) + implements ParameterizedGenericBuilderBuilder.With { +} diff --git a/record-builder-test/src/test/java/io/soabase/recordbuilder/test/TestParameterizedBuilder.java b/record-builder-test/src/test/java/io/soabase/recordbuilder/test/TestParameterizedBuilder.java new file mode 100644 index 00000000..5e9f8ee3 --- /dev/null +++ b/record-builder-test/src/test/java/io/soabase/recordbuilder/test/TestParameterizedBuilder.java @@ -0,0 +1,50 @@ +/* + * 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.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; + +class TestParameterizedBuilder { + @Test + void testNoSubclass() { + ParameterizedBuilderBuilder builder = ParameterizedBuilderBuilder.builder(); + builder = builder.foo("foo"); + ParameterizedBuilder record = builder.build(); + Assertions.assertEquals("foo", record.foo()); + } + + static final class SpecialBuilder extends ParameterizedBuilderBuilder { + SpecialBuilder doubleSetFoo(String s) { + foo(s + s); + return this; + } + } + + @Test + void testWithSubclass() { + SpecialBuilder builder = new SpecialBuilder(); + builder = builder.foo("bar"); // ensure foo returns SpecialBuilder + builder = builder.doubleSetFoo("foo"); + ParameterizedBuilder record = builder.build(); + Assertions.assertEquals("foofoo", record.foo()); + } +} diff --git a/record-builder-test/src/test/java/io/soabase/recordbuilder/test/TestParameterizedGenericBuilder.java b/record-builder-test/src/test/java/io/soabase/recordbuilder/test/TestParameterizedGenericBuilder.java new file mode 100644 index 00000000..6413bf1a --- /dev/null +++ b/record-builder-test/src/test/java/io/soabase/recordbuilder/test/TestParameterizedGenericBuilder.java @@ -0,0 +1,98 @@ +/* + * 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.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +class TestParameterizedGenericBuilder { + @Test + void testNoSubclass() { + ParameterizedGenericBuilderBuilder builder = ParameterizedGenericBuilderBuilder.builder(); + builder = builder.bar(1); + builder = builder.foo("foo"); + ParameterizedGenericBuilder record = builder.build(); + Assertions.assertEquals("foo", record.foo()); + Assertions.assertEquals(1, record.bar()); + } + + static final class SpecialBuilder + extends ParameterizedGenericBuilderBuilder> { + SpecialBuilder doubleSetFoo(String s) { + foo(s + s); + return this; + } + + SpecialBuilder setIfPositive(T t) { + if (t.doubleValue() > 0) { + bar(t); + } + + return this; + } + } + + @Test + void testWithSubclass() { + SpecialBuilder builder = new SpecialBuilder<>(); + builder = builder.foo("baz"); // ensure foo returns SpecialBuilder + builder = builder.doubleSetFoo("foo"); + builder = builder.bar(-3); + builder = builder.setIfPositive(4); + ParameterizedGenericBuilder record = builder.build(); + Assertions.assertEquals("foofoo", record.foo()); + Assertions.assertEquals(4, record.bar()); + } + + @Test + void testWither() { + ParameterizedGenericBuilderBuilder builder = new ParameterizedGenericBuilder<>("foo", 1).with(); + builder.bar(2); + ParameterizedGenericBuilder record = builder.build(); + Assertions.assertEquals(2, record.bar()); + + record = record.withFoo("foofoo"); + Assertions.assertEquals("foofoo", record.foo()); + + record = record.with(b -> b.bar(4)); + Assertions.assertEquals(4, record.bar()); + } + + @Test + void testFrom() { + ParameterizedGenericBuilderBuilder.With wither = ParameterizedGenericBuilderBuilder + .from(new ParameterizedGenericBuilder<>("foo", 1)); + ParameterizedGenericBuilder record = wither.withFoo("foofoo"); + Assertions.assertEquals("foofoo", record.foo()); + } + + @Test + void testStream() { + ParameterizedGenericBuilder record = new ParameterizedGenericBuilder<>("foo", 1); + var list = ParameterizedGenericBuilderBuilder.stream(record).map(Map.Entry::getValue).toList(); + Assertions.assertEquals(List.of("foo", 1), list); + } + + @Test + void testStaged() { + ParameterizedGenericBuilderBuilder.FooStage stage = ParameterizedGenericBuilderBuilder.stagedBuilder(); + ParameterizedGenericBuilder record = stage.foo("foo").bar(1).build(); + Assertions.assertEquals("foo", record.foo()); + } +}