-- 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