Created
April 15, 2014 17:13
-
-
Save mpeterv/10748836 to your computer and use it in GitHub Desktop.
Pure Lua implementation of checks
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--[[ | |
Argument type checking API. | |
This library declares a `checks()` function and a `checkers` table, which | |
allow to check the parameters passed to a Lua function in a fast and | |
unobtrusive way. | |
`checks (type_1, ..., type_n)`, when called directly inside function | |
`f`, checks that `f`'s 1st argument conforms to `type_1`, that its 2nd | |
argument conforms to `type_2`, etc. until `type_n`. Type specifiers | |
are strings, and if the arguments passed to `f` don't conform to their | |
specification, a proper error message is produced, pinpointing the | |
call to `f` as the faulty expression. | |
Each type description `type_n` must be a string, and can describe: | |
* the Lua type of an object, such as `"table"`, `"number"` etc.; | |
* an arbitrary name, which would be stored in the `__type` field of | |
the argument's metatable; | |
* a type-checking function, which would be stored in the `checkers` | |
global table. This table uses type names as keys, test functions | |
returning Booleans as keys. | |
Moreover, types can be prefixed with a `"?"`, which makes them | |
optional. For instance, `"?table"` accepts tables as well as `nil` | |
values. | |
A `"?"` alone accepts anything. It is mainly useful as a placeholder, | |
to skip an argument which doesn't need to be checked. | |
Finally, several types can be accepted, if their names are | |
concatenated with a bar `"|"` between them. For instance, | |
`"table|number"` accepts tables as well as numbers. It can be combined | |
with the question mark, so `"?table|number"` accepts tables, numbers | |
and nil values. It is actually equivalent to `"nil|table|number"`. | |
More formally, let's specify `conform(a, t)`, the property that | |
argument `a` conforms to the type denoted by `t`. `conform(a,t)` is | |
true if and only if at least one of the following propositions is | |
verified: | |
* `conforms(a, t:match "^(.-)|.*"` | |
* `t == "?"` | |
* `t:sub(1, 1) == "?" and (conforms(a, t:sub(2, -1)) or a==nil)` | |
* `type(a) == t` | |
* `getmetatable(a) and getmetatable(a).__type == t` | |
* `checkers[t] and checkers[t](a) is true` | |
* `conforms(a, t:match "^.-|(.*)")` | |
The above propositions are listed in the order in which they are | |
tried by `check`. The higher they appear in the list, the faster | |
`checks` accepts aconforming argument. For instance, | |
`checks("number")` is faster than | |
`checkers.mynumber=function(x) return type(x)=="number" end; checks("mynumber")`. | |
Usage examples | |
-------------- | |
require 'checks' | |
-- Custom checker function -- | |
function checkers.port(p) | |
return type(p)=='number' and p>0 and p<0x10000 | |
end | |
-- A new named type -- | |
socket_mt = { __type='socket' } | |
asocket = setmetatable ({ }, socket_mt) | |
-- A function that checks its parameters -- | |
function take_socket_then_port_then_maybe_string (sock, port, str) | |
checks ('socket', 'port', '?string') | |
end | |
take_socket_then_port_then_maybe_string (asocket, 1024, "hello") | |
take_socket_then_port_then_maybe_string (asocket, 1024) | |
-- A couple of other parameter-checking options -- | |
function take_number_or_string() | |
checks("number|string") | |
end | |
function take_number_or_string_or_nil() | |
checks("?number|string") | |
end | |
function take_anything_followed_by_a_number() | |
checks("?", "number") | |
end | |
-- Catch some incorrect arguments passed to the function -- | |
function must_fail(...) | |
assert (not pcall (take_socket_then_port_then_maybe_string, ...)) | |
end | |
must_fail ({ }, 1024, "string") -- 1st argument isn't a socket | |
must_fail (asocket, -1, "string") -- port number must be 0-0xffff | |
must_fail (asocket, 1024, { }) -- 3rd argument cannot be a table | |
]] | |
-- Table for custom checkers. | |
local checkers = {} | |
-- Generate and throw an error. | |
local function raiseError(level, narg, expected, got) | |
local funcName = debug.getinfo(level + 1, "n").name | |
local info = debug.getinfo(level + 2, "Sl") | |
return error(("%s:%d: bad argument #%d to %s (%s expected, got %s)"):format( | |
info.short_src, info.currentline, narg, funcName, expected, got | |
), 0) | |
end | |
--- Return true iff actual_type occurs in expected_types, the later being | |
-- a list of type names separate by '|' chars. | |
local function matches(actualType, expectedTypes) | |
for expectedType in expectedTypes:gmatch("[^|]+") do | |
if actualType == expectedType then | |
return true | |
end | |
end | |
end | |
local function checks(...) | |
local level = 2 -- outer function | |
local i = 1 -- starting argument index | |
local first = select(1, ...) | |
if type(first) == "number" then | |
level = first | |
i = 2 -- skip the level argument | |
end | |
assert(debug.getinfo(level, ""), "checks() must be called within a Lua function") | |
local total = select("#", ...) | |
while i <= total do | |
local expectedType = select(i, ...) | |
local _, value = debug.getlocal(level, i) | |
-- 1. Check whether the argument should be inspected. | |
if expectedType ~= "?" then | |
-- 2. Check whether the type definition uses short notation for nil. | |
if not (expectedType:sub(1, 1) == "?" and value == nil) then | |
expectedType = expectedType:gsub("^%?", "") | |
-- 3. Check actual type. | |
local actualType = type(value) | |
if not matches(actualType, expectedType) then | |
-- 4. Check type name in metatable. | |
local mt = debug.getmetatable(value) | |
if not (mt and mt.__type and matches(mt.__type, expectedType)) then | |
-- 5. Check custom typechecking functions. | |
local checkers = debug.getregistry().checkers | |
local ok, result | |
for checkerName in expectedType:gmatch("[^|]+") do | |
local checker = checkers[checkerName] | |
if checker and type(checker) == "function" then | |
ok, result = pcall(checker, value) | |
if ok and result then | |
break | |
end | |
end | |
end | |
if not (ok and result) then | |
raiseError(level, i, expectedType, actualType) | |
end | |
end | |
end | |
end | |
end | |
i = i + 1 | |
end | |
end | |
-- Export table of custom checkers and the `check` function. | |
_G.checkers = checkers | |
debug.getregistry().checkers = checkers -- Is it necessary to use the registry? | |
_G.checks = checks | |
return checks |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment