1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
|
--[[
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
|