validator.lua
-- Coypright 2015-2017 Stefan Göbel. -- -- This file is part of Corky. -- -- Corky is free software: you can redistribute it and/or modify it under the terms of the GNU General Public -- License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any -- later version. -- -- Corky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied -- warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more -- details. -- -- You should have received a copy of the GNU General Public License along with Corky. If not, see -- <http://www.gnu.org/licenses/>. ------------------------------------------------------------------------------------------------------------------- --- Parameter validation for *Corky*. -- -- This class provides methods for validating a value. -- -- Usage example: -- -- -- The module will return the constructor: -- local validator = require "corky.validator" -- -- -- Create a new validator instance for a value: -- local instance = validator (some_value) -- -- -- Validate it (the value must pass all tests): -- instance.number ( ) -- instance.less_than ( 10) -- instance.greater_than (-10) -- instance.not_equals ( 0) -- -- -- Get the value and the validation result: -- local valid, value = instance.get () -- -- A short form exists, too. You can write the code above like this: -- -- -- Get the constructor: -- local v = require "corky.validator" -- -- -- Same validations as above, plus getting the results, in one line: -- local valid, value = v (some_value) . n . lt (10) . gt (-10) . ne (0) . get () -- -- -- Same as above, using the "constructor" instead of get(): -- local valid, value = v{v (some_value) . n . lt (10) . gt (-10) . ne (0) } -- -- **General information:** -- -- A validator instance basically contains a *value* and a validation result. -- -- The first parameter passed to the constructor specifies the (initial) value of the -- *value*. The second (optional) parameter specifies the initial value of the validation result. If it is not -- specified, it defaults to `true`. -- -- The validation methods may change both the *value* and the validation result, see the description of the -- individual methods for details. -- -- If a validation fails (and thus the validation result is set to `false`), this validation result is final! -- Actual validation (or conversion) code will be skipped after that, no matter what methods are called. This also -- means that if the initial validation state is set to `false` via the constructor, no validation/conversion code -- will be run for that instance at all. -- -- The final result (i.e. the final, possibly modified *value*), and the validation result (either `true` or -- `false`) may be retrieved using the get() method, or by calling the -- "constructor" function with one parameter, which must be an array with one element, -- which must be the validator instance: -- -- local instance = validator (some_value) -- -- -- Validation method calls omitted… -- -- local valid, value = validator ({ instance }) -- -- Parentheses may be omitted in this case: -- -- local valid, value = validator { instance } -- -- Methods may be called one after the other as multiple statements, as shown in the very first example above. In -- that case, method notation, i.e. `instance.<method> ()`, is required, no matter if a method expects parameters -- or not. -- -- Validation and conversion methods return the instance, and thus methods may also be chained. For example: -- -- instance.number ().less_than (10) -- -- In that case, parentheses may be ommitted for methods that do not expect any parameters (unless the method is -- the last one in the statement): -- -- instance.number.less_than (10) -- -- For most methods there are short aliases defined, e.g. the number() method may also be -- called as `num` or `n`. All these features combined allow the short format mentioned above (with `v` being the -- "constructor"): -- -- local valid, value = v{v (some_value) . n . lt (10) . gt (-10) . ne (0) } -- -- Methods are named after the condition the *value* has to fulfill to pass the validation, e.g. the validation -- -- instance.number.less_than (10).not_equal (0) -- -- will only succeed if the *value* is a number (or a value that can be converted to a number), and if the value -- is less than `10`, and if the value is not `0`. -- -- To only retrieve the (possibly modified) *value* or the validation result, the two properties `value` and -- `valid` may be used. These are not methods: -- -- local value = instance.value -- local valid = instance.valid -- -- As a shortcut to get the validation result, the unary minus operator may be used: -- -- local valid = - instance -- -- This allows for omitting the outer "constructor" call, for example if the validation is -- done in an `if` statement: -- -- if - v (some_value) . n . lt (10) . ne (0) then -- -- Do some stuff if some_value is valid… -- end -- -- With Lua 5.2 or later, the length operator (`#`) may be used as a shortcut to get the *value*: -- -- local value = # instance -- -- @MO corky.validator -- @CO © 2015-2017 Stefan Göbel -- @RE 2017033001 -- @LI [GPLv3](http://www.gnu.org/copyleft/gpl.html) -- @AU Stefan Göbel [[⌂]](http://subtype.de/) [[✉]](mailto:corky@subtype.de) -- @AL methods -- @SE sort=false local function validator (value, valid) ---------------------------------------------------------------------------------------------------------------- local self = {} local methods = {} local metatable = {} local done = false local skip_next = 0 local last_func = nil local pack = table.pack if valid == nil then valid = true end ---------------------------------------------------------------------------------------------------------------- if not pack then pack = function (...) return { n = select ("#", ...), ... } end end ---------------------------------------------------------------------------------------------------------------- local function exec_last (...) if last_func then last_func (...) last_func = nil end end ---------------------------------------------------------------------------------------------------------------- function metatable.__call (t, ...) exec_last (...) return self end ---------------------------------------------------------------------------------------------------------------- --- Properties -- -- @section properties --- The following list shows the instance properties that are available. Note that these are read-only, trying -- to set them will be silently ignored. -- -- @table instance -- @field valid The current validation result, either `true` or `false`. -- @field value The current validation *value*. ---------------------------------------------------------------------------------------------------------------- --- Methods -- -- @section methods --- Get the validation status and the *value*. -- -- @function get -- @r The validation result, either `true` or `false`. -- @r The current *value*. function metatable.__index (t, k) exec_last () if k == "get" then return function () return valid, value end elseif k == "value" then return value elseif k == "valid" then return valid elseif not valid or done then return self elseif methods [k] then if skip_next > 0 then skip_next = skip_next - 1 else last_func = methods [k] end return self end end ---------------------------------------------------------------------------------------------------------------- function metatable.__unm (t) exec_last () return valid end ---------------------------------------------------------------------------------------------------------------- function metatable.__len (t) exec_last () return value end ---------------------------------------------------------------------------------------------------------------- function metatable.__newindex (t, k, v) end ---------------------------------------------------------------------------------------------------------------- metatable.__metatable = "validator" ---------------------------------------------------------------------------------------------------------------- --- General -- -- @section general ---------------------------------------------------------------------------------------------------------------- --- Check if a required *value* is `nil`. -- -- If the *value* is `nil`, it will be marked as invalid. -- -- **Aliases:** `r`, `req` function methods.required () if value == nil then valid = false end end methods.req = methods.required methods.r = methods.required ---------------------------------------------------------------------------------------------------------------- --- Check if an optional *value* is `nil`. -- -- If the *value* is `nil`, it will be considered valid, but all following validation methods will be skipped -- (the result is final). -- -- **Aliases:** `o`, `opt` function methods.optional () if value == nil then done = true end end methods.opt = methods.optional methods.o = methods.optional ---------------------------------------------------------------------------------------------------------------- --- Set a default value. -- -- **This method will modify the *value*!** -- -- If the *value* is `nil`, it will be set to the specified default value. This will be considered valid. All -- following validation methods will be skipped (the result is final). -- -- **Aliases:** `dv`, `def` -- -- @p default The default value. function methods.default_value (default) if value == nil then value = default done = true end end methods.def = methods.default_value methods.dv = methods.default_value ---------------------------------------------------------------------------------------------------------------- --- Changes the *value*. -- -- **This method will modify the *value*!** -- -- **This method will not modify the validation result.** -- -- Changes the *value* to the value specified by the parameter. -- -- **Aliases:** `ch` -- -- @p new_value The new value. function methods.change_value (new_value) value = new_value end methods.ch = methods.change_value ---------------------------------------------------------------------------------------------------------------- --- Check the type of the *value*. -- -- If the type of the *value* matches one of the parameters, the validation passes. Else the *value* will be -- marked as invalid. -- -- **Aliases:** `t` -- -- @p ... An arbitrary number of strings specifying the allowed types for the *value*. Valid type names are -- `"nil"`, `"boolean"`, `"number"`, `"string"`, `"userdata"`, `"function"`, `"thread"`, and `"table"`. function methods.type (...) local t = type (value) local p = pack (...) for i = 1, p.n do if t == p [i] then return end end valid = false end methods.t = methods.type ---------------------------------------------------------------------------------------------------------------- --- Call an external validation function. -- -- The first parameter must be a function. It will be called with the *value* as the first parameter, all -- remaining parameters will be passed to the function after the *value*. The return value of the function will -- be used in boolean context, if it evaluates to `false`, e.g. if it is `false` or `nil`, the *value* will be -- marked as invalid. -- -- **Aliases:** `x`, `ext` -- -- @f func The function to call. -- @p ... Additional parameters to pass to the function. function methods.external (func, ...) if not func (value, ...) then valid = false end end methods.ext = methods.external methods.x = methods.external ---------------------------------------------------------------------------------------------------------------- --- Check if the *value* equals one of the parameters. -- -- An arbitrary number of parameters may be specified. If the *value* matches one of these, the validation -- passes, if it does not match one of the parameters, it will be marked as invalid. -- -- Note: If called without any parameters the *value* will be marked as invalid! -- -- **Aliases:** `e`, `eq` -- -- @p ... An arbitrary number of arbitrary values against which the *value* will be checked. function methods.equal (...) local p = pack (...) for i = 1, p.n do if value == p [i] then return end end valid = false end methods.eq = methods.equal methods.e = methods.equal ---------------------------------------------------------------------------------------------------------------- --- Check if the *value* does not equal the parameter. -- -- If the *value* equals the parameter, it will be marked as invalid. -- -- **Aliases:** `ne`, `neq` -- -- @p match An arbitrary value to check against. function methods.not_equal (match) if value == match then valid = false end end methods.neq = methods.not_equal methods.ne = methods.not_equal ---------------------------------------------------------------------------------------------------------------- --- Numbers -- -- @section numbers ---------------------------------------------------------------------------------------------------------------- --- Check if the *value* can be converted to a number. -- -- **This method will modify the *value*!** -- -- This method will convert the *value* to a number (by calling tonumber()). If this fails, the -- *value* will be marked as invalid. -- -- Note: For a type check without conversion the type() method may be used. To check if the -- *value* can be converted to a number without converting it the external() method may -- be used with tonumber as parameter. -- -- **Aliases:** `n`, `num` -- -- @n[opt] base Will be passed to tonumber(), see there for more details. function methods.number (base) value = tonumber (value, base) if not value then valid = false end end methods.num = methods.number methods.n = methods.number ---------------------------------------------------------------------------------------------------------------- --- Check if the *value* is in the specified range. -- -- This method will mark the *value* as invalid if it is less than the specified minimum or greater than the -- specified maximum value. -- -- **Aliases:** `ir`, `range` -- -- @n min The minimum value. -- @n max The maximum value. function methods.in_range (min, max) if value < min or value > max then valid = false end end methods.range = methods.in_range methods.ir = methods.in_range ---------------------------------------------------------------------------------------------------------------- --- Check if the *value* is greater than the parameter. -- -- If the *value* is greater than the parameter, the validation passes, else it will be marked as invalid. -- -- **Aliases:** `gt` -- -- @n number The value to compare. function methods.greater_than (number) if value <= number then valid = false end end methods.gt = methods.greater_than ---------------------------------------------------------------------------------------------------------------- --- Check if the *value* is less than the parameter. -- -- If the *value* is less than the parameter, the validation passes, else it will be marked as invalid. -- -- **Aliases:** `lt` -- -- @n number The value to compare. function methods.less_than (number) if value >= number then valid = false end end methods.lt = methods.less_than ---------------------------------------------------------------------------------------------------------------- --- Check if the *value* is greater than or equal to the parameter. -- -- If the *value* is greater than or equal to the parameter, the validation passes, else it will be marked as -- invalid. -- -- **Aliases:** `ge` -- -- @n number The value to compare. function methods.greater_or_equal (number) if value < number then valid = false end end methods.ge = methods.greater_or_equal ---------------------------------------------------------------------------------------------------------------- --- Check if the *value* is less than or equal to the parameter. -- -- If the *value* is less than or equal to the parameter, the validation passes, else it will be marked as -- invalid. -- -- **Aliases:** `le` -- -- @n number The value to compare. function methods.less_or_equal (number) if value > number then valid = false end end methods.le = methods.less_or_equal ---------------------------------------------------------------------------------------------------------------- --- Check if the *value* is divisible by one of the parameters. -- -- If the *value* is divisible by one of the parameters, the check passes, else it will be marked as invalid. -- -- **Aliases:** `d`, `div` -- -- @p ... An arbitrary number of numbers to check against. function methods.divisible (...) local p = pack (...) for i = 1, p.n do if value % p [i] == 0 then return end end valid = false end methods.div = methods.divisible methods.d = methods.divisible ---------------------------------------------------------------------------------------------------------------- --- Strings -- -- @section strings ---------------------------------------------------------------------------------------------------------------- --- Check if the *value* can be converted to a string. -- -- **This method will modify the *value*!** -- -- This method will convert the *value* to a string (by calling tostring()). If this fails, the -- *value* will be marked as invalid *[not sure if this can happen]*. -- -- Note: For a type check without conversion the type() method may be used. function methods.string () value = tostring (value) if not value then valid = false end end methods.str = methods.string methods.s = methods.string ---------------------------------------------------------------------------------------------------------------- --- Check if an optional *value* is an empty string. -- -- If the *value* is an empty string (`""`), it will be considered valid, and all following validation methods -- will be skipped (the result is final). -- -- **Aliases:** `oe`, `oem` function methods.optional_empty () if value == "" then done = true end end methods.oe = methods.optional_empty ---------------------------------------------------------------------------------------------------------------- --- Check for an empty string. -- -- If the *value* (which must be a string, this is not checked by this method) is empty (i.e. `""`), it will be -- marked as invalid. -- -- **Aliases:** `nm`, `nem` function methods.not_empty () if value == "" then valid = false end end methods.nem = methods.not_empty methods.nm = methods.not_empty ---------------------------------------------------------------------------------------------------------------- --- Set the *value* to `nil` if it is an empty string. -- -- **This method will modify the *value*!** -- -- **This method will not modify the validation result.** -- -- **Aliases:** `en`, `etn` function methods.empty_to_nil () if value == "" then value = nil end end methods.etn = methods.empty_to_nil methods.en = methods.empty_to_nil ---------------------------------------------------------------------------------------------------------------- --- Change the *value* to its string length. -- -- **This method will modify the *value*!** -- -- **This method will not modify the validation result.** -- -- string.len() will be called for the current *value*, and the value will be changed to the -- result. -- -- **Aliases:** `l`, `len` function methods.length () value = string.len (value) end methods.len = methods.length methods.l = methods.length ---------------------------------------------------------------------------------------------------------------- --- Conditional -- -- @section conditional ---------------------------------------------------------------------------------------------------------------- --- Skip the next check(s) if a condition is met. -- -- **This method will not modify the validation result.** -- -- The first parameter will be evaluated in boolean context. If it is a false value, the following validation -- methods will be skipped, if it is a true value nothing will be done and processing continues with the next -- method. The number of methods to skip may be specified using the second parameter, it must be an integer -- greater than or equal to `1`. It defaults to `1` if not specified. The `next_if()` method itself does not -- modify the validation result or the *value*. -- -- **Aliases:** `ni`, `nif` -- -- @b condition The condition, its value will be evaluated in boolean context. -- @i[opt] number The number of validations to skip. function methods.next_if (condition, number) if not number then number = 1 end if not condition then skip_next = skip_next + number end end methods.nif = methods.next_if methods.ni = methods.next_if --- @section end ---------------------------------------------------------------------------------------------------------------- return setmetatable (self, metatable) ---------------------------------------------------------------------------------------------------------------- end ------------------------------------------------------------------------------------------------------------------- --- Constructor, or getter. -- -- This function is returned by the module: -- -- local validator = require "corky.validator" -- -- If the first parameter of this function is an array with one element (at position `1`), and this one element is -- a validator instance, the function will return the current validation result and the current *value* of that -- instance, in that order. This is just syntactic sugar, it may be used instead of the get() -- method. Please see above for usage examples. -- -- In all other cases, this function will return a new validator instance, with the current *value* set to the -- value specified as the first parameter. The optional second parameter may be used to set the initial validation -- result, it must be either `true` or `false`. -- -- @function validator -- @p value The initial *value* of a new validator instance, or an array containing an existing instance when used -- as a getter. -- @b[opt] valid The initial validation result. -- @r Either the new validator instance, or the instance's validation result and *value* when used as a getter. return function (value, valid) if type (value) == "table" and #value == 1 and type (value [1]) == "table" then local meta = getmetatable (value [1]) if type (meta) == "string" and meta == "validator" then return value [1].get () end end return validator (value, valid) end -- :indentSize=3:tabSize=3:noTabs=true:mode=lua:maxLineLen=115: