From d852810010d12bc55cb7fc5461f0a91b27b95406 Mon Sep 17 00:00:00 2001 From: Benjamin Brimeyer Date: Wed, 6 Apr 2022 22:28:03 -0700 Subject: [PATCH] Add from old branch --- src/Dictionary/getIn.lua | 20 ++++ src/Dictionary/init.lua | 6 +- src/Dictionary/removeIn.lua | 7 ++ src/Dictionary/setIn.lua | 9 ++ src/Dictionary/updateIn.lua | 71 +++++++++++++++ tests/Llama/Dictionary/getIn.spec.lua | 48 ++++++++++ tests/Llama/Dictionary/removeIn.spec.lua | 25 +++++ tests/Llama/Dictionary/set.spec.lua | 2 +- tests/Llama/Dictionary/setIn.spec.lua | 24 +++++ tests/Llama/Dictionary/updateIn.spec.lua | 111 +++++++++++++++++++++++ 10 files changed, 321 insertions(+), 2 deletions(-) create mode 100644 src/Dictionary/getIn.lua create mode 100644 src/Dictionary/removeIn.lua create mode 100644 src/Dictionary/setIn.lua create mode 100644 src/Dictionary/updateIn.lua create mode 100644 tests/Llama/Dictionary/getIn.spec.lua create mode 100644 tests/Llama/Dictionary/removeIn.spec.lua create mode 100644 tests/Llama/Dictionary/setIn.spec.lua create mode 100644 tests/Llama/Dictionary/updateIn.spec.lua diff --git a/src/Dictionary/getIn.lua b/src/Dictionary/getIn.lua new file mode 100644 index 0000000..610d3e5 --- /dev/null +++ b/src/Dictionary/getIn.lua @@ -0,0 +1,20 @@ +local Dictionary = script.Parent + +local Llama = Dictionary.Parent +local t = require(Llama.t) + +local validate = t.tuple(t.table, t.array(t.string), t.optional(t.any)) + +return function(dictionary, keyPath, default) + assert(validate(dictionary, keyPath, default)) + + 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 af1e0f5..ab20808 100644 --- a/src/Dictionary/init.lua +++ b/src/Dictionary/init.lua @@ -9,6 +9,7 @@ local Dictionary = { flatten = require(script.flatten), flip = require(script.flip), fromLists = require(script.fromLists), + getIn = require(script.getIn), has = require(script.has), includes = require(script.includes), join = require(script.merge), @@ -19,12 +20,15 @@ local Dictionary = { mergeDeep = require(script.mergeDeep), removeKey = require(script.removeKey), removeKeys = require(script.removeKeys), + removeIn = require(script.removeIn), removeValue = require(script.removeValue), removeValues = require(script.removeValues), 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 \ No newline at end of file +return Dictionary 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/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 new file mode 100644 index 0000000..67d6ac9 --- /dev/null +++ b/src/Dictionary/updateIn.lua @@ -0,0 +1,71 @@ +local Dictionary = script.Parent + +local Llama = Dictionary.Parent +local t = require(Llama.t) + + +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 + +local validate = t.tuple(t.table, t.array(t.string), t.optional(t.callback), t.optional(t.any)) + +return function(dictionary, keyPath, updater, notSetValue) + assert(validate(dictionary, keyPath, updater, notSetValue)) + + local updatedValue = updateInDeeply( + dictionary, keyPath, notSetValue, updater, 1 + ) + + return updatedValue or notSetValue +end diff --git a/tests/Llama/Dictionary/getIn.spec.lua b/tests/Llama/Dictionary/getIn.spec.lua new file mode 100644 index 0000000..d423cfb --- /dev/null +++ b/tests/Llama/Dictionary/getIn.spec.lua @@ -0,0 +1,48 @@ +return function() + local ReplicatedStorage = game:GetService("ReplicatedStorage") + + local Packages = ReplicatedStorage.Packages + local Llama = require(Packages.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 diff --git a/tests/Llama/Dictionary/removeIn.spec.lua b/tests/Llama/Dictionary/removeIn.spec.lua new file mode 100644 index 0000000..e917183 --- /dev/null +++ b/tests/Llama/Dictionary/removeIn.spec.lua @@ -0,0 +1,25 @@ +return function() + local ReplicatedStorage = game:GetService("ReplicatedStorage") + + local Packages = ReplicatedStorage.Packages + local Llama = require(Packages.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() + expect(removeIn({}, {})).to.never.be.ok() + end) +end diff --git a/tests/Llama/Dictionary/set.spec.lua b/tests/Llama/Dictionary/set.spec.lua index 95da1f6..c41e63c 100644 --- a/tests/Llama/Dictionary/set.spec.lua +++ b/tests/Llama/Dictionary/set.spec.lua @@ -57,4 +57,4 @@ return function() expect(b.baz).to.equal(3) end) -end \ No newline at end of file +end diff --git a/tests/Llama/Dictionary/setIn.spec.lua b/tests/Llama/Dictionary/setIn.spec.lua new file mode 100644 index 0000000..78b787a --- /dev/null +++ b/tests/Llama/Dictionary/setIn.spec.lua @@ -0,0 +1,24 @@ +return function() + local ReplicatedStorage = game:GetService("ReplicatedStorage") + + local Packages = ReplicatedStorage.Packages + local Llama = require(Packages.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() + 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 new file mode 100644 index 0000000..3e54dba --- /dev/null +++ b/tests/Llama/Dictionary/updateIn.spec.lua @@ -0,0 +1,111 @@ +return function() + local ReplicatedStorage = game:GetService("ReplicatedStorage") + + local Packages = ReplicatedStorage.Packages + local Llama = require(Packages.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() + 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