From ebc03a99aff4d02e20f5e90114d123026ecf7a53 Mon Sep 17 00:00:00 2001 From: Thomas Harning Jr Date: Tue, 30 Sep 2008 00:17:08 -0400 Subject: [PATCH] encoder: Break apart the encoder into distinct modules + add call encoding --- src/json/encode.lua | 244 ++++++++++++++++---------------------------- src/json/encode/array.lua | 51 +++++++++ src/json/encode/calls.lua | 45 ++++++++ src/json/encode/object.lua | 28 +++++ src/json/encode/others.lua | 18 ++++ src/json/encode/strings.lua | 34 ++++++ 6 files changed, 262 insertions(+), 158 deletions(-) rewrite src/json/encode.lua (81%) create mode 100644 src/json/encode/array.lua create mode 100644 src/json/encode/calls.lua create mode 100644 src/json/encode/object.lua create mode 100644 src/json/encode/others.lua create mode 100644 src/json/encode/strings.lua diff --git a/src/json/encode.lua b/src/json/encode.lua dissimilarity index 81% index 1a9cf10..2a93f33 100644 --- a/src/json/encode.lua +++ b/src/json/encode.lua @@ -1,158 +1,86 @@ ---[[ - Licensed according to the included 'LICENSE' document - Author: Thomas Harning Jr -]] -local tostring, string, type = tostring, string, type -local tonumber, math, assert = tonumber, math, assert -local table, pairs, ipairs = table, pairs, ipairs -local getmetatable, setmetatable = getmetatable, setmetatable -local select = select -local print = print -local error = error -local util = require("json.util") -local null = util.null -local externalIsArray = IsArray or util.IsArray -- Support for the special IsArray external function... - -module("json.encode") - -local encodingMap = { - ['\\'] = '\\\\', - ['"'] = '\\"', - ['\n'] = '\\n', - ['\t'] = '\\t', - ['\b'] = '\\b', - ['\f'] = '\\f', - ['\r'] = '\\r', - ['/'] = '\\/' -} - --- 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 = 1, 255 do - local c = string.char(i) - if c:match('%c') and not encodingMap[c] then - encodingMap[c] = string.format('\\u%.4X', i) - end -end -local stringPreprocess = nil -local function encodeString(s) - if stringPreprocess then - s = stringPreprocess(s) - end - return '"' .. string.gsub(s, '[\\"/%c%z]', encodingMap) .. '"' -end - -local function isArray(val) - 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' and select(2, math.modf(k)) == 0 and 1<=k then - assert(isEncodable(v), "Invalid array element type:" .. type(v)) - if k > len then -- Use Lua's length as absolute determiner - return false - end - else -- Not an integral key... - return false - end - end - - return true -end - -local function tonull(val) - if val == null then - return 'null' - end -end - --- Forward reference for encodeValue function -local encodeValue -local alreadyEncoded -- Table set at the beginning of every - -- encoding operation to empty to detect recursiveness -local function encodeTable(tab) - if alreadyEncoded[tab] then - error("Recursive table detected") - end - alreadyEncoded[tab] = true - local retVal = {} - -- Try for array - if isArray(tab) then - for i = 1,(tab.n or #tab) do - retVal[#retVal + 1] = encodeValue(tab[i]) - end - return '[' .. table.concat(retVal, ',') .. ']' - else - -- Is table - for i, v in pairs(tab) do - local ti = type(i) - if ti == 'string' or ti == 'number' or ti == 'boolean' then - i = encodeString(tostring(i)) - else - error("Invalid object index type: " .. ti) - end - retVal[#retVal + 1] = i .. ':' .. encodeValue(v) - end - return '{' .. table.concat(retVal, ',') .. '}' - end -end - -local function encodeNumber(number) - local str = tostring(number) - if str == "nan" then return "NaN" end - if str == "inf" then return "Infinity" end - if str == "-inf" then return "-Infinity" end - return str -end - -local allowAllNumbers = true - -local encodeMapping = { - ['table' ] = encodeTable, - ['number' ] = allowAllNumbers and encodeNumber or tostring, - ['boolean'] = tostring, - ['function'] = tonull, - ['string' ] = encodeString, - ['nil'] = function() return 'null' end -- For the case that nils are encountered count them as nulls -} -function isEncodable(item) - return encodeMapping[type(item)] and not (type(item) == 'function' and item ~= null) -end - ---[[local ]] function encodeValue(item) - local encoder = encodeMapping[type(item)] - if not encoder then - error("Invalid item to encode: " .. type(item)) - end - return encoder(item) -end - -local defaultOptions = { - strings = { - preProcess = false - } -} - -function encode(data, options) - options = options or defaultOptions - stringPreprocess = options and options.strings and options.strings.preProcess - alreadyEncoded = {} - return encodeValue(data) -end - -local mt = getmetatable(_M) or {} -mt.__call = function(self, ...) - return encode(...) -end -setmetatable(_M, mt) +--[[ + Licensed according to the included 'LICENSE' document + Author: Thomas Harning Jr +]] +local type = type +local assert = assert +local getmetatable, setmetatable = getmetatable, setmetatable +local util = require("json.util") +local null = util.null + +local require = require + +local strings = require("json.encode.strings") +local others = require("json.encode.others") + +module("json.encode") + +-- Load these modules after defining that json.encode exists +-- calls, object, and array need encodeValue and isEncodable +local calls = require("json.encode.calls") +local array = require("json.encode.array") +local object = require("json.encode.object") + + +local function encodeFunction(val, options) + if val ~= null and calls.isCall(val, options) then + return calls.encode(val, options) + end + return 'null' +end + +local alreadyEncoded -- Table set at the beginning of every + -- encoding operation to empty to detect recursiveness +local function encodeTable(tab, options) + assert(not alreadyEncoded[tab], "Recursive table detected") + alreadyEncoded[tab] = true + -- Pass off encoding to appropriate encoder + if calls.isCall(tab, options) then + return calls.encode(tab, options) + elseif array.isArray(tab, options) then + return array.encode(tab, options) + else + return object.encode(tab, options) + end +end + +local encodeMapping = { + ['table' ] = encodeTable, + ['number' ] = others.encodeNumber, + ['boolean'] = others.encodeBoolean, + ['function'] = encodeFunction, + ['string' ] = strings.encode, + ['nil'] = others.encodeNil -- For the case that nils are encountered count them as nulls +} +function isEncodable(item, options) + local isNotEncodableFunction = type(item) == 'function' and (item ~= null and not calls.isCall(item, options)) + return encodeMapping[type(item)] and not isNotEncodableFunction +end + +function encodeValue(item, options) + local itemType = type(item) + local encoder = encodeMapping[itemType] + assert(encoder, "Invalid item to encode: " .. itemType) + return encoder(item, options) +end + +local defaultOptions = { + strings = { + preProcess = false + }, + array = { + isArray = util.IsArray + } +} + +function encode(data, options) + options = options or defaultOptions + alreadyEncoded = {} + return encodeValue(data, options) +end + +local mt = getmetatable(_M) or {} +mt.__call = function(self, ...) + return encode(...) +end +setmetatable(_M, mt) diff --git a/src/json/encode/array.lua b/src/json/encode/array.lua new file mode 100644 index 0000000..6b75bce --- /dev/null +++ b/src/json/encode/array.lua @@ -0,0 +1,51 @@ +local jsonencode = require("json.encode") + +local type = type +local pairs = pairs +local assert = assert + +local table_concat = table.concat +local math_floor, math_modf = math.floor, math.modf + +module("json.encode.array") + +function isArray(val, options) + local externalIsArray = options and options.array and options.array.isArray + local isEncodable = jsonencode.isEncodable + + 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 + assert(isEncodable(v, options), "Invalid array element type:" .. type(v)) + if k > len then -- Use Lua's length as absolute determiner + return false + end + end + + return true +end + +function encode(tab, options) + local encodeValue = jsonencode.encodeValue + local retVal = {} + for i = 1,(tab.n or #tab) do + retVal[#retVal + 1] = encodeValue(tab[i], options) + end + return '[' .. table_concat(retVal, ',') .. ']' +end diff --git a/src/json/encode/calls.lua b/src/json/encode/calls.lua new file mode 100644 index 0000000..9949265 --- /dev/null +++ b/src/json/encode/calls.lua @@ -0,0 +1,45 @@ +local jsonutil = require("json.util") + +local table_concat = table.concat + +local select = select +local getmetatable, setmetatable = getmetatable, setmetatable + +module("json.encode.calls") + +function buildCall(name, ...) + return setmetatable({}, { + callData = { + name = name, + parameters = {n = select('#', ...), ...} + } + }) +end +function isCall(value, options) + local mt = getmetatable(value) + return mt and mt.callData +end +local function decodeCall(value) + local mt = getmetatable(value) + if not mt and mt.callData then + return + end + return mt.callData.name, mt.callData.parameters +end +--[[ + Encode '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 encode(value, options) + local name, params = decodeCall(value) + for i = 1, (params.n or #params) do + local val = params[i] + if val == nil then + val = jsonutil.null + end + params[i] = jsonencode.encode(val, options) + end + return name .. '(' .. table_concat(params, ',') .. ')' +end diff --git a/src/json/encode/object.lua b/src/json/encode/object.lua new file mode 100644 index 0000000..24452c8 --- /dev/null +++ b/src/json/encode/object.lua @@ -0,0 +1,28 @@ +local jsonencode = require("json.encode") + +local pairs = pairs +local assert = assert + +local type = type +local tostring = tostring + +local table_concat = table.concat + +local strings = require("json.encode.strings") + +module("json.encode.object") + +function encode(tab, options) + local encodeValue = jsonencode.encodeValue + local encodeString = strings.encode + local retVal = {} + -- Is table + for i, v in pairs(tab) do + local ti = type(i) + assert(ti == 'string' or ti == 'number' or ti == 'boolean', "Invalid object index type: " .. ti) + i = encodeString(tostring(i), options) + + retVal[#retVal + 1] = i .. ':' .. encodeValue(v, options) + end + return '{' .. table_concat(retVal, ',') .. '}' +end diff --git a/src/json/encode/others.lua b/src/json/encode/others.lua new file mode 100644 index 0000000..9c66bd4 --- /dev/null +++ b/src/json/encode/others.lua @@ -0,0 +1,18 @@ +local tostring = tostring + +module("json.encode.others") + +function encodeNumber(number, options) + local str = tostring(number) + if str == "nan" then return "NaN" end + if str == "inf" then return "Infinity" end + if str == "-inf" then return "-Infinity" end + return str +end + +-- Shortcut that works +encodeBoolean = tostring + +function encodeNil(value, options) + return 'null' +end diff --git a/src/json/encode/strings.lua b/src/json/encode/strings.lua new file mode 100644 index 0000000..e40dc6e --- /dev/null +++ b/src/json/encode/strings.lua @@ -0,0 +1,34 @@ +local string_char = string.char + +module("json.encode.strings") + +local encodingMap = { + ['\\'] = '\\\\', + ['"'] = '\\"', + ['\n'] = '\\n', + ['\t'] = '\\t', + ['\b'] = '\\b', + ['\f'] = '\\f', + ['\r'] = '\\r', + ['/'] = '\\/' +} + +-- 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 = 1, 255 do + local c = string_char(i) + if c:match('%c') and not encodingMap[c] then + encodingMap[c] = ('\\u%.4X'):format(i) + end +end + +function encode(s, options) + local stringPreprocess = options and options.strings and options.strings.preProcess + if stringPreprocess then + s = stringPreprocess(s) + end + return '"' .. s:gsub('[\\"/%c%z]', encodingMap) .. '"' +end -- 2.11.4.GIT