notex.nvim/lua/notex/utils/validation.lua

472 lines
12 KiB
Lua
Raw Permalink Normal View History

2025-10-05 20:16:33 -04:00
-- Data validation utilities
local M = {}
local type_utils = require('notex.utils.types')
-- Validation rules
local VALIDATION_RULES = {
string = {
min_length = 0,
max_length = 1000,
required = false,
pattern = nil,
enum = nil
},
number = {
min_value = nil,
max_value = nil,
integer = false,
required = false
},
boolean = {
required = false
},
date = {
required = false,
min_date = nil,
max_date = nil,
format = "iso8601"
},
email = {
required = false,
domain_whitelist = nil
},
url = {
required = false,
schemes = {"http", "https"},
domain_whitelist = nil
},
array = {
min_items = 0,
max_items = 100,
item_type = nil,
required = false
},
object = {
required_fields = {},
optional_fields = {},
field_types = {},
strict = false,
required = false
}
}
-- Validate value against schema
function M.validate_value(value, schema)
if not schema or not schema.type then
return false, "Schema must specify type"
end
-- Handle null/nil values
if value == nil then
if schema.required == true then
return false, "Value is required"
end
return true, "Value is optional and nil"
end
-- Type validation
local detected_type = type_utils.detect_type(value)
if detected_type ~= schema.type then
local converted = type_utils.convert_to_type(value, schema.type)
if converted == nil then
return false, string.format("Expected %s, got %s", schema.type, detected_type)
end
value = converted -- Use converted value for further validation
end
-- Type-specific validation
local type_validator = "validate_" .. schema.type
if M[type_validator] then
local valid, error = M[type_validator](value, schema)
if not valid then
return false, error
end
end
return true, "Validation passed"
end
-- Validate string
function M.validate_string(value, schema)
local str = tostring(value)
-- Length validation
if schema.min_length and #str < schema.min_length then
return false, string.format("String too short (min %d characters)", schema.min_length)
end
if schema.max_length and #str > schema.max_length then
return false, string.format("String too long (max %d characters)", schema.max_length)
end
-- Pattern validation
if schema.pattern then
if not str:match(schema.pattern) then
return false, "String does not match required pattern"
end
end
-- Enum validation
if schema.enum then
local found = false
for _, enum_value in ipairs(schema.enum) do
if str == tostring(enum_value) then
found = true
break
end
end
if not found then
return false, string.format("Value must be one of: %s", vim.inspect(schema.enum))
end
end
return true, "String validation passed"
end
-- Validate number
function M.validate_number(value, schema)
local num = tonumber(value)
if not num then
return false, "Value is not a number"
end
-- Integer validation
if schema.integer and num ~= math.floor(num) then
return false, "Value must be an integer"
end
-- Range validation
if schema.min_value and num < schema.min_value then
return false, string.format("Value too small (minimum: %s)", tostring(schema.min_value))
end
if schema.max_value and num > schema.max_value then
return false, string.format("Value too large (maximum: %s)", tostring(schema.max_value))
end
return true, "Number validation passed"
end
-- Validate boolean
function M.validate_boolean(value, schema)
local bool = value
if type(value) ~= "boolean" then
bool = type_utils.convert_to_boolean(tostring(value))
if bool == nil then
return false, "Value is not a boolean"
end
end
return true, "Boolean validation passed"
end
-- Validate date
function M.validate_date(value, schema)
local date_parser = require('notex.utils.date')
local timestamp = date_parser.parse_date(tostring(value))
if not timestamp then
return false, "Invalid date format"
end
-- Range validation
if schema.min_date then
local min_timestamp = date_parser.parse_date(schema.min_date)
if min_timestamp and timestamp < min_timestamp then
return false, "Date is before minimum allowed date"
end
end
if schema.max_date then
local max_timestamp = date_parser.parse_date(schema.max_date)
if max_timestamp and timestamp > max_timestamp then
return false, "Date is after maximum allowed date"
end
end
return true, "Date validation passed"
end
-- Validate email
function M.validate_email(value, schema)
local email = tostring(value)
if not email:match("^[^@]+@[^@]+%.[^@]+$") then
return false, "Invalid email format"
end
-- Domain whitelist validation
if schema.domain_whitelist then
local domain = email:match("@([^@]+)$")
local found = false
for _, allowed_domain in ipairs(schema.domain_whitelist) do
if domain == allowed_domain then
found = true
break
end
end
if not found then
return false, string.format("Domain not in whitelist: %s", domain)
end
end
return true, "Email validation passed"
end
-- Validate URL
function M.validate_url(value, schema)
local url = tostring(value)
-- Basic URL validation
if not url:match("^https?://") then
-- Check if scheme is required
if schema.schemes and #schema.schemes > 0 then
return false, string.format("URL must start with: %s", table.concat(schema.schemes, ", "))
end
end
-- Scheme validation
if schema.schemes then
local scheme = url:match("^(https?)://")
if not scheme then
return false, "Invalid URL scheme"
end
local found = false
for _, allowed_scheme in ipairs(schema.schemes) do
if scheme == allowed_scheme then
found = true
break
end
end
if not found then
return false, string.format("URL scheme not allowed: %s", scheme)
end
end
return true, "URL validation passed"
end
-- Validate array
function M.validate_array(value, schema)
local arr = value
if type(arr) ~= "table" then
-- Try to convert to array
arr = type_utils.convert_to_array(tostring(value))
if type(arr) ~= "table" then
return false, "Value is not an array"
end
end
-- Length validation
if schema.min_items and #arr < schema.min_items then
return false, string.format("Array too short (min %d items)", schema.min_items)
end
if schema.max_items and #arr > schema.max_items then
return false, string.format("Array too long (max %d items)", schema.max_items)
end
-- Item type validation
if schema.item_type then
for i, item in ipairs(arr) do
local valid, error = M.validate_value(item, {
type = schema.item_type,
required = false
})
if not valid then
return false, string.format("Array item %d invalid: %s", i, error)
end
end
end
return true, "Array validation passed"
end
-- Validate object
function M.validate_object(value, schema)
local obj = value
if type(obj) ~= "table" then
return false, "Value is not an object"
end
-- Required fields validation
for _, field in ipairs(schema.required_fields or {}) do
if obj[field] == nil then
return false, string.format("Required field missing: %s", field)
end
end
-- Field validation
local all_fields = {}
for field, _ in pairs(obj) do
table.insert(all_fields, field)
end
for _, field in ipairs(all_fields) do
local field_schema = schema.field_types and schema.field_types[field]
if field_schema then
local valid, error = M.validate_value(obj[field], field_schema)
if not valid then
return false, string.format("Field '%s' invalid: %s", field, error)
end
elseif schema.strict then
return false, string.format("Unexpected field '%s' (strict mode)", field)
end
end
return true, "Object validation passed"
end
-- Validate document schema
function M.validate_document_properties(properties, schema_definition)
local errors = {}
local warnings = {}
if not properties then
return false, {"No properties provided"}
end
-- Validate each property against schema
for prop_name, prop_value in pairs(properties) do
local prop_schema = schema_definition[prop_name]
if prop_schema then
local valid, error = M.validate_value(prop_value, prop_schema)
if not valid then
table.insert(errors, string.format("Property '%s': %s", prop_name, error))
end
else
table.insert(warnings, string.format("Unknown property '%s'", prop_name))
end
end
-- Check for missing required properties
for prop_name, prop_schema in pairs(schema_definition) do
if prop_schema.required and properties[prop_name] == nil then
table.insert(errors, string.format("Missing required property: %s", prop_name))
end
end
return #errors == 0, {errors = errors, warnings = warnings}
end
-- Create validation schema
function M.create_schema(field_name, options)
options = options or {}
local base_schema = VALIDATION_RULES[options.type] or VALIDATION_RULES.string
local schema = vim.tbl_deep_extend("force", base_schema, options)
schema.field_name = field_name
return schema
end
-- Validate query parameters
function M.validate_query_params(params, allowed_params)
local errors = {}
for param_name, param_value in pairs(params) do
if not allowed_params[param_name] then
table.insert(errors, string.format("Unknown parameter: %s", param_name))
end
end
for param_name, param_schema in pairs(allowed_params) do
if params[param_name] == nil and param_schema.required then
table.insert(errors, string.format("Required parameter missing: %s", param_name))
elseif params[param_name] ~= nil then
local valid, error = M.validate_value(params[param_name], param_schema)
if not valid then
table.insert(errors, string.format("Parameter '%s': %s", param_name, error))
end
end
end
return #errors == 0, errors
end
-- Sanitize input
function M.sanitize_input(value, options)
options = options or {}
local max_length = options.max_length or 1000
if not value then
return nil
end
local sanitized = tostring(value)
-- Remove potentially dangerous characters
sanitized = sanitized:gsub("[<>\"'&]", "")
-- Trim whitespace
sanitized = sanitized:trim()
-- Limit length
if #sanitized > max_length then
sanitized = sanitized:sub(1, max_length)
end
return sanitized
end
-- Validate file path
function M.validate_file_path(path)
if not path or path == "" then
return false, "Empty file path"
end
-- Check for invalid characters
if path:match('[<>:"|?*]') then
return false, "Invalid characters in file path"
end
-- Check for directory traversal
if path:match("%.%.") then
return false, "Directory traversal not allowed"
end
return true, "File path is valid"
end
-- Return validation summary
function M.create_validation_summary(results)
local summary = {
total = #results,
valid = 0,
invalid = 0,
errors = {},
warnings = {}
}
for _, result in ipairs(results) do
if result.valid then
summary.valid = summary.valid + 1
else
summary.invalid = summary.invalid + 1
if result.errors then
for _, error in ipairs(result.errors) do
table.insert(summary.errors, error)
end
end
if result.warnings then
for _, warning in ipairs(result.warnings) do
table.insert(summary.warnings, warning)
end
end
end
end
return summary
end
return M