local M = {}
local GRADER_TAG = {}
local function safe_check(fn)
return function(response, case)
local ok, val, err = pcall(fn, response, case)
if ok then return val, err end
return nil, tostring(val)
end
end
local function build(name, spec)
if type(spec.check) ~= "function" then
error(string.format("grader '%s': 'check' must be function, got %s", name, type(spec.check)), 3)
end
return {
_tag = GRADER_TAG,
name = name,
check = safe_check(spec.check),
}
end
setmetatable(M, {
__call = function(_, name)
if type(name) ~= "string" then
error(string.format("grader: name must be string, got %s", type(name)), 2)
end
return function(spec)
if type(spec) ~= "table" then
error(string.format("grader '%s': spec must be a table", name), 2)
end
return build(name, spec)
end
end,
})
function M.all(...)
local graders = { ... }
local names = {}
for _, g in ipairs(graders) do
if not M.is_grader(g) then
error("Grader.all: all arguments must be GraderDef", 2)
end
names[#names + 1] = g.name
end
return {
_tag = GRADER_TAG,
name = "all(" .. table.concat(names, ",") .. ")",
check = safe_check(function(response, case)
local min_val = nil
local all_bool = true
for _, g in ipairs(graders) do
local val, err = g.check(response, case)
if err then return nil, err end
if not val then return false end
if type(val) == "number" then
all_bool = false
if min_val == nil or val < min_val then min_val = val end
elseif type(val) ~= "boolean" then
all_bool = false
end
end
if all_bool then return true end
return min_val ~= nil and min_val or true
end),
}
end
function M.any(...)
local graders = { ... }
local names = {}
for _, g in ipairs(graders) do
if not M.is_grader(g) then
error("Grader.any: all arguments must be GraderDef", 2)
end
names[#names + 1] = g.name
end
return {
_tag = GRADER_TAG,
name = "any(" .. table.concat(names, ",") .. ")",
check = safe_check(function(response, case)
local last_err
for _, g in ipairs(graders) do
local val, err = g.check(response, case)
if not err and val then return val end
last_err = err
end
return false, last_err
end),
}
end
function M.is_grader(v)
return type(v) == "table" and v._tag == GRADER_TAG
end
return M