diff --git a/README.md b/README.md index ca477936b..4aa00b2af 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ _Automatic generation of the Builder pattern for Java 1.8+_ - [Eclipse](#eclipse) - [IntelliJ](#intellij) - [Release notes](#release-notes) + - [2.3—From method testability](#23from-method-testability) - [2.2—Primitive optional types](#22primitive-optional-types) - [2.1—Lists of buildable types](#21lists-of-buildable-types) - [Upgrading from v1](#upgrading-from-v1) @@ -134,7 +135,7 @@ If you write the Person interface shown above, you get: * getters (throwing `IllegalStateException` for unset fields) * setters * lambda-accepting mapper methods - * `from` and `mergeFrom` methods to copy data from existing values or builders + * `mergeFrom` and static `from` methods to copy data from existing values or builders * a `build` method that verifies all fields have been set * [see below for default values and constraint checking](#defaults-and-constraints) * An implementation of `Person` with: @@ -144,8 +145,6 @@ If you write the Person interface shown above, you get: * `UnsupportedOperationException`-throwing getters for unset fields * `toString` * `equals` and `hashCode` - * a special `toBuilder()` method that preserves the 'partial' aspect on newly-minted - Person instances ```java @@ -578,17 +577,20 @@ restrictions of the value type, partials can reduce the fragility of your test suite, allowing you to add new required fields or other constraints to an existing value type without breaking swathes of test code. -To allow robust tests of modify-rebuild code, the `toBuilder()` method on -partials returns a subclass of Builder that returns partials from the `build()` -method. +To allow robust tests of modify-rebuild code, Builders created from partials +(either via the static `Builder.from` method or the optional `toBuilder()` +method) will override `build()` to instead call `buildPartial()`. ```java Person anotherPerson = person.toBuilder().name("Bob").build(); System.out.println(anotherPerson); // prints: partial Person{name=Bob} ``` -(Note the `from` and `mergeFrom` methods do not behave this way; instead, they -will throw an UnsupportedOperationException if given a partial.) +This "infectious" behavior of partials is another reason to confine them to +test code. + +(Note the `mergeFrom` method does not behave this way; instead, it will throw an +UnsupportedOperationException if given a partial.) ### Jackson @@ -725,6 +727,20 @@ directory** setting) and select **Mark Directory As > Generated Sources Root**. Release notes ------------- +### 2.3—From method testability + +FreeBuilder 2.3 now allows partials to be passed to the static `Builder.from` +method. Previously this would have thrown an UnsupportedOperationException +if any field was unset; now, as with the optional `toBuilder` method, a Builder +subclass will be returned that redirects `build` to `buildPartial`. This allows +unit tests to be written that won't break if new constraints or required fields +are later added to the datatype. You can restore the old behaviour by overriding +the `from` method to delegate to `mergeFrom`. + +Note that use of partials outside of tests is considered undefined behaviour +by FreeBuilder, as documented here and on the `buildPartial` method. Incomplete +values should always be represented by Builder instances, not partials. + ### 2.2—Primitive optional types FreeBuilder 2.2 extends its [optional value API customization](#optional-values) diff --git a/src/main/java/org/inferred/freebuilder/processor/Analyser.java b/src/main/java/org/inferred/freebuilder/processor/Analyser.java index 5eaacc1bb..c37fb8354 100644 --- a/src/main/java/org/inferred/freebuilder/processor/Analyser.java +++ b/src/main/java/org/inferred/freebuilder/processor/Analyser.java @@ -146,6 +146,11 @@ GeneratedType analyse(TypeElement type) throws CannotGenerateCodeException { builder, constructionAndExtension.isExtensible(), methods)) .setBuilderSerializable(shouldBuilderBeSerializable(builder)) .setBuilder(Type.from(builder)); + if (datatypeBuilder.getBuilderFactory().isPresent() + && !datatypeBuilder.getHasToBuilderMethod()) { + datatypeBuilder.setRebuildableType( + generatedBuilder.nestedType("Rebuildable").withParameters(typeParameters)); + } Datatype baseDatatype = datatypeBuilder.build(); Map generatorsByProperty = pickPropertyGenerators( type, baseDatatype, builder, removeNonGetterMethods(builder, methods)); diff --git a/src/main/java/org/inferred/freebuilder/processor/BuildableType_Builder.java b/src/main/java/org/inferred/freebuilder/processor/BuildableType_Builder.java index 4a30542c8..97bd3883a 100644 --- a/src/main/java/org/inferred/freebuilder/processor/BuildableType_Builder.java +++ b/src/main/java/org/inferred/freebuilder/processor/BuildableType_Builder.java @@ -22,9 +22,17 @@ @Generated("org.inferred.freebuilder.processor.Processor") abstract class BuildableType_Builder { - /** Creates a new builder using {@code value} as a template. */ + /** + * Creates a new builder using {@code value} as a template. + * + *

If {@code value} is a partial, the builder will return more partials. + */ public static BuildableType.Builder from(BuildableType value) { - return new BuildableType.Builder().mergeFrom(value); + if (value instanceof Rebuildable) { + return ((Rebuildable) value).toBuilder(); + } else { + return new BuildableType.Builder().mergeFrom(value); + } } private enum Property { @@ -380,6 +388,10 @@ public BuildableType build() { * will not be performed. Unset properties will throw an {@link UnsupportedOperationException} * when accessed via the partial object. * + *

The builder returned by {@link BuildableType.Builder#from(BuildableType)} will propagate the + * partial status of its input, overriding {@link BuildableType.Builder#build() build()} to return + * another partial. This allows for robust tests of modify-rebuild code. + * *

Partials should only ever be used in tests. They permit writing robust test cases that won't * fail if this type gains more application-level constraints (e.g. new required fields) in * future. If you require partially complete values in production code, consider using a Builder. @@ -389,7 +401,11 @@ public BuildableType buildPartial() { return new Partial(this); } - private static final class Value extends BuildableType { + private abstract static class Rebuildable extends BuildableType { + public abstract Builder toBuilder(); + } + + private static final class Value extends Rebuildable { private final Type type; private final Type builderType; private final MergeBuilderMethod mergeBuilder; @@ -436,6 +452,19 @@ public Excerpt suppressUnchecked() { return suppressUnchecked; } + @Override + public Builder toBuilder() { + BuildableType_Builder builder = new Builder(); + builder.type = type; + builder.builderType = builderType; + builder.mergeBuilder = mergeBuilder; + builder.partialToBuilder = partialToBuilder; + builder.builderFactory = builderFactory; + builder.suppressUnchecked = suppressUnchecked; + builder._unsetProperties.clear(); + return (Builder) builder; + } + @Override public boolean equals(Object obj) { if (!(obj instanceof Value)) { @@ -474,7 +503,7 @@ public String toString() { } } - private static final class Partial extends BuildableType { + private static final class Partial extends Rebuildable { private final Type type; private final Type builderType; private final MergeBuilderMethod mergeBuilder; @@ -541,6 +570,27 @@ public Excerpt suppressUnchecked() { return suppressUnchecked; } + private static class PartialBuilder extends Builder { + @Override + public BuildableType build() { + return buildPartial(); + } + } + + @Override + public Builder toBuilder() { + BuildableType_Builder builder = new PartialBuilder(); + builder.type = type; + builder.builderType = builderType; + builder.mergeBuilder = mergeBuilder; + builder.partialToBuilder = partialToBuilder; + builder.builderFactory = builderFactory; + builder.suppressUnchecked = suppressUnchecked; + builder._unsetProperties.clear(); + builder._unsetProperties.addAll(_unsetProperties); + return (Builder) builder; + } + @Override public boolean equals(Object obj) { if (!(obj instanceof Partial)) { diff --git a/src/main/java/org/inferred/freebuilder/processor/Datatype.java b/src/main/java/org/inferred/freebuilder/processor/Datatype.java index d24d5f4af..b8e1a54ff 100644 --- a/src/main/java/org/inferred/freebuilder/processor/Datatype.java +++ b/src/main/java/org/inferred/freebuilder/processor/Datatype.java @@ -107,6 +107,9 @@ public String toString() { /** Returns the partial value class that should be generated. */ public abstract TypeClass getPartialType(); + /** Returns the Rebuildable interface that should be generated, if any. */ + public abstract Optional getRebuildableType(); + /** Returns the Property enum that may be generated. */ public abstract TypeClass getPropertyEnum(); diff --git a/src/main/java/org/inferred/freebuilder/processor/Datatype_Builder.java b/src/main/java/org/inferred/freebuilder/processor/Datatype_Builder.java index 32f23472c..2388a7378 100644 --- a/src/main/java/org/inferred/freebuilder/processor/Datatype_Builder.java +++ b/src/main/java/org/inferred/freebuilder/processor/Datatype_Builder.java @@ -35,9 +35,17 @@ @Generated("org.inferred.freebuilder.processor.Processor") abstract class Datatype_Builder { - /** Creates a new builder using {@code value} as a template. */ + /** + * Creates a new builder using {@code value} as a template. + * + *

If {@code value} is a partial, the builder will return more partials. + */ public static Datatype.Builder from(Datatype value) { - return new Datatype.Builder().mergeFrom(value); + if (value instanceof Rebuildable) { + return ((Rebuildable) value).toBuilder(); + } else { + return new Datatype.Builder().mergeFrom(value); + } } private enum Property { @@ -77,6 +85,10 @@ public String toString() { private TypeClass generatedBuilder; private TypeClass valueType; private TypeClass partialType; + // Store a nullable object instead of an Optional. Escape analysis then + // allows the JVM to optimize away the Optional objects created by and + // passed to our API. + private TypeClass rebuildableType = null; private TypeClass propertyEnum; private final LinkedHashMap standardMethodUnderrides = new LinkedHashMap<>(); @@ -400,6 +412,72 @@ public TypeClass getPartialType() { return partialType; } + /** + * Sets the value to be returned by {@link Datatype#getRebuildableType()}. + * + * @return this {@code Builder} object + * @throws NullPointerException if {@code rebuildableType} is null + */ + public Datatype.Builder setRebuildableType(TypeClass rebuildableType) { + this.rebuildableType = Objects.requireNonNull(rebuildableType); + return (Datatype.Builder) this; + } + + /** + * Sets the value to be returned by {@link Datatype#getRebuildableType()}. + * + * @return this {@code Builder} object + */ + public Datatype.Builder setRebuildableType(Optional rebuildableType) { + if (rebuildableType.isPresent()) { + return setRebuildableType(rebuildableType.get()); + } else { + return clearRebuildableType(); + } + } + + /** + * Sets the value to be returned by {@link Datatype#getRebuildableType()}. + * + * @return this {@code Builder} object + */ + public Datatype.Builder setNullableRebuildableType(TypeClass rebuildableType) { + if (rebuildableType != null) { + return setRebuildableType(rebuildableType); + } else { + return clearRebuildableType(); + } + } + + /** + * If the value to be returned by {@link Datatype#getRebuildableType()} is present, replaces it by + * applying {@code mapper} to it and using the result. + * + *

If the result is null, clears the value. + * + * @return this {@code Builder} object + * @throws NullPointerException if {@code mapper} is null + */ + public Datatype.Builder mapRebuildableType(UnaryOperator mapper) { + return setRebuildableType(getRebuildableType().map(mapper)); + } + + /** + * Sets the value to be returned by {@link Datatype#getRebuildableType()} to {@link + * Optional#empty() Optional.empty()}. + * + * @return this {@code Builder} object + */ + public Datatype.Builder clearRebuildableType() { + rebuildableType = null; + return (Datatype.Builder) this; + } + + /** Returns the value that will be returned by {@link Datatype#getRebuildableType()}. */ + public Optional getRebuildableType() { + return Optional.ofNullable(rebuildableType); + } + /** * Sets the value to be returned by {@link Datatype#getPropertyEnum()}. * @@ -1004,6 +1082,7 @@ public Datatype.Builder mergeFrom(Datatype value) { || !Objects.equals(value.getPartialType(), defaults.getPartialType())) { setPartialType(value.getPartialType()); } + value.getRebuildableType().ifPresent(this::setRebuildableType); if (defaults._unsetProperties.contains(Property.PROPERTY_ENUM) || !Objects.equals(value.getPropertyEnum(), defaults.getPropertyEnum())) { setPropertyEnum(value.getPropertyEnum()); @@ -1085,6 +1164,7 @@ public Datatype.Builder mergeFrom(Datatype.Builder template) { || !Objects.equals(template.getPartialType(), defaults.getPartialType()))) { setPartialType(template.getPartialType()); } + template.getRebuildableType().ifPresent(this::setRebuildableType); if (!base._unsetProperties.contains(Property.PROPERTY_ENUM) && (defaults._unsetProperties.contains(Property.PROPERTY_ENUM) || !Objects.equals(template.getPropertyEnum(), defaults.getPropertyEnum()))) { @@ -1128,6 +1208,7 @@ public Datatype.Builder clear() { generatedBuilder = defaults.generatedBuilder; valueType = defaults.valueType; partialType = defaults.partialType; + rebuildableType = defaults.rebuildableType; propertyEnum = defaults.propertyEnum; standardMethodUnderrides.clear(); builderSerializable = defaults.builderSerializable; @@ -1156,6 +1237,10 @@ public Datatype build() { * be performed. Unset properties will throw an {@link UnsupportedOperationException} when * accessed via the partial object. * + *

The builder returned by {@link Datatype.Builder#from(Datatype)} will propagate the partial + * status of its input, overriding {@link Datatype.Builder#build() build()} to return another + * partial. This allows for robust tests of modify-rebuild code. + * *

Partials should only ever be used in tests. They permit writing robust test cases that won't * fail if this type gains more application-level constraints (e.g. new required fields) in * future. If you require partially complete values in production code, consider using a Builder. @@ -1165,7 +1250,11 @@ public Datatype buildPartial() { return new Partial(this); } - private static final class Value extends Datatype { + private abstract static class Rebuildable extends Datatype { + public abstract Builder toBuilder(); + } + + private static final class Value extends Rebuildable { private final TypeClass type; private final boolean interfaceType; private final Type builder; @@ -1177,6 +1266,10 @@ private static final class Value extends Datatype { private final TypeClass generatedBuilder; private final TypeClass valueType; private final TypeClass partialType; + // Store a nullable object instead of an Optional. Escape analysis then + // allows the JVM to optimize away the Optional objects created by our + // getter method. + private final TypeClass rebuildableType; private final TypeClass propertyEnum; private final ImmutableMap standardMethodUnderrides; private final boolean builderSerializable; @@ -1195,6 +1288,7 @@ private Value(Datatype_Builder builder) { this.generatedBuilder = builder.generatedBuilder; this.valueType = builder.valueType; this.partialType = builder.partialType; + this.rebuildableType = builder.rebuildableType; this.propertyEnum = builder.propertyEnum; this.standardMethodUnderrides = ImmutableMap.copyOf(builder.standardMethodUnderrides); this.builderSerializable = builder.builderSerializable; @@ -1245,6 +1339,11 @@ public TypeClass getPartialType() { return partialType; } + @Override + public Optional getRebuildableType() { + return Optional.ofNullable(rebuildableType); + } + @Override public TypeClass getPropertyEnum() { return propertyEnum; @@ -1285,6 +1384,30 @@ public ImmutableList getNestedClasses() { return nestedClasses; } + @Override + public Builder toBuilder() { + Datatype_Builder builder = new Builder(); + builder.type = type; + builder.interfaceType = interfaceType; + builder.builder = this.builder; + builder.extensible = extensible; + builder.builderFactory = builderFactory; + builder.generatedBuilder = generatedBuilder; + builder.valueType = valueType; + builder.partialType = partialType; + builder.rebuildableType = rebuildableType; + builder.propertyEnum = propertyEnum; + builder.standardMethodUnderrides.putAll(standardMethodUnderrides); + builder.builderSerializable = builderSerializable; + builder.hasToBuilderMethod = hasToBuilderMethod; + builder.generatedBuilderAnnotations = generatedBuilderAnnotations; + builder.valueTypeAnnotations = valueTypeAnnotations; + builder.valueTypeVisibility = valueTypeVisibility; + builder.nestedClasses = nestedClasses; + builder._unsetProperties.clear(); + return (Builder) builder; + } + @Override public boolean equals(Object obj) { if (!(obj instanceof Value)) { @@ -1299,6 +1422,7 @@ public boolean equals(Object obj) { && Objects.equals(generatedBuilder, other.generatedBuilder) && Objects.equals(valueType, other.valueType) && Objects.equals(partialType, other.partialType) + && Objects.equals(rebuildableType, other.rebuildableType) && Objects.equals(propertyEnum, other.propertyEnum) && Objects.equals(standardMethodUnderrides, other.standardMethodUnderrides) && builderSerializable == other.builderSerializable @@ -1320,6 +1444,7 @@ public int hashCode() { generatedBuilder, valueType, partialType, + rebuildableType, propertyEnum, standardMethodUnderrides, builderSerializable, @@ -1344,13 +1469,17 @@ public String toString() { if (builderFactory != null) { result.append(", builderFactory=").append(builderFactory); } - return result + result .append(", generatedBuilder=") .append(generatedBuilder) .append(", valueType=") .append(valueType) .append(", partialType=") - .append(partialType) + .append(partialType); + if (rebuildableType != null) { + result.append(", rebuildableType=").append(rebuildableType); + } + return result .append(", propertyEnum=") .append(propertyEnum) .append(", standardMethodUnderrides=") @@ -1372,7 +1501,7 @@ public String toString() { } } - private static final class Partial extends Datatype { + private static final class Partial extends Rebuildable { private final TypeClass type; private final boolean interfaceType; private final Type builder; @@ -1384,6 +1513,10 @@ private static final class Partial extends Datatype { private final TypeClass generatedBuilder; private final TypeClass valueType; private final TypeClass partialType; + // Store a nullable object instead of an Optional. Escape analysis then + // allows the JVM to optimize away the Optional objects created by our + // getter method. + private final TypeClass rebuildableType; private final TypeClass propertyEnum; private final ImmutableMap standardMethodUnderrides; private final boolean builderSerializable; @@ -1403,6 +1536,7 @@ private static final class Partial extends Datatype { this.generatedBuilder = builder.generatedBuilder; this.valueType = builder.valueType; this.partialType = builder.partialType; + this.rebuildableType = builder.rebuildableType; this.propertyEnum = builder.propertyEnum; this.standardMethodUnderrides = ImmutableMap.copyOf(builder.standardMethodUnderrides); this.builderSerializable = builder.builderSerializable; @@ -1475,6 +1609,11 @@ public TypeClass getPartialType() { return partialType; } + @Override + public Optional getRebuildableType() { + return Optional.ofNullable(rebuildableType); + } + @Override public TypeClass getPropertyEnum() { if (_unsetProperties.contains(Property.PROPERTY_ENUM)) { @@ -1527,6 +1666,38 @@ public ImmutableList getNestedClasses() { return nestedClasses; } + private static class PartialBuilder extends Builder { + @Override + public Datatype build() { + return buildPartial(); + } + } + + @Override + public Builder toBuilder() { + Datatype_Builder builder = new PartialBuilder(); + builder.type = type; + builder.interfaceType = interfaceType; + builder.builder = this.builder; + builder.extensible = extensible; + builder.builderFactory = builderFactory; + builder.generatedBuilder = generatedBuilder; + builder.valueType = valueType; + builder.partialType = partialType; + builder.rebuildableType = rebuildableType; + builder.propertyEnum = propertyEnum; + builder.standardMethodUnderrides.putAll(standardMethodUnderrides); + builder.builderSerializable = builderSerializable; + builder.hasToBuilderMethod = hasToBuilderMethod; + builder.generatedBuilderAnnotations = generatedBuilderAnnotations; + builder.valueTypeAnnotations = valueTypeAnnotations; + builder.valueTypeVisibility = valueTypeVisibility; + builder.nestedClasses = nestedClasses; + builder._unsetProperties.clear(); + builder._unsetProperties.addAll(_unsetProperties); + return (Builder) builder; + } + @Override public boolean equals(Object obj) { if (!(obj instanceof Partial)) { @@ -1541,6 +1712,7 @@ public boolean equals(Object obj) { && Objects.equals(generatedBuilder, other.generatedBuilder) && Objects.equals(valueType, other.valueType) && Objects.equals(partialType, other.partialType) + && Objects.equals(rebuildableType, other.rebuildableType) && Objects.equals(propertyEnum, other.propertyEnum) && Objects.equals(standardMethodUnderrides, other.standardMethodUnderrides) && builderSerializable == other.builderSerializable @@ -1563,6 +1735,7 @@ public int hashCode() { generatedBuilder, valueType, partialType, + rebuildableType, propertyEnum, standardMethodUnderrides, builderSerializable, @@ -1601,6 +1774,9 @@ public String toString() { if (!_unsetProperties.contains(Property.PARTIAL_TYPE)) { result.append("partialType=").append(partialType).append(", "); } + if (rebuildableType != null) { + result.append("rebuildableType=").append(rebuildableType).append(", "); + } if (!_unsetProperties.contains(Property.PROPERTY_ENUM)) { result.append("propertyEnum=").append(propertyEnum).append(", "); } diff --git a/src/main/java/org/inferred/freebuilder/processor/GeneratedBuilder.java b/src/main/java/org/inferred/freebuilder/processor/GeneratedBuilder.java index 88e449502..aa94e278b 100644 --- a/src/main/java/org/inferred/freebuilder/processor/GeneratedBuilder.java +++ b/src/main/java/org/inferred/freebuilder/processor/GeneratedBuilder.java @@ -42,6 +42,7 @@ import org.inferred.freebuilder.processor.source.ObjectsExcerpts; import org.inferred.freebuilder.processor.source.PreconditionExcerpts; import org.inferred.freebuilder.processor.source.SourceBuilder; +import org.inferred.freebuilder.processor.source.TypeClass; import org.inferred.freebuilder.processor.source.Variable; import java.io.Serializable; @@ -104,6 +105,7 @@ public void addTo(SourceBuilder code) { addBuildMethod(code); addBuildPartialMethod(code); + addRebuildableSuperclass(code); addValueType(code); addPartialType(code); datatype.getNestedClasses().forEach(code::add); @@ -132,14 +134,25 @@ private void addStaticFromMethod(SourceBuilder code) { code.addLine("") .addLine("/**") .addLine(" * Creates a new builder using {@code value} as a template.") + .addLine(" *") + .addLine(" *

If {@code value} is a partial, the builder will return more partials.") .addLine(" */") .addLine("public static %s %s from(%s value) {", datatype.getType().declarationParameters(), datatype.getBuilder(), - datatype.getType()) - .addLine(" return %s.mergeFrom(value);", - builderFactory.newBuilder(datatype.getBuilder(), EXPLICIT_TYPES)) - .addLine("}"); + datatype.getType()); + if (datatype.getHasToBuilderMethod()) { + code.addLine(" return value.toBuilder();"); + } else { + TypeClass rebuildable = datatype.getRebuildableType().get(); + code.addLine(" if (value instanceof %s) {", rebuildable.getQualifiedName()) + .addLine(" return ((%s) value).toBuilder();", rebuildable) + .addLine(" } else {") + .addLine(" return %s.mergeFrom(value);", + builderFactory.newBuilder(datatype.getBuilder(), EXPLICIT_TYPES)) + .addLine(" }"); + } + code.addLine("}"); } private void addFieldDeclarations(SourceBuilder code) { @@ -246,16 +259,17 @@ private void addBuildPartialMethod(SourceBuilder code) { UnsupportedOperationException.class) .addLine(" * when accessed via the partial object."); } - if (datatype.getHasToBuilderMethod() - && datatype.getBuilderFactory().equals(Optional.of(BuilderFactory.NO_ARGS_CONSTRUCTOR))) { - code.addLine(" *") - .addLine(" *

The builder returned by a partial's %s", - datatype.getType().javadocNoArgMethodLink("toBuilder").withText("toBuilder")) - .addLine(" * method overrides %s to return another partial.", - datatype.getBuilder().javadocNoArgMethodLink("build").withText("build()")) - .addLine(" * This allows for robust tests of modify-rebuild code."); - } code.addLine(" *") + .addLine(" *

The builder returned by %s", + datatype.getBuilder().javadocMethodLink("from", datatype.getType())); + if (datatype.getHasToBuilderMethod()) { + code.addLine(" * or %s", datatype.getType().javadocNoArgMethodLink("toBuilder")); + } + code.addLine("will propagate the partial status of its input, overriding") + .addLine(" * %s to return another partial.", + datatype.getBuilder().javadocNoArgMethodLink("build").withText("build()")) + .addLine(" * This allows for robust tests of modify-rebuild code.") + .addLine(" *") .addLine(" *

Partials should only ever be used in tests. They permit writing robust") .addLine(" * test cases that won't fail if this type gains more application-level") .addLine(" * constraints (e.g. new required fields) in future. If you require partially") @@ -292,17 +306,29 @@ private void addPropertyEnum(SourceBuilder code) { .addLine("}"); } + private void addRebuildableSuperclass(SourceBuilder code) { + datatype.getRebuildableType().ifPresent(rebuildable -> { + code.addLine("") + .addLine("private abstract static class %s %s {", + rebuildable.declaration(), extending(datatype.getType(), datatype.isInterfaceType())) + .addLine(" public abstract %s toBuilder();", datatype.getBuilder()) + .addLine("}"); + }); + } + private void addValueType(SourceBuilder code) { code.addLine(""); datatype.getValueTypeAnnotations().forEach(code::add); code.addLine("%s static final class %s %s {", datatype.getValueTypeVisibility(), datatype.getValueType().declaration(), - extending(datatype.getType(), datatype.isInterfaceType())); + datatype.getRebuildableType() + .map(rebuildable -> extending(rebuildable, false)) + .orElse(extending(datatype.getType(), datatype.isInterfaceType()))); generatorsByProperty.values().forEach(generator -> generator.addValueFieldDeclaration(code)); addValueTypeConstructor(code); addValueTypeGetters(code); - if (datatype.getHasToBuilderMethod()) { + if (datatype.getHasToBuilderMethod() || datatype.getRebuildableType().isPresent()) { addValueTypeToBuilder(code); } switch (datatype.standardMethodUnderride(StandardMethod.EQUALS)) { @@ -428,7 +454,9 @@ private void addPartialType(SourceBuilder code) { code.addLine("") .addLine("private static final class %s %s {", datatype.getPartialType().declaration(), - extending(datatype.getType(), datatype.isInterfaceType())); + datatype.getRebuildableType() + .map(rebuildable -> extending(rebuildable, false)) + .orElse(extending(datatype.getType(), datatype.isInterfaceType()))); addPartialFields(code); addPartialConstructor(code); addPartialGetters(code); @@ -490,7 +518,7 @@ private void addPartialGetters(SourceBuilder code) { } private void addPartialToBuilderMethod(SourceBuilder code) { - if (!datatype.getHasToBuilderMethod()) { + if (!datatype.getHasToBuilderMethod() && !datatype.getRebuildableType().isPresent()) { return; } boolean hasRequiredProperties = generatorsByProperty.values().stream().anyMatch(IS_REQUIRED); diff --git a/src/main/java/org/inferred/freebuilder/processor/property/Property_Builder.java b/src/main/java/org/inferred/freebuilder/processor/property/Property_Builder.java index 3805fba13..932c6a023 100644 --- a/src/main/java/org/inferred/freebuilder/processor/property/Property_Builder.java +++ b/src/main/java/org/inferred/freebuilder/processor/property/Property_Builder.java @@ -29,10 +29,18 @@ @Generated("org.inferred.freebuilder.processor.Processor") abstract class Property_Builder { - /** Creates a new builder using {@code value} as a template. */ + /** + * Creates a new builder using {@code value} as a template. + * + *

If {@code value} is a partial, the builder will return more partials. + */ public static org.inferred.freebuilder.processor.property.Property.Builder from( org.inferred.freebuilder.processor.property.Property value) { - return new org.inferred.freebuilder.processor.property.Property.Builder().mergeFrom(value); + if (value instanceof Rebuildable) { + return ((Rebuildable) value).toBuilder(); + } else { + return new org.inferred.freebuilder.processor.property.Property.Builder().mergeFrom(value); + } } private enum Property { @@ -686,6 +694,12 @@ public org.inferred.freebuilder.processor.property.Property build() { * for use in unit tests. State checking will not be performed. Unset properties will throw an * {@link UnsupportedOperationException} when accessed via the partial object. * + *

The builder returned by {@link + * org.inferred.freebuilder.processor.property.Property.Builder#from(org.inferred.freebuilder.processor.property.Property)} + * will propagate the partial status of its input, overriding {@link + * org.inferred.freebuilder.processor.property.Property.Builder#build() build()} to return another + * partial. This allows for robust tests of modify-rebuild code. + * *

Partials should only ever be used in tests. They permit writing robust test cases that won't * fail if this type gains more application-level constraints (e.g. new required fields) in * future. If you require partially complete values in production code, consider using a Builder. @@ -695,7 +709,12 @@ public org.inferred.freebuilder.processor.property.Property buildPartial() { return new Partial(this); } - private static final class Value extends org.inferred.freebuilder.processor.property.Property { + private abstract static class Rebuildable + extends org.inferred.freebuilder.processor.property.Property { + public abstract Builder toBuilder(); + } + + private static final class Value extends Rebuildable { private final TypeMirror type; // Store a nullable object instead of an Optional. Escape analysis then // allows the JVM to optimize away the Optional objects created by our @@ -766,6 +785,22 @@ public ImmutableList getAccessorAnnotations() { return accessorAnnotations; } + @Override + public Builder toBuilder() { + Property_Builder builder = new Builder(); + builder.type = type; + builder.boxedType = boxedType; + builder.name = name; + builder.capitalizedName = capitalizedName; + builder.allCapsName = allCapsName; + builder.usingBeanConvention = usingBeanConvention; + builder.getterName = getterName; + builder.fullyCheckedCast = fullyCheckedCast; + builder.accessorAnnotations = accessorAnnotations; + builder._unsetProperties.clear(); + return (Builder) builder; + } + @Override public boolean equals(Object obj) { if (!(obj instanceof Value)) { @@ -823,7 +858,7 @@ public String toString() { } } - private static final class Partial extends org.inferred.freebuilder.processor.property.Property { + private static final class Partial extends Rebuildable { private final TypeMirror type; // Store a nullable object instead of an Optional. Escape analysis then // allows the JVM to optimize away the Optional objects created by our @@ -917,6 +952,30 @@ public ImmutableList getAccessorAnnotations() { return accessorAnnotations; } + private static class PartialBuilder extends Builder { + @Override + public org.inferred.freebuilder.processor.property.Property build() { + return buildPartial(); + } + } + + @Override + public Builder toBuilder() { + Property_Builder builder = new PartialBuilder(); + builder.type = type; + builder.boxedType = boxedType; + builder.name = name; + builder.capitalizedName = capitalizedName; + builder.allCapsName = allCapsName; + builder.usingBeanConvention = usingBeanConvention; + builder.getterName = getterName; + builder.fullyCheckedCast = fullyCheckedCast; + builder.accessorAnnotations = accessorAnnotations; + builder._unsetProperties.clear(); + builder._unsetProperties.addAll(_unsetProperties); + return (Builder) builder; + } + @Override public boolean equals(Object obj) { if (!(obj instanceof Partial)) { diff --git a/src/main/java/org/inferred/freebuilder/processor/source/Type.java b/src/main/java/org/inferred/freebuilder/processor/source/Type.java index 9b3d5e630..72a7400c8 100644 --- a/src/main/java/org/inferred/freebuilder/processor/source/Type.java +++ b/src/main/java/org/inferred/freebuilder/processor/source/Type.java @@ -111,6 +111,22 @@ public JavadocLink javadocNoArgMethodLink(String memberName) { return new JavadocLink("%s#%s()", getQualifiedName(), memberName); } + /** + * Returns a source excerpt of a JavaDoc link to a method on this type. + */ + public JavadocLink javadocMethodLink(String memberName, Type... types) { + return new JavadocLink("%s#%s(%s)", + getQualifiedName(), + memberName, + (Excerpt) code -> { + String separator = ""; + for (Type type : types) { + code.add("%s%s", separator, type.getQualifiedName()); + separator = ", "; + } + }); + } + /** * Returns a source excerpt of the type parameters of this type, including angle brackets. * Always an empty string if the type class is not generic. diff --git a/src/main/java/org/inferred/freebuilder/processor/source/TypeUsage_Builder.java b/src/main/java/org/inferred/freebuilder/processor/source/TypeUsage_Builder.java index 93ccdc49b..1cba29e5c 100644 --- a/src/main/java/org/inferred/freebuilder/processor/source/TypeUsage_Builder.java +++ b/src/main/java/org/inferred/freebuilder/processor/source/TypeUsage_Builder.java @@ -19,9 +19,17 @@ @Generated("org.inferred.freebuilder.processor.Processor") abstract class TypeUsage_Builder { - /** Creates a new builder using {@code value} as a template. */ + /** + * Creates a new builder using {@code value} as a template. + * + *

If {@code value} is a partial, the builder will return more partials. + */ public static TypeUsage.Builder from(TypeUsage value) { - return new TypeUsage.Builder().mergeFrom(value); + if (value instanceof Rebuildable) { + return ((Rebuildable) value).toBuilder(); + } else { + return new TypeUsage.Builder().mergeFrom(value); + } } private enum Property { @@ -299,6 +307,10 @@ public TypeUsage build() { * not be performed. Unset properties will throw an {@link UnsupportedOperationException} when * accessed via the partial object. * + *

The builder returned by {@link TypeUsage.Builder#from(TypeUsage)} will propagate the partial + * status of its input, overriding {@link TypeUsage.Builder#build() build()} to return another + * partial. This allows for robust tests of modify-rebuild code. + * *

Partials should only ever be used in tests. They permit writing robust test cases that won't * fail if this type gains more application-level constraints (e.g. new required fields) in * future. If you require partially complete values in production code, consider using a Builder. @@ -308,7 +320,11 @@ public TypeUsage buildPartial() { return new Partial(this); } - private static final class Value implements TypeUsage { + private abstract static class Rebuildable implements TypeUsage { + public abstract Builder toBuilder(); + } + + private static final class Value extends Rebuildable { private final int start; private final int end; private final QualifiedName type; @@ -344,6 +360,17 @@ public Optional scope() { return Optional.ofNullable(scope); } + @Override + public Builder toBuilder() { + TypeUsage_Builder builder = new Builder(); + builder.start = start; + builder.end = end; + builder.type = type; + builder.scope = scope; + builder._unsetProperties.clear(); + return (Builder) builder; + } + @Override public boolean equals(Object obj) { if (!(obj instanceof Value)) { @@ -377,7 +404,7 @@ public String toString() { } } - private static final class Partial implements TypeUsage { + private static final class Partial extends Rebuildable { private final int start; private final int end; private final QualifiedName type; @@ -424,6 +451,25 @@ public Optional scope() { return Optional.ofNullable(scope); } + private static class PartialBuilder extends Builder { + @Override + public TypeUsage build() { + return buildPartial(); + } + } + + @Override + public Builder toBuilder() { + TypeUsage_Builder builder = new PartialBuilder(); + builder.start = start; + builder.end = end; + builder.type = type; + builder.scope = scope; + builder._unsetProperties.clear(); + builder._unsetProperties.addAll(_unsetProperties); + return (Builder) builder; + } + @Override public boolean equals(Object obj) { if (!(obj instanceof Partial)) { diff --git a/src/test/java/org/inferred/freebuilder/processor/AnalyserTest.java b/src/test/java/org/inferred/freebuilder/processor/AnalyserTest.java index e139ff85c..f9e9d9a3b 100644 --- a/src/test/java/org/inferred/freebuilder/processor/AnalyserTest.java +++ b/src/test/java/org/inferred/freebuilder/processor/AnalyserTest.java @@ -155,6 +155,7 @@ public void classWithNoProperties() throws CannotGenerateCodeException { .setInterfaceType(false) .setPartialType(generatedType.nestedType("Partial").withParameters()) .setPropertyEnum(generatedType.nestedType("Property").withParameters()) + .setRebuildableType(generatedType.nestedType("Rebuildable").withParameters()) .setType(dataType.withParameters()) .setValueType(generatedType.nestedType("Value").withParameters()) .build(), @@ -189,6 +190,7 @@ public void genericInterfaceWithNoProperties() throws CannotGenerateCodeExceptio .setInterfaceType(true) .setPartialType(generatedType.nestedType("Partial").withParameters(k, v)) .setPropertyEnum(generatedType.nestedType("Property").withParameters()) + .setRebuildableType(generatedType.nestedType("Rebuildable").withParameters(k, v)) .setType(dataType.withParameters(k, v)) .setValueType(generatedType.nestedType("Value").withParameters(k, v)) .build(), @@ -1085,6 +1087,7 @@ public void genericClassWithTwoProperties() throws CannotGenerateCodeException { .setInterfaceType(false) .setPartialType(generatedType.nestedType("Partial").withParameters(a, b)) .setPropertyEnum(generatedType.nestedType("Property").withParameters()) + .setRebuildableType(generatedType.nestedType("Rebuildable").withParameters(a, b)) .setType(dataType.withParameters(a, b)) .setValueType(generatedType.nestedType("Value").withParameters(a, b)) .build()); @@ -1180,25 +1183,23 @@ public void explicitPackageScopeNoArgsConstructor() throws CannotGenerateCodeExc " public static class Builder extends DataType_Builder {}", "}")); - QualifiedName expectedBuilder = QualifiedName.of("com.example", "DataType_Builder"); - QualifiedName partialType = expectedBuilder.nestedType("Partial"); - QualifiedName propertyType = expectedBuilder.nestedType("Property"); - QualifiedName valueType = expectedBuilder.nestedType("Value"); - Datatype expectedDatatype = new Datatype.Builder() + QualifiedName generatedType = QualifiedName.of("com.example", "DataType_Builder"); + Datatype datatype = new Datatype.Builder() .setBuilder(QualifiedName.of("com.example", "DataType", "Builder").withParameters()) .setExtensible(true) .setBuilderFactory(NO_ARGS_CONSTRUCTOR) .setBuilderSerializable(false) - .setGeneratedBuilder(expectedBuilder.withParameters()) + .setGeneratedBuilder(generatedType.withParameters()) .setHasToBuilderMethod(false) .setInterfaceType(false) - .setPartialType(partialType.withParameters()) - .setPropertyEnum(propertyType.withParameters()) + .setPartialType(generatedType.nestedType("Partial").withParameters()) + .setPropertyEnum(generatedType.nestedType("Property").withParameters()) + .setRebuildableType(generatedType.nestedType("Rebuildable").withParameters()) .setType(QualifiedName.of("com.example", "DataType").withParameters()) - .setValueType(valueType.withParameters()) + .setValueType(generatedType.nestedType("Value").withParameters()) .build(); - assertEquals(expectedDatatype, builder.getDatatype()); + assertEquals(datatype, builder.getDatatype()); } @Test @@ -1212,25 +1213,23 @@ public void multipleConstructors() throws CannotGenerateCodeException { " public static class Builder extends DataType_Builder {}", "}")); - QualifiedName expectedBuilder = QualifiedName.of("com.example", "DataType_Builder"); - QualifiedName partialType = expectedBuilder.nestedType("Partial"); - QualifiedName propertyType = expectedBuilder.nestedType("Property"); - QualifiedName valueType = expectedBuilder.nestedType("Value"); - Datatype expectedDatatype = new Datatype.Builder() + QualifiedName generatedType = QualifiedName.of("com.example", "DataType_Builder"); + Datatype datatype = new Datatype.Builder() .setBuilder(QualifiedName.of("com.example", "DataType", "Builder").withParameters()) .setExtensible(true) .setBuilderFactory(NO_ARGS_CONSTRUCTOR) .setBuilderSerializable(false) - .setGeneratedBuilder(expectedBuilder.withParameters()) + .setGeneratedBuilder(generatedType.withParameters()) .setHasToBuilderMethod(false) .setInterfaceType(false) - .setPartialType(partialType.withParameters()) - .setPropertyEnum(propertyType.withParameters()) + .setPartialType(generatedType.nestedType("Partial").withParameters()) + .setPropertyEnum(generatedType.nestedType("Property").withParameters()) + .setRebuildableType(generatedType.nestedType("Rebuildable").withParameters()) .setType(QualifiedName.of("com.example", "DataType").withParameters()) - .setValueType(valueType.withParameters()) + .setValueType(generatedType.nestedType("Value").withParameters()) .build(); - assertEquals(expectedDatatype, builder.getDatatype()); + assertEquals(datatype, builder.getDatatype()); } @Test diff --git a/src/test/java/org/inferred/freebuilder/processor/ProcessorTest.java b/src/test/java/org/inferred/freebuilder/processor/ProcessorTest.java index 138cd5689..0aa446e7a 100644 --- a/src/test/java/org/inferred/freebuilder/processor/ProcessorTest.java +++ b/src/test/java/org/inferred/freebuilder/processor/ProcessorTest.java @@ -213,6 +213,160 @@ public void testFrom() { .runTest(); } + @Test + public void testFrom_partial() { + behaviorTester + .with(new Processor(features)) + .with(SourceBuilder.forTesting() + .addLine("package com.example;") + .addLine("@%s", FreeBuilder.class) + .addLine("public interface DataType {") + .addLine(" String getName();") + .addLine(" int getAge();") + .addLine("") + .addLine(" class Builder extends DataType_Builder {}") + .addLine("}")) + .with(testBuilder() + .addLine("DataType value = new DataType.Builder()") + .addLine(" .setName(\"fred\")") + .addLine(" .buildPartial();") + .addLine("DataType.Builder copyBuilder = DataType.Builder.from(value);") + .addLine("copyBuilder.setName(copyBuilder.getName() + \" 2\");") + .addLine("DataType copy = copyBuilder.build();") + .addLine("assertEquals(\"partial DataType{name=fred 2}\", copy.toString());") + .build()) + .runTest(); + } + + @Test + public void testFrom_partial_withGenerics() { + behaviorTester + .with(new Processor(features)) + .with(SourceBuilder.forTesting() + .addLine("package com.example;") + .addLine("@%s", FreeBuilder.class) + .addLine("public interface DataType {") + .addLine(" T getName();") + .addLine(" int getAge();") + .addLine("") + .addLine(" class Builder extends DataType_Builder {}") + .addLine("}")) + .with(testBuilder() + .addLine("DataType value = new DataType.Builder()") + .addLine(" .setName(\"fred\")") + .addLine(" .buildPartial();") + .addLine("DataType.Builder copyBuilder = DataType.Builder.from(value);") + .addLine("copyBuilder.setName(copyBuilder.getName() + \" 2\");") + .addLine("DataType copy = copyBuilder.build();") + .addLine("assertEquals(\"partial DataType{name=fred 2}\", copy.toString());") + .build()) + .compiles() + .withNoWarnings() + .allTestsPass(); + } + + @Test + public void testFrom_partial_withProtectedConstructorAndStaticBuilderMethod() { + behaviorTester + .with(new Processor(features)) + .with(SourceBuilder.forTesting() + .addLine("package com.example;") + .addLine("@%s", FreeBuilder.class) + .addLine("public abstract class DataType {") + .addLine(" public abstract String getName();") + .addLine(" public abstract int getAge();") + .addLine("") + .addLine(" public static Builder builder() { return new Builder(); }") + .addLine(" public static class Builder extends DataType_Builder {") + .addLine(" Builder() {}") + .addLine(" }") + .addLine("}")) + .with(testBuilder() + .addLine("DataType value = DataType.builder()") + .addLine(" .setName(\"fred\")") + .addLine(" .buildPartial();") + .addLine("DataType.Builder copyBuilder = DataType.Builder.from(value);") + .addLine("copyBuilder.setName(copyBuilder.getName() + \" 2\");") + .addLine("DataType copy = copyBuilder.build();") + .addLine("assertEquals(\"partial DataType{name=fred 2}\", copy.toString());") + .build()) + .runTest(); + } + + @Test + public void testFrom_partial_withProtectedConstructorAndParameterizedBuilderMethod() { + behaviorTester + .with(new Processor(features)) + .with(SourceBuilder.forTesting() + .addLine("package com.example;") + .addLine("@%s", FreeBuilder.class) + .addLine("public abstract class DataType {") + .addLine(" public abstract String getName();") + .addLine(" public abstract int getAge();") + .addLine("") + .addLine(" public static Builder builder(String name) {") + .addLine(" return new Builder().setName(name);") + .addLine(" }") + .addLine(" public static class Builder extends DataType_Builder {") + .addLine(" Builder() {}") + .addLine(" }") + .addLine("}")) + .with(testBuilder() + .addLine("DataType value = DataType.builder(\"fred\")") + .addLine(" .buildPartial();") + .addLine("DataType.Builder copyBuilder = DataType.Builder.from(value);") + .addLine("copyBuilder.setName(copyBuilder.getName() + \" 2\");") + .addLine("DataType copy = copyBuilder.build();") + .addLine("assertEquals(\"partial DataType{name=fred 2}\", copy.toString());") + .build()) + .runTest(); + } + + @Test + public void testFrom_propertyCalledBuilder() { + behaviorTester + .with(new Processor(features)) + .with(SourceBuilder.forTesting() + .addLine("package com.example;") + .addLine("@%s", FreeBuilder.class) + .addLine("public interface DataType {") + .addLine(" String builder();") + .addLine("") + .addLine(" class Builder extends DataType_Builder {}") + .addLine("}")) + .with(testBuilder() + .addLine("DataType value = new DataType.Builder()") + .addLine(" .builder(\"Bob\")") + .addLine(" .build();") + .addLine("DataType.Builder copyBuilder = DataType.Builder.from(value);") + .addLine("copyBuilder.builder(copyBuilder.builder() + \" 2\");") + .addLine("DataType copy = copyBuilder.build();") + .addLine("assertEquals(\"DataType{builder=Bob 2}\", copy.toString());") + .build()) + .runTest(); + } + + @Test + public void testFrom_otherImplementation() { + behaviorTester + .with(new Processor(features)) + .with(twoPropertyType()) + .with(testBuilder() + .addLine("DataType value = new DataType() {") + .addLine(" @Override public int getPropertyA() {") + .addLine(" return 11;") + .addLine(" }") + .addLine(" @Override public boolean isPropertyB() {") + .addLine(" return true;") + .addLine(" }") + .addLine("};") + .addLine("DataType.Builder builder = DataType.Builder.from(value);") + .addLine("assertEquals(11, builder.getPropertyA());") + .addLine("assertTrue(builder.isPropertyB());") + .build()) + .runTest(); + } + @Test public void testClear_implicitConstructor() { thrown.expect(IllegalStateException.class); diff --git a/src/test/java/org/inferred/freebuilder/processor/property/DefaultSourceTest.java b/src/test/java/org/inferred/freebuilder/processor/property/DefaultSourceTest.java index 93f905b95..5d16f3843 100644 --- a/src/test/java/org/inferred/freebuilder/processor/property/DefaultSourceTest.java +++ b/src/test/java/org/inferred/freebuilder/processor/property/DefaultSourceTest.java @@ -75,9 +75,13 @@ name, new DefaultProperty(datatype, name, true, unaryOperator(STRING)), + "derived from the API of {@link Person}. */", "abstract class Person_Builder {", "", - " /** Creates a new builder using {@code value} as a template. */", + " /**", + " * Creates a new builder using {@code value} as a template.", + " *", + " *

If {@code value} is a partial, the builder will return more partials.", + " */", " public static Person.Builder from(Person value) {", - " return new Person.Builder().mergeFrom(value);", + " return value.toBuilder();", " }", "", " private String name;", @@ -193,11 +197,12 @@ name, new DefaultProperty(datatype, name, true, unaryOperator(STRING)), + "State checking will not", " * be performed.", " *", - " *

The builder returned by a partial's {@link Person#toBuilder() toBuilder} method " - + "overrides", - " * {@link Person.Builder#build() build()} to return another partial. This allows for " - + "robust tests", - " * of modify-rebuild code.", + " *

The builder returned by {@link Person.Builder#from(Person)} or " + + "{@link Person#toBuilder()}", + " * will propagate the partial status of its input, overriding " + + "{@link Person.Builder#build()", + " * build()} to return another partial. This allows for robust tests of modify-rebuild " + + "code.", " *", " *

Partials should only ever be used in tests. " + "They permit writing robust test cases that won't", @@ -371,9 +376,13 @@ name, new DefaultProperty(datatype, name, false, unaryOperator(STRING)), + "derived from the API of {@link Person}. */", "abstract class Person_Builder {", "", - " /** Creates a new builder using {@code value} as a template. */", + " /**", + " * Creates a new builder using {@code value} as a template.", + " *", + " *

If {@code value} is a partial, the builder will return more partials.", + " */", " public static Person.Builder from(Person value) {", - " return new Person.Builder().mergeFrom(value);", + " return value.toBuilder();", " }", "", " private enum Property {", @@ -542,11 +551,12 @@ name, new DefaultProperty(datatype, name, false, unaryOperator(STRING)), + "when", " * accessed via the partial object.", " *", - " *

The builder returned by a partial's {@link Person#toBuilder() toBuilder} method " - + "overrides", - " * {@link Person.Builder#build() build()} to return another partial. This allows for " - + "robust tests", - " * of modify-rebuild code.", + " *

The builder returned by {@link Person.Builder#from(Person)} or " + + "{@link Person#toBuilder()}", + " * will propagate the partial status of its input, overriding " + + "{@link Person.Builder#build()", + " * build()} to return another partial. This allows for robust tests of modify-rebuild " + + "code.", " *", " *

Partials should only ever be used in tests. " + "They permit writing robust test cases that won't", @@ -742,9 +752,13 @@ name, new DefaultProperty(datatype, name, false, unaryOperator(paramA.asType())) + "derived from the API of {@link Person}. */", "abstract class Person_Builder {", "", - " /** Creates a new builder using {@code value} as a template. */", + " /**", + " * Creates a new builder using {@code value} as a template.", + " *", + " *

If {@code value} is a partial, the builder will return more partials.", + " */", " public static Person.Builder from(Person value) {", - " return new Person.Builder().mergeFrom(value);", + " return value.toBuilder();", " }", "", " private enum Property {", @@ -914,11 +928,12 @@ name, new DefaultProperty(datatype, name, false, unaryOperator(paramA.asType())) + "when", " * accessed via the partial object.", " *", - " *

The builder returned by a partial's {@link Person#toBuilder() toBuilder} method " - + "overrides", - " * {@link Person.Builder#build() build()} to return another partial. This allows for " - + "robust tests", - " * of modify-rebuild code.", + " *

The builder returned by {@link Person.Builder#from(Person)} or " + + "{@link Person#toBuilder()}", + " * will propagate the partial status of its input, overriding " + + "{@link Person.Builder#build()", + " * build()} to return another partial. This allows for robust tests of modify-rebuild " + + "code.", " *", " *

Partials should only ever be used in tests. " + "They permit writing robust test cases that won't", diff --git a/src/test/java/org/inferred/freebuilder/processor/property/GuavaOptionalSourceTest.java b/src/test/java/org/inferred/freebuilder/processor/property/GuavaOptionalSourceTest.java index 79a972e2d..ae0d4f161 100644 --- a/src/test/java/org/inferred/freebuilder/processor/property/GuavaOptionalSourceTest.java +++ b/src/test/java/org/inferred/freebuilder/processor/property/GuavaOptionalSourceTest.java @@ -57,9 +57,17 @@ public void testSource() { + "derived from the API of {@link Person}. */", "abstract class Person_Builder {", "", - " /** Creates a new builder using {@code value} as a template. */", + " /**", + " * Creates a new builder using {@code value} as a template.", + " *", + " *

If {@code value} is a partial, the builder will return more partials.", + " */", " public static Person.Builder from(Person value) {", - " return new Person.Builder().mergeFrom(value);", + " if (value instanceof Rebuildable) {", + " return ((Rebuildable) value).toBuilder();", + " } else {", + " return new Person.Builder().mergeFrom(value);", + " }", " }", "", " // Store a nullable object instead of an Optional. Escape analysis then", @@ -267,6 +275,12 @@ public void testSource() { + "State checking will not", " * be performed.", " *", + " *

The builder returned by {@link Person.Builder#from(Person)} will propagate the " + + "partial", + " * status of its input, overriding {@link Person.Builder#build() build()} to return " + + "another", + " * partial. This allows for robust tests of modify-rebuild code.", + " *", " *

Partials should only ever be used in tests. " + "They permit writing robust test cases that won't", " * fail if this type gains more application-level constraints " @@ -279,7 +293,11 @@ public void testSource() { " return new Partial(this);", " }", "", - " private static final class Value extends Person {", + " private abstract static class Rebuildable extends Person {", + " public abstract Person.Builder toBuilder();", + " }", + "", + " private static final class Value extends Rebuildable {", " // Store a nullable object instead of an Optional. Escape analysis then", " // allows the JVM to optimize away the Optional objects created by our", " // getter method.", @@ -305,6 +323,14 @@ public void testSource() { " }", "", " @Override", + " public Person.Builder toBuilder() {", + " Person_Builder builder = new Person.Builder();", + " builder.name = name;", + " builder.age = age;", + " return (Person.Builder) builder;", + " }", + "", + " @Override", " public boolean equals(Object obj) {", " if (!(obj instanceof Value)) {", " return false;", @@ -333,7 +359,7 @@ public void testSource() { " }", " }", "", - " private static final class Partial extends Person {", + " private static final class Partial extends Rebuildable {", " // Store a nullable object instead of an Optional. Escape analysis then", " // allows the JVM to optimize away the Optional objects created by our", " // getter method.", @@ -358,6 +384,21 @@ public void testSource() { " return Optional.fromNullable(age);", " }", "", + " private static class PartialBuilder extends Person.Builder {", + " @Override", + " public Person build() {", + " return buildPartial();", + " }", + " }", + "", + " @Override", + " public Person.Builder toBuilder() {", + " Person_Builder builder = new PartialBuilder();", + " builder.name = name;", + " builder.age = age;", + " return (Person.Builder) builder;", + " }", + "", " @Override", " public boolean equals(Object obj) {", " if (!(obj instanceof Partial)) {", @@ -405,6 +446,7 @@ private static GeneratedBuilder builder() { .setInterfaceType(false) .setPartialType(generatedBuilder.nestedType("Partial").withParameters()) .setPropertyEnum(generatedBuilder.nestedType("Property").withParameters()) + .setRebuildableType(generatedBuilder.nestedType("Rebuildable").withParameters()) .setType(person.withParameters()) .setValueType(generatedBuilder.nestedType("Value").withParameters()) .build(); diff --git a/src/test/java/org/inferred/freebuilder/processor/property/JavaUtilOptionalSourceTest.java b/src/test/java/org/inferred/freebuilder/processor/property/JavaUtilOptionalSourceTest.java index 6f11f0e76..831472f85 100644 --- a/src/test/java/org/inferred/freebuilder/processor/property/JavaUtilOptionalSourceTest.java +++ b/src/test/java/org/inferred/freebuilder/processor/property/JavaUtilOptionalSourceTest.java @@ -57,9 +57,17 @@ public void testSource() { + "derived from the API of {@link Person}. */", "abstract class Person_Builder {", "", - " /** Creates a new builder using {@code value} as a template. */", + " /**", + " * Creates a new builder using {@code value} as a template.", + " *", + " *

If {@code value} is a partial, the builder will return more partials.", + " */", " public static Person.Builder from(Person value) {", - " return new Person.Builder().mergeFrom(value);", + " if (value instanceof Rebuildable) {", + " return ((Rebuildable) value).toBuilder();", + " } else {", + " return new Person.Builder().mergeFrom(value);", + " }", " }", "", " // Store a nullable object instead of an Optional. Escape analysis then", @@ -249,6 +257,12 @@ public void testSource() { + "State checking will not", " * be performed.", " *", + " *

The builder returned by {@link Person.Builder#from(Person)} will propagate the " + + "partial", + " * status of its input, overriding {@link Person.Builder#build() build()} to return " + + "another", + " * partial. This allows for robust tests of modify-rebuild code.", + " *", " *

Partials should only ever be used in tests. " + "They permit writing robust test cases that won't", " * fail if this type gains more application-level constraints " @@ -261,7 +275,11 @@ public void testSource() { " return new Partial(this);", " }", "", - " private static final class Value extends Person {", + " private abstract static class Rebuildable extends Person {", + " public abstract Person.Builder toBuilder();", + " }", + "", + " private static final class Value extends Rebuildable {", " // Store a nullable object instead of an Optional. Escape analysis then", " // allows the JVM to optimize away the Optional objects created by our", " // getter method.", @@ -287,6 +305,14 @@ public void testSource() { " }", "", " @Override", + " public Person.Builder toBuilder() {", + " Person_Builder builder = new Person.Builder();", + " builder.name = name;", + " builder.age = age;", + " return (Person.Builder) builder;", + " }", + "", + " @Override", " public boolean equals(Object obj) {", " if (!(obj instanceof Value)) {", " return false;", @@ -315,7 +341,7 @@ public void testSource() { " }", " }", "", - " private static final class Partial extends Person {", + " private static final class Partial extends Rebuildable {", " // Store a nullable object instead of an Optional. Escape analysis then", " // allows the JVM to optimize away the Optional objects created by our", " // getter method.", @@ -340,6 +366,21 @@ public void testSource() { " return Optional.ofNullable(age);", " }", "", + " private static class PartialBuilder extends Person.Builder {", + " @Override", + " public Person build() {", + " return buildPartial();", + " }", + " }", + "", + " @Override", + " public Person.Builder toBuilder() {", + " Person_Builder builder = new PartialBuilder();", + " builder.name = name;", + " builder.age = age;", + " return (Person.Builder) builder;", + " }", + "", " @Override", " public boolean equals(Object obj) {", " if (!(obj instanceof Partial)) {", @@ -387,6 +428,7 @@ private static GeneratedBuilder builder() { .setInterfaceType(false) .setPartialType(generatedBuilder.nestedType("Partial").withParameters()) .setPropertyEnum(generatedBuilder.nestedType("Property").withParameters()) + .setRebuildableType(generatedBuilder.nestedType("Rebuildable").withParameters()) .setType(person.withParameters()) .setValueType(generatedBuilder.nestedType("Value").withParameters()) .build(); diff --git a/src/test/java/org/inferred/freebuilder/processor/property/ListSourceTest.java b/src/test/java/org/inferred/freebuilder/processor/property/ListSourceTest.java index c39a76160..8ff069545 100644 --- a/src/test/java/org/inferred/freebuilder/processor/property/ListSourceTest.java +++ b/src/test/java/org/inferred/freebuilder/processor/property/ListSourceTest.java @@ -62,9 +62,17 @@ public void test_noGuava() { + "{@link Person}. */", "abstract class Person_Builder {", "", - " /** Creates a new builder using {@code value} as a template. */", + " /**", + " * Creates a new builder using {@code value} as a template.", + " *", + " *

If {@code value} is a partial, the builder will return more partials.", + " */", " public static Person.Builder from(Person value) {", - " return new Person.Builder().mergeFrom(value);", + " if (value instanceof Rebuildable) {", + " return ((Rebuildable) value).toBuilder();", + " } else {", + " return new Person.Builder().mergeFrom(value);", + " }", " }", "", " private final ArrayList name = new ArrayList<>();", @@ -316,6 +324,12 @@ public void test_noGuava() { + "State checking will not", " * be performed.", " *", + " *

The builder returned by {@link Person.Builder#from(Person)} will propagate the " + + "partial", + " * status of its input, overriding {@link Person.Builder#build() build()} to return " + + "another", + " * partial. This allows for robust tests of modify-rebuild code.", + " *", " *

Partials should only ever be used in tests. " + "They permit writing robust test cases that won't", " * fail if this type gains more application-level constraints " @@ -327,7 +341,11 @@ public void test_noGuava() { " return new Partial(this);", " }", "", - " private static final class Value extends Person {", + " private abstract static class Rebuildable extends Person {", + " public abstract Person.Builder toBuilder();", + " }", + "", + " private static final class Value extends Rebuildable {", " private final List name;", " private final List age;", "", @@ -347,6 +365,14 @@ public void test_noGuava() { " }", "", " @Override", + " public Person.Builder toBuilder() {", + " Person_Builder builder = new Person.Builder();", + " builder.name.addAll(name);", + " builder.age.addAll(age);", + " return (Person.Builder) builder;", + " }", + "", + " @Override", " public boolean equals(Object obj) {", " if (!(obj instanceof Value)) {", " return false;", @@ -366,7 +392,7 @@ public void test_noGuava() { " }", " }", "", - " private static final class Partial extends Person {", + " private static final class Partial extends Rebuildable {", " private final List name;", " private final List age;", "", @@ -385,6 +411,21 @@ public void test_noGuava() { " return age;", " }", "", + " private static class PartialBuilder extends Person.Builder {", + " @Override", + " public Person build() {", + " return buildPartial();", + " }", + " }", + "", + " @Override", + " public Person.Builder toBuilder() {", + " Person_Builder builder = new PartialBuilder();", + " builder.name.addAll(name);", + " builder.age.addAll(age);", + " return (Person.Builder) builder;", + " }", + "", " @Override", " public boolean equals(Object obj) {", " if (!(obj instanceof Partial)) {", @@ -444,9 +485,17 @@ public void test_guava() { + "derived from the API of {@link Person}. */", "abstract class Person_Builder {", "", - " /** Creates a new builder using {@code value} as a template. */", + " /**", + " * Creates a new builder using {@code value} as a template.", + " *", + " *

If {@code value} is a partial, the builder will return more partials.", + " */", " public static Person.Builder from(Person value) {", - " return new Person.Builder().mergeFrom(value);", + " if (value instanceof Rebuildable) {", + " return ((Rebuildable) value).toBuilder();", + " } else {", + " return new Person.Builder().mergeFrom(value);", + " }", " }", "", " private List name = ImmutableList.of();", @@ -734,6 +783,12 @@ public void test_guava() { + "State checking will not", " * be performed.", " *", + " *

The builder returned by {@link Person.Builder#from(Person)} will propagate the " + + "partial", + " * status of its input, overriding {@link Person.Builder#build() build()} to return " + + "another", + " * partial. This allows for robust tests of modify-rebuild code.", + " *", " *

Partials should only ever be used in tests. " + "They permit writing robust test cases that won't", " * fail if this type gains more application-level constraints " @@ -746,7 +801,11 @@ public void test_guava() { " return new Partial(this);", " }", "", - " private static final class Value extends Person {", + " private abstract static class Rebuildable extends Person {", + " public abstract Person.Builder toBuilder();", + " }", + "", + " private static final class Value extends Rebuildable {", " private final ImmutableList name;", " private final ImmutableList age;", "", @@ -766,6 +825,14 @@ public void test_guava() { " }", "", " @Override", + " public Person.Builder toBuilder() {", + " Person_Builder builder = new Person.Builder();", + " builder.name = name;", + " builder.age = age;", + " return (Person.Builder) builder;", + " }", + "", + " @Override", " public boolean equals(Object obj) {", " if (!(obj instanceof Value)) {", " return false;", @@ -785,7 +852,7 @@ public void test_guava() { " }", " }", "", - " private static final class Partial extends Person {", + " private static final class Partial extends Rebuildable {", " private final ImmutableList name;", " private final ImmutableList age;", "", @@ -804,6 +871,21 @@ public void test_guava() { " return age;", " }", "", + " private static class PartialBuilder extends Person.Builder {", + " @Override", + " public Person build() {", + " return buildPartial();", + " }", + " }", + "", + " @Override", + " public Person.Builder toBuilder() {", + " Person_Builder builder = new PartialBuilder();", + " builder.name = name;", + " builder.age = age;", + " return (Person.Builder) builder;", + " }", + "", " @Override", " public boolean equals(Object obj) {", " if (!(obj instanceof Partial)) {", @@ -846,6 +928,7 @@ private static GeneratedBuilder builder() { .setInterfaceType(false) .setPartialType(generatedBuilder.nestedType("Partial").withParameters()) .setPropertyEnum(generatedBuilder.nestedType("Property").withParameters()) + .setRebuildableType(generatedBuilder.nestedType("Rebuildable").withParameters()) .setType(person.withParameters()) .setValueType(generatedBuilder.nestedType("Value").withParameters()) .build(); diff --git a/src/test/java/org/inferred/freebuilder/processor/property/MapSourceTest.java b/src/test/java/org/inferred/freebuilder/processor/property/MapSourceTest.java index 1c595bd3d..aff3552b7 100644 --- a/src/test/java/org/inferred/freebuilder/processor/property/MapSourceTest.java +++ b/src/test/java/org/inferred/freebuilder/processor/property/MapSourceTest.java @@ -59,9 +59,17 @@ public void test_noGuava() { + "derived from the API of {@link Person}. */", "abstract class Person_Builder {", "", - " /** Creates a new builder using {@code value} as a template. */", + " /**", + " * Creates a new builder using {@code value} as a template.", + " *", + " *

If {@code value} is a partial, the builder will return more partials.", + " */", " public static Person.Builder from(Person value) {", - " return new Person.Builder().mergeFrom(value);", + " if (value instanceof Rebuildable) {", + " return ((Rebuildable) value).toBuilder();", + " } else {", + " return new Person.Builder().mergeFrom(value);", + " }", " }", "", " private final LinkedHashMap name = new LinkedHashMap<>();", @@ -189,6 +197,12 @@ public void test_noGuava() { + "State checking will not", " * be performed.", " *", + " *

The builder returned by {@link Person.Builder#from(Person)} will propagate the " + + "partial", + " * status of its input, overriding {@link Person.Builder#build() build()} to return " + + "another", + " * partial. This allows for robust tests of modify-rebuild code.", + " *", " *

Partials should only ever be used in tests. " + "They permit writing robust test cases that won't", " * fail if this type gains more application-level constraints " @@ -200,7 +214,11 @@ public void test_noGuava() { " return new Partial(this);", " }", "", - " private static final class Value extends Person {", + " private abstract static class Rebuildable extends Person {", + " public abstract Person.Builder toBuilder();", + " }", + "", + " private static final class Value extends Rebuildable {", " private final Map name;", "", " private Value(Person_Builder builder) {", @@ -213,6 +231,13 @@ public void test_noGuava() { " }", "", " @Override", + " public Person.Builder toBuilder() {", + " Person_Builder builder = new Person.Builder();", + " builder.name.putAll(name);", + " return (Person.Builder) builder;", + " }", + "", + " @Override", " public boolean equals(Object obj) {", " if (!(obj instanceof Value)) {", " return false;", @@ -232,7 +257,7 @@ public void test_noGuava() { " }", " }", "", - " private static final class Partial extends Person {", + " private static final class Partial extends Rebuildable {", " private final Map name;", "", " Partial(Person_Builder builder) {", @@ -244,6 +269,20 @@ public void test_noGuava() { " return name;", " }", "", + " private static class PartialBuilder extends Person.Builder {", + " @Override", + " public Person build() {", + " return buildPartial();", + " }", + " }", + "", + " @Override", + " public Person.Builder toBuilder() {", + " Person_Builder builder = new PartialBuilder();", + " builder.name.putAll(name);", + " return (Person.Builder) builder;", + " }", + "", " @Override", " public boolean equals(Object obj) {", " if (!(obj instanceof Partial)) {", @@ -298,9 +337,17 @@ public void test_guava() { + "derived from the API of {@link Person}. */", "abstract class Person_Builder {", "", - " /** Creates a new builder using {@code value} as a template. */", + " /**", + " * Creates a new builder using {@code value} as a template.", + " *", + " *

If {@code value} is a partial, the builder will return more partials.", + " */", " public static Person.Builder from(Person value) {", - " return new Person.Builder().mergeFrom(value);", + " if (value instanceof Rebuildable) {", + " return ((Rebuildable) value).toBuilder();", + " } else {", + " return new Person.Builder().mergeFrom(value);", + " }", " }", "", " private final LinkedHashMap name = new LinkedHashMap<>();", @@ -427,6 +474,12 @@ public void test_guava() { + "State checking will not", " * be performed.", " *", + " *

The builder returned by {@link Person.Builder#from(Person)} will propagate the " + + "partial", + " * status of its input, overriding {@link Person.Builder#build() build()} to return " + + "another", + " * partial. This allows for robust tests of modify-rebuild code.", + " *", " *

Partials should only ever be used in tests. " + "They permit writing robust test cases that won't", " * fail if this type gains more application-level constraints " @@ -439,7 +492,11 @@ public void test_guava() { " return new Partial(this);", " }", "", - " private static final class Value extends Person {", + " private abstract static class Rebuildable extends Person {", + " public abstract Person.Builder toBuilder();", + " }", + "", + " private static final class Value extends Rebuildable {", " private final ImmutableMap name;", "", " private Value(Person_Builder builder) {", @@ -452,6 +509,13 @@ public void test_guava() { " }", "", " @Override", + " public Person.Builder toBuilder() {", + " Person_Builder builder = new Person.Builder();", + " builder.name.putAll(name);", + " return (Person.Builder) builder;", + " }", + "", + " @Override", " public boolean equals(Object obj) {", " if (!(obj instanceof Value)) {", " return false;", @@ -471,7 +535,7 @@ public void test_guava() { " }", " }", "", - " private static final class Partial extends Person {", + " private static final class Partial extends Rebuildable {", " private final ImmutableMap name;", "", " Partial(Person_Builder builder) {", @@ -483,6 +547,20 @@ public void test_guava() { " return name;", " }", "", + " private static class PartialBuilder extends Person.Builder {", + " @Override", + " public Person build() {", + " return buildPartial();", + " }", + " }", + "", + " @Override", + " public Person.Builder toBuilder() {", + " Person_Builder builder = new PartialBuilder();", + " builder.name.putAll(name);", + " return (Person.Builder) builder;", + " }", + "", " @Override", " public boolean equals(Object obj) {", " if (!(obj instanceof Partial)) {", @@ -520,6 +598,7 @@ private static GeneratedBuilder builder() { .setInterfaceType(false) .setPartialType(generatedBuilder.nestedType("Partial").withParameters()) .setPropertyEnum(generatedBuilder.nestedType("Property").withParameters()) + .setRebuildableType(generatedBuilder.nestedType("Rebuildable").withParameters()) .setType(person.withParameters()) .setValueType(generatedBuilder.nestedType("Value").withParameters()) .build(); diff --git a/src/test/java/org/inferred/freebuilder/processor/property/NullableSourceTest.java b/src/test/java/org/inferred/freebuilder/processor/property/NullableSourceTest.java index 00d4d46cc..c278794be 100644 --- a/src/test/java/org/inferred/freebuilder/processor/property/NullableSourceTest.java +++ b/src/test/java/org/inferred/freebuilder/processor/property/NullableSourceTest.java @@ -53,9 +53,17 @@ public void testSource() { + "derived from the API of {@link Person}. */", "abstract class Person_Builder {", "", - " /** Creates a new builder using {@code value} as a template. */", + " /**", + " * Creates a new builder using {@code value} as a template.", + " *", + " *

If {@code value} is a partial, the builder will return more partials.", + " */", " public static Person.Builder from(Person value) {", - " return new Person.Builder().mergeFrom(value);", + " if (value instanceof Rebuildable) {", + " return ((Rebuildable) value).toBuilder();", + " } else {", + " return new Person.Builder().mergeFrom(value);", + " }", " }", "", " @Nullable private String name = null;", @@ -182,6 +190,12 @@ public void testSource() { + "State checking will not", " * be performed.", " *", + " *

The builder returned by {@link Person.Builder#from(Person)} will propagate the " + + "partial", + " * status of its input, overriding {@link Person.Builder#build() build()} to return " + + "another", + " * partial. This allows for robust tests of modify-rebuild code.", + " *", " *

Partials should only ever be used in tests. " + "They permit writing robust test cases that won't", " * fail if this type gains more application-level constraints " @@ -194,7 +208,11 @@ public void testSource() { " return new Partial(this);", " }", "", - " private static final class Value extends Person {", + " private abstract static class Rebuildable extends Person {", + " public abstract Person.Builder toBuilder();", + " }", + "", + " private static final class Value extends Rebuildable {", " @Nullable private final String name;", " @Nullable private final Integer age;", "", @@ -216,6 +234,14 @@ public void testSource() { " }", "", " @Override", + " public Person.Builder toBuilder() {", + " Person_Builder builder = new Person.Builder();", + " builder.name = name;", + " builder.age = age;", + " return (Person.Builder) builder;", + " }", + "", + " @Override", " public boolean equals(Object obj) {", " if (!(obj instanceof Value)) {", " return false;", @@ -244,7 +270,7 @@ public void testSource() { " }", " }", "", - " private static final class Partial extends Person {", + " private static final class Partial extends Rebuildable {", " @Nullable private final String name;", " @Nullable private final Integer age;", "", @@ -265,6 +291,21 @@ public void testSource() { " return age;", " }", "", + " private static class PartialBuilder extends Person.Builder {", + " @Override", + " public Person build() {", + " return buildPartial();", + " }", + " }", + "", + " @Override", + " public Person.Builder toBuilder() {", + " Person_Builder builder = new PartialBuilder();", + " builder.name = name;", + " builder.age = age;", + " return (Person.Builder) builder;", + " }", + "", " @Override", " public boolean equals(Object obj) {", " if (!(obj instanceof Partial)) {", @@ -310,6 +351,7 @@ private static GeneratedBuilder builder() { .setInterfaceType(false) .setPartialType(generatedBuilder.nestedType("Partial").withParameters()) .setPropertyEnum(generatedBuilder.nestedType("Property").withParameters()) + .setRebuildableType(generatedBuilder.nestedType("Rebuildable").withParameters()) .setType(person.withParameters()) .setValueType(generatedBuilder.nestedType("Value").withParameters()) .build(); diff --git a/src/test/java/org/inferred/freebuilder/processor/property/PrimitiveOptionalSourceTest.java b/src/test/java/org/inferred/freebuilder/processor/property/PrimitiveOptionalSourceTest.java index 2fbe7f518..b0e94d708 100644 --- a/src/test/java/org/inferred/freebuilder/processor/property/PrimitiveOptionalSourceTest.java +++ b/src/test/java/org/inferred/freebuilder/processor/property/PrimitiveOptionalSourceTest.java @@ -56,9 +56,17 @@ public void testSource() { + "derived from the API of {@link Item}. */", "abstract class Item_Builder {", "", - " /** Creates a new builder using {@code value} as a template. */", + " /**", + " * Creates a new builder using {@code value} as a template.", + " *", + " *

If {@code value} is a partial, the builder will return more partials.", + " */", " public static Item.Builder from(Item value) {", - " return new Item.Builder().mergeFrom(value);", + " if (value instanceof Rebuildable) {", + " return ((Rebuildable) value).toBuilder();", + " } else {", + " return new Item.Builder().mergeFrom(value);", + " }", " }", "", " private OptionalInt cost = OptionalInt.empty();", @@ -217,6 +225,12 @@ public void testSource() { + "State checking will not be", " * performed.", " *", + " *

The builder returned by {@link Item.Builder#from(Item)} will propagate the " + + "partial status of", + " * its input, overriding {@link Item.Builder#build() build()} to return another " + + "partial. This", + " * allows for robust tests of modify-rebuild code.", + " *", " *

Partials should only ever be used in tests. " + "They permit writing robust test cases that won't", " * fail if this type gains more application-level constraints " @@ -229,7 +243,11 @@ public void testSource() { " return new Partial(this);", " }", "", - " private static final class Value extends Item {", + " private abstract static class Rebuildable extends Item {", + " public abstract Item.Builder toBuilder();", + " }", + "", + " private static final class Value extends Rebuildable {", " private final OptionalInt cost;", " private final OptionalDouble tax;", "", @@ -249,6 +267,14 @@ public void testSource() { " }", "", " @Override", + " public Item.Builder toBuilder() {", + " Item_Builder builder = new Item.Builder();", + " builder.cost = cost;", + " builder.tax = tax;", + " return (Item.Builder) builder;", + " }", + "", + " @Override", " public boolean equals(Object obj) {", " if (!(obj instanceof Value)) {", " return false;", @@ -277,7 +303,7 @@ public void testSource() { " }", " }", "", - " private static final class Partial extends Item {", + " private static final class Partial extends Rebuildable {", " private final OptionalInt cost;", " private final OptionalDouble tax;", "", @@ -296,6 +322,21 @@ public void testSource() { " return tax;", " }", "", + " private static class PartialBuilder extends Item.Builder {", + " @Override", + " public Item build() {", + " return buildPartial();", + " }", + " }", + "", + " @Override", + " public Item.Builder toBuilder() {", + " Item_Builder builder = new PartialBuilder();", + " builder.cost = cost;", + " builder.tax = tax;", + " return (Item.Builder) builder;", + " }", + "", " @Override", " public boolean equals(Object obj) {", " if (!(obj instanceof Partial)) {", @@ -342,6 +383,7 @@ private static GeneratedBuilder builder() { .setInterfaceType(false) .setPartialType(generatedBuilder.nestedType("Partial").withParameters()) .setPropertyEnum(generatedBuilder.nestedType("Property").withParameters()) + .setRebuildableType(generatedBuilder.nestedType("Rebuildable").withParameters()) .setType(person.withParameters()) .setValueType(generatedBuilder.nestedType("Value").withParameters()) .build(); diff --git a/src/test/java/org/inferred/freebuilder/processor/property/SetSourceTest.java b/src/test/java/org/inferred/freebuilder/processor/property/SetSourceTest.java index 648a98ca9..b3b91a4c0 100644 --- a/src/test/java/org/inferred/freebuilder/processor/property/SetSourceTest.java +++ b/src/test/java/org/inferred/freebuilder/processor/property/SetSourceTest.java @@ -60,9 +60,17 @@ public void test_noGuava() { + "derived from the API of {@link Person}. */", "abstract class Person_Builder {", "", - " /** Creates a new builder using {@code value} as a template. */", + " /**", + " * Creates a new builder using {@code value} as a template.", + " *", + " *

If {@code value} is a partial, the builder will return more partials.", + " */", " public static Person.Builder from(Person value) {", - " return new Person.Builder().mergeFrom(value);", + " if (value instanceof Rebuildable) {", + " return ((Rebuildable) value).toBuilder();", + " } else {", + " return new Person.Builder().mergeFrom(value);", + " }", " }", "", " private final LinkedHashSet name = new LinkedHashSet<>();", @@ -223,6 +231,12 @@ public void test_noGuava() { + "State checking will not", " * be performed.", " *", + " *

The builder returned by {@link Person.Builder#from(Person)} will propagate the " + + "partial", + " * status of its input, overriding {@link Person.Builder#build() build()} to return " + + "another", + " * partial. This allows for robust tests of modify-rebuild code.", + " *", " *

Partials should only ever be used in tests. " + "They permit writing robust test cases that won't", " * fail if this type gains more application-level constraints " @@ -234,7 +248,11 @@ public void test_noGuava() { " return new Partial(this);", " }", "", - " private static final class Value extends Person {", + " private abstract static class Rebuildable extends Person {", + " public abstract Person.Builder toBuilder();", + " }", + "", + " private static final class Value extends Rebuildable {", " private final Set name;", "", " private Value(Person_Builder builder) {", @@ -247,6 +265,13 @@ public void test_noGuava() { " }", "", " @Override", + " public Person.Builder toBuilder() {", + " Person_Builder builder = new Person.Builder();", + " builder.name.addAll(name);", + " return (Person.Builder) builder;", + " }", + "", + " @Override", " public boolean equals(Object obj) {", " if (!(obj instanceof Value)) {", " return false;", @@ -266,7 +291,7 @@ public void test_noGuava() { " }", " }", "", - " private static final class Partial extends Person {", + " private static final class Partial extends Rebuildable {", " private final Set name;", "", " Partial(Person_Builder builder) {", @@ -278,6 +303,20 @@ public void test_noGuava() { " return name;", " }", "", + " private static class PartialBuilder extends Person.Builder {", + " @Override", + " public Person build() {", + " return buildPartial();", + " }", + " }", + "", + " @Override", + " public Person.Builder toBuilder() {", + " Person_Builder builder = new PartialBuilder();", + " builder.name.addAll(name);", + " return (Person.Builder) builder;", + " }", + "", " @Override", " public boolean equals(Object obj) {", " if (!(obj instanceof Partial)) {", @@ -334,9 +373,17 @@ public void test_guava() { + "derived from the API of {@link Person}. */", "abstract class Person_Builder {", "", - " /** Creates a new builder using {@code value} as a template. */", + " /**", + " * Creates a new builder using {@code value} as a template.", + " *", + " *

If {@code value} is a partial, the builder will return more partials.", + " */", " public static Person.Builder from(Person value) {", - " return new Person.Builder().mergeFrom(value);", + " if (value instanceof Rebuildable) {", + " return ((Rebuildable) value).toBuilder();", + " } else {", + " return new Person.Builder().mergeFrom(value);", + " }", " }", "", " private Set name = ImmutableSet.of();", @@ -519,6 +566,12 @@ public void test_guava() { + "State checking will not", " * be performed.", " *", + " *

The builder returned by {@link Person.Builder#from(Person)} will propagate the " + + "partial", + " * status of its input, overriding {@link Person.Builder#build() build()} to return " + + "another", + " * partial. This allows for robust tests of modify-rebuild code.", + " *", " *

Partials should only ever be used in tests. " + "They permit writing robust test cases that won't", " * fail if this type gains more application-level constraints " @@ -531,7 +584,11 @@ public void test_guava() { " return new Partial(this);", " }", "", - " private static final class Value extends Person {", + " private abstract static class Rebuildable extends Person {", + " public abstract Person.Builder toBuilder();", + " }", + "", + " private static final class Value extends Rebuildable {", " private final ImmutableSet name;", "", " private Value(Person_Builder builder) {", @@ -544,6 +601,13 @@ public void test_guava() { " }", "", " @Override", + " public Person.Builder toBuilder() {", + " Person_Builder builder = new Person.Builder();", + " builder.name = name;", + " return (Person.Builder) builder;", + " }", + "", + " @Override", " public boolean equals(Object obj) {", " if (!(obj instanceof Value)) {", " return false;", @@ -563,7 +627,7 @@ public void test_guava() { " }", " }", "", - " private static final class Partial extends Person {", + " private static final class Partial extends Rebuildable {", " private final ImmutableSet name;", "", " Partial(Person_Builder builder) {", @@ -575,6 +639,20 @@ public void test_guava() { " return name;", " }", "", + " private static class PartialBuilder extends Person.Builder {", + " @Override", + " public Person build() {", + " return buildPartial();", + " }", + " }", + "", + " @Override", + " public Person.Builder toBuilder() {", + " Person_Builder builder = new PartialBuilder();", + " builder.name = name;", + " return (Person.Builder) builder;", + " }", + "", " @Override", " public boolean equals(Object obj) {", " if (!(obj instanceof Partial)) {", @@ -612,6 +690,7 @@ private static GeneratedBuilder builder() { .setInterfaceType(false) .setPartialType(generatedBuilder.nestedType("Partial").withParameters()) .setPropertyEnum(generatedBuilder.nestedType("Property").withParameters()) + .setRebuildableType(generatedBuilder.nestedType("Rebuildable").withParameters()) .setType(person.withParameters()) .setValueType(generatedBuilder.nestedType("Value").withParameters()) .build();