From e738aa8b6c87ed8ed89128ad31849f216c575e0d Mon Sep 17 00:00:00 2001 From: Thijs Schreijer Date: Thu, 13 Jan 2022 19:06:17 +0100 Subject: feat(utils) enum to accept hash tables as well --- CHANGELOG.md | 5 ++ lua/pl/utils.lua | 105 ++++++++++++++++++++++++++++++-------- spec/utils-enum_spec.lua | 128 ++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 199 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70fd196..b3385f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,13 @@ deprecation policy. see [CONTRIBUTING.md](CONTRIBUTING.md#release-instructions-for-a-new-version) for release instructions ## 1.13.0 (unreleased) +<<<<<<< HEAD - fix: `compat.warn` raised write guard warning in OpenResty [#414](https://github.com/lunarmodules/Penlight/pull/414) +======= + - feat: `utils.enum` now accepts hash tables, to enable better error handling + +>>>>>>> b38c390 (feat(utils) enum to accept hash tables as well) ## 1.12.0 (2022-Jan-10) - deprecate: module `pl.text` the contents have moved to `pl.stringx` (removal later) diff --git a/lua/pl/utils.lua b/lua/pl/utils.lua index 1cb8a64..d56a6ef 100644 --- a/lua/pl/utils.lua +++ b/lua/pl/utils.lua @@ -249,17 +249,20 @@ function utils.assert_arg (n,val,tp,verify,msg,lev) return val end ---- creates an Enum table. +--- creates an Enum or constants lookup table for improved error handling. -- This helps prevent magic strings in code by throwing errors for accessing --- non-existing values. +-- non-existing values, and/or converting strings/identifiers to other values. -- --- Calling on the object does the same, but returns a soft error; `nil + err`. +-- Calling on the object does the same, but returns a soft error; `nil + err`, if +-- the call is succesful (the key exists), it will return the value. -- --- The values are equal to the keys. The enum object is --- read-only. --- @param ... strings that make up the enumeration. --- @return Enum object --- @usage -- accessing at runtime +-- When calling with varargs or an array the values will be equal to the keys. +-- The enum object is read-only. +-- @tparam table|vararg ... the input for the Enum. If varargs or an array then the +-- values in the Enum will be equal to the names (must be strings), if a hash-table +-- then values remain (any type), and the keys must be strings. +-- @return Enum object (read-only table/object) +-- @usage -- Enum access at runtime -- local obj = {} -- obj.MOVEMENT = utils.enum("FORWARD", "REVERSE", "LEFT", "RIGHT") -- @@ -271,21 +274,83 @@ end -- -- "'REVERES' is not a valid value (expected one of: 'FORWARD', 'REVERSE', 'LEFT', 'RIGHT')" -- -- end --- @usage -- validating user-input --- local parameter = "...some user provided option..." --- local ok, err = obj.MOVEMENT(parameter) -- calling on the object --- if not ok then --- print("bad 'parameter', " .. err) +-- @usage -- standardized error codes +-- local obj = { +-- ERR = utils.enum { +-- NOT_FOUND = "the item was not found", +-- OUT_OF_BOUNDS = "the index is outside the allowed range" +-- }, +-- +-- some_method = function(self) +-- return self.ERR.OUT_OF_BOUNDS +-- end, +-- } +-- +-- local result, err = obj:some_method() +-- if not result then +-- if err == obj.ERR.NOT_FOUND then +-- -- check on error code, not magic strings +-- +-- else +-- -- return the error description, contained in the constant +-- return nil, "error: "..err -- "error: the index is outside the allowed range" +-- end +-- end +-- @usage -- validating/converting user-input +-- local color = "purple" +-- local ansi_colors = utils.enum { +-- black = 30, +-- red = 31, +-- green = 32, +-- } +-- local color_code, err = ansi_colors(color) -- calling on the object, returns the value from the enum +-- if not color_code then +-- print("bad 'color', " .. err) +-- -- "bad 'color', 'purple' is not a valid value (expected one of: 'black', 'red', 'green')" -- os.exit(1) -- end function utils.enum(...) - local lst = utils.pack(...) - utils.assert_arg(1, lst[1], "string") -- at least 1 string - + local first = select(1, ...) local enum = {} - for i, value in ipairs(lst) do - utils.assert_arg(i, value, "string") - enum[value] = value + local lst + + if type(first) ~= "table" then + -- vararg with strings + lst = utils.pack(...) + for i, value in ipairs(lst) do + utils.assert_arg(i, value, "string") + enum[value] = value + end + + else + -- table/array with values + utils.assert_arg(1, first, "table") + lst = {} + -- first add array part + for i, value in ipairs(first) do + if type(value) ~= "string" then + error(("expected 'string' but got '%s' at index %d"):format(type(value), i), 2) + end + lst[i] = value + enum[value] = value + end + -- add key-ed part + for key, value in pairs(first) do + if not lst[key] then + if type(key) ~= "string" then + error(("expected key to be 'string' but got '%s'"):format(type(key)), 2) + end + if enum[key] then + error(("duplicate entry in array and hash part: '%s'"):format(key), 2) + end + enum[key] = value + lst[#lst+1] = key + end + end + end + + if not lst[1] then + error("expected at least 1 entry", 2) end local valid = "(expected one of: '" .. concat(lst, "', '") .. "')" @@ -299,7 +364,7 @@ function utils.enum(...) __call = function(self, key) if type(key) == "string" then local v = rawget(self, key) - if v then + if v ~= nil then return v end end diff --git a/spec/utils-enum_spec.lua b/spec/utils-enum_spec.lua index 752acb9..21fde62 100644 --- a/spec/utils-enum_spec.lua +++ b/spec/utils-enum_spec.lua @@ -5,45 +5,130 @@ describe("pl.utils", function () before_each(function() enum = require("pl.utils").enum - t = enum("ONE", "two", "THREE") end) - it("holds enumerated values", function() - assert.equal("ONE", t.ONE) - assert.equal("two", t.two) - assert.equal("THREE", t.THREE) - end) + describe("creating", function() + it("accepts a vararg", function() + t = enum("ONE", "two", "THREE") + assert.same({ + ONE = "ONE", + two = "two", + THREE = "THREE", + }, t) + end) - describe("accessing", function() + it("vararg entries must be strings", function() + assert.has.error(function() + t = enum("hello", true, "world") + end, "argument 2 expected a 'string', got a 'boolean'") + end) - it("errors on unknown values", function() + + it("vararg requires at least 1 entry", function() assert.has.error(function() - print(t.four) - end, "'four' is not a valid value (expected one of: 'ONE', 'two', 'THREE')") + t = enum() + end, "expected at least 1 entry") end) - it("errors on setting new keys", function() + it("accepts an array", function() + t = enum { "ONE", "two", "THREE" } + assert.same({ + ONE = "ONE", + two = "two", + THREE = "THREE", + }, t) + end) + + + it("array entries must be strings", function() assert.has.error(function() - t.four = "four" - end, "the Enum object is read-only") + t = enum { "ONE", 999, "THREE" } + end, "expected 'string' but got 'number' at index 2") end) - it("entries must be strings", function() + it("array requires at least 1 entry", function() assert.has.error(function() - t = enum("hello", true, "world") - end, "argument 2 expected a 'string', got a 'boolean'") + t = enum {} + end, "expected at least 1 entry") + end) + + + it("accepts a hash-table", function() + t = enum { + FILE_NOT_FOUND = "The file was not found in the filesystem", + FILE_READ_ONLY = "The file is read-only", + } + assert.same({ + FILE_NOT_FOUND = "The file was not found in the filesystem", + FILE_READ_ONLY = "The file is read-only", + }, t) end) - it("requires at least 1 entry", function() + it("hash-table keys must be strings", function() assert.has.error(function() - t = enum() - end, "argument 1 expected a 'string', got a 'nil'") + t = enum { [{}] = "ONE" } + end, "expected key to be 'string' but got 'table'") + end) + + + it("hash-table requires at least 1 entry", function() + assert.has.error(function() + t = enum {} + end, "expected at least 1 entry") + end) + + + it("accepts a combined array/hash-table", function() + t = enum { + "BAD_FD", + FILE_NOT_FOUND = "The file was not found in the filesystem", + FILE_READ_ONLY = "The file is read-only", + } + assert.same({ + BAD_FD = "BAD_FD", + FILE_NOT_FOUND = "The file was not found in the filesystem", + FILE_READ_ONLY = "The file is read-only", + }, t) + end) + + + it("keys must be unique with combined array/has-table", function() + assert.has.error(function() + t = enum { + "FILE_NOT_FOUND", + FILE_NOT_FOUND = "The file was not found in the filesystem", + } + end, "duplicate entry in array and hash part: 'FILE_NOT_FOUND'") + end) + + end) + + + + describe("accessing", function() + + before_each(function() + t = enum("ONE", "two", "THREE") + end) + + + it("errors on unknown values", function() + assert.has.error(function() + print(t.four) + end, "'four' is not a valid value (expected one of: 'ONE', 'two', 'THREE')") + end) + + + it("errors on setting new keys", function() + assert.has.error(function() + t.four = "four" + end, "the Enum object is read-only") end) @@ -60,6 +145,11 @@ describe("pl.utils", function () describe("calling", function() + before_each(function() + t = enum("ONE", "two", "THREE") + end) + + it("returns error on unknown values", function() local ok, err = t("four") assert.equal(err, "'four' is not a valid value (expected one of: 'ONE', 'two', 'THREE')") -- cgit v1.2.3