diff --git a/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleMinimalBuilder.java b/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleMinimalBuilder.java new file mode 100644 index 00000000..c3968263 --- /dev/null +++ b/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleMinimalBuilder.java @@ -0,0 +1,100 @@ +/* + * MIT License + * + * Copyright (c) 2026 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.core.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.javahelpers.simple.builders.core.enums.OptionState; + +/** + * Built-in minimal builder template annotation. + * + *
Generates the smallest possible fluent builder by disabling all optional features. Use this + * when you only need {@code create()}, field setters and {@code build()} without suppliers, + * consumers, varargs, collection helpers, {@code With} interface, Jackson integration or generated + * annotations. + * + *
This annotation is implemented purely as a {@link SimpleBuilder.Template} with every optional + * feature set to {@link OptionState#DISABLED}. Because the template is {@link Inherited}, + * subclasses of an annotated type also receive a minimal builder unless explicitly excluded by + * {@link Ignore4BuilderGeneration}. + * + *
Example: + * + *
{@code
+ * @SimpleMinimalBuilder
+ * public class PersonDto {
+ * private String name;
+ *
+ * public String getName() { return name; }
+ * public void setName(String name) { this.name = name; }
+ * }
+ * }
+ *
+ * Generated builder usage: + * + *
{@code
+ * PersonDto person = PersonDtoBuilder.create()
+ * .name("John")
+ * .build();
+ * }
+ *
+ * @see SimpleBuilder
+ * @see SimpleBuilder.Template
+ * @see SimpleBuilder.Options
+ * @see Ignore4BuilderGeneration
+ */
+@SimpleBuilder.Template(
+ options =
+ @SimpleBuilder.Options(
+ generateFieldSupplier = OptionState.DISABLED,
+ generateFieldConsumer = OptionState.DISABLED,
+ generateBuilderConsumer = OptionState.DISABLED,
+ generateConditionalHelper = OptionState.DISABLED,
+ generateVarArgsHelpers = OptionState.DISABLED,
+ generateStringFormatHelpers = OptionState.DISABLED,
+ generateAddToCollectionHelpers = OptionState.DISABLED,
+ generateUnboxedOptional = OptionState.DISABLED,
+ generateWithInterface = OptionState.DISABLED,
+ usingArrayListBuilder = OptionState.DISABLED,
+ usingArrayListBuilderWithElementBuilders = OptionState.DISABLED,
+ usingHashSetBuilder = OptionState.DISABLED,
+ usingHashSetBuilderWithElementBuilders = OptionState.DISABLED,
+ usingHashMapBuilder = OptionState.DISABLED,
+ usingGeneratedAnnotation = OptionState.DISABLED,
+ usingBuilderImplementationAnnotation = OptionState.DISABLED,
+ usingJacksonDeserializerAnnotation = OptionState.DISABLED,
+ generateJacksonModule = OptionState.DISABLED,
+ copyTypeAnnotations = OptionState.DISABLED,
+ implementsBuilderBase = OptionState.DISABLED,
+ builderSuffix = "Builder",
+ setterSuffix = ""))
+@Retention(RetentionPolicy.CLASS)
+@Target(ElementType.TYPE)
+@Inherited
+public @interface SimpleMinimalBuilder {}
diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md
index 01a0ca35..70ede75f 100644
--- a/docs/CONFIGURATION.md
+++ b/docs/CONFIGURATION.md
@@ -85,9 +85,10 @@ public class PersonDto {
| Need | Use |
|------|-----|
| Generate a builder for a single class/record | `@SimpleBuilder` on the class/record |
+| Generate a minimal builder (no optional features) | `@SimpleMinimalBuilder` on the class/record |
| Share the same configuration across many classes | `@SimpleBuilder.Template` on a custom `@interface`, then the custom annotation on each class |
-Create reusable configuration presets with custom template annotations:
+Create reusable configuration presets with custom template annotations. The built-in `@SimpleMinimalBuilder` already disables every optional feature; use the following pattern only when you need a differently named or customized template:
```java
@SimpleBuilder.Template(options = @SimpleBuilder.Options(
@@ -109,7 +110,8 @@ Create reusable configuration presets with custom template annotations:
usingGeneratedAnnotation = OptionState.DISABLED,
usingBuilderImplementationAnnotation = OptionState.DISABLED,
implementsBuilderBase = OptionState.DISABLED,
- usingJacksonDeserializerAnnotation = OptionState.DISABLED
+ usingJacksonDeserializerAnnotation = OptionState.DISABLED,
+ generateJacksonModule = OptionState.DISABLED
))
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
@@ -903,40 +905,23 @@ reported as warnings so compilation can continue.
### Minimal Builder
-Generate only essential builder methods:
+Use the built-in `@SimpleMinimalBuilder` template to generate only essential builder methods with a single annotation:
```java
-@SimpleBuilder(
- options = @SimpleBuilder.Options(
- generateFieldSupplier = OptionState.DISABLED,
- generateFieldConsumer = OptionState.DISABLED,
- generateBuilderConsumer = OptionState.DISABLED,
- generateConditionalHelper = OptionState.DISABLED,
- generateVarArgsHelpers = OptionState.DISABLED,
- generateStringFormatHelpers = OptionState.DISABLED,
- generateAddToCollectionHelpers = OptionState.DISABLED,
- generateUnboxedOptional = OptionState.DISABLED,
- copyTypeAnnotations = OptionState.DISABLED,
- usingArrayListBuilder = OptionState.DISABLED,
- usingArrayListBuilderWithElementBuilders = OptionState.DISABLED,
- usingHashSetBuilder = OptionState.DISABLED,
- usingHashSetBuilderWithElementBuilders = OptionState.DISABLED,
- usingHashMapBuilder = OptionState.DISABLED,
- generateWithInterface = OptionState.DISABLED,
- usingGeneratedAnnotation = OptionState.DISABLED,
- usingBuilderImplementationAnnotation = OptionState.DISABLED,
- implementsBuilderBase = OptionState.DISABLED,
- usingJacksonDeserializerAnnotation = OptionState.DISABLED
-)
+import org.javahelpers.simple.builders.core.annotations.SimpleMinimalBuilder;
+
+@SimpleMinimalBuilder
public class MinimalDto {
private String name;
-
+
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
```
-**Generated**: Only basic builder methods (`create()`, field setters, `build()`)
+**Generated**: Only basic builder methods (`create()`, field setters, `build()`).
+
+If you need a different name or extra customization, you can still build a custom `@SimpleBuilder.Template` with all optional features disabled.
### Internal API Builder
@@ -993,7 +978,7 @@ TeamDto team = TeamDtoBuilder.create()
### Minimal Builder Template
-Create a reusable template for lightweight builders:
+The built-in `@SimpleMinimalBuilder` is the simplest way to get a lightweight builder. If you need a differently named template or want to build your own preset, create a custom annotation meta-annotated with `@SimpleBuilder.Template`:
```java
@SimpleBuilder.Template(options = @SimpleBuilder.Options(
@@ -1015,7 +1000,8 @@ Create a reusable template for lightweight builders:
usingGeneratedAnnotation = OptionState.DISABLED,
usingBuilderImplementationAnnotation = OptionState.DISABLED,
implementsBuilderBase = OptionState.DISABLED,
- usingJacksonDeserializerAnnotation = OptionState.DISABLED
+ usingJacksonDeserializerAnnotation = OptionState.DISABLED,
+ generateJacksonModule = OptionState.DISABLED
))
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
diff --git a/example/generated-example-builder/org/javahelpers/simple/builders/example/CustomerDtoBuilder.java b/example/generated-example-builder/org/javahelpers/simple/builders/example/CustomerDtoBuilder.java
new file mode 100644
index 00000000..bbcebcd2
--- /dev/null
+++ b/example/generated-example-builder/org/javahelpers/simple/builders/example/CustomerDtoBuilder.java
@@ -0,0 +1,218 @@
+package org.javahelpers.simple.builders.example;
+
+import static org.javahelpers.simple.builders.core.util.TrackedValue.changedValue;
+import static org.javahelpers.simple.builders.core.util.TrackedValue.initialValue;
+import static org.javahelpers.simple.builders.core.util.TrackedValue.unsetValue;
+import java.util.List;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.javahelpers.simple.builders.core.util.BuilderToStringStyle;
+import org.javahelpers.simple.builders.core.util.TrackedValue;
+
+/**
+ * Builder for {@code org.javahelpers.simple.builders.example.CustomerDto}.
+ * + * This builder provides a fluent API for creating instances of org.javahelpers.simple.builders.example.CustomerDto with + * method chaining and validation. Use the static {@code create()} method to obtain a new builder instance, configure + * the desired properties using the setter methods, and then call {@code build()} to create the final DTO. + * + *
{@code
+ * CustomerDto result = CustomerDtoBuilder.create()
+ * .email("example value")
+ * .id(42L)
+ * .name("example value")
+ * .tags(List.of("example value"))
+ * .build();
+ * }
+ */
+public class CustomerDtoBuilder {
+
+ /**
+ * Tracked value for email: email.
+ */
+ private TrackedValueid: id.
+ */
+ private TrackedValuename: name.
+ */
+ private TrackedValuetags: tags.
+ */
+ private TrackedValue{@code
+ * CustomerDtoBuilder builder = CustomerDtoBuilder.create();
+ * }
+ *
+ * @return builder for {@code org.javahelpers.simple.builders.example.CustomerDto}
+ */
+ public static CustomerDtoBuilder create() {
+ return new CustomerDtoBuilder();
+ }
+
+ /**
+ * Sets the value for email.
+ * + * Generated from setter {@link CustomerDto#setEmail(String) setEmail(String email)} + * + *
{@code
+ * builder.email("example value");
+ * }
+ *
+ * @param email email
+ * @return current instance of builder
+ */
+ public CustomerDtoBuilder email(String email) {
+ this.email = changedValue(email);
+ return this;
+ }
+
+ /**
+ * Sets the value for id.
+ * + * Generated from setter {@link CustomerDto#setId(Long) setId(Long id)} + * + *
{@code
+ * builder.id(42L);
+ * }
+ *
+ * @param id id
+ * @return current instance of builder
+ */
+ public CustomerDtoBuilder id(Long id) {
+ this.id = changedValue(id);
+ return this;
+ }
+
+ /**
+ * Sets the value for name.
+ * + * Generated from setter {@link CustomerDto#setName(String) setName(String name)} + * + *
{@code
+ * builder.name("example value");
+ * }
+ *
+ * @param name name
+ * @return current instance of builder
+ */
+ public CustomerDtoBuilder name(String name) {
+ this.name = changedValue(name);
+ return this;
+ }
+
+ /**
+ * Sets the value for tags.
+ *
+ * Generated from setter {@link CustomerDto#setTags(List) setTags(List
+ * Generated from setter {@link CustomerDto#setEmail(String) setEmail(String email)}
+ *
+ * @return this builder instance for chaining
+ * @throws IllegalArgumentException if email is null or empty
+ */
+ CustomerDtoBuilder validateEmail() {
+ if (!email.isSet() || email.value().trim().isEmpty()) {
+ throw new IllegalArgumentException("Email cannot be null or empty");
+ }
+ return this;
+ }
+
+ /**
+ * Validates that the name field is not null or empty.
+ *
+ * Generated from setter {@link CustomerDto#setName(String) setName(String name)}
+ *
+ * @return this builder instance for chaining
+ * @throws IllegalArgumentException if name is null or empty
+ */
+ CustomerDtoBuilder validateName() {
+ if (!name.isSet() || name.value().trim().isEmpty()) {
+ throw new IllegalArgumentException("Name cannot be null or empty");
+ }
+ return this;
+ }
+
+ /**
+ * Builds the configured DTO instance.
+ *
+ * This generates a minimal builder with only {@code create()}, field setters and {@code build()}.
+ */
+@SimpleMinimalBuilder
+public class CustomerDto {
+ private Long id;
+ private String name;
+ private String email;
+ private List Verifies that {@code @SimpleMinimalBuilder} disables all optional features and produces a
+ * builder with only the basic fluent API: constructors, {@code create()}, field setters, {@code
+ * build()} and {@code toString()}.
+ */
+class SimpleMinimalBuilderTest {
+
+ @Test
+ void simpleMinimalBuilderGeneratesMinimalBuilder() {
+ JavaFileObject source =
+ ProcessorTestUtils.forSource(
+ """
+ package test;
+ import org.javahelpers.simple.builders.core.annotations.SimpleMinimalBuilder;
+
+ @SimpleMinimalBuilder
+ public class PersonDto {
+ private String name;
+ private java.util.List
+ * This builder provides a fluent API for creating instances of test.PersonDto with method chaining and validation. Use
+ * the static {@code create()} method to obtain a new builder instance, configure the desired properties using the
+ * setter methods, and then call {@code build()} to create the final DTO.
+ *
+ *
+ * Generated from setter {@link PersonDto#setName(String) setName(String name)}
+ *
+ *
+ * Generated from setter {@link PersonDto#setTags(List) setTags(ListExample:
+ *
+ * {@code
+ * builder.tags(List.of("example value"));
+ * }
+ *
+ * @param tags tags
+ * @return current instance of builder
+ */
+ public CustomerDtoBuilder tags(ListExample:
+ *
+ * {@code
+ * CustomerDto result = builder.build();
+ * }
+ */
+ public CustomerDto build() {
+ CustomerDto result = new CustomerDto();
+ this.email.ifSet(result::setEmail);
+ this.id.ifSet(result::setId);
+ this.name.ifSet(result::setName);
+ this.tags.ifSet(result::setTags);
+ return result;
+ }
+
+ /**
+ * Returns a string representation of this builder, including only fields that have been set.
+ *
+ * @return string representation of the builder
+ */
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE).append("email", this.email)
+ .append("id", this.id)
+ .append("name", this.name)
+ .append("tags", this.tags)
+ .toString();
+ }
+}
\ No newline at end of file
diff --git a/example/src/main/java/org/javahelpers/simple/builders/example/CustomerDto.java b/example/src/main/java/org/javahelpers/simple/builders/example/CustomerDto.java
new file mode 100644
index 00000000..58edb8ce
--- /dev/null
+++ b/example/src/main/java/org/javahelpers/simple/builders/example/CustomerDto.java
@@ -0,0 +1,73 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2026 Andreas Igel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.javahelpers.simple.builders.example;
+
+import java.util.List;
+import org.javahelpers.simple.builders.core.annotations.SimpleMinimalBuilder;
+
+/**
+ * Example DTO demonstrating the built-in {@link SimpleMinimalBuilder} template.
+ *
+ * Example:
+ *
+ * {@code
+ * PersonDto result = PersonDtoBuilder.create().name("example value").tags(List.of("example value")).build();
+ * }
+ */
+ public class PersonDtoBuilder {
+
+ /**
+ * Tracked value for name: name.
+ */
+ private TrackedValuetags: tags.
+ */
+ private TrackedValue> tags = unsetValue();
+
+ /**
+ * Empty constructor of builder for {@code test.PersonDto}.
+ */
+ public PersonDtoBuilder() {
+ }
+
+ /**
+ * Initialisation of builder for {@code test.PersonDto} by a instance.
+ *
+ * @param instance object instance for initialisiation
+ */
+ public PersonDtoBuilder(PersonDto instance) {
+ this.name = initialValue(instance.getName());
+ this.tags = initialValue(instance.getTags());
+ }
+
+ /**
+ * Creating a new builder for {@code test.PersonDto}.
+ *
+ *
Example:
+ *
+ * {@code
+ * PersonDtoBuilder builder = PersonDtoBuilder.create();
+ * }
+ *
+ * @return builder for {@code test.PersonDto}
+ */
+ public static PersonDtoBuilder create() {
+ return new PersonDtoBuilder();
+ }
+
+ /**
+ * Sets the value for name.
+ * Example:
+ *
+ * {@code
+ * builder.name("example value");
+ * }
+ *
+ * @param name name
+ * @return current instance of builder
+ */
+ public PersonDtoBuilder name(String name) {
+ this.name = changedValue(name);
+ return this;
+ }
+
+ /**
+ * Sets the value for tags.
+ * Example:
+ *
+ * {@code
+ * builder.tags(List.of("example value"));
+ * }
+ *
+ * @param tags tags
+ * @return current instance of builder
+ */
+ public PersonDtoBuilder tags(ListExample:
+ *
+ * {@code
+ * PersonDto result = builder.build();
+ * }
+ */
+ public PersonDto build() {
+ PersonDto result = new PersonDto();
+ this.name.ifSet(result::setName);
+ this.tags.ifSet(result::setTags);
+ return result;
+ }
+
+ /**
+ * Returns a string representation of this builder, including only fields that have been set.
+ *
+ * @return string representation of the builder
+ */
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE).append("name", this.name)
+ .append("tags", this.tags)
+ .toString();
+ }
+ }
+ """;
+
+ ProcessorAsserts.assertNormalizedEquals(
+ expectedCode,
+ generatedCode,
+ "Generated minimal builder does not match expected. All optional features should be disabled.");
+ }
+
+ @Test
+ void inheritedSimpleMinimalBuilderPropagatesToSubclass() {
+ JavaFileObject parentSource =
+ ProcessorTestUtils.forSource(
+ """
+ package test;
+ import org.javahelpers.simple.builders.core.annotations.SimpleMinimalBuilder;
+
+ @SimpleMinimalBuilder
+ public class PersonDto {
+ private String name;
+
+ public String getName() { return name; }
+ public void setName(String name) { this.name = name; }
+ }
+ """);
+
+ JavaFileObject childSource =
+ ProcessorTestUtils.forSource(
+ """
+ package test;
+
+ public class ChildDto extends PersonDto {
+ private int age;
+
+ public int getAge() { return age; }
+ public void setAge(int age) { this.age = age; }
+ }
+ """);
+
+ Compilation compilation =
+ ProcessorTestUtils.createCompiler().compile(parentSource, childSource);
+
+ assertThat(compilation).succeededWithoutWarnings();
+
+ String parentBuilder = loadGeneratedSource(compilation, "PersonDtoBuilder");
+ assertGenerationSucceeded(compilation, "PersonDtoBuilder", parentBuilder);
+
+ String childBuilder = loadGeneratedSource(compilation, "ChildDtoBuilder");
+ assertGenerationSucceeded(compilation, "ChildDtoBuilder", childBuilder);
+
+ assertContaining(childBuilder, "public ChildDtoBuilder name(String name)");
+ assertContaining(childBuilder, "public ChildDtoBuilder age(int age)");
+
+ assertNotContaining(
+ childBuilder,
+ "Supplier<",
+ "Consumer<",
+ "WithChildDto",
+ "@Generated",
+ "@BuilderImplementation",
+ "IBuilderBase",
+ "@JsonPOJOBuilder",
+ "ChildDtoBuilder name(String... name)",
+ "ChildDtoBuilder tags(String... tags)");
+ }
+}