diff --git a/example/generated-example-builder/org/javahelpers/simple/builders/example/scoping/ScopedOwnerDtoBuilder.java b/example/generated-example-builder/org/javahelpers/simple/builders/example/scoping/ScopedOwnerDtoBuilder.java index d098f5fb..9557e1d1 100644 --- a/example/generated-example-builder/org/javahelpers/simple/builders/example/scoping/ScopedOwnerDtoBuilder.java +++ b/example/generated-example-builder/org/javahelpers/simple/builders/example/scoping/ScopedOwnerDtoBuilder.java @@ -14,6 +14,7 @@ import org.javahelpers.simple.builders.core.util.BuilderToStringStyle; import org.javahelpers.simple.builders.core.util.TrackedValue; import org.javahelpers.simple.builders.example.SponsorDto; +import org.javahelpers.simple.builders.example.SponsorDtoBuilder; import org.javahelpers.simple.builders.example.library.LibraryHelperDto; /** @@ -32,6 +33,7 @@ * .library(LibraryHelperDto::new) * .sponsor(new SponsorDto()) * .sponsor(SponsorDto::new) + * .sponsor(sponsorDtoBuilder -> sponsorDtoBuilder) * .trusted(new TrustedHelperDto()) * .trusted(TrustedHelperDto::new) * .trusted(trustedHelperDtoBuilder -> trustedHelperDtoBuilder) @@ -180,17 +182,25 @@ public ScopedOwnerDtoBuilder sponsor(SponsorDto sponsor) { } /** - * Sets the value for sponsor by executing the provided consumer. + * Sets the value for sponsor using a builder consumer that produces the value. *

* Generated from setter {@link ScopedOwnerDto#setSponsor(SponsorDto) setSponsor(SponsorDto sponsor)} * - * @param sponsorConsumer consumer providing an instance of sponsor + *

Example:

+ * + *
{@code
+   * builder.sponsor(sponsorDtoBuilder -> sponsorDtoBuilder);
+   * }
+ * + * @param sponsorBuilderConsumer consumer providing an instance of a builder for sponsor * @return current instance of builder */ - public ScopedOwnerDtoBuilder sponsor(Consumer sponsorConsumer) { - SponsorDto consumer = this.sponsor.isSet() ? this.sponsor.value() : new SponsorDto(); - sponsorConsumer.accept(consumer); - this.sponsor = changedValue(consumer); + public ScopedOwnerDtoBuilder sponsor(Consumer sponsorBuilderConsumer) { + SponsorDtoBuilder builder = this.sponsor.isSet() + ? new SponsorDtoBuilder(this.sponsor.value()) + : new SponsorDtoBuilder(); + sponsorBuilderConsumer.accept(builder); + this.sponsor = changedValue(builder.build()); return this; } @@ -217,7 +227,8 @@ public ScopedOwnerDtoBuilder sponsor(Supplier sponsorSupplier) { * Updates the current value of sponsor in place by applying the given operator, instead of reading it * out, changing it and setting it again. Useful for adjustments relative to the current value, e.g. trimming, * upper-casing, clamping or incrementing, and in combination with the With copy-and-modify flow. The - * value must have been set before (directly or via an existing instance). + * value must have been set before (directly or via an existing instance). For changing multiple values of a nested + * DTO, prefer the builder-consumer helper {@link #sponsor(Consumer)}. *

* Generated from setter {@link ScopedOwnerDto#setSponsor(SponsorDto) setSponsor(SponsorDto sponsor)} * diff --git a/example/src/test/java/org/javahelpers/simple/builders/example/scoping/ScopedOwnerDtoBuilderTest.java b/example/src/test/java/org/javahelpers/simple/builders/example/scoping/ScopedOwnerDtoBuilderTest.java index f7c6bee8..14ed7c12 100644 --- a/example/src/test/java/org/javahelpers/simple/builders/example/scoping/ScopedOwnerDtoBuilderTest.java +++ b/example/src/test/java/org/javahelpers/simple/builders/example/scoping/ScopedOwnerDtoBuilderTest.java @@ -45,7 +45,9 @@ void exposesScopedBuilderConsumerOverloads() { assertFalse( hasBuilderConsumerMethod( "library", "org.javahelpers.simple.builders.example.library.LibraryHelperDtoBuilder")); - assertFalse(hasBuilderConsumerMethod("sponsor", SponsorDtoBuilder.class.getName())); + // SponsorDto's builder is generated in the same compilation, so it is trusted and used + // regardless of the usage scope + assertTrue(hasBuilderConsumerMethod("sponsor", SponsorDtoBuilder.class.getName())); assertTrue(hasMethod("trusted", TrustedHelperDto.class)); assertTrue(hasMethod("library", LibraryHelperDto.class)); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/BuilderScopeResolver.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/BuilderScopeResolver.java index 0f9bc265..dac1431f 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/BuilderScopeResolver.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/BuilderScopeResolver.java @@ -80,12 +80,13 @@ public BuilderScopeResolver(ProcessingContext context) { *

The decision follows these rules: * *

    + *
  1. If the referenced type's builder is generated in the current processing round (registered + * via {@link #registerGeneratedTypes}), the candidate builder (using {@code builderSuffix}) + * is returned immediately — trusted without a classpath lookup or contract check, and + * exempt from the usage scope: a builder this processor generates is always used. *
  2. If the usage scope is set and the referenced type's package is not in it, no builder may * be referenced. The usage scope includes generation-scope packages automatically. When the * scope is empty, any package is allowed (backward compatibility). - *
  3. If the referenced type's builder is generated in the current processing round (registered - * via {@link #registerGeneratedTypes}), the candidate builder (using {@code builderSuffix}) - * is returned immediately — trusted without a classpath lookup or contract check. *
  4. Otherwise, the candidate builder name is constructed using {@code builderUsageSuffix} * (falling back to {@code builderSuffix} if not configured). The candidate is looked up on * the classpath and returned if it satisfies the builder contract: a constructor accepting @@ -162,17 +163,11 @@ private Optional resolve(TypeElement referencedType) { String referencedTypeFqn = referencedType.getQualifiedName().toString(); String packageName = context.getPackageName(referencedType); - // The usage scope determines whether a type is eligible to be referenced as a builder - // helper. When empty, any package is allowed (backward compatibility). When set, only - // packages in the scope qualify. The scope already includes generation-scope packages. - if (!usagePackages.isEmpty() && !usagePackages.includes(packageName)) { - return Optional.empty(); - } - // Types whose builders are generated in the current processing round are trusted // immediately — our own generators always produce the builder contract, so no - // classpath lookup or contract check is needed. The candidate uses builderSuffix - // because that is what our own generators produce. + // classpath lookup or contract check is needed. Being generated by this processor also + // makes them exempt from the usage scope: an explicitly generated builder is always used. + // The candidate uses builderSuffix because that is what our own generators produce. if (generatedTypeNames.contains(referencedTypeFqn)) { TypeName candidate = JavaLangMapper.createBuilderTypeName( @@ -180,6 +175,13 @@ private Optional resolve(TypeElement referencedType) { return Optional.of(candidate); } + // The usage scope determines whether a type is eligible to be referenced as a builder + // helper. When empty, any package is allowed (backward compatibility). When set, only + // packages in the scope qualify. The scope already includes generation-scope packages. + if (!usagePackages.isEmpty() && !usagePackages.includes(packageName)) { + return Optional.empty(); + } + // For types not generated in this round, look up the candidate on the classpath using // builderUsageSuffix (which falls back to builderSuffix if not configured) and verify // the builder contract. diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderScopeResolverTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderScopeResolverTest.java index c7bcd27c..2fcb2a2e 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderScopeResolverTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderScopeResolverTest.java @@ -117,8 +117,11 @@ public class LibHelper { public LibHelper() {} } assertThat(compilation).succeeded(); // With usage scope "other" (not "lib") and no registration → empty assertEquals(Optional.empty(), ResolverProbeProcessor.beforeRegistration); - // Registration alone is not enough — the type must also be in the usage scope - assertEquals(Optional.empty(), ResolverProbeProcessor.afterRegistration); + // Registered types are trusted and exempt from the usage scope — a builder this + // processor generates is always used + assertEquals( + "lib.LibHelperBuilder", + ResolverProbeProcessor.afterRegistration.get().getFullQualifiedName()); // With usage scope "lib" but no registration → empty (builder not on classpath) assertEquals(Optional.empty(), ResolverProbeProcessor.usageBeforeRegistration); // With usage scope "lib" AND registration → builder resolved