aboutsummaryrefslogtreecommitdiff
path: root/valua.lua
blob: d4d62b93bb00dc4f856a85f0cdedfb9b77faba3a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
-- 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 _<index> 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(<value var>,<other var>). 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,'<error message>' 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