kong/spec/01-unit/01-db/01-schema/01-schema_spec.lua (3,766 lines of code) (raw):
local Schema = require "kong.db.schema"
local cjson = require "cjson"
local luacov_ok = pcall(require, "luacov")
if luacov_ok then
local busted_it = it
-- luacheck: globals it
it = function(desc, fn)
busted_it(desc, function()
local luacov = require("luacov")
luacov.init()
fn()
luacov.save_stats()
end)
end
end
local SchemaKind = {
{ name = "schema", new = Schema.new, },
{ name = "subschema", new = function(definition)
local schema = assert(Schema.new({
name = "test",
subschema_key = "name",
fields = definition.fields,
}))
assert(schema:new_subschema("subtest", definition))
return assert(schema.subschemas["subtest"])
end
}
}
describe("schema", function()
local uuid_pattern = "^" .. ("%x"):rep(8) .. "%-" .. ("%x"):rep(4) .. "%-"
.. ("%x"):rep(4) .. "%-" .. ("%x"):rep(4) .. "%-"
.. ("%x"):rep(12) .. "$"
local function check_all_types_covered(fields)
local covered = {}
for _, item in ipairs(fields) do
local field = item[next(item)]
covered[field.type] = true
end
covered["foreign"] = true
for name, _ in pairs(Schema.valid_types) do
assert.truthy(covered[name], "type '" .. name .. "' not covered")
end
end
describe("construction", function()
it("fails if no definition is given", function()
local Test, err = Schema.new()
assert.falsy(Test)
assert.string(err)
end)
it("fails if schema fields are not defined", function()
local Test, err = Schema.new({ fields = nil })
assert.falsy(Test)
assert.string(err)
end)
it("fails on invalid foreign reference", function()
local Test, err = Schema.new({
fields = {
{ f = { type = "foreign", reference = "invalid_reference" } },
{ b = { type = "number" }, },
{ c = { type = "number" }, },
}
})
assert.falsy(Test)
assert.match("invalid_reference", err)
end)
end)
describe("validate", function()
it("orders validators", function()
local validators_len = 0
local validators_order_len = 0
for _ in pairs(Schema.validators) do
validators_len = validators_len + 1
end
for _ in pairs(Schema.validators_order) do
validators_order_len = validators_order_len + 1
end
assert.equal(validators_len, validators_order_len)
end)
it("fails if given no input", function()
local Test = Schema.new({ fields = {} })
assert.has_error(function()
Test:validate(nil)
end)
end)
it("fails if given a bad field type", function()
local Test = Schema.new({
fields = {
{ foo = { type = "typo" }, },
}
})
assert.falsy(Test:validate({ foo = "foo" }))
end)
it("validates a range with 'between'", function()
local Test = Schema.new({
fields = {
{ a_number = { type = "number", between = { 10, 20 } } }
}
})
assert.truthy(Test:validate({ a_number = 15 }))
assert.truthy(Test:validate({ a_number = 10 }))
assert.truthy(Test:validate({ a_number = 20 }))
assert.falsy(Test:validate({ a_number = 9 }))
assert.falsy(Test:validate({ a_number = 21 }))
assert.falsy(Test:validate({ a_number = "wat" }))
end)
it("forces a value with 'eq'", function()
local Test = Schema.new({
fields = {
{ a_number = { type = "number", eq = 9 } }
}
})
assert.truthy(Test:validate({ a_number = 9 }))
assert.falsy(Test:validate({ a_number = 8 }))
assert.falsy(Test:validate({ a_number = "wat" }))
end)
it("'eq' accepts false", function()
local Test = Schema.new({
fields = {
{ a_boolean = { type = "boolean", eq = false } }
}
})
assert.truthy(Test:validate({ a_boolean = false }))
assert.falsy(Test:validate({ a_boolean = true }))
assert.falsy(Test:validate({ a_boolean = "false" }))
end)
it("'eq' accepts null", function()
local Test = Schema.new({
fields = {
{ a_boolean = { type = "boolean", eq = ngx.null } }
}
})
assert.truthy(Test:validate({ a_boolean = ngx.null }))
-- null means unset, so not passing a value matches it
assert.truthy(Test:validate({ a_boolean = nil }))
assert.falsy(Test:validate({ a_boolean = "null" }))
end)
it("'eq' returns custom error message for null value", function()
local Test = Schema.new({
fields = {
{ a_null_array = {
type = "array",
elements = { type = "string" },
eq = ngx.null,
err = "cannot set value for this field",
}}
}
})
assert.truthy(Test:validate({ a_null_array = ngx.null }))
local ok, err = Test:validate({ a_null_array = { "foo" }})
assert.falsy(ok)
assert.same("cannot set value for this field", err.a_null_array)
end)
it("'eq' returns default error message if no custom message is given", function()
local Test = Schema.new({
fields = {
{ a_null_array = {
type = "array",
elements = { type = "string" },
eq = ngx.null,
}}
}
})
assert.truthy(Test:validate({ a_null_array = ngx.null }))
local ok, err = Test:validate({ a_null_array = { "foo" }})
assert.falsy(ok)
assert.same("value must be null", err.a_null_array)
end)
it("'eq' returns custom error message for non-null values", function()
local Test = Schema.new({
fields = {
{ a_field = {
type = "string",
eq = "foo",
err = "can only set this field to 'foo'",
}}
}
})
assert.falsy(Test:validate({ a_field = ngx.null }))
local ok, err = Test:validate({ a_field = "bar" })
assert.falsy(ok)
assert.same("can only set this field to 'foo'", err.a_field)
end)
it("'ne' returns custom error message for null value", function()
local Test = Schema.new({
fields = {
{ a_null_array = {
type = "array",
elements = { type = "string" },
ne = ngx.null,
err = "cannot set this field to null",
}}
}
})
assert.truthy(Test:validate({ a_null_array = { "foo" }}))
local ok, err = assert.falsy(Test:validate({ a_null_array = ngx.null }))
assert.falsy(ok)
assert.same("cannot set this field to null", err.a_null_array)
end)
it("'ne' returns custom error message for non-null values", function()
local Test = Schema.new({
fields = {
{ a_field = {
type = "string",
ne = "foo",
err = "cannot set this field to 'foo'",
}}
}
})
assert.truthy(Test:validate({ a_field = ngx.null }))
local ok, err = Test:validate({ a_field = "foo" })
assert.falsy(ok)
assert.same("cannot set this field to 'foo'", err.a_field)
end)
it("'ne' returns default error message if no custom message is given", function()
local Test = Schema.new({
fields = {
{ a_field = {
type = "string",
ne = "foo",
}}
}
})
assert.truthy(Test:validate({ a_field = ngx.null }))
local ok, err = Test:validate({ a_field = "foo" })
assert.falsy(ok)
assert.same("value must not be foo", err.a_field)
end)
it("'eq' returns default error message if no custom message is given", function()
local Test = Schema.new({
fields = {
{ a_null_array = {
type = "array",
elements = { type = "string" },
eq = ngx.null,
}}
}
})
assert.truthy(Test:validate({ a_null_array = ngx.null }))
local ok, err = Test:validate({ a_null_array = { "foo" }})
assert.falsy(ok)
assert.same("value must be null", err.a_null_array)
end)
it("forces a value with 'gt'", function()
local Test = Schema.new({
fields = {
{ a_number = { type = "number", gt = 5 } }
}
})
assert.truthy(Test:validate({ a_number = 6 }))
assert.falsy(Test:validate({ a_number = 5 }))
assert.falsy(Test:validate({ a_number = 4 }))
assert.falsy(Test:validate({ a_number = "wat" }))
end)
it("validates arrays with 'contains'", function()
local Test = Schema.new({
fields = {
{ pirate = { type = "array",
elements = { type = "string" },
contains = "arrr",
},
}
}
})
assert.truthy(Test:validate({ pirate = { "aye", "arrr", "treasure" } }))
assert.falsy(Test:validate({ pirate = { "let's do our taxes", "please" } }))
assert.falsy(Test:validate({ pirate = {} }))
end)
it("makes sure all types run validators", function()
local num = { type = "number" }
local tests = {
{ { type = "array", elements = num, len_eq = 2 },
{ 10, 20, 30 } },
{ { type = "set", elements = num, len_eq = 2 },
{ 10, 20, 30 } },
{ { type = "string", len_eq = 2 },
"foo" },
{ { type = "number", between = { 1, 3 } },
4 },
{ { type = "integer", between = { 1, 3 } },
4 },
{ { type = "map" }, -- no map-specific validators
"fail" },
{ { type = "record" }, -- no record-specific validators
"fail" },
{ { type = "boolean" }, -- no boolean-specific validators
"fail" },
{ { type = "function" },
"fail" },
}
local covered_check = {}
for i, test in pairs(tests) do
table.insert(covered_check, { ["a"..tostring(i)] = test[1] })
local Test = Schema.new({
fields = {
{ x = test[1] }
}
})
local ret, errs = Test:validate({ x = test[2] })
local case_msg = "Error case: "..test[1].type
assert.falsy(ret, case_msg)
assert.truthy(errs["x"], case_msg)
end
check_all_types_covered(covered_check)
end)
it("validates a pattern with 'match'", function()
local Test = Schema.new({
fields = {
{ f = { type = "string", match = "^%u+$" } }
}
})
assert.truthy(Test:validate({ f = "HELLO" }))
assert.truthy(Test:validate({ f = "O" }))
assert.falsy(Test:validate({ f = "" }))
assert.falsy(Test:validate({ f = 1 }))
end)
it("validates an anti-pattern with 'not_match'", function()
local Test = Schema.new({
fields = {
{ f = { type = "string", not_match = "^%u+$" } }
}
})
assert.truthy(Test:validate({ f = "hello" }))
assert.truthy(Test:validate({ f = "o" }))
assert.falsy(Test:validate({ f = "HELLO" }))
assert.falsy(Test:validate({ f = 1 }))
end)
it("validates one pattern among many with 'match_any'", function()
local Test = Schema.new({
fields = {
{ f = {
type = "string",
match_any = {
patterns = { "^hello", "world$" },
}
}
}
}
})
assert.truthy(Test:validate({ f = "hello Earth" }))
assert.truthy(Test:validate({ f = "goodbye world" }))
assert.falsy(Test:validate({ f = "hi universe" }))
assert.falsy(Test:validate({ f = 1 }))
end)
it("'match_any' produces custom messages", function()
local Test2 = Schema.new({
fields = {
{ f = {
type = "string",
match_any = {
patterns = { "^hello", "world$" },
err = "custom message",
}
}
}
}
})
local ok, err = Test2:validate({ f = "hi universe" })
assert.falsy(ok)
assert.same({ f = "custom message" }, err)
end)
it("validates all patterns in 'match_all'", function()
local Test = Schema.new({
fields = {
{ f = {
type = "string",
match_all = {
{ pattern = "^hello" },
{ pattern = "world$" },
}
}
}
}
})
assert.truthy(Test:validate({ f = "helloworld" }))
assert.truthy(Test:validate({ f = "hello crazy world" }))
assert.falsy(Test:validate({ f = "hello universe" }))
assert.falsy(Test:validate({ f = "goodbye world" }))
assert.falsy(Test:validate({ f = "hi universe" }))
assert.falsy(Test:validate({ f = 1 }))
end)
it("'match_all' produces custom messages", function()
local Test2 = Schema.new({
fields = {
{ f = {
type = "string",
match_all = {
{ pattern = "^hello", err = "error 1" },
{ pattern = "world$", err = "error 2" },
}
}
}
}
})
local ok, err = Test2:validate({ f = "hi universe" })
assert.falsy(ok)
assert.same({ f = "error 1" }, err)
ok, err = Test2:validate({ f = "hello universe" })
assert.falsy(ok)
assert.same({ f = "error 2" }, err)
end)
it("validates all anti-patterns in 'match_none'", function()
local Test = Schema.new({
fields = {
{ f = {
type = "string",
match_none = {
{ pattern = "^hello" },
{ pattern = "world$" },
}
}
}
}
})
assert.falsy(Test:validate({ f = "helloworld" }))
assert.falsy(Test:validate({ f = "hello crazy world" }))
assert.falsy(Test:validate({ f = "hello universe" }))
assert.falsy(Test:validate({ f = "goodbye world" }))
assert.truthy(Test:validate({ f = "hi universe" }))
end)
it("'match_none' produces custom messages", function()
local Test2 = Schema.new({
fields = {
{ f = {
type = "string",
match_none = {
{ pattern = "^hello", err = "error 1" },
{ pattern = "world$", err = "error 2" },
}
}
}
}
})
local ok, err = Test2:validate({ f = "hello universe" })
assert.falsy(ok)
assert.same({ f = "error 1" }, err)
ok, err = Test2:validate({ f = "goodbye world" })
assert.falsy(ok)
assert.same({ f = "error 2" }, err)
end)
it("validates an array length with 'len_eq'", function()
local Test = Schema.new({
fields = {
{
arr = {
type = "array",
elements = { type = "number" },
len_eq = 3
},
},
}
})
assert.truthy(Test:validate({ arr = { 1, 2, 3 }}))
assert.falsy(Test:validate({ arr = { 1 }}))
assert.falsy(Test:validate({ arr = { 1, 2, 3, 4 }}))
end)
it("validates an array and a set is sequentical", function()
local Test = Schema.new({
fields = {
{ set = { type = "set", elements = { type = "number" } } },
{ arr = { type = "array", elements = { type = "number" } } },
}
})
local tests = {
[{}] = true,
[{ 1 }] = true,
[{ nil, 1 }] = false,
[{ 1, 2, 3 }] = true,
[{ 1, 2, 3, nil }] = true,
[{ 1, 2, 3, nil, 4, nil }] = false
}
for t, result in pairs(tests) do
local fields = Test:process_auto_fields({
arr = t,
set = t,
})
if result then
assert.truthy(Test:validate(fields))
else
assert.falsy(Test:validate(fields))
end
end
end)
it("validates a set length with 'len_eq'", function()
local Test = Schema.new({
fields = {
{
set = {
type = "set",
elements = { type = "number" },
len_eq = 3
},
}
}
})
local function check(set)
set = Test:process_auto_fields(set)
return Test:validate(set)
end
assert.truthy(check({ set = { 4, 5, 6 }}))
assert.truthy(check({ set = { 4, 5, 6, 4 }}))
assert.falsy(check({ set = { 4, 4, 4 }}))
assert.falsy(check({ set = { 4, 5 }}))
end)
it("validates a string length with 'len_min'", function()
local Test = Schema.new({
fields = {
{ s = { type = "string", len_min = 1 }, },
}
})
assert.truthy(Test:validate({ s = "A" }))
assert.truthy(Test:validate({ s = "AAAAA" }))
assert.falsy(Test:validate({ s = "" }))
end)
it("strings cannot be empty unless said otherwise", function()
local Test = Schema.new({
fields = {
{ a = { type = "string" }, },
{ b = { type = "string", len_min = 0 }, },
}
})
assert.truthy(Test:validate({ a = "AA", b = "AA" }))
assert.truthy(Test:validate({ a = "A", b = "A" }))
assert.truthy(Test:validate({ a = "A", b = "" }))
local ok, errs = Test:validate({ a = "", b = "" })
assert.falsy(ok)
assert.string(errs["a"])
assert.falsy(errs["b"])
end)
it("validates a string length with 'len_max'", function()
local Test = Schema.new({
fields = {
{ s = { type = "string", len_min = 1 }, },
}
})
assert.truthy(Test:validate({ s = "A" }))
assert.truthy(Test:validate({ s = "AAAAA" }))
assert.falsy(Test:validate({ s = "" }))
end)
it("validates a timestamp with 'timestamp'", function()
local Test = Schema.new({
fields = {
{ a_number = { type = "number", timestamp = true } }
}
})
for _, n in ipairs({ 1, 1234567890, 9876543210 }) do
assert.truthy(Test:validate({ a_number = n }))
end
for _, n in ipairs({ -1, 0, "wat" }) do
assert.falsy(Test:validate({ a_number = n }))
end
end)
it("validates the shape of UUIDs with 'uuid'", function()
local Test = Schema.new({
fields = {
{ f = { type = "string", uuid = true } }
}
})
local tests = {
-- correct
{ "truthy", "cbb297c0-a956-486d-ad1d-f9b42df9465a" },
-- invalid variant, but accepts
{ "truthy", "cbb297c0-a956-486d-dd1d-f9b42df9465a" },
-- "null" UUID
{ "truthy", "00000000-0000-0000-0000-000000000000" },
-- incorrect characters
{ "falsy", "cbb297c0-a956-486d-ad1d-f9bZZZZZZZZZ" },
-- no dashes
{ "falsy", "cbb297c0a956486dad1df9b42df9465a" },
}
for _, test in ipairs(tests) do
assert[test[1]](Test:validate({ f = test[2] }))
end
end)
it("validates mutually exclusive set values", function()
local Test = Schema.new({
fields = {
{ f = {
type = "array",
elements = { type = "string", one_of = {"v1", "v2", "v3", "v4"} },
mutually_exclusive_subsets = { {"v1", "v3"}, {"v2", "v4"} },
}}
}
})
local tests = {
-- valid
{"truthy", {}},
{"truthy", {"v1"}},
{"truthy", {"v2"}},
{"truthy", {"v1", "v3"}},
{"truthy", {"v2", "v4"}},
-- invalid
{"falsy", {"v1", "v2"}},
{"falsy", {"v1", "v4"}},
{"falsy", {"v3", "v2"}},
{"falsy", {"v3", "v4"}},
}
for _, test in ipairs(tests) do
assert[test[1]](Test:validate({ f = test[2] }))
end
end)
it("ensures an array is a table", function()
local Test = Schema.new({
fields = {
{ f = { type = "array", elements = { type = "string" } } }
}
})
assert.truthy(Test:validate({ f = {} }))
assert.falsy(Test:validate({ f = "foo" }))
end)
it("validates array elements", function()
local Test = Schema.new({
fields = {
{ f = { type = "array", elements = { type = "number" } } }
}
})
assert.truthy(Test:validate({ f = {} }))
assert.truthy(Test:validate({ f = {1} }))
assert.truthy(Test:validate({ f = {1, -1} }))
assert.falsy(Test:validate({ f = {"hello"} }))
assert.falsy(Test:validate({ f = {1, 2, "foo"} }))
end)
it("validates rules in array elements", function()
local Test = Schema.new({
fields = {
{
f = {
type = "array",
elements = {
type = "string",
one_of = { "foo", "bar", "baz" },
not_one_of = { "forbidden", "also_forbidden" },
}
}
}
}
})
assert.truthy(Test:validate({ f = {} }))
assert.truthy(Test:validate({ f = {"foo"} }))
assert.truthy(Test:validate({ f = {"baz", "foo"} }))
assert.falsy(Test:validate({ f = {"hello"} }))
assert.falsy(Test:validate({ f = {"foo", "hello", "foo"} }))
assert.falsy(Test:validate({ f = {"baz", "foo", "forbidden"} }))
assert.falsy(Test:validate({ f = {"baz", "foo", "also_forbidden"} }))
end)
it("ensures a set is a table", function()
local Test = Schema.new({
fields = {
{ f = { type = "set", elements = { type = "string" } } }
}
})
assert.truthy(Test:validate({ f = {} }))
assert.falsy(Test:validate({ f = "foo" }))
end)
it("validates set elements", function()
local Test = Schema.new({
fields = {
{ f = { type = "set", elements = { type = "number" } } }
}
})
assert.truthy(Test:validate({ f = {} }))
assert.truthy(Test:validate({ f = {1} }))
assert.truthy(Test:validate({ f = {1, -1} }))
assert.falsy(Test:validate({ f = {"hello"} }))
assert.falsy(Test:validate({ f = {1, 2, "foo"} }))
end)
it("validates set elements", function()
local Test = Schema.new({
fields = {
{ f = { type = "set", elements = { type = "number" } } }
}
})
assert.truthy(Test:validate({ f = {} }))
assert.truthy(Test:validate({ f = {1} }))
assert.truthy(Test:validate({ f = {1, -1} }))
assert.falsy(Test:validate({ f = {"hello"} }))
assert.falsy(Test:validate({ f = {1, 2, "foo"} }))
end)
it("ensures a map is a table", function()
local Test = Schema.new({
fields = {
{
f = {
type = "map",
keys = { type = "string" },
values = { type = "string" },
}
}
}
})
assert.truthy(Test:validate({ f = {} }))
assert.falsy(Test:validate({ f = "foo" }))
end)
it("accepts a map with `keys` and `values`", function()
local Test = Schema.new({
fields = {
{
f = {
type = "map",
keys = { type = "string" },
values = { type = "string" },
}
}
}
})
assert.truthy(Test:validate({ f = {} }))
end)
it("validates map elements", function()
local Test = Schema.new({
fields = {
{
f = {
type = "map",
keys = { type = "string" },
values = { type = "number" },
}
}
}
})
assert.truthy(Test:validate({ f = { foo = 2 } }))
assert.falsy(Test:validate({ f = { [2] = 2 } }))
assert.falsy(Test:validate({ f = { [2] = "foo" } }))
assert.falsy(Test:validate({ f = { foo = "foo" } }))
assert.truthy(Test:validate({ f = { bar = 3, foo = 2 } }))
assert.falsy(Test:validate({ f = { bar = 3, [2] = 2 } }))
assert.falsy(Test:validate({ f = { bar = 3, [2] = "foo" } }))
assert.falsy(Test:validate({ f = { bar = 3, foo = "foo" } }))
end)
it("ensures a record is a table", function()
local Test = Schema.new({
fields = {
{
f = {
type = "record",
fields = { r = { type = "string" } },
}
}
}
})
assert.truthy(Test:validate({ f = {} }))
assert.falsy(Test:validate({ f = "foo" }))
end)
it("accepts a record with empty `fields`", function()
local Test = Schema.new({
fields = {
{
f = {
type = "record",
fields = {},
}
}
}
})
assert.truthy(Test:validate({ f = {} }))
end)
it("validates record elements", function()
local Test = Schema.new({
fields = {
{
f = {
type = "record",
fields = {
{ a = { type = "string" }, },
{ b = { type = "number" }, },
},
}
}
}
})
assert.truthy(Test:validate({ f = { a = "foo" } }))
assert.truthy(Test:validate({ f = { b = 42 } }))
assert.truthy(Test:validate({ f = { a = "foo", b = 42 } }))
assert.falsy(Test:validate({ f = { a = 2 } }))
assert.falsy(Test:validate({ f = { b = "foo" } }))
assert.falsy(Test:validate({ f = { a = 2, b = "foo" } }))
end)
it("validates nested records", function()
local Test = Schema.new({
fields = {
{ f = {
type = "record",
fields = {
{ r = {
type = "record",
fields = {
{ a = { type = "string" } },
{ b = { type = "number" } } }}}}}}}})
assert.truthy(Test:validate({ f = { r = { a = "foo" }}}))
assert.truthy(Test:validate({ f = { r = { b = 42 }}}))
assert.truthy(Test:validate({ f = { r = { a = "foo", b = 42 }}}))
assert.falsy(Test:validate({ f = { r = { a = 2 }}}))
assert.falsy(Test:validate({ f = { r = { b = "foo" }}}))
assert.falsy(Test:validate({ f = { r = { a = 2, b = "foo" }}}))
end)
it("validates an integer", function()
local Test = Schema.new({
fields = {
{ f = { type = "integer" } }
}
})
assert.truthy(Test:validate({ f = 123 }))
assert.truthy(Test:validate({ f = 0 }))
assert.truthy(Test:validate({ f = -123 }))
assert.falsy(Test:validate({ f = 0.5 }))
assert.falsy(Test:validate({ f = -0.5 }))
assert.falsy(Test:validate({ f = 1/0 }))
assert.falsy(Test:validate({ f = -1/0 }))
assert.falsy(Test:validate({ f = math.huge }))
assert.falsy(Test:validate({ f = "123" }))
assert.falsy(Test:validate({ f = "foo" }))
end)
it("validates a number", function()
local Test = Schema.new({
fields = {
{ f = { type = "number" } }
}
})
assert.truthy(Test:validate({ f = 123 }))
assert.truthy(Test:validate({ f = 0 }))
assert.truthy(Test:validate({ f = -123 }))
assert.truthy(Test:validate({ f = 0.5 }))
assert.truthy(Test:validate({ f = -0.5 }))
assert.truthy(Test:validate({ f = 1/0 }))
assert.truthy(Test:validate({ f = -1/0 }))
assert.truthy(Test:validate({ f = math.huge }))
assert.falsy(Test:validate({ f = "123" }))
assert.falsy(Test:validate({ f = "foo" }))
end)
it("validates a boolean", function()
local Test = Schema.new({
fields = {
{ f = { type = "boolean" } }
}
})
assert.truthy(Test:validate({ f = true }))
assert.truthy(Test:validate({ f = false }))
assert.falsy(Test:validate({ f = 0 }))
assert.falsy(Test:validate({ f = 1 }))
assert.falsy(Test:validate({ f = "true" }))
assert.falsy(Test:validate({ f = "false" }))
assert.falsy(Test:validate({ f = "foo" }))
end)
it("fails on unknown fields", function()
local Test = Schema.new({
fields = {
{ f = { type = "number", required = true } }
}
})
assert.falsy(Test:validate({ f = 1, k = "wat" }))
end)
it("validates on unknown fields with value of null in data plane", function()
local Test = Schema.new({
fields = {
{ f = { type = "number", required = true } }
}
})
assert.falsy(Test:validate({ f = 1, k = "wat" }))
assert.falsy(Test:validate({ f = 1, k = ngx.null }))
_G.kong = {
configuration = {
role = "data_plane",
},
}
local ok = Test:validate({ f = 1, k = ngx.null })
_G.kong = nil
assert.truthy(ok)
end)
local function run_custom_check_producing_error(error)
local Test = Schema.new({
fields = {
{ password = { type = "string" }, },
{ confirm_password = { type = "string" }, },
}
})
local check = function(fields)
if fields.password ~= fields.confirm_password then
return nil, error
end
return true
end
Test.check = check
local data, errs = Test:validate({
password = "123456",
confirm_password = "123456",
})
Test.check = nil
assert.is_nil(errs)
assert.truthy(data)
Test.check = check
local entity_errs
data, errs, entity_errs = Test:validate({
password = "123456",
confirm_password = "1234",
})
Test.check = nil
assert.falsy(data)
return errs, entity_errs
end
it("runs a custom check with string error", function()
local errors = run_custom_check_producing_error(
"passwords must match"
)
assert.same({ ["@entity"] = { "passwords must match" } }, errors)
end)
it("runs a custom check with table keyed error", function()
local errors = run_custom_check_producing_error(
{ password = "passwords must match" }
)
assert.same({ password = "passwords must match" }, errors)
end)
it("runs a custom check with table numbered error", function()
local errors = run_custom_check_producing_error(
{ "passwords must match", "a second error" }
)
assert.same({
["@entity"] = {"passwords must match", "a second error" }
}, errors)
end)
it("runs a custom check with no message", function()
local errors = run_custom_check_producing_error(nil)
-- still produces a default message
assert.same({
["@entity"] = { "entity check failed" }
}, errors)
end)
it("merges field and custom checks", function()
local Test = Schema.new({
fields = {
{ fail1 = { type = "string", match = "aaa" } },
{ fail2 = { type = "string", match = "bbb" } },
},
check = function()
return nil, {
[1] = "a generic check error",
[2] = "another generic check error",
fail2 = "my own field error",
}
end,
})
local data, errs = Test:validate({
fail1 = "ccc",
fail2 = "ddd",
})
assert.falsy(data)
assert.same("a generic check error", errs["@entity"][1])
assert.same("another generic check error", errs["@entity"][2])
assert.string(errs["fail1"])
assert.same("my own field error", errs["fail2"])
end)
it("can make a string from an error", function()
local Test = Schema.new({
fields = {
{ foo = { type = "typo" }, },
}
})
local ret, errs = Test:validate({ foo = "foo" })
assert.falsy(ret)
assert.string(errs["foo"])
local errmsg = Test:errors_to_string(errs)
assert.string(errmsg)
-- Produced string mentions the relevant error
assert.match("foo", errmsg)
end)
it("produces no string when given no errors", function()
local Test = Schema.new({
fields = {}
})
local errmsg = Test:errors_to_string({})
assert.falsy(errmsg)
errmsg = Test:errors_to_string(nil)
assert.falsy(errmsg)
errmsg = Test:errors_to_string("not a table")
assert.falsy(errmsg)
end)
describe("subschemas", function()
it("validates loading a subschema", function()
local Test = Schema.new({
name = "test",
subschema_key = "name",
fields = {
{ name = { type = "string", required = true, } },
{ config = { type = "record", abstract = true, } },
}
})
assert(Test:new_subschema("my_subschema", {
fields = {
{ config = {
type = "record",
fields = {
{ foo = { type = "string" } },
{ bar = { type = "integer" } },
}
} }
}
}))
assert.truthy(Test:validate({
name = "my_subschema",
config = {
foo = "hello",
bar = 123,
}
}))
end)
it("fails if subschema doesn't exist", function()
local Test = Schema.new({
name = "test",
subschema_key = "name",
fields = {
{ name = { type = "string", required = true, } },
}
})
local ok, errors = Test:validate({
name = "my_invalid_subschema",
})
assert.falsy(ok)
assert.same({
["name"] = "unknown type: my_invalid_subschema",
}, errors)
end)
it("fails if subschema doesn't exist", function()
local Test = Schema.new({
name = "test",
subschema_key = "protocols",
fields = {
{ protocols = { type = "array", elements = { type = "string", one_of = { "p1", "p2" }}, } },
}
})
local ok, errors = Test:validate({
protocols = { "p1" },
})
assert.falsy(ok)
assert.same({
["protocols"] = "unknown type: p1",
}, errors)
local ok, errors = Test:validate({
protocols = { "p2" },
})
assert.falsy(ok)
assert.same({
["protocols"] = "unknown type: p2",
}, errors)
end)
it("ignores missing non-required abstract fields", function()
local Test = Schema.new({
name = "test",
subschema_key = "name",
fields = {
{ name = { type = "string", required = true, } },
{ config = { type = "record", abstract = true, } },
{ bla = { type = "integer", abstract = true, } },
}
})
assert(Test:new_subschema("my_subschema", {
fields = {
{ config = {
type = "record",
fields = {
{ foo = { type = "string" } },
{ bar = { type = "integer" } },
}
} }
}
}))
assert.truthy(Test:validate({
name = "my_subschema",
config = {
foo = "hello",
bar = 123,
}
}))
end)
it("cannot introduce new top-level fields", function()
local Test = Schema.new({
name = "test",
subschema_key = "name",
fields = {
{ name = { type = "string", required = true, } },
{ config = {
type = "record",
fields = {
{ foo = { type = "integer" } },
}
} },
}
})
local ok, err = Test:new_subschema("my_subschema", {
fields = {
{ config = {
type = "record",
fields = {
{ foo = { type = "integer" } },
{ bar = { type = "integer" } },
}
} },
{ new_field = { type = "string", required = true, } },
}
})
assert.falsy(ok)
assert.matches("new_field: cannot create a new field", err, 1, true)
end)
it("fails when trying to use an abstract field (incomplete subschema)", function()
local Test = Schema.new({
name = "test",
subschema_key = "name",
fields = {
{ name = { type = "string", required = true, } },
{ config = { type = "record", abstract = true, } },
{ bla = { type = "integer", abstract = true, } },
}
})
assert(Test:new_subschema("my_subschema", {
fields = {
{ config = {
type = "record",
fields = {
{ foo = { type = "string" } },
{ bar = { type = "integer" } },
}
} }
}
}))
local ok, errors = Test:validate({
name = "my_subschema",
config = {
foo = "hello",
bar = 123,
},
bla = 456,
})
assert.falsy(ok)
assert.same({
bla = "error in schema definition: abstract field was not specialized",
}, errors)
end)
it("validates using both schema and subschema", function()
local Test = Schema.new({
name = "test",
subschema_key = "name",
fields = {
{ name = { type = "string", required = true, } },
{ bla = { type = "integer", } },
{ config = { type = "record", abstract = true, } },
}
})
assert(Test:new_subschema("my_subschema", {
fields = {
{ config = {
type = "record",
fields = {
{ foo = { type = "string" } },
{ bar = { type = "integer" } },
}
} }
}
}))
local ok, errors = Test:validate({
name = "my_subschema",
bla = 4.5,
config = {
foo = 456,
bar = 123,
}
})
assert.falsy(ok)
assert.same({
bla = "expected an integer",
config = {
foo = "expected a string",
}
}, errors)
end)
it("can specialize a field of the parent schema", function()
local Test = Schema.new({
name = "test",
subschema_key = "name",
fields = {
{ name = { type = "string", required = true, } },
{ consumer = { type = "string", } },
}
})
assert(Test:new_subschema("length_5", {
fields = {
{ consumer = {
type = "string",
len_eq = 5,
} }
}
}))
assert(Test:new_subschema("no_restrictions", {
fields = {}
}))
local ok, errors = Test:validate({
name = "length_5",
consumer = "aaa",
})
assert.falsy(ok)
assert.same({
consumer = "length must be 5",
}, errors)
ok = Test:validate({
name = "length_5",
consumer = "aaaaa",
})
assert.truthy(ok)
ok = Test:validate({
name = "no_restrictions",
consumer = "aaa",
})
assert.truthy(ok)
end)
it("cannot change type when specializing a field", function()
local Test = Schema.new({
name = "test",
subschema_key = "name",
fields = {
{ name = { type = "string", required = true, } },
{ consumer = { type = "string", } },
}
})
local ok, err = Test:new_subschema("length_5", {
fields = {
{ consumer = {
type = "integer",
} }
}
})
assert.falsy(ok)
assert.matches("consumer: cannot change type in a specialized field", err, 1, true)
end)
it("a specialized field can force a value using 'eq'", function()
assert(Schema.new({
name = "mock_consumers",
primary_key = { "id" },
fields = {
{ id = { type = "string" }, },
}
}))
local Test = assert(Schema.new({
name = "test",
subschema_key = "name",
fields = {
{ name = { type = "string", required = true, } },
{ consumer = { type = "foreign", reference = "mock_consumers" } },
}
}))
assert(Test:new_subschema("no_consumer", {
fields = {
{ consumer = { type = "foreign", reference = "mock_consumers", eq = ngx.null } }
}
}))
assert(Test:new_subschema("no_restrictions", {
fields = {}
}))
local ok, errors = Test:validate({
name = "no_consumer",
consumer = { id = "hello" },
})
assert.falsy(ok)
assert.same({
consumer = "value must be null",
}, errors)
ok = Test:validate({
name = "no_consumer",
consumer = ngx.null,
})
assert.truthy(ok)
ok = Test:validate({
name = "no_restrictions",
consumer = { id = "hello" },
})
assert.truthy(ok)
end)
end)
describe("entity_checkers", function()
describe("conditional_at_least_one_of", function()
local Test = Schema.new({
fields = {
{ a = { type = "number" }, },
{ b = { type = "string" }, },
{ c = { type = "string" }, },
},
entity_checks = {
{ conditional_at_least_one_of = { if_field = "a",
if_match = { gt = 0 },
then_at_least_one_of = { "b", "c" }}
},
}
})
it("sanity", function()
local ok, errs = Test:validate_insert({ a = 1 })
assert.is_nil(ok)
assert.same({
"at least one of these fields must be non-empty: 'b', 'c'"
}, errs["@entity"])
local ok, errs = Test:validate_insert({ a = 1, b = "foo" })
assert.is_nil(errs)
assert.truthy(ok)
end)
it("does not run when condition is evaluated to false", function()
local ok, errs = Test:validate_insert({ a = 0 })
assert.is_nil(errs)
assert.truthy(ok)
end)
it("does not run when the 'if_field' is missing", function()
local ok, errs = Test:validate_insert({ b = "foo" })
assert.is_nil(errs)
assert.truthy(ok)
end)
it("works on updates", function()
assert.truthy(Test:validate_insert({ }))
-- Can update to whole valid record
assert.truthy(Test:validate_update({ a = 123, b = "foo" }))
-- Empty update works
assert.truthy(Test:validate_update({ }))
-- Cannot update if_field without respecifying at least one
-- of the then_at_least_one_of fields, because this checker
-- does not trigger a read-before-write (yet)
local ok, err = Test:validate_update({ a = 123 })
assert.falsy(ok)
assert.same({
["@entity"] = {
[[when updating, at least one of these fields must be non-empty: 'b', 'c']]
}
}, err)
end)
it("supports an 'else' clause", function()
local Test = Schema.new({
fields = {
{ a = { type = "number" }, },
{ b = { type = "string" }, },
{ c = { type = "string" }, },
{ d = { type = "string" }, },
},
entity_checks = {
{ conditional_at_least_one_of = { if_field = "a",
if_match = { gt = 0 },
then_at_least_one_of = { "b", "c" },
else_match = { ne = 0 },
else_then_at_least_one_of = { "c", "d" }, }
},
}
})
local ok, errs = Test:validate_insert({ a = -1 })
assert.is_nil(ok)
assert.same({
"at least one of these fields must be non-empty: 'c', 'd'"
}, errs["@entity"])
local ok, errs = Test:validate_insert({ a = -1, d = "foo" })
assert.is_nil(errs)
assert.truthy(ok)
local ok, errs = Test:validate_insert({ a = 0 })
assert.is_nil(errs)
assert.truthy(ok)
end)
it("supports a custom error message", function()
local Test = Schema.new({
fields = {
{ a = { type = "number" }, },
{ b = { type = "string" }, },
{ c = { type = "string" }, },
},
entity_checks = {
{ conditional_at_least_one_of = { if_field = "a",
if_match = { gt = 0 },
then_at_least_one_of = { "b", "c" },
then_err = "must set one of %s if 'a' is like this",
else_match = { ne = 0 },
else_then_at_least_one_of = { "c", "d" },
else_then_err = "must set one of %s if 'a' is like that" }, },
}
})
local ok, errs = Test:validate_insert({ a = 1 })
assert.falsy(ok)
assert.same({
"must set one of 'b', 'c' if 'a' is like this"
}, errs["@entity"])
local ok, errs = Test:validate_insert({ a = -1 })
assert.falsy(ok)
assert.same({
"must set one of 'c', 'd' if 'a' is like that"
}, errs["@entity"])
end)
end)
describe("conditional", function()
it("can check on false", function()
local Test = Schema.new({
fields = {
{ a = { type = "boolean" }, },
{ b = { type = "boolean" }, },
},
entity_checks = {
{ conditional = { if_field = "a",
if_match = { eq = true },
then_field = "b",
then_match = { eq = false },
then_err = "can't have a and b at the same time", }
},
}
})
assert.truthy(Test:validate_insert({ a = true, b = false }))
local ok, errs = Test:validate_insert({ a = true, b = true })
assert.falsy(ok)
assert.same({
"can't have a and b at the same time"
}, errs["@entity"])
end)
it("supports a custom error message", function()
local Test = Schema.new({
fields = {
{ a = { type = "number" }, },
{ b = { type = "string" }, },
{ c = { type = "string" }, },
},
entity_checks = {
{ conditional = { if_field = "a",
if_match = { gt = 0 },
then_field = "b",
then_match = { gt = 0 },
then_err = "must set 'b > 0' if '%s' is like this", }
},
}
})
local ok, errs = Test:validate_insert({ a = 1, b = 0 })
assert.falsy(ok)
assert.same({
"must set 'b > 0' if 'a' is like this"
}, errs["@entity"])
end)
end)
end)
end)
describe("validate_primary_key", function()
it("validates primary keys", function()
local Test = Schema.new({
fields = {
{ a = { type = "string" }, },
{ b = { type = "number" }, },
{ c = { type = "number", default = 110 }, },
}
})
Test.primary_key = { "a", "c" }
assert.truthy(Test:validate_primary_key({
a = "hello",
c = 195
}))
end)
it("fails on missing required primary keys", function()
local Test = Schema.new({
fields = {
{ a = { type = "string" }, },
{ b = { type = "number" }, },
{ c = { type = "number", required = true }, },
}
})
Test.primary_key = { "a", "c" }
local ok, errs = Test:validate_primary_key({
a = "hello",
})
assert.falsy(ok)
assert.truthy(errs["c"])
end)
it("fails on missing foreign primary keys", function()
assert(Schema.new({
name = "schema-test",
primary_key = { "id" },
fields = {
{ id = { type = "string" }, },
}
}))
local Test = assert(Schema.new({
name = "Test",
fields = {
{ f = { type = "foreign", reference = "schema-test" } },
{ b = { type = "number" }, },
{ c = { type = "number" }, },
}
}))
Test.primary_key = { "f" }
local ok, errs = Test:validate_primary_key({})
assert.falsy(ok)
assert.match("missing primary key", errs["f"])
end)
it("fails on bad foreign primary keys", function()
assert(Schema.new({
name = "schema-test",
primary_key = { "id" },
fields = {
{ id = { type = "string", required = true }, },
}
}))
local Test = assert(Schema.new({
name = "Test",
fields = {
{ f = { type = "foreign", reference = "schema-test" } },
{ b = { type = "number" }, },
{ c = { type = "number" }, },
}
}))
Test.primary_key = { "f" }
local ok, errs = Test:validate_primary_key({
f = { id = ngx.null },
})
assert.falsy(ok)
assert.match("required field missing", errs["f"].id)
end)
it("accepts a null in foreign if a null fails on bad foreign primary keys", function()
package.loaded["kong.db.schema.entities.schema-test"] = {
name = "schema-test",
primary_key = { "id" },
fields = {
{ id = { type = "string", required = true }, },
}
}
local Test = assert(Schema.new({
name = "Test",
fields = {
{ f = { type = "foreign", reference = "schema-test" } },
{ b = { type = "number" }, },
{ c = { type = "number" }, },
}
}))
Test.primary_key = { "f" }
local ok, errs = Test:validate_primary_key({
f = { id = ngx.null },
})
assert.falsy(ok)
assert.match("required field missing", errs["f"].id)
end)
it("fails given non-primary keys", function()
local Test = Schema.new({
fields = {
{ a = { type = "string" }, },
{ b = { type = "number" }, },
{ c = { type = "number", required = true }, },
}
})
Test.primary_key = { "a", "c" }
local ok, errs = Test:validate_primary_key({
a = "hello",
b = 123,
c = 9,
})
assert.falsy(ok)
assert.truthy(errs["b"])
end)
it("fails given invalid keys", function()
local Test = Schema.new({
fields = {
{ a = { type = "string" }, },
{ b = { type = "number" }, },
{ c = { type = "number", required = true }, },
}
})
Test.primary_key = { "a", "c" }
local ok, errs = Test:validate_primary_key({
a = "hello",
x = 123,
})
assert.falsy(ok)
assert.truthy(errs["x"])
end)
it("fails on missing non-required primary key", function()
local Test = Schema.new({
fields = {
a = { type = "string" },
b = { type = "number" },
c = { type = "number" },
}
})
Test.primary_key = { "a", "c" }
assert.falsy(Test:validate_primary_key({
a = "hello",
}))
end)
end)
describe("validate_insert", function()
it("demands required fields", function()
local Test = Schema.new({
fields = {
{ f = { type = "number", required = true } }
}
})
assert.truthy(Test:validate_insert({ f = 123 }))
assert.falsy(Test:validate_insert({}))
end)
end)
describe("validate_update", function()
it("does not demand required fields", function()
local Test = Schema.new({
fields = {
{ f = { type = "number", required = true } }
}
})
assert.truthy(Test:validate_update({ f = 123 }))
assert.truthy(Test:validate_update({}))
end)
it("demands interdependent fields", function()
local Test = Schema.new({
fields = {
{ a = { type = "number" } },
{ b = { type = "number" } },
{ c = { type = "number" } },
{ d = { type = "number" } },
},
entity_checks = {
{ only_one_of = { "a", "b" } },
}
})
assert.falsy(Test:validate_update({ a = 12 }))
assert.falsy(Test:validate_update({ a = ngx.null, b = ngx.null }))
assert.truthy(Test:validate_update({ a = 12, b = ngx.null }))
end)
it("test conditional checks", function()
local Test = Schema.new({
fields = {
{ policy = {
type = "string",
one_of = { "redis", "bla" },
not_one_of = { "cluster" },
}
},
{ redis_host = { type = "string" } },
{ redis_port = { type = "number" } },
},
entity_checks = {
{ conditional = { if_field = "policy",
if_match = { match = "^redis$" },
then_field = "redis_host",
then_match = { required = true } } },
{ conditional = { if_field = "policy",
if_match = { match = "^redis$" },
then_field = "redis_port",
then_match = { required = true } } },
}
})
local ok, err = Test:validate_update({ policy = "redis" })
assert.falsy(ok)
assert.truthy(err)
assert.falsy(Test:validate_update({
policy = "redis",
redis_host = ngx.null,
redis_port = ngx.null,
}))
assert.truthy(Test:validate_update({
policy = "redis",
redis_host = "example.com",
redis_port = 80
}))
assert.truthy(Test:validate_update({
policy = "bla",
}))
assert.falsy(Test:validate_update({
policy = "redis",
}))
assert.falsy(Test:validate_update({
policy = "cluster",
}))
end)
it("test mutually required checks", function()
local Test = Schema.new({
fields = {
{ a1 = { type = "string" } },
{ a2 = { type = "string" } },
{ a3 = { type = "string" } },
},
entity_checks = {
{ mutually_required = { "a2" } },
{ mutually_required = { "a1", "a3" } },
}
})
local ok, err = Test:validate_update({
a1 = "foo"
})
assert.is_falsy(ok)
assert.match("all or none of these fields must be set: 'a1', 'a3'", err["@entity"][1])
ok, err = Test:validate_update({
a2 = "foo"
})
assert.truthy(ok)
assert.falsy(err)
end)
it("test mutually exclusive checks", function()
local Test = Schema.new({
fields = {
{ a1 = { type = "string" } },
{ a2 = { type = "string" } },
{ a3 = { type = "string" } },
},
entity_checks = {
{ mutually_exclusive = { "a2" } },
{ mutually_exclusive = { "a1", "a3" } },
}
})
local ok, err = Test:validate_update({
a1 = "foo",
a3 = "foo",
})
assert.is_falsy(ok)
assert.match("only one or none of these fields must be set: 'a1', 'a3'", err["@entity"][1])
ok, err = Test:validate_update({
a2 = "foo"
})
assert.truthy(ok)
assert.falsy(err)
ok, err = Test:validate_update({
a1 = "foo",
a2 = "foo",
})
assert.truthy(ok)
assert.falsy(err)
ok, err = Test:validate_update({})
assert.truthy(ok)
assert.falsy(err)
end)
for i = 1, 2 do
it("test mutually required checks specified by transformations (" .. SchemaKind[i].name .. ")", function()
local Test = SchemaKind[i].new({
fields = {
{ name = { type = "string", required = true, } },
{ a1 = { type = "string" } },
{ a2 = { type = "string" } },
{ a3 = { type = "string" } },
},
transformations = {
{
input = { "a2" },
on_write = function() return {} end
},
{
input = { "a1", "a3" },
on_write = function() return {} end
},
}
})
local ok, err = Test:validate_update({
name = "test",
a1 = "foo"
})
assert.is_falsy(ok)
assert.match("all or none of these fields must be set: 'a1', 'a3'", err["@entity"][1])
ok, err = Test:validate_update({
a2 = "foo"
})
assert.truthy(ok)
assert.falsy(err)
ok, err = Test:validate_update({
a1 = "aaa",
a2 = "bbb",
a3 = "ccc",
a4 = "ddd",
}, {
a1 = "foo"
})
assert.is_falsy(ok)
assert.match("all or none of these fields must be set: 'a1', 'a3'", err["@entity"][1])
end)
end
for i = 1, 2 do
it("test mutually required checks specified by transformations with needs (" .. SchemaKind[i].name .. ")", function()
local Test = SchemaKind[i].new({
fields = {
{ a1 = { type = "string" } },
{ a2 = { type = "string" } },
{ a3 = { type = "string" } },
{ a4 = { type = "string" } },
},
transformations = {
{
input = { "a2" },
on_write = function() return {} end
},
{
input = { "a1", "a3" },
needs = { "a4" },
on_write = function() return {} end
},
}
})
local ok, err = Test:validate_update({
a1 = "foo"
})
assert.is_falsy(ok)
assert.match("all or none of these fields must be set: 'a1', 'a3', 'a4'", err["@entity"][1])
local ok, err = Test:validate_update(
{
a1 = "foo",
a3 = "bar",
a4 = "car",
},
{
a1 = "foo",
a3 = "bar",
}
)
assert.truthy(ok)
assert.falsy(err)
ok, err = Test:validate_update({
a2 = "foo"
})
assert.truthy(ok)
assert.falsy(err)
end)
end
for i = 1, 2 do
it("test mutually required checks specified by transformations with needs (combinations) (" .. SchemaKind[i].name .. ")", function()
-- {
-- input = I1, I2
-- needs = N1, N2
-- }
--
-- ### PATCH result
-- -----------------------------------------
-- 01. (no input) ok
-- 02. I1 I2 N1 N2 ok
-- 03. I1 I2 N1 ok, rbw N2
-- 04. I1 I2 N2 ok, rbw N1
-- 05. I1 I2 ok, rbw N1 N2
-- 06. I1 I2 fail, rbw N1, missing N2
-- 07. I1 I2 fail, rbw N2, missing N1
-- 08. I1 I2 fail, missing N1 N2
-- 09. I1 N1 N2 fail, missing I2
-- 10. I1 N1 fail, missing I2
-- 11. I1 N1 fail, missing I2, rbw N2
-- 12. I1 N1 fail, rbw I2 N2
-- 13. I1 N2 fail, missing I2
-- 14. I1 N2 fail, missing I2, rbw N1
-- 15. I1 N2 fail, rbw I2 N1
-- 16. I1 fail, missing I2
-- 17. I1 fail, missing I2, rbw N1
-- 18. I1 fail, missing I2, rbw N1 N2
-- 19. I1 fail, rbw I2 N1 N2
-- 20. I2 N1 N2 fail, missing I1
-- 21. I2 N1 fail, missing I1
-- 22. I2 N2 fail, missing I1
-- 23. I2 fail, missing I1
-- 24. N1 N2 fail, needs changes would invalidate I1 I2
-- 25. N1 fail, needs changes would invalidate I1 I2
-- 26. N2 fail, needs changes would invalidate I1 I2
-- 27. N1 N2 ok, no changes in needs, would not invalidate I1 I2
-- 28. N1 ok, no changes in needs, would not invalidate I1 I2
-- 29. N2 ok, no changes in needs, would not invalidate I1 I2
local Test = SchemaKind[i].new({
fields = {
{ i1 = { type = "string" } },
{ i2 = { type = "string" } },
{ n1 = { type = "string" } },
{ n2 = { type = "string" } },
},
transformations = {
{
input = { "i1", "i2" },
needs = { "n1", "n2" },
on_write = function() return {} end,
},
},
})
-- 01. (no input): ok
local ok, err = Test:validate_update(
{
},
{
}
)
assert.truthy(ok)
assert.falsy(err)
-- 02. I1 I2 N1 N2: ok
local ok, err = Test:validate_update(
{
},
{
i1 = "foo",
i2 = "bar",
n1 = "foo",
n2 = "bar",
}
)
assert.truthy(ok)
assert.falsy(err)
-- 03. I1 I2 N1: ok, rbw N2
local ok, err = Test:validate_update(
{
i1 = "foo",
i2 = "bar",
n1 = "foo",
n2 = "bar",
},
{
i1 = "foo",
i2 = "bar",
n1 = "foo",
}
)
assert.truthy(ok)
assert.falsy(err)
-- 04. I1 I2 N2: ok, rbw N1
local ok, err = Test:validate_update(
{
i1 = "foo",
i2 = "bar",
n1 = "foo",
n2 = "bar",
},
{
i1 = "foo",
i2 = "bar",
n2 = "bar",
}
)
assert.truthy(ok)
assert.falsy(err)
-- 05. I1 I2 ok, rbw N1 N2
local ok, err = Test:validate_update(
{
i1 = "foo",
i2 = "bar",
n1 = "foo",
n2 = "bar",
},
{
i1 = "foo",
i2 = "bar",
}
)
assert.truthy(ok)
assert.falsy(err)
-- 06. I1 I2: fail, rbw N1, missing N2
local ok, err = Test:validate_update(
{
i1 = "foo",
i2 = "bar",
n1 = "foo",
},
{
i1 = "foo",
i2 = "bar",
}
)
assert.falsy(ok)
assert.match("all or none of these fields must be set: 'i1', 'i2', 'n1', 'n2'", err["@entity"][1])
-- 07. I1 I2: fail, rbw N2, missing N1
local ok, err = Test:validate_update(
{
i1 = "foo",
i2 = "bar",
n2 = "bar",
},
{
i1 = "foo",
i2 = "bar",
}
)
assert.falsy(ok)
assert.match("all or none of these fields must be set: 'i1', 'i2', 'n1', 'n2'", err["@entity"][1])
-- 08. I1 I2: fail, missing N1 N2
local ok, err = Test:validate_update(
{
i1 = "foo",
i2 = "bar",
n2 = "bar",
},
{
i1 = "foo",
i2 = "bar",
}
)
assert.falsy(ok)
assert.match("all or none of these fields must be set: 'i1', 'i2', 'n1', 'n2'", err["@entity"][1])
-- 09. I1 N1 N2: fail, missing I2
local ok, err = Test:validate_update(
{
i1 = "foo",
n1 = "foo",
n2 = "bar",
},
{
i1 = "foo",
n1 = "foo",
n2 = "bar",
}
)
assert.falsy(ok)
assert.match("all or none of these fields must be set: 'i1', 'i2', 'n1', 'n2'", err["@entity"][1])
-- 10. I1 N1: fail, missing I2
local ok, err = Test:validate_update(
{
i1 = "foo",
n1 = "foo",
},
{
i1 = "foo",
n1 = "foo",
}
)
assert.falsy(ok)
assert.match("all or none of these fields must be set: 'i1', 'i2', 'n1', 'n2'", err["@entity"][1])
-- 11. I1 N1: fail, missing I2, rbw N2
local ok, err = Test:validate_update(
{
i1 = "foo",
n1 = "foo",
n2 = "bar",
},
{
i1 = "foo",
n1 = "foo",
}
)
assert.falsy(ok)
assert.match("all or none of these fields must be set: 'i1', 'i2', 'n1', 'n2'", err["@entity"][1])
-- 12. I1 N1: fail, rbw I2 N2
local ok, err = Test:validate_update(
{
i1 = "foo",
i2 = "bar",
n1 = "foo",
n2 = "bar",
},
{
i1 = "foo",
n1 = "foo",
}
)
assert.falsy(ok)
assert.match("all or none of these fields must be set: 'i1', 'i2'", err["@entity"][1])
-- 13. I1 N2: fail, missing I2
local ok, err = Test:validate_update(
{
i1 = "foo",
n2 = "bar",
},
{
i1 = "foo",
n2 = "bar",
}
)
assert.falsy(ok)
assert.match("all or none of these fields must be set: 'i1', 'i2', 'n1', 'n2'", err["@entity"][1])
-- 14. I1 N2: fail, missing I2, rbw N1
local ok, err = Test:validate_update(
{
i1 = "foo",
n1 = "foo",
n2 = "bar",
},
{
i1 = "foo",
n2 = "bar",
}
)
assert.falsy(ok)
assert.match("all or none of these fields must be set: 'i1', 'i2', 'n1', 'n2'", err["@entity"][1])
-- 15. I1 N2: fail, missing I2, rbw I2 N1
local ok, err = Test:validate_update(
{
i1 = "foo",
i2 = "bar",
n1 = "foo",
n2 = "bar",
},
{
i1 = "foo",
n2 = "bar",
}
)
assert.falsy(ok)
assert.match("all or none of these fields must be set: 'i1', 'i2'", err["@entity"][1])
-- 16. I1: fail, missing I2
local ok, err = Test:validate_update(
{
i1 = "foo",
},
{
i1 = "foo",
}
)
assert.falsy(ok)
assert.match("all or none of these fields must be set: 'i1', 'i2', 'n1', 'n2'", err["@entity"][1])
-- 17. I1: fail, missing I2, rbw N1
local ok, err = Test:validate_update(
{
i1 = "foo",
n1 = "foo",
},
{
i1 = "foo",
}
)
assert.falsy(ok)
assert.match("all or none of these fields must be set: 'i1', 'i2', 'n1', 'n2'", err["@entity"][1])
-- 18. I1: fail, missing I2, rbw N1 N2
local ok, err = Test:validate_update(
{
i1 = "foo",
n1 = "foo",
n2 = "bar",
},
{
i1 = "foo",
}
)
assert.falsy(ok)
assert.match("all or none of these fields must be set: 'i1', 'i2', 'n1', 'n2'", err["@entity"][1])
-- 19. I1: fail, rbw I2 N1 N2
local ok, err = Test:validate_update(
{
i1 = "foo",
i2 = "bar",
n1 = "foo",
n2 = "bar",
},
{
i1 = "foo",
}
)
assert.falsy(ok)
assert.match("all or none of these fields must be set: 'i1', 'i2'", err["@entity"][1])
-- 20. I2 N1 N2: fail, missing I1
local ok, err = Test:validate_update(
{
i2 = "bar",
n1 = "foo",
n2 = "bar",
},
{
i2 = "bar",
n1 = "foo",
n2 = "bar",
}
)
assert.falsy(ok)
assert.match("all or none of these fields must be set: 'i1', 'i2', 'n1', 'n2'", err["@entity"][1])
-- 21. I2 N1: fail, missing I1
local ok, err = Test:validate_update(
{
i2 = "bar",
n1 = "foo",
},
{
i2 = "bar",
n1 = "foo",
}
)
assert.falsy(ok)
assert.match("all or none of these fields must be set: 'i1', 'i2', 'n1', 'n2'", err["@entity"][1])
-- 22. I2 N2: fail, missing I1
local ok, err = Test:validate_update(
{
i2 = "bar",
n2 = "bar",
},
{
i2 = "bar",
n2 = "bar",
}
)
assert.falsy(ok)
assert.match("all or none of these fields must be set: 'i1', 'i2', 'n1', 'n2'", err["@entity"][1])
-- 23. I2: fail, missing I1
local ok, err = Test:validate_update(
{
i2 = "bar",
},
{
i2 = "bar",
}
)
assert.falsy(ok)
assert.match("all or none of these fields must be set: 'i1', 'i2', 'n1', 'n2'", err["@entity"][1])
-- 24. N1 N2: fail, needs changes would invalidate I1 I2
local ok, err = Test:validate_update(
{
n1 = "foo",
n2 = "bar",
},
{
n1 = "foo",
n2 = "bar",
},
{
n1 = "old-foo",
n2 = "old-bar",
}
)
assert.falsy(ok)
assert.match("all or none of these fields must be set: 'i1', 'i2', 'n1', 'n2'", err["@entity"][1])
-- 25. N1: fail, needs changes would invalidate I1 I2
local ok, err = Test:validate_update(
{
n1 = "foo",
},
{
n1 = "foo",
},
{
n1 = "old-foo",
}
)
assert.falsy(ok)
assert.match("all or none of these fields must be set: 'i1', 'i2', 'n1', 'n2'", err["@entity"][1])
-- 26. N2: fail, needs changes would invalidate I1 I2
local ok, err = Test:validate_update(
{
n2 = "bar",
},
{
n2 = "bar",
},
{
n2 = "old-bar",
}
)
assert.falsy(ok)
assert.match("all or none of these fields must be set: 'i1', 'i2', 'n1', 'n2'", err["@entity"][1])
-- 27. N1 N2: ok, no changes in needs, would not invalidate I1 I2
local ok, err = Test:validate_update(
{
n1 = "foo",
n2 = "bar",
},
{
n1 = "foo",
n2 = "bar",
},
{
n1 = "foo",
n2 = "bar",
}
)
assert.truthy(ok)
assert.falsy(err)
-- 28. N1: fail, ok, no changes in needs, would not invalidate I1 I2
local ok, err = Test:validate_update(
{
n1 = "foo",
},
{
n1 = "foo",
},
{
n1 = "foo",
}
)
assert.truthy(ok)
assert.falsy(err)
-- 29. N2: ok, no changes in needs, would not invalidate I1 I2
local ok, err = Test:validate_update(
{
n2 = "bar",
},
{
n2 = "bar",
},
{
n2 = "bar",
}
)
assert.truthy(ok)
assert.falsy(err)
end)
end
it("test mutually exclusive checks", function()
local Test = Schema.new({
fields = {
{ a1 = { type = "string" } },
{ a2 = { type = "string" } },
{ a3 = { type = "string" } },
{ a4 = { type = "string" } },
{ a5 = { type = "string" } },
},
entity_checks = {
{ mutually_exclusive_sets = { set1 = {"a3"}, set2 = {"a5"}} },
{ mutually_exclusive_sets = { set1 = {"a1", "a2"}, set2 = {"a4", "a5"}} },
}
})
local ok, err = Test:validate_update({
a1 = "foo",
a5 = "bla",
})
assert.is_falsy(ok)
assert.same("these sets are mutually exclusive: ('a1'), ('a5')", err["@entity"][1])
ok, err = Test:validate_update({
a1 = "foo",
})
assert.truthy(ok)
assert.falsy(err)
ok, err = Test:validate_update({
a3 = "foo",
a5 = "bla",
})
assert.is_falsy(ok)
assert.same("these sets are mutually exclusive: ('a3'), ('a5')", err["@entity"][1])
ok, err = Test:validate_update({
a5 = "foo",
})
assert.truthy(ok)
assert.falsy(err)
end)
it("test conditional checks on set elements", function()
local Test = Schema.new({
fields = {
{ redis_host = { type = "string" } },
{ a_set = { type = "set", elements = { type = "string", one_of = { "foo", "bar" }, not_one_of = { "forbidden", "also_forbidden" } } } },
},
entity_checks = {
{ conditional = { if_field = "a_set",
if_match = { elements = { type = "string", one_of = { "foo" } } },
then_field = "redis_host",
then_match = { eq = "host_foo" } } },
}
})
local ok, err = Test:validate_update({
a_set = { "foo" },
redis_host = "host_foo",
})
assert.truthy(ok)
assert.is_nil(err)
ok, err = Test:validate_update({
a_set = { "foo" },
redis_host = "host_bar",
})
assert.falsy(ok)
assert.same("value must be host_foo", err.redis_host)
ok, err = Test:validate_update({
a_set = { "bar" },
redis_host = "any_other_host",
})
assert.truthy(ok)
assert.is_nil(err)
ok, err = Test:validate_update({
a_set = { "forbidden" },
redis_host = "host_foo",
})
assert.falsy(ok)
assert.same("must not be one of: forbidden, also_forbidden", err.a_set[1])
end)
it("test custom entity checks", function()
local Test = Schema.new({
fields = {
{ aaa = { type = "string" } },
{ bbb = { type = "string" } },
{ ccc = { type = "number" } },
},
entity_checks = {
{ custom_entity_check = {
field_sources = { "bbb", "ccc" },
fn = function(entity)
assert(entity.aaa == nil)
if entity.bbb == "foo" and entity.ccc == 42 then
return true
end
return nil, "oh no"
end,
} }
}
})
local ok, err = Test:validate_update({
aaa = "bar",
bbb = "foo",
ccc = 42
})
assert.truthy(ok)
assert.falsy(err)
ok, err = Test:validate_update({
aaa = ngx.null,
bbb = "foo",
ccc = 42
})
assert.truthy(ok)
assert.falsy(err)
ok, err = Test:validate({
aaa = ngx.null,
bbb = "foo",
})
assert.falsy(ok)
assert.match("field required for entity check", err["ccc"])
ok, err = Test:validate_update({
aaa = ngx.null,
bbb = "foo",
})
assert.falsy(ok)
assert.match("field required for entity check when updating", err["ccc"])
ok, err = Test:validate_update({
aaa = ngx.null,
})
assert.truthy(ok)
assert.falsy(err)
ok, err = Test:validate_update({
bbb = "foo",
ccc = 43
})
assert.falsy(ok)
assert.match("oh no", err["@entity"][1])
ok, err = Test:validate_update({
bbb = "foooo",
ccc = 42
})
assert.falsy(ok)
assert.match("oh no", err["@entity"][1])
end)
it("does not run an entity check if fields have errors", function()
local Test = Schema.new({
fields = {
{ aaa = { type = "string" } },
{ bbb = { type = "string", len_min = 8 } },
{ ccc = { type = "number", between = { 0, 10 } } },
},
entity_checks = {
{ custom_entity_check = {
field_sources = { "bbb", "ccc" },
fn = function(entity)
assert(entity.aaa == nil)
if entity.bbb == "12345678" and entity.ccc == 2 then
return true
end
return nil, "oh no"
end,
} }
}
})
local ok, err = Test:validate_update({
aaa = "bar",
bbb = "foo",
ccc = 42
})
assert.falsy(ok)
assert.match("length must be at least 8", err["bbb"])
assert.match("value should be between 0 and 10", err["ccc"])
assert.falsy(err["@entity"])
ok, err = Test:validate({
aaa = ngx.null,
bbb = "foo",
ccc = 42
})
assert.falsy(ok)
assert.match("length must be at least 8", err["bbb"])
assert.match("value should be between 0 and 10", err["ccc"])
assert.falsy(err["@entity"])
ok, err = Test:validate({
bbb = "AAAAAAAA",
ccc = 9,
})
assert.falsy(ok)
assert.match("oh no", err["@entity"][1])
ok, err = Test:validate({
bbb = "12345678",
ccc = 2,
})
assert.truthy(ok)
assert.falsy(err)
end)
it("supports entity checks on nested fields", function()
local Test = Schema.new({
fields = {
{ config = {
type = "record",
fields = {
{ policy = { type = "string", one_of = { "redis", "bla" } } },
{ redis_host = { type = "string" } },
{ redis_port = { type = "number" } },
}
} }
},
entity_checks = {
{ conditional = { if_field = "config.policy",
if_match = { eq = "redis" },
then_field = "config.redis_host",
then_match = { required = true } } },
{ conditional = { if_field = "config.policy",
if_match = { eq = "redis" },
then_field = "config.redis_port",
then_match = { required = true } } },
}
})
local ok, err = Test:validate_update({ config = { policy = "redis" } })
assert.falsy(ok)
assert.truthy(err)
assert.falsy(Test:validate_update({
config = {
policy = "redis",
redis_host = ngx.null,
redis_port = ngx.null,
}
}))
assert.truthy(Test:validate_update({
config = {
policy = "redis",
redis_host = "example.com",
redis_port = 80
}
}))
assert.falsy(Test:validate_update({
config = {
policy = "redis",
}
}))
assert.truthy(Test:validate_update({
config = {
policy = "bla",
}
}))
end)
it("does not demand interdependent fields that aren't being updated", function()
local Test = Schema.new({
fields = {
{ a = { type = "number" } },
{ b = { type = "number" } },
{ c = { type = "number" } },
{ d = { type = "number" } },
},
entity_checks = {
{ only_one_of = { "a", "b" } },
}
})
assert.truthy(Test:validate_update({ c = 15 }))
end)
end)
describe("process_auto_fields", function()
for _, context in ipairs({ "insert", "update", "upsert"}) do
it('returns new table when called with "' .. context .. '" context', function()
local Test = Schema.new({
fields = {
{ f = { type = "string", default = "test" } },
}
})
local original = {}
local data, err = Test:process_auto_fields(original, context)
assert.is_nil(err)
assert.not_equal(original, data)
if context == "update" then
assert.is_nil(data.f)
else
assert.equal("test", data.f)
end
assert.is_nil(original.f)
end)
end
it('modifies table in place when called with "select" context', function()
local Test = Schema.new({
fields = {
{ f = { type = "string", default = "test" } },
}
})
local original = {}
local data, err = Test:process_auto_fields(original, "select")
assert.is_nil(err)
assert.equal(original, data)
assert.equal("test", data.f)
assert.equal("test", original.f)
end)
it("produces ngx.null for non-required fields", function()
local Test = Schema.new({
fields = {
{ a = { type = "array", elements = { type = "string" } }, },
{ b = { type = "set", elements = { type = "string" } }, },
{ c = { type = "number" }, },
{ d = { type = "integer" }, },
{ e = { type = "boolean" }, },
{ f = { type = "string" }, },
{ g = { type = "record", fields = {} }, },
{ h = { type = "map", keys = {}, values = {} }, },
{ i = { type = "function" }, },
}
})
check_all_types_covered(Test.fields)
local data, err = Test:process_auto_fields({})
assert.is_nil(err)
assert.same(ngx.null, data.a)
assert.same(ngx.null, data.b)
assert.same(ngx.null, data.c)
assert.same(ngx.null, data.d)
assert.same(ngx.null, data.e)
assert.same(ngx.null, data.f)
assert.same(ngx.null, data.g)
assert.same(ngx.null, data.h)
assert.same(ngx.null, data.i)
end)
it("produces nil for empty string fields with selects", function()
local Test = Schema.new({
fields = {
{ str = { type = "string" }, },
{ rec = { type = "record", fields = { {
str = { type = "string" }, }, {
arr = { type = "array", elements = { type = "string" } }, }, {
set = { type = "set", elements = { type = "string" } }, }, {
map = { type = "map", keys = { type = "string" }, values = { type = "string" } }, }, {
est = { type = "string", len_min = 0 }, }, }, }, },
{ arr = { type = "array", elements = { type = "string" } }, },
{ set = { type = "set", elements = { type = "string" } }, },
{ map = { type = "map", keys = { type = "string" }, values = { type = "string" } }, },
{ est = { type = "string", len_min = 0 }, },
}
})
local data, err = Test:process_auto_fields({
str = "",
rec = {
str = "",
arr = { "", "a", "" },
set = { "", "a", "" },
map = { key = "" },
est = "",
},
arr = { "", "a", "" },
set = { "", "a", "" },
map = { key = "" },
est = "",
}, "select")
assert.is_nil(err)
assert.equal(nil, data.str) -- string
assert.same({"", "a", ""}, data.arr) -- array, TODO: should we remove empty strings from arrays?
assert.same({"", "a" }, data.set) -- set, TODO: should we remove empty strings from sets?
assert.same({ key = "" }, data.map) -- map, TODO: should we remove empty strings from maps?
assert.equal("", data.est)
-- record
assert.equal(nil, data.rec.str) -- string
assert.same({"", "a", ""}, data.rec.arr) -- array, TODO: should we remove empty strings from arrays?
assert.same({"", "a" }, data.rec.set) -- set, TODO: should we remove empty strings from sets?
assert.same({ key = "" }, data.rec.map) -- map, TODO: should we remove empty strings from maps?
assert.equal("", data.rec.est)
end)
it("produces ngx.null (when asked) for empty string fields with selects", function()
local Test = Schema.new({
fields = {
{ str = { type = "string" }, },
{ rec = { type = "record", fields = { {
str = { type = "string" }, }, {
arr = { type = "array", elements = { type = "string" } }, }, {
set = { type = "set", elements = { type = "string" } }, }, {
map = { type = "map", keys = { type = "string" }, values = { type = "string" } }, }, {
est = { type = "string", len_min = 0 }, }, }, }, },
{ arr = { type = "array", elements = { type = "string" } }, },
{ set = { type = "set", elements = { type = "string" } }, },
{ map = { type = "map", keys = { type = "string" }, values = { type = "string" } }, },
{ est = { type = "string", len_min = 0 }, },
}
})
local data, err = Test:process_auto_fields({
str = "",
rec = {
str = "",
arr = { "", "a", "" },
set = { "", "a", "" },
map = { key = "" },
est = "",
},
arr = { "", "a", "" },
set = { "", "a", "" },
map = { key = "" },
est = "",
}, "select", true)
assert.is_nil(err)
assert.equal(cjson.null, data.str) -- string
assert.same({"", "a", ""}, data.arr) -- array, TODO: should we set null empty strings from arrays?
assert.same({"", "a" }, data.set) -- set, TODO: should we set null empty strings from sets?
assert.same({ key = "" }, data.map) -- map, TODO: should we set null empty strings from maps?
assert.equal("", data.est)
-- record
assert.equal(cjson.null, data.rec.str) -- string
assert.same({"", "a", ""}, data.rec.arr) -- array, TODO: should we set null empty strings from arrays?
assert.same({"", "a" }, data.rec.set) -- set, TODO: should we set null empty strings from sets?
assert.same({ key = "" }, data.rec.map) -- map, TODO: should we set null empty strings from maps?
assert.equal("", data.rec.est)
end)
it("does not produce non-required fields on 'update'", function()
local Test = Schema.new({
fields = {
{ a = { type = "array", elements = { type = "string" } }, },
{ b = { type = "set", elements = { type = "string" } }, },
{ c = { type = "number" }, },
{ d = { type = "integer" }, },
{ e = { type = "boolean" }, },
{ f = { type = "string" }, },
{ g = { type = "record", fields = {} }, },
{ h = { type = "map", keys = {}, values = {} }, },
{ i = { type = "function" }, },
}
})
check_all_types_covered(Test.fields)
local data, err = Test:process_auto_fields({}, "update")
assert.is_nil(err)
assert.is_nil(data.a)
assert.is_nil(data.b)
assert.is_nil(data.c)
assert.is_nil(data.d)
assert.is_nil(data.e)
assert.is_nil(data.f)
assert.is_nil(data.g)
assert.is_nil(data.h)
assert.is_nil(data.i)
end)
-- regression test for #3910
it("lets invalid values pass unchanged", function()
local Test = Schema.new({
fields = {
{ my_array = { type = "array", elements = { type = "string" } }, },
{ my_set = { type = "set", elements = { type = "string" } }, },
{ my_number = { type = "number" }, },
{ my_integer = { type = "integer" }, },
{ my_boolean = { type = "boolean" }, },
{ my_string = { type = "string" }, },
{ my_record = { type = "record", fields = { { my_field = { type = "integer" } } } } },
{ my_map = { type = "map", keys = {}, values = {} }, },
{ my_function = { type = "function" }, },
}
})
check_all_types_covered(Test.fields)
local bad_value = {
my_array = "hello",
my_set = "hello",
my_number = "hello",
my_integer = "hello",
my_boolean = "hello",
my_string = 123,
my_record = "hello",
my_map = "hello",
my_function = "hello",
}
local data, err = Test:process_auto_fields(bad_value)
assert.is_nil(err)
assert.same(data, bad_value)
local data2, err = Test:process_auto_fields(bad_value, "update")
assert.is_nil(err)
assert.same(data2, bad_value)
end)
it("honors given default values", function()
local f = function() end
local Test = Schema.new({
fields = {
{ a = { type = "array",
elements = { type = "string" },
default = { "foo", "bar" } }, },
{ b = { type = "set",
elements = { type = "number" },
default = { 2112, 5150 } }, },
{ c = { type = "number", default = 1984 }, },
{ d = { type = "integer", default = 42 }, },
{ e = { type = "boolean", default = true }, },
{ f = { type = "string", default = "foo" }, },
{ g = { type = "map",
keys = { type = "string" },
values = { type = "number" },
default = { foo = 1, bar = 2 } }, },
{ h = { type = "record",
fields = {
{ f = { type = "number" }, },
},
default = { f = 123 } }, },
{ i = { type = "function", default = f } },
{ nested_record = {
type = "record",
default = {
r = {
a = "nr",
b = 123,
}
},
fields = {
{ r = {
type = "record",
fields = {
{ a = { type = "string" } },
{ b = { type = "number" } }
}
} }
}
} }
}
})
check_all_types_covered(Test.fields)
local data = Test:process_auto_fields({})
assert.same({ "foo", "bar" }, data.a)
assert.same({ 2112, 5150 }, data.b)
assert.same(1984, data.c)
assert.same(42, data.d)
assert.is_true(data.e)
assert.same("foo", data.f)
assert.same({ foo = 1, bar = 2 }, data.g)
assert.same({ f = 123 }, data.h)
assert.same(f, data.i)
assert.same({ r = { a = "nr", b = 123, }}, data.nested_record)
end)
it("detects an empty Lua table as a default for an set and marks it as a json array", function()
local Test = Schema.new({
fields = {
{ s = { type = "set",
elements = { type = "string" },
default = {} }, },
}
})
local data = Test:process_auto_fields({})
assert.equals('{"s":[]}', cjson.encode(data))
end)
it("detects an empty Lua table as a default for an array and marks it as a json array", function()
local Test = Schema.new({
fields = {
{ a = { type = "array",
elements = { type = "string" },
default = {} }, },
}
})
local data = Test:process_auto_fields({})
assert.equals('{"a":[]}', cjson.encode(data))
end)
it("accepts cjson.empty_array as a default for an array", function()
local Test = Schema.new({
fields = {
{ b = { type = "array",
elements = { type = "string" },
default = cjson.empty_array }, },
}
})
local data = Test:process_auto_fields({})
assert.equals('{"b":[]}', cjson.encode(data))
end)
it("accepts a table marked with cjson.empty_array_mt as a default for an array", function()
local Test = Schema.new({
fields = {
{ c = { type = "array",
elements = { type = "string" },
default = setmetatable({}, cjson.empty_array_mt) }, },
}
})
local data = Test:process_auto_fields({})
assert.equals('{"c":[]}', cjson.encode(data))
end)
it("accepts a table marked with cjson.array_mt as a default for an array", function()
local Test = Schema.new({
fields = {
{ d = { type = "array",
elements = { type = "string" },
default = setmetatable({}, cjson.array_mt) }, },
}
})
local data = Test:process_auto_fields({})
assert.equals('{"d":[]}', cjson.encode(data))
end)
it("nested defaults in required records produce a default record", function()
local Test = Schema.new({
fields = {
{ nested_record = {
type = "record",
required = true,
fields = {
{ r = {
type = "record",
required = true,
fields = {
{ a = { type = "string", default = "nr", } },
{ b = { type = "number", default = 123, } }
}
} }
}
} }
}
})
local data = Test:process_auto_fields({})
assert.same({ r = { a = "nr", b = 123, }}, data.nested_record)
end)
it("null in required records only produces a default record on select", function()
local Test = Schema.new({
fields = {
{ nested_record = {
type = "record",
required = true,
fields = {
{ r = {
type = "record",
required = true,
fields = {
{ a = { type = "string", default = "nr", } },
{ b = { type = "number", default = 123, } }
}
} }
}
} }
}
})
local data = Test:process_auto_fields({ nested_record = ngx.null }, "insert")
assert.same(ngx.null, data.nested_record)
assert.falsy(Test:validate(data))
data = Test:process_auto_fields({ nested_record = ngx.null }, "update")
assert.same(ngx.null, data.nested_record)
assert.falsy(Test:validate_update(data))
data = Test:process_auto_fields({ nested_record = ngx.null }, "upsert")
assert.same(ngx.null, data.nested_record)
assert.falsy(Test:validate_update(data))
data = Test:process_auto_fields({ nested_record = ngx.null }, "select")
assert.same({ r = { a = "nr", b = 123, }}, data.nested_record)
assert.truthy(Test:validate(data))
end)
it("honors 'false' as a default", function()
local Test = Schema.new({
fields = {
{ b = { type = "boolean", default = false }, },
}
})
local t1 = Test:process_auto_fields({})
assert.is_false(t1.b)
local t2 = Test:process_auto_fields({ b = false })
assert.is_false(t2.b)
local t3 = Test:process_auto_fields({ b = true })
assert.is_true(t3.b)
end)
it("honors 'true' as a default", function()
local Test = Schema.new({
fields = {
{ b = { type = "boolean", default = true }, },
}
})
local t1 = Test:process_auto_fields({})
assert.is_true(t1.b)
local t2 = Test:process_auto_fields({ b = false })
assert.is_false(t2.b)
local t3 = Test:process_auto_fields({ b = true })
assert.is_true(t3.b)
end)
it("does not demand required fields", function()
local Test = Schema.new({
fields = {
{ f = { type = "number", required = true } }
}
})
assert.truthy(Test:process_auto_fields({ f = 123 }))
assert.truthy(Test:process_auto_fields({}))
end)
it("removes duplicates preserving order", function()
local Test = Schema.new({
fields = {
{ f = { type = "set", elements = { type = "string" } } }
}
})
local tests = {
{ {}, {} },
{ {"foo"}, {"foo"} },
{ {"foo", "bar"}, {"foo", "bar"} },
{ {"bar", "foo"}, {"bar", "foo"} },
{ {"foo", "foo", "bar"}, {"foo", "bar"} },
{ {"foo", "bar", "foo"}, {"foo", "bar"} },
{ {"foo", "foo", "foo"}, {"foo"} },
{ {"bar", "foo", "foo"}, {"bar", "foo"} },
}
for _, test in ipairs(tests) do
assert.same({ f = test[2] }, Test:process_auto_fields({ f = test[1] }))
end
end)
-- TODO is this behavior correct?
it("non-required fields do not generate defaults", function()
local Test = Schema.new({
fields = {
{ f = { type = "number" }, },
}
})
local data = Test:process_auto_fields({})
assert.same(ngx.null, data.f)
end)
it("auto-produces an UUID with 'uuid' and 'auto'", function()
local Test = Schema.new({
fields = {
{ f = { type = "string", uuid = true, auto = true } }
}
})
local tbl = {}
tbl = Test:process_auto_fields(tbl, "insert")
assert.match(uuid_pattern, tbl.f)
end)
it("auto-produces a random with 'string' and 'auto'", function()
local Test = Schema.new({
fields = {
{ f = { type = "string", auto = true } }
}
})
local tbl = {}
tbl = Test:process_auto_fields(tbl, "insert")
assert.is_string(tbl.f)
assert.equals(32, #tbl.f)
end)
it("auto-produces a timestamp with 'created_at' and 'auto'", function()
local Test = Schema.new({
fields = {
{ created_at = { type = "number", timestamp = true, auto = true } }
}
})
local tbl = {}
-- Does not insert `created_at` on "update"
tbl = Test:process_auto_fields(tbl, "update")
assert.falsy(tbl.created_at)
-- It does insert it on "insert"
tbl = Test:process_auto_fields(tbl, "insert")
assert.number(tbl.created_at)
end)
it("auto-updates a timestamp with 'updated_at' and 'auto'", function()
local Test = Schema.new({
fields = {
{ updated_at = { type = "number", timestamp = true, auto = true } }
}
})
local tbl = {}
tbl = Test:process_auto_fields(tbl, "update")
assert.number(tbl.updated_at)
-- force updated_at downwards...
local ts = tbl.updated_at - 10
tbl.updated_at = ts
-- ...and updates it again
tbl = Test:process_auto_fields(tbl, "update")
assert.number(tbl.updated_at)
-- Note: this assumes the clock only moves forwards during the execution
-- of the test. As we store UTC timestamps, we're immune to DST
-- downward adjustments, and ntp leap second adjustments only move
-- forward.
assert.truthy(tbl.updated_at > ts)
end)
it("does not auto-update a timestamp with 'created_at' or 'updated_at' and 'auto' upon retrival", function()
local Test = Schema.new({
fields = {
{ created_at = { type = "number", timestamp = true, auto = true } },
{ updated_at = { type = "number", timestamp = true, auto = true } },
}
})
local tbl = {}
tbl = Test:process_auto_fields(tbl, "insert")
assert.number(tbl.created_at)
assert.number(tbl.updated_at)
-- force updated_at downwards...
local created_ts = tbl.created_at - 10
local updated_ts = tbl.updated_at - 10
tbl.created_at = created_ts
tbl.updated_at = updated_ts
-- ...and doesn't updates it again
tbl = Test:process_auto_fields(tbl, "select")
assert.number(tbl.created_at)
assert.same(updated_ts, tbl.created_at)
assert.number(tbl.updated_at)
assert.same(updated_ts, tbl.updated_at)
end)
it("strips down the decimal part on integers when selecting, but not in other contexts", function()
local Test = Schema.new({
fields = {
{ fingers = { type = "integer" } }
}
})
local tbl = Test:process_auto_fields({ fingers = 5.5 }, "select")
assert.equals(5, tbl.fingers)
local tbl = Test:process_auto_fields({ fingers = 5.5 }, "insert")
assert.equals(5.5, tbl.fingers)
end)
it("adds cjson.array_mt on non-empty array fields", function()
local Test = Schema.new({
fields = {
{ arr = { type = "array", elements = { type = "string" } } },
},
})
local tbl = Test:process_auto_fields({
arr = { "hello" },
}, "insert")
assert.same(cjson.array_mt, getmetatable(tbl.arr))
end)
it("adds cjson.array_mt on empty array and set fields", function()
local Test = Schema.new({
fields = {
{ arr = { type = "array", elements = { type = "string" } } },
{ set = { type = "set", elements = { type = "string" } } },
},
})
local tbl = Test:process_auto_fields({
arr = {},
set = {}
}, "insert")
assert.same(cjson.array_mt, getmetatable(tbl.arr))
assert.same(cjson.array_mt, getmetatable(tbl.set))
end)
it("adds cjson.array_mt on empty array and set fields", function()
local Test = Schema.new({
fields = {
{ arr = { type = "array", elements = { type = "string" } } },
{ set = { type = "set", elements = { type = "string" } } },
},
})
for _, operation in pairs{ "insert", "update", "select", "delete" } do
local tbl = Test:process_auto_fields({
arr = {},
set = {}
}, operation)
assert.same(cjson.array_mt, getmetatable(tbl.arr))
assert.same(cjson.array_mt, getmetatable(tbl.set))
end
end)
it("adds a helper metatable to sets", function()
local Test = Schema.new({
fields = {
{ set = { type = "set", elements = { type = "string" } } },
},
})
for _, operation in pairs{ "insert", "update", "select", "delete" } do
local tbl = Test:process_auto_fields({
set = { "http", "https" },
}, operation)
assert.equal("table", type(getmetatable(tbl.set)))
assert.truthy(tbl.set.http)
assert.truthy(tbl.set.https)
assert.falsy(tbl.set.smtp)
end
end)
it("does not add a helper metatable to maps", function()
local Test = Schema.new({
fields = {
{ map = { type = "map", keys = { type = "string" }, values = { type = "boolean" } } },
},
})
for _, operation in pairs{ "insert", "update", "select", "delete" } do
local tbl = Test:process_auto_fields({
map = { http = true },
}, operation)
assert.is_nil(getmetatable(tbl.map))
assert.is_true(tbl.map.http)
assert.is_nil(tbl.map.https)
end
end)
it("does add array_mt metatable to arrays", function()
local Test = Schema.new({
fields = {
{ arr = { type = "array", elements = { type = "string" } } },
},
})
for _, operation in pairs{ "insert", "update", "select", "delete" } do
local tbl = Test:process_auto_fields({
arr = { "http", "https" },
}, operation)
assert.is_equal(cjson.array_mt, getmetatable(tbl.arr))
assert.is_equal("http", tbl.arr[1])
assert.is_nil(tbl.arr.http)
end
end)
it("correctly flags check_immutable_fields when immutable present in schema", function()
local test_schema = {
name = "test",
fields = {
{ name = { type = "string", immutable = true }, },
},
}
local test_entity = { name = "bob" }
local TestEntities = Schema.new(test_schema)
local _, _, check_immutable_fields =
TestEntities:process_auto_fields(test_entity, "update")
assert.truthy(check_immutable_fields)
end)
it("correctly flags check_immutable_fields when immutable absent from schema", function()
local test_schema = {
name = "test",
fields = {
{ name = { type = "string" }, },
},
}
local test_entity = { name = "bob" }
local TestEntities = Schema.new(test_schema)
local _, _, check_immutable_fields =
TestEntities:process_auto_fields(test_entity, "update")
assert.falsy(check_immutable_fields)
end)
describe("in subschemas", function()
it("a specialized field can set a default", function()
local Test = Schema.new({
name = "test",
subschema_key = "name",
fields = {
{ name = { type = "string", required = true, } },
{ config = { type = "record", abstract = true } },
}
})
assert(Test:new_subschema("my_subschema", {
fields = {
{ config = {
type = "record",
fields = {
{ foo = { type = "string", default = "bar" } },
},
default = { foo = "bla" }
} }
}
}))
local input = {
name = "my_subschema",
config = { foo = "hello" },
}
local ok = Test:validate(input)
assert.truthy(ok)
local output = Test:process_auto_fields(input)
assert.same(input, output)
input = {
name = "my_subschema",
config = nil,
}
ok = Test:validate(input)
assert.truthy(ok)
output = Test:process_auto_fields(input)
assert.same({
name = "my_subschema",
config = {
foo = "bla",
}
}, output)
end)
it("removes fields that have been removed from the schema (on select context)", function()
local Test = Schema.new({
name = "test",
subschema_key = "name",
fields = {
{ name = { type = "string", required = true, } },
{ config = { type = "record", abstract = true } },
}
})
assert(Test:new_subschema("my_subschema", {
fields = {
{ config = {
type = "record",
fields = {
{ foo = { type = "string" } },
},
default = { foo = "bla" }
} }
}
}))
local input = {
name = "my_subschema",
config = { foo = "hello", bar = "world" },
}
local output = Test:process_auto_fields(input, "select")
input.config.bar = nil
assert.same(input, output)
end)
end)
end)
describe("merge_values", function()
it("should correctly merge records", function()
local Test = Schema.new({
name = "test", fields = {
{ config = {
type = "record",
fields = {
foo = { type = "string" },
bar = { type = "string" }
}
}
},
{ name = { type = "string" }
}}
})
local old_values = {
name = "test",
config = { foo = "dog", bar = "cat" },
}
local new_values = {
name = "test",
config = { foo = "pig" },
}
local expected_values = {
name = "test",
config = { foo = "pig", bar = "cat" }
}
local values = Test:merge_values(new_values, old_values)
assert.equals(values.config.foo, expected_values.config.foo)
assert.equals(values.config.bar, expected_values.config.bar)
end)
end)
describe("validate_immutable_fields", function()
it("returns ok when immutable unset in schema fields", function()
local test_schema = {
name = "test",
fields = {
{ name = { type = "string" }, },
},
}
local entity_to_update = { name = "test1" }
local db_entity = { name = "test2" }
local TestEntities = Schema.new(test_schema)
local ok, _ = TestEntities:validate_immutable_fields(entity_to_update, db_entity)
assert.truthy(ok)
end)
it("returns errors when immutable set incoming field being updated", function()
local test_schema = {
name = "test",
fields = {
{ name = { type = "string", immutable = true }, },
{ address = { type = "string", immutable = true }, },
{ email = { type = "string" }, },
},
}
local entity_to_update = { name = "test1", address = "a", email = "a@thing.com" }
local db_entity = { name = "test2", address = "b", email = "b@thing.com" }
local TestEntities = Schema.new(test_schema)
local ok, errors = TestEntities:validate_immutable_fields(entity_to_update, db_entity)
assert.falsy(ok)
assert.equals(errors.name, 'immutable field cannot be updated')
assert.equals(errors.address, 'immutable field cannot be updated')
assert.falsy(errors.email)
end)
it("returns ok when immutable set incoming field being updated and value is same", function()
local test_schema = {
name = "test",
fields = {
{ name = { type = "string", immutable = true }, },
},
}
local entity_to_update = { name = "test1" }
local db_entity = { name = "test1" }
local TestEntities = Schema.new(test_schema)
local ok, _ = TestEntities:validate_immutable_fields(entity_to_update, db_entity)
assert.truthy(ok)
end)
it("can assess if set type immutable fields are similar", function()
local test_schema = {
name = "test",
fields = {
{ table = { type = "set", immutable = true }, },
},
}
local entity_to_update = { table = { dog = "hello", cat = { bat = "hello", }, }, }
local db_entity = { table = { dog = "hello", cat = { bat = "hello", }, }, }
local TestEntities = Schema.new(test_schema)
local ok, _ = TestEntities:validate_immutable_fields(entity_to_update, db_entity)
assert.truthy(ok)
end)
it("can assess if foriegn type immutable fields are similar", function()
local test_schema = {
name = "test",
fields = {
{ entity = { type = "foriegn", immutable = true }, },
},
}
local entity_to_update = { entity = { id = '1' }, }
local db_entity = { entity = { id = '1' }, }
local TestEntities = Schema.new(test_schema)
local ok, _ = TestEntities:validate_immutable_fields(entity_to_update, db_entity)
assert.truthy(ok)
end)
it("can assess if array type immutable fields are similar", function()
local test_schema = {
name = "test",
fields = {
{ list = { type = "array", immutable = true }, },
},
}
local entity_to_update = { 'dog', 'bat', 'cat', }
local db_entity = { 'bat', 'cat', 'dog', }
local TestEntities = Schema.new(test_schema)
local ok, _ = TestEntities:validate_immutable_fields(entity_to_update, db_entity)
assert.truthy(ok)
end)
it("can assess if set type immutable fields are not similar", function()
local test_schema = {
name = "test",
fields = {
{ table = { type = "set", immutable = true }, },
},
}
local entity_to_update = { table = { dog = "hello", cat = { bat = "hello", }, }, }
local db_entity = { table = { dog = "hello", cat = { bat = "goodbye", }, }, }
local TestEntities = Schema.new(test_schema)
local ok, err = TestEntities:validate_immutable_fields(entity_to_update, db_entity)
assert.falsy(ok)
assert.equals(err.table, 'immutable field cannot be updated')
end)
it("can assess if foriegn type immutable fields are not similar", function()
local test_schema = {
name = "test",
fields = {
{ entity = { type = "foriegn", immutable = true }, },
},
}
local entity_to_update = { entity = { id = '1' }, }
local db_entity = { entity = { id = '2' }, }
local TestEntities = Schema.new(test_schema)
local ok, err = TestEntities:validate_immutable_fields(entity_to_update, db_entity)
assert.falsy(ok)
assert.equals(err.entity, 'immutable field cannot be updated')
end)
it("can assess if array type immutable fields are not similar", function()
local test_schema = {
name = "test",
fields = {
{ list = { type = "array", immutable = true }, },
},
}
local entity_to_update = { list = { 'dog', 'bat', 'cat', }, }
local db_entity = { list = { 'bat', 'cat', 'rat', }, }
local TestEntities = Schema.new(test_schema)
local ok, err = TestEntities:validate_immutable_fields(entity_to_update, db_entity)
assert.falsy(ok)
assert.equals(err.list, 'immutable field cannot be updated')
end)
end)
describe("shorthand_fields", function()
it("converts fields", function()
local TestSchema = Schema.new({
name = "test",
fields = {
{ name = { type = "string" } },
},
shorthand_fields = {
{
username = {
type = "string",
func = function(value)
return {
name = value
}
end,
},
},
},
})
local input = { username = "test1" }
local output, _ = TestSchema:process_auto_fields(input)
assert.same({ name = "test1" }, output)
end)
it("can produce multiple fields", function()
local TestSchema = Schema.new({
name = "test",
fields = {
{ name = { type = "string" } },
{ address = { type = "string" } },
},
shorthand_fields = {
{
username = {
type = "string",
func = function(value)
return {
name = value,
address = value:upper(),
}
end,
},
},
},
})
local input = { username = "test1" }
local output, _ = TestSchema:process_auto_fields(input)
assert.same({ name = "test1", address = "TEST1" }, output)
end)
it("type checks", function()
local TestSchema = Schema.new({
name = "test",
fields = {
{ name = { type = "string" } },
{ address = { type = "string" } },
},
shorthand_fields = {
{
username = {
type = "string",
func = function(value)
return {
name = value,
address = value:upper(),
}
end,
},
},
},
})
local input = { username = 123 }
local ok, err = TestSchema:process_auto_fields(input)
assert.falsy(ok)
assert.same({ username = "expected a string" }, err)
end)
it("accepts arrays", function()
local TestSchema = Schema.new({
name = "test",
fields = {
{ name = { type = "string" } },
{ address = { type = "string" } },
},
shorthand_fields = {
{
user = {
type = "array",
elements = { type = "string" },
func = function(value)
return {
name = value[1] or "mario",
address = value[2] or "world",
}
end,
},
},
},
})
local input = { user = { "luigi", "land" } }
local output, _ = TestSchema:process_auto_fields(input)
assert.same({ name = "luigi", address = "land" }, output)
end)
it("type checks arrays", function()
local TestSchema = Schema.new({
name = "test",
fields = {
{ name = { type = "string" } },
{ address = { type = "string" } },
},
shorthand_fields = {
{
user = {
type = "array",
elements = { type = "string" },
func = function(value)
return {
name = value[1] or "mario",
address = value[2] or "world",
}
end,
},
},
},
})
local input = { user = "luigi,land" }
local ok, err = TestSchema:process_auto_fields(input)
assert.falsy(ok)
assert.same({ user = "expected an array" }, err)
end)
end)
describe("get_constraints", function()
it("returns empty constraints", function()
local test_schema = {
name = "test",
fields = { { name = { type = "string" }, }, },
}
local TestEntities = Schema.new(test_schema)
local constraints = TestEntities:get_constraints()
assert.are.same({}, constraints)
end)
it("returns constraints", function()
local schema1 = {
name = "test1",
fields = { { name = { type = "string" }, }, }
}
local schema2 = {
name = "test2",
fields = {
{ foreign_reference1 = { type = "foreign", reference = "test1" } },
},
}
local schema3 = {
name = "test3",
fields = {
{ foreign_reference2 = { type = "foreign", reference = "test1", on_delete = "cascade" } },
},
}
local Entities1 = Schema.new(schema1)
assert.is.Truthy(Entities1)
local Entities2 = Schema.new(schema2)
assert.is.Truthy(Entities2)
local Entities3 = Schema.new(schema3)
assert.is.Truthy(Entities3)
local constraints = Entities1:get_constraints()
table.sort(constraints, function(a, b)
return a.field_name < b.field_name
end)
assert.are.same({
{ schema = Entities2, field_name = 'foreign_reference1', on_delete = nil },
{ schema = Entities3, field_name = 'foreign_reference2', on_delete = "cascade" },
}, constraints)
end)
it("merges workspaceable constraints", function()
local workspace_schema = {
name = "workspaces",
fields = { { name = { type = "string" }, }, }
}
local schema1 = {
name = "test4",
workspaceable = true,
fields = { { name = { type = "string" }, }, }
}
local WorkspaceEntity = Schema.new(workspace_schema)
assert.is.Truthy(WorkspaceEntity)
local Entities2 = Schema.new(schema1)
assert.is.Truthy(Entities2)
local constraints = WorkspaceEntity:get_constraints()
assert.are.same({
test4 = true,
{ schema = Entities2 }
}, constraints)
end)
end)
for i = 1, 2 do
describe("transform (" .. SchemaKind[i].name .. ")", function()
it("transforms fields", function()
local test_schema = {
name = "test",
fields = {
{
name = {
type = "string"
},
},
},
transformations = {
{
input = { "name" },
on_write = function(name)
return { name = name:upper() }
end,
},
},
}
local entity = { name = "test1" }
local TestEntities = SchemaKind[i].new(test_schema)
local transformed_entity, _ = TestEntities:transform(entity)
assert.truthy(transformed_entity)
assert.equal("TEST1", transformed_entity.name)
end)
it("transforms fields on write and read", function()
local test_schema = {
name = "test",
fields = {
{
name = {
type = "string"
},
},
},
transformations = {
{
input = { "name" },
on_write = function(name)
return { name = name:upper() }
end,
on_read = function(name)
return { name = name:lower() }
end,
},
},
}
local entity = { name = "TeSt1" }
local TestEntities = SchemaKind[i].new(test_schema)
local transformed_entity, _ = TestEntities:transform(entity)
assert.truthy(transformed_entity)
assert.equal("TEST1", transformed_entity.name)
transformed_entity, _ = TestEntities:transform(transformed_entity, nil, "select")
assert.truthy(transformed_entity)
assert.equal("test1", transformed_entity.name)
end)
it("transforms fields with input table", function()
local test_schema = {
name = "test",
fields = {
{
name = {
type = "string"
},
},
},
transformations = {
{
input = { "name" },
on_write = function(name)
return { name = name:upper() }
end,
},
},
}
local entity = { name = "test1" }
local input = { name = "we have a value" }
local TestEntities = SchemaKind[i].new(test_schema)
local transformed_entity, _ = TestEntities:transform(entity, input)
assert.truthy(transformed_entity)
assert.equal("TEST1", transformed_entity.name)
end)
it("skips transformation when none of input matches", function()
local test_schema = {
name = "test",
fields = {
{
name = {
type = "string"
},
},
},
transformations = {
{
input = { "non_existent" },
on_write = function(non_existent)
return { name = non_existent:upper() }
end,
},
},
}
local entity = { name = "test1" }
local TestEntities = SchemaKind[i].new(test_schema)
local transformed_entity, _ = TestEntities:transform(entity)
assert.truthy(transformed_entity)
assert.equal("test1", transformed_entity.name)
end)
it("skips transformation when none of input matches using input table", function()
local test_schema = {
name = "test",
fields = {
{
name = {
type = "string"
},
},
},
transformations = {
{
input = { "name" },
on_write = function(non_existent)
return { name = non_existent:upper() }
end,
},
},
}
local entity = { name = "test1" }
local input = { name = nil }
local TestEntities = SchemaKind[i].new(test_schema)
local transformed_entity, _ = TestEntities:transform(entity, input)
assert.truthy(transformed_entity)
assert.equal("test1", transformed_entity.name)
end)
it("transforms fields with multiple transformations", function()
local test_schema = {
name = "test",
fields = {
{
name = {
type = "string"
},
},
},
transformations = {
{
input = { "name" },
on_write = function(name)
return { name = "How are you " .. name }
end,
},
{
input = { "name" },
on_write = function(name)
return { name = name .. "?" }
end,
},
},
}
local entity = { name = "Bob" }
local TestEntities = SchemaKind[i].new(test_schema)
local transformed_entity, _ = TestEntities:transform(entity)
assert.truthy(transformed_entity)
assert.equal("How are you Bob?", transformed_entity.name)
end)
it("transforms any field not just those given as an input", function()
local test_schema = {
name = "test",
fields = {
{
name = {
type = "string"
},
age = {
type = "integer"
}
},
},
transformations = {
{
input = { "name" },
on_write = function(name)
return { age = #name }
end,
},
},
}
local entity = { name = "Bob" }
local TestEntities = SchemaKind[i].new(test_schema)
local transformed_entity, _ = TestEntities:transform(entity)
assert.truthy(transformed_entity)
assert.equal("Bob", transformed_entity.name)
assert.equal(3, transformed_entity.age)
end)
it("returns error if transformation returns an error", function()
local test_schema = {
name = "test",
fields = {
{
name = {
type = "string"
},
},
},
transformations = {
{
input = { "name" },
on_write = function(name)
return nil, "unable to transform name"
end,
},
},
}
local entity = { name = "test1" }
local TestEntities = SchemaKind[i].new(test_schema)
local transformed_entity, err = TestEntities:transform(entity)
assert.falsy(transformed_entity)
assert.equal("transformation failed: unable to transform name", err)
end)
it("skips transformation if needs are not fulfilled", function()
local test_schema = {
name = "test",
fields = {
{
name = {
type = "string"
},
age = {
type = "integer"
},
},
},
transformations = {
{
input = { "name" },
needs = { "age" },
on_write = function(name, age)
return { name = name:upper() }
end,
},
},
}
local entity = { name = "test1" }
local TestEntities = SchemaKind[i].new(test_schema)
local transformed_entity, _ = TestEntities:transform(entity)
assert.truthy(transformed_entity)
assert.equal("test1", transformed_entity.name)
end)
it("transforms fields with needs given to function", function()
local test_schema = {
name = "test",
fields = {
{
name = {
type = "string"
},
age = {
type = "integer"
},
},
},
transformations = {
{
input = { "name" },
needs = { "age" },
on_write = function(name, age)
return { name = name .. " " .. age }
end,
},
},
}
local entity = { name = "John", age = 13 }
local TestEntities = SchemaKind[i].new(test_schema)
local transformed_entity, _ = TestEntities:transform(entity)
assert.truthy(transformed_entity)
assert.equal("John 13", transformed_entity.name)
end)
end)
end
end)