Jump to content

Module:ErrorHandling

Documentation for this module may be created at Module:ErrorHandling/doc

--[[
* Name: ErrorHandling
* Author: Mark W. Datysgeld
* Description: Centralized error handling system for templates with error tracking, reporting, and graceful failure recovery
* Notes: Formats errors as data attributes for debug monitor display in browser console; creates error contexts; provides protected function execution; supports emergency fallback displays; completely deprecates mw.log usage
]]

local ErrorHandling = {}

-- Standard error messages for common scenarios
ErrorHandling.STANDARD_MESSAGES = {
    TEMPLATE_RENDER_ERROR = "<!-- Error rendering template -->",
    MODULE_LOAD_ERROR = "<!-- Error loading module -->",
    PROPERTY_ERROR = "<!-- Error processing semantic properties -->"
    -- Add more standard messages as needed
}

-- Creates a new error context for a template
-- @param templateName The name of the template (for error reporting)
-- @return A new error context table
function ErrorHandling.createContext(templateName)
    return {
        TEMPLATE_NAME = templateName or "UnknownTemplate",
        START_TIME = os.clock() * 1000,  -- Store in milliseconds
        ERROR_COUNT = 0,
        STATUS_COUNT = 0,
        ERRORS = {},
        STATUSES = {},
        HAS_CRITICAL_ERROR = false
    }
end

-- Add an error to the context
-- @param context The error context
-- @param source The source of the error (function name)
-- @param message The error message
-- @param details Optional additional details about the error
-- @param isCritical Whether this is a critical error (default: false)
-- @return The error context (for chaining)
function ErrorHandling.addError(context, source, message, details, isCritical)
    -- Increment the error count
    context.ERROR_COUNT = context.ERROR_COUNT + 1
    
    -- Add the error to the list
    table.insert(context.ERRORS, {
        id = context.ERROR_COUNT,
        source = source or "unknown",
        message = message or "Unknown error",
        details = details or "",
        isCritical = isCritical or false
    })
    
    -- Update critical error flag if needed
    if isCritical then
        context.HAS_CRITICAL_ERROR = true
    end
    
    -- Return the error context for chaining
    return context
end

-- Add a status message to the context
-- @param context The error context
-- @param source The source of the status message (function name)
-- @param message The status message
-- @param details Optional additional details
-- @return The context (for chaining)
function ErrorHandling.addStatus(context, source, message, details)
    -- Increment the status count
    context.STATUS_COUNT = (context.STATUS_COUNT or 0) + 1
    
    -- Add the status to the list
    table.insert(context.STATUSES, {
        id = context.STATUS_COUNT,
        source = source or "unknown",
        message = message or "Unknown status",
        details = details or ""
    })
    
    -- Return the context for chaining
    return context
end

-- Format the error context for debugging output using data attributes
-- @param context The error context
-- @return HTML string with data attributes containing error information
function ErrorHandling.formatOutput(context)
    -- If no errors, return empty string
    if context.ERROR_COUNT == 0 then
        return ""
    end
    
    -- Build minimal data attribute div
    local divAttributes = {
        ['data-template-error'] = "1",
        ['data-error-count'] = tostring(context.ERROR_COUNT)
    }
    
    -- Add individual error attributes with minimal naming
    for _, err in ipairs(context.ERRORS) do
        divAttributes['data-error-' .. err.id .. '-source'] = err.source
        divAttributes['data-error-' .. err.id .. '-msg'] = err.message
        if err.details and err.details ~= "" then
            divAttributes['data-error-' .. err.id .. '-details'] = err.details
        end
    end
    
    -- Build attribute string efficiently
    local attrStr = ""
    for k, v in pairs(divAttributes) do
        attrStr = attrStr .. ' ' .. k .. '="' .. v .. '"'
    end
    
    -- Create hidden div with minimal footprint
    return string.format('<div style="display:none"%s></div>', attrStr)
end

-- Format the status context for debugging output using data attributes
-- @param context The error context
-- @return HTML string with data attributes containing status information
function ErrorHandling.formatStatusOutput(context)
    -- If no statuses, return empty string
    if not context.STATUSES or #context.STATUSES == 0 then
        return ""
    end
    
    -- Build minimal data attribute div
    local divAttributes = {
        ['data-template-status'] = "1",
        ['data-status-count'] = tostring(#context.STATUSES)
    }
    
    -- Add individual status attributes with minimal naming
    for _, stat in ipairs(context.STATUSES) do
        divAttributes['data-status-' .. stat.id .. '-source'] = stat.source
        divAttributes['data-status-' .. stat.id .. '-msg'] = stat.message
        if stat.details and stat.details ~= "" then
            divAttributes['data-status-' .. stat.id .. '-details'] = stat.details
        end
    end
    
    -- Build attribute string efficiently
    local attrStr = ""
    for k, v in pairs(divAttributes) do
        attrStr = attrStr .. ' ' .. k .. '="' .. v .. '"'
    end
    
    -- Create hidden div with minimal footprint
    return string.format('<div style="display:none"%s></div>', attrStr)
end

-- Formats and combines both error and status outputs
-- @param context The context object
-- @return A string containing HTML for both errors and statuses
function ErrorHandling.formatCombinedOutput(context)
    local errorOutput = ErrorHandling.formatOutput(context)
    local statusOutput = ErrorHandling.formatStatusOutput(context)
    
    -- Simply concatenate the two outputs. If one is empty, it won't affect the other.
    -- A newline is added to ensure they are on separate lines in the HTML source.
    if errorOutput ~= "" and statusOutput ~= "" then
        return errorOutput .. "\n" .. statusOutput
    elseif errorOutput ~= "" then
        return errorOutput
    else
        return statusOutput
    end
end

-- Create emergency display for catastrophic failures with minimal markup
-- @param args The template arguments
-- @param errorSource The source of the error
-- @param errorMessage The error message
-- @param templateType The type of template (e.g. "Event", "Person")
-- @return HTML string with a minimal template display and error data
function ErrorHandling.createEmergencyDisplay(args, errorSource, errorMessage, templateType)
    -- Extract critical information for minimal display
    local title = args.name or ("Unnamed " .. (templateType or "Item"))
    
    -- Create minimal fallback with error data attributes
    return string.format(
        '{| class="template-table" cellpadding="2"\n' ..
        '|-\n| class="template-title template-title-%s" | <span>%s</span>\n' ..
        '|}\n' ..
        '<div style="display:none" data-template-error="1" data-critical="1" ' ..
        'data-error-1-source="%s" data-error-1-msg="%s"></div>',
        string.lower(templateType or "generic"),
        title,
        errorSource,
        errorMessage
    )
end

-- Protect a function with pcall and error handling
-- @param context The error context
-- @param funcName Name of the function (for error reporting)
-- @param func The function to protect
-- @param fallback Value to return if the function fails
-- @param ... Arguments to pass to the function
-- @return The result of the function, or fallback value on error
function ErrorHandling.protect(context, funcName, func, fallback, ...)
    local success, result = pcall(func, ...)
    
    if not success then
        ErrorHandling.addError(
            context,
            funcName,
            "Function execution failed",
            tostring(result),
            false
        )
        return fallback
    end
    
    return result
end

-- Get a standard error message with fallback
-- @param messageKey The key of the standard message to retrieve
-- @param defaultMessage Optional fallback message if the key is not found
-- @return The standard message or the default message
function ErrorHandling.getMessage(messageKey, defaultMessage)
    if ErrorHandling.STANDARD_MESSAGES and ErrorHandling.STANDARD_MESSAGES[messageKey] then
        return ErrorHandling.STANDARD_MESSAGES[messageKey]
    end
    return defaultMessage or "<!-- Error -->"
end

-- Protected module loading
-- @param context The error context
-- @param moduleName Name of the module to load
-- @param isCritical Whether failure to load is critical (default: false)
-- @return The loaded module, or an empty table on failure
function ErrorHandling.safeRequire(context, moduleName, isCritical)
    local success, module = pcall(require, moduleName)
    if not success then
        -- Record the error
        ErrorHandling.addError(
            context,
            "ModuleLoading",
            "Failed to load module",
            moduleName,
            isCritical or false
        )
        
        -- Return an empty table as fallback
        return {}
    end
    return module
end

return ErrorHandling