From b34267af7feb6c698be94fca4f00629765d198cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89amonn=20McManus?= Date: Wed, 8 Apr 2026 13:28:58 -0700 Subject: [PATCH] Add a demo to `AutoBuilderTest` for null properties without defaults. RELNOTES=n/a PiperOrigin-RevId: 896670565 --- .../google/auto/value/AutoBuilderTest.java | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/value/src/it/functional/src/test/java/com/google/auto/value/AutoBuilderTest.java b/value/src/it/functional/src/test/java/com/google/auto/value/AutoBuilderTest.java index f54d2fc0ce..567789729b 100644 --- a/value/src/it/functional/src/test/java/com/google/auto/value/AutoBuilderTest.java +++ b/value/src/it/functional/src/test/java/com/google/auto/value/AutoBuilderTest.java @@ -16,6 +16,7 @@ package com.google.auto.value; import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; import static com.google.common.truth.Truth.assertThat; import static java.lang.annotation.ElementType.TYPE_USE; import static java.util.stream.Collectors.joining; @@ -913,4 +914,87 @@ public void builderExposingNullableAsOptionalLong() { assertThat(builtEmpty.aLong()).isEmpty(); assertThat(builtEmpty.toBuilder().build().aLong()).isEmpty(); } + + /** + * Demo for how to require that null or optional properties have to be set explicitly. + * + *

Normally properties of type {@code @Nullable Foo} or {@code Optional} are optional in + * the builder, meaning that you can call {@code build()} without setting them. If you want to + * require that they be set explicitly, you need a subterfuge like the one used here. AutoBuilder + * allows the {@code foo} property to be set either via {@code setFoo(Foo)} or {@code foo(Foo)}. + * In the demo, we expose {@code foo(@Nullable Foo)} in the public API, but the abstract method + * that we get AutoBuilder to implement is the package-private {@code setFoo(@Nullable Foo)}. The + * public {@code foo} calls this package-private {@code setFoo} but also records whether it was + * called. Then we do a similar trick with {@code build()} in the public API calling the + * package-private abstract {@code autoBuild()} that AutoBuilder generates, but first checking + * that all required properties have been set. + * + *

Unfortunately it's harder to do the same thing with {@code AutoValue.Builder} because it has + * a rule whereby all the abstract setter methods must follow the same naming convention, either + * all {@code setFoo} or all {@code foo}. So if you want to use the trick here you may end up + * having to define pairs of {@code foo} and {@code setFoo} methods for every property {@code + * foo}, even ones where you don't need to track whether they were set because the generated + * builder code will do it for you. + */ + public static class NoDefaultDemo { + final @Nullable String nullableString; + final Optional optionalString; + + NoDefaultDemo(@Nullable String nullableString, Optional optionalString) { + this.nullableString = nullableString; + this.optionalString = optionalString; + } + + public static Builder builder() { + return new AutoBuilder_AutoBuilderTest_NoDefaultDemo_Builder(); + } + + @AutoBuilder + public abstract static class Builder { + private boolean nullableStringSet = false; + private boolean optionalStringSet = false; + + abstract Builder setNullableString(@Nullable String nullableString); + + abstract Builder setOptionalString(Optional optionalString); + + public Builder nullableString(@Nullable String nullableString) { + nullableStringSet = true; + return setNullableString(nullableString); + } + + public Builder optionalString(Optional optionalString) { + optionalStringSet = true; + return setOptionalString(optionalString); + } + + abstract NoDefaultDemo autoBuild(); + + public NoDefaultDemo build() { + checkState(nullableStringSet, "nullableString is required"); + checkState(optionalStringSet, "optionalString is required"); + return autoBuild(); + } + } + } + + @Test + public void noDefaultDemo() { + NoDefaultDemo builtWithRealValues = + NoDefaultDemo.builder().nullableString("foo").optionalString(Optional.of("bar")).build(); + assertThat(builtWithRealValues.nullableString).isEqualTo("foo"); + assertThat(builtWithRealValues.optionalString).hasValue("bar"); + + NoDefaultDemo builtWithDefaultValues = + NoDefaultDemo.builder().nullableString(null).optionalString(Optional.empty()).build(); + assertThat(builtWithDefaultValues.nullableString).isNull(); + assertThat(builtWithDefaultValues.optionalString).isEmpty(); + + NoDefaultDemo.Builder builder1 = NoDefaultDemo.builder().nullableString("foo"); + IllegalStateException e1 = assertThrows(IllegalStateException.class, () -> builder1.build()); + assertThat(e1).hasMessageThat().isEqualTo("optionalString is required"); + NoDefaultDemo.Builder builder2 = NoDefaultDemo.builder().optionalString(Optional.of("bar")); + IllegalStateException e2 = assertThrows(IllegalStateException.class, () -> builder2.build()); + assertThat(e2).hasMessageThat().isEqualTo("nullableString is required"); + } }