Add ModuleLoader for conditional module loading with graceful fallbacks

- Create ModuleLoader.lua with safeRequire() for optional module loading
- Implement null-object pattern for missing optional modules
- Update FlexLove.lua to use ModuleLoader for Performance, Animation, Blur, Theme, ImageRenderer, ImageScaler, ImageCache, NinePatch, and GestureRecognizer
- Add comprehensive test suite for ModuleLoader (18 tests)
- Validate FlexLove works correctly when optional modules are missing
- All tests pass (1253/1254 successes)
This commit is contained in:
Michael Freno
2025-11-25 13:27:14 -05:00
parent 57da711492
commit 94d1b759ae
4 changed files with 464 additions and 39 deletions

190
modules/ModuleLoader.lua Normal file
View File

@@ -0,0 +1,190 @@
---@class ModuleLoader
local ModuleLoader = {}
-- Module registry to track loaded vs. stub modules
ModuleLoader._registry = {}
ModuleLoader._ErrorHandler = nil
--- Initialize ModuleLoader with dependencies
---@param deps table
function ModuleLoader.init(deps)
ModuleLoader._ErrorHandler = deps.ErrorHandler
end
--- Create a null-object stub for a missing optional module
--- Provides safe defaults that won't cause runtime errors
---@param moduleName string
---@return table
local function createNullObject(moduleName)
local stub = {
_isStub = true,
_moduleName = moduleName,
}
-- Common method stubs that return safe defaults
local metatable = {
__index = function(_, key)
-- Common initialization method
if key == "init" then
return function() return stub end
end
-- Common constructor method
if key == "new" then
return function() return stub end
end
-- Common draw method
if key == "draw" then
return function() end
end
-- Common update method
if key == "update" then
return function() end
end
-- Common render method
if key == "render" then
return function() end
end
-- Common cleanup method
if key == "destroy" then
return function() end
end
-- Common cleanup method
if key == "cleanup" then
return function() end
end
-- Common clear method
if key == "clear" then
return function() end
end
-- Common reset method
if key == "reset" then
return function() end
end
-- Common get method
if key == "get" then
return function() return nil end
end
-- Common set method
if key == "set" then
return function() end
end
-- Common load method
if key == "load" then
return function() return stub end
end
-- Common cache-related methods
if key == "cache" or key == "getCache" or key == "clearCache" then
return function() return {} end
end
-- Return nil for unknown properties (allows safe property access)
return nil
end,
-- Make function calls safe (in case the stub itself is called)
__call = function()
return stub
end,
}
setmetatable(stub, metatable)
return stub
end
--- Safely require a module with graceful fallback for optional modules
--- Returns the module if it exists, or a null-object stub if it's optional and missing
--- Throws an error if a required module is missing
---@param modulePath string Full path to the module (e.g., "modules.Performance")
---@param isOptional boolean If true, returns null-object on failure; if false, throws error
---@return table module The loaded module or a null-object stub
function ModuleLoader.safeRequire(modulePath, isOptional)
-- Check if already loaded
if ModuleLoader._registry[modulePath] then
return ModuleLoader._registry[modulePath]
end
-- Attempt to load the module
local success, result = pcall(require, modulePath)
if success then
-- Module loaded successfully
ModuleLoader._registry[modulePath] = result
return result
else
-- Module failed to load
if isOptional then
-- Create null-object stub for optional module
local stub = createNullObject(modulePath)
ModuleLoader._registry[modulePath] = stub
-- Log warning about missing optional module
if ModuleLoader._ErrorHandler then
ModuleLoader._ErrorHandler:warn(
"ModuleLoader",
string.format("Optional module '%s' not found, using stub implementation", modulePath)
)
end
return stub
else
-- Required module is missing - throw error
error(string.format("Required module '%s' not found: %s", modulePath, tostring(result)))
end
end
end
--- Check if a module is actually loaded (not a stub)
---@param modulePath string Full path to the module
---@return boolean isLoaded True if module is loaded, false if it's a stub or not loaded
function ModuleLoader.isModuleLoaded(modulePath)
local module = ModuleLoader._registry[modulePath]
if not module then
return false
end
-- Check if it's a stub
return not module._isStub
end
--- Get list of all loaded modules
---@return table modules List of module paths that are actually loaded (not stubs)
function ModuleLoader.getLoadedModules()
local loaded = {}
for path, module in pairs(ModuleLoader._registry) do
if not module._isStub then
table.insert(loaded, path)
end
end
return loaded
end
--- Get list of all stub modules
---@return table stubs List of module paths that are stubs
function ModuleLoader.getStubModules()
local stubs = {}
for path, module in pairs(ModuleLoader._registry) do
if module._isStub then
table.insert(stubs, path)
end
end
return stubs
end
--- Clear the module registry (useful for testing)
function ModuleLoader._clearRegistry()
ModuleLoader._registry = {}
end
return ModuleLoader