aboutsummaryrefslogtreecommitdiff
path: root/src/constrain.lua
blob: 56f3b6a11cac0ad33d6b5f7774433fe64f7dac0a (plain)
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