[*] Initial commit.
parent
604e0858db
commit
523995e41b
@ -1,3 +1,40 @@
|
||||
# serialize
|
||||
serialize - A brainless Lua table serializer
|
||||
============================================
|
||||
|
||||
The brainless Lua table serialization library.
|
||||
**serialize** provides two functions:
|
||||
|
||||
* `pack()` packs Lua tables to string.
|
||||
* `unpack()` unpacks a string back to a Lua table.
|
||||
|
||||
The implementation strives to be useful under the majority of reasonable use cases,
|
||||
to be compact, understandable and sufficiently fast.
|
||||
There is no pretense of complete generality, nor of absolute efficiency.
|
||||
In case **serialize** does not meet exactly your requirements, the code
|
||||
should be immediate enough to tweak to your needs.
|
||||
|
||||
Documentation
|
||||
=============
|
||||
|
||||
Code is documented with [LDoc](https://github.com/lunarmodules/LDoc).
|
||||
|
||||
Documentation may be generated running the command:
|
||||
|
||||
```sh
|
||||
ldoc init.lua
|
||||
```
|
||||
|
||||
Test suite
|
||||
==========
|
||||
|
||||
The test suite uses [busted](https://olivinelabs.com/busted/).
|
||||
|
||||
Tests may be run with the command:
|
||||
|
||||
```sh
|
||||
lua spec/serialize_spec.lua
|
||||
```
|
||||
|
||||
License
|
||||
=======
|
||||
|
||||
See [LICENSE](LICENSE) for details.
|
||||
|
@ -0,0 +1,150 @@
|
||||
--- Brainless serialization library.
|
||||
--
|
||||
-- Takes Lua tables and turns them into their string
|
||||
-- representation. Also takes a Lua table string representation
|
||||
-- and turns it back into its corresponding table.
|
||||
-- That is all.
|
||||
--
|
||||
-- @copyright 2022 The DoubleFourteen Code Forge
|
||||
-- @author Lorenzo Cogotti
|
||||
|
||||
local math = require 'math'
|
||||
|
||||
local serialize = {}
|
||||
|
||||
|
||||
local dopack -- forward declare for mutual recursion
|
||||
|
||||
local function isfinite(x)
|
||||
return x ~= math.huge and x ~= -math.huge and x == x
|
||||
end
|
||||
|
||||
local function keys(k, i)
|
||||
local t = type(k)
|
||||
|
||||
if t == 'boolean' then
|
||||
return k and "[true]" or "[false]"
|
||||
elseif t == 'string' then
|
||||
local f = ("%q"):format(k)
|
||||
if k:find(" ") or f ~= '"'..k..'"' then
|
||||
return "["..f.."]"
|
||||
else
|
||||
return k
|
||||
end
|
||||
elseif t == 'number' then
|
||||
if not isfinite(k) then
|
||||
error("Can't serialize.pack() table with non-finite key `"..k.."'.")
|
||||
end
|
||||
|
||||
return "["..k.."]"
|
||||
elseif t == 'table' then
|
||||
return "["..dopack(k, i+1).."]"
|
||||
else
|
||||
error("Can't serialize.pack() table with key `"..tostring(k).."'.")
|
||||
end
|
||||
end
|
||||
|
||||
local function vals(v, i)
|
||||
local t = type(v)
|
||||
|
||||
if t == 'boolean' then
|
||||
return v and 'true' or 'false'
|
||||
elseif t == 'string' then
|
||||
return ("%q"):format(v)
|
||||
elseif t == 'number' then
|
||||
if not isfinite(v) then
|
||||
error("Can't serialize.pack() table with non-finite value `"..v.."'.")
|
||||
end
|
||||
|
||||
return tostring(v)
|
||||
elseif t == 'table' then
|
||||
return dopack(v, i+1)
|
||||
else
|
||||
error("Can't serialize.pack() table with value `"..tostring(v).."'.")
|
||||
end
|
||||
end
|
||||
|
||||
-- local
|
||||
function dopack(o, i, mode)
|
||||
local fields = {}
|
||||
local seen = {}
|
||||
local is = (" "):rep(i)
|
||||
local lastis = (" "):rep(i-1)
|
||||
|
||||
-- Attempt to encode as array.
|
||||
for k,v in ipairs(o) do
|
||||
if mode == 'skip-functions' and type(v) == 'function' then
|
||||
goto skip
|
||||
end
|
||||
|
||||
fields[#fields + 1] = ("%s%s"):format(is, vals(v, i))
|
||||
|
||||
::skip:: seen[k] = true
|
||||
end
|
||||
|
||||
-- Process leftover fields.
|
||||
for k,v in pairs(o) do
|
||||
if seen[k] or (mode == 'skip-functions' and type(v) == 'function') then
|
||||
goto skip
|
||||
end
|
||||
|
||||
local f = ("%s%s = %s"):format(is, keys(k, i), vals(v, i))
|
||||
|
||||
fields[#fields + 1] = f
|
||||
|
||||
::skip::
|
||||
end
|
||||
|
||||
return "{\n"..table.concat(fields, ",\n").."\n"..lastis.."}"
|
||||
end
|
||||
|
||||
--- Construct string recreating a Lua table.
|
||||
--
|
||||
-- @param o (table) a Lua table.
|
||||
-- @param indent (number|nil) optional initial indent.
|
||||
-- @param mode (string|nil) one of two modes: 'strict' (default), where
|
||||
-- attempt to serialize a function is an error, or
|
||||
-- 'skip-functions', where functions are ignored.
|
||||
--
|
||||
-- @return string recreating the table, use serialize.unpack() to do so.
|
||||
function serialize.pack(o, indent, mode)
|
||||
if type(o) ~= 'table' then
|
||||
error("Can't serialize.pack() a `"..type(o).."'.")
|
||||
end
|
||||
|
||||
return dopack(o, indent or 1, mode)
|
||||
end
|
||||
|
||||
--- Reconstruct Lua table from string.
|
||||
--
|
||||
-- @param s (string) Lua table in its string form.
|
||||
-- @param chunk (string|nil) optional string providing a chunk's name
|
||||
-- for better diagnostics, if left nil a default value
|
||||
-- of "<serialize.unpack>" is used.
|
||||
--
|
||||
-- @return the reconstructed table and an error string, on success
|
||||
-- the error value is nil, on failure the table is nil.
|
||||
function serialize.unpack(s, chunk)
|
||||
if type(s) ~= 'string' then
|
||||
error("Can only serialize.unpack() strings.")
|
||||
end
|
||||
|
||||
chunk = chunk or "<serialize.unpack>"
|
||||
|
||||
local fun, res = load("return "..s, chunk, 't', {})
|
||||
if not fun then
|
||||
return nil, res
|
||||
end
|
||||
|
||||
local ok, o = pcall(fun)
|
||||
if not ok then
|
||||
return nil, o -- o is now pcall()'s error message
|
||||
end
|
||||
if type(o) ~= 'table' then
|
||||
return nil, "[string \""..chunk.."\"] resulted in a `"..type(o).."'."
|
||||
end
|
||||
|
||||
return o
|
||||
end
|
||||
|
||||
return serialize
|
@ -0,0 +1,202 @@
|
||||
require 'busted.runner'()
|
||||
|
||||
describe("serialize", function()
|
||||
setup(function()
|
||||
serialize = require 'init'
|
||||
math = require 'math'
|
||||
end)
|
||||
|
||||
it("converts tables to strings and back", function()
|
||||
local tests = {
|
||||
-- Empty table
|
||||
['{\n\n}'] = {},
|
||||
|
||||
-- Arrays
|
||||
['{\n true,\n true,\n false\n}'] =
|
||||
{ true, true, false },
|
||||
['{\n 1,\n 2,\n 3\n}'] =
|
||||
{ 1, 2, 3 },
|
||||
['{\n 1.0,\n 0.5,\n 0.25,\n 0.125,\n 0.0625\n}'] =
|
||||
{ 1.0, 0.5, 0.25, 0.125, 0.0625 },
|
||||
|
||||
-- Basic types
|
||||
['{\n key = "strings are double quoted"\n}'] =
|
||||
{ key = "strings are double quoted" },
|
||||
|
||||
['{\n key = "\'string\'"\n}'] =
|
||||
{ key = "\'string\'" },
|
||||
['{\n key = "\\"string\\""\n}'] =
|
||||
{ key = "\"string\"" },
|
||||
|
||||
['{\n ikey = 10\n}'] = { ikey = 10 },
|
||||
['{\n fkey = 0.9843\n}'] = { fkey = 0.9843 },
|
||||
['{\n falsekey = false\n}'] = { falsekey = false },
|
||||
['{\n truekey = true\n}'] = { truekey = true },
|
||||
|
||||
-- Mixed
|
||||
['{\n 3,\n 2,\n 1,\n what = "half array"\n}'] =
|
||||
{ 3, 2, 1, what = "half array" },
|
||||
}
|
||||
|
||||
for k,v in pairs(tests) do
|
||||
local s = assert.has_no.errors(function()
|
||||
return serialize.pack(v)
|
||||
end)
|
||||
assert.are.equal(s, k)
|
||||
|
||||
local o = assert(serialize.unpack(s))
|
||||
assert.are.same(v, o)
|
||||
end
|
||||
end)
|
||||
|
||||
it("accepts tables with nil entries", function()
|
||||
local tests = {
|
||||
['{ nil }'] = {},
|
||||
['{ key = nil }'] = {},
|
||||
['{ 1, 2, 3, 4, nil, 5 }'] = { 1, 2, 3, 4, nil, 5 },
|
||||
['{ 1, 2, 3, 4, nil }'] = { 1, 2, 3, 4 },
|
||||
['{ nil, 2, 3, 4 }'] = { nil, 2, 3, 4 },
|
||||
}
|
||||
|
||||
for k,v in pairs(tests) do
|
||||
local o = assert(serialize.unpack(k))
|
||||
assert.are.same(v, o)
|
||||
end
|
||||
end)
|
||||
|
||||
it("errors on attempt to serialize functions in strict mode", function()
|
||||
assert.has.error(function()
|
||||
serialize.pack({
|
||||
boom = function() end
|
||||
})
|
||||
end)
|
||||
assert.has.error(function()
|
||||
serialize.pack({
|
||||
function() end
|
||||
})
|
||||
end)
|
||||
assert.has.error(function()
|
||||
serialize.pack({
|
||||
1, 2, 3,
|
||||
key = "a key",
|
||||
boom = function() end
|
||||
})
|
||||
end)
|
||||
assert.has.error(function()
|
||||
serialize.pack({
|
||||
1, 2, function() end,
|
||||
key = "a key",
|
||||
val = 10.0e-4
|
||||
})
|
||||
end)
|
||||
end)
|
||||
|
||||
it("supports tables with complex keys", function()
|
||||
local tablekey = { key = "table key", value = { nil, 1, 2, 3 }, booga = true }
|
||||
|
||||
local test = {
|
||||
['key with spaces'] = 1,
|
||||
['key with \'single quotes\''] = 2,
|
||||
['key with "double quotes"'] = 3,
|
||||
['key with "both" \'quotes\''] = 4,
|
||||
['\b key \\ with \a escapes \t'] = 5,
|
||||
['key with embedded \0 zero'] = 6,
|
||||
[tablekey] = 7,
|
||||
[10] = 8,
|
||||
[0.5] = 9,
|
||||
[true] = 10,
|
||||
[false] = 11,
|
||||
[-1] = 12
|
||||
}
|
||||
|
||||
local s = assert.has_no.errors(function()
|
||||
return serialize.pack(test)
|
||||
end)
|
||||
|
||||
local o = assert(serialize.unpack(s))
|
||||
|
||||
-- Compare for equality (table key requires extra care).
|
||||
for k,v in pairs(o) do
|
||||
if type(k) == 'table' then
|
||||
assert.are.same(tablekey, k)
|
||||
assert.are.same(test[tablekey], v)
|
||||
else
|
||||
assert.are.same(test[k], v)
|
||||
end
|
||||
end
|
||||
|
||||
local function countkeys(o)
|
||||
local n = 0
|
||||
for _ in pairs(o) do n = n + 1 end
|
||||
return n
|
||||
end
|
||||
|
||||
assert.are.equal(countkeys(test), countkeys(o))
|
||||
end)
|
||||
|
||||
it("supports nested tables", function()
|
||||
pending("to be tested...")
|
||||
end)
|
||||
|
||||
it("allows explicit skip of functions during pack()", function()
|
||||
local expected = {
|
||||
'{\n\n}',
|
||||
'{\n\n}',
|
||||
'{\n 1,\n 2,\n key = "a key"\n}',
|
||||
'{\n 1,\n 2,\n 3,\n key = "a key"\n}'
|
||||
}
|
||||
local actual = {}
|
||||
|
||||
assert.has_no.errors(function()
|
||||
actual[#actual+1] = serialize.pack({
|
||||
boom = function() end
|
||||
}, 1, 'skip-functions')
|
||||
actual[#actual+1] = serialize.pack({
|
||||
function() end
|
||||
}, 1, 'skip-functions')
|
||||
actual[#actual+1] = serialize.pack({
|
||||
1, 2, function() end,
|
||||
key = "a key"
|
||||
}, 1, 'skip-functions')
|
||||
actual[#actual+1] = serialize.pack({
|
||||
1, 2, 3,
|
||||
key = "a key",
|
||||
boom = function() end
|
||||
}, 1, 'skip-functions')
|
||||
end)
|
||||
|
||||
assert.are.same(expected, actual)
|
||||
end)
|
||||
|
||||
it("errors on non-finite keys or values", function()
|
||||
assert.has.error(function() serialize.pack({ math.huge }) end)
|
||||
assert.has.error(function() serialize.pack({ -math.huge }) end)
|
||||
assert.has.error(function() serialize.pack({ 0/0 }) end)
|
||||
|
||||
assert.has.error(function() serialize.pack({ [-math.huge] = "-inf" }) end)
|
||||
assert.has.error(function() serialize.pack({ [ math.huge] = "inf" }) end)
|
||||
assert.has.error(function() serialize.pack({ [ 0/0] = "NaN" }) end)
|
||||
|
||||
assert.has.error(function() serialize.pack({ inf = math.huge }) end)
|
||||
assert.has.error(function() serialize.pack({ neginf = -math.huge }) end)
|
||||
assert.has.error(function() serialize.pack({ nan = 0/0 }) end)
|
||||
end)
|
||||
|
||||
it("may only pack() tables", function()
|
||||
assert.has.error(function() serialize.pack("meow") end)
|
||||
assert.has.error(function() serialize.pack(true) end)
|
||||
assert.has.error(function() serialize.pack(42) end)
|
||||
assert.has.error(function() serialize.pack(25.12) end)
|
||||
assert.has.error(function() serialize.pack(nil) end)
|
||||
assert.has.error(function() serialize.pack(function() end) end)
|
||||
end)
|
||||
|
||||
it("may only unpack() strings", function()
|
||||
assert.has.error(function() serialize.unpack({}) end)
|
||||
assert.has.error(function() serialize.unpack(true) end)
|
||||
assert.has.error(function() serialize.unpack(42) end)
|
||||
assert.has.error(function() serialize.unpack(25.12) end)
|
||||
assert.has.error(function() serialize.unpack(nil) end)
|
||||
assert.has.error(function() serialize.unpack(function() end) end)
|
||||
end)
|
||||
end)
|
Loading…
Reference in New Issue