[*] Initial commit.

master
Lorenzo Cogotti 2 years ago
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…
Cancel
Save