[*] 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