CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-org-immutables--value

Compile-time annotation processor for generating immutable value objects with builder patterns and compile-time validation.

Pending
Overview
Eval results
Files

validation.mddocs/

Validation and Constraints

Built-in validation framework with constraint checking, invariant validation, custom exception handling, and attribute privacy controls for ensuring object consistency and correctness.

Capabilities

Invariant Validation

Mark methods for automatic invariant validation after instance creation.

/**
 * Annotates method that should be invoked internally to validate invariants
 * after instance has been created, but before returned to client.
 * Method must be parameter-less, non-private, void return type, and not
 * throw checked exceptions.
 */
@interface Value.Check { }

Usage Examples:

@Value.Immutable
public interface Rectangle {
    double width();
    double height();
    
    @Value.Check
    protected void validate() {
        if (width() <= 0) {
            throw new IllegalStateException("Width must be positive, got: " + width());
        }
        if (height() <= 0) {
            throw new IllegalStateException("Height must be positive, got: " + height());
        }
    }
}

// Validation runs automatically during construction
Rectangle rect = ImmutableRectangle.builder()
    .width(10.0)
    .height(5.0)
    .build(); // Validation passes

// This would throw IllegalStateException during build()
// Rectangle invalid = ImmutableRectangle.builder()
//     .width(-5.0)
//     .height(10.0)
//     .build();

// Normalization with @Value.Check returning instance
@Value.Immutable
public interface NormalizedText {
    String text();
    
    @Value.Check
    default NormalizedText normalize() {
        String normalized = text().trim().toLowerCase();
        if (!normalized.equals(text())) {
            return ImmutableNormalizedText.builder()
                .text(normalized)
                .build();
        }
        return this; // Return this if already normalized
    }
}

// Automatic normalization during construction
NormalizedText text = ImmutableNormalizedText.builder()
    .text("  Hello World  ")
    .build();

assert text.text().equals("hello world"); // Normalized automatically

Attribute Privacy and Security

Control attribute visibility and secure sensitive data in string representations.

/**
 * Marks attribute for exclusion from auto-generated toString() method.
 * Can be excluded completely or replaced with masking characters.
 */
@interface Value.Redacted { }

Usage Examples:

@Value.Immutable
public interface UserCredentials {
    String username();
    
    @Value.Redacted
    String password();
    
    @Value.Redacted
    String apiKey();
    
    String email();
}

UserCredentials creds = ImmutableUserCredentials.builder()
    .username("alice")
    .password("secret123")
    .apiKey("sk-1234567890abcdef")
    .email("alice@example.com")
    .build();

// Redacted attributes hidden from toString()
System.out.println(creds);
// Output: UserCredentials{username=alice, email=alice@example.com}
// password and apiKey are omitted

// Custom masking with Style configuration
@Value.Style(redactedMask = "***")
@Value.Immutable
public interface MaskedCredentials {
    String username();
    
    @Value.Redacted
    String password();
}

MaskedCredentials masked = ImmutableMaskedCredentials.builder()
    .username("bob")
    .password("topsecret")
    .build();

System.out.println(masked);
// Output: MaskedCredentials{username=bob, password=***}

Custom Exception Handling

Configure custom exception types for validation failures.

/**
 * Style configuration for custom exception types
 */
@interface Value.Style {
    /**
     * Runtime exception to throw when immutable object is in invalid state.
     * Exception class must have constructor taking single string.
     */
    Class<? extends RuntimeException> throwForInvalidImmutableState() 
        default IllegalStateException.class;
    
    /**
     * Runtime exception to throw when null reference is passed to 
     * non-nullable parameter. Default is NullPointerException.
     */
    Class<? extends RuntimeException> throwForNullPointer() 
        default NullPointerException.class;
}

Usage Examples:

// Custom validation exception
public class ValidationException extends RuntimeException {
    public ValidationException(String message) {
        super(message);
    }
    
    public ValidationException(String... missingFields) {
        super("Missing required fields: " + String.join(", ", missingFields));
    }
}

@Value.Style(
    throwForInvalidImmutableState = ValidationException.class,
    throwForNullPointer = ValidationException.class
)
@Value.Immutable
public interface ValidatedOrder {
    @Nullable String customerId();
    List<String> items();
    BigDecimal total();
    
    @Value.Check
    protected void validateOrder() {
        if (items().isEmpty()) {
            throw new ValidationException("Order must contain at least one item");
        }
        if (total().compareTo(BigDecimal.ZERO) < 0) {
            throw new ValidationException("Order total cannot be negative");
        }
    }
}

// Custom exceptions thrown on validation failures
try {
    ValidatedOrder order = ImmutableValidatedOrder.builder()
        .customerId("123")
        .total(new BigDecimal("100.00"))
        // Missing items
        .build();
} catch (ValidationException e) {
    // Custom exception with meaningful message
    System.err.println(e.getMessage()); // "Missing required fields: items"
}

Bean Validation Integration

Integration with JSR 303 Bean Validation API for declarative constraint validation.

/**
 * Bean Validation integration via ValidationMethod.VALIDATION_API
 * Disables null checks in favor of @NotNull annotations and uses
 * static validator per object type.
 */
enum ValidationMethod {
    VALIDATION_API  // Use JSR 303 Bean Validation API
}

Usage Examples:

import javax.validation.constraints.*;

@Value.Style(validationMethod = ValidationMethod.VALIDATION_API)
@Value.Immutable
public interface ValidatedUser {
    @NotNull
    @Size(min = 2, max = 50)
    String firstName();
    
    @NotNull
    @Size(min = 2, max = 50)
    String lastName();
    
    @NotNull
    @Email
    String email();
    
    @Min(0)
    @Max(120)
    Integer age();
    
    @Pattern(regexp = "^\\+?[1-9]\\d{1,14}$")
    String phoneNumber();
    
    @DecimalMin("0.0")
    @Digits(integer = 10, fraction = 2)
    BigDecimal salary();
}

// Bean Validation constraints enforced during construction
ValidatedUser user = ImmutableValidatedUser.builder()
    .firstName("John")
    .lastName("Doe")
    .email("john.doe@example.com")
    .age(30)
    .phoneNumber("+1234567890")
    .salary(new BigDecimal("50000.00"))
    .build(); // All constraints validated

// Constraint violations throw ConstraintViolationException
try {
    ValidatedUser invalid = ImmutableValidatedUser.builder()
        .firstName("J")              // Too short
        .lastName("Doe")
        .email("invalid-email")      // Invalid format
        .age(150)                    // Too old
        .phoneNumber("invalid")      // Invalid pattern
        .salary(new BigDecimal("-1000")) // Below minimum
        .build();
} catch (ConstraintViolationException e) {
    e.getConstraintViolations().forEach(violation -> 
        System.err.println(violation.getPropertyPath() + ": " + violation.getMessage())
    );
}

Complex Validation Scenarios

Advanced validation patterns for complex business rules and cross-field validation.

Usage Examples:

@Value.Immutable
public interface DateRange {
    LocalDate startDate();
    LocalDate endDate();
    
    @Value.Check
    protected void validateRange() {
        if (startDate().isAfter(endDate())) {
            throw new IllegalStateException(
                "Start date " + startDate() + " must be before end date " + endDate()
            );
        }
        
        long daysBetween = ChronoUnit.DAYS.between(startDate(), endDate());
        if (daysBetween > 365) {
            throw new IllegalStateException(
                "Date range cannot exceed 365 days, got " + daysBetween + " days"
            );
        }
    }
    
    @Value.Derived  
    default long durationDays() {
        return ChronoUnit.DAYS.between(startDate(), endDate()) + 1;
    }
}

// Complex validation with business rules
@Value.Immutable
public interface Loan {
    BigDecimal principal();
    BigDecimal interestRate();
    int termMonths();
    String borrowerCreditScore();
    
    @Value.Check
    protected void validateLoan() {
        // Principal limits
        if (principal().compareTo(new BigDecimal("1000")) < 0) {
            throw new IllegalStateException("Minimum loan amount is $1,000");
        }
        if (principal().compareTo(new BigDecimal("1000000")) > 0) {
            throw new IllegalStateException("Maximum loan amount is $1,000,000");
        }
        
        // Interest rate validation
        if (interestRate().compareTo(BigDecimal.ZERO) <= 0 || 
            interestRate().compareTo(new BigDecimal("50")) > 0) {
            throw new IllegalStateException("Interest rate must be between 0% and 50%");
        }
        
        // Term validation
        if (termMonths < 1 || termMonths > 360) {
            throw new IllegalStateException("Loan term must be between 1 and 360 months");
        }
        
        // Credit score dependent validation
        int creditScore = Integer.parseInt(borrowerCreditScore());
        if (creditScore < 300 || creditScore > 850) {
            throw new IllegalStateException("Credit score must be between 300 and 850");
        }
        
        // Business rule: high principal requires good credit
        if (principal().compareTo(new BigDecimal("100000")) > 0 && creditScore < 700) {
            throw new IllegalStateException(
                "Loans over $100,000 require credit score of at least 700"
            );
        }
    }
    
    @Value.Lazy
    default BigDecimal monthlyPayment() {
        // Complex calculation using all validated fields
        double monthlyRate = interestRate().doubleValue() / 100.0 / 12.0;
        double amount = principal().doubleValue();
        
        if (monthlyRate == 0) {
            return principal().divide(BigDecimal.valueOf(termMonths), 2, RoundingMode.HALF_UP);
        }
        
        double payment = amount * (monthlyRate * Math.pow(1 + monthlyRate, termMonths)) 
                        / (Math.pow(1 + monthlyRate, termMonths) - 1);
        
        return BigDecimal.valueOf(payment).setScale(2, RoundingMode.HALF_UP);
    }
}

Install with Tessl CLI

npx tessl i tessl/maven-org-immutables--value

docs

advanced-features.md

attributes.md

core-immutable.md

index.md

style-configuration.md

validation.md

tile.json