---@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 ---@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.tbl_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 ---@param name string ---@return cave.Meta function Meta.MapLiteral(meta_like_tbl, name) ---@type table 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.tbl_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 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