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)