--[[ A function that allows adding constraints on tables. This works by makeing a shallow copy of the table, and setting __index and __newindex metamethods of the (now empty) table, that access the appropriate values of the shallow copy after doing checks. constrain(tbl, ("get " | "set ") + "field name", function(tbl, value) --this function is called with the table ("self") and the --value that is being set or retrived end) This module is just a function that can add constraints on a table. Use it like this: local constrain = require("constrain") local my_tbl = {} constrain(my_tbl,"get my_field",function(self,value) --Here, self is my_tbl, value will be --"my_field" assert(value > 0 and value < 20, "my_value must be between 0 and 20") end) --From now on, if my_tbl.my_field gets set to anything outside of (0:20), --an error will be thrown. This function should be totally transparent to the outside. ]] local constrained_tables = {} return function(tbl,trigger,func) local is_empty = true for k,v in pairs(tbl) do is_empty = false break; end local meta = getmetatable(tbl) --This table has never had constrain() called on it before, --make the shallow copy with hooks and stuff if constrained_tables[tbl] == nil then --Copy all the variables into a shallow copy local shallow_copy = {} for k,v in pairs(tbl) do shallow_copy[k] = v tbl[k] = nil end --Set the shallow copy's metatable to the original table's --metatable setmetatable(shallow_copy,meta) --Set the original table's metatable to the hookable thing local t_meta = {} t_meta.get_hooks = {} t_meta.set_hooks = {} t_meta.__index = function(self,key) local ret = shallow_copy[key] for _,hook in pairs(t_meta.get_hooks[key] or {}) do hook(self,ret) end return ret end t_meta.__newindex = function(self,key,value) for _,hook in pairs(t_meta.set_hooks[key] or {}) do hook(self,value) end shallow_copy[key] = value end t_meta.__pairs = function(self) return pairs(shallow_copy) end t_meta.__len = function(self) return #shallow_copy end t_meta.__call = function(self,...) return shallow_copy(...) end t_meta.__mode = meta and meta.__mode or "" t_meta.__gc = meta and meta.__gc t_meta.__metatable = meta setmetatable(tbl,t_meta) constrained_tables[tbl] = t_meta end --By this point, tbl is a "constrainable" table. we can just --add functios to it's get_hooks and set_hooks to do whatever checking we need --functions added to get_hooks should be -- function(self,value) ... end --functions added to set_hooks should be -- function(self,value) ... end -- local getset,field = string.match(trigger,"(get) (.+)") if not getset then getset,field = string.match(trigger,"(set) (.+)") end --local getset,field = string.match(trigger,"(get|set) (.+)") assert(getset,"constrain() must be called with \"get\" or \"set\" as the first word in the pattern") assert(field,"constrain() must specify a field to trigger on") if getset == "get" then local gh = constrained_tables[tbl].get_hooks gh[field] = gh[field] or {} table.insert(gh[field],func) elseif getset == "set" then local sh = constrained_tables[tbl].set_hooks sh[field] = sh[field] or {} table.insert(sh[field],func) end end