mirror of
https://github.com/centau/vide.git
synced 2026-08-20 06:21:37 +00:00
* allow nesting parent property * update changelog * fix mock instances Parent property not being a proxy * add tests Closes #47
305 lines
8.4 KiB
Text
305 lines
8.4 KiB
Text
--[[
|
|
|
|
contains mock Roblox API interfaces for unit testing
|
|
|
|
]]
|
|
|
|
local Signal = {} :: any do
|
|
Signal.__index = Signal
|
|
Signal.__type = "RBXScriptSignal"
|
|
|
|
local function new_connection(signal, fn)
|
|
return {
|
|
signal = signal,
|
|
fn = fn,
|
|
|
|
Disconnect = function(self)
|
|
local i = table.find(signal.connections, self)
|
|
if not i then return end
|
|
table.remove(signal.connections, i)
|
|
end
|
|
}
|
|
end
|
|
|
|
function Signal.new()
|
|
return setmetatable({
|
|
connections = {}
|
|
}, Signal)
|
|
end
|
|
|
|
function Signal.Connect(self, fn)
|
|
local con = new_connection(self, fn)
|
|
table.insert(self.connections, con)
|
|
return con
|
|
end
|
|
|
|
function Signal.fire(self, ...)
|
|
for i = #self.connections, 1, -1 do
|
|
self.connections[i].fn(...)
|
|
end
|
|
end
|
|
end
|
|
|
|
local Instance = {} :: any do
|
|
--[[
|
|
|
|
attempt to mimic roblox engine's method of userdata reflection
|
|
proxy can gc independantly of actual instance data
|
|
proxy prevents gc of actual instance data
|
|
user code never has direct access to actual instance data, only to proxy
|
|
internal weak map kept data -> proxy
|
|
|
|
]]
|
|
|
|
type userdata = { __USERDATA: true }
|
|
|
|
type ProxyMT = {
|
|
proxy: userdata,
|
|
data: Data,
|
|
__index: any,
|
|
__newindex: any
|
|
}
|
|
|
|
type Data = {
|
|
name: string,
|
|
parent: Data?,
|
|
children: { Data },
|
|
changed: { [string]: RBXScriptSignal },
|
|
properties: { [string]: unknown },
|
|
destroying: RBXScriptSignal,
|
|
class: string,
|
|
type: "Instance"
|
|
}
|
|
|
|
local function deep_clone<T>(template: T & {}): T
|
|
local t = table.clone(template :: {}) :: {}
|
|
|
|
for i, v in next, t do
|
|
if type(v) == "table" then
|
|
t[i] = deep_clone(v)
|
|
end
|
|
end
|
|
|
|
return t :: T & {}
|
|
end
|
|
|
|
local proxies = {} :: { [Data]: userdata? }
|
|
setmetatable(proxies :: any, { __mode = "v" })
|
|
|
|
-- allocate variables for the metamethods as __index and get_proxy cross refrences each other
|
|
local __index, __newindex
|
|
|
|
local function get_data(userdata: userdata): Data
|
|
local function f(userdata: userdata): ProxyMT
|
|
return getmetatable(userdata :: any)
|
|
end
|
|
|
|
return f(userdata).data
|
|
end
|
|
|
|
local function get_proxy(data: Data): userdata
|
|
return proxies[data] or (function()
|
|
local userdata = newproxy(true)
|
|
local proxy = getmetatable(userdata)
|
|
proxy.proxy = userdata
|
|
proxy.data = data
|
|
proxy.__index = __index
|
|
proxy.__newindex = __newindex
|
|
proxies[data] = userdata
|
|
return userdata
|
|
end)()
|
|
end
|
|
|
|
local function is_instance(value: unknown): boolean
|
|
local mt = getmetatable(value :: any)
|
|
return mt and mt.data and mt.data.type == "Instance"
|
|
end
|
|
|
|
local methods = {}
|
|
|
|
__index = function(userdata: userdata, property: string): ()
|
|
local data = get_data(userdata)
|
|
return if methods[property] then methods[property]
|
|
elseif property == "Name" then data.name
|
|
elseif property == "Parent" then (data.parent and get_proxy(data.parent))
|
|
elseif property == "Destroying" then data.destroying
|
|
else data.properties[property]
|
|
end
|
|
|
|
__newindex = function(userdata: userdata, property: string, value: unknown)
|
|
local data = get_data(userdata)
|
|
if property == "Name" then
|
|
if type(value) ~= "string" then error("name must be a string", 2) end
|
|
data.name = value :: string
|
|
elseif property == "Parent" then
|
|
assert(value == nil or is_instance(value), "attempt to set non-instance as parent")
|
|
local parent = data.parent
|
|
if parent then
|
|
data.parent = nil
|
|
table.remove(parent.children, table.find(parent.children, data))
|
|
end
|
|
if value then
|
|
data.parent = get_data(value :: userdata)
|
|
table.insert(get_data(value :: userdata).children, data)
|
|
end
|
|
else
|
|
data.properties[property] = value
|
|
end
|
|
|
|
if data.changed[property] then
|
|
Signal.fire(data.changed[property])
|
|
end
|
|
end
|
|
|
|
function Instance.new(class: string): Instance
|
|
local data = {
|
|
name = "UNNAMED",
|
|
parent = nil,
|
|
children = {},
|
|
changed = {},
|
|
properties = {},
|
|
class = class,
|
|
destroying = Signal.new() :: any,
|
|
type = "Instance" :: "Instance"
|
|
}
|
|
|
|
return get_proxy(data) :: any
|
|
end
|
|
|
|
function Instance.is_instance(value: unknown): boolean
|
|
return is_instance(value)
|
|
end
|
|
|
|
function methods.Clone(userdata: userdata): userdata
|
|
local data = get_data(userdata)
|
|
local clone_userdata = (Instance.new("") :: any) :: userdata
|
|
local clone_data = get_data(clone_userdata)
|
|
|
|
for i, v in next, deep_clone(data) do
|
|
clone_data[i] = v
|
|
end
|
|
|
|
return clone_userdata
|
|
end
|
|
|
|
function methods.FindFirstChild(userdata: userdata, target: string): userdata?
|
|
local data = get_data(userdata)
|
|
for _, child in data.children do
|
|
if child.name == target then
|
|
return get_proxy(child)
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
function methods.GetChildren(userdata: userdata): { userdata }
|
|
local children = get_data(userdata).children
|
|
local userdatas = table.create(#children)
|
|
|
|
for i, child in next, children do
|
|
userdatas[i] = get_proxy(child)
|
|
end
|
|
|
|
return userdatas
|
|
end
|
|
|
|
function methods.GetPropertyChangedSignal(userdata: userdata, property: string): RBXScriptSignal
|
|
local data = get_data(userdata)
|
|
if not data.changed[property] then
|
|
data.changed[property] = Signal.new() :: any
|
|
end
|
|
return data.changed[property]
|
|
end
|
|
|
|
function methods.Destroy(userdata: userdata)
|
|
local data = get_data(userdata);
|
|
Signal.fire(data.destroying)
|
|
data.parent = nil
|
|
if data.changed["Parent"] then
|
|
Signal.fire(data.changed["Parent"])
|
|
end
|
|
end
|
|
end
|
|
|
|
local function table_to_proxy(t: any)
|
|
local proxy = newproxy(true)
|
|
local mt = getmetatable(proxy)
|
|
|
|
for i, v in getmetatable(t) do
|
|
mt[i] = v
|
|
end
|
|
|
|
function mt:__index(i)
|
|
return t[i]
|
|
end
|
|
|
|
function mt:__newindex(i, v)
|
|
t[i] = v
|
|
end
|
|
|
|
return proxy
|
|
end
|
|
|
|
local Color3 = { __type = "Color3" } :: any do
|
|
function Color3.new(r, g, b)
|
|
return table_to_proxy(setmetatable({ r = r, g = g, b = b }, Color3))
|
|
end
|
|
|
|
function Color3.__eq(a, b)
|
|
return a.r == b.r and a.g == b.g and a.b == b.b
|
|
end
|
|
end
|
|
|
|
local Vector2 = { __type = "Vector2" } :: any do
|
|
function Vector2.new(x, y)
|
|
return table_to_proxy(setmetatable({ x = x, y = y }, Vector2))
|
|
end
|
|
|
|
function Vector2.__eq(a, b)
|
|
return a.x == b.x and a.y == b.y
|
|
end
|
|
end
|
|
|
|
local UDim2 = { __type = "UDim2" } :: any do
|
|
function UDim2.new(sx, ox, sy, oy)
|
|
return table_to_proxy(setmetatable({ x = { scale = sx, offset = ox }, y = { scale = sy, offset = oy } }, UDim2))
|
|
end
|
|
|
|
function UDim2.fromScale(x, y)
|
|
return table_to_proxy(setmetatable({ x = { scale = x, offset = 0 }, y = { scale = y, offset = 0 } }, UDim2))
|
|
end
|
|
|
|
function UDim2.__eq(a, b)
|
|
return a.x.scale == b.x.scale and
|
|
b.x.offset == b.x.offset and
|
|
a.y.scale == b.y.scale and
|
|
a.y.offset == b.y.offset
|
|
end
|
|
end
|
|
|
|
local Enum = {} :: any do
|
|
setmetatable(Enum, { __index = function(self, index)
|
|
local v = setmetatable({}, { __index = function(self, index)
|
|
self[index] = true
|
|
return true
|
|
end})
|
|
self[index] = v
|
|
return v
|
|
end})
|
|
end
|
|
|
|
local typeof = function(v)
|
|
return if Instance.is_instance(v) then "Instance"
|
|
elseif getmetatable(v) and getmetatable(v).__type then getmetatable(v).__type
|
|
else type(v)
|
|
end :: any
|
|
|
|
return {
|
|
Signal = Signal,
|
|
Instance = Instance :: typeof(Instance),
|
|
Color3 = Color3 :: typeof(Color3),
|
|
Vector2 = Vector2 :: typeof(Vector2),
|
|
UDim2 = UDim2 :: typeof(UDim2),
|
|
Enum = Enum :: typeof(Enum),
|
|
typeof = typeof :: typeof(typeof)
|
|
}
|