Skip to content
This repository was archived by the owner on Oct 16, 2024. It is now read-only.

Commit 03bc2c8

Browse files
authored
#407 Support passing partials to Builder.from
2 parents 1f3e1a4 + bc7d2f2 commit 03bc2c8

19 files changed

+1100
-124
lines changed

README.md

+24-8
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ _Automatic generation of the Builder pattern for Java 1.8+_
4141
- [Eclipse](#eclipse)
4242
- [IntelliJ](#intellij)
4343
- [Release notes](#release-notes)
44+
- [2.3—From method testability](#23from-method-testability)
4445
- [2.2—Primitive optional types](#22primitive-optional-types)
4546
- [2.1—Lists of buildable types](#21lists-of-buildable-types)
4647
- [Upgrading from v1](#upgrading-from-v1)
@@ -134,7 +135,7 @@ If you write the Person interface shown above, you get:
134135
* getters (throwing `IllegalStateException` for unset fields)
135136
* setters
136137
* lambda-accepting mapper methods
137-
* `from` and `mergeFrom` methods to copy data from existing values or builders
138+
* `mergeFrom` and static `from` methods to copy data from existing values or builders
138139
* a `build` method that verifies all fields have been set
139140
* [see below for default values and constraint checking](#defaults-and-constraints)
140141
* An implementation of `Person` with:
@@ -144,8 +145,6 @@ If you write the Person interface shown above, you get:
144145
* `UnsupportedOperationException`-throwing getters for unset fields
145146
* `toString`
146147
* `equals` and `hashCode`
147-
* a special `toBuilder()` method that preserves the 'partial' aspect on newly-minted
148-
Person instances
149148

150149

151150
```java
@@ -578,17 +577,20 @@ restrictions of the value type, partials can reduce the fragility of your test
578577
suite, allowing you to add new required fields or other constraints to an
579578
existing value type without breaking swathes of test code.
580579

581-
To allow robust tests of modify-rebuild code, the `toBuilder()` method on
582-
partials returns a subclass of Builder that returns partials from the `build()`
583-
method.
580+
To allow robust tests of modify-rebuild code, Builders created from partials
581+
(either via the static `Builder.from` method or the optional `toBuilder()`
582+
method) will override `build()` to instead call `buildPartial()`.
584583

585584
```java
586585
Person anotherPerson = person.toBuilder().name("Bob").build();
587586
System.out.println(anotherPerson); // prints: partial Person{name=Bob}
588587
```
589588

590-
(Note the `from` and `mergeFrom` methods do not behave this way; instead, they
591-
will throw an UnsupportedOperationException if given a partial.)
589+
This "infectious" behavior of partials is another reason to confine them to
590+
test code.
591+
592+
(Note the `mergeFrom` method does not behave this way; instead, it will throw an
593+
UnsupportedOperationException if given a partial.)
592594

593595

594596
### Jackson
@@ -725,6 +727,20 @@ directory** setting) and select **Mark Directory As > Generated Sources Root**.
725727
Release notes
726728
-------------
727729

730+
### 2.3—From method testability
731+
732+
FreeBuilder 2.3 now allows partials to be passed to the static `Builder.from`
733+
method. Previously this would have thrown an UnsupportedOperationException
734+
if any field was unset; now, as with the optional `toBuilder` method, a Builder
735+
subclass will be returned that redirects `build` to `buildPartial`. This allows
736+
unit tests to be written that won't break if new constraints or required fields
737+
are later added to the datatype. You can restore the old behaviour by overriding
738+
the `from` method to delegate to `mergeFrom`.
739+
740+
Note that use of partials outside of tests is considered undefined behaviour
741+
by FreeBuilder, as documented here and on the `buildPartial` method. Incomplete
742+
values should always be represented by Builder instances, not partials.
743+
728744
### 2.2—Primitive optional types
729745

730746
FreeBuilder 2.2 extends its [optional value API customization](#optional-values)

src/main/java/org/inferred/freebuilder/processor/Analyser.java

+5
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,11 @@ GeneratedType analyse(TypeElement type) throws CannotGenerateCodeException {
146146
builder, constructionAndExtension.isExtensible(), methods))
147147
.setBuilderSerializable(shouldBuilderBeSerializable(builder))
148148
.setBuilder(Type.from(builder));
149+
if (datatypeBuilder.getBuilderFactory().isPresent()
150+
&& !datatypeBuilder.getHasToBuilderMethod()) {
151+
datatypeBuilder.setRebuildableType(
152+
generatedBuilder.nestedType("Rebuildable").withParameters(typeParameters));
153+
}
149154
Datatype baseDatatype = datatypeBuilder.build();
150155
Map<Property, PropertyCodeGenerator> generatorsByProperty = pickPropertyGenerators(
151156
type, baseDatatype, builder, removeNonGetterMethods(builder, methods));

src/main/java/org/inferred/freebuilder/processor/BuildableType_Builder.java

+54-4
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,17 @@
2222
@Generated("org.inferred.freebuilder.processor.Processor")
2323
abstract class BuildableType_Builder {
2424

25-
/** Creates a new builder using {@code value} as a template. */
25+
/**
26+
* Creates a new builder using {@code value} as a template.
27+
*
28+
* <p>If {@code value} is a partial, the builder will return more partials.
29+
*/
2630
public static BuildableType.Builder from(BuildableType value) {
27-
return new BuildableType.Builder().mergeFrom(value);
31+
if (value instanceof Rebuildable) {
32+
return ((Rebuildable) value).toBuilder();
33+
} else {
34+
return new BuildableType.Builder().mergeFrom(value);
35+
}
2836
}
2937

3038
private enum Property {
@@ -380,6 +388,10 @@ public BuildableType build() {
380388
* will not be performed. Unset properties will throw an {@link UnsupportedOperationException}
381389
* when accessed via the partial object.
382390
*
391+
* <p>The builder returned by {@link BuildableType.Builder#from(BuildableType)} will propagate the
392+
* partial status of its input, overriding {@link BuildableType.Builder#build() build()} to return
393+
* another partial. This allows for robust tests of modify-rebuild code.
394+
*
383395
* <p>Partials should only ever be used in tests. They permit writing robust test cases that won't
384396
* fail if this type gains more application-level constraints (e.g. new required fields) in
385397
* future. If you require partially complete values in production code, consider using a Builder.
@@ -389,7 +401,11 @@ public BuildableType buildPartial() {
389401
return new Partial(this);
390402
}
391403

392-
private static final class Value extends BuildableType {
404+
private abstract static class Rebuildable extends BuildableType {
405+
public abstract Builder toBuilder();
406+
}
407+
408+
private static final class Value extends Rebuildable {
393409
private final Type type;
394410
private final Type builderType;
395411
private final MergeBuilderMethod mergeBuilder;
@@ -436,6 +452,19 @@ public Excerpt suppressUnchecked() {
436452
return suppressUnchecked;
437453
}
438454

455+
@Override
456+
public Builder toBuilder() {
457+
BuildableType_Builder builder = new Builder();
458+
builder.type = type;
459+
builder.builderType = builderType;
460+
builder.mergeBuilder = mergeBuilder;
461+
builder.partialToBuilder = partialToBuilder;
462+
builder.builderFactory = builderFactory;
463+
builder.suppressUnchecked = suppressUnchecked;
464+
builder._unsetProperties.clear();
465+
return (Builder) builder;
466+
}
467+
439468
@Override
440469
public boolean equals(Object obj) {
441470
if (!(obj instanceof Value)) {
@@ -474,7 +503,7 @@ public String toString() {
474503
}
475504
}
476505

477-
private static final class Partial extends BuildableType {
506+
private static final class Partial extends Rebuildable {
478507
private final Type type;
479508
private final Type builderType;
480509
private final MergeBuilderMethod mergeBuilder;
@@ -541,6 +570,27 @@ public Excerpt suppressUnchecked() {
541570
return suppressUnchecked;
542571
}
543572

573+
private static class PartialBuilder extends Builder {
574+
@Override
575+
public BuildableType build() {
576+
return buildPartial();
577+
}
578+
}
579+
580+
@Override
581+
public Builder toBuilder() {
582+
BuildableType_Builder builder = new PartialBuilder();
583+
builder.type = type;
584+
builder.builderType = builderType;
585+
builder.mergeBuilder = mergeBuilder;
586+
builder.partialToBuilder = partialToBuilder;
587+
builder.builderFactory = builderFactory;
588+
builder.suppressUnchecked = suppressUnchecked;
589+
builder._unsetProperties.clear();
590+
builder._unsetProperties.addAll(_unsetProperties);
591+
return (Builder) builder;
592+
}
593+
544594
@Override
545595
public boolean equals(Object obj) {
546596
if (!(obj instanceof Partial)) {

src/main/java/org/inferred/freebuilder/processor/Datatype.java

+3
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@ public String toString() {
107107
/** Returns the partial value class that should be generated. */
108108
public abstract TypeClass getPartialType();
109109

110+
/** Returns the Rebuildable interface that should be generated, if any. */
111+
public abstract Optional<TypeClass> getRebuildableType();
112+
110113
/** Returns the Property enum that may be generated. */
111114
public abstract TypeClass getPropertyEnum();
112115

0 commit comments

Comments
 (0)