Design by contract support for Groovy with @Invariant, @Requires, and @Ensures annotations
—
Groovy Contracts provides specialized exception classes for different types of contract violations and utilities for tracking multiple violations during contract evaluation.
abstract class AssertionViolation extends AssertionError {
protected AssertionViolation()
protected AssertionViolation(Object o)
protected AssertionViolation(boolean b)
protected AssertionViolation(char c)
protected AssertionViolation(int i)
protected AssertionViolation(long l)
protected AssertionViolation(float v)
protected AssertionViolation(double v)
}Abstract base class for all contract assertion violations. Automatically registers with ViolationTracker upon instantiation to support chronological violation tracking.
Features:
AssertionError for integration with Java assertion systemclass PreconditionViolation extends AssertionViolation {
PreconditionViolation()
PreconditionViolation(Object o)
PreconditionViolation(boolean b)
PreconditionViolation(char c)
PreconditionViolation(int i)
PreconditionViolation(long l)
PreconditionViolation(float v)
PreconditionViolation(double v)
}Thrown when a @Requires annotation condition fails. Indicates that a method was called with invalid arguments or in an invalid state.
Usage Example:
@Requires({ amount > 0 })
void withdraw(BigDecimal amount) {
balance -= amount
}
// Calling withdraw(-100) will throw PreconditionViolationCatching Precondition Violations:
try {
account.withdraw(new BigDecimal("-100"))
} catch (PreconditionViolation e) {
println "Invalid withdrawal amount: ${e.message}"
}class PostconditionViolation extends AssertionViolation {
PostconditionViolation()
PostconditionViolation(Object o)
PostconditionViolation(boolean b)
PostconditionViolation(char c)
PostconditionViolation(int i)
PostconditionViolation(long l)
PostconditionViolation(float v)
PostconditionViolation(double v)
}Thrown when an @Ensures annotation condition fails. Indicates that a method did not fulfill its guaranteed postcondition.
Usage Example:
@Ensures({ result > 0 })
int calculateAge(Date birthDate) {
// Bug: might return negative value for future dates
return (new Date().time - birthDate.time) / (365 * 24 * 60 * 60 * 1000)
}
// Method will throw PostconditionViolation if it returns negative ageCatching Postcondition Violations:
try {
int age = person.calculateAge(futureDate)
} catch (PostconditionViolation e) {
println "Method failed to satisfy postcondition: ${e.message}"
}class ClassInvariantViolation extends AssertionViolation {
ClassInvariantViolation()
ClassInvariantViolation(Object o)
ClassInvariantViolation(boolean b)
ClassInvariantViolation(char c)
ClassInvariantViolation(int i)
ClassInvariantViolation(long l)
ClassInvariantViolation(float v)
ClassInvariantViolation(double v)
}Thrown when an @Invariant annotation condition fails. Indicates that an object's state became invalid during construction or method execution.
Usage Example:
@Invariant({ balance >= 0 })
class BankAccount {
private BigDecimal balance = BigDecimal.ZERO
void withdraw(BigDecimal amount) {
balance -= amount // Might violate invariant
}
}
// Creating account or calling withdraw might throw ClassInvariantViolationCatching Invariant Violations:
try {
BankAccount account = new BankAccount()
account.withdraw(new BigDecimal("1000"))
} catch (ClassInvariantViolation e) {
println "Object state became invalid: ${e.message}"
}class CircularAssertionCallException extends RuntimeException {
CircularAssertionCallException()
CircularAssertionCallException(String s)
CircularAssertionCallException(String s, Throwable throwable)
CircularAssertionCallException(Throwable throwable)
}Thrown when contract evaluation results in circular method calls, preventing infinite recursion. Unlike other contract violations, this extends RuntimeException rather than AssertionViolation.
Usage Example:
@Invariant({ isValid() })
class CircularExample {
boolean isValid() {
return someCondition() // If this triggers invariant check, causes circular call
}
void doSomething() {
// Method call will trigger invariant check
}
}Preventing Circular Calls:
@Invariant({ balance >= 0 }) // Simple field check, no method calls
class SafeAccount {
private BigDecimal balance = BigDecimal.ZERO
boolean isValid() {
return true // Don't call methods in contracts
}
}class ViolationTracker {
static final ThreadLocal<ViolationTracker> INSTANCE
static void init()
static void deinit()
static boolean violationsOccurred()
static void rethrowFirst()
static void rethrowLast()
void track(AssertionViolation assertionViolation)
boolean hasViolations()
AssertionViolation first()
AssertionViolation last()
}Thread-local utility for tracking multiple contract violations in chronological order. Used internally by the contract system to manage violation reporting.
Important: The INSTANCE.get() method can return null if no tracker has been initialized for the current thread. Most static methods include null checking, but direct access requires manual null checking.
Static Methods:
init(): Initialize violation tracker for current thread
ViolationTracker.init()deinit(): Remove violation tracker from current thread
ViolationTracker.deinit()violationsOccurred(): Check if any violations occurred
// Safe to call - includes null checking
if (ViolationTracker.violationsOccurred()) {
// Handle violations
}rethrowFirst(): Rethrow the first violation that occurred
ViolationTracker.rethrowFirst()rethrowLast(): Rethrow the most recent violation
ViolationTracker.rethrowLast()Instance Methods:
track(AssertionViolation): Track a violation (called automatically)
hasViolations(): Check if tracker has recorded violations
first(): Get the first violation
last(): Get the most recent violation
try {
performContractedOperation()
} catch (PreconditionViolation e) {
// Handle invalid input or state
log.error("Invalid precondition: ${e.message}")
return errorResponse("Invalid parameters provided")
} catch (PostconditionViolation e) {
// Handle implementation bugs
log.error("Method failed postcondition: ${e.message}")
return errorResponse("Internal processing error")
} catch (ClassInvariantViolation e) {
// Handle object state corruption
log.error("Object invariant violated: ${e.message}")
return errorResponse("Object state became invalid")
} catch (CircularAssertionCallException e) {
// Handle contract design issues
log.error("Circular contract evaluation: ${e.message}")
return errorResponse("Contract evaluation error")
}// Contracts work with Java assertion system
try {
contractedMethod()
} catch (AssertionError e) {
if (e instanceof AssertionViolation) {
// Handle contract violations specifically
handleContractViolation((AssertionViolation) e)
} else {
// Handle other assertion failures
handleGeneralAssertion(e)
}
}// Disable assertions in production to avoid contract exceptions
// Use JVM flags: -da (disable assertions)
// Or package-specific: -da:com.example.contracts.*
class ProductionService {
@Requires({ input != null })
@Ensures({ result != null })
String processInput(String input) {
// In production with -da, contracts won't throw exceptions
// But defensive programming is still recommended
if (input == null) {
throw new IllegalArgumentException("Input cannot be null")
}
String result = doProcessing(input)
if (result == null) {
throw new IllegalStateException("Processing failed")
}
return result
}
}Install with Tessl CLI
npx tessl i tessl/maven-org-apache-groovy--groovy-contracts