Files
FlexLove/modules/ErrorHandler.lua
2025-11-19 12:14:58 -05:00

959 lines
29 KiB
Lua

---@class ErrorCodes
---@field categories table
---@field codes table
local ErrorCodes = {
categories = {
VAL = "Validation",
LAY = "Layout",
REN = "Render",
THM = "Theme",
EVT = "Event",
RES = "Resource",
SYS = "System",
},
codes = {
-- Validation Errors (VAL_001 - VAL_099)
VAL_001 = {
code = "FLEXLOVE_VAL_001",
category = "VAL",
description = "Invalid property type",
suggestion = "Check the property type matches the expected type (e.g., number, string, table)",
},
VAL_002 = {
code = "FLEXLOVE_VAL_002",
category = "VAL",
description = "Property value out of range",
suggestion = "Ensure the value is within the allowed min/max range",
},
VAL_003 = {
code = "FLEXLOVE_VAL_003",
category = "VAL",
description = "Required property missing",
suggestion = "Provide the required property in your element definition",
},
VAL_004 = {
code = "FLEXLOVE_VAL_004",
category = "VAL",
description = "Invalid color format",
suggestion = "Use valid color format: {r, g, b, a} with values 0-1, hex string, or Color object",
},
VAL_005 = {
code = "FLEXLOVE_VAL_005",
category = "VAL",
description = "Invalid unit format",
suggestion = "Use valid unit format: number (px), '50%', '10vw', '5vh', etc.",
},
VAL_006 = {
code = "FLEXLOVE_VAL_006",
category = "VAL",
description = "Invalid file path",
suggestion = "Check that the file path is correct and the file exists",
},
VAL_007 = {
code = "FLEXLOVE_VAL_007",
category = "VAL",
description = "Invalid enum value",
suggestion = "Use one of the allowed enum values for this property",
},
VAL_008 = {
code = "FLEXLOVE_VAL_008",
category = "VAL",
description = "Invalid text input",
suggestion = "Ensure text meets validation requirements (length, pattern, allowed characters)",
},
-- Layout Errors (LAY_001 - LAY_099)
LAY_001 = {
code = "FLEXLOVE_LAY_001",
category = "LAY",
description = "Invalid flex direction",
suggestion = "Use 'horizontal' or 'vertical' for flexDirection",
},
LAY_002 = {
code = "FLEXLOVE_LAY_002",
category = "LAY",
description = "Circular dependency detected",
suggestion = "Remove circular references in element hierarchy or layout constraints",
},
LAY_003 = {
code = "FLEXLOVE_LAY_003",
category = "LAY",
description = "Invalid dimensions (negative or NaN)",
suggestion = "Ensure width and height are positive numbers",
},
LAY_004 = {
code = "FLEXLOVE_LAY_004",
category = "LAY",
description = "Layout calculation overflow",
suggestion = "Reduce complexity of layout or increase recursion limit",
},
LAY_005 = {
code = "FLEXLOVE_LAY_005",
category = "LAY",
description = "Invalid alignment value",
suggestion = "Use valid alignment values (flex-start, center, flex-end, etc.)",
},
LAY_006 = {
code = "FLEXLOVE_LAY_006",
category = "LAY",
description = "Invalid positioning mode",
suggestion = "Use 'absolute', 'relative', 'flex', or 'grid' for positioning",
},
LAY_007 = {
code = "FLEXLOVE_LAY_007",
category = "LAY",
description = "Grid layout error",
suggestion = "Check grid template columns/rows and item placement",
},
-- Rendering Errors (REN_001 - REN_099)
REN_001 = {
code = "FLEXLOVE_REN_001",
category = "REN",
description = "Invalid render state",
suggestion = "Ensure element is properly initialized before rendering",
},
REN_002 = {
code = "FLEXLOVE_REN_002",
category = "REN",
description = "Texture loading failed",
suggestion = "Check image path and format, ensure file exists",
},
REN_003 = {
code = "FLEXLOVE_REN_003",
category = "REN",
description = "Font loading failed",
suggestion = "Check font path and format, ensure file exists",
},
REN_004 = {
code = "FLEXLOVE_REN_004",
category = "REN",
description = "Invalid color value",
suggestion = "Color components must be numbers between 0 and 1",
},
REN_005 = {
code = "FLEXLOVE_REN_005",
category = "REN",
description = "Clipping stack overflow",
suggestion = "Reduce nesting depth or check for missing scissor pops",
},
REN_006 = {
code = "FLEXLOVE_REN_006",
category = "REN",
description = "Shader compilation failed",
suggestion = "Check shader code for syntax errors",
},
REN_007 = {
code = "FLEXLOVE_REN_007",
category = "REN",
description = "Invalid nine-patch configuration",
suggestion = "Check nine-patch slice values and image dimensions",
},
-- Theme Errors (THM_001 - THM_099)
THM_001 = {
code = "FLEXLOVE_THM_001",
category = "THM",
description = "Theme file not found",
suggestion = "Check theme file path and ensure file exists",
},
THM_002 = {
code = "FLEXLOVE_THM_002",
category = "THM",
description = "Invalid theme structure",
suggestion = "Theme must return a table with 'name' and component styles",
},
THM_003 = {
code = "FLEXLOVE_THM_003",
category = "THM",
description = "Required theme property missing",
suggestion = "Ensure theme has required properties (name, base styles, etc.)",
},
THM_004 = {
code = "FLEXLOVE_THM_004",
category = "THM",
description = "Invalid component style",
suggestion = "Component styles must be tables with valid properties",
},
THM_005 = {
code = "FLEXLOVE_THM_005",
category = "THM",
description = "Theme loading failed",
suggestion = "Check theme file for Lua syntax errors",
},
THM_006 = {
code = "FLEXLOVE_THM_006",
category = "THM",
description = "Invalid theme color",
suggestion = "Theme colors must be valid color values (hex, rgba, Color object)",
},
-- Event Errors (EVT_001 - EVT_099)
EVT_001 = {
code = "FLEXLOVE_EVT_001",
category = "EVT",
description = "Invalid event type",
suggestion = "Use valid event types (mousepressed, textinput, etc.)",
},
EVT_002 = {
code = "FLEXLOVE_EVT_002",
category = "EVT",
description = "Event handler error",
suggestion = "Check event handler function for errors",
},
EVT_003 = {
code = "FLEXLOVE_EVT_003",
category = "EVT",
description = "Event propagation error",
suggestion = "Check event bubbling/capturing logic",
},
EVT_004 = {
code = "FLEXLOVE_EVT_004",
category = "EVT",
description = "Invalid event target",
suggestion = "Ensure event target element exists and is valid",
},
EVT_005 = {
code = "FLEXLOVE_EVT_005",
category = "EVT",
description = "Event handler not a function",
suggestion = "Event handlers must be functions",
},
-- Resource Errors (RES_001 - RES_099)
RES_001 = {
code = "FLEXLOVE_RES_001",
category = "RES",
description = "File not found",
suggestion = "Check file path and ensure file exists in the filesystem",
},
RES_002 = {
code = "FLEXLOVE_RES_002",
category = "RES",
description = "Permission denied",
suggestion = "Check file permissions and access rights",
},
RES_003 = {
code = "FLEXLOVE_RES_003",
category = "RES",
description = "Invalid file format",
suggestion = "Ensure file format is supported (png, jpg, ttf, etc.)",
},
RES_004 = {
code = "FLEXLOVE_RES_004",
category = "RES",
description = "Resource loading failed",
suggestion = "Check file integrity and format compatibility",
},
RES_005 = {
code = "FLEXLOVE_RES_005",
category = "RES",
description = "Image cache error",
suggestion = "Clear image cache or check memory availability",
},
-- System Errors (SYS_001 - SYS_099)
SYS_001 = {
code = "FLEXLOVE_SYS_001",
category = "SYS",
description = "Memory allocation failed",
suggestion = "Reduce memory usage or check available memory",
},
SYS_002 = {
code = "FLEXLOVE_SYS_002",
category = "SYS",
description = "Stack overflow",
suggestion = "Reduce recursion depth or check for infinite loops",
},
SYS_003 = {
code = "FLEXLOVE_SYS_003",
category = "SYS",
description = "Invalid state",
suggestion = "Check initialization order and state management",
},
SYS_004 = {
code = "FLEXLOVE_SYS_004",
category = "SYS",
description = "Module initialization failed",
suggestion = "Check module dependencies and initialization order",
},
-- Performance Warnings (PERF_001 - PERF_099)
PERF_001 = {
code = "FLEXLOVE_PERF_001",
category = "PERF",
description = "Performance threshold exceeded",
suggestion = "Operation took longer than recommended. Monitor for patterns.",
},
PERF_002 = {
code = "FLEXLOVE_PERF_002",
category = "PERF",
description = "Critical performance threshold exceeded",
suggestion = "Operation is causing frame drops. Consider optimizing or reducing frequency.",
},
-- Memory Warnings (MEM_001 - MEM_099)
MEM_001 = {
code = "FLEXLOVE_MEM_001",
category = "MEM",
description = "Memory leak detected",
suggestion = "Table is growing consistently. Review cache eviction policies and ensure objects are properly released.",
},
-- State Management Warnings (STATE_001 - STATE_099)
STATE_001 = {
code = "FLEXLOVE_STATE_001",
category = "STATE",
description = "CallSite counters accumulating",
suggestion = "This indicates incrementFrame() may not be called properly. Check immediate mode frame management.",
},
},
}
--- Get error information by code
--- @param code string Error code (e.g., "VAL_001" or "FLEXLOVE_VAL_001")
--- @return table? errorInfo Error information or nil if not found
function ErrorCodes.get(code)
-- Handle both short and full format
local shortCode = code:gsub("^FLEXLOVE_", "")
return ErrorCodes.codes[shortCode]
end
--- Get human-readable description for error code
--- @param code string Error code
--- @return string description Error description
function ErrorCodes.describe(code)
local info = ErrorCodes.get(code)
if info then
return info.description
end
return "Unknown error code: " .. code
end
--- Get suggested fix for error code
--- @param code string Error code
--- @return string suggestion Suggested fix
function ErrorCodes.getSuggestion(code)
local info = ErrorCodes.get(code)
if info then
return info.suggestion
end
return "No suggestion available"
end
--- Get category for error code
--- @param code string Error code
--- @return string category Error category name
function ErrorCodes.getCategory(code)
local info = ErrorCodes.get(code)
if info then
return ErrorCodes.categories[info.category] or info.category
end
return "Unknown"
end
--- List all error codes in a category
--- @param category string Category code (e.g., "VAL", "LAY")
--- @return table codes List of error codes in category
function ErrorCodes.listByCategory(category)
local result = {}
for code, info in pairs(ErrorCodes.codes) do
if info.category == category then
table.insert(result, {
code = code,
fullCode = info.code,
description = info.description,
suggestion = info.suggestion,
})
end
end
table.sort(result, function(a, b)
return a.code < b.code
end)
return result
end
--- Search error codes by keyword
--- @param keyword string Keyword to search for
--- @return table codes Matching error codes
function ErrorCodes.search(keyword)
keyword = keyword:lower()
local result = {}
for code, info in pairs(ErrorCodes.codes) do
local searchText = (code .. " " .. info.description .. " " .. info.suggestion):lower()
if searchText:find(keyword, 1, true) then
table.insert(result, {
code = code,
fullCode = info.code,
description = info.description,
suggestion = info.suggestion,
category = ErrorCodes.categories[info.category],
})
end
end
return result
end
--- Get all error codes
--- @return table codes All error codes
function ErrorCodes.listAll()
local result = {}
for code, info in pairs(ErrorCodes.codes) do
table.insert(result, {
code = code,
fullCode = info.code,
description = info.description,
suggestion = info.suggestion,
category = ErrorCodes.categories[info.category],
})
end
table.sort(result, function(a, b)
return a.code < b.code
end)
return result
end
--- Format error message with code
--- @param code string Error code
--- @param message string Error message
--- @return string formattedMessage Formatted error message with code
function ErrorCodes.formatMessage(code, message)
local info = ErrorCodes.get(code)
if info then
return string.format("[%s] %s", info.code, message)
end
return message
end
--- Validate that all error codes are unique and properly formatted
--- @return boolean, string? Returns true if valid, or false with error message
function ErrorCodes.validate()
local seen = {}
local fullCodes = {}
for code, info in pairs(ErrorCodes.codes) do
-- Check for duplicates
if seen[code] then
return false, "Duplicate error code: " .. code
end
seen[code] = true
if fullCodes[info.code] then
return false, "Duplicate full error code: " .. info.code
end
fullCodes[info.code] = true
-- Check format
if not code:match("^[A-Z]+_[0-9]+$") then
return false, "Invalid code format: " .. code .. " (expected CATEGORY_NUMBER)"
end
-- Check full code format
local expectedFullCode = "FLEXLOVE_" .. code
if info.code ~= expectedFullCode then
return false, "Mismatched full code for " .. code .. ": expected " .. expectedFullCode .. ", got " .. info.code
end
-- Check required fields
if not info.description or info.description == "" then
return false, "Missing description for " .. code
end
if not info.suggestion or info.suggestion == "" then
return false, "Missing suggestion for " .. code
end
if not info.category or info.category == "" then
return false, "Missing category for " .. code
end
end
return true, nil
end
---@enum LOG_LEVEL
local LOG_LEVEL = {
CRITICAL = 1,
ERROR = 2,
WARNING = 3,
INFO = 4,
DEBUG = 5,
}
---@enum LOG_TARGET
local LOG_TARGET = {
CONSOLE = "console",
FILE = "file",
BOTH = "both",
NONE = "none",
}
---@class ErrorHandler
---@field errorCodes ErrorCodes
---@field includeStackTrace boolean -- Default: false
---@field logLevel LOG_LEVEL --Default: LOG_LEVEL.WARNING
---@field logTarget "console" | "file" | "both"
---@field logFile string
---@field maxLogSize number in bytes
---@field maxLogFiles number files to rotate
---@field enableRotation boolean see maxLogFiles
---@field _currentLogSize number private
---@field _logFileHandle file* private
local ErrorHandler = {
errorCodes = ErrorCodes,
}
ErrorHandler.__index = ErrorHandler
---@type ErrorHandler|nil
local instance = nil
---@param config { includeStackTrace?: boolean, logLevel?: LOG_LEVEL, logTarget?: "console" | "file" | "both", logFile?: string, maxLogSize?: number, maxLogFiles?: number, enableRotation?: boolean }|nil
---@return ErrorHandler
function ErrorHandler.init(config)
if instance == nil then
local self = setmetatable({}, ErrorHandler)
self.includeStackTrace = config and config.includeStackTrace or false
self.logLevel = config and config.logLevel or LOG_LEVEL.WARNING
self.logTarget = config and config.logTarget or LOG_TARGET.CONSOLE
self.logFile = config and config.logFile or "flexlove-errors.log"
self.maxLogSize = config and config.maxLogSize or 10 * 1024 * 1024
self.maxLogFiles = config and config.maxLogFiles or 5
self.enableRotation = config and config.enableRotation or true
self._currentLogSize = 0
self._logFileHandle = nil
instance = self
end
return instance
end
--- Get the singleton instance (lazily initializes if needed)
---@return ErrorHandler
function ErrorHandler.getInstance()
if instance == nil then
ErrorHandler.init()
end
return instance
end
--- Get current timestamp with milliseconds
---@return string|osdate Formatted timestamp
function ErrorHandler:_getTimestamp()
local time = os.time()
local date = os.date("%Y-%m-%d %H:%M:%S", time)
-- Note: Lua doesn't have millisecond precision by default, so we approximate
return date
end
--- Rotate log file if needed
function ErrorHandler:_rotateLogIfNeeded()
if not self.enableRotation then
return
end
if self._currentLogSize < self.maxLogSize then
return
end
-- Close current log
if self._logFileHandle then
self._logFileHandle:close()
self._logFileHandle = nil
end
-- Rotate existing logs
for i = self.maxLogFiles - 1, 1, -1 do
local oldName = self.logFile .. "." .. i
local newName = self.logFile .. "." .. (i + 1)
os.rename(oldName, newName) -- Will fail silently if file doesn't exist
end
-- Move current log to .1
os.rename(self.logFile, self.logFile .. ".1")
-- Create new log file
self._logFileHandle = io.open(self.logFile, "a")
self._currentLogSize = 0
end
--- Escape string for JSON
---@param str string String to escape
---@return string Escaped string
function ErrorHandler:_escapeJson(str)
str = tostring(str)
str = str:gsub("\\", "\\\\")
str = str:gsub('"', '\\"')
str = str:gsub("\n", "\\n")
str = str:gsub("\r", "\\r")
str = str:gsub("\t", "\\t")
return str
end
--- Format details as JSON object
---@param details table|nil Details object
---@return string JSON string
function ErrorHandler:_formatDetailsJson(details)
if not details or type(details) ~= "table" then
return "{}"
end
local parts = {}
for key, value in pairs(details) do
local jsonKey = self:_escapeJson(tostring(key))
local jsonValue = self:_escapeJson(tostring(value))
table.insert(parts, string.format('"%s":"%s"', jsonKey, jsonValue))
end
return "{" .. table.concat(parts, ",") .. "}"
end
--- Format details object as readable key-value pairs
---@param details table|nil Details object
---@return string Formatted details
function ErrorHandler:_formatDetails(details)
if not details or type(details) ~= "table" then
return ""
end
local lines = {}
for key, value in pairs(details) do
local formattedKey = tostring(key):gsub("^%l", string.upper)
local formattedValue = tostring(value)
-- Truncate very long values
if #formattedValue > 100 then
formattedValue = formattedValue:sub(1, 97) .. "..."
end
table.insert(lines, string.format(" %s: %s", formattedKey, formattedValue))
end
if #lines > 0 then
return "\n\nDetails:\n" .. table.concat(lines, "\n")
end
return ""
end
--- Extract and format stack trace
---@param level number Stack level to start from
---@return string Formatted stack trace
function ErrorHandler:_formatStackTrace(level)
if not self.includeStackTrace then
return ""
end
local lines = {}
local currentLevel = level or 3
while true do
local info = debug.getinfo(currentLevel, "Sl")
if not info then
break
end
-- Skip internal Lua files
if info.source:match("^@") and not info.source:match("loveStub") then
local source = info.source:sub(2) -- Remove @ prefix
local location = string.format("%s:%d", source, info.currentline)
table.insert(lines, " " .. location)
end
currentLevel = currentLevel + 1
if currentLevel > level + 10 then
break
end -- Limit depth
end
if #lines > 0 then
return "\n\nStack trace:\n" .. table.concat(lines, "\n")
end
return ""
end
--- Format an error or warning message with optional error code
---@param module string The module name (e.g., "Element", "Units", "Theme")
---@param level string "Error" or "Warning"
---@param codeOrMessage string Error code (e.g., "VAL_001") or message
---@param messageOrDetails string|table|nil Message or details object
---@param detailsOrSuggestion table|string|nil Details or suggestion
---@param suggestionOrNil string|nil Suggestion
---@return string Formatted message
function ErrorHandler:_formatMessage(module, level, codeOrMessage, messageOrDetails, detailsOrSuggestion, suggestionOrNil)
local code = nil
local message = codeOrMessage
local details = nil
local suggestion = nil
-- Parse arguments (support multiple signatures)
if type(codeOrMessage) == "string" and ErrorCodes.get(codeOrMessage) then
-- Called with error code
code = codeOrMessage
message = messageOrDetails or ErrorCodes.describe(code)
if type(detailsOrSuggestion) == "table" then
details = detailsOrSuggestion
suggestion = suggestionOrNil or ErrorCodes.getSuggestion(code)
elseif type(detailsOrSuggestion) == "string" then
suggestion = detailsOrSuggestion
else
suggestion = ErrorCodes.getSuggestion(code)
end
else
-- Called with message only (backward compatibility)
message = codeOrMessage
if type(messageOrDetails) == "table" then
details = messageOrDetails
suggestion = detailsOrSuggestion
elseif type(messageOrDetails) == "string" then
suggestion = messageOrDetails
end
end
-- Build formatted message
local parts = {}
-- Header: [FlexLove - Module] Level [CODE]: Message
if code then
local codeInfo = ErrorCodes.get(code)
table.insert(parts, string.format("[FlexLove - %s] %s [%s]: %s", module, level, codeInfo.code, message))
else
table.insert(parts, string.format("[FlexLove - %s] %s: %s", module, level, message))
end
-- Details section
if details then
table.insert(parts, self:_formatDetails(details))
end
-- Suggestion section
if suggestion and suggestion ~= "" then
table.insert(parts, string.format("\n\nSuggestion: %s", suggestion))
end
return table.concat(parts, "")
end
--- Write log entry to file and/or console
---@param level string Log level
---@param levelNum number Log level number
---@param module string Module name
---@param code string|nil Error code
---@param message string Message
---@param details table|nil Details
---@param suggestion string|nil Suggestion
function ErrorHandler:_writeLog(level, levelNum, module, code, message, details, suggestion)
-- Check if we should log this level
if levelNum > self.logLevel then
return
end
local timestamp = self:_getTimestamp()
local logEntry
local jsonParts = {
string.format('"timestamp":"%s"', self:_escapeJson(timestamp)),
string.format('"level":"%s"', level),
string.format('"module":"%s"', self:_escapeJson(module)),
string.format('"message":"%s"', self:_escapeJson(message)),
}
if code then
table.insert(jsonParts, string.format('"code":"%s"', self:_escapeJson(code)))
end
if details then
table.insert(jsonParts, string.format('"details":%s', self:_formatDetailsJson(details)))
end
if suggestion then
table.insert(jsonParts, string.format('"suggestion":"%s"', self:_escapeJson(suggestion)))
end
logEntry = "{" .. table.concat(jsonParts, ",") .. "}\n"
if self.logTarget == "console" or self.logTarget == "both" then
io.write(logEntry)
io.flush()
end
-- Write to file
if self.logTarget == "file" or self.logTarget == "both" then
-- Lazy file opening: open on first write
if not self._logFileHandle then
self._logFileHandle = io.open(self.logFile, "a")
if self._logFileHandle then
-- Get current file size
local currentPos = self._logFileHandle:seek("end")
self._currentLogSize = currentPos or 0
end
end
if self._logFileHandle then
self:_rotateLogIfNeeded()
-- Reopen if rotation closed it
if not self._logFileHandle then
self._logFileHandle = io.open(self.logFile, "a")
end
if self._logFileHandle then
self._logFileHandle:write(logEntry)
self._logFileHandle:flush()
self._currentLogSize = self._currentLogSize + #logEntry
end
end
end
end
--- Throw a critical error (stops execution)
---@param module string The module name
---@param codeOrMessage string Error code or message
---@param messageOrDetails string|table|nil Message or details
---@param detailsOrSuggestion table|string|nil Details or suggestion
---@param suggestion string|nil Suggestion
function ErrorHandler:error(module, codeOrMessage, messageOrDetails, detailsOrSuggestion, suggestion)
local formattedMessage = self:_formatMessage(module, "Error", codeOrMessage, messageOrDetails, detailsOrSuggestion, suggestion)
-- Parse arguments for logging
local code = nil
local message = codeOrMessage
local details = nil
local logSuggestion = nil
if type(codeOrMessage) == "string" and ErrorCodes.get(codeOrMessage) then
code = codeOrMessage
message = messageOrDetails or ErrorCodes.describe(code)
if type(detailsOrSuggestion) == "table" then
details = detailsOrSuggestion
logSuggestion = suggestion or ErrorCodes.getSuggestion(code)
elseif type(detailsOrSuggestion) == "string" then
logSuggestion = detailsOrSuggestion
else
logSuggestion = ErrorCodes.getSuggestion(code)
end
else
message = codeOrMessage
if type(messageOrDetails) == "table" then
details = messageOrDetails
logSuggestion = detailsOrSuggestion
elseif type(messageOrDetails) == "string" then
logSuggestion = messageOrDetails
end
end
-- Log the error
self:_writeLog("ERROR", LOG_LEVEL.ERROR, module, code, message, details, logSuggestion)
if self.includeStackTrace then
formattedMessage = formattedMessage .. self:_formatStackTrace(3)
end
error(formattedMessage, 2)
end
--- Print a warning (non-critical, continues execution)
---@param module string The module name
---@param codeOrMessage string Warning code or message
---@param messageOrDetails string|table|nil Message or details
---@param detailsOrSuggestion table|string|nil Details or suggestion
---@param suggestion string|nil Suggestion
function ErrorHandler:warn(module, codeOrMessage, messageOrDetails, detailsOrSuggestion, suggestion)
-- Parse arguments for logging
local code = nil
local message = codeOrMessage
local details = nil
local logSuggestion = nil
if type(codeOrMessage) == "string" and ErrorCodes.get(codeOrMessage) then
code = codeOrMessage
message = messageOrDetails or ErrorCodes.describe(code)
if type(detailsOrSuggestion) == "table" then
details = detailsOrSuggestion
logSuggestion = suggestion or ErrorCodes.getSuggestion(code)
elseif type(detailsOrSuggestion) == "string" then
logSuggestion = detailsOrSuggestion
else
logSuggestion = ErrorCodes.getSuggestion(code)
end
else
message = codeOrMessage
if type(messageOrDetails) == "table" then
details = messageOrDetails
logSuggestion = detailsOrSuggestion
elseif type(messageOrDetails) == "string" then
logSuggestion = messageOrDetails
end
end
-- Log the warning
self:_writeLog("WARNING", LOG_LEVEL.WARNING, module, code, message, details, logSuggestion)
end
--- Validate that a value is not nil
---@param module string The module name
---@param value any The value to check
---@param paramName string The parameter name
---@return boolean True if valid
function ErrorHandler:assertNotNil(module, value, paramName)
if value == nil then
self:error(module, "VAL_003", "Required parameter missing", {
parameter = paramName,
})
return false
end
return true
end
--- Validate that a value is of the expected type
---@param module string The module name
---@param value any The value to check
---@param expectedType string The expected type name
---@param paramName string The parameter name
---@return boolean True if valid
function ErrorHandler:assertType(module, value, expectedType, paramName)
local actualType = type(value)
if actualType ~= expectedType then
self:error(module, "VAL_001", "Invalid property type", {
property = paramName,
expected = expectedType,
got = actualType,
})
return false
end
return true
end
--- Validate that a number is within a range
---@param module string The module name
---@param value number The value to check
---@param min number Minimum value (inclusive)
---@param max number Maximum value (inclusive)
---@param paramName string The parameter name
---@return boolean True if valid
function ErrorHandler:assertRange(module, value, min, max, paramName)
if value < min or value > max then
self:error(module, "VAL_002", "Property value out of range", {
property = paramName,
min = tostring(min),
max = tostring(max),
value = tostring(value),
})
return false
end
return true
end
--- Warn if a value is deprecated
---@param module string The module name
---@param oldName string The deprecated name
---@param newName string The new name to use
function ErrorHandler:warnDeprecated(module, oldName, newName)
self:warn(module, string.format("'%s' is deprecated. Use '%s' instead", oldName, newName))
end
--- Warn about a common mistake
---@param module string The module name
---@param issue string Description of the issue
---@param suggestion string Suggested fix
function ErrorHandler:warnCommonMistake(module, issue, suggestion)
self:warn(module, string.format("%s. Suggestion: %s", issue, suggestion))
end
return ErrorHandler