local T = require("alc_shapes.t")
local check = require("alc_shapes.check")
local M = {}
local function is_schema(v)
return type(v) == "table" and rawget(v, "kind") ~= nil
end
local function coerce_shape_ref(v)
if v == nil then return nil end
if type(v) == "string" then
return T.ref(v)
end
if is_schema(v) then
return v
end
error(
"alc_shapes.spec_resolver: shape field must be string or schema, got "
.. type(v), 2)
end
local function coerce_args_list(v, entry_name)
if v == nil then return nil end
if type(v) ~= "table" then
error(string.format(
"alc_shapes.spec_resolver: spec.entries.%s.args must be an array "
.. "of shapes (got %s)",
tostring(entry_name), type(v)), 2)
end
local n = #v
local out = {}
for i = 1, n do
local slot = v[i]
if slot == nil then
out[i] = nil
elseif type(slot) == "string" then
out[i] = T.ref(slot)
elseif is_schema(slot) then
out[i] = slot
else
error(string.format(
"alc_shapes.spec_resolver: spec.entries.%s.args[%d] must be "
.. "string / schema / nil (got %s)",
tostring(entry_name), i, type(slot)), 2)
end
end
return out
end
function M.resolve(pkg)
if type(pkg) ~= "table" then
error("alc_shapes.spec_resolver.resolve: pkg must be a table", 2)
end
local spec = rawget(pkg, "spec")
if type(spec) == "table" and type(spec.entries) == "table" then
local entries = {}
for name, entry in pairs(spec.entries) do
if type(entry) ~= "table" then
error(string.format(
"alc_shapes.spec_resolver: spec.entries.%s must be a table",
tostring(name)), 2)
end
if entry.input ~= nil and entry.args ~= nil then
error(string.format(
"alc_shapes.spec_resolver: spec.entries.%s declares both "
.. "`input` (ctx-threading) and `args` (direct-args); "
.. "these modes are mutually exclusive",
tostring(name)), 2)
end
entries[name] = {
input = coerce_shape_ref(entry.input),
result = coerce_shape_ref(entry.result),
args = coerce_args_list(entry.args, name),
}
end
return {
kind = "typed",
origin = "spec",
entries = entries,
compose = spec.compose,
exports = spec.exports,
}
end
return {
kind = "opaque",
origin = "none",
entries = {},
compose = nil,
exports = nil,
}
end
function M.run(pkg, ctx, entry_name)
entry_name = entry_name or "run"
local fn = rawget(pkg, entry_name)
if type(fn) ~= "function" then
error(string.format(
"alc_shapes.spec_resolver.run: pkg has no function '%s'",
entry_name), 2)
end
local resolved = M.resolve(pkg)
local pkg_name = (type(pkg.meta) == "table" and pkg.meta.name) or "<anon>"
local ctx_hint = pkg_name .. "." .. entry_name
local entry = resolved.entries[entry_name]
if entry and entry.input then
check.assert_dev(ctx, entry.input, ctx_hint .. ":input")
end
local returned = fn(ctx)
if entry and entry.result then
local actual
if type(returned) == "table" and returned.result ~= nil then
actual = returned.result
else
actual = returned
end
check.assert_dev(actual, entry.result, ctx_hint .. ":result")
end
return returned
end
function M.is_passthrough(pkg, shape_name)
local resolved = M.resolve(pkg)
if resolved.kind ~= "typed" then return false end
if not resolved.compose then return false end
local pt = resolved.compose.passthrough
if pt == nil then return false end
if type(pt) == "string" then
return pt == shape_name
end
if type(pt) == "table" then
for _, n in ipairs(pt) do
if n == shape_name then return true end
end
end
return false
end
return M