Interface Guard<T>

Type Parameters:
T - the type of value being validated
Functional Interface:
This is a functional interface and can therefore be used as the assignment target for a lambda expression or method reference.

@FunctionalInterface @NullMarked public interface Guard<T>
A named, composable predicate that produces a Validated result when applied to a value.

Guard<T> is a @FunctionalInterface whose single abstract method is check(T), which returns Validated<NonEmptyList<String>, T>: Valid(value) when the predicate passes, or Invalid(errors) when it fails. All composition operators (and(Guard), or(Guard), negate(), andThen(Guard), contramap(Function)) are default methods, so guards can be defined as lambdas and composed without inheritance. The choice to use @FunctionalInterface with default methods rather than an abstract class is documented in ADR-011 — Guard<T> as a @FunctionalInterface with default methods.

Guards are designed to be defined once and reused across validation pipelines, eliminating the repetitive if/Validated.invalidNel(Object) pattern:

Guard<String> notBlank     = Guard.of(s -> !s.isBlank(),         "must not be blank");
Guard<String> minLength3   = Guard.of(s -> s.length() >= 3,      "must be at least 3 chars");
Guard<String> alphanumeric = Guard.of(s -> s.matches("[\\w]+"),  "must be alphanumeric");

Guard<String> username = notBlank.and(minLength3).and(alphanumeric);

username.check("al");  // Invalid(["must be at least 3 chars"])
username.check("ok?"); // Invalid(["must be alphanumeric"])
username.check("alice"); // Valid("alice")

Error type

The error type is fixed as NonEmptyList<String> — human-readable messages accumulated across all failing guards. This design avoids requiring a BinaryOperator<E> for merging in and(Guard)/or(Guard), and guarantees at least one error is always present. The trade-off is that typed domain error objects require working directly with Validated<E, A> instead. This decision is documented in ADR-005 — Guard<T> accumulates errors as a fixed NonEmptyList<String>.

Composition semantics

  • and — both guards must pass; errors from all failing guards are accumulated (not fail-fast).
  • or — the first passing guard short-circuits; if all fail, all errors are accumulated.
  • negate / negate(message) / negate(messageFromValue) — inverts the predicate.
  • Method Details

    • check

      Validated<NonEmptyList<String>, T> check(T value)
      Applies this guard to value.
      Parameters:
      value - the value to validate
      Returns:
      Valid(value) if the predicate passes, or Invalid(errors) if it fails
    • of

      static <T> Guard<T> of(Predicate<? super T> predicate, String errorMessage)
      Creates a Guard<T> from a predicate and a static error message.

      Example:

      Guard<String> notBlank = Guard.of(s -> !s.isBlank(), "must not be blank");
      
      Type Parameters:
      T - the value type
      Parameters:
      predicate - the condition that must hold for the value to be valid
      errorMessage - the error message produced when the predicate fails
      Returns:
      a new Guard<T>
      Throws:
      NullPointerException - if predicate or errorMessage is null
    • of

      static <T> Guard<T> of(Predicate<? super T> predicate, Function<? super T, String> errorMessageFn)
      Creates a Guard<T> from a predicate and a dynamic error message function.

      The errorMessageFn receives the failing value so it can produce a context-specific message.

      Example:

      Guard<Integer> max = Guard.of(
          n -> n <= 100,
          n -> "must be ≤ 100, got " + n
      );
      
      Type Parameters:
      T - the value type
      Parameters:
      predicate - the condition that must hold for the value to be valid
      errorMessageFn - function that produces an error message from the failing value
      Returns:
      a new Guard<T>
      Throws:
      NullPointerException - if predicate or errorMessageFn is null
    • and

      default Guard<T> and(Guard<T> other)
      Returns a composed guard that requires both this guard and other to pass.

      Both guards are always evaluated — this is not fail-fast. Errors from all failing guards are accumulated into a single NonEmptyList, so the caller receives a complete picture of all violations at once.

      Example:

      Guard<Integer> positive = Guard.of(n -> n > 0,      "must be positive");
      Guard<Integer> even     = Guard.of(n -> n % 2 == 0, "must be even");
      Guard<Integer> positiveEven = positive.and(even);
      
      positiveEven.check(4);   // Valid(4)
      positiveEven.check(3);   // Invalid(["must be even"])
      positiveEven.check(-1);  // Invalid(["must be positive", "must be even"])
                               //  — both guards evaluated, both errors collected
      
      Parameters:
      other - the guard that must also pass; must not be null
      Returns:
      a composed Guard<T>
      Throws:
      NullPointerException - if other is null
    • andThen

      default Guard<T> andThen(Guard<T> next)
      Returns a composed guard that evaluates next only when this guard passes.

      Unlike and, evaluation is short-circuit: if this guard returns Invalid, next is never called and its error is never accumulated. This makes andThen the safe choice when the downstream guard's predicate would throw on the values rejected by this guard — most notably when composing nonNull() with a rule that dereferences the value:

      Guard<@Nullable String> nonNullAndNotBlank =
          Guard.<@Nullable String>nonNull()
              .andThen(Guard.<@Nullable String>of(s -> s != null && !s.isBlank(),
                                                  "must not be blank"));
      
      nonNullAndNotBlank.check("hello"); // Valid("hello")
      nonNullAndNotBlank.check(null);    // Invalid(["must not be null"]) — next not evaluated
      nonNullAndNotBlank.check("   ");   // Invalid(["must not be blank"])
      

      Use and when you want both guards evaluated regardless of the first result (error accumulation). Use andThen when the second guard must not run until the first has passed.

      Parameters:
      next - the guard to evaluate when this guard passes; must not be null
      Returns:
      a composed Guard<T>
      Throws:
      NullPointerException - if next is null
    • or

      default Guard<T> or(Guard<T> other)
      Returns a composed guard that passes when at least one of this guard or other passes.

      Evaluation is short-circuit: if this guard passes, other is never evaluated. If both fail, errors from both guards are accumulated.

      Example:

      Guard<String> email = Guard.of(s -> s.contains("@"),  "must contain @");
      Guard<String> phone = Guard.of(s -> s.matches("\\d+"), "must be digits");
      Guard<String> contact = email.or(phone);
      
      contact.check("alice@example.com");  // Valid — email passes, phone not evaluated
      contact.check("12345");             // Valid — phone passes
      contact.check("hello");             // Invalid(["must contain @", "must be digits"])
      
      Parameters:
      other - the alternative guard; must not be null
      Returns:
      a composed Guard<T>
      Throws:
      NullPointerException - if other is null
    • negate

      default Guard<T> negate()
      Returns a guard that is the logical negation of this guard, using a generic error message.

      The composed guard returns Valid(value) when this guard fails, and Invalid(["must not satisfy the condition"]) when this guard passes. Use negate(message) to supply a domain-specific error message.

      Returns:
      the negated Guard<T>
    • negate

      default Guard<T> negate(String errorMessage)
      Returns a guard that is the logical negation of this guard, using the supplied error message when the original guard passes.

      Example:

      Guard<String> notAdmin = Guard.of(s -> s.equals("admin"), "is admin")
                                    .negate("username must not be 'admin'");
      
      notAdmin.check("alice"); // Valid("alice")
      notAdmin.check("admin"); // Invalid(["username must not be 'admin'"])
      
      Parameters:
      errorMessage - the error message returned when the original guard passes
      Returns:
      the negated Guard<T>
      Throws:
      NullPointerException - if errorMessage is null
    • negate

      default Guard<T> negate(Function<? super T, String> messageFromValue)
      Returns a guard that is the logical negation of this guard, using a dynamic error message.

      The messageFromValue function receives the value that passed the original guard, allowing a context-specific error message.

      Example:

      Guard<String> notReserved = Guard.of(s -> s.equals("admin") || s.equals("root"), "")
          .negate(s -> "username '" + s + "' is reserved");
      
      notReserved.check("alice"); // Valid("alice")
      notReserved.check("admin"); // Invalid(["username 'admin' is reserved"])
      
      Parameters:
      messageFromValue - function producing an error message from the passing value; must not be null
      Returns:
      the negated Guard<T>
      Throws:
      NullPointerException - if messageFromValue is null
    • withMessage

      default Guard<T> withMessage(String message)
      Returns a guard that replaces any error messages produced by this guard with message.

      Useful when you want to expose a single clean message at a public API boundary regardless of the internal validation details.

      Example:

      Guard<String> username = notBlank.and(minLength3).withMessage("invalid username");
      
      username.check("");    // Invalid(["invalid username"])
      username.check("a");   // Invalid(["invalid username"])
      username.check("alice"); // Valid("alice")
      
      Parameters:
      message - the replacement error message; must not be null
      Returns:
      a new Guard<T> that returns a single fixed error when this guard fails
      Throws:
      NullPointerException - if message is null
    • asPredicate

      default Predicate<T> asPredicate()
      Returns a standard Predicate<T> that returns true when this guard passes and false when it fails.

      Use this to integrate guards with standard Java APIs that accept Predicate (e.g., Stream.filter, Collection.removeIf).

      Example:

      Guard<String> notBlank = Guard.of(s -> !s.isBlank(), "must not be blank");
      
      List<String> valid = Stream.of("alice", "  ", "bob", "")
          .filter(notBlank.asPredicate())
          .toList();
      // ["alice", "bob"]
      
      Returns:
      a Predicate<T> backed by this guard
    • contramap

      default <U> Guard<U> contramap(Function<? super U, ? extends T> mapper)
      Returns a Guard<U> that applies mapper to its input before checking.

      This is the contravariant map operation: it adapts a guard written for type T to work on an enclosing type U by projecting U → T first. It is the idiomatic way to reuse field-level guards on whole objects.

      Example:

      Guard<String> notBlank = Guard.of(s -> !s.isBlank(), "username must not be blank");
      
      // Lift notBlank to validate User objects by their username field
      Guard<User> userGuard = notBlank.contramap(User::username);
      
      userGuard.check(new User("alice")); // Valid(user)
      userGuard.check(new User("   "));   // Invalid(["username must not be blank"])
      
      Type Parameters:
      U - the input type of the returned guard
      Parameters:
      mapper - function that extracts the T value from a U; must not be null
      Returns:
      a new Guard<U> that projects U → T before checking
      Throws:
      NullPointerException - if mapper is null
    • checkToResult

      default Result<T, NonEmptyList<String>> checkToResult(T value)
      Applies this guard to value and returns a Result<T, NonEmptyList<String>>.

      Equivalent to this.check(value).toResult() but removes the need to import and chain the conversion manually.

      Parameters:
      value - the value to validate
      Returns:
      Result.ok(value) if the guard passes, or Result.err(errors) if it fails
    • checkToResult

      default <E> Result<T,E> checkToResult(T value, Function<NonEmptyList<String>, E> toError)
      Applies this guard to value and returns a Result<T, E>, mapping the accumulated error list to a domain-specific error type via toError.

      Use this at domain service boundaries where Result is the preferred container and the error type is richer than a plain list of strings.

      Example:

      Guard<String> username = notBlank.and(minLength3);
      
      Result<String, ValidationException> result = username.checkToResult(
          input,
          errors -> new ValidationException("username", errors.toList())
      );
      
      Type Parameters:
      E - the domain error type
      Parameters:
      value - the value to validate
      toError - function mapping the accumulated error list to E
      Returns:
      Result.ok(value) on success, or Result.err(toError(errors)) on failure
      Throws:
      NullPointerException - if toError is null
    • checkToOption

      default Option<T> checkToOption(T value)
      Applies this guard to value and returns an Option<T>.

      Returns Some(value) when the guard passes and None when it fails, discarding the error details. Use this when you only need to know whether a value is valid, not why it is not.

      Example:

      Guard<String> notBlank = Guard.of(s -> !s.isBlank(), "must not be blank");
      
      // Filter a stream keeping only valid values
      List<String> valid = Stream.of("alice", "  ", "bob")
          .flatMap(s -> notBlank.checkToOption(s).stream())
          .toList();
      // ["alice", "bob"]
      
      Parameters:
      value - the value to validate
      Returns:
      Option.some(value) if the guard passes, or Option.none() if it fails
    • checkToEither

      default Either<NonEmptyList<String>, T> checkToEither(T value)
      Applies this guard to value and returns an Either<NonEmptyList<String>, T>.

      Returns Either.right(value) when the guard passes and Either.left(errors) when it fails. Use this when downstream logic is already expressed in terms of Either.

      Example:

      Guard<String> notBlank = Guard.of(s -> !s.isBlank(), "must not be blank");
      
      Either<NonEmptyList<String>, String> right = notBlank.checkToEither("hello");
      // Either.right("hello")
      
      Either<NonEmptyList<String>, String> left = notBlank.checkToEither("   ");
      // Either.left(NonEmptyList.of("must not be blank"))
      
      Parameters:
      value - the value to validate
      Returns:
      Either.right(value) if the guard passes, or Either.left(errors) if it fails
    • checkToTry

      default Try<T> checkToTry(T value)
      Applies this guard to value and returns a Try<T>.

      Returns Try.success(value) when the guard passes. When it fails, the accumulated error messages are joined with "; " and wrapped in an IllegalArgumentException. Use checkToTry(Object, Function) to supply a domain-specific exception instead.

      Example:

      Guard<String> notBlank = Guard.of(s -> !s.isBlank(), "must not be blank");
      
      Try<String> success = notBlank.checkToTry("hello");
      // Try.success("hello")
      
      Try<String> failure = notBlank.checkToTry("   ");
      // Try.failure(new IllegalArgumentException("must not be blank"))
      
      Parameters:
      value - the value to validate
      Returns:
      Try.success(value) if the guard passes, or a Try.failure wrapping an IllegalArgumentException with the joined error messages
    • checkToTry

      default <X extends Throwable> Try<T> checkToTry(T value, Function<NonEmptyList<String>, X> toThrowable)
      Applies this guard to value and returns a Try<T>, converting any accumulated errors to a domain-specific Throwable via toThrowable.

      Use this at boundaries where the caller controls the exception type.

      Example:

      Guard<String> username = notBlank.and(minLength3);
      
      Try<String> result = username.checkToTry(
          input,
          errors -> new ValidationException(errors.toList())
      );
      
      Type Parameters:
      X - the throwable type
      Parameters:
      value - the value to validate
      toThrowable - function mapping the accumulated error list to a Throwable; must not be null
      Returns:
      Try.success(value) if the guard passes, or Try.failure(toThrowable(errors)) if it fails
      Throws:
      NullPointerException - if toThrowable is null
    • checkToOptional

      default Optional<T> checkToOptional(T value)
      Applies this guard to value and returns a standard Optional<T>.

      Returns Optional.of(value) when the guard passes and Optional.empty() when it fails, discarding the error details. Use this to integrate with Java standard library APIs that work with Optional.

      Example:

      Guard<String> notBlank = Guard.of(s -> !s.isBlank(), "must not be blank");
      
      Optional<String> present = notBlank.checkToOptional("hello"); // Optional.of("hello")
      Optional<String> empty   = notBlank.checkToOptional("   ");   // Optional.empty()
      

      Not suitable when null is a valid success value. This method calls Optional.of(value) on the validated value, which throws NullPointerException if value is null. Under @NullMarked, T is non-null by default, so this is safe for ordinary guards. However, if T is a nullable type (e.g., Guard<@Nullable String>) and the guard can return Valid(null), calling this method will throw NullPointerException. In that case, use check(value) and work with the resulting Validated directly, applying Optional.ofNullable(Object) to the extracted value when needed.

      Parameters:
      value - the value to validate
      Returns:
      Optional.of(value) if the guard passes, Optional.empty() if the guard fails
    • nonNull

      static <T extends @Nullable Object> Guard<T> nonNull()
      Creates and returns a Guard instance that ensures a value is non-null.
      Type Parameters:
      T - the type of the value to be guarded
      Returns:
      a Guard that validates the value is not null