Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Simple-builders supports fine-grained configuration through the `@SimpleBuilder.
- [Collection Helpers](#collection-helpers)
- [Component Filtering](#component-filtering)
- [Integration](#integration)
- [Reliability](#reliability)
- [Examples](#examples)
- [Minimal Builder](#minimal-builder)
- [Internal API Builder](#internal-api-builder)
Expand Down Expand Up @@ -846,6 +847,16 @@ public class PersonDto {
// Generated method: withName(String name) instead of name(String name)
```

### Reliability

#### `strict`

**Default**: `DISABLED` | **Compiler Option**: `-Asimplebuilder.strict=ENABLED|DISABLED`

When enabled, builder and Jackson-module generation failures are promoted from compiler warnings
to errors that fail the build. By default, strict mode is disabled and generation failures are
reported as warnings so compilation can continue.

## Examples

### Minimal Builder
Expand Down Expand Up @@ -1231,6 +1242,9 @@ methodAccess = AccessModifier.PRIVATE
# Naming
-Asimplebuilder.builderSuffix=CustomSuffix
-Asimplebuilder.setterSuffix=customPrefix

# Reliability
-Asimplebuilder.strict=ENABLED|DISABLED
```

### Complete Options Example
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,9 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
try {
codeGenerator.generateClass(moduleClassDef);
} catch (BuilderException e) {
context.warning(
// By default Jackson module generation failures are warnings. In opt-in strict mode
// they are promoted to errors that fail the build.
context.reportBasedOnStrictMode(
"simple-builders: Error generating Jackson module for package %s: %s",
packageName, e.getMessage());
}
Expand Down Expand Up @@ -169,9 +171,9 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
process(annotatedElement, config);
successfulGenerations++;
} catch (BuilderException ex) {
// All builder generation failures are warnings to allow other builders to be
// generated
context.warning(
// By default builder generation failures are warnings so other builders are still
// generated. In opt-in strict mode they are promoted to errors that fail the build.
context.reportBasedOnStrictMode(
annotatedElement, "simple-builders: Failed to generate builder - %s", ex.getMessage());
} finally {
context.debugEndOperation();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
* @param generateWithInterface Generate With interface
* @param builderSuffix Suffix for builder class name
* @param setterSuffix Suffix for setter method names
* @param strict Strict/fail-fast generation mode
*/
public record BuilderConfiguration(
OptionState generateFieldSupplier,
Expand Down Expand Up @@ -91,7 +92,8 @@ public record BuilderConfiguration(
OptionState generateJacksonModule,
String jacksonModulePackage,
String builderSuffix,
String setterSuffix) {
String setterSuffix,
OptionState strict) {

public static final BuilderConfiguration DEFAULT =
builder()
Expand Down Expand Up @@ -121,6 +123,7 @@ public record BuilderConfiguration(
.jacksonModulePackage(null)
.builderSuffix("Builder")
.setterSuffix("")
.strict(DISABLED)
.build();

// === Convenience accessors with 'is' prefix for boolean properties ===
Expand Down Expand Up @@ -229,6 +232,10 @@ public String getSetterSuffix() {
return setterSuffix;
}

public boolean isStrictModeEnabled() {
return strict == ENABLED;
}

/**
* Merges this configuration with another configuration.
*
Expand Down Expand Up @@ -295,6 +302,7 @@ public BuilderConfiguration merge(BuilderConfiguration other) {
.jacksonModulePackage(mergeString(other.jacksonModulePackage, this.jacksonModulePackage))
.builderSuffix(mergeString(other.builderSuffix, this.builderSuffix))
.setterSuffix(mergeString(other.setterSuffix, this.setterSuffix))
.strict(mergeOptionState(other.strict, this.strict))
.build();
}

Expand Down Expand Up @@ -360,6 +368,7 @@ public String toString() {
.appendIfNotEmpty("jacksonModulePackage", jacksonModulePackage)
.appendIfNotEmpty("builderSuffix", builderSuffix)
.appendIfNotEmpty("setterSuffix", setterSuffix)
.appendValueIfSet("strict", strict)
.toString();
}

Expand Down Expand Up @@ -440,6 +449,9 @@ public static class Builder {
private String builderSuffix = null;
private String setterSuffix = null;

// === Error Handling ===
private OptionState strict = OptionState.UNSET;

// === Setters ===
public Builder generateSupplier(OptionState value) {
this.generateFieldSupplier = value;
Expand Down Expand Up @@ -686,6 +698,16 @@ public Builder setterSuffix(String value) {
return this;
}

public Builder strict(OptionState value) {
this.strict = value;
return this;
}

public Builder strict(boolean value) {
this.strict = value ? ENABLED : DISABLED;
return this;
}

public BuilderConfiguration build() {
return new BuilderConfiguration(
generateFieldSupplier,
Expand Down Expand Up @@ -713,7 +735,8 @@ public BuilderConfiguration build() {
generateJacksonModule,
jacksonModulePackage,
builderSuffix,
setterSuffix);
setterSuffix,
strict);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,15 @@ public BuilderConfigurationReader(
this.elementUtils = elementUtils;
}

/**
* Gets the global builder configuration read from compiler arguments.
*
* @return the global builder configuration
*/
public BuilderConfiguration getGlobalConfiguration() {
return globalConfiguration;
}

/**
* Reads builder configuration from {@code @SimpleBuilder(options = ...)} inline options.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,15 @@ public enum CompilerArgumentsEnum {

// === Logging ===
/** Option for verbose logging output. */
VERBOSE("verbose");
VERBOSE("verbose"),

// === Error Handling ===
/**
* Option for strict/fail-fast generation mode. When enabled, builder (and Jackson module)
* generation failures are reported as compiler errors that fail the build instead of warnings.
* Defaults to disabled (warnings only, build does not fail).
*/
STRICT("strict");

/** Compiler option prefix for all simple-builders options. */
private static final String OPTION_PREFIX = "simplebuilder.";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ public BuilderConfiguration readBuilderConfiguration() {
.jacksonModulePackage(readValue(CompilerArgumentsEnum.JACKSON_MODULE_PACKAGE))
.builderSuffix(readValue(CompilerArgumentsEnum.BUILDER_SUFFIX))
.setterSuffix(readValue(CompilerArgumentsEnum.SETTER_SUFFIX))
.strict(readOptionState(CompilerArgumentsEnum.STRICT))
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -323,4 +323,44 @@ public void warning(Element element, String format, Object... args) {
public void error(String format, Object... args) {
logger.error(format, args);
}

/**
* Returns whether strict/fail-fast generation mode is enabled via the global compiler
* configuration.
*
* @return true if {@code -Asimplebuilder.strict=true} was supplied, false otherwise
*/
public boolean isStrictModeEnabled() {
return configurationReader.getGlobalConfiguration().isStrictModeEnabled();
}

/**
* Reports an error or warning based on strict mode. In strict mode a build failure is emitted;
* otherwise a warning is logged so generation of remaining builders can continue.
*
* @param element the element associated with the problem, for compiler location information
* @param format the format string
* @param args arguments referenced by the format specifiers
*/
public void reportBasedOnStrictMode(Element element, String format, Object... args) {
if (isStrictModeEnabled()) {
logger.error(element, format, args);
} else {
logger.warning(element, format, args);
}
}

/**
* Reports an error or warning based on strict mode, without an element location.
*
* @param format the format string
* @param args arguments referenced by the format specifiers
*/
public void reportBasedOnStrictMode(String format, Object... args) {
if (isStrictModeEnabled()) {
logger.error(format, args);
} else {
logger.warning(format, args);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,17 @@ public void error(String format, Object... args) {
messager.printMessage(Diagnostic.Kind.ERROR, String.format(format, args));
}

/**
* Reports an error at the location of the given element with a formatted message.
*
* @param e the element where the error occurred, used for location information
* @param format the format string
* @param args arguments referenced by the format specifiers in the format string
*/
public void error(Element e, String format, Object... args) {
messager.printMessage(Diagnostic.Kind.ERROR, String.format(format, args), e);
}

/**
* Posts an info-level message (NOTE level in Maven). Used for important status information about
* builder generation.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,43 @@
/*
* 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.processor;

import static com.google.testing.compile.CompilationSubject.assertThat;
import static org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils.createCompiler;
import static org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils.simpleBuilderClass;

import com.google.testing.compile.Compilation;
import com.google.testing.compile.JavaFileObjects;
import javax.tools.JavaFileObject;
import org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils;
import org.junit.jupiter.api.Test;

class JacksonModuleWarningTest {

private static final String STRICT_MODE_PACKAGE = "pkg.jacksonstrict";

@Test
void generateJacksonModuleEnabled_ButAnnotationDisabled_ShouldEmitWarningAndSkipGeneration() {
// Given
Expand Down Expand Up @@ -72,4 +99,50 @@ void generateJacksonModule_WhenEnabledAndDeserializerEnabled_ShouldGenerate() {
// Verify file IS generated
ProcessorTestUtils.loadGeneratedSource(compilation, "SimpleBuildersJacksonModule");
}

private JavaFileObject dto() {
return simpleBuilderClass(
STRICT_MODE_PACKAGE,
"JacksonStrictDto",
"""
private String name;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
""");
}

/** A hand-written class colliding with the generated Jackson module for the package. */
private JavaFileObject collidingModule() {
return JavaFileObjects.forSourceLines(
STRICT_MODE_PACKAGE + ".SimpleBuildersJacksonModule",
"package " + STRICT_MODE_PACKAGE + ";",
"public class SimpleBuildersJacksonModule {}");
}

@Test
void defaultMode_jacksonModuleFailure_isWarning_andCompilationSucceeds() {
Compilation compilation =
createCompiler()
.withOptions(
"-Asimplebuilder.generateJacksonModule=true",
"-Asimplebuilder.usingJacksonDeserializerAnnotation=true")
.compile(dto(), collidingModule());

assertThat(compilation).succeeded();
assertThat(compilation).hadWarningContaining("Error generating Jackson module");
}

@Test
void strictMode_jacksonModuleFailure_isError_andCompilationFails() {
Compilation compilation =
createCompiler()
.withOptions(
"-Asimplebuilder.generateJacksonModule=true",
"-Asimplebuilder.usingJacksonDeserializerAnnotation=true",
"-Asimplebuilder.strict=true")
.compile(dto(), collidingModule());

assertThat(compilation).failed();
assertThat(compilation).hadErrorContaining("Error generating Jackson module");
}
}
Loading