vl/spec/vl_spec.lua
2025-06-04 14:46:37 +03:00

197 lines
7.5 KiB
Lua

local v = require "vl"
describe("vl", function()
-- helper for ok, err pattern
local function assert_fails(validator, value, expected_err)
local ok, err = validator(value)
assert.is_nil(ok, "Expected validation to fail")
assert.is_string(err)
if expected_err then
assert.is_equal(err, expected_err)
end
end
describe("primitives", function()
it("has basic building blocks", function()
local always = v.always()
assert.are.equal(always(true), true)
assert.are.equal(always(1984), 1984)
local never = v.never()
assert_fails(never, 17, "validator failed unconditionally")
assert_fails(never, "hello world", "validator failed unconditionally")
end)
it("validates strings", function()
local str = v.string()
assert.are.equal(str("hello"), "hello")
assert_fails(str, 2005, "expected string, got number")
end)
it("validates numbers", function()
local n = v.number()
assert.are.equal(n(1993), 1993)
assert_fails(n, "foo", "expected number, got string")
end)
it("validates booleans", function()
local b = v.boolean()
assert.are.equal(b(true), true)
-- the boolean validator is constructed via combinators, so the error message is different from the ones above
assert_fails(b, nil, "no validators matched: [1]: expected value to be true, got nil; [2]: expected value to be false, got nil")
end)
it("validates literals", function()
local literal = v.literal("vl")
assert.are.equal(literal("vl"), "vl")
assert_fails(literal, true, "expected value to be vl, got true")
end)
end)
describe("combinators", function()
it("validates optionals", function()
local maybe_string = v.maybe(v.string())
assert.are.equal(maybe_string("a string"), "a string")
assert.are.equal(maybe_string(), nil)
-- maybe() is just sugar for attempts(p_validator, literal(nil))
assert_fails(maybe_string, 42, "no validators matched: [1]: expected value to be nil, got 42; [2]: expected string, got number")
end)
it("chains with also()", function()
local emailish = v.string():also(function(x) return x:find("@") end, "string should be email")
assert.are.equal(emailish("lua@rocks.org"), "lua@rocks.org")
assert_fails(emailish, "not-an-email", "string should be email")
end)
it("combines with attempts()", function()
local num_or_str = v.attempts(v.number(), v.string())
assert.are.equal(num_or_str(79), 79)
assert.are.equal(num_or_str("97"), "97")
assert_fails(num_or_str, {cant = "be a table"}, "no validators matched: [1]: expected number, got table; [2]: expected string, got table")
end)
end)
describe("schemas", function()
it("validates strict schemas", function()
local person = v.schema{
name = v.string(),
age = v.maybe(v.number())
}
assert.are.same(person{name = "Roberto", age = 65}, {name = "Roberto", age = 65})
assert.are.same(person{name = "Mike"}, {name = "Mike"})
assert_fails(person, {name = 1960}, "schema errors: key name: expected string, got number")
assert_fails(person, {name = "Luiz", some = "field"}, "schema errors: unexpected field: some")
end)
it("validates open schemas", function()
local open = v.open_schema{
just_one = v.string()
}
assert.are.same(open{just_one = "yes!"}, {just_one = "yes!"})
assert.are.same(open{just_one = "not at all", some = "are out there"}, {just_one = "not at all", some = "are out there"})
assert_fails(open, {none = "at all"}, "schema errors: key just_one: expected string, got nil")
end)
it("validates strict arrays", function()
local array = v.array{v.number(), v.string()}
assert.are.same(array{2007, "LuaRocks"}, {2007, "LuaRocks"})
assert_fails(array, {"nineteen-ninety-three", false}, "array errors: 1: expected number, got string; 2: expected string, got boolean")
assert_fails(array, {0, "cannot have more than two", "really!"}, "expected exactly 2 elements, got 3")
end)
it("validates open arrays", function()
local open_array = v.open_array{v.boolean()}
assert.are.same(open_array{true}, {true})
assert.are.same(open_array{false, true, "maybe"}, {false, true, "maybe"})
assert_fails(open_array, {}, "expected at least 1 elements, got 0")
end)
it("runs a validator over an array", function()
local array_of = v.array_of(v.attempts(v.boolean(), v.string())) -- an array of mixed bools or strings
assert.are.same(array_of({true, "hello", false}), {true, "hello", false})
assert_fails(array_of, {true, -5}, "array_of errors: [2]: no validators matched: [1]: no validators matched: [1]: expected value to be true, got -5; [2]: expected value to be false, got -5; [2]: expected string, got number")
end)
end)
describe("complex scenario", function()
it("validates a user profile with nested rules", function()
local function string_length_between(min, max)
return function(s)
return #s >= min and #s <= max
end
end
local username = v.string()
:also(string_length_between(3, 20), "3-20 chars")
:also(function(s) return not s:find("%s") end, "no spaces")
local password = v.string()
:also(string_length_between(8, 64), "8-64 chars")
:also(function(s) return s:find("[%d]") and s:find("_") end, "must contain a number and an underscore")
local age = v.number():also(function(n) return n > 13 end, "must be over 13")
local user_schema = v.schema{
-- required fields
username = username,
password = password,
--optional fields
age = v.maybe(age),
gender = v.maybe(v.always()), -- accept anything
socials = v.maybe(v.open_schema{
bsky = v.maybe(v.string():also(function(s) return s:find("@") end)),
git = v.maybe(v.string():also(function(s) return s:find("git.", 1, true) end)),
}:also(function(t) return t.bsky or t.git end, "must specify at least one of bsky OR git")),
interests = v.maybe(v.array_of(
v.string():also(string_length_between(3, 25))
):also(function(t) return string.lower(t[1]) == "lua" end, "first interest must be lua"))
}
local good_user = {
username = "yagich",
password = "noway_jose_54",
age = 30,
gender = "yes",
socials = {
bsky = "@yagich.bsky.social"
},
interests = {"lua", "lua", "more lua"}
}
assert.are.equal(user_schema(good_user), good_user)
-- the schema validator uses pairs() internally, so the error messages would be different each time
-- therefore i didn't pass them in these
-- this fails the password
assert_fails(user_schema, {
username = "name with spaces",
password = "wrongpassword",
})
-- this fails the socials constraint (must have at least either git or bskys)
assert_fails(user_schema, {
username = "valid_username",
password = "2ok4u_maybe",
socials = {
youtube = "@someone"
}
})
-- this fails the interests constraint (first must be lua)
assert_fails(user_schema, {
username = "verygood",
password = "correct_HorseBatt3ryStaple",
interests = {"some-other-lang"}
})
end)
end)
end)