a lua data validation library
Go to file
2025-06-04 14:46:37 +03:00
spec initial commit 2025-06-04 14:46:37 +03:00
LICENSE initial commit 2025-06-04 14:46:37 +03:00
README.md initial commit 2025-06-04 14:46:37 +03:00
vl.lua initial commit 2025-06-04 14:46:37 +03:00

vl

vl is a simple data validation library for Lua.

how-to

the preferred way to use vl is to vendor it - include vl.lua in your project somewhere and require it:

local v = require "vl"

quick example

local v = require "vl"

local over_13 = v.number():also(function(n) return n > 13 end, "must be over 13")

local ok, err = over_13(user_input.age)
if not ok then
  print(err)
else
  print(ok) -- prints the original number if > 13
end

validators

validators are the building blocks of vl. Validator is a light class that can be called with some input to validate it. if successful, it will return the input back. if there was an error, the return will be nil and a second value will be returned which is the error message.

every function in the module returns a new instance of a Validator.

from here on, it will be assumed that vl is required as v.

primitives

v.always()

a validator that always succeeds, no matter the input.

v.never()

a validator that never succeeds, no matter the input.

v.maybe(v: Validator)

a validator that succeeds if v succeeds, or if the value is exactly nil.

v.string()

a validator that succeeds if the input is a string.

v.number()

a validator that succeeds if the input is a number.

v.boolean()

a validator that succeeds if the input is a boolean.

v.literal(expected: any)

a validator that succeeds if the input is exactly equal to expected, via __eq.

combinators

Validator:also(f: function, err: string?)

appends an additional constraint to the validator that will determine if it succeeds. the parameter f takes the input value as the sole parameter and must return a truthy or falsy value. err will be the error message displayed if the match fails.

this method returns the new Validator, for chaining.

local has_at = v.string():also(function(s) return s:find("@") end, "must contain @")

v.attempts(...Validator)

a validator that succeeds on the first successful inner validator (ordered choice). collects all errors along the way. can be thought of as an OR operator.

local string_or_num = v.attempts(v.string(), v.number())
print(string_or_num("yes")) -- "yes"
print(string_or_num(1993)) -- 1993, 
print(string_or_num(true)) -- nil, no validators matched: [1]: expected string, got boolean; [2]: expected number, got boolean

v.maybe(T) is implemented as v.attempts(v.literal(nil), T) and v.boolean() is implemented as v.attempts(v.literal(true), v.literal(false)).

schemas

v.schema(shape: table<string, Validator>)

a validator that succeeds if and only if all the validator elements of shape validate entries of the input under the same keys. will fail if the input contains any keys not present in shape.

local user = v.schema{
  username = v.string():also(function(x) return #x > 3 end, "at least 3 characters"),
  password = v.string():also(function(x) return x:find("[%d]") end, "password must have a digit"),
}

print(user{username = "yoyo", password = "nodigits"}) -- nil, schema errors: key password: password must have a digit

v.open_schema(shape: table<string, Validator>)

like v.schema(), but allows keys not present in the shape.

v.array(shape: Validator[])

a validator that succeeds if and only if all the validator elements of the shape array validate entries of the input with the same numerical index. will fail if the input is not exactly the same size as shape.

local numeric_tuple = v.array{v.number(), v.number()}

print(numeric_tuple{1, 2, 3}) -- nil, expected exactly 2 elements, got 3

v.open_array(shape: Validator[])

like v.array(), but allows additional elements after the ones in shape.

local numeric_tuple = v.open_array{v.number(), v.number()}

print(numeric_tuple{1, 2, 3}) -- {1, 2, 3}

v.array_of(v: Validator)

a validator that will validate that every element of the input array matches against the validator v.

local str_or_bool_ary = v.array_of(v.attempts(v.boolean(), v.string()))

print(str_or_bool_ary{true, true, "true", false, "ahoy", false}) -- {true, true, "true", false, "ahoy", false}

that's it!