Created
November 22, 2023 04:40
-
-
Save AMD-NICK/56577317d3355ff13b67bfb84b1f1d07 to your computer and use it in GitHub Desktop.
Parameters validation on lua. Initially created for one application on lua-express: https://github.com/TRIGONIM/lua-express/
This file contains hidden or 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
-- Как парламентёр, только (парам)ентёр :) | |
-- Новый валидатор, который имеет больше опций и убирает лишнюю дичь, типа такой: | |
-- https://file.def.pm/o9LZgS3D.jpg | |
-- require("middlewares.paramenter").validator(params, messages) | |
local utf8 -- https://gist.github.com/Stepets/3b4dbaf5e6e6a60f3862 | |
local ok, utflib = pcall(require, "utf8") | |
if ok then utf8 = utflib end | |
local rulesets = {} | |
rulesets["numeric"] = function(value) | |
-- local numeric = tostring(value):match("^%-?%d+%.?%d*$") -- .1 123.1 123, -.0 | |
if tonumber(value) then | |
return true, tonumber(value) | |
else | |
return false | |
end | |
end | |
rulesets["integer"] = function(value) | |
-- local integer = tostring(value):match("^%-?%d+$") | |
-- съедает даже hex строку, возвращает число | |
-- при длинной мантиссе "100.000000000000001" вернет все равно целое 100 (особенность lua?) | |
local numeric = tonumber(value) | |
if numeric and numeric % 1 == 0 then | |
return true, numeric | |
else | |
return false | |
end | |
end | |
-- rulesets["json"] = function(value) | |
-- local ok, res = pcall(json.decode, value) | |
-- if not ok then return false end | |
-- return true, res | |
-- end | |
-- для случаев, когда надо валидировать строго строку | |
rulesets["string"] = function(value) | |
local typ = type(value) | |
if typ == "string" then | |
return true, value | |
-- else | |
-- return true, tostring(value) | |
end | |
return false | |
end | |
rulesets["match"] = function(value, patt) | |
local matched = value:match(patt) | |
if not matched then return false end | |
return true, matched | |
end | |
rulesets["required"] = function(value) | |
return value ~= nil and value ~= "" | |
end | |
rulesets["nullable"] = function(value) | |
local nulled = value == nil | |
if nulled then return "break" end | |
return true -- пусть проверяет другие правила | |
end | |
-- rulesets["starts_with"] = function(value, ...) | |
-- for _, patt in ipairs({...}) do | |
-- if value:sub(1, patt:len()) == patt then return true end | |
-- end | |
-- return false | |
-- end | |
local get_size = function(value) | |
local typ = type(value) | |
if typ == "number" then | |
return value | |
elseif typ == "string" then | |
local string_len = utf8 and utf8.len or string.len | |
return string_len(value) | |
elseif typ == "table" then | |
return #value | |
end | |
return nil -- nil, boolean, function, thread, userdata... | |
end | |
rulesets["size"] = function(value, size) | |
assert(size:match("^%d+$"), "size must be positive integer") | |
return get_size(value) == tonumber(size) | |
end | |
rulesets["min"] = function(value, min) | |
return get_size(value) >= tonumber(min) | |
end | |
rulesets["max"] = function(value, max) | |
return get_size(value) <= tonumber(max) | |
end | |
-- rulesets["in"] = function(value, ...) | |
-- for _, v in ipairs({...}) do | |
-- if value == v then return true end | |
-- end | |
-- return false | |
-- end | |
-- rulesets["not_in"] = function(value, ...) | |
-- for _, v in ipairs({...}) do | |
-- if value == v then return false end | |
-- end | |
-- return true | |
-- end | |
-- CUSTOM RULES -- | |
rulesets["sid64"] = function(value) | |
value = tostring(value) | |
return value:len() == 17 and value:sub(1, 4) == "7656", value | |
end | |
local is_valid_ip = function(str) | |
local parts = { str:match("(%d+)%.(%d+)%.(%d+)%.(%d+)") } | |
if #parts ~= 4 then return false end | |
for _, part in ipairs(parts) do | |
if tonumber(part) > 255 then | |
return false | |
end | |
end | |
return true | |
end | |
rulesets["ip"] = function(value) | |
return is_valid_ip(value) | |
end | |
-- return true, "formatted_value" | |
-- or return false, "failed_rule_name" | |
local function validate_value(value, ruleset) | |
local rules = {} | |
for rule in ruleset:gmatch("[^|]+") do | |
local name, argsstr = rule:match("([^:]+):?(.*)") | |
if not rulesets[name] then return false, name end -- unknown rule | |
local args = {} | |
for arg in argsstr:gmatch("[^,]+") do | |
args[#args + 1] = arg | |
end | |
rules[#rules + 1] = {name, args} | |
end | |
for _, rule in ipairs(rules) do | |
local res, newval = rulesets[rule[1]](value, unpack(rule[2])) | |
if newval then value = newval end | |
if not res then return false, rule[1] end | |
if res == "break" then break end -- for nullable | |
end | |
return true, value -- formatted value | |
end | |
-- params_rules: {param_name = "required|starts_with:765|size:17", ...} | |
-- params_values: {param_name = param_value, ...} | |
local function validate_all_simple(params_rules, params_values) | |
for param_name, ruleset in pairs(params_rules) do | |
local ok, failed_rule_name = validate_value(params_values[param_name], ruleset) | |
if not ok then | |
return false, param_name, failed_rule_name | |
else | |
params_values[param_name] = failed_rule_name -- formatted value | |
end | |
end | |
return true | |
end | |
-- messages variations: | |
-- {param_name.rule_name = "message", ...} | |
-- {param_name.* = "message", ...} | |
-- {* = "message", ...} | |
local function validate_all(params_rules, params_values, messages) | |
messages = messages or {} | |
local ok, param_name, failed_rule_name = validate_all_simple(params_rules, params_values) | |
if not ok then | |
local msg = messages[param_name .. "." .. failed_rule_name] | |
or messages[param_name .. ".*"] | |
or messages["*"] | |
or "invalid_" .. param_name | |
return false, param_name, msg | |
end | |
return true | |
end | |
-- Извлекает параметры из запроса в соответствии с правилами и проверяет их | |
-- Если указан messages, то в случае ошибки будет использовано сообщение из него | |
local function validator(params_with_rules, messages) -- messages may be nil | |
return function(req, res, next) | |
local params_values = {} | |
for param_name in pairs(params_with_rules) do | |
params_values[param_name] = req:param(param_name) | |
end | |
local ok, param_name, msg = validate_all(params_with_rules, params_values, messages) | |
if not ok then | |
res:json_error(msg or ("invalid_" .. param_name), 400) | |
return | |
end | |
if req.valid then | |
print("Какой-то из мидлверов уже создал поле req.valid. Конфликтная ситуация. Оверрайдим") | |
end | |
req.valid = params_values | |
next() | |
end | |
end | |
-- usage: | |
-- app:get("/path", validator({ | |
-- sid = "required|starts_with:765|size:17", | |
-- s = "required|numeric|min:0|max:255", | |
-- sum = "required|numeric|min:1|max:10000", | |
-- note = "max:80", | |
-- }, { | |
-- ["sid.required"] = "SteamID64 is required", | |
-- ["sid.*"] = "SteamID64 is invalid", | |
-- ["*"] = "Something went wrong", | |
-- ["note.*"] = "Note should be less than 80 characters", | |
-- })) | |
-- for _, test in ipairs({ | |
-- {"32.5", "numeric", true, 32.5}, | |
-- {"6.5", "numeric|min:5|max:6", false, "max"}, | |
-- {nil, "nullable|integer", true, nil}, | |
-- {"0xFF", "numeric", true, 255}, -- само конвертировало hex в dec | |
-- {5, "size:5", true, 5}, -- сравнивает как дано (числом) | |
-- {"5", "size:5", false, "size"}, -- сравнивает как дано (строкой) | |
-- {"5", "integer|size:5", true, 5}, -- сравнивает как число, конвертировав в него | |
-- {"5.0", "integer|size:5", true, 5}, -- оно снимет после запятой | |
-- {"100.000000000000001", "integer", true, 100}, -- тут мантисса довольно длинная и число возвращается целым | |
-- {"100.00000000000001", "integer", false, "integer"}, -- тут короткая, так что получаем float | |
-- {"qqqqqqqqq", "max:9", true, "qqqqqqqqq"}, -- длина строки | |
-- {"кириллица", "max:9", false, "max"}, -- кириллица имеет бОльшую длину для lua, чем латиница | |
-- }) do | |
-- local ok, val = validate_value(test[1], test[2]) | |
-- -- print(ok, val, ok and type(val) or nil) | |
-- assert(ok == test[3] and val == test[4], "validate_value failed: " .. tostring(test[1]) .. " " .. test[2] .. | |
-- ". Expected (" .. tostring(test[3]) .. ", " .. tostring(test[4]) .. | |
-- "), got (" .. tostring(ok) .. " " .. tostring(val) .. ")") | |
-- end | |
return { | |
-- rulesets = rulesets, | |
-- validate_value = validate_value, | |
validate_all_simple = validate_all_simple, | |
-- validate_all = validate_all, | |
validator = validator, | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment