From c55ee84a4336de45c6a0a993d2d5fa8f9f5a95ce Mon Sep 17 00:00:00 2001 From: Benjamin Brimeyer Date: Sun, 7 Feb 2021 14:00:44 -0800 Subject: [PATCH 1/7] implement getIn --- src/Dictionary/getIn.lua | 13 ++++++++ src/Dictionary/init.lua | 3 +- tests/Llama/Dictionary/getIn.spec.lua | 47 +++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 src/Dictionary/getIn.lua create mode 100644 tests/Llama/Dictionary/getIn.spec.lua diff --git a/src/Dictionary/getIn.lua b/src/Dictionary/getIn.lua new file mode 100644 index 0000000..18b552d --- /dev/null +++ b/src/Dictionary/getIn.lua @@ -0,0 +1,13 @@ +return function(dictionary, keyPath, default) + assert(type(keyPath) == "table" and #keyPath > 0, 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 diff --git a/src/Dictionary/init.lua b/src/Dictionary/init.lua index a823644..e85598c 100644 --- a/src/Dictionary/init.lua +++ b/src/Dictionary/init.lua @@ -11,6 +11,7 @@ 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), @@ -25,4 +26,4 @@ local Dictionary = { values = require(script.values), } -return Dictionary \ No newline at end of file +return Dictionary diff --git a/tests/Llama/Dictionary/getIn.spec.lua b/tests/Llama/Dictionary/getIn.spec.lua new file mode 100644 index 0000000..b40be7e --- /dev/null +++ b/tests/Llama/Dictionary/getIn.spec.lua @@ -0,0 +1,47 @@ +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)) + describe("GIVEN dictionary as keyPath", itShouldThrowAsKeyPath({ foo = "bar" })) +end From 452a211503f2b09df380bd120aa02287af8d3da1 Mon Sep 17 00:00:00 2001 From: Benjamin Brimeyer Date: Wed, 10 Feb 2021 00:52:41 -0800 Subject: [PATCH 2/7] updateIn-working --- src/Dictionary/init.lua | 1 + src/Dictionary/updateIn.lua | 43 +++++++++ tests/Llama/Dictionary/updateIn.spec.lua | 115 +++++++++++++++++++++++ 3 files changed, 159 insertions(+) create mode 100644 src/Dictionary/updateIn.lua create mode 100644 tests/Llama/Dictionary/updateIn.spec.lua diff --git a/src/Dictionary/init.lua b/src/Dictionary/init.lua index e85598c..8360498 100644 --- a/src/Dictionary/init.lua +++ b/src/Dictionary/init.lua @@ -23,6 +23,7 @@ local Dictionary = { set = require(script.set), some = require(script.some), update = require(script.update), + updateIn = require(script.updateIn), values = require(script.values), } diff --git a/src/Dictionary/updateIn.lua b/src/Dictionary/updateIn.lua new file mode 100644 index 0000000..eee6521 --- /dev/null +++ b/src/Dictionary/updateIn.lua @@ -0,0 +1,43 @@ +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 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 + 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 key = keyPath[i] + local nextExisting = (wasNotSet and notSetValue or existing)[key] + local nextUpdated = updateInDeeply(nextExisting, keyPath, notSetValue, updater, i + 1) + + if nextUpdated == nil then + return removeKey(existing, key) + else + return set(existing or notSetValue, key, nextUpdated) + end +end + +return function(dictionary, keyPath, updater, notSetValue) + local updatedValue = updateInDeeply( + dictionary, keyPath, notSetValue, updater, 1 + ) + + return updatedValue or notSetValue +end diff --git a/tests/Llama/Dictionary/updateIn.spec.lua b/tests/Llama/Dictionary/updateIn.spec.lua new file mode 100644 index 0000000..d702a80 --- /dev/null +++ b/tests/Llama/Dictionary/updateIn.spec.lua @@ -0,0 +1,115 @@ +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 = {[{"item"}] = true }} + updateIn(deep, {"key", "foo", "item"}) + 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) + + fit("does not perform edit when new value is the same as old value", function() + local m = { a = { b = { c = 10 } } } + local m2 = updateIn(m, {"a", "b", "c"}, function(id) + return id + end) + expect(m2).to.equal(m); + end) +end From 8a62ca9ef3192c627dc60d000b38209b305ac438 Mon Sep 17 00:00:00 2001 From: Benjamin Brimeyer Date: Wed, 10 Feb 2021 00:52:55 -0800 Subject: [PATCH 3/7] unfocus test --- tests/Llama/Dictionary/updateIn.spec.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Llama/Dictionary/updateIn.spec.lua b/tests/Llama/Dictionary/updateIn.spec.lua index d702a80..ab9ae53 100644 --- a/tests/Llama/Dictionary/updateIn.spec.lua +++ b/tests/Llama/Dictionary/updateIn.spec.lua @@ -105,7 +105,7 @@ return function() expect(equalsDeep(result, { a = 1, b = 20, c = 3 })).to.equal(true) end) - fit("does not perform edit when new value is the same as old value", function() + it("does not perform edit when new value is the same as old value", function() local m = { a = { b = { c = 10 } } } local m2 = updateIn(m, {"a", "b", "c"}, function(id) return id From 07f5138bd62ea467448e2e7f073c143cf7617cb5 Mon Sep 17 00:00:00 2001 From: Benjamin Brimeyer Date: Wed, 10 Feb 2021 01:01:46 -0800 Subject: [PATCH 4/7] add setIn --- src/Dictionary/init.lua | 1 + src/Dictionary/setIn.lua | 9 +++++++++ src/Dictionary/updateIn.lua | 2 +- tests/Llama/Dictionary/setIn.spec.lua | 25 ++++++++++++++++++++++++ tests/Llama/Dictionary/updateIn.spec.lua | 8 -------- 5 files changed, 36 insertions(+), 9 deletions(-) create mode 100644 src/Dictionary/setIn.lua create mode 100644 tests/Llama/Dictionary/setIn.spec.lua diff --git a/src/Dictionary/init.lua b/src/Dictionary/init.lua index 8360498..794ea9f 100644 --- a/src/Dictionary/init.lua +++ b/src/Dictionary/init.lua @@ -21,6 +21,7 @@ local Dictionary = { 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), diff --git a/src/Dictionary/setIn.lua b/src/Dictionary/setIn.lua new file mode 100644 index 0000000..6260085 --- /dev/null +++ b/src/Dictionary/setIn.lua @@ -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 diff --git a/src/Dictionary/updateIn.lua b/src/Dictionary/updateIn.lua index eee6521..664e296 100644 --- a/src/Dictionary/updateIn.lua +++ b/src/Dictionary/updateIn.lua @@ -28,7 +28,7 @@ local function updateInDeeply(existing, keyPath, notSetValue, updater, i) local nextUpdated = updateInDeeply(nextExisting, keyPath, notSetValue, updater, i + 1) if nextUpdated == nil then - return removeKey(existing, key) + return removeKey(existing or notSetValue, key) else return set(existing or notSetValue, key, nextUpdated) end diff --git a/tests/Llama/Dictionary/setIn.spec.lua b/tests/Llama/Dictionary/setIn.spec.lua new file mode 100644 index 0000000..2688636 --- /dev/null +++ b/tests/Llama/Dictionary/setIn.spec.lua @@ -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 diff --git a/tests/Llama/Dictionary/updateIn.spec.lua b/tests/Llama/Dictionary/updateIn.spec.lua index ab9ae53..5ea9804 100644 --- a/tests/Llama/Dictionary/updateIn.spec.lua +++ b/tests/Llama/Dictionary/updateIn.spec.lua @@ -104,12 +104,4 @@ return function() end) expect(equalsDeep(result, { a = 1, b = 20, c = 3 })).to.equal(true) end) - - it("does not perform edit when new value is the same as old value", function() - local m = { a = { b = { c = 10 } } } - local m2 = updateIn(m, {"a", "b", "c"}, function(id) - return id - end) - expect(m2).to.equal(m); - end) end From d037a8a1dba4a247666a95c7b5b287ef2fd0558b Mon Sep 17 00:00:00 2001 From: Benjamin Brimeyer Date: Wed, 10 Feb 2021 01:17:43 -0800 Subject: [PATCH 5/7] add removeIn --- src/Dictionary/init.lua | 1 + src/Dictionary/removeIn.lua | 7 +++++++ src/Dictionary/updateIn.lua | 14 +++++++++++-- tests/Llama/Dictionary/removeIn.spec.lua | 26 ++++++++++++++++++++++++ 4 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 src/Dictionary/removeIn.lua create mode 100644 tests/Llama/Dictionary/removeIn.spec.lua diff --git a/src/Dictionary/init.lua b/src/Dictionary/init.lua index 794ea9f..1ef8e90 100644 --- a/src/Dictionary/init.lua +++ b/src/Dictionary/init.lua @@ -18,6 +18,7 @@ local Dictionary = { 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), diff --git a/src/Dictionary/removeIn.lua b/src/Dictionary/removeIn.lua new file mode 100644 index 0000000..5715b07 --- /dev/null +++ b/src/Dictionary/removeIn.lua @@ -0,0 +1,7 @@ +local updateIn = require(script.Parent.updateIn) + +return function(dictionary, keyPath) + return updateIn(dictionary, keyPath, function() + return nil + end, nil) +end diff --git a/src/Dictionary/updateIn.lua b/src/Dictionary/updateIn.lua index 664e296..1afb14f 100644 --- a/src/Dictionary/updateIn.lua +++ b/src/Dictionary/updateIn.lua @@ -24,14 +24,24 @@ local function updateInDeeply(existing, keyPath, notSetValue, updater, i) end local key = keyPath[i] - local nextExisting = (wasNotSet and notSetValue or existing)[key] + 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 - return removeKey(existing or notSetValue, key) + if existing or notSetValue then + return removeKey(existing or notSetValue, key) + end else return set(existing or notSetValue, key, nextUpdated) end + + return nil end return function(dictionary, keyPath, updater, notSetValue) diff --git a/tests/Llama/Dictionary/removeIn.spec.lua b/tests/Llama/Dictionary/removeIn.spec.lua new file mode 100644 index 0000000..cd86fdd --- /dev/null +++ b/tests/Llama/Dictionary/removeIn.spec.lua @@ -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 From 35ed9910de7ec427ed0b5c0988bc4ac71d56f841 Mon Sep 17 00:00:00 2001 From: Benjamin Brimeyer Date: Wed, 10 Feb 2021 01:31:56 -0800 Subject: [PATCH 6/7] add test for throwing --- src/Dictionary/updateIn.lua | 22 +++++++++++++++------- tests/Llama/Dictionary/updateIn.spec.lua | 12 ++++++++---- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/Dictionary/updateIn.lua b/src/Dictionary/updateIn.lua index 1afb14f..051d500 100644 --- a/src/Dictionary/updateIn.lua +++ b/src/Dictionary/updateIn.lua @@ -7,6 +7,14 @@ 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 @@ -15,12 +23,8 @@ local function updateInDeeply(existing, keyPath, notSetValue, updater, i) return newValue == existingValue and existing or newValue end - if not wasNotSet and type(existing) ~= "table" then - error(string.format( - "Cannot update within non-table value in path [%s] = %s", - table.concat(map(slice(keyPath, 1, i-1), quoteString), ", "), - tostring(existing) - )) + if (not wasNotSet and type(existing) ~= "table") then + throw(existing, keyPath, i) end local key = keyPath[i] @@ -38,7 +42,11 @@ local function updateInDeeply(existing, keyPath, notSetValue, updater, i) return removeKey(existing or notSetValue, key) end else - return set(existing or notSetValue, key, nextUpdated) + if existing or notSetValue then + return set(existing or notSetValue, key, nextUpdated) + else + throw(existing, keyPath, i) + end end return nil diff --git a/tests/Llama/Dictionary/updateIn.spec.lua b/tests/Llama/Dictionary/updateIn.spec.lua index 5ea9804..121ac6c 100644 --- a/tests/Llama/Dictionary/updateIn.spec.lua +++ b/tests/Llama/Dictionary/updateIn.spec.lua @@ -17,10 +17,14 @@ return function() expect(equalsDeep(result, { a = { b = { c = 20 }}})).to.equal(true) end) - --[[it("deep edit throws if non-editable path", function() - local deep = { key = {[{"item"}] = true }} - updateIn(deep, {"key", "foo", "item"}) - 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 } From 1e5f0d1f2037372205d94a5a559ae2fa6dad20aa Mon Sep 17 00:00:00 2001 From: Benjamin Brimeyer Date: Wed, 10 Feb 2021 01:37:38 -0800 Subject: [PATCH 7/7] clean up assertions --- src/Dictionary/getIn.lua | 4 +++- src/Dictionary/updateIn.lua | 4 ++++ tests/Llama/Dictionary/getIn.spec.lua | 1 - 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Dictionary/getIn.lua b/src/Dictionary/getIn.lua index 18b552d..d205287 100644 --- a/src/Dictionary/getIn.lua +++ b/src/Dictionary/getIn.lua @@ -1,5 +1,7 @@ return function(dictionary, keyPath, default) - assert(type(keyPath) == "table" and #keyPath > 0, string.format("Invalid keyPath: expected array: %s", tostring(keyPath))) + 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 diff --git a/src/Dictionary/updateIn.lua b/src/Dictionary/updateIn.lua index 051d500..7ae6604 100644 --- a/src/Dictionary/updateIn.lua +++ b/src/Dictionary/updateIn.lua @@ -53,6 +53,10 @@ local function updateInDeeply(existing, keyPath, notSetValue, updater, i) 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 ) diff --git a/tests/Llama/Dictionary/getIn.spec.lua b/tests/Llama/Dictionary/getIn.spec.lua index b40be7e..291c671 100644 --- a/tests/Llama/Dictionary/getIn.spec.lua +++ b/tests/Llama/Dictionary/getIn.spec.lua @@ -43,5 +43,4 @@ return function() describe("GIVEN a number as keyPath", itShouldThrowAsKeyPath(10)) describe("GIVEN string as keyPath", itShouldThrowAsKeyPath("abc")) describe("GIVEN nil as keyPath", itShouldThrowAsKeyPath(nil)) - describe("GIVEN dictionary as keyPath", itShouldThrowAsKeyPath({ foo = "bar" })) end