340 lines
9.1 KiB
Lua
340 lines
9.1 KiB
Lua
-- SPDX-License-Identifier: GPL-3.0-or-later
|
|
local base_class = {
|
|
cur_indent = 0,
|
|
}
|
|
|
|
-- shared constructor: use as serializer_class:new()
|
|
function base_class.new(class, on_unrepresentable)
|
|
on_unrepresentable = on_unrepresentable or 'comment'
|
|
if on_unrepresentable ~= 'comment'
|
|
and on_unrepresentable ~= 'error' then
|
|
error('unsupported val2expr on_unrepresentable option '
|
|
.. tostring(on_unrepresentable))
|
|
end
|
|
local inst = {}
|
|
inst.on_unrepresentable = on_unrepresentable
|
|
inst.done = {}
|
|
inst.tab_key_path = {}
|
|
setmetatable(inst, class.__inst_mt)
|
|
return inst
|
|
end
|
|
|
|
-- format comment with leading/ending whitespace if needed
|
|
function base_class.format_note(_, note, ws_prefix, ws_suffix)
|
|
if note == nil then
|
|
return ''
|
|
else
|
|
return string.format('%s--[[ %s ]]%s',
|
|
ws_prefix or '', note, ws_suffix or '')
|
|
end
|
|
end
|
|
|
|
function base_class.indent_head(self)
|
|
return string.rep(' ', self.cur_indent)
|
|
end
|
|
|
|
function base_class.indent_inc(self)
|
|
self.cur_indent = self.cur_indent + self.indent_step
|
|
end
|
|
|
|
function base_class.indent_dec(self)
|
|
self.cur_indent = self.cur_indent - self.indent_step
|
|
end
|
|
|
|
function base_class._fallback(self, val)
|
|
if self.on_unrepresentable == 'comment' then
|
|
return 'nil', string.format('missing %s', val)
|
|
elseif self.on_unrepresentable == 'error' then
|
|
local key_path_msg
|
|
if #self.tab_key_path > 0 then
|
|
local str_key_path = {}
|
|
for _, key in ipairs(self.tab_key_path) do
|
|
table.insert(str_key_path,
|
|
string.format('%s %s', type(key), self:string(tostring(key))))
|
|
end
|
|
local key_path = '[' .. table.concat(str_key_path, '][') .. ']'
|
|
key_path_msg = string.format(' (found at [%s])', key_path)
|
|
else
|
|
key_path_msg = ''
|
|
end
|
|
error(string.format('cannot serialize type %s%s', type(val), key_path_msg), 2)
|
|
end
|
|
end
|
|
|
|
function base_class.val2expr(self, val)
|
|
local val_type = type(val)
|
|
local val_repr = self[val_type]
|
|
if val_repr then
|
|
return val_repr(self, val)
|
|
else
|
|
return self:_fallback(val)
|
|
end
|
|
end
|
|
|
|
-- "nil" is a Lua keyword so assignment below is workaround to create
|
|
-- function base_class.nil(self, val)
|
|
base_class['nil'] = function(_, val)
|
|
assert(type(val) == 'nil')
|
|
return 'nil'
|
|
end
|
|
|
|
function base_class.number(_, val)
|
|
assert(type(val) == 'number')
|
|
if val == math.huge then
|
|
return 'math.huge'
|
|
elseif val == -math.huge then
|
|
return '-math.huge'
|
|
elseif tostring(val) == 'nan' then
|
|
return 'tonumber(\'nan\')'
|
|
else
|
|
return string.format("%.60f", val)
|
|
end
|
|
end
|
|
|
|
function base_class.char_is_printable(_, c)
|
|
-- ASCII (from space to ~) and not ' or \
|
|
return (c >= 0x20 and c < 0x7f)
|
|
and c ~= 0x27 and c ~= 0x5C
|
|
end
|
|
|
|
function base_class.string(self, val)
|
|
assert(type(val) == 'string')
|
|
local chars = {'\''}
|
|
for i = 1, #val do
|
|
local c = string.byte(val, i)
|
|
if self:char_is_printable(c) then
|
|
table.insert(chars, string.char(c))
|
|
else
|
|
table.insert(chars, string.format('\\%03d', c))
|
|
end
|
|
end
|
|
table.insert(chars, '\'')
|
|
return table.concat(chars)
|
|
end
|
|
|
|
function base_class.boolean(_, val)
|
|
assert(type(val) == 'boolean')
|
|
return tostring(val)
|
|
end
|
|
|
|
local function ordered_iter(unordered_tt)
|
|
local keys = {}
|
|
for k in pairs(unordered_tt) do
|
|
table.insert(keys, k)
|
|
end
|
|
table.sort(keys,
|
|
function (a, b)
|
|
if type(a) ~= type(b) then
|
|
return type(a) < type(b)
|
|
end
|
|
if type(a) == 'number' then
|
|
return a < b
|
|
else
|
|
return tostring(a) < tostring(b)
|
|
end
|
|
end)
|
|
local i = 0
|
|
return function()
|
|
i = i + 1
|
|
if keys[i] ~= nil then
|
|
return keys[i], unordered_tt[keys[i]]
|
|
end
|
|
end
|
|
end
|
|
|
|
function base_class.table(self, tab)
|
|
assert(type(tab) == 'table')
|
|
if self.done[tab] then
|
|
error('cyclic reference', 0)
|
|
end
|
|
self.done[tab] = true
|
|
|
|
local items = {'{'}
|
|
local previdx = 0
|
|
self:indent_inc()
|
|
for idx, val in ordered_iter(tab) do
|
|
local errors, valok, valexpr, valnote, idxok, idxexpr, idxnote
|
|
errors = {}
|
|
-- push current index onto key path stack to make it available to sub-printers
|
|
table.insert(self.tab_key_path, idx)
|
|
|
|
valok, valexpr, valnote = pcall(self.val2expr, self, val)
|
|
if not valok then
|
|
table.insert(errors, string.format('value: %s', valexpr))
|
|
end
|
|
|
|
local addidx
|
|
if previdx and type(idx) == 'number' and idx - 1 == previdx then
|
|
-- monotonic sequence, do not print key
|
|
previdx = idx
|
|
addidx = false
|
|
else
|
|
-- end of monotonic sequence
|
|
-- from now on print keys as well
|
|
previdx = nil
|
|
addidx = true
|
|
end
|
|
|
|
if addidx then
|
|
idxok, idxexpr, idxnote = pcall(self.val2expr, self, idx)
|
|
if not idxok or idxexpr == 'nil' then
|
|
table.insert(errors, string.format('key: not serializable', idxexpr))
|
|
end
|
|
end
|
|
|
|
local item = ''
|
|
if #errors == 0 then
|
|
-- finally serialize one [key=]?value expression
|
|
local indent = self:indent_head()
|
|
local note
|
|
if addidx then
|
|
note = self:format_note(idxnote, nil, self.key_val_sep)
|
|
item = string.format('%s%s[%s]%s=%s',
|
|
indent, note,
|
|
idxexpr, self.key_val_sep, self.key_val_sep)
|
|
indent = ''
|
|
end
|
|
note = self:format_note(valnote, nil, self.item_sep)
|
|
item = item .. string.format('%s%s%s,', indent, note, valexpr)
|
|
else
|
|
local errmsg = string.format('cannot print %s = %s (%s)',
|
|
self:string(tostring(idx)),
|
|
self:string(tostring(val)),
|
|
table.concat(errors, ', '))
|
|
if self.on_unrepresentable == 'error' then
|
|
error(errmsg, 0)
|
|
else
|
|
errmsg = string.format('--[[ missing %s ]]', errmsg)
|
|
item = errmsg
|
|
end
|
|
end
|
|
table.insert(items, item)
|
|
table.remove(self.tab_key_path) -- pop current index from key path stack
|
|
end -- one key+value
|
|
self:indent_dec()
|
|
table.insert(items, self:indent_head() .. '}')
|
|
return table.concat(items, self.item_sep), string.format('%s follows', tab)
|
|
end
|
|
|
|
-- machine readable variant, cannot represent all types and repeated references to a table
|
|
local serializer_class = {
|
|
indent_step = 0,
|
|
item_sep = ' ',
|
|
key_val_sep = ' ',
|
|
__inst_mt = {}
|
|
}
|
|
-- inheritance form base class (for :new())
|
|
setmetatable(serializer_class, { __index = base_class })
|
|
-- class instances with following metatable inherit all class members
|
|
serializer_class.__inst_mt.__index = serializer_class
|
|
|
|
local function static_serializer(val, on_unrepresentable)
|
|
local inst = serializer_class:new(on_unrepresentable)
|
|
local expr, note = inst:val2expr(val)
|
|
return string.format('%s%s', inst:format_note(note, nil, inst.item_sep), expr)
|
|
end
|
|
|
|
-- human friendly variant, not stable and not intended for machine consumption
|
|
local pprinter_class = {
|
|
indent_step = 4,
|
|
item_sep = '\n',
|
|
key_val_sep = ' ',
|
|
__inst_mt = {},
|
|
}
|
|
|
|
-- should be always empty because pretty-printer has fallback for all types
|
|
function pprinter_class.format_note()
|
|
return ''
|
|
end
|
|
|
|
function pprinter_class._fallback(self, val)
|
|
if self.on_unrepresentable == 'error' then
|
|
base_class._fallback(self, val)
|
|
end
|
|
return tostring(val)
|
|
end
|
|
|
|
function pprinter_class.char_is_printable(_, c)
|
|
-- ASCII (from space to ~) + tab or newline
|
|
-- and not ' or \
|
|
return ((c >= 0x20 and c < 0x7f)
|
|
or c == 0x09 or c == 0x0A)
|
|
and c ~= 0x27 and c ~= 0x5C
|
|
end
|
|
|
|
-- "function" is a Lua keyword so assignment below is workaround to create
|
|
-- function pprinter_class.function(self, f)
|
|
pprinter_class['function'] = function(self, f)
|
|
-- thanks to AnandA777 from StackOverflow! Function funcsign is adapted version of
|
|
-- https://stackoverflow.com/questions/51095022/inspect-function-signature-in-lua-5-1
|
|
assert(type(f) == 'function', "bad argument #1 to 'funcsign' (function expected)")
|
|
local debuginfo = debug.getinfo(f)
|
|
local func_args = {}
|
|
local args_str
|
|
if debuginfo.what == 'C' then -- names N/A
|
|
args_str = '(?)'
|
|
goto add_name
|
|
end
|
|
|
|
pcall(function()
|
|
local oldhook
|
|
local delay = 2
|
|
local function hook()
|
|
delay = delay - 1
|
|
if delay == 0 then -- call this only for the introspected function
|
|
-- stack depth 2 is the introspected function
|
|
for i = 1, debuginfo.nparams do
|
|
local k = debug.getlocal(2, i)
|
|
table.insert(func_args, k)
|
|
end
|
|
if debuginfo.isvararg then
|
|
table.insert(func_args, "...")
|
|
end
|
|
debug.sethook(oldhook)
|
|
error('aborting the call to introspected function')
|
|
end
|
|
end
|
|
oldhook = debug.sethook(hook, "c") -- invoke hook() on function call
|
|
f(unpack({})) -- huh?
|
|
end)
|
|
args_str = "(" .. table.concat(func_args, ", ") .. ")"
|
|
::add_name::
|
|
local name
|
|
if #self.tab_key_path > 0 then
|
|
name = string.format('function %s', self.tab_key_path[#self.tab_key_path])
|
|
else
|
|
name = 'function '
|
|
end
|
|
return string.format('%s%s: %s', name, args_str, string.sub(tostring(f), 11))
|
|
end
|
|
|
|
-- default tostring method is better suited for human-intended output
|
|
function pprinter_class.number(_, number)
|
|
return tostring(number)
|
|
end
|
|
|
|
local function deserialize_lua(serial)
|
|
assert(type(serial) == 'string')
|
|
local deserial_func = loadstring('return ' .. serial)
|
|
if type(deserial_func) ~= 'function' then
|
|
panic('input is not a valid Lua expression')
|
|
end
|
|
return deserial_func()
|
|
end
|
|
|
|
setmetatable(pprinter_class, { __index = base_class })
|
|
pprinter_class.__inst_mt.__index = pprinter_class
|
|
|
|
local function static_pprint(val, on_unrepresentable)
|
|
local inst = pprinter_class:new(on_unrepresentable)
|
|
local expr, note = inst:val2expr(val)
|
|
return string.format('%s%s', inst:format_note(note, nil, inst.item_sep), expr)
|
|
end
|
|
|
|
local M = {
|
|
serialize_lua = static_serializer,
|
|
deserialize_lua = deserialize_lua,
|
|
pprint = static_pprint
|
|
}
|
|
|
|
return M
|