Sleitnick / AeroGameFramework

AeroGameFramework is a Roblox game framework that makes development easy and fun. The framework is designed to simplify the communication between modules and seamlessly bridge the gap between the server and client.
https://sleitnick.github.io/AeroGameFramework/
MIT License
215 stars 57 forks source link

TableUtil Cannot copy tables with circular dependencies. #207

Open brain-xiang opened 3 years ago

brain-xiang commented 3 years ago

I was working with some linked lists earlier when I ran into the issue that there was no way to copy a table that had circular dependencies with TableUtil. So I wrote up a little function that can do just that and runs pretty efficiently. I think it would be a great addition to TableUtil. Instead of being stuck in an endless loop of trying to duplicate the circular dependency over and over again "SafeCopyTable" makes the copied table mirror the circular dependency by referencing itself just like how the original Table does it.

local function SafeCopyTable(t, parentTables)
    --[[
        CopyTable that accounts for cyclic dependencies

        parentTables = {
            originalTable* = copiedTable*
        }
    ]]
    assert(type(t) == "table", "First argument must be a table")
    local tCopy = table.create(#t)
    parentTables = parentTables and CopyTableShallow(parentTables) or {}
    parentTables[t] = tCopy
    for k,v in pairs(t) do
        if (type(v) == "table") then
            if parentTables[v] then
                tCopy[k] = parentTables[v]
            else
                tCopy[k] = SafeCopyTable(v, parentTables)
            end
        else
            tCopy[k] = v
        end
    end
    return tCopy
end

Here is the .spec file I used to test it with TestEZ which all checked out

return function()
    while (not _G.Aero) do wait() end
    local aero = _G.Aero
    local TableUtil = aero.Shared.TableUtil

    describe("SafeCopyTable", function()
        it("should copy deep tables", function()
            local table = {1, 2, {3,4}}
            local copy = TableUtil.SafeCopy(table)

            -- Copied single values should stay the same
            expect(copy[1]).to.equal(table[1])

            -- Make copy of instead of refferencing nested tables
            expect(copy[3]).never.to.equal(table[3])

            -- Nested values should stay the same
            expect(copy[3][1]).to.equal(table[3][1])
        end)

        it("should copy tables with cyclic dependencies", function()
            local table = {}
            table[1] = table
            table[2] = {t = table}
            table[3] = 3

            -- table should have a cyclic dependency
            expect(table).to.equal(table[1])
            expect(function()
                copy = TableUtil.Copy(table)
            end).to.throw()

            expect(function()
                copy = TableUtil.SafeCopy(table)
            end).never.to.throw()

            -- Copied table should have same cyclic dependency as original
            expect(copy[1]).to.equal(copy)

            -- nested dependencies should also copy over
            expect(copy[2].t).to.equal(copy)

            -- copied cyclic dependencies should still read values as intended
            expect(copy[1][3]).to.equal(3)
        end)
    end)
end