From 6ebe59ae59f4849eb3cd0420fd0103b5490cca50 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 1 Aug 2026 14:59:40 +0000 Subject: [PATCH 1/4] Document feature parity the Simple Builders way for #225 Co-Authored-By: Andreas Igel --- README.md | 12 ++++++++++ docs/CONFIGURATION.md | 55 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/README.md b/README.md index b6fbe8d7..c8a26fce 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ A zero-reflection Java annotation processor that generates fluent, type-safe bui ## Table of Contents - [What is Simple Builders?](#what-is-simple-builders) - [How Simple Builders compares](#how-simple-builders-compares) + - [Doing what other builders advertise — the Simple Builders way](#doing-what-other-builders-advertise-the-simple-builders-way) - [Features](#features) - [Requirements](#requirements) - [Installation](#installation) @@ -47,6 +48,17 @@ Simple Builders generates fluent, type-safe builders for your **existing** class Use Simple Builders when you want fluent, type-safe builders for the classes and records you already have, generated as plain readable source, with no bytecode manipulation and no IDE plugin — and no lock-in: because the builders are ordinary generated Java, you can drop the dependency at any time by copying the generated builder classes into your own sources, and they keep working. +### Doing what other builders advertise — the Simple Builders way + +- **Required fields:** Primitive fields and fields annotated with an annotation named `NotNull` or `NonNull` are non-nullable; constructor parameters are builder inputs. `build()` enforces the required/non-null contract with `IllegalStateException` ([configuration details](docs/CONFIGURATION.md#required-fields-and-null-safety)). +- **Copy / `with` / `toBuilder`:** The generated `With` interface provides `instance.with(b -> ...)` for copy-and-modify and `instance.with()` for a builder pre-populated from the instance ([`generateWithInterface`](docs/CONFIGURATION.md#generatewithinterface)). +- **Collection immutability:** The target type owns the collection contract. Simple Builders passes through what the type stores; use defensive copying such as `List.copyOf(...)` in the type when the result must be immutable ([configuration details](docs/CONFIGURATION.md#collection-immutability)). +- **Incremental / singular collection API:** `add2X` helpers, `ArrayList`/`HashSet`/`HashMap` collection builders, and varargs helpers cover incremental collection construction ([collection helper options](docs/CONFIGURATION.md#collection-helpers)). +- **Default values:** Classes can use field initializers; records can establish defaults in their canonical or compact constructor, or behind a static factory ([configuration options](docs/CONFIGURATION.md#configuration-options)). +- **Inheritance:** Inherited setters are discovered, and constructors exposed by the annotated subclass are used. A final superclass field not exposed by that constructor is intentionally not bypassed ([configuration options](docs/CONFIGURATION.md#configuration-options)). + +Value semantics (`equals`, `hashCode`, `toString`) and generating brand-new immutable value types are deliberate out-of-scope paradigm choices, not missing builder features. + ## Features - **Low Runtime Dependencies**: The generated code has only dependencies to a core dependency for CollectionBuilders and to Apache.CommonLang3 diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 7534ad7b..e011a0f6 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -15,7 +15,9 @@ Simple-builders supports fine-grained configuration through the `@SimpleBuilder. - [Field Setter Generation](#field-setter-generation) - [Conditional Logic](#conditional-logic) - [Access Control](#access-control) + - [Required Fields and Null-Safety](#required-fields-and-null-safety) - [Collection Helpers](#collection-helpers) + - [Collection Immutability](#collection-immutability) - [Component Filtering](#component-filtering) - [Integration](#integration) - [Reliability](#reliability) @@ -498,6 +500,38 @@ public Builder items(List<@NotNull String> items) { ... } --- +### Required Fields and Null-Safety + +Simple Builders treats constructor parameters as inputs to the selected target constructor. A field +is non-nullable when its type is primitive or its parameter carries an annotation whose simple name +is `NotNull` or `NonNull`, regardless of the annotation package. The annotation name is matched +without requiring a particular validation framework. + +For non-nullable constructor fields, `build()` requires the builder value to be set and non-null. +For non-nullable setter fields, the field remains optional, but a value supplied to the builder must +be non-null. Violations throw `IllegalStateException`. + +For example, the primitive `price` below is required and cannot be null: + +```java +@SimpleBuilder +public record Product(@NotNull String name, double price) {} + +ProductBuilder.create() + .name("Keyboard") + .build(); // throws IllegalStateException: price must be set + +ProductBuilder.create() + .name(null) + .price(99.0) + .build(); // throws IllegalStateException: name cannot be null +``` + +The `NotNull`/`NonNull` simple-name check is framework-agnostic; use the annotation type already +used by your project. + +--- + ### Collection Helpers #### `usingArrayListBuilder` @@ -608,6 +642,27 @@ Generates methods using `HashMapBuilder` for fluent Map construction. --- +#### Collection Immutability + +Immutability of collections in the built object is the target type's responsibility. Simple +Builders stores exactly what the target type stores and does not silently wrap collections, so the +target type's collection contract remains intact. For a record, enforce that contract in its +canonical or compact constructor: + +```java +@SimpleBuilder +public record Basket(List items) { + public Basket { + items = List.copyOf(items); + } +} +``` + +The builder passes its collection value to `Basket`; the record makes the stored collection +unmodifiable. + +--- + ### Component Filtering #### `deactivateGenerationComponents` From 022e756a81b42e0eac584e53db05e644ceb02ba1 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 1 Aug 2026 15:06:47 +0000 Subject: [PATCH 2/4] Move inherent behavior docs into README for #226 Co-Authored-By: Andreas Igel --- README.md | 54 ++++++++++++++++++++++++++++++++++++++++-- docs/CONFIGURATION.md | 55 ------------------------------------------- 2 files changed, 52 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index c8a26fce..c6363a88 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,10 @@ A zero-reflection Java annotation processor that generates fluent, type-safe bui - [Usage](#usage) - [Basic Usage](#basic-usage) - [Validation Annotations](#validation-annotations) + - [Required Fields and Null-Safety](#required-fields-and-null-safety) - [Conditional Builder Logic](#conditional-builder-logic) - [Collections and Nested Objects](#collections-and-nested-objects) + - [Collection Immutability](#collection-immutability) - [With Interface Pattern](#with-interface-pattern) - [Builder Configuration](#builder-configuration) - [Compiler Arguments](#compiler-arguments) @@ -50,9 +52,9 @@ Use Simple Builders when you want fluent, type-safe builders for the classes and ### Doing what other builders advertise — the Simple Builders way -- **Required fields:** Primitive fields and fields annotated with an annotation named `NotNull` or `NonNull` are non-nullable; constructor parameters are builder inputs. `build()` enforces the required/non-null contract with `IllegalStateException` ([configuration details](docs/CONFIGURATION.md#required-fields-and-null-safety)). +- **Required fields:** Primitive fields and fields annotated with an annotation named `NotNull` or `NonNull` are non-nullable; constructor parameters are builder inputs. `build()` enforces the required/non-null contract with `IllegalStateException` ([configuration details](#required-fields-and-null-safety)). - **Copy / `with` / `toBuilder`:** The generated `With` interface provides `instance.with(b -> ...)` for copy-and-modify and `instance.with()` for a builder pre-populated from the instance ([`generateWithInterface`](docs/CONFIGURATION.md#generatewithinterface)). -- **Collection immutability:** The target type owns the collection contract. Simple Builders passes through what the type stores; use defensive copying such as `List.copyOf(...)` in the type when the result must be immutable ([configuration details](docs/CONFIGURATION.md#collection-immutability)). +- **Collection immutability:** The target type owns the collection contract. Simple Builders passes through what the type stores; use defensive copying such as `List.copyOf(...)` in the type when the result must be immutable ([configuration details](#collection-immutability)). - **Incremental / singular collection API:** `add2X` helpers, `ArrayList`/`HashSet`/`HashMap` collection builders, and varargs helpers cover incremental collection construction ([collection helper options](docs/CONFIGURATION.md#collection-helpers)). - **Default values:** Classes can use field initializers; records can establish defaults in their canonical or compact constructor, or behind a static factory ([configuration options](docs/CONFIGURATION.md#configuration-options)). - **Inheritance:** Inherited setters are discovered, and constructors exposed by the annotated subclass are used. A final superclass field not exposed by that constructor is intentionally not bypassed ([configuration options](docs/CONFIGURATION.md#configuration-options)). @@ -191,6 +193,35 @@ User user = UserBuilder.create() This ensures validation frameworks work seamlessly with builder-generated objects. +#### Required Fields and Null-Safety + +Simple Builders treats a field as non-nullable when its type is primitive or its parameter carries +an annotation whose simple name is `NotNull` or `NonNull`, regardless of the annotation package. +The name is matched without requiring a particular validation framework. + +For non-nullable constructor fields, `build()` requires the builder value to be set and non-null. +For non-nullable setter fields, the field remains optional, but a value supplied to the builder must +be non-null. Violations throw `IllegalStateException`. + +For example, the primitive `price` below is required and cannot be null: + +```java +@SimpleBuilder +public record Product(@NotNull String name, double price) {} + +ProductBuilder.create() + .name("Keyboard") + .build(); // throws IllegalStateException: price must be set + +ProductBuilder.create() + .name(null) + .price(99.0) + .build(); // throws IllegalStateException: name cannot be null +``` + +The `NotNull`/`NonNull` simple-name check is framework-agnostic; use the annotation type already +used by your project. + #### Conditional Builder Logic Apply builder modifications conditionally using the `conditional()` method: @@ -267,6 +298,25 @@ Project project = ProjectBuilder.create() .build(); ``` +#### Collection Immutability + +Immutability of collections in the built object is the target type's responsibility. Simple +Builders stores exactly what the target type stores and does not silently wrap collections, so the +target type's collection contract remains intact. For a record, enforce that contract in its +canonical or compact constructor: + +```java +@SimpleBuilder +public record Basket(List items) { + public Basket { + items = List.copyOf(items); + } +} +``` + +The builder passes its collection value to `Basket`; the record makes the stored collection +unmodifiable. + ### With Interface Pattern diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index e011a0f6..7534ad7b 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -15,9 +15,7 @@ Simple-builders supports fine-grained configuration through the `@SimpleBuilder. - [Field Setter Generation](#field-setter-generation) - [Conditional Logic](#conditional-logic) - [Access Control](#access-control) - - [Required Fields and Null-Safety](#required-fields-and-null-safety) - [Collection Helpers](#collection-helpers) - - [Collection Immutability](#collection-immutability) - [Component Filtering](#component-filtering) - [Integration](#integration) - [Reliability](#reliability) @@ -500,38 +498,6 @@ public Builder items(List<@NotNull String> items) { ... } --- -### Required Fields and Null-Safety - -Simple Builders treats constructor parameters as inputs to the selected target constructor. A field -is non-nullable when its type is primitive or its parameter carries an annotation whose simple name -is `NotNull` or `NonNull`, regardless of the annotation package. The annotation name is matched -without requiring a particular validation framework. - -For non-nullable constructor fields, `build()` requires the builder value to be set and non-null. -For non-nullable setter fields, the field remains optional, but a value supplied to the builder must -be non-null. Violations throw `IllegalStateException`. - -For example, the primitive `price` below is required and cannot be null: - -```java -@SimpleBuilder -public record Product(@NotNull String name, double price) {} - -ProductBuilder.create() - .name("Keyboard") - .build(); // throws IllegalStateException: price must be set - -ProductBuilder.create() - .name(null) - .price(99.0) - .build(); // throws IllegalStateException: name cannot be null -``` - -The `NotNull`/`NonNull` simple-name check is framework-agnostic; use the annotation type already -used by your project. - ---- - ### Collection Helpers #### `usingArrayListBuilder` @@ -642,27 +608,6 @@ Generates methods using `HashMapBuilder` for fluent Map construction. --- -#### Collection Immutability - -Immutability of collections in the built object is the target type's responsibility. Simple -Builders stores exactly what the target type stores and does not silently wrap collections, so the -target type's collection contract remains intact. For a record, enforce that contract in its -canonical or compact constructor: - -```java -@SimpleBuilder -public record Basket(List items) { - public Basket { - items = List.copyOf(items); - } -} -``` - -The builder passes its collection value to `Basket`; the record makes the stored collection -unmodifiable. - ---- - ### Component Filtering #### `deactivateGenerationComponents` From b251ea59e2737a61d9c513459bf067d7564c31c6 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 1 Aug 2026 16:36:47 +0000 Subject: [PATCH 3/4] Fix required-field example and drop defaults note for #226 Co-Authored-By: Andreas Igel --- README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/README.md b/README.md index c6363a88..db746e9f 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,6 @@ Use Simple Builders when you want fluent, type-safe builders for the classes and - **Copy / `with` / `toBuilder`:** The generated `With` interface provides `instance.with(b -> ...)` for copy-and-modify and `instance.with()` for a builder pre-populated from the instance ([`generateWithInterface`](docs/CONFIGURATION.md#generatewithinterface)). - **Collection immutability:** The target type owns the collection contract. Simple Builders passes through what the type stores; use defensive copying such as `List.copyOf(...)` in the type when the result must be immutable ([configuration details](#collection-immutability)). - **Incremental / singular collection API:** `add2X` helpers, `ArrayList`/`HashSet`/`HashMap` collection builders, and varargs helpers cover incremental collection construction ([collection helper options](docs/CONFIGURATION.md#collection-helpers)). -- **Default values:** Classes can use field initializers; records can establish defaults in their canonical or compact constructor, or behind a static factory ([configuration options](docs/CONFIGURATION.md#configuration-options)). - **Inheritance:** Inherited setters are discovered, and constructors exposed by the annotated subclass are used. A final superclass field not exposed by that constructor is intentionally not bypassed ([configuration options](docs/CONFIGURATION.md#configuration-options)). Value semantics (`equals`, `hashCode`, `toString`) and generating brand-new immutable value types are deliberate out-of-scope paradigm choices, not missing builder features. @@ -212,11 +211,6 @@ public record Product(@NotNull String name, double price) {} ProductBuilder.create() .name("Keyboard") .build(); // throws IllegalStateException: price must be set - -ProductBuilder.create() - .name(null) - .price(99.0) - .build(); // throws IllegalStateException: name cannot be null ``` The `NotNull`/`NonNull` simple-name check is framework-agnostic; use the annotation type already From b42cb1bec5ec6905230a49bbcdd094e3b836f0f7 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 1 Aug 2026 16:40:52 +0000 Subject: [PATCH 4/4] Add required-field example that omits name for #226 Co-Authored-By: Andreas Igel --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index db746e9f..bd3aec0f 100644 --- a/README.md +++ b/README.md @@ -202,7 +202,8 @@ For non-nullable constructor fields, `build()` requires the builder value to be For non-nullable setter fields, the field remains optional, but a value supplied to the builder must be non-null. Violations throw `IllegalStateException`. -For example, the primitive `price` below is required and cannot be null: +For example, both the primitive `price` and the `@NotNull` `name` below are required — omitting +either makes `build()` fail: ```java @SimpleBuilder @@ -211,6 +212,10 @@ public record Product(@NotNull String name, double price) {} ProductBuilder.create() .name("Keyboard") .build(); // throws IllegalStateException: price must be set + +ProductBuilder.create() + .price(99.0) + .build(); // throws IllegalStateException: name must be set ``` The `NotNull`/`NonNull` simple-name check is framework-agnostic; use the annotation type already