diff --git a/docs/JSON License.txt b/docs/JSON License.txt new file mode 100644 index 00000000..f12cb56c --- /dev/null +++ b/docs/JSON License.txt @@ -0,0 +1,27 @@ +The following license is applied to all documents in this project with the +exception of the 'tests' directory at the root. +The 'tests' directory is dual-licenses Public Domain / MIT, whichever is +least restrictive in your legal jurisdiction. + +The MIT License + +Copyright (c) 2008 Thomas Harning Jr. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/install/mushclient.nsi b/install/mushclient.nsi index 77792ce9..e30e8eb0 100644 --- a/install/mushclient.nsi +++ b/install/mushclient.nsi @@ -248,6 +248,7 @@ Section "Documentation" File "..\docs\lpeg.html" File "..\docs\re.html" File "..\docs\Lua Colors LICENSE.txt" + File "..\docs\JSON License.txt" SectionEnd @@ -321,6 +322,41 @@ SetOverwrite ifnewer File "..\lua\ppi.lua" File "..\lua\mapper.lua" + ; JSON stuff from http://luaforge.net/projects/luajson/ + ; version 1.1 + + File "..\lua\json.lua" + + CreateDirectory "$INSTDIR\lua\json" + SetOutPath $INSTDIR\lua + + File "..\lua\json\encode.lua" + File "..\lua\json\decode.lua" + File "..\lua\json\encode.lua" + + CreateDirectory "$INSTDIR\lua\json\encode" + SetOutPath $INSTDIR\lua\encode + + File "..\lua\json\encode\array.lua" + File "..\lua\json\encode\calls.lua" + File "..\lua\json\encode\number.lua" + File "..\lua\json\encode\object.lua" + File "..\lua\json\encode\others.lua" + File "..\lua\json\encode\output.lua" + File "..\lua\json\encode\output_utility.lua" + File "..\lua\json\encode\strings.lua" + + CreateDirectory "$INSTDIR\lua\json\decode" + SetOutPath $INSTDIR\lua\decode + + File "..\lua\json\decode\array.lua" + File "..\lua\json\decode\calls.lua" + File "..\lua\json\decode\number.lua" + File "..\lua\json\decode\object.lua" + File "..\lua\json\decode\others.lua" + File "..\lua\json\decode\strings.lua" + File "..\lua\json\decode\util.lua" + ; Set output path to the scripts subdirectory. SetOutPath $INSTDIR\scripts @@ -472,6 +508,30 @@ Section Uninstall Delete "$INSTDIR\lua\ppi.lua" Delete "$INSTDIR\lua\mapper.lua" + Delete "$INSTDIR\lua\json.lua" + Delete "$INSTDIR\lua\json\encode.lua" + Delete "$INSTDIR\lua\json\decode.lua" + Delete "$INSTDIR\lua\json\encode.lua" + Delete "$INSTDIR\lua\json\encode\array.lua" + Delete "$INSTDIR\lua\json\encode\calls.lua" + Delete "$INSTDIR\lua\json\encode\number.lua" + Delete "$INSTDIR\lua\json\encode\object.lua" + Delete "$INSTDIR\lua\json\encode\others.lua" + Delete "$INSTDIR\lua\json\encode\output.lua" + Delete "$INSTDIR\lua\json\encode\output_utility.lua" + Delete "$INSTDIR\lua\json\encode\strings.lua" + Delete "$INSTDIR\lua\json\decode\array.lua" + Delete "$INSTDIR\lua\json\decode\calls.lua" + Delete "$INSTDIR\lua\json\decode\number.lua" + Delete "$INSTDIR\lua\json\decode\object.lua" + Delete "$INSTDIR\lua\json\decode\others.lua" + Delete "$INSTDIR\lua\json\decode\strings.lua" + Delete "$INSTDIR\lua\json\decode\util.lua" + + RMDir "$INSTDIR\lua\json\decode" + RMDir "$INSTDIR\lua\json\encode" + RMDir "$INSTDIR\lua\json" + ; spell checker stuff Delete "$INSTDIR\spellchecker.lua" Delete "$INSTDIR\spell\english-words.10" @@ -498,6 +558,7 @@ Section Uninstall Delete "$INSTDIR\docs\lpeg.html" Delete "$INSTDIR\docs\re.html" Delete "$INSTDIR\docs\Lua Colors LICENSE.txt" + Delete "$INSTDIR\docs\JSON License.txt" RMDir "$INSTDIR\docs" diff --git a/lua/json.lua b/lua/json.lua new file mode 100644 index 00000000..296fb0d0 --- /dev/null +++ b/lua/json.lua @@ -0,0 +1,12 @@ +--[[ + Licensed according to the included 'LICENSE' document + Author: Thomas Harning Jr +]] +local decode = require("json.decode") +local encode = require("json.encode") +local util = require("json.util") + +module("json") +_M.decode = decode +_M.encode = encode +_M.util = util diff --git a/lua/json/decode.lua b/lua/json/decode.lua new file mode 100644 index 00000000..a1276f19 --- /dev/null +++ b/lua/json/decode.lua @@ -0,0 +1,132 @@ +--[[ + Licensed according to the included 'LICENSE' document + Author: Thomas Harning Jr +]] +local lpeg = require("lpeg") + +local error = error + +local object = require("json.decode.object") +local array = require("json.decode.array") + +local merge = require("json.util").merge +local util = require("json.decode.util") + +local setmetatable, getmetatable = setmetatable, getmetatable +local assert = assert +local ipairs, pairs = ipairs, pairs +local string_char = string.char + +local require = require +module("json.decode") + +local modulesToLoad = { + "array", + "object", + "strings", + "number", + "calls", + "others" +} +local loadedModules = { +} + +default = { + unicodeWhitespace = true, + initialObject = false +} + +local modes_defined = { "default", "strict", "simple" } + +simple = {} + +strict = { + unicodeWhitespace = true, + initialObject = true +} + +-- Register generic value type +util.register_type("VALUE") +for _,name in ipairs(modulesToLoad) do + local mod = require("json.decode." .. name) + for _, mode in pairs(modes_defined) do + if mod[mode] then + _M[mode][name] = mod[mode] + end + end + loadedModules[name] = mod + -- Register types + if mod.register_types then + mod.register_types() + end +end + +-- Shift over default into defaultOptions to permit build optimization +local defaultOptions = default +default = nil + + +local function buildDecoder(mode) + mode = mode and merge({}, defaultOptions, mode) or defaultOptions + local ignored = mode.unicodeWhitespace and util.unicode_ignored or util.ascii_ignored + -- Store 'ignored' in the global options table + mode.ignored = ignored + + local value_id = util.types.VALUE + local value_type = lpeg.V(value_id) + local object_type = lpeg.V(util.types.OBJECT) + local array_type = lpeg.V(util.types.ARRAY) + local grammar = { + [1] = mode.initialObject and (ignored * (object_type + array_type)) or value_type + } + for _, name in pairs(modulesToLoad) do + local mod = loadedModules[name] + mod.load_types(mode[name], mode, grammar) + end + -- HOOK VALUE TYPE WITH WHITESPACE + grammar[value_id] = ignored * grammar[value_id] * ignored + grammar = lpeg.P(grammar) * ignored * lpeg.Cp() * -1 + return function(data) + local ret, next_index = lpeg.match(grammar, data) + assert(nil ~= next_index, "Invalid JSON data") + return ret + end +end + +-- Since 'default' is nil, we cannot take map it +local defaultDecoder = buildDecoder(default) +local prebuilt_decoders = {} +for _, mode in pairs(modes_defined) do + if _M[mode] ~= nil then + prebuilt_decoders[_M[mode]] = buildDecoder(_M[mode]) + end +end + +--[[ +Options: + number => number decode options + string => string decode options + array => array decode options + object => object decode options + initialObject => whether or not to require the initial object to be a table/array + allowUndefined => whether or not to allow undefined values +]] +function getDecoder(mode) + mode = mode == true and strict or mode or default + local decoder = mode == nil and defaultDecoder or prebuilt_decoders[mode] + if decoder then + return decoder + end + return buildDecoder(mode) +end + +function decode(data, mode) + local decoder = getDecoder(mode) + return decoder(data) +end + +local mt = getmetatable(_M) or {} +mt.__call = function(self, ...) + return decode(...) +end +setmetatable(_M, mt) diff --git a/lua/json/decode/array.lua b/lua/json/decode/array.lua new file mode 100644 index 00000000..fc7182f1 --- /dev/null +++ b/lua/json/decode/array.lua @@ -0,0 +1,64 @@ +--[[ + Licensed according to the included 'LICENSE' document + Author: Thomas Harning Jr +]] +local lpeg = require("lpeg") + +local util = require("json.decode.util") +local jsonutil = require("json.util") + +local table_maxn = table.maxn + +local unpack = unpack + +module("json.decode.array") + +-- Utility function to help manage slighly sparse arrays +local function processArray(array) + local max_n = table_maxn(array) + -- Only populate 'n' if it is necessary + if #array ~= max_n then + array.n = max_n + end + if jsonutil.InitArray then + array = jsonutil.InitArray(array) or array + end + return array +end + +local defaultOptions = { + trailingComma = true +} + +default = nil -- Let the buildCapture optimization take place +strict = { + trailingComma = false +} + +local function buildCapture(options, global_options) + local ignored = global_options.ignored + -- arrayItem == element + local arrayItem = lpeg.V(util.types.VALUE) + local arrayElements = lpeg.Ct(arrayItem * (ignored * lpeg.P(',') * ignored * arrayItem)^0 + 0) / processArray + + options = options and jsonutil.merge({}, defaultOptions, options) or defaultOptions + local capture = lpeg.P("[") + capture = capture * ignored + * arrayElements * ignored + if options.trailingComma then + capture = capture * (lpeg.P(",") + 0) * ignored + end + capture = capture * lpeg.P("]") + return capture +end + +function register_types() + util.register_type("ARRAY") +end + +function load_types(options, global_options, grammar) + local capture = buildCapture(options, global_options) + local array_id = util.types.ARRAY + grammar[array_id] = capture + util.append_grammar_item(grammar, "VALUE", lpeg.V(array_id)) +end diff --git a/lua/json/decode/calls.lua b/lua/json/decode/calls.lua new file mode 100644 index 00000000..4f9c8a02 --- /dev/null +++ b/lua/json/decode/calls.lua @@ -0,0 +1,116 @@ +--[[ + Licensed according to the included 'LICENSE' document + Author: Thomas Harning Jr +]] +local lpeg = require("lpeg") +local tostring = tostring +local pairs, ipairs = pairs, ipairs +local next, type = next, type +local error = error + +local util = require("json.decode.util") + +local buildCall = require("json.util").buildCall + +local getmetatable = getmetatable + +module("json.decode.calls") + +local defaultOptions = { + defs = nil, + -- By default, do not allow undefined calls to be de-serialized as call objects + allowUndefined = false +} + +-- No real default-option handling needed... +default = nil +strict = nil + +local isPattern +if lpeg.type then + function isPattern(value) + return lpeg.type(value) == 'pattern' + end +else + local metaAdd = getmetatable(lpeg.P("")).__add + function isPattern(value) + return getmetatable(value).__add == metaAdd + end +end + +local function buildDefinedCaptures(argumentCapture, defs) + local callCapture + if not defs then return end + for name, func in pairs(defs) do + if type(name) ~= 'string' and not isPattern(name) then + error("Invalid functionCalls name: " .. tostring(name) .. " not a string or LPEG pattern") + end + -- Allow boolean or function to match up w/ encoding permissions + if type(func) ~= 'boolean' and type(func) ~= 'function' then + error("Invalid functionCalls item: " .. name .. " not a function") + end + local nameCallCapture + if type(name) == 'string' then + nameCallCapture = lpeg.P(name .. "(") * lpeg.Cc(name) + else + -- Name matcher expected to produce a capture + nameCallCapture = name * "(" + end + -- Call func over nameCallCapture and value to permit function receiving name + + -- Process 'func' if it is not a function + if type(func) == 'boolean' then + local allowed = func + func = function(name, ...) + if not allowed then + error("Function call on '" .. name .. "' not permitted") + end + return buildCall(name, ...) + end + else + local inner_func = func + func = function(...) + return (inner_func(...)) + end + end + local newCapture = (nameCallCapture * argumentCapture) / func * ")" + if not callCapture then + callCapture = newCapture + else + callCapture = callCapture + newCapture + end + end + return callCapture +end + +local function buildCapture(options) + if not options -- No ops, don't bother to parse + or not (options.defs and (nil ~= next(options.defs)) or options.allowUndefined) then + return nil + end + -- Allow zero or more arguments separated by commas + local value = lpeg.V(util.types.VALUE) + local argumentCapture = (value * (lpeg.P(",") * value)^0) + 0 + local callCapture = buildDefinedCaptures(argumentCapture, options.defs) + if options.allowUndefined then + local function func(name, ...) + return buildCall(name, ...) + end + -- Identifier-type-match + local nameCallCapture = lpeg.C(util.identifier) * "(" + local newCapture = (nameCallCapture * argumentCapture) / func * ")" + if not callCapture then + callCapture = newCapture + else + callCapture = callCapture + newCapture + end + end + return callCapture +end + +function load_types(options, global_options, grammar) + local capture = buildCapture(options, global_options) + if capture then + util.append_grammar_item(grammar, "VALUE", capture) + end +end diff --git a/lua/json/decode/number.lua b/lua/json/decode/number.lua new file mode 100644 index 00000000..cdca5cfe --- /dev/null +++ b/lua/json/decode/number.lua @@ -0,0 +1,81 @@ +--[[ + Licensed according to the included 'LICENSE' document + Author: Thomas Harning Jr +]] +local lpeg = require("lpeg") +local tonumber = tonumber +local merge = require("json.util").merge +local util = require("json.decode.util") + +module("json.decode.number") + +local digit = lpeg.R("09") +local digits = digit^1 + +int = (lpeg.P('-') + 0) * (lpeg.R("19") * digits + digit) +local int = int + +local frac = lpeg.P('.') * digits + +local exp = lpeg.S("Ee") * (lpeg.S("-+") + 0) * digits + +local nan = lpeg.S("Nn") * lpeg.S("Aa") * lpeg.S("Nn") +local inf = (lpeg.P('-') + 0) * lpeg.S("Ii") * lpeg.P("nfinity") +local hex = (lpeg.P("0x") + lpeg.P("0X")) * lpeg.R("09","AF","af")^1 + +local defaultOptions = { + nan = true, + inf = true, + frac = true, + exp = true, + hex = false +} + +default = nil -- Let the buildCapture optimization take place +strict = { + nan = false, + inf = false +} +--[[ + Options: configuration options for number rules + nan: match NaN + inf: match Infinity + frac: match fraction portion (.0) + exp: match exponent portion (e1) + DEFAULT: nan, inf, frac, exp +]] +local function buildMatch(options) + options = options and merge({}, defaultOptions, options) or defaultOptions + local ret = int + if options.frac then + ret = ret * (frac + 0) + end + if options.exp then + ret = ret * (exp + 0) + end + if options.hex then + ret = hex + ret + end + if options.nan then + ret = ret + nan + end + if options.inf then + ret = ret + inf + end + return ret +end + +local function buildCapture(options) + return buildMatch(options) / tonumber +end + +function register_types() + util.register_type("INTEGER") +end + +function load_types(options, global_options, grammar) + local integer_id = util.types.INTEGER + local capture = buildCapture(options) + util.append_grammar_item(grammar, "VALUE", capture) + grammar[integer_id] = int / tonumber +end diff --git a/lua/json/decode/object.lua b/lua/json/decode/object.lua new file mode 100644 index 00000000..b6bbdb07 --- /dev/null +++ b/lua/json/decode/object.lua @@ -0,0 +1,96 @@ +--[[ + Licensed according to the included 'LICENSE' document + Author: Thomas Harning Jr +]] +local lpeg = require("lpeg") + +local util = require("json.decode.util") +local merge = require("json.util").merge + +local tonumber = tonumber +local unpack = unpack +local print = print +local tostring = tostring + +local rawset = rawset + +local DecimalLpegVersion = util.DecimalLpegVersion + +module("json.decode.object") + +-- BEGIN LPEG < 0.9 SUPPORT +local initObject, applyObjectKey +if DecimalLpegVersion < 0.9 then + function initObject() + return {} + end + function applyObjectKey(tab, key, val) + tab[key] = val + return tab + end +end +-- END LPEG < 0.9 SUPPORT + +local defaultOptions = { + number = true, + identifier = true, + trailingComma = true +} + +default = nil -- Let the buildCapture optimization take place + +strict = { + number = false, + identifier = false, + trailingComma = false +} + +local function buildItemSequence(objectItem, ignored) + return (objectItem * (ignored * lpeg.P(",") * ignored * objectItem)^0) + 0 +end + +local function buildCapture(options, global_options) + local ignored = global_options.ignored + local string_type = lpeg.V(util.types.STRING) + local integer_type = lpeg.V(util.types.INTEGER) + local value_type = lpeg.V(util.types.VALUE) + options = options and merge({}, defaultOptions, options) or defaultOptions + local key = string_type + if options.identifier then + key = key + lpeg.C(util.identifier) + end + if options.number then + key = key + integer_type + end + local objectItems + local objectItem = (key * ignored * lpeg.P(":") * ignored * value_type) + -- BEGIN LPEG < 0.9 SUPPORT + if DecimalLpegVersion < 0.9 then + objectItems = buildItemSequence(objectItem / applyObjectKey, ignored) + objectItems = lpeg.Ca(lpeg.Cc(false) / initObject * objectItems) + -- END LPEG < 0.9 SUPPORT + else + objectItems = buildItemSequence(lpeg.Cg(objectItem), ignored) + objectItems = lpeg.Cf(lpeg.Ct(0) * objectItems, rawset) + end + + + local capture = lpeg.P("{") * ignored + capture = capture * objectItems * ignored + if options.trailingComma then + capture = capture * (lpeg.P(",") + 0) * ignored + end + capture = capture * lpeg.P("}") + return capture +end + +function register_types() + util.register_type("OBJECT") +end + +function load_types(options, global_options, grammar) + local capture = buildCapture(options, global_options) + local object_id = util.types.OBJECT + grammar[object_id] = capture + util.append_grammar_item(grammar, "VALUE", lpeg.V(object_id)) +end diff --git a/lua/json/decode/others.lua b/lua/json/decode/others.lua new file mode 100644 index 00000000..ffcf0890 --- /dev/null +++ b/lua/json/decode/others.lua @@ -0,0 +1,51 @@ +--[[ + Licensed according to the included 'LICENSE' document + Author: Thomas Harning Jr +]] +local lpeg = require("lpeg") +local jsonutil = require("json.util") +local util = require("json.decode.util") + +-- Container module for other JavaScript types (bool, null, undefined) +module("json.decode.others") + +-- For null and undefined, use the util.null value to preserve null-ness +local booleanCapture = + lpeg.P("true") * lpeg.Cc(true) + + lpeg.P("false") * lpeg.Cc(false) + +local nullCapture = lpeg.P("null") +local undefinedCapture = lpeg.P("undefined") + +local defaultOptions = { + allowUndefined = true, + null = jsonutil.null, + undefined = jsonutil.undefined +} + +default = nil -- Let the buildCapture optimization take place +simple = { + null = false, -- Mapped to nil + undefined = false -- Mapped to nil +} +strict = { + allowUndefined = false +} + +local function buildCapture(options) + -- The 'or nil' clause allows false to map to a nil value since 'nil' cannot be merged + options = options and jsonutil.merge({}, defaultOptions, options) or defaultOptions + local valueCapture = ( + booleanCapture + + nullCapture * lpeg.Cc(options.null or nil) + ) + if options.allowUndefined then + valueCapture = valueCapture + undefinedCapture * lpeg.Cc(options.undefined or nil) + end + return valueCapture +end + +function load_types(options, global_options, grammar) + local capture = buildCapture(options) + util.append_grammar_item(grammar, "VALUE", capture) +end diff --git a/lua/json/decode/strings.lua b/lua/json/decode/strings.lua new file mode 100644 index 00000000..f123112e --- /dev/null +++ b/lua/json/decode/strings.lua @@ -0,0 +1,131 @@ +--[[ + Licensed according to the included 'LICENSE' document + Author: Thomas Harning Jr +]] +local lpeg = require("lpeg") +local util = require("json.decode.util") +local merge = require("json.util").merge + +local tonumber = tonumber +local string = string +local string_char = string.char +local floor = math.floor +local table_concat = table.concat + +local error = error +module("json.decode.strings") +local function get_error(item) + local fmt_string = item .. " in string [%q] @ %i:%i" + return function(data, index) + local line, line_index, bad_char, last_line = util.get_invalid_character_info(data, index) + local err = fmt_string:format(bad_char, line, line_index) + error(err) + end +end + +local bad_unicode = get_error("Illegal unicode escape") +local bad_hex = get_error("Illegal hex escape") +local bad_character = get_error("Illegal character") +local bad_escape = get_error("Illegal escape") + +local knownReplacements = { + ["'"] = "'", + ['"'] = '"', + ['\\'] = '\\', + ['/'] = '/', + b = '\b', + f = '\f', + n = '\n', + r = '\r', + t = '\t', + v = '\v', + z = '\z' +} + +-- according to the table at http://da.wikipedia.org/wiki/UTF-8 +local function utf8DecodeUnicode(code1, code2) + code1, code2 = tonumber(code1, 16), tonumber(code2, 16) + if code1 == 0 and code2 < 0x80 then + return string_char(code2) + end + if code1 < 0x08 then + return string_char( + 0xC0 + code1 * 4 + floor(code2 / 64), + 0x80 + code2 % 64) + end + return string_char( + 0xE0 + floor(code1 / 16), + 0x80 + (code1 % 16) * 4 + floor(code2 / 64), + 0x80 + code2 % 64) +end + +local function decodeX(code) + code = tonumber(code, 16) + return string_char(code) +end + +local doSimpleSub = lpeg.C(lpeg.S("'\"\\/bfnrtvz")) / knownReplacements +local doUniSub = lpeg.P('u') * (lpeg.C(util.hexpair) * lpeg.C(util.hexpair) + lpeg.P(bad_unicode)) +local doXSub = lpeg.P('x') * (lpeg.C(util.hexpair) + lpeg.P(bad_hex)) + +local defaultOptions = { + badChars = '', + additionalEscapes = false, -- disallow untranslated escapes + escapeCheck = #lpeg.S('bfnrtv/\\"xu\'z'), -- no check on valid characters + decodeUnicode = utf8DecodeUnicode, + strict_quotes = false +} + +default = nil -- Let the buildCapture optimization take place + +strict = { + badChars = '\b\f\n\r\t\v', + additionalEscapes = false, -- no additional escapes + escapeCheck = #lpeg.S('bfnrtv/\\"u'), --only these chars are allowed to be escaped + strict_quotes = true +} + +local function buildCaptureString(quote, badChars, escapeMatch) + local captureChar = (1 - lpeg.S("\\" .. badChars .. quote)) + (lpeg.P("\\") / "" * escapeMatch) + captureChar = captureChar + (-#lpeg.P(quote) * lpeg.P(bad_character)) + local captureString = captureChar^0 + return lpeg.P(quote) * lpeg.Cs(captureString) * lpeg.P(quote) +end + +local function buildCapture(options) + options = options and merge({}, defaultOptions, options) or defaultOptions + local quotes = { '"' } + if not options.strict_quotes then + quotes[#quotes + 1] = "'" + end + local escapeMatch = doSimpleSub + escapeMatch = escapeMatch + doXSub / decodeX + escapeMatch = escapeMatch + doUniSub / options.decodeUnicode + if options.additionalEscapes then + escapeMatch = escapeMatch + options.additionalEscapes + end + if options.escapeCheck then + escapeMatch = options.escapeCheck * escapeMatch + lpeg.P(bad_escape) + end + local captureString + for i = 1, #quotes do + local cap = buildCaptureString(quotes[i], options.badChars, escapeMatch) + if captureString == nil then + captureString = cap + else + captureString = captureString + cap + end + end + return captureString +end + +function register_types() + util.register_type("STRING") +end + +function load_types(options, global_options, grammar) + local capture = buildCapture(options) + local string_id = util.types.STRING + grammar[string_id] = capture + util.append_grammar_item(grammar, "VALUE", lpeg.V(string_id)) +end diff --git a/lua/json/decode/util.lua b/lua/json/decode/util.lua new file mode 100644 index 00000000..6876eb16 --- /dev/null +++ b/lua/json/decode/util.lua @@ -0,0 +1,92 @@ +--[[ + Licensed according to the included 'LICENSE' document + Author: Thomas Harning Jr +]] +local lpeg = require("lpeg") +local select = select +local pairs, ipairs = pairs, ipairs +local tonumber = tonumber +local string_char = string.char + +local error = error +local setmetatable = setmetatable + +module("json.decode.util") + +-- 09, 0A, 0B, 0C, 0D, 20 +ascii_space = lpeg.S("\t\n\v\f\r ") +do + local chr = string_char + local u_space = ascii_space + -- \u0085 \u00A0 + u_space = u_space + lpeg.P(chr(0xC2)) * lpeg.S(chr(0x85) .. chr(0xA0)) + -- \u1680 \u180E + u_space = u_space + lpeg.P(chr(0xE1)) * (lpeg.P(chr(0x9A, 0x80)) + chr(0xA0, 0x8E)) + -- \u2000 - \u200A, also 200B + local spacing_end = "" + for i = 0x80,0x8b do + spacing_end = spacing_end .. chr(i) + end + -- \u2028 \u2029 \u202F + spacing_end = spacing_end .. chr(0xA8) .. chr(0xA9) .. chr(0xAF) + u_space = u_space + lpeg.P(chr(0xE2, 0x80)) * lpeg.S(spacing_end) + -- \u205F + u_space = u_space + lpeg.P(chr(0xE2, 0x81, 0x9F)) + -- \u3000 + u_space = u_space + lpeg.P(chr(0xE3, 0x80, 0x80)) + -- BOM \uFEFF + u_space = u_space + lpeg.P(chr(0xEF, 0xBB, 0xBF)) + _M.unicode_space = u_space +end + +identifier = lpeg.R("AZ","az","__") * lpeg.R("AZ","az", "__", "09") ^0 + +hex = lpeg.R("09","AF","af") +hexpair = hex * hex + +comments = { + cpp = lpeg.P("//") * (1 - lpeg.P("\n"))^0 * lpeg.P("\n"), + c = lpeg.P("/*") * (1 - lpeg.P("*/"))^0 * lpeg.P("*/") +} + +comment = comments.cpp + comments.c + +ascii_ignored = (ascii_space + comment)^0 + +unicode_ignored = (unicode_space + comment)^0 + +local types = setmetatable({false}, { + __index = function(self, k) + error("Unknown type: " .. k) + end +}) + +function register_type(name) + types[#types + 1] = name + types[name] = #types + return #types +end + +_M.types = types + +function append_grammar_item(grammar, name, capture) + local id = types[name] + local original = grammar[id] + if original then + grammar[id] = original + capture + else + grammar[id] = capture + end +end + +-- Parse the lpeg version skipping patch-values +-- LPEG <= 0.7 have no version value... so 0.7 is value +DecimalLpegVersion = lpeg.version and tonumber(lpeg.version():match("^(%d+%.%d+)")) or 0.7 + +function get_invalid_character_info(input, index) + local parsed = input:sub(1, index) + local bad_character = input:sub(index, index) + local _, line_number = parsed:gsub('\n',{}) + local last_line = parsed:match("\n([^\n]+.)$") or parsed + return line_number, #last_line, bad_character, last_line +end diff --git a/lua/json/encode.lua b/lua/json/encode.lua new file mode 100644 index 00000000..961872d5 --- /dev/null +++ b/lua/json/encode.lua @@ -0,0 +1,159 @@ +--[[ + Licensed according to the included 'LICENSE' document + Author: Thomas Harning Jr +]] +local type = type +local assert, error = assert, error +local getmetatable, setmetatable = getmetatable, setmetatable +local util = require("json.util") + +local ipairs, pairs = ipairs, pairs +local require = require + +local output = require("json.encode.output") + +local util = require("json.util") +local util_merge, isCall = util.merge, util.isCall + +module("json.encode") + +--[[ + List of encoding modules to load. + Loaded in sequence such that earlier encoders get priority when + duplicate type-handlers exist. +]] +local modulesToLoad = { + "strings", + "number", + "calls", + "others", + "array", + "object" +} +-- Modules that have been loaded +local loadedModules = {} + +-- Default configuration options to apply +local defaultOptions = {} +-- Configuration bases for client apps +default = nil +strict = { + initialObject = true -- Require an object at the root +} + +-- For each module, load it and its defaults +for _,name in ipairs(modulesToLoad) do + local mod = require("json.encode." .. name) + defaultOptions[name] = mod.default + strict[name] = mod.strict + loadedModules[name] = mod +end + +-- Merges values, assumes all tables are arrays, inner values flattened, optionally constructing output +local function flattenOutput(out, values) + out = not out and {} or type(out) == 'table' and out or {out} + if type(values) == 'table' then + for _, v in ipairs(values) do + out[#out + 1] = v + end + else + out[#out + 1] = values + end + return out +end + +-- Prepares the encoding map from the already provided modules and new config +local function prepareEncodeMap(options) + local map = {} + for _, name in ipairs(modulesToLoad) do + local encodermap = loadedModules[name].getEncoder(options[name]) + for valueType, encoderSet in pairs(encodermap) do + map[valueType] = flattenOutput(map[valueType], encoderSet) + end + end + return map +end + +--[[ + Encode a value with a given encoding map and state +]] +local function encodeWithMap(value, map, state) + local t = type(value) + local encoderList = assert(map[t], "Failed to encode value, unhandled type: " .. t) + for _, encoder in ipairs(encoderList) do + local ret = encoder(value, state) + if false ~= ret then + return ret + end + end + error("Failed to encode value, encoders for " .. t .. " deny encoding") +end + + +local function getBaseEncoder(options) + local encoderMap = prepareEncodeMap(options) + if options.preProcess then + local preProcess = options.preProcess + return function(value, state) + local ret = preProcess(value) + if nil ~= ret then + value = ret + end + return encodeWithMap(value, encoderMap, state) + end + end + return function(value, state) + return encodeWithMap(value, encoderMap, state) + end +end +--[[ + Retreive an initial encoder instance based on provided options + the initial encoder is responsible for initializing state + State has at least these values configured: encode, check_unique, already_encoded +]] +function getEncoder(options) + options = options and util_merge({}, defaultOptions, options) or defaultOptions + local encode = getBaseEncoder(options) + + local function initialEncode(value) + if options.initialObject then + local errorMessage = "Invalid arguments: expects a JSON Object or Array at the root" + assert(type(value) == 'table' and not isCall(value, options), errorMessage) + end + + local alreadyEncoded = {} + local function check_unique(value) + assert(not alreadyEncoded[value], "Recursive encoding of value") + alreadyEncoded[value] = true + end + + local outputEncoder = options.output and options.output() or output.getDefault() + local state = { + encode = encode, + check_unique = check_unique, + already_encoded = alreadyEncoded, -- To unmark encoding when moving up stack + outputEncoder = outputEncoder + } + local ret = encode(value, state) + if nil ~= ret then + return outputEncoder.simple and outputEncoder.simple(ret) or ret + end + end + return initialEncode +end + +-- CONSTRUCT STATE WITH FOLLOWING (at least) +--[[ + encoder + check_unique -- used by inner encoders to make sure value is unique + already_encoded -- used to unmark a value as unique +]] +function encode(data, options) + return getEncoder(options)(data) +end + +local mt = getmetatable(_M) or {} +mt.__call = function(self, ...) + return encode(...) +end +setmetatable(_M, mt) diff --git a/lua/json/encode/array.lua b/lua/json/encode/array.lua new file mode 100644 index 00000000..2cd12678 --- /dev/null +++ b/lua/json/encode/array.lua @@ -0,0 +1,97 @@ +--[[ + Licensed according to the included 'LICENSE' document + Author: Thomas Harning Jr +]] +local jsonutil = require("json.util") + +local type = type +local pairs = pairs +local assert = assert + +local table_concat = table.concat +local math_floor, math_modf = math.floor, math.modf + +local util_merge = require("json.util").merge +local util_IsArray = require("json.util").IsArray + +module("json.encode.array") + +local defaultOptions = { + isArray = util_IsArray +} + +default = nil +strict = nil + +--[[ + Utility function to determine whether a table is an array or not. + Criteria for it being an array: + * ExternalIsArray returns true (or false directly reports not-array) + * If the table has an 'n' value that is an integer >= 1 then it + is an array... may result in false positives (should check some values + before it) + * It is a contiguous list of values with zero string-based keys +]] +function isArray(val, options) + local externalIsArray = options and options.isArray + + if externalIsArray then + local ret = externalIsArray(val) + if ret == true or ret == false then + return ret + end + end + -- Use the 'n' element if it's a number + if type(val.n) == 'number' and math_floor(val.n) == val.n and val.n >= 1 then + return true + end + local len = #val + for k,v in pairs(val) do + if type(k) ~= 'number' then + return false + end + local _, decim = math_modf(k) + if not (decim == 0 and 1<=k) then + return false + end + if k > len then -- Use Lua's length as absolute determiner + return false + end + end + + return true +end + +--[[ + Cleanup function to unmark a value as in the encoding process and return + trailing results +]] +local function unmarkAfterEncode(tab, state, ...) + state.already_encoded[tab] = nil + return ... +end +function getEncoder(options) + options = options and util_merge({}, defaultOptions, options) or defaultOptions + local function encodeArray(tab, state) + if not isArray(tab, options) then + return false + end + -- Make sure this value hasn't been encoded yet + state.check_unique(tab) + local encode = state.encode + local compositeEncoder = state.outputEncoder.composite + local valueEncoder = [[ + for i = 1, (composite.n or #composite) do + local val = composite[i] + PUTINNER(i ~= 1) + val = encode(val, state) + val = val or '' + if val then + PUTVALUE(val) + end + end + ]] + return unmarkAfterEncode(tab, state, compositeEncoder(valueEncoder, '[', ']', ',', tab, encode, state)) + end + return { table = encodeArray } +end diff --git a/lua/json/encode/calls.lua b/lua/json/encode/calls.lua new file mode 100644 index 00000000..a75a1245 --- /dev/null +++ b/lua/json/encode/calls.lua @@ -0,0 +1,60 @@ +--[[ + Licensed according to the included 'LICENSE' document + Author: Thomas Harning Jr +]] +local jsonutil = require("json.util") + +local table_concat = table.concat + +local select = select +local getmetatable, setmetatable = getmetatable, setmetatable +local assert = assert + +local util = require("json.util") + +local util_merge, isCall, decodeCall = util.merge, util.isCall, util.decodeCall + +module("json.encode.calls") + + +local defaultOptions = { +} + +-- No real default-option handling needed... +default = nil +strict = nil + + +--[[ + Encodes 'value' as a function call + Must have parameters in the 'callData' field of the metatable + name == name of the function call + parameters == array of parameters to encode +]] +function getEncoder(options) + options = options and util_merge({}, defaultOptions, options) or defaultOptions + local function encodeCall(value, state) + if not isCall(value) then + return false + end + local encode = state.encode + local name, params = decodeCall(value) + local compositeEncoder = state.outputEncoder.composite + local valueEncoder = [[ + for i = 1, (composite.n or #composite) do + local val = composite[i] + PUTINNER(i ~= 1) + val = encode(val, state) + val = val or '' + if val then + PUTVALUE(val) + end + end + ]] + return compositeEncoder(valueEncoder, name .. '(', ')', ',', params, encode, state) + end + return { + table = encodeCall, + ['function'] = encodeCall + } +end diff --git a/lua/json/encode/number.lua b/lua/json/encode/number.lua new file mode 100644 index 00000000..ff1d942b --- /dev/null +++ b/lua/json/encode/number.lua @@ -0,0 +1,46 @@ +--[[ + Licensed according to the included 'LICENSE' document + Author: Thomas Harning Jr +]] +local tostring = tostring +local assert = assert +local util = require("json.util") + +module("json.encode.number") + +local defaultOptions = { + nan = true, + inf = true +} + +default = nil -- Let the buildCapture optimization take place +strict = { + nan = false, + inf = false +} + +local function encodeNumber(number, options) + local str = tostring(number) + if str == "nan" then + assert(options.nan, "Invalid number: NaN not enabled") + return "NaN" + end + if str == "inf" then + assert(options.inf, "Invalid number: Infinity not enabled") + return "Infinity" + end + if str == "-inf" then + assert(options.inf, "Invalid number: Infinity not enabled") + return "-Infinity" + end + return str +end + +function getEncoder(options) + options = options and util.merge({}, defaultOptions, options) or defaultOptions + return { + number = function(number, state) + return encodeNumber(number, options) + end + } +end diff --git a/lua/json/encode/object.lua b/lua/json/encode/object.lua new file mode 100644 index 00000000..3263e19d --- /dev/null +++ b/lua/json/encode/object.lua @@ -0,0 +1,67 @@ +--[[ + Licensed according to the included 'LICENSE' document + Author: Thomas Harning Jr +]] +local pairs = pairs +local assert = assert + +local type = type +local tostring = tostring + +local table_concat = table.concat +local util_merge = require("json.util").merge + +module("json.encode.object") + +local defaultOptions = { +} + +default = nil +strict = nil + +--[[ + Cleanup function to unmark a value as in the encoding process and return + trailing results +]] +local function unmarkAfterEncode(tab, state, ...) + state.already_encoded[tab] = nil + return ... +end +--[[ + Encode a table as a JSON Object ( keys = strings, values = anything else ) +]] +local function encodeTable(tab, options, state) + -- Make sure this value hasn't been encoded yet + state.check_unique(tab) + local encode = state.encode + local compositeEncoder = state.outputEncoder.composite + local valueEncoder = [[ + local first = true + for k, v in pairs(composite) do + local ti = type(k) + assert(ti == 'string' or ti == 'number' or ti == 'boolean', "Invalid object index type: " .. ti) + local name = encode(tostring(k), state) + if first then + first = false + else + name = ',' .. name + end + PUTVALUE(name .. ':') + local val = encode(v, state) + val = val or '' + if val then + PUTVALUE(val) + end + end + ]] + return unmarkAfterEncode(tab, state, compositeEncoder(valueEncoder, '{', '}', nil, tab, encode, state)) +end + +function getEncoder(options) + options = options and util_merge({}, defaultOptions, options) or defaultOptions + return { + table = function(tab, state) + return encodeTable(tab, options, state) + end + } +end diff --git a/lua/json/encode/others.lua b/lua/json/encode/others.lua new file mode 100644 index 00000000..ea2c88ff --- /dev/null +++ b/lua/json/encode/others.lua @@ -0,0 +1,55 @@ +--[[ + Licensed according to the included 'LICENSE' document + Author: Thomas Harning Jr +]] +local tostring = tostring + +local assert = assert +local jsonutil = require("json.util") +local util_merge = require("json.util").merge +local type = type + +module("json.encode.others") + +-- Shortcut that works +encodeBoolean = tostring + +local defaultOptions = { + allowUndefined = true, + null = jsonutil.null, + undefined = jsonutil.undefined +} + +default = nil -- Let the buildCapture optimization take place +strict = { + allowUndefined = false +} + +function getEncoder(options) + options = options and util_merge({}, defaultOptions, options) or defaultOptions + local function encodeOthers(value, state) + if value == options.null then + return 'null' + elseif value == options.undefined then + assert(options.allowUndefined, "Invalid value: Unsupported 'Undefined' parameter") + return 'undefined' + else + return false + end + end + local function encodeBoolean(value, state) + return value and 'true' or 'false' + end + local nullType = type(options.null) + local undefinedType = options.undefined and type(options.undefined) + -- Make sure that all of the types handled here are handled + local ret = { + boolean = encodeBoolean, + ['nil'] = function() return 'null' end, + [nullType] = encodeOthers + } + if undefinedType then + ret[undefinedType] = encodeOthers + end + return ret +end diff --git a/lua/json/encode/output.lua b/lua/json/encode/output.lua new file mode 100644 index 00000000..54a8255f --- /dev/null +++ b/lua/json/encode/output.lua @@ -0,0 +1,84 @@ +--[[ + Licensed according to the included 'LICENSE' document + Author: Thomas Harning Jr +]] +local type = type +local assert, error = assert, error +local table_concat = table.concat +local loadstring = loadstring + +local io = io + +local setmetatable = setmetatable + +local output_utility = require("json.encode.output_utility") + +module("json.encode.output") + +local tableCompositeCache = setmetatable({}, {__mode = 'v'}) + +local TABLE_VALUE_WRITER = [[ + ret[#ret + 1] = %VALUE% +]] + +local TABLE_INNER_WRITER = "" + +--[[ + nextValues can output a max of two values to throw into the data stream + expected to be called until nil is first return value + value separator should either be attached to v1 or in innerValue +]] +local function defaultTableCompositeWriter(nextValues, beginValue, closeValue, innerValue, composite, encode, state) + if type(nextValues) == 'string' then + local fun = output_utility.prepareEncoder(defaultTableCompositeWriter, nextValues, innerValue, TABLE_VALUE_WRITER, TABLE_INNER_WRITER) + local ret = {} + fun(composite, ret, encode, state) + return beginValue .. table_concat(ret, innerValue) .. closeValue + end +end + +-- no 'simple' as default action is just to return the value +function getDefault() + return { composite = defaultTableCompositeWriter } +end + +-- BEGIN IO-WRITER OUTPUT +local IO_INNER_WRITER = [[ + if %WRITE_INNER% then + state.__outputFile:write(%INNER_VALUE%) + end +]] +local IO_VALUE_WRITER = [[ + state.__outputFile:write(%VALUE%) +]] + +local function buildIoWriter(output) + if not output then -- Default to stdout + output = io.output() + end + local function ioWriter(nextValues, beginValue, closeValue, innerValue, composite, encode, state) + -- HOOK OUTPUT STATE + state.__outputFile = output + if type(nextValues) == 'string' then + local fun = output_utility.prepareEncoder(ioWriter, nextValues, innerValue, IO_VALUE_WRITER, IO_INNER_WRITER) + local ret = {} + output:write(beginValue) + fun(composite, ret, encode, state) + output:write(closeValue) + return nil + end + end + + local function ioSimpleWriter(encoded) + if encoded then + output:write(encoded) + end + return nil + end + return { composite = ioWriter, simple = ioSimpleWriter } +end +function getIoWriter(output) + return function() + return buildIoWriter(output) + end +end diff --git a/lua/json/encode/output_utility.lua b/lua/json/encode/output_utility.lua new file mode 100644 index 00000000..b8d35739 --- /dev/null +++ b/lua/json/encode/output_utility.lua @@ -0,0 +1,48 @@ +--[[ + Licensed according to the included 'LICENSE' document + Author: Thomas Harning Jr +]] +local setmetatable = setmetatable +local assert, loadstring = assert, loadstring + +module("json.encode.output_utility") + +-- Key == weak, if main key goes away, then cache cleared +local outputCache = setmetatable({}, {__mode = 'k'}) +-- TODO: inner tables weak? + +local function buildFunction(nextValues, innerValue, valueWriter, innerWriter) + local putInner = "" + if innerValue and innerWriter then + -- Prepare the lua-string representation of the separator to put in between values + local formattedInnerValue = ("%q"):format(innerValue) + -- Fill in the condition %WRITE_INNER% and the %INNER_VALUE% to actually write + putInner = innerWriter:gsub("%%WRITE_INNER%%", "%%1"):gsub("%%INNER_VALUE%%", formattedInnerValue) + end + -- Template-in the value writer (if present) and its conditional argument + local functionCode = nextValues:gsub("PUTINNER(%b())", putInner) + -- %VALUE% is to be filled in by the value-to-write + valueWriter = valueWriter:gsub("%%VALUE%%", "%%1") + -- Template-in the value writer with its argument + functionCode = functionCode:gsub("PUTVALUE(%b())", valueWriter) + functionCode = [[ + return function(composite, ret, encode, state) + ]] .. functionCode .. [[ + end + ]] + return assert(loadstring(functionCode))() +end + +function prepareEncoder(cacheKey, nextValues, innerValue, valueWriter, innerWriter) + local cache = outputCache[cacheKey] + if not cache then + cache = {} + outputCache[cacheKey] = cache + end + local fun = cache[nextValues] + if not fun then + fun = buildFunction(nextValues, innerValue, valueWriter, innerWriter) + cache[nextValues] = fun + end + return fun +end diff --git a/lua/json/encode/strings.lua b/lua/json/encode/strings.lua new file mode 100644 index 00000000..1908405f --- /dev/null +++ b/lua/json/encode/strings.lua @@ -0,0 +1,67 @@ +--[[ + Licensed according to the included 'LICENSE' document + Author: Thomas Harning Jr +]] +local string_char = string.char +local pairs = pairs + +local util_merge = require("json.util").merge +module("json.encode.strings") + +local normalEncodingMap = { + ['"'] = '\\"', + ['\\'] = '\\\\', + ['/'] = '\\/', + ['\b'] = '\\b', + ['\f'] = '\\f', + ['\n'] = '\\n', + ['\r'] = '\\r', + ['\t'] = '\\t', + ['\v'] = '\\v' -- not in official spec, on report, removing +} + +local xEncodingMap = {} +for char, encoded in pairs(normalEncodingMap) do + xEncodingMap[char] = encoded +end + +-- Pre-encode the control characters to speed up encoding... +-- NOTE: UTF-8 may not work out right w/ JavaScript +-- JavaScript uses 2 bytes after a \u... yet UTF-8 is a +-- byte-stream encoding, not pairs of bytes (it does encode +-- some letters > 1 byte, but base case is 1) +for i = 0, 255 do + local c = string_char(i) + if c:match('[%z\1-\031\128-\255]') and not normalEncodingMap[c] then + -- WARN: UTF8 specializes values >= 0x80 as parts of sequences... + -- without \x encoding, do not allow encoding > 7F + normalEncodingMap[c] = ('\\u%.4X'):format(i) + xEncodingMap[c] = ('\\x%.2X'):format(i) + end +end + +local defaultOptions = { + xEncode = false, -- Encode single-bytes as \xXX + -- / is not required to be quoted but it helps with certain decoding + -- Required encoded characters, " \, and 00-1F (0 - 31) + encodeSet = '\\"/%z\1-\031', + encodeSetAppend = nil -- Chars to append to the default set +} + +default = nil +strict = nil + +function getEncoder(options) + options = options and util_merge({}, defaultOptions, options) or defaultOptions + local encodeSet = options.encodeSet + if options.encodeSetAppend then + encodeSet = encodeSet .. options.encodeSetAppend + end + local encodingMap = options.xEncode and xEncodingMap or normalEncodingMap + local function encodeString(s, state) + return '"' .. s:gsub('[' .. encodeSet .. ']', encodingMap) .. '"' + end + return { + string = encodeString + } +end diff --git a/lua/json/util.lua b/lua/json/util.lua new file mode 100644 index 00000000..28e47ea6 --- /dev/null +++ b/lua/json/util.lua @@ -0,0 +1,108 @@ +--[[ + Licensed according to the included 'LICENSE' document + Author: Thomas Harning Jr +]] +local type = type +local print = print +local tostring = tostring +local pairs = pairs +local getmetatable, setmetatable = getmetatable, setmetatable +local select = select + +module("json.util") +local function foreach(tab, func) + for k, v in pairs(tab) do + func(k,v) + end +end +function printValue(tab, name) + local parsed = {} + local function doPrint(key, value, space) + space = space or '' + if type(value) == 'table' then + if parsed[value] then + print(space .. key .. '= <' .. parsed[value] .. '>') + else + parsed[value] = key + print(space .. key .. '= {') + space = space .. ' ' + foreach(value, function(key, value) doPrint(key, value, space) end) + end + else + if type(value) == 'string' then + value = '[[' .. tostring(value) .. ']]' + end + print(space .. key .. '=' .. tostring(value)) + end + end + doPrint(name, tab) +end + +function clone(t) + local ret = {} + for k,v in pairs(t) do + ret[k] = v + end + return ret +end + +local function merge(t, from, ...) + if not from then + return t + end + for k,v in pairs(from) do + t[k] = v + end + return merge(t, ...) +end +_M.merge = merge + +-- Function to insert nulls into the JSON stream +function null() + return null +end + +-- Marker for 'undefined' values +function undefined() + return undefined +end + +local ArrayMT = {} + +--[[ + Return's true if the metatable marks it as an array.. + Or false if it has no array component at all + Otherwise nil to get the normal detection component working +]] +function IsArray(value) + if type(value) ~= 'table' then return false end + local ret = getmetatable(value) == ArrayMT + if not ret then + if #value == 0 then return false end + else + return ret + end +end +function InitArray(array) + setmetatable(array, ArrayMT) + return array +end + +local CallMT = {} + +function isCall(value) + return CallMT == getmetatable(value) +end + +function buildCall(name, ...) + local callData = { + name = name, + parameters = {n = select('#', ...), ...} + } + return setmetatable(callData, CallMT) +end + +function decodeCall(callData) + if not isCall(callData) then return nil end + return callData.name, callData.parameters +end