| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672 |
- -----------------------------------------------------------------------------
- -- Imports and dependencies
- -----------------------------------------------------------------------------
- local math = require("math")
- local string = require("string")
- local table = require("table")
- -----------------------------------------------------------------------------
- -- Module declaration
- -----------------------------------------------------------------------------
- local json = {} -- Public namespace
- local json_private = {} -- Private namespace
- -- Public constants
- json.EMPTY_ARRAY = {}
- json.EMPTY_OBJECT = {}
- json_private.attributeMap = {}
- -- Public functions
- -- Private functions
- local decode_scanArray
- local decode_scanComment
- local decode_scanConstant
- local decode_scanNumber
- local decode_scanObject
- local setObjectType
- local decode_scanString
- local decode_scanWhitespace
- local isArray
- local isEncodable
- local avoidLoopOver
- local index
- local getTableType
- local checkAttributeInfo
- local setClassType
- local bsonIgnore = "MongoDB.Bson.Serialization.Attributes.BsonIgnoreAttribute"
- local bsonRequired = "MongoDB.Bson.Serialization.Attributes.BsonRequiredAttribute"
- -----------------------------------------------------------------------------
- -- PUBLIC FUNCTIONS
- -----------------------------------------------------------------------------
- --- Encodes an arbitrary Lua object / variable.
- -- @param v The Lua object / variable to be JSON encoded.
- -- @return String containing the JSON encoding in internal Lua string format (i.e. not unicode)
- function json.encode(v)
- avoidLoopOver()
- -- Handle nil values
- if v == nil then
- return "null"
- end
- local vtype = type(v)
- -- Handle strings
- if vtype == "string" then
- return '"' .. json_private.encodeString(v) .. '"' -- Need to handle encoding in string
- end
- -- Handle booleans
- if vtype == "number" or vtype == "boolean" then
- return tostring(v)
- end
- -- Handle tables
- if vtype == "table" then
- local rval = {}
- if not getmetatable(v) then
- return "{}"
- end
- table.insert(rval,'"_t":"'.. getmetatable(v).__name__ .. '"')
- -- Consider arrays separately
- local tType = getTableType(v)
- if (tType == "array") then
- for i = 1, #v do
- table.insert(rval, json.encode(v[i]))
- end
- elseif tType == "hashset" then
- for i, _ in pairs(v) do
- table.insert(rval, json.encode(i))
- end
- elseif tType == "dictionary" then
- for i, j in pairs(v) do
- if isEncodable(i) and isEncodable(j) then
- table.insert(rval, '"' .. json_private.encodeString(i) .. '":' .. json.encode(j))
- end
- end
- else
- local map = {}
- local needMap = {}
- checkAttributeInfo(v, map, needMap)
- map = table.unique(map, true)
- needMap = table.unique(needMap, true)
- for i, j in pairs(v) do
- if isEncodable(i) and isEncodable(j) and not IsVlaueInTable(map, i) then
- table.insert(rval, '"' .. json_private.encodeString(i) .. '":' .. json.encode(j))
- end
- end
- for i = 1, #needMap do
- local kk = needMap[i]
- local vv = v[kk]
- if isEncodable(kk) and isEncodable(vv) then
- table.insert(rval, '"' .. json_private.encodeString(kk) .. '":' .. json.encode(vv))
- end
- end
- end
- if tType == "array" or tType == "hashset" then
- return "[" .. table.concat(rval, ",") .. "]"
- else
- return "{" .. table.concat(rval, ",") .. "}"
- end
- end
- -- Handle null values
- if vtype == "function" and v == json.null then
- return "null"
- end
- assert(false, "encode attempt to encode unsupported type " .. vtype .. ":" .. tostring(v))
- end
- function avoidLoopOver()
- index = index + 1
- if index > 1000 then
- assert(false, "stack overflow")
- end
- end
- function getTableType(t)
- if not t then
- return nil
- end
- local meta = getmetatable(t)
- if (meta.__genericT__) then
- if (meta.__genericTKey__) then
- if (meta.__genericTValue__) then
- return "dictionary"
- else
- return "hashset"
- end
- else
- return "array"
- end
- else
- return nil
- end
- end
- function checkAttributeInfo(t, map, needMap)
- local base, meta, fields, properties, attributeName
- base = t
- meta = getmetatable(t)
- repeat
- avoidLoopOver()
- if (meta and meta.__metadata__) then
- fields = meta.__metadata__.fields
- if (fields) then
- for _, v in pairs(fields) do
- for i = 4, #v do
- attributeName = getmetatable(v[i]).__name
- if (attributeName == bsonIgnore) then
- table.insert(map, v[1])
- elseif attributeName == bsonRequired then
- table.insert(needMap, v[1])
- end
- end
- end
- end
- end
- if (meta and meta.__metadata__) then
- properties = meta.__metadata__.properties
- if (properties) then
- for _, v in pairs(properties) do
- for i = 5, #v do
- attributeName = getmetatable(v[i]).__name
- if (attributeName == bsonIgnore) then
- table.insert(map, v[1])
- elseif attributeName == bsonRequired then
- table.insert(needMap, v[1])
- end
- end
- end
- end
- end
- base = System.base(base)
- if (not base or not base.__name__) then
- break
- end
- meta = base
- until false
- end
- --- Decodes a JSON string and returns the decoded value as a Lua data structure / value.
- -- @param s The string to scan.
- -- @param startPos Optional starting position where the JSON string is located. Defaults to 1.
- -- @param Lua object, number The object that was scanned, as a Lua table / string / number / boolean or nil,
- -- and the position of the first character after
- -- the scanned JSON object.
- function json.decode(s, startPos)
- startPos = startPos and startPos or 1
- startPos = decode_scanWhitespace(s, startPos)
- assert(startPos <= string.len(s), "Unterminated JSON encoded object found at position in [" .. s .. "]")
- local curChar = string.sub(s, startPos, startPos)
- -- Object
- if curChar == "{" then
- return decode_scanObject(s, startPos)
- end
- -- Array
- if curChar == "[" then
- return decode_scanArray(s, startPos)
- end
- -- Number
- if string.find("+-0123456789.e", curChar, 1, true) then
- return decode_scanNumber(s, startPos)
- end
- -- String
- if curChar == [["]] or curChar == [[']] then
- return decode_scanString(s, startPos)
- end
- if string.sub(s, startPos, startPos + 1) == "/*" then
- return json.decode(s, decode_scanComment(s, startPos))
- end
- -- Otherwise, it must be a constant
- return decode_scanConstant(s, startPos)
- end
- --- The null function allows one to specify a null value in an associative array (which is otherwise
- -- discarded if you set the value with 'nil' in Lua. Simply set t = { first=json.null }
- function json.null()
- return json.null -- so json.null() will also return null ;-)
- end
- -----------------------------------------------------------------------------
- -- Internal, PRIVATE functions.
- -- Following a Python-like convention, I have prefixed all these 'PRIVATE'
- -- functions with an underscore.
- -----------------------------------------------------------------------------
- --- Scans an array from JSON into a Lua object
- -- startPos begins at the start of the array.
- -- Returns the array and the next starting position
- -- @param s The string being scanned.
- -- @param startPos The starting position for the scan.
- -- @return table, int The scanned array as a table, and the position of the next character to scan.
- function decode_scanArray(s, startPos)
- local array = {} -- The return value
- local stringLen = string.len(s)
- local objectType
- assert(
- string.sub(s, startPos, startPos) == "[",
- "decode_scanArray called but array does not start at position " .. startPos .. " in string:\n" .. s
- )
- startPos = startPos + 1
- -- Infinite loop for array elements
- local index = 1
- repeat
- startPos = decode_scanWhitespace(s, startPos)
- assert(startPos <= stringLen, "JSON String ended unexpectedly scanning array.")
- local curChar = string.sub(s, startPos, startPos)
- if (curChar == "]") then
- array = setObjectType(array,objectType)
- return array, startPos + 1
- end
- if (curChar == "\"") then
- assert(startPos <= stringLen, "JSON string ended unexpectedly scanning object.")
- -- Scan the key
- local key
- key, startPos = json.decode(s, startPos)
- assert(startPos <= stringLen, "JSON string ended unexpectedly searching for value of key " .. key)
- startPos = decode_scanWhitespace(s, startPos)
- assert(startPos <= stringLen, "JSON string ended unexpectedly searching for value of key " .. key)
- assert(string.sub(s, startPos, startPos) == ":", "JSON object key-value assignment mal-formed at " .. startPos)
- startPos = decode_scanWhitespace(s, startPos + 1)
- assert(startPos <= stringLen, "JSON string ended unexpectedly searching for value of key " .. key)
- local value
- value, startPos = json.decode(s, startPos)
- if key =="_t" then
- objectType = value
- end
- else
- if (curChar == ",") then
- startPos = decode_scanWhitespace(s, startPos + 1)
- end
- assert(startPos <= stringLen, "JSON String ended unexpectedly scanning array.")
- local object
- object, startPos = json.decode(s, startPos)
- array[index] = object
- index = index + 1
- end
- until false
- end
- --- Scans a comment and discards the comment.
- -- Returns the position of the next character following the comment.
- -- @param string s The JSON string to scan.
- -- @param int startPos The starting position of the comment
- function decode_scanComment(s, startPos)
- assert(
- string.sub(s, startPos, startPos + 1) == "/*",
- "decode_scanComment called but comment does not start at position " .. startPos
- )
- local endPos = string.find(s, "*/", startPos + 2)
- assert(endPos ~= nil, "Unterminated comment in string at " .. startPos)
- return endPos + 2
- end
- --- Scans for given constants: true, false or null
- -- Returns the appropriate Lua type, and the position of the next character to read.
- -- @param s The string being scanned.
- -- @param startPos The position in the string at which to start scanning.
- -- @return object, int The object (true, false or nil) and the position at which the next character should be
- -- scanned.
- function decode_scanConstant(s, startPos)
- local consts = {["true"] = true, ["false"] = false, ["null"] = nil}
- local constNames = {"true", "false", "null"}
- for i, k in pairs(constNames) do
- if string.sub(s, startPos, startPos + string.len(k) - 1) == k then
- return consts[k], startPos + string.len(k)
- end
- end
- assert(nil, "Failed to scan constant from string " .. s .. " at starting position " .. startPos)
- end
- --- Scans a number from the JSON encoded string.
- -- (in fact, also is able to scan numeric +- eqns, which is not
- -- in the JSON spec.)
- -- Returns the number, and the position of the next character
- -- after the number.
- -- @param s The string being scanned.
- -- @param startPos The position at which to start scanning.
- -- @return number, int The extracted number and the position of the next character to scan.
- function decode_scanNumber(s, startPos)
- local endPos = startPos + 1
- local stringLen = string.len(s)
- local acceptableChars = "+-0123456789.e"
- while (string.find(acceptableChars, string.sub(s, endPos, endPos), 1, true) and endPos <= stringLen) do
- endPos = endPos + 1
- end
- -- local stringValue = 'return ' .. string.sub(s, startPos, endPos - 1)
- -- local stringEval = loadstring(stringValue)
- -- assert(stringEval, 'Failed to scan number [ ' .. stringValue .. '] in JSON string at position ' .. startPos .. ' : ' .. endPos)
- local numberValue = string.sub(s, startPos, endPos - 1)
- return tonumber(numberValue), endPos
- end
- --- Scans a JSON object into a Lua object.
- -- startPos begins at the start of the object.
- -- Returns the object and the next starting position.
- -- @param s The string being scanned.
- -- @param startPos The starting position of the scan.
- -- @return table, int The scanned object as a table and the position of the next character to scan.
- function decode_scanObject(s, startPos)
- local object = {}
- local stringLen = string.len(s)
- local key, value,objectType
- assert(
- string.sub(s, startPos, startPos) == "{",
- "decode_scanObject called but object does not start at position " .. startPos .. " in string:\n" .. s
- )
- startPos = startPos + 1
- repeat
- startPos = decode_scanWhitespace(s, startPos)
- assert(startPos <= stringLen, "JSON string ended unexpectedly while scanning object.")
- local curChar = string.sub(s, startPos, startPos)
- if (curChar == "}") then
- object = setObjectType(object,objectType)
- return object, startPos + 1
- end
- if (curChar == ",") then
- startPos = decode_scanWhitespace(s, startPos + 1)
- end
- assert(startPos <= stringLen, "JSON string ended unexpectedly scanning object.")
- -- Scan the key
- key, startPos = json.decode(s, startPos)
- assert(startPos <= stringLen, "JSON string ended unexpectedly searching for value of key " .. key)
- startPos = decode_scanWhitespace(s, startPos)
- assert(startPos <= stringLen, "JSON string ended unexpectedly searching for value of key " .. key)
- assert(string.sub(s, startPos, startPos) == ":", "JSON object key-value assignment mal-formed at " .. startPos)
- startPos = decode_scanWhitespace(s, startPos + 1)
- assert(startPos <= stringLen, "JSON string ended unexpectedly searching for value of key " .. key)
- value, startPos = json.decode(s, startPos)
- if key =="_t" then
- objectType = value
- else
- object[key] = value
- end
- until false -- infinite loop while key-value pairs are found
- end
- function setObjectType(t, typeStr)
- if not typeStr then
- return t
- end
- t = setClassType(t, typeStr)
- return t
- end
- local getGenericTtype
- function setClassType(t, name)
- if not t then
- return nil
- end
- local meta = System.getClass(name)
- if meta then
- return setmetatable(t, meta)
- else
- local obj
- local mapMeta = json_private.attributeMap[name]
- if mapMeta then
- obj = setmetatable(t, mapMeta)
- else
- mapMeta = getGenericTtype(name)
- json_private.attributeMap[name] = mapMeta
- obj = setmetatable(t, mapMeta)
- end
- --后处理
- if (mapMeta.__genericT__) then
- if (mapMeta.__genericTKey__) then
- if (mapMeta.__genericTValue__) then
- --dictionary
- if (mapMeta.__genericTKey__.__name__ ~= "System.String") then
- local newT = {}
- for k, v in pairs(obj) do
- local i = tonumber(k)
- newT[i] = v
- end
- obj = setmetatable(newT, mapMeta)
- end
- else
- --hashset
- local newT = {}
- newT = setmetatable(newT, mapMeta)
- for _, v in pairs(obj) do
- newT:Add(v)
- end
- obj = newT
- end
- else
- --array/list
- end
- end
- return obj
- end
- end
- getGenericTtype = function(name)
- avoidLoopOver()
- local typeBase, typeT
- local arr = name:Split("`", 2)
- if (#arr > 1) then
- typeBase = arr[1]
- local s = arr[2]
- typeT = string.sub(s, string.find(s, "%[") + 1, string.find(s, "%]$") - 1)
- local tArr = typeT:Split(",")
- local tTable = {}
- for k, v in pairs(tArr) do
- tTable[k] = getGenericTtype(v)
- end
- return System.getClass(typeBase)(table.unpack(tTable))
- else
- if (name:EndsWith("[]")) then
- typeBase = "System.Array"
- typeT = string.sub(name, 1, string.find(name, "%[") - 1)
- return System.getClass(typeBase)(getGenericTtype(typeT))
- else
- return System.getClass(name)
- end
- end
- end
- -- START SoniEx2
- -- Initialize some things used by decode_scanString
- -- You know, for efficiency
- local escapeSequences = {
- ["\\t"] = "\t",
- ["\\f"] = "\f",
- ["\\r"] = "\r",
- ["\\n"] = "\n",
- ["\\b"] = "\b"
- }
- setmetatable(
- escapeSequences,
- {
- __index = function(t, k)
- -- skip "\" aka strip escape
- return string.sub(k, 2)
- end
- }
- )
- -- END SoniEx2
- --- Scans a JSON string from the opening inverted comma or single quote to the
- -- end of the string.
- -- Returns the string extracted as a Lua string,
- -- and the position of the next non-string character
- -- (after the closing inverted comma or single quote).
- -- @param s The string being scanned.
- -- @param startPos The starting position of the scan.
- -- @return string, int The extracted string as a Lua string, and the next character to parse.
- function decode_scanString(s, startPos)
- assert(startPos, "decode_scanString(..) called without start position")
- local startChar = string.sub(s, startPos, startPos)
- -- START SoniEx2
- -- PS: I don't think single quotes are valid JSON
- assert(startChar == [["]] or startChar == [[']], "decode_scanString called for a non-string")
- --assert(startPos, "String decoding failed: missing closing " .. startChar .. " for string at position " .. oldStart)
- local t = {}
- local i, j = startPos, startPos
- while string.find(s, startChar, j + 1) ~= j + 1 do
- local oldj = j
- i, j = string.find(s, "\\.", j + 1)
- local x, y = string.find(s, startChar, oldj + 1)
- if not i or x < i then
- i, j = x, y - 1
- end
- table.insert(t, string.sub(s, oldj + 1, i - 1))
- if string.sub(s, i, j) == "\\u" then
- local a = string.sub(s, j + 1, j + 4)
- j = j + 4
- local n = tonumber(a, 16)
- assert(n, "String decoding failed: bad Unicode escape " .. a .. " at position " .. i .. " : " .. j)
- -- math.floor(x/2^y) == lazy right shift
- -- a % 2^b == bitwise_and(a, (2^b)-1)
- -- 64 = 2^6
- -- 4096 = 2^12 (or 2^6 * 2^6)
- local x
- if n < 0x80 then
- x = string.char(n % 0x80)
- elseif n < 0x800 then
- -- [110x xxxx] [10xx xxxx]
- x = string.char(0xC0 + (math.floor(n / 64) % 0x20), 0x80 + (n % 0x40))
- else
- -- [1110 xxxx] [10xx xxxx] [10xx xxxx]
- x =
- string.char(
- 0xE0 + (math.floor(n / 4096) % 0x10),
- 0x80 + (math.floor(n / 64) % 0x40),
- 0x80 + (n % 0x40)
- )
- end
- table.insert(t, x)
- else
- table.insert(t, escapeSequences[string.sub(s, i, j)])
- end
- end
- table.insert(t, string.sub(j, j + 1))
- assert(
- string.find(s, startChar, j + 1),
- "String decoding failed: missing closing " ..
- startChar .. " at position " .. j .. "(for string at position " .. startPos .. ")"
- )
- return table.concat(t, ""), j + 2
- -- END SoniEx2
- end
- --- Scans a JSON string skipping all whitespace from the current start position.
- -- Returns the position of the first non-whitespace character, or nil if the whole end of string is reached.
- -- @param s The string being scanned
- -- @param startPos The starting position where we should begin removing whitespace.
- -- @return int The first position where non-whitespace was encountered, or string.len(s)+1 if the end of string
- -- was reached.
- function decode_scanWhitespace(s, startPos)
- local whitespace = " \n\r\t"
- local stringLen = string.len(s)
- while (string.find(whitespace, string.sub(s, startPos, startPos), 1, true) and startPos <= stringLen) do
- startPos = startPos + 1
- end
- return startPos
- end
- --- Encodes a string to be JSON-compatible.
- -- This just involves back-quoting inverted commas, back-quotes and newlines, I think ;-)
- -- @param s The string to return as a JSON encoded (i.e. backquoted string)
- -- @return The string appropriately escaped.
- local escapeList = {
- ['"'] = '\\"',
- ["\\"] = "\\\\",
- ["/"] = "\\/",
- ["\b"] = "\\b",
- ["\f"] = "\\f",
- ["\n"] = "\\n",
- ["\r"] = "\\r",
- ["\t"] = "\\t"
- }
- function json_private.encodeString(s)
- local s = tostring(s)
- return s:gsub(
- ".",
- function(c)
- return escapeList[c]
- end
- ) -- SoniEx2: 5.0 compat
- end
- -- Determines whether the given Lua type is an array or a table / dictionary.
- -- We consider any table an array if it has indexes 1..n for its n items, and no
- -- other data in the table.
- -- I think this method is currently a little 'flaky', but can't think of a good way around it yet...
- -- @param t The table to evaluate as an array
- -- @return boolean, number True if the table can be represented as an array, false otherwise. If true,
- -- the second returned value is the maximum
- -- number of indexed elements in the array.
- function isArray(t)
- -- Next we count all the elements, ensuring that any non-indexed elements are not-encodable
- -- (with the possible exception of 'n')
- if (t == json.EMPTY_ARRAY) then
- return true, 0
- end
- if (t == json.EMPTY_OBJECT) then
- return false
- end
- local maxIndex = 0
- for k, v in pairs(t) do
- if (type(k) == "number" and math.floor(k) == k and 1 <= k) then -- k,v is an indexed pair
- if (not isEncodable(v)) then
- return false
- end -- All array elements must be encodable
- maxIndex = math.max(maxIndex, k)
- else
- if (k == "n") then
- if v ~= (t.n or #t) then
- return false
- end -- False if n does not hold the number of elements
- else -- Else of (k=='n')
- if isEncodable(v) then
- return false
- end
- end -- End of (k~='n')
- end -- End of k,v not an indexed pair
- end -- End of loop across all pairs
- return true, maxIndex
- end
- --- Determines whether the given Lua object / table / variable can be JSON encoded. The only
- -- types that are JSON encodable are: string, boolean, number, nil, table and json.null.
- -- In this implementation, all other types are ignored.
- -- @param o The object to examine.
- -- @return boolean True if the object should be JSON encoded, false if it should be ignored.
- function isEncodable(o)
- local t = type(o)
- return (t == "string" or t == "boolean" or t == "number" or t == "nil" or t == "table") or
- (t == "function" and o == json.null)
- end
- function table.unique(t, bArray)
- local check = {}
- local n = {}
- local idx = 1
- for k, v in pairs(t) do
- if not check[v] then
- if bArray then
- n[idx] = v
- idx = idx + 1
- else
- n[k] = v
- end
- check[v] = true
- end
- end
- return n
- end
- function IsVlaueInTable(t, v)
- if not t then
- return false
- end
- for _, vv in pairs(t) do
- if vv == v then
- return true
- end
- end
- return false
- end
- function IsKeyInTable(t, key)
- return not t and not t[key]
- end
- return json
|