191 lines
4.4 KiB
Lua
191 lines
4.4 KiB
Lua
|
--[[
|
||
|
Licensed according to the included 'LICENSE' document
|
||
|
Author: Thomas Harning Jr <harningt@gmail.com>
|
||
|
]]
|
||
|
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
|