local mod = {} local kumo = require 'kumo' local function is_table_or_ud(o) local ot = type(o) if ot == 'table' then return true end if ot == 'userdata' then return true end return false end local function has_mt(object, name) local mt = getmetatable(object) if not mt then return false end if mt[name] then return true end return false end ---@param o1 any|table First object to compare ---@param o2 any|table Second object to compare ---@param ignore_mt boolean True to ignore metatables (a recursive function to tests tables inside tables) -- https://stackoverflow.com/a/32660766/149111 function mod.equals(o1, o2, ignore_mt) if o1 == o2 then return true end if not is_table_or_ud(o1) then return false end if not is_table_or_ud(o2) then return false end if not ignore_mt then local mt1 = getmetatable(o1) if mt1 and mt1.__eq then --compare using built in method return o1 == o2 end end local keySet = {} for key1, value1 in pairs(o1) do local value2 = o2[key1] if value2 == nil or mod.equals(value1, value2, ignore_mt) == false then return false end keySet[key1] = true end for key2, _ in pairs(o2) do if not keySet[key2] then return false end end return true end function mod.assert_eq(a, b, message) if not mod.equals(a, b) then if message then message = string.format('%s. ', message) else message = '' end error( string.format( '%sexpected\n%s\n == \n%s', message, kumo.json_encode(a), kumo.json_encode(b) ), 2 ) end end function mod.assert_ne(a, b, message) if mod.equals(a, b) then if message then message = string.format('%s. ', message) else message = '' end error( string.format( '%sexpected\n%s\n != \n%s', message, kumo.json_encode(a), kumo.json_encode(b) ), 2 ) end end function mod.assert_matches(a, pattern, message) local re = kumo.regex.compile(pattern) if not re:is_match(a) then if message then message = string.format('%s. ', message) else message = '' end error( string.format( '%sexpected\n "%s"\n to match\n "%s"', message, a, pattern ), 2 ) end end -- Helper function that merges the values from `src` into `dest` function mod.merge_into(src, dest) if src then for k, v in pairs(src) do dest[k] = v end end end -- Helper function that merges the values from `src` into `dest` function mod.recursive_merge_into(src, dest) if src then for k, v in pairs(src) do if type(v) == 'table' then if not dest[k] then dest[k] = v else mod.recursive_merge_into(v, dest[k]) end else dest[k] = v end end end end -- Returns true if the table tbl contains value v function mod.table_contains(tbl, v) if v == nil then return false end for _, value in pairs(tbl) do if value == v then return true end end return false end -- Helper function to return the keys of a object style table as an array style -- table function mod.table_keys(tbl) local result = {} for k, _v in pairs(tbl) do table.insert(result, k) end return result end -- Returns true if table is empty, false otherwise function mod.table_is_empty(tbl) for _k, _v in pairs(tbl) do return false end return true end function mod.load_json_or_toml_file(filename) if type(filename) == 'table' then -- allow inline lua objects to be passed through as-is return filename end if filename:match '.toml$' then return kumo.toml_load(filename) end return kumo.json_load(filename) end -- Returns true if the string `s` starts with the string `text` function mod.starts_with(s, text) return string.sub(s, 1, #text) == text end -- Returns true if the string `s` ends with the string `text` function mod.ends_with(s, text) return string.sub(s, -#text) == text end -- Given an IP:port, split it into a tuple of IP and port function mod.split_ip_port(ip_port) -- "127.0.0.1:25" -> "127.0.0.1", "25" -- "[::1]:25" -> "[::1]", "25" local ip, port = string.match(ip_port, '^(.*):(%d+)$') -- If we have `[ip]` for ip, remove the square brackets local remove_square = string.match(ip, '^%[(.*)%]$') if remove_square then return remove_square, port end return ip, port end local function dump_impl(value, indent, done) if done[value] then return '[recursive node]' end local ty = type(value) if ty == 'table' then if has_mt(value, '__tostring') then local s = tostring(value) if s then return s end end done[value] = true local list = {} for key in pairs(value) do table.insert(list, key) end table.sort(list) local dumped = '{' for idx, key in ipairs(list) do if idx > 1 then dumped = dumped .. ',\n' else dumped = dumped .. '\n' end dumped = dumped .. string.rep(' ', indent) dumped = dumped .. string.format( '[%q] = %s', key, dump_impl(value[key], indent + 2, done) ) end done[value] = false if #dumped > 1 then dumped = dumped .. '\n' .. string.rep(' ', indent - 2) end dumped = dumped .. '}' return dumped end if ty == 'number' or ty == 'boolean' then return tostring(value) end return string.format('%q', tostring(value)) end -- Returns a psuedo-lua repr style dump of the provided value -- as a string. This is useful for getting a sense of what the -- value might be during debugging function mod.dumps(value) if value == nil then return nil end local done = {} return dump_impl(value, 2, done) end function mod:test() mod.assert_eq(mod.dumps(true), 'true') mod.assert_eq(mod.dumps(false), 'false') mod.assert_eq(mod.dumps(42), '42') mod.assert_eq(mod.dumps(1.5), '1.5') mod.assert_eq(mod.dumps {}, '{}') mod.assert_eq(mod.dumps { 1, 2 }, '{\n [1] = 1,\n [2] = 2\n}') mod.assert_eq(mod.dumps { foo = 'bar' }, '{\n ["foo"] = "bar"\n}') end return mod