197 lines
7.5 KiB
Lua
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)
|