Skip to content
tessoir edited this page Mar 17, 2026 · 1 revision

Rule

Rule is a functional interface that represents a single validation check:

fun interface Rule {
    fun check(): Violation?
}

It returns null on success and a Violation on failure. This contract is what the scope's enforce function expects — every validation in KVerify ultimately reduces to a Rule.

Why Rule exists

Rule separates the definition of a check from its execution. The scope decides what happens when a rule fails — collecting, throwing, or anything else — and the rule itself stays focused on one thing: determining whether a condition holds.

This also means rules are values. You can store them, pass them around, and compose them programmatically without coupling to any specific scope or validation context.

Verification extension functions

The most common way to write reusable rules is as extension functions on Verification<T>. This is exactly how all built-in rules are implemented, and your own rules follow the same pattern:

fun Verification<String>.validEmail(): Verification<String> =
    apply {
        scope.failIf({ !value.contains("@") }) {
            violation("Must be a valid email address")
        }
    }

Under the hood, scope.failIf constructs a Rule and passes it to scope.enforce. The scope then decides what to do with the violation — collect it, throw it, or handle it in any other way. The rule itself has no knowledge of the scope's behavior.

Returning this from the extension function allows chaining:

validateCollecting {
    verify(user::email).notBlank().validEmail()
}

For rules that carry structured failure information, produce a typed violation instead:

data class InvalidEmailViolation(
    val actual: String,
    override val validationPath: ValidationPath,
) : PathAwareViolation {
    override val reason = "'$actual' is not a valid email address"
}

fun Verification<String>.validEmail(): Verification<String> =
    apply {
        scope.failIf({ !value.contains("@") }) {
            InvalidEmailViolation(
                actual = value,
                validationPath = scope.validationContext.validationPath(),
            )
        }
    }

Use Verification<T> extensions when the rule is about a specific value type and you want it to chain naturally with other rules.

ValidationScope extension functions

For validating whole objects or grouping related checks, extend ValidationScope instead:

fun ValidationScope.validateSignUpRequest(request: SignUpRequest) {
    verify(request::username).notBlank().minLength(3).maxLength(20)
    verify(request::email).notBlank().validEmail()
    verify(request::age).atLeast(18)
}

These compose freely inside any scope:

val result = validateCollecting {
    validateSignUpRequest(request)
}

Because the extension receives the active scope as its receiver, it participates fully in whatever scope it is called from — collecting or throwing, with whatever context is active at the call site.

Use ValidationScope extensions when you are validating a whole object or grouping multiple related checks that belong together conceptually.

Using Rule directly

Rule is a functional interface, so it can be instantiated as a lambda. Here is a simple example with no external dependencies:

val alwaysFails = Rule {
    violation("This always fails")
}

validateCollecting {
    enforce(alwaysFails)
}

In practice, rules often capture values from the surrounding scope — this is a closure. If you are unfamiliar with the concept, Kotlin's documentation on lambdas is a good starting point.

With closures, rules can be built dynamically and passed around as values:

fun nameNotBlank(user: User): Rule = Rule {
    if (user.name.isBlank()) violation("Name must not be blank") else null
}

fun ageAtLeast(user: User, min: Int): Rule = Rule {
    if (user.age < min) violation("User must be at least $min years old") else null
}

val rules = listOf(nameNotBlank(user), ageAtLeast(user, 18))

validateCollecting {
    rules.forEach { enforce(it) }
}

This is useful when you need to build a list of rules dynamically, store them, or compose checks programmatically.

When to use each approach

Approach Use when
Verification<T> extension The rule applies to a specific value type and should chain with other rules
ValidationScope extension You are validating a whole object or grouping related checks
Rule directly You need to store, pass around, or compose checks as values

Clone this wiki locally