mirror of
https://github.com/neovim/neovim.git
synced 2025-10-26 12:27:24 +00:00
feat(vim.validate): improve fast form and deprecate spec form
Problem: `vim.validate()` takes two forms when it only needs one. Solution: - Teach the fast form all the features of the spec form. - Deprecate the spec form. - General optimizations for both forms. - Add a `message` argument which can be used alongside or in place of the `optional` argument.
This commit is contained in:
committed by
Lewis Russell
parent
6fd13eedda
commit
3572319b4c
@@ -251,7 +251,8 @@ end
|
||||
---@param t table<any, T> Table
|
||||
---@return table : Table of transformed values
|
||||
function vim.tbl_map(func, t)
|
||||
vim.validate({ func = { func, 'c' }, t = { t, 't' } })
|
||||
vim.validate('func', func, 'callable')
|
||||
vim.validate('t', t, 'table')
|
||||
--- @cast t table<any,any>
|
||||
|
||||
local rettab = {} --- @type table<any,any>
|
||||
@@ -268,7 +269,8 @@ end
|
||||
---@param t table<any, T> (table) Table
|
||||
---@return T[] : Table of filtered values
|
||||
function vim.tbl_filter(func, t)
|
||||
vim.validate({ func = { func, 'c' }, t = { t, 't' } })
|
||||
vim.validate('func', func, 'callable')
|
||||
vim.validate('t', t, 'table')
|
||||
--- @cast t table<any,any>
|
||||
|
||||
local rettab = {} --- @type table<any,any>
|
||||
@@ -311,7 +313,7 @@ function vim.tbl_contains(t, value, opts)
|
||||
|
||||
local pred --- @type fun(v: any): boolean?
|
||||
if opts and opts.predicate then
|
||||
vim.validate({ value = { value, 'c' } })
|
||||
vim.validate('value', value, 'callable')
|
||||
pred = value
|
||||
else
|
||||
pred = function(v)
|
||||
@@ -779,237 +781,226 @@ function vim.endswith(s, suffix)
|
||||
end
|
||||
|
||||
do
|
||||
--- @alias vim.validate.LuaType
|
||||
--- | 'nil'
|
||||
--- | 'number'
|
||||
--- | 'string'
|
||||
--- | 'boolean'
|
||||
--- | 'table'
|
||||
--- | 'function'
|
||||
--- | 'thread'
|
||||
--- | 'userdata'
|
||||
---
|
||||
--- @alias vim.validate.Type vim.validate.LuaType | 't' | 's' | 'n' | 'f' | 'c'
|
||||
--- @alias vim.validate.Validator
|
||||
--- | type|'callable'
|
||||
--- | (type|'callable')[]
|
||||
--- | fun(v:any):boolean, string?
|
||||
|
||||
local type_names = {
|
||||
['table'] = 'table',
|
||||
t = 'table',
|
||||
['string'] = 'string',
|
||||
s = 'string',
|
||||
['number'] = 'number',
|
||||
n = 'number',
|
||||
['boolean'] = 'boolean',
|
||||
local type_aliases = {
|
||||
b = 'boolean',
|
||||
['function'] = 'function',
|
||||
f = 'function',
|
||||
['callable'] = 'callable',
|
||||
c = 'callable',
|
||||
['nil'] = 'nil',
|
||||
['thread'] = 'thread',
|
||||
['userdata'] = 'userdata',
|
||||
f = 'function',
|
||||
n = 'number',
|
||||
s = 'string',
|
||||
t = 'table',
|
||||
}
|
||||
|
||||
--- @nodoc
|
||||
--- @class vim.validate.Spec [any, string|string[], boolean]
|
||||
--- @class vim.validate.Spec
|
||||
--- @field [1] any Argument value
|
||||
--- @field [2] vim.validate.Type|vim.validate.Type[]|fun(v:any):boolean, string? Type name, or callable
|
||||
--- @field [3]? boolean
|
||||
--- @field [2] vim.validate.Validator Argument validator
|
||||
--- @field [3]? boolean|string Optional flag or error message
|
||||
|
||||
local function _is_type(val, t)
|
||||
local function is_type(val, t)
|
||||
return type(val) == t or (t == 'callable' and vim.is_callable(val))
|
||||
end
|
||||
|
||||
--- @param param_name string
|
||||
--- @param spec vim.validate.Spec
|
||||
--- @param val any
|
||||
--- @param validator vim.validate.Validator
|
||||
--- @param message? string
|
||||
--- @param allow_alias? boolean Allow short type names: 'n', 's', 't', 'b', 'f', 'c'
|
||||
--- @return string?
|
||||
local function is_param_valid(param_name, spec)
|
||||
if type(spec) ~= 'table' then
|
||||
return string.format('opt[%s]: expected table, got %s', param_name, type(spec))
|
||||
end
|
||||
local function is_valid(param_name, val, validator, message, allow_alias)
|
||||
if type(validator) == 'string' then
|
||||
local expected = allow_alias and type_aliases[validator] or validator
|
||||
|
||||
local val = spec[1] -- Argument value
|
||||
local types = spec[2] -- Type name, or callable
|
||||
local optional = (true == spec[3])
|
||||
if not expected then
|
||||
return string.format('invalid type name: %s', validator)
|
||||
end
|
||||
|
||||
if type(types) == 'string' then
|
||||
types = { types }
|
||||
end
|
||||
|
||||
if vim.is_callable(types) then
|
||||
if not is_type(val, expected) then
|
||||
return string.format('%s: expected %s, got %s', param_name, expected, type(val))
|
||||
end
|
||||
elseif vim.is_callable(validator) then
|
||||
-- Check user-provided validation function
|
||||
local valid, optional_message = types(val)
|
||||
local valid, opt_msg = validator(val)
|
||||
if not valid then
|
||||
local error_message =
|
||||
string.format('%s: expected %s, got %s', param_name, (spec[3] or '?'), tostring(val))
|
||||
if optional_message ~= nil then
|
||||
error_message = string.format('%s. Info: %s', error_message, optional_message)
|
||||
local err_msg =
|
||||
string.format('%s: expected %s, got %s', param_name, message or '?', tostring(val))
|
||||
|
||||
if opt_msg then
|
||||
err_msg = string.format('%s. Info: %s', err_msg, opt_msg)
|
||||
end
|
||||
|
||||
return error_message
|
||||
return err_msg
|
||||
end
|
||||
elseif type(types) == 'table' then
|
||||
local success = false
|
||||
for i, t in ipairs(types) do
|
||||
local t_name = type_names[t]
|
||||
if not t_name then
|
||||
elseif type(validator) == 'table' then
|
||||
for _, t in ipairs(validator) do
|
||||
local expected = allow_alias and type_aliases[t] or t
|
||||
if not expected then
|
||||
return string.format('invalid type name: %s', t)
|
||||
end
|
||||
types[i] = t_name
|
||||
|
||||
if (optional and val == nil) or _is_type(val, t_name) then
|
||||
success = true
|
||||
break
|
||||
if is_type(val, expected) then
|
||||
return -- success
|
||||
end
|
||||
end
|
||||
if not success then
|
||||
return string.format(
|
||||
'%s: expected %s, got %s',
|
||||
param_name,
|
||||
table.concat(types, '|'),
|
||||
type(val)
|
||||
)
|
||||
|
||||
-- Normalize validator types for error message
|
||||
if allow_alias then
|
||||
for i, t in ipairs(validator) do
|
||||
validator[i] = type_aliases[t] or t
|
||||
end
|
||||
end
|
||||
|
||||
return string.format(
|
||||
'%s: expected %s, got %s',
|
||||
param_name,
|
||||
table.concat(validator, '|'),
|
||||
type(val)
|
||||
)
|
||||
else
|
||||
return string.format('invalid type name: %s', tostring(types))
|
||||
return string.format('invalid validator: %s', tostring(validator))
|
||||
end
|
||||
end
|
||||
|
||||
--- @param opt table<vim.validate.Type,vim.validate.Spec>
|
||||
--- @return boolean, string?
|
||||
local function is_valid(opt)
|
||||
if type(opt) ~= 'table' then
|
||||
return false, string.format('opt: expected table, got %s', type(opt))
|
||||
end
|
||||
|
||||
--- @param opt table<type|'callable',vim.validate.Spec>
|
||||
--- @return string?
|
||||
local function validate_spec(opt)
|
||||
local report --- @type table<string,string>?
|
||||
|
||||
for param_name, spec in pairs(opt) do
|
||||
local msg = is_param_valid(param_name, spec)
|
||||
if msg then
|
||||
local err_msg --- @type string?
|
||||
if type(spec) ~= 'table' then
|
||||
err_msg = string.format('opt[%s]: expected table, got %s', param_name, type(spec))
|
||||
else
|
||||
local value, validator = spec[1], spec[2]
|
||||
local msg = type(spec[3]) == 'string' and spec[3] or nil --[[@as string?]]
|
||||
local optional = spec[3] == true
|
||||
if not (optional and value == nil) then
|
||||
err_msg = is_valid(param_name, value, validator, msg, true)
|
||||
end
|
||||
end
|
||||
|
||||
if err_msg then
|
||||
report = report or {}
|
||||
report[param_name] = msg
|
||||
report[param_name] = err_msg
|
||||
end
|
||||
end
|
||||
|
||||
if report then
|
||||
for _, msg in vim.spairs(report) do -- luacheck: ignore
|
||||
return false, msg
|
||||
return msg
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
--- Validate function arguments.
|
||||
---
|
||||
--- This function has two valid forms:
|
||||
---
|
||||
--- 1. vim.validate(name: str, value: any, type: string, optional?: bool)
|
||||
--- 2. vim.validate(spec: table)
|
||||
--- 1. `vim.validate(name, value, validator[, optional][, message])`
|
||||
---
|
||||
--- Form 1 validates that argument {name} with value {value} has the type
|
||||
--- {type}. {type} must be a value returned by |lua-type()|. If {optional} is
|
||||
--- true, then {value} may be null. This form is significantly faster and
|
||||
--- should be preferred for simple cases.
|
||||
--- Validates that argument {name} with value {value} satisfies
|
||||
--- {validator}. If {optional} is given and is `true`, then {value} may be
|
||||
--- `nil`. If {message} is given, then it is used as the expected type in the
|
||||
--- error message.
|
||||
---
|
||||
--- Example:
|
||||
--- Example:
|
||||
---
|
||||
--- ```lua
|
||||
--- function vim.startswith(s, prefix)
|
||||
--- vim.validate('s', s, 'string')
|
||||
--- vim.validate('prefix', prefix, 'string')
|
||||
--- ...
|
||||
--- end
|
||||
--- ```
|
||||
--- ```lua
|
||||
--- function vim.startswith(s, prefix)
|
||||
--- vim.validate('s', s, 'string')
|
||||
--- vim.validate('prefix', prefix, 'string')
|
||||
--- ...
|
||||
--- end
|
||||
--- ```
|
||||
---
|
||||
--- Form 2 validates a parameter specification (types and values). Specs are
|
||||
--- evaluated in alphanumeric order, until the first failure.
|
||||
--- 2. `vim.validate(spec)` (deprecated)
|
||||
--- where `spec` is of type
|
||||
--- `table<string,[value:any, validator: vim.validate.Validator, optional_or_msg? : boolean|string]>)`
|
||||
---
|
||||
--- Usage example:
|
||||
--- Validates a argument specification.
|
||||
--- Specs are evaluated in alphanumeric order, until the first failure.
|
||||
---
|
||||
--- ```lua
|
||||
--- function user.new(name, age, hobbies)
|
||||
--- vim.validate{
|
||||
--- name={name, 'string'},
|
||||
--- age={age, 'number'},
|
||||
--- hobbies={hobbies, 'table'},
|
||||
--- }
|
||||
--- ...
|
||||
--- end
|
||||
--- ```
|
||||
--- Example:
|
||||
---
|
||||
--- ```lua
|
||||
--- function user.new(name, age, hobbies)
|
||||
--- vim.validate{
|
||||
--- name={name, 'string'},
|
||||
--- age={age, 'number'},
|
||||
--- hobbies={hobbies, 'table'},
|
||||
--- }
|
||||
--- ...
|
||||
--- end
|
||||
--- ```
|
||||
---
|
||||
--- Examples with explicit argument values (can be run directly):
|
||||
---
|
||||
--- ```lua
|
||||
--- vim.validate{arg1={{'foo'}, 'table'}, arg2={'foo', 'string'}}
|
||||
--- vim.validate('arg1', {'foo'}, 'table')
|
||||
--- --> NOP (success)
|
||||
--- vim.validate('arg2', 'foo', 'string')
|
||||
--- --> NOP (success)
|
||||
---
|
||||
--- vim.validate{arg1={1, 'table'}}
|
||||
--- vim.validate('arg1', 1, 'table')
|
||||
--- --> error('arg1: expected table, got number')
|
||||
---
|
||||
--- vim.validate{arg1={3, function(a) return (a % 2) == 0 end, 'even number'}}
|
||||
--- vim.validate('arg1', 3, function(a) return (a % 2) == 0 end, 'even number')
|
||||
--- --> error('arg1: expected even number, got 3')
|
||||
--- ```
|
||||
---
|
||||
--- If multiple types are valid they can be given as a list.
|
||||
---
|
||||
--- ```lua
|
||||
--- vim.validate{arg1={{'foo'}, {'table', 'string'}}, arg2={'foo', {'table', 'string'}}}
|
||||
--- vim.validate('arg1', {'foo'}, {'table', 'string'})
|
||||
--- vim.validate('arg2', 'foo', {'table', 'string'})
|
||||
--- -- NOP (success)
|
||||
---
|
||||
--- vim.validate{arg1={1, {'string', 'table'}}}
|
||||
--- vim.validate('arg1', 1, {'string', 'table'})
|
||||
--- -- error('arg1: expected string|table, got number')
|
||||
--- ```
|
||||
---
|
||||
---@param opt table<vim.validate.Type,vim.validate.Spec> (table) Names of parameters to validate. Each key is a parameter
|
||||
--- name; each value is a tuple in one of these forms:
|
||||
--- 1. (arg_value, type_name, optional)
|
||||
--- - arg_value: argument value
|
||||
--- - type_name: string|table type name, one of: ("table", "t", "string",
|
||||
--- "s", "number", "n", "boolean", "b", "function", "f", "nil",
|
||||
--- "thread", "userdata") or list of them.
|
||||
--- - optional: (optional) boolean, if true, `nil` is valid
|
||||
--- 2. (arg_value, fn, msg)
|
||||
--- - arg_value: argument value
|
||||
--- - fn: any function accepting one argument, returns true if and
|
||||
--- only if the argument is valid. Can optionally return an additional
|
||||
--- informative error message as the second returned value.
|
||||
--- - msg: (optional) error string if validation fails
|
||||
--- @overload fun(name: string, val: any, expected: vim.validate.LuaType, optional?: boolean)
|
||||
function vim.validate(opt, ...)
|
||||
local ok = false
|
||||
local err_msg ---@type string?
|
||||
local narg = select('#', ...)
|
||||
if narg == 0 then
|
||||
ok, err_msg = is_valid(opt)
|
||||
elseif narg >= 2 then
|
||||
-- Overloaded signature for fast/simple cases
|
||||
local name = opt --[[@as string]]
|
||||
local v, expected, optional = ... ---@type string, string, boolean?
|
||||
local actual = type(v)
|
||||
|
||||
ok = (actual == expected) or (v == nil and optional == true)
|
||||
--- @note `validator` set to a value returned by |lua-type()| provides the
|
||||
--- best performance.
|
||||
---
|
||||
--- @param name string Argument name
|
||||
--- @param value string Argument value
|
||||
--- @param validator vim.validate.Validator
|
||||
--- - (`string|string[]`): Any value that can be returned from |lua-type()| in addition to
|
||||
--- `'callable'`: `'boolean'`, `'callable'`, `'function'`, `'nil'`, `'number'`, `'string'`, `'table'`,
|
||||
--- `'thread'`, `'userdata'`.
|
||||
--- - (`fun(val:any): boolean, string?`) A function that returns a boolean and an optional
|
||||
--- string message.
|
||||
--- @param optional? boolean Argument is optional (may be omitted)
|
||||
--- @param message? string message when validation fails
|
||||
--- @overload fun(name: string, val: any, validator: vim.validate.Validator, message: string)
|
||||
--- @overload fun(spec: table<string,[any, vim.validate.Validator, boolean|string]>)
|
||||
function vim.validate(name, value, validator, optional, message)
|
||||
local err_msg --- @type string?
|
||||
if validator then -- Form 1
|
||||
-- Check validator as a string first to optimize the common case.
|
||||
local ok = (type(value) == validator) or (value == nil and optional == true)
|
||||
if not ok then
|
||||
if not jit and (actual ~= 'string' or actual ~= 'number') then
|
||||
-- PUC-Lua can only handle string and number for %s in string.format()
|
||||
v = vim.inspect(v)
|
||||
end
|
||||
err_msg = ('%s: expected %s, got %s%s'):format(
|
||||
name,
|
||||
expected,
|
||||
actual,
|
||||
v and (' (%s)'):format(v) or ''
|
||||
)
|
||||
local msg = type(optional) == 'string' and optional or message --[[@as string?]]
|
||||
-- Check more complicated validators
|
||||
err_msg = is_valid(name, value, validator, msg, false)
|
||||
end
|
||||
elseif type(name) == 'table' then -- Form 2
|
||||
vim.deprecate('vim.validate', 'vim.validate(name, value, validator, optional_or_msg)', '1.0')
|
||||
err_msg = validate_spec(name)
|
||||
else
|
||||
error('invalid arguments')
|
||||
end
|
||||
|
||||
if not ok then
|
||||
if err_msg then
|
||||
error(err_msg, 2)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Returns true if object `f` can be called as a function.
|
||||
---
|
||||
---@param f any Any object
|
||||
|
||||
Reference in New Issue
Block a user