Hmm I managed to get intercept __index recursively... ish... there's probably some cases where it doesn't work, and a determined user could definitely break it.
local outer = {
value = 1,
inner = {
value = 2,
deeper = {
value = 3,
weMustGoDeeper = {
value = 4,
},
},
},
}
function interceptIndexing(t, interceptor)
local interface = {}
setmetatable(interface, {
__index = function(_, k)
interceptor(t, k)
local v = rawget(t, k)
if type(v) == "table" then
return interceptIndexing(v, interceptor)
else
return v
end
end,
__eq = function(t1, t2)
if rawequal(t1, t2) then return true end
--Ensure t1 is interface
if rawequal(interface, t2) then
t1, t2 = t2, t1
end
if rawequal(t2, t) then return true end
return false
end
})
return interface
end
local interface = interceptIndexing(outer, function(t, k)
print("I '" .. k .. "' on " .. tostring(t))
end)
print("R", interface.value)
print("R", interface.inner.value)
print("R", interface.inner.deeper.value)
print("R", interface.inner.deeper.weMustGoDeeper.value)
print("Eq", interface == outer)
print("Eq", interface.inner == outer.inner)
Aaaand…
...here's my attempt at recursive __newindex intercepting:
function interceptIndexing(t, interceptIndex, interceptNewindex)
local interface = {}
return setmetatable(interface, {
__index = function(_, k)
interceptIndex(t, k)
local v = rawget(t, k)
if type(v) == "table" then
return interceptIndexing(v, interceptIndex, interceptNewindex)
else
return v
end
end,
__newindex = function (_, k, v)
interceptNewindex(t, k, v)
rawset(t, k, v)
end,
__eq = function(t1, t2)
if rawequal(t1, t2) then return true end
--Ensure t1 is interface
if rawequal(interface, t2) then
t2 = t1
end
if rawequal(t2, t) then return true end
return false
end
})
end
Here's my test data:
function namedTable(name, t) --so I don't have to compare table addresses >.<'
local t = t or {}
return setmetatable(t, {
__tostring = function(t)
return name
end
})
end
local outer = namedTable("tOuter", {
value = 1,
inner = namedTable("tInner", {
value = 2,
deeper = namedTable("tDeeper", {
value = 3,
weMustGoDeeper = namedTable("tWeMustGoDeeper", {
value = 4,
}),
}),
}),
})
And here are my tests:
local interface = interceptIndexing(outer, function(t, k)
print("index '" .. k .. "' on '" .. tostring(t) .. "'")
end, function(t, k, v)
print("newindex '" .. k .. "' = " .. tostring(v) .. " on '" .. tostring(t) .. "'")
end)
print("R", interface.value)
print("R", interface.inner.value)
print("R", interface.inner.deeper.value)
print("R", interface.inner.deeper.weMustGoDeeper.value)
print("Eq", interface == outer)
print("Eq", interface.inner == outer.inner)
interface.value = 'a'
print("R", interface.value)
interface.inner.value = 'b'
print("R", interface.inner.value)
interface.inner.deeper.weMustGoDeeper.value = 'd'
print("R", interface.inner.deeper.weMustGoDeeper.value)
Well… it doesn’t actually intercept because the interceptor functions don’t determine the actual result of indexing and/or settin
Here's a version that actually intercepts things, and my insane test cases that actually show some pretty cool possibilities
local outer = namedTable("tOuter", {
valueAyy = 1,
innerAyy = namedTable("tInner", {
valueAyy = 2,
deeperAyy = namedTable("tDeeper", {
valueAyy = 3,
weMustGoDeeperAyy = namedTable("tWeMustGoDeeper", {
valueAyy = 4,
}),
}),
}),
})
function interceptIndexing(t, interceptIndex, interceptNewindex)
local interface = {}
return setmetatable(interface, {
__index = function(_, k)
local v = interceptIndex(t, k)
if type(v) == "table" then
return interceptIndexing(v, interceptIndex, interceptNewindex)
else
return v
end
end,
__newindex = function (_, k, v)
interceptNewindex(t, k, v)
end,
__eq = function(t1, t2)
if rawequal(t1, t2) then return true end
--Ensure t1 is interface
if rawequal(interface, t2) then
t2 = t1
end
if rawequal(t2, t) then return true end
return false
end
})
end
local interface = interceptIndexing(outer, function(t, k)
print("index '" .. k .. "' on '" .. tostring(t) .. "'")
if k == 'mustNotBeIndexed' then
error(k)
end
return rawget(t, k .. "Ayy")
end, function(t, k, v)
print("newindex '" .. k .. "' = " .. tostring(v) .. " on '" .. tostring(t) .. "'")
if k == 'mustNotBeOverwritten' then
error(k)
elseif k == 'mustBe0Or1' then
if v == 0 or v == 1 then
rawset(t, k, v)
else
error(k)
end
else
rawset(t, k .. "Ayy", v .. "+You got INTERCEPTED! 😎")
end
end)
print("E", pcall(function()
return interface.inner.mustNotBeIndexed
end))
print("E", pcall(function()
interface.inner.mustNotBeOverwritten = "swag swag"
end))
print("E", pcall(function()
interface.inner.mustBe0Or1 = 1
return "mustBe0Or1"
end))
print("E", pcall(function()
interface.inner.mustBe0Or1 = 2
end))
print("R", interface.value)
print("R", interface.inner.value)
print("R", interface.inner.deeper.value)
print("R", interface.inner.deeper.weMustGoDeeper.value)
print("Eq", interface == outer)
print("Eq", interface.inner == outer.inner)
interface.value = 'a'
print("R", interface.value)
interface.inner.value = 'b'
print("R", interface.inner.value)
interface.inner.deeper.weMustGoDeeper.value = 'd'
print("R", interface.inner.deeper.weMustGoDeeper.value)