Skip to content
This repository was archived by the owner on Jun 24, 2022. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/Dictionary/getIn.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
return function(dictionary, keyPath, default)
local dictionaryType = type(dictionary)
assert(dictionaryType == "table", "expected a table for first argument, got " .. dictionaryType)
assert(type(keyPath) == "table", string.format("Invalid keyPath: expected array: %s", tostring(keyPath)))

local node = dictionary
for _, path in ipairs(keyPath) do
node = node[path]
if not node then
return default
end
end

return node
end
6 changes: 5 additions & 1 deletion src/Dictionary/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,22 @@ local Dictionary = {
flatten = require(script.flatten),
flip = require(script.flip),
get = require(script.get),
getIn = require(script.getIn),
has = require(script.has),
includes = require(script.includes),
join = require(script.join),
joinDeep = require(script.joinDeep),
keys = require(script.keys),
map = require(script.map),
removeIn = require(script.removeIn),
removeKey = require(script.removeKey),
removeValue = require(script.removeValue),
set = require(script.set),
setIn = require(script.setIn),
some = require(script.some),
update = require(script.update),
updateIn = require(script.updateIn),
values = require(script.values),
}

return Dictionary
return Dictionary
7 changes: 7 additions & 0 deletions src/Dictionary/removeIn.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
local updateIn = require(script.Parent.updateIn)

return function(dictionary, keyPath)
return updateIn(dictionary, keyPath, function()
return nil
end, nil)
end
9 changes: 9 additions & 0 deletions src/Dictionary/setIn.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
local updateIn = require(script.Parent.updateIn)

return function(dictionary, keyPath, value)
local result = updateIn(dictionary, keyPath, function()
return value
end, {})

return result
end
65 changes: 65 additions & 0 deletions src/Dictionary/updateIn.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
local removeKey = require(script.Parent.removeKey)
local set = require(script.Parent.set)
local map = require(script.Parent.map)
local slice = require(script.Parent.Parent.List.slice)

local function quoteString(value)
return string.format("%q", value)
end

local function throw(existing, keyPath, i)
error(string.format(
"Cannot update within non-table value in path [%s] = %s",
table.concat(map(slice(keyPath, 1, i - 1), quoteString), ", "),
tostring(existing)
))
end

local function updateInDeeply(existing, keyPath, notSetValue, updater, i)
local wasNotSet = existing == nil
if i > #keyPath then
local existingValue = wasNotSet and notSetValue or existing
local newValue = updater(existingValue)
return newValue == existingValue and existing or newValue
end

if (not wasNotSet and type(existing) ~= "table") then
throw(existing, keyPath, i)
end

local key = keyPath[i]
local nextExisting
if wasNotSet then
nextExisting = notSetValue and notSetValue[key] or nil
else
nextExisting = existing[key]
end

local nextUpdated = updateInDeeply(nextExisting, keyPath, notSetValue, updater, i + 1)

if nextUpdated == nil then
if existing or notSetValue then
return removeKey(existing or notSetValue, key)
end
else
if existing or notSetValue then
return set(existing or notSetValue, key, nextUpdated)
else
throw(existing, keyPath, i)
end
end

return nil
end

return function(dictionary, keyPath, updater, notSetValue)
local dictionaryType = type(dictionary)
assert(dictionaryType == "table", "expected a table for first argument, got " .. dictionaryType)
assert(type(keyPath) == "table", string.format("Invalid keyPath: expected array: %s", tostring(keyPath)))

local updatedValue = updateInDeeply(
dictionary, keyPath, notSetValue, updater, 1
)

return updatedValue or notSetValue
end
46 changes: 46 additions & 0 deletions tests/Llama/Dictionary/getIn.spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
return function()
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local lib = ReplicatedStorage.lib
local Llama = require(lib.Llama)
local getIn = Llama.Dictionary.getIn

describe("GIVEN a dictionary with layers x, y, and z", function()
local dictionary = { x = { y = { z = 123 }}}

it("SHOULD return 123 when given {x, y, z}", function()
local result = getIn(dictionary, {"x", "y", "z"})
expect(result).to.equal(123)
end)

it("SHOULD return the given default when given {x, q, p}", function()
local result = getIn(dictionary, {"x", "q", "p"}, "someDefaultValue")
expect(result).to.equal("someDefaultValue")
end)

it("SHOULD return nil if path does not match and there is no default given", function()
expect(getIn(dictionary, {"a", "b", "c"})).to.equal(nil)
expect(getIn(dictionary, {"x", "b", "c"})).to.equal(nil)
expect(getIn(dictionary, {"x", "y", "c"})).to.equal(nil)
end)

it("SHOULD return not found if path encounters non-data-structure", function()
local m = { a = { b = { c = nil }}}
local keyPath = { "a", "b", "c", "x" }
expect(getIn(m, keyPath)).to.equal(nil)
expect(getIn(m, keyPath, "default")).to.equal("default")
end)
end)

local function itShouldThrowAsKeyPath(given)
return function()
it("SHOULD throw", function()
expect(function()
getIn({}, given)
end).to.throw()
end)
end
end
describe("GIVEN a number as keyPath", itShouldThrowAsKeyPath(10))
describe("GIVEN string as keyPath", itShouldThrowAsKeyPath("abc"))
describe("GIVEN nil as keyPath", itShouldThrowAsKeyPath(nil))
end
26 changes: 26 additions & 0 deletions tests/Llama/Dictionary/removeIn.spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
return function()
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local lib = ReplicatedStorage.lib
local Llama = require(lib.Llama)

local Dictionary = Llama.Dictionary
local removeIn = Dictionary.removeIn
local equalsDeep = Dictionary.equalsDeep

it("provides shorthand for updateIn to remove a single value", function()
local m = { a = { b = { c = "X", d = "Y" } } }
local result = removeIn(m, { "a", "b", "c" })
expect(equalsDeep(result, { a = { b = { d = "Y" }} })).to.equal(true)
end)

it("does not create empty maps for an unset path", function()
local result = removeIn({}, { "a", "b", "c" })
expect(equalsDeep(result, {})).to.equal(true)
end)

it("removes itself when removing empty path", function()
local m = {}
expect(removeIn({}, {})).to.never.be.ok()
end)
end
25 changes: 25 additions & 0 deletions tests/Llama/Dictionary/setIn.spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
return function()
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local lib = ReplicatedStorage.lib
local Llama = require(lib.Llama)

local Dictionary = Llama.Dictionary
local setIn = Dictionary.setIn
local equalsDeep = Dictionary.equalsDeep

it("provides shorthand for updateIn to set a single value", function()
local m = setIn({}, { "a", "b", "c" }, "X");
expect(equalsDeep(m, { a = { b = { c = "X" }} })).to.equal(true)
end)

it("returns value when setting empty path", function()
local m = {}
expect(setIn({}, {}, "X")).to.equal("X");
end)

it("can setIn nil", function()
local m = setIn({}, { "a", "b", "c" }, nil);
expect(equalsDeep(m, { a = { b = { c = nil }} })).to.equal(true)
end)
end
111 changes: 111 additions & 0 deletions tests/Llama/Dictionary/updateIn.spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
return function()
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local lib = ReplicatedStorage.lib
local Llama = require(lib.Llama)

local Dictionary = Llama.Dictionary
local updateIn = Dictionary.updateIn
local equalsDeep = Dictionary.equalsDeep

it("deep edit", function()
local m = { a = { b = { c = 10 }}}
local result = updateIn(m, {"a", "b", "c"}, function(value)
return value * 2
end)

expect(equalsDeep(result, { a = { b = { c = 20 }}})).to.equal(true)
end)

it("deep edit throws if non-editable path", function()
local deep = { key = { bar = { item = 10 }} }
expect(function()
updateIn(deep, {"key", "foo", "item"}, function()
return "newValue"
end)
end).to.throw("Cannot update within non-table value in path")
end)

it("shallow remove", function()
local m = { a = 123 }
local result = updateIn(m, {"a"}, function()
return nil
end)
expect(equalsDeep(result, {})).to.equal(true)
end)

it("deep remove", function()
local removeKey = Dictionary.removeKey

local m = { a = { b = { c = 10 } } }
local result = updateIn(m, {"a", "b"}, function(map)
return removeKey(map, "c")
end)
expect(equalsDeep(result, { a = { b = {} }})).to.equal(true)
end)

it("deep set", function()
local set = Dictionary.set

local m = { a = { b = { c = 10 } } }
local result = updateIn(m, {"a", "b"}, function(map)
return set(map, "d", 20)
end)
expect(equalsDeep(result, { a = { b = { c = 10, d = 20 } }})).to.equal(true)
end)

it("deep push", function()
local push = Llama.List.push
local m = { a = { b = { 1, 2, 3 }}}
local result = updateIn(m, {"a", "b"}, function(list)
return push(list, 4)
end)
expect(equalsDeep(result, { a = { b = { 1, 2, 3, 4 } }})).to.equal(true)
end)

it("deep map", function()
local map = Llama.List.map
local m = { a = { b = { 1, 2, 3 }}}
local result = updateIn(m, {"a", "b"}, function(list)
return map(list, function(value)
return value * 10
end)
end)
expect(equalsDeep(result, { a = { b = { 10, 20, 30 } }})).to.equal(true)
end)

it("creates new maps if path contains gaps", function()
local set = Llama.Dictionary.set
local m = { a = { b = { c = 10 }}}
local result = updateIn(m, { "a", "x", "y" }, function(map)
return set(map, "z", 20)
end, {})
expect(equalsDeep(result, { a = { b = { c = 10 }, x = { y = { z = 20 }} }})).to.equal(true)
end)

it("throws if path cannot be set", function()
local m = { a = { b = { c = 10 } } }
expect(function()
updateIn(m, { "a", "b", "c", "d" }, function(v)
return 20
end)
end).to.throw("Cannot update within non-table value in path")
end)

it("update with notSetValue when non-existing key", function()
local m = { a = { b = { c = 10 } } }
local result = updateIn(m, {"x"}, function(map)
return map + 1
end, 100)
expect(equalsDeep(result, { x = 101, a = { b = { c = 10 } }})).to.equal(true)
end)

it("updates self for empty path", function()
local set = Llama.Dictionary.set
local m = { a = 1, b = 2, c = 3 }
local result = updateIn(m, {}, function(map)
return set(map, "b", 20)
end)
expect(equalsDeep(result, { a = 1, b = 20, c = 3 })).to.equal(true)
end)
end