---@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.", }, PERF_003 = { code = "FLEXLOVE_PERF_003", category = "PERF", description = "Large blur area in immediate mode", suggestion = "Consider using retained mode for this component to avoid recreating blur effects every frame.", }, -- 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 not levelNum or not self.logLevel or 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