279 lines
7.8 KiB
Lua
279 lines
7.8 KiB
Lua
---@class Validator
|
|
---@overload fun(value: any): any, string?
|
|
---validates some value and returns it back if successful. if the validation fails, returns nil and an error message.
|
|
local Validator = {}
|
|
|
|
local validator_mt = {
|
|
__index = Validator,
|
|
__call = function(self, value)
|
|
local ok, err = self._validate(value)
|
|
if not ok then
|
|
return nil, err
|
|
end
|
|
return value
|
|
end
|
|
}
|
|
|
|
local function construct_validator(v_fn)
|
|
return setmetatable({
|
|
_validate = v_fn
|
|
}, validator_mt)
|
|
end
|
|
|
|
-- additional constraints
|
|
---attaches an additional constraint to the validator and returns it.
|
|
---@param fn fun(value: any): boolean the validation function
|
|
---@param err_message string? the error message to display when this constraint fails
|
|
---@return Validator
|
|
function Validator:also(fn, err_message)
|
|
local original_validate = self._validate
|
|
self._validate = function(value)
|
|
local ok, err = original_validate(value)
|
|
if not ok then return ok, err end
|
|
if not fn(value) then
|
|
return false, err_message or "additional constraint failed"
|
|
end
|
|
return true
|
|
end
|
|
return self
|
|
end
|
|
|
|
-- core
|
|
|
|
---constructs a validator that always succeeds.
|
|
---@return Validator
|
|
local function always()
|
|
return construct_validator(function(_) return true end)
|
|
end
|
|
|
|
---constructs a validator that never succeeds.
|
|
---@return Validator
|
|
local function never()
|
|
return construct_validator(function(_) return false, "validator failed unconditionally" end)
|
|
end
|
|
|
|
---constructs a validator that succeeds on the first successful inner validator (ordered choice).
|
|
---@param ... Validator
|
|
---@return Validator
|
|
local function attempts(...)
|
|
local validators = {...}
|
|
return construct_validator(function(value)
|
|
local errors = {}
|
|
for i, v in ipairs(validators) do
|
|
local ok, result = v._validate(value)
|
|
if ok then
|
|
return true, result
|
|
end
|
|
table.insert(errors, ("[%d]: %s"):format(i, result or "validation failed"))
|
|
end
|
|
return false, "no validators matched: " .. table.concat(errors, "; ")
|
|
end)
|
|
end
|
|
|
|
-- primitives
|
|
|
|
---constructs a validator that succeeds if the input is a string.
|
|
---@return Validator
|
|
local function string()
|
|
return construct_validator(function(value)
|
|
if type(value) ~= "string" then
|
|
return false, "expected string, got " .. type(value)
|
|
end
|
|
return true
|
|
end)
|
|
end
|
|
|
|
---constructs a validator that succeeds if the input is a number.
|
|
---@return Validator
|
|
local function number()
|
|
return construct_validator(function(value)
|
|
if type(value) ~= "number" then
|
|
return false, "expected number, got " .. type(value)
|
|
end
|
|
return true
|
|
end)
|
|
end
|
|
|
|
---constructs a validator that succeeds if and only if the input value is exactly equal to `expected` (via `__eq`).
|
|
---@param expected any
|
|
---@return Validator
|
|
local function literal(expected)
|
|
return construct_validator(function(value)
|
|
if value ~= expected then
|
|
return false, ("expected value to be %s, got %s"):format(expected, value)
|
|
end
|
|
return true
|
|
end)
|
|
end
|
|
|
|
---constructs a validator that succeeds if the input is a boolean.
|
|
---@return Validator
|
|
local function boolean()
|
|
return attempts(
|
|
literal(true),
|
|
literal(false)
|
|
)
|
|
end
|
|
|
|
-- shaped validators
|
|
|
|
---constructs a validator that succeeds if `v` returns `nil`. equivalent to `attempts(p_validator, literal(nil))`.
|
|
---@param v Validator
|
|
---@return Validator
|
|
local function maybe(v)
|
|
return attempts(literal(nil), v)
|
|
end
|
|
|
|
---constructs 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`.
|
|
---@param shape table<string, Validator>
|
|
---@return Validator
|
|
local function schema(shape)
|
|
return construct_validator(function(value)
|
|
if type(value) ~= "table" then
|
|
return false, "expected table, got " .. type(value)
|
|
end
|
|
|
|
local errors = {}
|
|
for k, elem_validator in pairs(shape) do
|
|
local ok, err = elem_validator._validate(value[k])
|
|
if not ok then
|
|
table.insert(errors, ("key %s: %s"):format(k, err))
|
|
end
|
|
end
|
|
|
|
for k in pairs(value) do
|
|
if shape[k] == nil then
|
|
table.insert(errors, "unexpected field: "..k)
|
|
end
|
|
end
|
|
|
|
if #errors > 0 then
|
|
return false, "schema errors: "..table.concat(errors, "; ")
|
|
end
|
|
return true
|
|
end)
|
|
end
|
|
|
|
---constructs a validator that succeeds if all the validator elements of `shape` validate entries of the input under the same keys. will NOT fail if the input contains any keys not present in `shape`.
|
|
---@param shape table<string, Validator>
|
|
---@return Validator
|
|
local function open_schema(shape)
|
|
return construct_validator(function(value)
|
|
if type(value) ~= "table" then
|
|
return false, "expected table, got " .. type(value)
|
|
end
|
|
|
|
local errors = {}
|
|
for k, elem_validator in pairs(shape) do
|
|
local ok, err = elem_validator._validate(value[k])
|
|
if not ok then
|
|
table.insert(errors, ("key %s: %s"):format(k, err))
|
|
end
|
|
end
|
|
|
|
if #errors > 0 then
|
|
return false, "schema errors: "..table.concat(errors, "; ")
|
|
end
|
|
return true
|
|
end)
|
|
end
|
|
|
|
---constructs 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`.
|
|
---@param shape Validator[]
|
|
---@return Validator
|
|
local function array(shape)
|
|
return construct_validator(function(value)
|
|
if type(value) ~= "table" then
|
|
return false, "expected table, got " .. type(value)
|
|
end
|
|
|
|
if #shape ~= #value then
|
|
return false, ("expected exactly %d elements, got %d"):format(#shape, #value)
|
|
end
|
|
|
|
local errors = {}
|
|
for i, elem_validator in ipairs(shape) do
|
|
local ok, err = elem_validator._validate(value[i])
|
|
if not ok then
|
|
table.insert(errors, ("%d: %s"):format(i, err))
|
|
end
|
|
end
|
|
|
|
if #errors > 0 then
|
|
return false, "array errors: "..table.concat(errors, "; ")
|
|
end
|
|
return true
|
|
end)
|
|
end
|
|
|
|
---constructs a validator that succeeds if all the validator elements of the `shape` array validate entries of the input with the same numerical index. will fail if the input array's size is smaller than that of `shape`'s.
|
|
---@param shape Validator[]
|
|
---@return Validator
|
|
local function open_array(shape)
|
|
return construct_validator(function(value)
|
|
if type(value) ~= "table" then
|
|
return false, "expected table, got " .. type(value)
|
|
end
|
|
|
|
if #value < #shape then
|
|
return false, ("expected at least %d elements, got %d"):format(#shape, #value)
|
|
end
|
|
|
|
local errors = {}
|
|
for i, elem_validator in ipairs(shape) do
|
|
local ok, err = elem_validator._validate(value[i])
|
|
if not ok then
|
|
table.insert(errors, ("%d: %s"):format(i, err))
|
|
end
|
|
end
|
|
|
|
if #errors > 0 then
|
|
return false, "array errors: "..table.concat(errors, "; ")
|
|
end
|
|
return true
|
|
end)
|
|
end
|
|
|
|
---constructs a validator that will validate every element in an array with the `v` validator.
|
|
---@param v Validator
|
|
---@return Validator
|
|
local function array_of(v)
|
|
return construct_validator(function(value)
|
|
if type(value) ~= "table" then
|
|
return false, "expected table, got " .. type(value)
|
|
end
|
|
|
|
local errors = {}
|
|
for i, elem in ipairs(value) do
|
|
local ok, err = v._validate(elem)
|
|
if not ok then
|
|
table.insert(errors, ("[%d]: %s"):format(i, err))
|
|
end
|
|
end
|
|
|
|
if #errors > 0 then
|
|
return false, "array_of errors: "..table.concat(errors, "; ")
|
|
end
|
|
return true
|
|
end)
|
|
end
|
|
|
|
return {
|
|
always = always,
|
|
never = never,
|
|
maybe = maybe,
|
|
|
|
string = string,
|
|
number = number,
|
|
boolean = boolean,
|
|
literal = literal,
|
|
|
|
attempts = attempts,
|
|
|
|
schema = schema,
|
|
open_schema = open_schema,
|
|
array = array,
|
|
open_array = open_array,
|
|
array_of = array_of,
|
|
}
|