Defensive Lua: Advanced Techniques for Robust Code

August 29, 2024

Defensive Lua

-- Defensive programming with metatables in Lua

-- Create a function to generate a defensive object
local function createDefensiveObject(initialData)
  local data = initialData or {}
  local methods = {}
  local readOnlyKeys = {}

  -- Metatable for the object
  local mt = {
    __index = function(t, k)
      if methods[k] then
        return methods[k]
      elseif data[k] ~= nil then
        return data[k]
      else
        error("Attempt to access non-existent key: " .. tostring(k), 2)
      end
    end,

    __newindex = function(t, k, v)
      if readOnlyKeys[k] then
        error("Attempt to modify read-only property: " .. tostring(k), 2)
      elseif data[k] ~= nil then
        if type(v) ~= type(data[k]) then
          error("Type mismatch. Expected " .. type(data[k]) .. ", got " .. type(v), 2)
        end
        data[k] = v
      else
        error("Attempt to add new key: " .. tostring(k) .. ". Use addProperty() instead.", 2)
      end
    end,

    __pairs = function()
      return next, data, nil
    end
  }

  -- The object itself
  local obj = {}

  -- Method to add a new property
  function methods.addProperty(key, value, readOnly)
    if data[key] ~= nil then
      error("Property already exists: " .. tostring(key), 2)
    end
    data[key] = value
    if readOnly then
      readOnlyKeys[key] = true
    end
  end

  -- Method to add a new method
  function methods.addMethod(name, func)
    if methods[name] then
      error("Method already exists: " .. tostring(name), 2)
    end
    methods[name] = func
  end

  -- Method to validate method calls
  function methods.validateMethodCall(name, ...)
    local method = methods[name]
    if not method then
      error("Attempt to call non-existent method: " .. tostring(name), 2)
    end
    -- Add any additional validation logic here
    return method(...)
  end

  return setmetatable(obj, mt)
end

-- Usage example
local person = createDefensiveObject({name = "Alice", age = 30})

-- Add a read-only property
person:addProperty("id", "12345", true)

-- Add a method
person:addMethod("greet", function(self)
  return "Hello, I'm " .. self.name
end)

-- Accessing properties
print(person.name)  -- Output: Alice
print(person:greet())  -- Output: Hello, I'm Alice

-- These will raise errors:
-- person.id = "54321"  -- Attempt to modify read-only property
-- person.newProp = "value"  -- Attempt to add new key
-- person.age = "31"  -- Type mismatch
-- print(person.nonexistent)  -- Attempt to access non-existent key
-- person:nonexistentMethod()  -- Attempt to call non-existent method

-- Iterate over properties
for k, v in pairs(person) do
  print(k, v)
end