initial commit
This commit is contained in:
196
spec/vl_spec.lua
Normal file
196
spec/vl_spec.lua
Normal file
@ -0,0 +1,196 @@
|
||||
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)
|
Reference in New Issue
Block a user