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

Commit

Permalink
Support passing partials to Builder.from
Browse files Browse the repository at this point in the history
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`.
  • Loading branch information
alicederyn committed Jan 27, 2019
1 parent 1f3e1a4 commit bc7d2f2
Show file tree
Hide file tree
Showing 19 changed files with 1,100 additions and 124 deletions.
32 changes: 24 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Property, PropertyCodeGenerator> generatorsByProperty = pickPropertyGenerators(
type, baseDatatype, builder, removeNonGetterMethods(builder, methods));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>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 {
Expand Down Expand Up @@ -380,6 +388,10 @@ public BuildableType build() {
* will not be performed. Unset properties will throw an {@link UnsupportedOperationException}
* when accessed via the partial object.
*
* <p>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.
*
* <p>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.
Expand All @@ -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;
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<TypeClass> getRebuildableType();

/** Returns the Property enum that may be generated. */
public abstract TypeClass getPropertyEnum();

Expand Down
Loading

0 comments on commit bc7d2f2

Please sign in to comment.