Skip to content

Commit

Permalink
Merge c89a8b0 into c401f83
Browse files Browse the repository at this point in the history
  • Loading branch information
Tieske committed Feb 13, 2022
2 parents c401f83 + c89a8b0 commit a00eed8
Show file tree
Hide file tree
Showing 4 changed files with 285 additions and 39 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,16 @@ 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
[#413](https://github.com/lunarmodules/Penlight/pull/413)
- feat: `utils.kpairs` new iterator over all non-integer keys
[#413](https://github.com/lunarmodules/Penlight/pull/413)

>>>>>>> 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)
Expand Down
146 changes: 126 additions & 20 deletions lua/pl/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ local concat = table.concat
local _unpack = table.unpack -- always injected by 'compat'
local find = string.find
local sub = string.sub
local next = next
local floor = math.floor

local is_windows = compat.is_windows
local err_mode = 'default'
Expand Down Expand Up @@ -223,6 +225,47 @@ end



--- an iterator over all non-integer keys (inverse of `ipairs`).
-- It will skip any key that is an integer number, so negative indices or an
-- array with holes will not return those either (so it returns slightly less than
-- 'the inverse of `ipairs`').
--
-- This uses `pairs` under the hood, so any value that is iterable using `pairs`
-- will work with this function.
-- @tparam table t the table to iterate over
-- @treturn key
-- @treturn value
-- @usage
-- local t = {
-- "hello",
-- "world",
-- hello = "hallo",
-- world = "Welt",
-- }
--
-- for k, v in utils.kpairs(t) do
-- print("German: ", v)
-- end
--
-- -- output;
-- -- German: hallo
-- -- German: Welt
function utils.kpairs(t)
local index
return function()
local value
while true do
index, value = next(t, index)
if type(index) ~= "number" or floor(index) ~= index then
break
end
end
return index, value
end
end



--- Error handling
-- @section Error-handling

Expand All @@ -249,17 +292,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")
--
Expand All @@ -271,21 +317,81 @@ 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 utils.npairs(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 utils.kpairs(first) do
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

if not lst[1] then
error("expected at least 1 entry", 2)
end

local valid = "(expected one of: '" .. concat(lst, "', '") .. "')"
Expand All @@ -299,7 +405,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
Expand Down
132 changes: 113 additions & 19 deletions spec/utils-enum_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,45 +5,134 @@ 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'")
-- no holes
assert.has.error(function()
t = enum("hello", nil, "world")
end, "argument 2 expected a 'string', got a 'nil'")
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)


Expand All @@ -60,6 +149,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')")
Expand Down
Loading

0 comments on commit a00eed8

Please sign in to comment.