diff --git a/lua/json.lua b/lua/json.lua index 3dd7a784..cc44cd04 100644 --- a/lua/json.lua +++ b/lua/json.lua @@ -1,12 +1,24 @@ --[[ 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 +local _G = _G + +local _ENV = nil + +local json = { + _VERSION = "1.3.4", + _DESCRIPTION = "LuaJSON : customizable JSON decoder/encoder", + _COPYRIGHT = "Copyright (c) 2007-2014 Thomas Harning Jr. ", + decode = decode, + encode = encode, + util = util +} + +_G.json = json + +return json diff --git a/lua/json/decode.lua b/lua/json/decode.lua index 23f032d5..b2c357cf 100644 --- a/lua/json/decode.lua +++ b/lua/json/decode.lua @@ -1,104 +1,137 @@ --[[ Licensed according to the included 'LICENSE' document Author: Thomas Harning Jr ---]] +]] local lpeg = require("lpeg") local error = error +local pcall = pcall -local object = require("json.decode.object") -local array = require("json.decode.array") - -local merge = require("json.util").merge +local jsonutil = require("json.util") +local merge = jsonutil.merge local util = require("json.decode.util") +local decode_state = require("json.decode.state") + local setmetatable, getmetatable = setmetatable, getmetatable local assert = assert local ipairs, pairs = ipairs, pairs local string_char = require("string").char +local type = type + local require = require -module("json.decode") + +local _ENV = nil local modulesToLoad = { - "array", - "object", + "composite", "strings", "number", - "calls", "others" } local loadedModules = { } -default = { +local json_decode = {} + +json_decode.default = { unicodeWhitespace = true, - initialObject = false + initialObject = false, + nothrow = false } local modes_defined = { "default", "strict", "simple" } -simple = {} +json_decode.simple = {} -strict = { +json_decode.strict = { unicodeWhitespace = true, - initialObject = true + initialObject = true, + nothrow = false } --- 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] + if mod.mergeOptions then + for _, mode in pairs(modes_defined) do + mod.mergeOptions(json_decode[mode], mode) end end - loadedModules[name] = mod - -- Register types - if mod.register_types then - mod.register_types() - end + loadedModules[#loadedModules + 1] = mod end -- Shift over default into defaultOptions to permit build optimization -local defaultOptions = default -default = nil - +local defaultOptions = json_decode.default +json_decode.default = nil + +local function generateDecoder(lexer, options) + -- Marker to permit detection of final end + local marker = {} + local parser = lpeg.Ct((options.ignored * lexer)^0 * lpeg.Cc(marker)) * options.ignored * (lpeg.P(-1) + util.unexpected()) + local decoder = function(data) + local state = decode_state.create(options) + local parsed = parser:match(data) + assert(parsed, "Invalid JSON data") + local i = 0 + while true do + i = i + 1 + local item = parsed[i] + if item == marker then break end + if type(item) == 'function' and item ~= jsonutil.undefined and item ~= jsonutil.null then + item(state) + else + state:set_value(item) + end + end + if options.initialObject then + assert(type(state.previous) == 'table', "Initial value not an object or array") + end + -- Make sure stack is empty + assert(state.i == 0, "Unclosed elements present") + return state.previous + end + if options.nothrow then + return function(data) + local status, rv = pcall(decoder, data) + if status then + return rv + else + return nil, rv + end + end + end + return decoder +end local function buildDecoder(mode) mode = mode and merge({}, defaultOptions, mode) or defaultOptions + for _, mod in ipairs(loadedModules) do + if mod.mergeOptions then + mod.mergeOptions(mode) + end + end 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 + --local grammar = { + -- [1] = mode.initialObject and (ignored * (object_type + array_type)) or value_type + --} + local lexer + for _, mod in ipairs(loadedModules) do + local new_lexer = mod.generateLexer(mode) + lexer = lexer and lexer + new_lexer or new_lexer end + return generateDecoder(lexer, mode) end -- Since 'default' is nil, we cannot take map it -local defaultDecoder = buildDecoder(default) +local defaultDecoder = buildDecoder(json_decode.default) local prebuilt_decoders = {} for _, mode in pairs(modes_defined) do - if _M[mode] ~= nil then - prebuilt_decoders[_M[mode]] = buildDecoder(_M[mode]) + if json_decode[mode] ~= nil then + prebuilt_decoders[json_decode[mode]] = buildDecoder(json_decode[mode]) end end @@ -110,9 +143,9 @@ 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 function getDecoder(mode) + mode = mode == true and json_decode.strict or mode or json_decode.default local decoder = mode == nil and defaultDecoder or prebuilt_decoders[mode] if decoder then return decoder @@ -120,13 +153,19 @@ function getDecoder(mode) return buildDecoder(mode) end -function decode(data, mode) +local function decode(data, mode) local decoder = getDecoder(mode) return decoder(data) end -local mt = getmetatable(_M) or {} +local mt = {} mt.__call = function(self, ...) return decode(...) end -setmetatable(_M, mt) + +json_decode.getDecoder = getDecoder +json_decode.decode = decode +json_decode.util = util +setmetatable(json_decode, mt) + +return json_decode diff --git a/lua/json/decode/composite.lua b/lua/json/decode/composite.lua new file mode 100644 index 00000000..cd9c2896 --- /dev/null +++ b/lua/json/decode/composite.lua @@ -0,0 +1,190 @@ +--[[ + Licensed according to the included 'LICENSE' document + Author: Thomas Harning Jr +]] +local pairs = pairs +local type = type + +local lpeg = require("lpeg") + +local util = require("json.decode.util") +local jsonutil = require("json.util") + +local rawset = rawset + +local assert = assert +local tostring = tostring + +local error = error +local getmetatable = getmetatable + +local _ENV = nil + +local defaultOptions = { + array = { + trailingComma = true + }, + object = { + trailingComma = true, + number = true, + identifier = true, + setObjectKey = rawset + }, + calls = { + defs = nil, + -- By default, do not allow undefined calls to be de-serialized as call objects + allowUndefined = false + } +} + +local modeOptions = { + default = nil, + strict = { + array = { + trailingComma = false + }, + object = { + trailingComma = false, + number = false, + identifier = false + } + } +} + +local function BEGIN_ARRAY(state) + state:push() + state:new_array() +end +local function END_ARRAY(state) + state:end_array() + state:pop() +end + +local function BEGIN_OBJECT(state) + state:push() + state:new_object() +end +local function END_OBJECT(state) + state:end_object() + state:pop() +end + +local function END_CALL(state) + state:end_call() + state:pop() +end + +local function SET_KEY(state) + state:set_key() +end + +local function NEXT_VALUE(state) + state:put_value() +end + +local function mergeOptions(options, mode) + jsonutil.doOptionMerge(options, true, 'array', defaultOptions, mode and modeOptions[mode]) + jsonutil.doOptionMerge(options, true, 'object', defaultOptions, mode and modeOptions[mode]) + jsonutil.doOptionMerge(options, true, 'calls', defaultOptions, mode and modeOptions[mode]) +end + + +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 generateSingleCallLexer(name, func) + 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 function buildCallCapture(name) + return function(state) + if func == false then + error("Function call on '" .. name .. "' not permitted") + end + state:push() + state:new_call(name, func) + end + end + local nameCallCapture + if type(name) == 'string' then + nameCallCapture = lpeg.P(name .. "(") * lpeg.Cc(name) / buildCallCapture + else + -- Name matcher expected to produce a capture + nameCallCapture = name * "(" / buildCallCapture + end + -- Call func over nameCallCapture and value to permit function receiving name + return nameCallCapture +end + +local function generateNamedCallLexers(options) + if not options.calls or not options.calls.defs then + return + end + local callCapture + for name, func in pairs(options.calls.defs) do + local newCapture = generateSingleCallLexer(name, func) + if not callCapture then + callCapture = newCapture + else + callCapture = callCapture + newCapture + end + end + return callCapture +end + +local function generateCallLexer(options) + local lexer + local namedCapture = generateNamedCallLexers(options) + if options.calls and options.calls.allowUndefined then + lexer = generateSingleCallLexer(lpeg.C(util.identifier), true) + end + if namedCapture then + lexer = lexer and lexer + namedCapture or namedCapture + end + if lexer then + lexer = lexer + lpeg.P(")") * lpeg.Cc(END_CALL) + end + return lexer +end + +local function generateLexer(options) + local ignored = options.ignored + local array_options, object_options = options.array, options.object + local lexer = + lpeg.P("[") * lpeg.Cc(BEGIN_ARRAY) + + lpeg.P("]") * lpeg.Cc(END_ARRAY) + + lpeg.P("{") * lpeg.Cc(BEGIN_OBJECT) + + lpeg.P("}") * lpeg.Cc(END_OBJECT) + + lpeg.P(":") * lpeg.Cc(SET_KEY) + + lpeg.P(",") * lpeg.Cc(NEXT_VALUE) + if object_options.identifier then + -- Add identifier match w/ validation check that it is in key + lexer = lexer + lpeg.C(util.identifier) * ignored * lpeg.P(":") * lpeg.Cc(SET_KEY) + end + local callLexers = generateCallLexer(options) + if callLexers then + lexer = lexer + callLexers + end + return lexer +end + +local composite = { + mergeOptions = mergeOptions, + generateLexer = generateLexer +} + +return composite diff --git a/lua/json/decode/number.lua b/lua/json/decode/number.lua index f9a2157b..94ed3b88 100644 --- a/lua/json/decode/number.lua +++ b/lua/json/decode/number.lua @@ -1,19 +1,22 @@ --[[ 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 jsonutil = require("json.util") +local merge = jsonutil.merge local util = require("json.decode.util") -module("json.decode.number") +local _ENV = nil local digit = lpeg.R("09") local digits = digit^1 -int = (lpeg.P('-') + 0) * (lpeg.R("19") * digits + digit) -local int = int +-- Illegal octal declaration +local illegal_octal_detect = #(lpeg.P('0') * digits) * util.denied("Octal numbers") + +local int = (lpeg.P('-') + 0) * (lpeg.R("19") * digits + illegal_octal_detect + digit) local frac = lpeg.P('.') * digits @@ -32,8 +35,9 @@ local defaultOptions = { hex = false } -default = nil -- Let the buildCapture optimization take place -strict = { +local modeOptions = {} + +modeOptions.strict = { nan = false, inf = false } @@ -49,37 +53,48 @@ local ninf_value = -1/0 frac: match fraction portion (.0) exp: match exponent portion (e1) DEFAULT: nan, inf, frac, exp ---]] -local function buildCapture(options) - options = options and merge({}, defaultOptions, options) or defaultOptions +]] +local function mergeOptions(options, mode) + jsonutil.doOptionMerge(options, false, 'number', defaultOptions, mode and modeOptions[mode]) +end + +local function generateLexer(options) + options = options.number local ret = int if options.frac then ret = ret * (frac + 0) + else + ret = ret * (#frac * util.denied("Fractions", "number.frac") + 0) end if options.exp then ret = ret * (exp + 0) + else + ret = ret * (#exp * util.denied("Exponents", "number.exp") + 0) end if options.hex then ret = hex + ret + else + ret = #hex * util.denied("Hexadecimal", "number.hex") + ret end -- Capture number now ret = ret / tonumber if options.nan then ret = ret + nan / function() return nan_value end + else + ret = ret + #nan * util.denied("NaN", "number.nan") end if options.inf then ret = ret + ninf / function() return ninf_value end + inf / function() return inf_value end + else + ret = ret + (#ninf + #inf) * util.denied("+/-Inf", "number.inf") end return ret end -function register_types() - util.register_type("INTEGER") -end +local number = { + int = int, + mergeOptions = mergeOptions, + generateLexer = generateLexer +} -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 +return number diff --git a/lua/json/decode/others.lua b/lua/json/decode/others.lua index 804b6663..9fab7a83 100644 --- a/lua/json/decode/others.lua +++ b/lua/json/decode/others.lua @@ -1,15 +1,15 @@ --[[ Licensed according to the included 'LICENSE' document Author: Thomas Harning Jr ---]] +]] local lpeg = require("lpeg") local jsonutil = require("json.util") +local merge = jsonutil.merge local util = require("json.decode.util") -local rawset = rawset - -- Container module for other JavaScript types (bool, null, undefined) -module("json.decode.others") + +local _ENV = nil -- For null and undefined, use the util.null value to preserve null-ness local booleanCapture = @@ -22,33 +22,41 @@ local undefinedCapture = lpeg.P("undefined") local defaultOptions = { allowUndefined = true, null = jsonutil.null, - undefined = jsonutil.undefined, - setObjectKey = rawset + undefined = jsonutil.undefined } -default = nil -- Let the buildCapture optimization take place -simple = { +local modeOptions = {} + +modeOptions.simple = { null = false, -- Mapped to nil undefined = false -- Mapped to nil } -strict = { +modeOptions.strict = { allowUndefined = false } -local function buildCapture(options) +local function mergeOptions(options, mode) + jsonutil.doOptionMerge(options, false, 'others', defaultOptions, mode and modeOptions[mode]) +end + +local function generateLexer(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 + options = options.others local valueCapture = ( booleanCapture + nullCapture * lpeg.Cc(options.null or nil) ) if options.allowUndefined then valueCapture = valueCapture + undefinedCapture * lpeg.Cc(options.undefined or nil) + else + valueCapture = valueCapture + #undefinedCapture * util.denied("undefined", "others.allowUndefined") end return valueCapture end -function load_types(options, global_options, grammar) - local capture = buildCapture(options) - util.append_grammar_item(grammar, "VALUE", capture) -end +local others = { + mergeOptions = mergeOptions, + generateLexer = generateLexer +} + +return others diff --git a/lua/json/decode/state.lua b/lua/json/decode/state.lua new file mode 100644 index 00000000..693d5df3 --- /dev/null +++ b/lua/json/decode/state.lua @@ -0,0 +1,189 @@ +--[[ + Licensed according to the included 'LICENSE' document + Author: Thomas Harning Jr +]] + +local setmetatable = setmetatable +local jsonutil = require("json.util") +local assert = assert +local type = type +local next = next +local unpack = require("table").unpack or unpack + +local _ENV = nil + +local state_ops = {} +local state_mt = { + __index = state_ops +} + +function state_ops.pop(self) + self.previous_set = true + self.previous = self.active + local i = self.i + -- Load in this array into the active item + self.active = self.stack[i] + self.active_state = self.state_stack[i] + self.active_key = self.key_stack[i] + self.stack[i] = nil + self.state_stack[i] = nil + self.key_stack[i] = nil + + self.i = i - 1 +end + +function state_ops.push(self) + local i = self.i + 1 + self.i = i + + self.stack[i] = self.active + self.state_stack[i] = self.active_state + self.key_stack[i] = self.active_key +end + +function state_ops.put_object_value(self, trailing) + local object_options = self.options.object + if trailing and object_options.trailingComma then + if not self.active_key then + return + end + end + assert(self.active_key, "Missing key value") + object_options.setObjectKey(self.active, self.active_key, self:grab_value()) + self.active_key = nil +end + +function state_ops.put_array_value(self, trailing) + -- Safety check + if trailing and not self.previous_set and self.options.array.trailingComma then + return + end + local new_index = self.active_state + 1 + self.active_state = new_index + self.active[new_index] = self:grab_value() +end + +function state_ops.put_value(self, trailing) + if self.active_state == 'object' then + self:put_object_value(trailing) + else + self:put_array_value(trailing) + end +end + +function state_ops.new_array(self) + local new_array = {} + if jsonutil.InitArray then + new_array = jsonutil.InitArray(new_array) or new_array + end + self.active = new_array + self.active_state = 0 + self.active_key = nil + self:unset_value() +end + +function state_ops.end_array(self) + if self.previous_set or self.active_state ~= 0 then + -- Not an empty array + self:put_value(true) + end + if self.active_state ~= #self.active then + -- Store the length in + self.active.n = self.active_state + end +end + +function state_ops.new_object(self) + local new_object = {} + self.active = new_object + self.active_state = 'object' + self.active_key = nil + self:unset_value() +end + +function state_ops.end_object(self) + if self.previous_set or next(self.active) then + -- Not an empty object + self:put_value(true) + end +end + +function state_ops.new_call(self, name, func) + -- TODO setup properly + local new_call = {} + new_call.name = name + new_call.func = func + self.active = new_call + self.active_state = 0 + self.active_key = nil + self:unset_value() +end + +function state_ops.end_call(self) + if self.previous_set or self.active_state ~= 0 then + -- Not an empty array + self:put_value(true) + end + if self.active_state ~= #self.active then + -- Store the length in + self.active.n = self.active_state + end + local func = self.active.func + if func == true then + func = jsonutil.buildCall + end + self.active = func(self.active.name, unpack(self.active, 1, self.active.n or #self.active)) +end + + +function state_ops.unset_value(self) + self.previous_set = false + self.previous = nil +end + +function state_ops.grab_value(self) + assert(self.previous_set, "Previous value not set") + self.previous_set = false + return self.previous +end + +function state_ops.set_value(self, value) + assert(not self.previous_set, "Value set when one already in slot") + self.previous_set = true + self.previous = value +end + +function state_ops.set_key(self) + assert(self.active_state == 'object', "Cannot set key on array") + local value = self:grab_value() + local value_type = type(value) + if self.options.object.number then + assert(value_type == 'string' or value_type == 'number', "As configured, a key must be a number or string") + else + assert(value_type == 'string', "As configured, a key must be a string") + end + self.active_key = value +end + + +local function create(options) + local ret = { + options = options, + stack = {}, + state_stack = {}, + key_stack = {}, + i = 0, + active = nil, + active_key = nil, + previous = nil, + active_state = nil + + } + return setmetatable(ret, state_mt) +end + +local state = { + create = create +} + +return state diff --git a/lua/json/decode/strings.lua b/lua/json/decode/strings.lua index 8f01ce74..cb3b5cc0 100644 --- a/lua/json/decode/strings.lua +++ b/lua/json/decode/strings.lua @@ -1,10 +1,11 @@ --[[ 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") -local merge = require("json.util").merge +local merge = jsonutil.merge local tonumber = tonumber local string_char = require("string").char @@ -12,14 +13,16 @@ local floor = require("math").floor local table_concat = require("table").concat local error = error -module("json.decode.strings") + +local _ENV = nil + local function get_error(item) local fmt_string = item .. " in string [%q] @ %i:%i" - return function(data, index) + return lpeg.P(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) * 1 end local bad_unicode = get_error("Illegal unicode escape") @@ -64,8 +67,8 @@ local function decodeX(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 doUniSub = lpeg.P('u') * (lpeg.C(util.hexpair) * lpeg.C(util.hexpair) + bad_unicode) +local doXSub = lpeg.P('x') * (lpeg.C(util.hexpair) + bad_hex) local defaultOptions = { badChars = '', @@ -75,24 +78,28 @@ local defaultOptions = { strict_quotes = false } -default = nil -- Let the buildCapture optimization take place +local modeOptions = {} -strict = { +modeOptions.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 mergeOptions(options, mode) + jsonutil.doOptionMerge(options, false, 'strings', defaultOptions, mode and modeOptions[mode]) +end + 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 + -- During error, force end + local captureString = captureChar^0 + (-#lpeg.P(quote) * bad_character + -1) return lpeg.P(quote) * lpeg.Cs(captureString) * lpeg.P(quote) end -local function buildCapture(options) - options = options and merge({}, defaultOptions, options) or defaultOptions +local function generateLexer(options) + options = options.strings local quotes = { '"' } if not options.strict_quotes then quotes[#quotes + 1] = "'" @@ -104,7 +111,7 @@ local function buildCapture(options) escapeMatch = escapeMatch + options.additionalEscapes end if options.escapeCheck then - escapeMatch = options.escapeCheck * escapeMatch + lpeg.P(bad_escape) + escapeMatch = options.escapeCheck * escapeMatch + bad_escape end local captureString for i = 1, #quotes do @@ -118,13 +125,9 @@ local function buildCapture(options) return captureString end -function register_types() - util.register_type("STRING") -end +local strings = { + mergeOptions = mergeOptions, + generateLexer = generateLexer +} -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 +return strings diff --git a/lua/json/decode/util.lua b/lua/json/decode/util.lua index cba4ce76..b90c0b7c 100644 --- a/lua/json/decode/util.lua +++ b/lua/json/decode/util.lua @@ -1,21 +1,67 @@ --[[ 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 = require("string").char local rawset = rawset +local jsonutil = require("json.util") local error = error local setmetatable = setmetatable -module("json.decode.util") +local table_concat = require("table").concat + +local merge = require("json.util").merge + +local _ENV = nil + +local 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 + +local function build_report(msg) + local fmt = msg:gsub("%%", "%%%%") .. " @ character: %i %i:%i [%s] line:\n%s" + return lpeg.P(function(data, pos) + local line, line_index, bad_char, last_line = get_invalid_character_info(data, pos) + local text = fmt:format(pos, line, line_index, bad_char, last_line) + error(text) + end) * 1 +end +local function unexpected() + local msg = "unexpected character" + return build_report(msg) +end +local function expected(...) + local items = {...} + local msg + if #items > 1 then + msg = "expected one of '" .. table_concat(items, "','") .. "'" + else + msg = "expected '" .. items[1] .. "'" + end + return build_report(msg) +end +local function denied(item, option) + local msg + if option then + msg = ("'%s' denied by option set '%s'"):format(item, option) + else + msg = ("'%s' denied"):format(item) + end + return build_report(msg) +end -- 09, 0A, 0B, 0C, 0D, 20 -ascii_space = lpeg.S("\t\n\v\f\r ") +local ascii_space = lpeg.S("\t\n\v\f\r ") +local unicode_space do local chr = string_char local u_space = ascii_space @@ -37,62 +83,50 @@ do 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 + unicode_space = u_space end -identifier = lpeg.R("AZ","az","__") * lpeg.R("AZ","az", "__", "09") ^0 +local identifier = lpeg.R("AZ","az","__") * lpeg.R("AZ","az", "__", "09") ^0 -hex = lpeg.R("09","AF","af") -hexpair = hex * hex +local hex = lpeg.R("09","AF","af") +local hexpair = hex * hex -comments = { +local 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 +local comment = comments.cpp + comments.c -ascii_ignored = (ascii_space + comment)^0 +local 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 +local unicode_ignored = (unicode_space + comment)^0 -- 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 +local DecimalLpegVersion = lpeg.version and tonumber(lpeg.version():match("^(%d+%.%d+)")) or 0.7 -function setObjectKeyForceNumber(t, key, value) +local function setObjectKeyForceNumber(t, key, value) key = tonumber(key) or key return rawset(t, key, value) end + +local util = { + unexpected = unexpected, + expected = expected, + denied = denied, + ascii_space = ascii_space, + unicode_space = unicode_space, + identifier = identifier, + hex = hex, + hexpair = hexpair, + comments = comments, + comment = comment, + ascii_ignored = ascii_ignored, + unicode_ignored = unicode_ignored, + DecimalLpegVersion = DecimalLpegVersion, + get_invalid_character_info = get_invalid_character_info, + setObjectKeyForceNumber = setObjectKeyForceNumber +} + +return util diff --git a/lua/json/encode.lua b/lua/json/encode.lua index 5bfc2aff..e07a6b84 100644 --- a/lua/json/encode.lua +++ b/lua/json/encode.lua @@ -1,11 +1,10 @@ --[[ 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 @@ -15,13 +14,13 @@ local output = require("json.encode.output") local util = require("json.util") local util_merge, isCall = util.merge, util.isCall -module("json.encode") +local _ENV = nil --[[ List of encoding modules to load. Loaded in sequence such that earlier encoders get priority when duplicate type-handlers exist. ---]] +]] local modulesToLoad = { "strings", "number", @@ -33,19 +32,24 @@ local modulesToLoad = { -- Modules that have been loaded local loadedModules = {} --- Default configuration options to apply -local defaultOptions = {} +local json_encode = {} + -- Configuration bases for client apps -default = nil -strict = { +local modes_defined = { "default", "strict" } + +json_encode.default = {} +json_encode.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 + if mod.mergeOptions then + for _, mode in pairs(modes_defined) do + mod.mergeOptions(json_encode[mode], mode) + end + end loadedModules[name] = mod end @@ -76,12 +80,12 @@ end --[[ Encode a value with a given encoding map and state ---]] -local function encodeWithMap(value, map, state) +]] +local function encodeWithMap(value, map, state, isObjectKey) 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) + local ret = encoder(value, state, isObjectKey) if false ~= ret then return ret end @@ -94,15 +98,15 @@ local function getBaseEncoder(options) local encoderMap = prepareEncodeMap(options) if options.preProcess then local preProcess = options.preProcess - return function(value, state) - local ret = preProcess(value) + return function(value, state, isObjectKey) + local ret = preProcess(value, isObjectKey or false) if nil ~= ret then value = ret end return encodeWithMap(value, encoderMap, state) end end - return function(value, state) + return function(value, state, isObjectKey) return encodeWithMap(value, encoderMap, state) end end @@ -110,9 +114,9 @@ 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 +]] +function json_encode.getEncoder(options) + options = options and util_merge({}, json_encode.default, options) or json_encode.default local encode = getBaseEncoder(options) local function initialEncode(value) @@ -147,13 +151,16 @@ end 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) +]] +function json_encode.encode(data, options) + return json_encode.getEncoder(options)(data) end -local mt = getmetatable(_M) or {} +local mt = {} mt.__call = function(self, ...) - return encode(...) + return json_encode.encode(...) end -setmetatable(_M, mt) + +setmetatable(json_encode, mt) + +return json_encode diff --git a/lua/json/encode/array.lua b/lua/json/encode/array.lua index 8ba03f68..3744409c 100644 --- a/lua/json/encode/array.lua +++ b/lua/json/encode/array.lua @@ -1,7 +1,7 @@ --[[ Licensed according to the included 'LICENSE' document Author: Thomas Harning Jr ---]] +]] local jsonutil = require("json.util") local type = type @@ -13,17 +13,20 @@ local math = require("math") 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 +local jsonutil = require("json.util") +local util_IsArray = jsonutil.IsArray -module("json.encode.array") +local _ENV = nil local defaultOptions = { isArray = util_IsArray } -default = nil -strict = nil +local modeOptions = {} + +local function mergeOptions(options, mode) + jsonutil.doOptionMerge(options, false, 'array', defaultOptions, mode and modeOptions[mode]) +end --[[ Utility function to determine whether a table is an array or not. @@ -33,8 +36,8 @@ strict = nil 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 function isArray(val, options) local externalIsArray = options and options.isArray if externalIsArray then @@ -67,13 +70,13 @@ 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 getEncoder(options) + options = options and jsonutil.merge({}, defaultOptions, options) or defaultOptions local function encodeArray(tab, state) if not isArray(tab, options) then return false @@ -97,3 +100,11 @@ function getEncoder(options) end return { table = encodeArray } end + +local array = { + mergeOptions = mergeOptions, + isArray = isArray, + getEncoder = getEncoder +} + +return array diff --git a/lua/json/encode/calls.lua b/lua/json/encode/calls.lua index 84f2483b..11dddfe8 100644 --- a/lua/json/encode/calls.lua +++ b/lua/json/encode/calls.lua @@ -1,9 +1,7 @@ --[[ Licensed according to the included 'LICENSE' document Author: Thomas Harning Jr ---]] -local jsonutil = require("json.util") - +]] local table = require("table") local table_concat = table.concat @@ -11,19 +9,21 @@ 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 +local jsonutil = require("json.util") -module("json.encode.calls") +local isCall, decodeCall = jsonutil.isCall, jsonutil.decodeCall +local _ENV = nil local defaultOptions = { } -- No real default-option handling needed... -default = nil -strict = nil +local modeOptions = {} + +local function mergeOptions(options, mode) + jsonutil.doOptionMerge(options, false, 'calls', defaultOptions, mode and modeOptions[mode]) +end --[[ @@ -31,9 +31,9 @@ strict = nil 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 getEncoder(options) + options = options and jsonutil.merge({}, defaultOptions, options) or defaultOptions local function encodeCall(value, state) if not isCall(value) then return false @@ -59,3 +59,10 @@ function getEncoder(options) ['function'] = encodeCall } end + +local calls = { + mergeOptions = mergeOptions, + getEncoder = getEncoder +} + +return calls diff --git a/lua/json/encode/number.lua b/lua/json/encode/number.lua index de97eabd..290b4404 100644 --- a/lua/json/encode/number.lua +++ b/lua/json/encode/number.lua @@ -1,25 +1,30 @@ --[[ Licensed according to the included 'LICENSE' document Author: Thomas Harning Jr ---]] +]] local tostring = tostring local assert = assert -local util = require("json.util") +local jsonutil = require("json.util") local huge = require("math").huge -module("json.encode.number") +local _ENV = nil local defaultOptions = { nan = true, inf = true } -default = nil -- Let the buildCapture optimization take place -strict = { +local modeOptions = {} +modeOptions.strict = { nan = false, inf = false } +local function mergeOptions(options, mode) + jsonutil.doOptionMerge(options, false, 'number', defaultOptions, mode and modeOptions[mode]) +end + + local function encodeNumber(number, options) if number ~= number then assert(options.nan, "Invalid number: NaN not enabled") @@ -36,11 +41,18 @@ local function encodeNumber(number, options) return tostring(number) end -function getEncoder(options) - options = options and util.merge({}, defaultOptions, options) or defaultOptions +local function getEncoder(options) + options = options and jsonutil.merge({}, defaultOptions, options) or defaultOptions return { number = function(number, state) return encodeNumber(number, options) end } end + +local number = { + mergeOptions = mergeOptions, + getEncoder = getEncoder +} + +return number diff --git a/lua/json/encode/object.lua b/lua/json/encode/object.lua index 71c29cf3..4716d526 100644 --- a/lua/json/encode/object.lua +++ b/lua/json/encode/object.lua @@ -1,7 +1,7 @@ --[[ Licensed according to the included 'LICENSE' document Author: Thomas Harning Jr ---]] +]] local pairs = pairs local assert = assert @@ -9,27 +9,30 @@ local type = type local tostring = tostring local table_concat = require("table").concat -local util_merge = require("json.util").merge +local jsonutil = require("json.util") -module("json.encode.object") +local _ENV = nil local defaultOptions = { } -default = nil -strict = nil +local modeOptions = {} + +local function mergeOptions(options, mode) + jsonutil.doOptionMerge(options, false, 'object', defaultOptions, mode and modeOptions[mode]) +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 --[[ 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) @@ -40,7 +43,7 @@ local function encodeTable(tab, options, state) 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) + local name = encode(tostring(k), state, true) if first then first = false else @@ -57,11 +60,18 @@ local function encodeTable(tab, options, state) return unmarkAfterEncode(tab, state, compositeEncoder(valueEncoder, '{', '}', nil, tab, encode, state)) end -function getEncoder(options) - options = options and util_merge({}, defaultOptions, options) or defaultOptions +local function getEncoder(options) + options = options and jsonutil.merge({}, defaultOptions, options) or defaultOptions return { table = function(tab, state) return encodeTable(tab, options, state) end } end + +local object = { + mergeOptions = mergeOptions, + getEncoder = getEncoder +} + +return object diff --git a/lua/json/encode/others.lua b/lua/json/encode/others.lua index 64fe83ea..b5270443 100644 --- a/lua/json/encode/others.lua +++ b/lua/json/encode/others.lua @@ -1,18 +1,17 @@ --[[ 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") +local _ENV = nil -- Shortcut that works -encodeBoolean = tostring +local encodeBoolean = tostring local defaultOptions = { allowUndefined = true, @@ -20,13 +19,17 @@ local defaultOptions = { undefined = jsonutil.undefined } -default = nil -- Let the buildCapture optimization take place -strict = { +local modeOptions = {} + +modeOptions.strict = { allowUndefined = false } -function getEncoder(options) - options = options and util_merge({}, defaultOptions, options) or defaultOptions +local function mergeOptions(options, mode) + jsonutil.doOptionMerge(options, false, 'others', defaultOptions, mode and modeOptions[mode]) +end +local function getEncoder(options) + options = options and jsonutil.merge({}, defaultOptions, options) or defaultOptions local function encodeOthers(value, state) if value == options.null then return 'null' @@ -53,3 +56,11 @@ function getEncoder(options) end return ret end + +local others = { + encodeBoolean = encodeBoolean, + mergeOptions = mergeOptions, + getEncoder = getEncoder +} + +return others diff --git a/lua/json/encode/output.lua b/lua/json/encode/output.lua index ccb4004a..8293b622 100644 --- a/lua/json/encode/output.lua +++ b/lua/json/encode/output.lua @@ -1,11 +1,11 @@ --[[ Licensed according to the included 'LICENSE' document Author: Thomas Harning Jr ---]] +]] local type = type local assert, error = assert, error local table_concat = require("table").concat -local loadstring = loadstring +local loadstring = loadstring or load local io = require("io") @@ -13,7 +13,7 @@ local setmetatable = setmetatable local output_utility = require("json.encode.output_utility") -module("json.encode.output") +local _ENV = nil local tableCompositeCache = setmetatable({}, {__mode = 'v'}) @@ -27,7 +27,7 @@ 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) @@ -38,7 +38,7 @@ local function defaultTableCompositeWriter(nextValues, beginValue, closeValue, i end -- no 'simple' as default action is just to return the value -function getDefault() +local function getDefault() return { composite = defaultTableCompositeWriter } end @@ -77,8 +77,15 @@ local function buildIoWriter(output) end return { composite = ioWriter, simple = ioSimpleWriter } end -function getIoWriter(output) +local function getIoWriter(output) return function() return buildIoWriter(output) end end + +local output = { + getDefault = getDefault, + getIoWriter = getIoWriter +} + +return output diff --git a/lua/json/encode/output_utility.lua b/lua/json/encode/output_utility.lua index 0873e6dd..b6607d1a 100644 --- a/lua/json/encode/output_utility.lua +++ b/lua/json/encode/output_utility.lua @@ -1,11 +1,11 @@ --[[ Licensed according to the included 'LICENSE' document Author: Thomas Harning Jr ---]] +]] local setmetatable = setmetatable -local assert, loadstring = assert, loadstring +local assert, loadstring = assert, loadstring or load -module("json.encode.output_utility") +local _ENV = nil -- Key == weak, if main key goes away, then cache cleared local outputCache = setmetatable({}, {__mode = 'k'}) @@ -33,7 +33,7 @@ local function buildFunction(nextValues, innerValue, valueWriter, innerWriter) return assert(loadstring(functionCode))() end -function prepareEncoder(cacheKey, nextValues, innerValue, valueWriter, innerWriter) +local function prepareEncoder(cacheKey, nextValues, innerValue, valueWriter, innerWriter) local cache = outputCache[cacheKey] if not cache then cache = {} @@ -46,3 +46,9 @@ function prepareEncoder(cacheKey, nextValues, innerValue, valueWriter, innerWrit end return fun end + +local output_utility = { + prepareEncoder = prepareEncoder +} + +return output_utility diff --git a/lua/json/encode/strings.lua b/lua/json/encode/strings.lua index d0eae651..09d85a91 100644 --- a/lua/json/encode/strings.lua +++ b/lua/json/encode/strings.lua @@ -1,12 +1,14 @@ --[[ Licensed according to the included 'LICENSE' document Author: Thomas Harning Jr ---]] +]] local string_char = require("string").char local pairs = pairs -local util_merge = require("json.util").merge -module("json.encode.strings") +local jsonutil = require("json.util") +local util_merge = jsonutil.merge + +local _ENV = nil local normalEncodingMap = { ['"'] = '\\"', @@ -42,26 +44,45 @@ end local defaultOptions = { xEncode = false, -- Encode single-bytes as \xXX + processor = nil, -- Simple processor for the string prior to quoting -- / 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 +local modeOptions = {} + +local function mergeOptions(options, mode) + jsonutil.doOptionMerge(options, false, 'strings', defaultOptions, mode and modeOptions[mode]) +end -function getEncoder(options) +local 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) .. '"' + local encodeString + if options.processor then + local processor = options.processor + encodeString = function(s, state) + return '"' .. processor(s:gsub('[' .. encodeSet .. ']', encodingMap)) .. '"' + end + else + encodeString = function(s, state) + return '"' .. s:gsub('[' .. encodeSet .. ']', encodingMap) .. '"' + end end return { string = encodeString } end + +local strings = { + mergeOptions = mergeOptions, + getEncoder = getEncoder +} + +return strings diff --git a/lua/json/util.lua b/lua/json/util.lua index 33db1d7f..a4599db3 100644 --- a/lua/json/util.lua +++ b/lua/json/util.lua @@ -1,7 +1,7 @@ --[[ Licensed according to the included 'LICENSE' document Author: Thomas Harning Jr ---]] +]] local type = type local print = print local tostring = tostring @@ -9,13 +9,14 @@ local pairs = pairs local getmetatable, setmetatable = getmetatable, setmetatable local select = select -module("json.util") +local _ENV = nil + local function foreach(tab, func) for k, v in pairs(tab) do func(k,v) end end -function printValue(tab, name) +local function printValue(tab, name) local parsed = {} local function doPrint(key, value, space) space = space or '' @@ -38,7 +39,7 @@ function printValue(tab, name) doPrint(name, tab) end -function clone(t) +local function clone(t) local ret = {} for k,v in pairs(t) do ret[k] = v @@ -46,24 +47,37 @@ function clone(t) return ret end -local function merge(t, from, ...) - if not from then +local function inner_merge(t, remaining, from, ...) + if remaining == 0 then return t end - for k,v in pairs(from) do - t[k] = v + if from then + for k,v in pairs(from) do + t[k] = v + end end - return merge(t, ...) + return inner_merge(t, remaining - 1, ...) +end + +--[[* + Shallow-merges tables in order onto the first table. + + @param t table to merge entries onto + @param ... sequence of 0 or more tables to merge onto 't' + + @returns table 't' from input +]] +local function merge(t, ...) + return inner_merge(t, select('#', ...), ...) end -_M.merge = merge -- Function to insert nulls into the JSON stream -function null() +local function null() return null end -- Marker for 'undefined' values -function undefined() +local function undefined() return undefined end @@ -73,28 +87,29 @@ 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) +]] +local function IsArray(value) if type(value) ~= 'table' then return false end - local ret = getmetatable(value) == ArrayMT + local meta = getmetatable(value) + local ret = meta == ArrayMT or (meta ~= nil and meta.__is_luajson_array) if not ret then if #value == 0 then return false end else return ret end end -function InitArray(array) +local function InitArray(array) setmetatable(array, ArrayMT) return array end local CallMT = {} -function isCall(value) +local function isCall(value) return CallMT == getmetatable(value) end -function buildCall(name, ...) +local function buildCall(name, ...) local callData = { name = name, parameters = {n = select('#', ...), ...} @@ -102,7 +117,36 @@ function buildCall(name, ...) return setmetatable(callData, CallMT) end -function decodeCall(callData) +local function decodeCall(callData) if not isCall(callData) then return nil end return callData.name, callData.parameters end + +local function doOptionMerge(options, nested, name, defaultOptions, modeOptions) + if nested then + modeOptions = modeOptions and modeOptions[name] + defaultOptions = defaultOptions and defaultOptions[name] + end + options[name] = merge( + {}, + defaultOptions, + modeOptions, + options[name] + ) +end + +local json_util = { + printValue = printValue, + clone = clone, + merge = merge, + null = null, + undefined = undefined, + IsArray = IsArray, + InitArray = InitArray, + isCall = isCall, + buildCall = buildCall, + decodeCall = decodeCall, + doOptionMerge = doOptionMerge +} + +return json_util