Files
cave.nvim/lua/cave/meta.lua
2025-01-19 11:32:56 +01:00

239 lines
6.7 KiB
Lua

---@alias cave.Validator fun(v: any) : boolean
---@class cave.Meta
---@field valid cave.Validator
---@field repr string
local Meta = {}
Meta.__index = Meta
---@alias cave.MetaLikeItem string|table|cave.Meta
---@alias cave.MetaLike cave.MetaLikeItem|cave.MetaLikeItem[]
---@param repr string
---@param valid cave.Validator
---@return cave.Meta
function Meta.new(repr, valid)
vim.validate {
repr = { repr, "string" },
valid = { valid, "function" },
}
local meta = setmetatable({}, Meta)
meta.valid = valid
meta.repr = repr
return meta
end
---@param ... cave.MetaLikeItem
---@return cave.Meta
function Meta.Union(...)
local value_valids = {}
local value_reprs = {}
for idx, value_meta_like in ipairs { ... } do
local value_meta = Meta.like(value_meta_like)
value_valids[idx] = value_meta.valid
value_reprs[idx] = value_meta.repr
end
local repr = ("Union<%s>"):format(table.concat(value_reprs, ","))
local function valid(v)
for _, value_valid in pairs(value_valids) do
if value_valid(v) then return true end
end
return false
end
return Meta.new(repr, valid)
end
---@param value_meta_like cave.MetaLike
---@return cave.Meta
function Meta.Optional(value_meta_like)
local value_meta = Meta.like(value_meta_like)
local repr = ("Optional<%s>"):format(value_meta.repr)
local function valid(v) return v == nil or value_meta.valid(v) end
return Meta.new(repr, valid)
end
---@param mt table
---@param name string
---@param valid cave.Validator?
---@return cave.Meta
function Meta.Class(mt, name, valid)
assert(type(mt) == "table")
local repr = name
---@param v any
---@return boolean
local function default_valid(v)
if type(v) ~= "table" then return false end
local v_mt = getmetatable(v)
while v_mt ~= nil do
if v_mt == mt then return true end
v_mt = getmetatable(v_mt)
end
return false
end
valid = valid or default_valid
return Meta.new(repr, valid)
end
---@param tbl table<string,string>
---@param name string
---@return cave.Meta
function Meta.Enum(tbl, name)
assert(type(tbl) == "table")
local repr = ("Enum<%s>(%s)"):format(name, table.concat(vim.tbl_values(tbl), "|"))
local function valid(v) return type(v) == "string" and tbl[v] == v end
return Meta.new(repr, valid)
end
---@param value_meta_like cave.MetaLike
---@return cave.Meta
function Meta.List(value_meta_like)
local value_meta = Meta.like(value_meta_like)
local repr = ("List<%s>"):format(value_meta.repr)
local function valid(vs)
if not vim.islist(vs) then return false end
for _, v in ipairs(vs) do
if not value_meta.valid(v) then return false end
end
return true
end
return Meta.new(repr, valid)
end
---@param key_meta_like cave.MetaLike
---@param value_meta_like cave.MetaLike
---@return cave.Meta
function Meta.Map(key_meta_like, value_meta_like)
local key_meta = Meta.like(key_meta_like)
local value_meta = Meta.like(value_meta_like)
local repr = ("Map<%s,%s>"):format(key_meta.repr, value_meta.repr)
local function valid(vs)
if type(vs) ~= "table" then return false end
for k, v in ipairs(vs) do
if not key_meta.valid(k) or value_meta.valid(v) then return false end
end
return true
end
return Meta.new(repr, valid)
end
---@param meta_like_tbl table<string, cave.MetaLike>
---@param name string
---@return cave.Meta
function Meta.MapLiteral(meta_like_tbl, name)
---@type table<string, cave.Meta>
local metas = {}
for key, meta_like in meta_like_tbl do
metas[key] = Meta.like(meta_like)
end
---@param vs any
---@return boolean
local function valid(vs)
if type(vs) ~= "table" then return false end
for key, meta in pairs(metas) do
local value = vs[key]
if not meta.valid(value) then return false end
end
for key, _ in pairs(vs) do
if metas[key] == nil then return false end
end
return true
end
return Meta.new(name, valid)
end
Meta.Bool = Meta.new("boolean", function(v) return type(v) == "boolean" end)
Meta.Function = Meta.new("function", function(v) return type(v) == "function" end)
Meta.Number = Meta.new("number", function(v) return type(v) == "number" end)
Meta.String = Meta.new("string", function(v) return type(v) == "string" end)
Meta.Table = Meta.new("table", function(v) return type(v) == "table" end)
Meta.Nil = Meta.new("nil", function(v) return v == nil end)
local meta_aliases = {
["b"] = Meta.Bool,
["bool"] = Meta.Bool,
["boolean"] = Meta.Bool,
["f"] = Meta.Function,
["fn"] = Meta.Function,
["function"] = Meta.Function,
["n"] = Meta.Number,
["num"] = Meta.Number,
["number"] = Meta.Number,
["nil"] = Meta.Nil,
["s"] = Meta.String,
["str"] = Meta.String,
["string"] = Meta.String,
["t"] = Meta.Table,
["tbl"] = Meta.Table,
["table"] = Meta.Table,
}
---@param meta_like cave.MetaLike
---@return cave.Meta
function Meta.like(meta_like)
local meta_like_type = type(meta_like)
local meta
if meta_like_type == "string" then
meta = meta_aliases[meta_like]
else
assert(meta_like_type == "table")
local mt = getmetatable(meta_like)
if mt == Meta then
meta = meta_like
elseif meta_like.__meta ~= nil then
meta = meta_like.__meta
elseif vim.islist(meta_like) then
meta = Meta.Union(unpack(meta_like))
else
assert(false)
end
end
assert(getmetatable(meta) == Meta)
return meta
end
---@return cave.Validator
---@return string
function Meta:check() return self.valid, self.repr end
---@param params table<string,table>
function Meta.validate(params)
local vim_params = {}
for param_name, param_specs in pairs(params) do
assert(type(param_name) == "string", "param name is not a string")
assert(type(param_specs) == "table", "param spec for '" .. param_name .. "' is not a table")
assert(#param_specs == 2, "param spec length for '" .. param_name .. "' is not 2")
local param_value = param_specs[1] --[[@as any]]
local param_meta_like = param_specs[2] --[[@as cave.MetaLike]]
local param_meta = Meta.like(param_meta_like)
vim_params[param_name] = { param_value, param_meta:check() }
end
vim.validate(vim_params)
end
---@param name string
---@param base_mt table?
---@return table
function Meta.derive(name, base_mt)
Meta.validate { name = { name, Meta.String }, base_mt = { base_mt, Meta.Optional(Meta.Table) } }
local mt = {}
mt.__index = mt
mt.__meta = Meta.Class(mt, name)
return (base_mt and setmetatable(mt, base_mt)) or mt
end
---@param obj table
---@return string
function Meta.get_repr(obj)
Meta.validate { obj = { obj, Meta.Table } }
local mt = getmetatable(obj)
Meta.validate { mt = { mt, Meta.Table } }
local meta = mt.__meta
Meta.validate { meta = { meta, Meta.Table } }
local meta_mt = getmetatable(obj)
assert(meta_mt == Meta)
---@cast meta cave.Meta
return meta.repr
end
return Meta