Skip to content
This repository has been archived by the owner on Jul 12, 2020. It is now read-only.

BaseInstance:Clone #180

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
17 changes: 16 additions & 1 deletion lib/InstanceProperty.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
local typeof = import("./functions/typeof")
local assign = import("./assign")
local cloneKey = import("./cloneKey")
local typeof = import("./functions/typeof")

local InstanceProperty = {}

Expand All @@ -14,6 +15,20 @@ function InstanceProperty.normal(config)
getDefault = function()
return nil
end,
clone = function(self, key)
local value = getmetatable(self).instance.properties[key]

if typeof(value) == "Instance" then
return value
elseif type(value) == "userdata" and typeof(value) ~= "userdata" then
-- Lemur implemented userdata, should have its own Clone function
local metatable = assert(getmetatable(value), "no metatable on cloning userdata")
local cloneImpl = assert(metatable[cloneKey], "no clone implementation for " .. typeof(value))
return cloneImpl(value)
end

return value
end,
}, config)
end

Expand Down
2 changes: 2 additions & 0 deletions lib/Signal.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
executing an event.
]]

local cloneKey = import("./cloneKey")
local typeKey = import("./typeKey")

local function immutableAppend(list, ...)
Expand Down Expand Up @@ -46,6 +47,7 @@ function Signal.new()
local self = newproxy(true)
getmetatable(self).__index = Signal
getmetatable(self).internal = internal
getmetatable(self)[cloneKey] = Signal.new
getmetatable(self)[typeKey] = "RBXScriptSignal"

return self
Expand Down
13 changes: 13 additions & 0 deletions lib/cloneKey.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
--[[
This key is used to mark Roblox objects and implement Clone implementations

Use it as a key into a userdata object's metatable.
]]

local cloneKey = newproxy(true)

getmetatable(cloneKey).__tostring = function()
return "<Lemur Clone Identifier>"
end

return cloneKey
6 changes: 6 additions & 0 deletions lib/createEnum.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
local cloneKey = import("./cloneKey")
local typeKey = import("./typeKey")

local function cloneEnumItem(item)
return item
end

local function createEnumVariant(enum, variantName, variantValue)
local enumVariant = newproxy(true)

Expand All @@ -9,6 +14,7 @@ local function createEnumVariant(enum, variantName, variantValue)
EnumType = enum,
}

getmetatable(enumVariant)[cloneKey] = cloneEnumItem
getmetatable(enumVariant)[typeKey] = "EnumItem"

getmetatable(enumVariant).__tostring = function()
Expand Down
23 changes: 23 additions & 0 deletions lib/createEnum_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,27 @@ describe("createEnum", function()
tostring(variant.something)
end)
end)

it("should clone", function()
local BaseInstance = import("./instances/BaseInstance")
local InstanceProperty = import("./InstanceProperty")

local CreatableClass = BaseInstance:extend("Creatable", {
creatable = true,
})

CreatableClass.properties.Value = InstanceProperty.normal({})

local enum = createEnum("TestEnum", {
A = 0,
B = 1,
C = 2,
})

local instance = CreatableClass:new()
instance.Value = enum.A

local clone = instance:Clone()
assert.equal(clone.Value, enum.A)
end)
end)
30 changes: 30 additions & 0 deletions lib/instances/BaseInstance.lua
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ BaseInstance.properties.Parent = InstanceProperty.normal({

self:_PropagateAncestryChanged(self, value)
end,

clone = function()
return nil
end,
})

BaseInstance.prototype = {}
Expand All @@ -102,6 +106,29 @@ function BaseInstance.prototype:ClearAllChildren()
end
end

function BaseInstance.prototype:Clone()
local class = getmetatable(self).class
if not class.options.creatable then
error(string.format("%s cannot be cloned", class.name))
end

local clone = class:new()
local cloneProperties = getmetatable(clone).instance.properties
local classProperties = class.properties

for _, child in pairs(self:GetChildren()) do
child:Clone().Parent = clone
end

for propertyName, prototype in pairs(classProperties) do
cloneProperties[propertyName] = prototype.clone(self, propertyName)
end

class.postClone(clone, self)

return clone
end

function BaseInstance.prototype:FindFirstAncestor(name)
local level = self.Parent

Expand Down Expand Up @@ -372,6 +399,9 @@ end
function BaseInstance:init(instance, ...)
end

function BaseInstance:postClone(old)
end

--[[
Create a new instance class with the given name.
]]
Expand Down
97 changes: 96 additions & 1 deletion lib/instances/BaseInstance_spec.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
local Game = import("./Game")
local Folder = import("./Folder")
local Game = import("./Game")
local typeof = import("../functions/typeof")

local BaseInstance = import("./BaseInstance")
Expand Down Expand Up @@ -719,4 +719,99 @@ describe("instances.BaseInstance", function()
assert.spy(spy).was_called_with(child)
end)
end)

describe("Clone", function()
it("should clone the entire object", function()
local CreatableClass = BaseInstance:extend("Creatable", {
creatable = true,
})

local instance = CreatableClass:new()
instance.Name = "Alpha"

local clone = instance:Clone()
assert.not_equals(instance, clone)
assert.equal(instance.Name, "Alpha")
assert.equal(clone.Name, "Alpha")

clone.Name = "Beta"
assert.equal(instance.Name, "Alpha")
assert.equal(clone.Name, "Beta")
end)

it("should clone fresh events", function()
local CreatableClass = BaseInstance:extend("Creatable", {
creatable = true,
})

local instanceA = CreatableClass:new()
local spyA = spy.new(function() end)
instanceA.ChildAdded:Connect(spyA)

local instanceB = instanceA:Clone()
local spyB = spy.new(function() end)
instanceB.ChildAdded:Connect(spyB)

assert.spy(spyA).was_not_called()
assert.spy(spyB).was_not_called()

BaseInstance:new().Parent = instanceA
assert.spy(spyA).was_called()
assert.spy(spyB).was_not_called()
end)

it("should not clone the parent", function()
local CreatableClass = BaseInstance:extend("Creatable", {
creatable = true,
})

local instance = CreatableClass:new()
instance.Parent = CreatableClass:new()

local clone = instance:Clone()
assert.is_nil(clone.Parent)
assert.not_nil(instance.Parent)
end)

it("should clone children", function()
local CreatableClass = BaseInstance:extend("Creatable", {
creatable = true,
})

local parent = CreatableClass:new()
local child = CreatableClass:new()
child.Name = "Child"
child.Parent = parent

local clone = parent:Clone()
assert.equal(#clone:GetChildren(), 1)
assert.not_nil(clone:FindFirstChild("Child"))
end)

it("should not clone instance properties", function()
local InstanceProperty = import("../InstanceProperty")

local CreatableClass = BaseInstance:extend("Creatable", {
creatable = true,
})

CreatableClass.properties.Value = InstanceProperty.normal({})

local ref = CreatableClass:new()

local instance = CreatableClass:new()
instance.Value = ref

local clone = instance:Clone()
assert.equal(clone.Value, ref)
assert.equal(instance.Value, ref)
end)

it("should error when trying to clone a non-creatable object", function()
local instance = BaseInstance:new()
assert.has.errors(function()
instance:Clone()
end)
end)
end)
end)
4 changes: 4 additions & 0 deletions lib/instances/ModuleScript.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ local ModuleScript = BaseInstance:extend("ModuleScript", {
creatable = true,
})

function ModuleScript:postClone(old)
getmetatable(self).instance.modulePath = getmetatable(old).instance.modulePath
end

ModuleScript.properties.Source = InstanceProperty.normal({
getDefault = function()
return ""
Expand Down
4 changes: 4 additions & 0 deletions lib/types/Color3.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
local assign = import("../assign")
local cloneKey = import("../cloneKey")
local typeKey = import("../typeKey")

local function lerpNumber(a, b, alpha)
Expand All @@ -24,6 +25,9 @@ function prototype:lerp(goal, alpha)
end

local metatable = {}
metatable[cloneKey] = function(color3)
return Color3.new(color3.r, color3.g, color3.b)
end
metatable[typeKey] = "Color3"

function metatable:__index(key)
Expand Down
20 changes: 20 additions & 0 deletions lib/types/Color3_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,24 @@ describe("types.Color3", function()
local type = typeof(Color3.new())
assert.are.equal("Color3", type)
end)

it("should clone", function()
local BaseInstance = import("../instances/BaseInstance")
local InstanceProperty = import("../InstanceProperty")

local CreatableClass = BaseInstance:extend("Creatable", {
creatable = true,
})

CreatableClass.properties.Value = InstanceProperty.normal({})

local color3 = Color3.new(1, 0, 0)

local instance = CreatableClass:new()
instance.Value = color3

local clone = instance:Clone()
assert.equal(clone.Value, color3)
assert.not_equals(getmetatable(color3), getmetatable(clone.Value))
end)
end)
4 changes: 4 additions & 0 deletions lib/types/Rect.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
local assign = import("../assign")
local cloneKey = import("../cloneKey")
local typeKey = import("../typeKey")
local typeof = import("../functions/typeof")
local Vector2 = import("./Vector2")
Expand All @@ -14,6 +15,9 @@ setmetatable(Rect, {
local prototype = {}

local metatable = {}
metatable[cloneKey] = function(rect)
return Rect.new(rect.Min, rect.Max)
end
metatable[typeKey] = "Rect"

function metatable:__index(key)
Expand Down
20 changes: 20 additions & 0 deletions lib/types/Rect_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,24 @@ describe("types.Rect", function()
assert.not_equals(rectA, rectB3)
assert.not_equals(rectA, rectB4)
end)

it("should clone", function()
local BaseInstance = import("../instances/BaseInstance")
local InstanceProperty = import("../InstanceProperty")

local CreatableClass = BaseInstance:extend("Creatable", {
creatable = true,
})

CreatableClass.properties.Value = InstanceProperty.normal({})

local rect = Rect.new(1, 2, 3, 4)

local instance = CreatableClass:new()
instance.Value = rect

local clone = instance:Clone()
assert.equal(clone.Value, rect)
assert.not_equals(getmetatable(rect), getmetatable(clone.Value))
end)
end)
4 changes: 4 additions & 0 deletions lib/types/UDim.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
local assign = import("../assign")
local cloneKey = import("../cloneKey")
local typeKey = import("../typeKey")

local UDim = {}
Expand All @@ -12,6 +13,9 @@ setmetatable(UDim, {
local prototype = {}

local metatable = {}
metatable[cloneKey] = function(udim)
return UDim.new(udim.Scale, udim.Offset)
end
metatable[typeKey] = "UDim"

function metatable:__index(key)
Expand Down
4 changes: 4 additions & 0 deletions lib/types/UDim2.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
local assign = import("../assign")
local cloneKey = import("../cloneKey")
local typeKey = import("../typeKey")
local typeof = import("../functions/typeof")
local UDim = import("./UDim")
Expand Down Expand Up @@ -27,6 +28,9 @@ function prototype:Lerp(goal, alpha)
end

local metatable = {}
metatable[cloneKey] = function(udim2)
return UDim2.new(udim2.X.Scale, udim2.X.Offset, udim2.Y.Scale, udim2.Y.Offset)
end
metatable[typeKey] = "UDim2"

function metatable:__index(key)
Expand Down
Loading