-- Valua 0.2.1 -- Copyright (c) 2014 Etiene Dalcol -- License: MIT -- -- Originally bundled with Sailor MVC Web Framework, now released as a separated module. -- http://sailorproject.org -- -- This module provides tools for validating values, very useful in forms, but also usable elsewhere. -- It works in appended chains. Create a new validation object and start chaining your test functions. -- If your value fails a test, it breaks the chain and does not evaluate the rest of it. -- It returns a boolean and an error string (nil when tests succeeded) -- -- Example 1 - Just create, chain and use: -- valua:new().type("string").len(3,5)("test string!") -- false, "should have 3-5 characters" -- -- Example 2 - Create, chain and later use it multiple times: -- local reusable_validation = valua:new().type("string").len(3,5) -- reusable_validation("test string!") -- false, "should have 3-5 characters" -- reusable_validation("test!") -- true -- local tinsert,setmetatable,len,match = table.insert,setmetatable,string.len,string.match local tonumber,tostring = tonumber,tostring local next,type,floor,ipairs = next,type,math.floor, ipairs local unpack = unpack or table.unpack local pack = table.pack or function(...) return { n = select('#', ...), ... } end local _ENV = nil local valua = {} -- CORE -- Caution, this is confusing -- creates a new validation object, useful for reusable stuff and creating many validation tests at a time function valua:new(obj) obj = obj or {} setmetatable(obj,self) -- __index will be called always when chaining validation functions self.__index = function(t,k) --saves a function named _ with its args in a funcs table, to be used later when validating return function(...) local args = pack(...) if k == 'optional' then obj.allow_nil = true else local f = function(value) return valua['_'..k](value, unpack(args, 1, args.n)) end tinsert(t.funcs,f) end return t end end -- __call will run only when the value is validated self.__call = function(t,value) local res = true local err = nil local fres if value == nil and t.allow_nil then return res, err end -- iterates through all chained validations funcs that were packed, passing the value to be validated for _,f in ipairs(t.funcs) do fres,err = f(value) res = res and fres -- breaks the chain if a test fails if err then break end end -- boolean, error message or nil return res,err end obj.funcs = {} obj.allow_nil = false return obj end -- -- VALIDATION FUNCS -- Add new funcs at will, they all should have the value to be validated as first parameter -- and their names must be preceded by '_'. -- For example, if you want to use .custom_val(42) on your validation chain, you need to create a -- function valua._custom_val(,). Just remember the value var will be known -- at the end of the chain and the other var, in this case, will receive '42'. You can add multiple other vars. -- These functions can be called directly (valua._len("test",2,5))in a non-chained and isolated way of life -- for quick stuff, but chaining is much cooler! -- Return false,'' if the value fails the test and simply true if it succeeds. -- aux funcs local function empty(v) return not v or (type(v)=='string' and len(v) == 0) or (type(v)=='table' and not next(v)) end -- -- String function valua._len(value,min,max) local l = len(value or '') if l < min or l > max then return false,"should have "..tostring(min).."-"..tostring(max).." characters" end return true end function valua._compare(value,another_value) if value ~= another_value then return false, "values are not equal" end return true end function valua._email(value) if not empty(value) and not value:match("^[%w+%.%-_]+@[%w+%.%-_]+%.%a%a+$") then return false, "is not a valid email address" end return true end function valua._match(value,pattern) if not empty(value) and not value:match(pattern) then return false, "does not match pattern" end return true end function valua._alnum(value) if not empty(value) and value:match("%W") then return false, "constains improper characters" end return true end function valua._contains(value,substr) if not empty(value) and not value:find(substr) then return false, "does not contain '"..substr.."'" end return true end function valua._no_white(value) if not empty(value) and value:find("%s") then return false, "must not contain white spaces" end return true end -- -- Numbers function valua._min(value,n) if not empty(value) and value < n then return false,"must be greater than "..tostring(n) end return true end function valua._max(value,n) if not empty(value) and value > n then return false,"must not be greater than "..tostring(n) end return true end function valua._integer(value) if not empty(value) and floor(tonumber(value)) ~= tonumber(value) then return false, "must be an integer" end return true end -- -- Date -- Check for a UK date pattern dd/mm/yyyy , dd-mm-yyyy, dd.mm.yyyy -- or US pattern mm/dd/yyyy, mm-dd-yyyy, mm.dd.yyyy -- or ISO pattern yyyy/mm/dd, yyyy-mm-dd, yyyy.mm.dd -- Default is UK function valua._date(value,format) local valid = true if (match(value, "^%d+%p%d+%p%d%d%d%d$")) then local d, m, y if format and format:lower() == 'us' then m, d, y = match(value, "(%d+)%p(%d+)%p(%d+)") elseif format and format:lower() == 'iso' then y, m, d = match(value, "(%d+)%p(%d+)%p(%d+)") else d, m, y = match(value, "(%d+)%p(%d+)%p(%d+)") end d, m, y = tonumber(d), tonumber(m), tonumber(y) local dm2 = d*m*m if d>31 or m>12 or dm2==116 or dm2==120 or dm2==124 or dm2==496 or dm2==1116 or dm2==2511 or dm2==3751 then -- invalid unless leap year if not (dm2==116 and (y%400 == 0 or (y%100 ~= 0 and y%4 == 0))) then valid = false end end else valid = false end if not valid then return false, "is not a valid date" end return true end -- -- Datetime -- Check for a UK date pattern dd/mm/yyyy hh:mi:ss, dd-mm-yyyy, dd.mm.yyyy -- or US pattern mm/dd/yyyy, mm-dd-yyyy, mm.dd.yyyy -- or ISO pattern yyyy/mm/dd, yyyy-mm-dd, yyyy.mm.dd -- Default is UK function valua._datetime(value,format) local valid = true if (match(value, "^%d+%p%d+%p%d%d%d%d %d%d%p%d%d%p%d%d$")) then local d, m, y, hh, mm, ss if format and format:lower() == 'us' then m, d, y, hh, mm, ss = match(value, "(%d+)%p(%d+)%p(%d+) (%d%d)%p(%d%d)%p(%d%d)") elseif format and format:lower() == 'iso' then y, m, d, hh, mm, ss = match(value, "(%d+)%p(%d+)%p(%d+) (%d%d)%p(%d%d)%p(%d%d)") else d, m, y, hh, mm, ss = match(value, "(%d+)%p(%d+)%p(%d+) (%d%d)%p(%d%d)%p(%d%d)") end d, m, y, hh, mm, ss = tonumber(d), tonumber(m), tonumber(y), tonumber(hh), tonumber(mm), tonumber(ss) local dm2 = d*m*m if d>31 or m>12 or dm2==116 or dm2==120 or dm2==124 or dm2==496 or dm2==1116 or dm2==2511 or dm2==3751 then -- invalid unless leap year if not (dm2==116 and (y%400 == 0 or (y%100 ~= 0 and y%4 == 0))) then valid = false end end -- time validation if not (hh >= 0 and hh <= 24) then valid = false end if not (mm >= 0 and mm <= 60) then valid = false end if not (ss >= 0 and ss <= 60) then valid = false end else valid = false end if not valid then return false, "is not a valid datetime" end return true end -- -- Abstract function valua._empty(value) if not empty(value) then return false,"must be empty" end return true end function valua._not_empty(value) if empty(value) then return false,"must not be empty" end return true end function valua._type(value,value_type) if type(value) ~= value_type then return false,"must be a "..value_type end return true end function valua._boolean(value) if type(value) ~= 'boolean' then return false,"must be a boolean" end return true end function valua._number(value) if type(value) ~= 'number' then return false,"must be a number" end return true end function valua._string(value) if type(value) ~= 'string' then return false,"must be a string" end return true end function valua._in_list(value,list) local valid = false for _,v in ipairs(list) do if value == v then valid = true break end end if not valid then return false,"is not in the list" end return true end -- -- return valua