Module:Jf-JSON explained

-- -*- coding: utf-8 -*----- Simple JSON encoding and decoding in pure Lua.---- Copyright 2010-2016 Jeffrey Friedl-- http://regex.info/blog/-- Latest version: http://regex.info/blog/lua/json---- This code is released under a Creative Commons CC-BY "Attribution" License:-- http://creativecommons.org/licenses/by/3.0/deed.en_US---- It can be used for any purpose so long as:-- 1) the copyright notice above is maintained-- 2) the web-page links above are maintained-- 3) the 'AUTHOR_NOTE' string below is maintained--local VERSION = '20161109.21' -- version history at end of filelocal AUTHOR_NOTE = "-[JSON.lua package by Jeffrey Friedl (http://regex.info/blog/lua/json) version 20161109.21 ]-"

---- The 'AUTHOR_NOTE' variable exists so that information about the source-- of the package is maintained even in compiled versions. It's also-- included in OBJDEF below mostly to quiet warnings about unused variables.--local OBJDEF =

---- Simple JSON encoding and decoding in pure Lua.-- JSON definition: http://www.json.org/------ JSON = assert(loadfile "JSON.lua") -- one-time load of the routines---- local lua_value = JSON:decode(raw_json_text)---- local raw_json_text = JSON:encode(lua_table_or_value)-- local pretty_json_text = JSON:encode_pretty(lua_table_or_value) -- "pretty printed" version for human readability-------- DECODING (from a JSON string to a Lua table)------ JSON = assert(loadfile "JSON.lua") -- one-time load of the routines---- local lua_value = JSON:decode(raw_json_text)---- If the JSON text is for an object or an array, e.g.-- -- or-- ["Larry", "Curly", "Moe" ]---- the result is a Lua table, e.g.-- -- or-- ------ The encode and decode routines accept an optional second argument,-- "etc", which is not used during encoding or decoding, but upon error-- is passed along to error handlers. It can be of any type (including nil).-------- ERROR HANDLING---- With most errors during decoding, this code calls---- JSON:onDecodeError(message, text, location, etc)---- with a message about the error, and if known, the JSON text being-- parsed and the byte count where the problem was discovered. You can-- replace the default JSON:onDecodeError with your own function.---- The default onDecodeError merely augments the message with data-- about the text and the location if known (and if a second 'etc'-- argument had been provided to decode, its value is tacked onto the-- message as well), and then calls JSON.assert, which itself defaults-- to Lua's built-in assert, and can also be overridden.---- For example, in an Adobe Lightroom plugin, you might use something like---- function JSON:onDecodeError(message, text, location, etc)-- LrErrors.throwUserError("Internal Error: invalid JSON data")-- end---- or even just---- function JSON.assert(message)-- LrErrors.throwUserError("Internal Error: " .. message)-- end---- If JSON:decode is passed a nil, this is called instead:---- JSON:onDecodeOfNilError(message, nil, nil, etc)---- and if JSON:decode is passed HTML instead of JSON, this is called:---- JSON:onDecodeOfHTMLError(message, text, nil, etc)---- The use of the fourth 'etc' argument allows stronger coordination-- between decoding and error reporting, especially when you provide your-- own error-handling routines. Continuing with the the Adobe Lightroom-- plugin example:---- function JSON:onDecodeError(message, text, location, etc)-- local note = "Internal Error: invalid JSON data"-- if type(etc) = 'table' and etc.photo then-- note = note .. " while processing for " .. etc.photo:getFormattedMetadata('fileName')-- end-- LrErrors.throwUserError(note)-- end---- :-- :---- for i, photo in ipairs(photosToProcess) do-- : -- : -- local data = JSON:decode(someJsonText,)-- : -- : -- end-------- If the JSON text passed to decode has trailing garbage (e.g. as with the JSON "[123]xyzzy"),-- the method---- JSON:onTrailingGarbage(json_text, location, parsed_value, etc)---- is invoked, where:---- json_text is the original JSON text being parsed,-- location is the count of bytes into json_text where the garbage starts (6 in the example),-- parsed_value is the Lua result of what was successfully parsed (in the example),-- etc is as above.---- If JSON:onTrailingGarbage does not abort, it should return the value decode should return,-- or nil + an error message.---- local new_value, error_message = JSON:onTrailingGarbage---- The default handler just invokes JSON:onDecodeError("trailing garbage"...), but you can have-- this package ignore trailing garbage via---- function JSON:onTrailingGarbage(json_text, location, parsed_value, etc)-- return parsed_value-- end------ DECODING AND STRICT TYPES---- Because both JSON objects and JSON arrays are converted to Lua tables,-- it's not normally possible to tell which original JSON type a-- particular Lua table was derived from, or guarantee decode-encode-- round-trip equivalency.---- However, if you enable strictTypes, e.g.---- JSON = assert(loadfile "JSON.lua") --load the routines-- JSON.strictTypes = true---- then the Lua table resulting from the decoding of a JSON object or-- JSON array is marked via Lua metatable, so that when re-encoded with-- JSON:encode it ends up as the appropriate JSON type.---- (This is not the default because other routines may not work well with-- tables that have a metatable set, for example, Lightroom API calls.)------ ENCODING (from a lua table to a JSON string)---- JSON = assert(loadfile "JSON.lua") -- one-time load of the routines---- local raw_json_text = JSON:encode(lua_table_or_value)-- local pretty_json_text = JSON:encode_pretty(lua_table_or_value) -- "pretty printed" version for human readability-- local custom_pretty = JSON:encode(lua_table_or_value, etc,)---- On error during encoding, this code calls:---- JSON:onEncodeError(message, etc)---- which you can override in your local JSON object.---- The 'etc' in the error call is the second argument to encode-- and encode_pretty, or nil if it wasn't provided.------ ENCODING OPTIONS---- An optional third argument, a table of options, can be provided to encode.---- encode_options = -- -- json_string = JSON:encode(mytable, etc, encode_options)-------- For reference, the defaults are:---- pretty = false-- null = nil,-- stringsAreUtf8 = false,-------- PRETTY-PRINTING---- Enabling the 'pretty' encode option helps generate human-readable JSON.---- pretty = JSON:encode(val, etc,)---- encode_pretty is also provided: it's identical to encode except-- that encode_pretty provides a default options table if none given in the call:---- ---- For example, if---- JSON:encode(data)---- produces:---- ---- then---- JSON:encode_pretty(data)---- produces:---- ---- The following three lines return identical results:-- JSON:encode_pretty(data)-- JSON:encode_pretty(data, nil,)-- JSON:encode (data, nil,)---- An example of setting your own indent string:---- JSON:encode_pretty(data, nil,)---- produces:---- ---- An example of setting align_keys to true:---- JSON:encode_pretty(data, nil,)-- -- produces:-- -- ---- which I must admit is kinda ugly, sorry. This was the default for-- encode_pretty prior to version 20141223.14.------ HANDLING UNICODE LINE AND PARAGRAPH SEPARATORS FOR JAVA---- If the 'stringsAreUtf8' encode option is set to true, consider Lua strings not as a sequence of bytes,-- but as a sequence of UTF-8 characters.---- Currently, the only practical effect of setting this option is that Unicode LINE and PARAGRAPH-- separators, if found in a string, are encoded with a JSON escape instead of being dumped as is.-- The JSON is valid either way, but encoding this way, apparently, allows the resulting JSON-- to also be valid Java.---- AMBIGUOUS SITUATIONS DURING THE ENCODING---- During the encode, if a Lua table being encoded contains both string-- and numeric keys, it fits neither JSON's idea of an object, nor its-- idea of an array. To get around this, when any string key exists (or-- when non-positive numeric keys exist), numeric keys are converted to-- strings.---- For example, -- JSON:encode)-- produces the JSON object-- ---- To prohibit this conversion and instead make it an error condition, set-- JSON.noKeyConversion = true------ ENCODING JSON NULL VALUES---- Lua tables completely omit keys whose value is nil, so without special handling there's-- no way to get a field in a JSON object with a null value. For example-- JSON:encode-- produces-- ---- In order to actually produce-- -- one can include a string value for a "null" field in the options table passed to encode.... -- any Lua table entry with that value becomes null in the JSON output:-- JSON:encode(nil,)-- produces-- ---- Just be sure to use a string that is otherwise unlikely to appear in your data.-- The string "\0" (a string with one null byte) may well be appropriate for many applications.---- The "null" options also applies to Lua tables that become JSON arrays.-- JSON:encode-- produces-- ["one","two"]-- while-- NULL = "\0"-- JSON:encode(nil,)-- produces-- ["one","two",null,null]---------- HANDLING LARGE AND/OR PRECISE NUMBERS------ Without special handling, numbers in JSON can lose precision in Lua.-- For example:-- -- T = JSON:decode('')---- print("small: ", type(T.small), T.small)-- print("big: ", type(T.big), T.big)-- print("precise: ", type(T.precise), T.precise)-- -- produces-- -- small: number 12345-- big: number 1.2345678901235e+28-- precise: number 9876.6789012346---- Precision is lost with both 'big' and 'precise'.---- This package offers ways to try to handle this better (for some definitions of "better")...---- The most precise method is by setting the global:-- -- JSON.decodeNumbersAsObjects = true-- -- When this is set, numeric JSON data is encoded into Lua in a form that preserves the exact-- JSON numeric presentation when re-encoded back out to JSON, or accessed in Lua as a string.---- (This is done by encoding the numeric data with a Lua table/metatable that returns-- the possibly-imprecise numeric form when accessed numerically, but the original precise-- representation when accessed as a string. You can also explicitly access-- via JSON:forceString and JSON:forceNumber)---- Consider the example above, with this option turned on:---- JSON.decodeNumbersAsObjects = true-- -- T = JSON:decode('')---- print("small: ", type(T.small), T.small)-- print("big: ", type(T.big), T.big)-- print("precise: ", type(T.precise), T.precise)-- -- This now produces:-- -- small: table 12345-- big: table 12345678901234567890123456789-- precise: table 9876.67890123456789012345-- -- However, within Lua you can still use the values (e.g. T.precise in the example above) in numeric-- contexts. In such cases you'll get the possibly-imprecise numeric version, but in string contexts-- and when the data finds its way to this package's encode function, the original full-precision-- representation is used.---- Even without using the JSON.decodeNumbersAsObjects option, you can encode numbers-- in your Lua table that retain high precision upon encoding to JSON, by using the JSON:asNumber-- function:---- T = ---- print(JSON:encode_pretty(T))---- This produces:---- -------- A different way to handle big/precise JSON numbers is to have decode merely return-- the exact string representation of the number instead of the number itself.-- This approach might be useful when the numbers are merely some kind of opaque-- object identifier and you want to work with them in Lua as strings anyway.-- -- This approach is enabled by setting---- JSON.decodeIntegerStringificationLength = 10---- The value is the number of digits (of the integer part of the number) at which to stringify numbers.---- Consider our previous example with this option set to 10:---- JSON.decodeIntegerStringificationLength = 10-- -- T = JSON:decode('')---- print("small: ", type(T.small), T.small)-- print("big: ", type(T.big), T.big)-- print("precise: ", type(T.precise), T.precise)---- This produces:---- small: number 12345-- big: string 12345678901234567890123456789-- precise: number 9876.6789012346---- The long integer of the 'big' field is at least JSON.decodeIntegerStringificationLength digits-- in length, so it's converted not to a Lua integer but to a Lua string. Using a value of 0 or 1 ensures-- that all JSON numeric data becomes strings in Lua.---- Note that unlike-- JSON.decodeNumbersAsObjects = true-- this stringification is simple and unintelligent: the JSON number simply becomes a Lua string, and that's the end of it.-- If the string is then converted back to JSON, it's still a string. After running the code above, adding-- print(JSON:encode(T))-- produces-- -- which is unlikely to be desired.---- There's a comparable option for the length of the decimal part of a number:---- JSON.decodeDecimalStringificationLength---- This can be used alone or in conjunction with---- JSON.decodeIntegerStringificationLength---- to trip stringification on precise numbers with at least JSON.decodeIntegerStringificationLength digits after-- the decimal point.---- This example:---- JSON.decodeIntegerStringificationLength = 10-- JSON.decodeDecimalStringificationLength = 5---- T = JSON:decode('')-- -- print("small: ", type(T.small), T.small)-- print("big: ", type(T.big), T.big)-- print("precise: ", type(T.precise), T.precise)---- produces:---- small: number 12345-- big: string 12345678901234567890123456789-- precise: string 9876.67890123456789012345------------ SUMMARY OF METHODS YOU CAN OVERRIDE IN YOUR LOCAL LUA JSON OBJECT---- assert-- onDecodeError-- onDecodeOfNilError-- onDecodeOfHTMLError-- onTrailingGarbage-- onEncodeError---- If you want to create a separate Lua JSON object with its own error handlers,-- you can reload JSON.lua or use the :new method.-----------------------------------------------------------------------------

local default_pretty_indent = " "local default_pretty_options =

local isArray = isArray.__index = isArraylocal isObject = isObject.__index = isObject

function OBJDEF:newArray(tbl) return setmetatable(tbl or, isArray)end

function OBJDEF:newObject(tbl) return setmetatable(tbl or, isObject)end

local function getnum(op) return type(op)

'number' and op or op.Nend

local isNumber = isNumber.__index = isNumber

function OBJDEF:asNumber(item)

if getmetatable(item)

isNumber then -- it's already a JSON number object. return item elseif type(item)

'table' and type(item.S)

'string' and type(item.N)

'number' then -- it's a number-object table that lost its metatable, so give it one return setmetatable(item, isNumber) else -- the normal situation... given a number or a string representation of a number.... local holder = return setmetatable(holder, isNumber) endend

---- Given an item that might be a normal string or number, or might be an 'isNumber' object defined above,-- return the string version. This shouldn't be needed often because the 'isNumber' object should autoconvert-- to a string in most cases, but it's here to allow it to be forced when needed.--function OBJDEF:forceString(item) if type(item)

'table' and type(item.S)

'string' then return item.S else return tostring(item) endend

---- Given an item that might be a normal string or number, or might be an 'isNumber' object defined above,-- return the numeric version.--function OBJDEF:forceNumber(item) if type(item)

'table' and type(item.N)

'number' then return item.N else return tonumber(item) endend

local function unicode_codepoint_as_utf8(codepoint) -- -- codepoint is a number -- if codepoint <= 127 then return string.char(codepoint)

elseif codepoint <= 2047 then -- -- 110yyyxx 10xxxxxx <-- useful notation from http://en.wikipedia.org/wiki/Utf8 -- local highpart = math.floor(codepoint / 0x40) local lowpart = codepoint - (0x40 * highpart) return string.char(0xC0 + highpart, 0x80 + lowpart)

elseif codepoint <= 65535 then -- -- 1110yyyy 10yyyyxx 10xxxxxx -- local highpart = math.floor(codepoint / 0x1000) local remainder = codepoint - 0x1000 * highpart local midpart = math.floor(remainder / 0x40) local lowpart = remainder - 0x40 * midpart

highpart = 0xE0 + highpart midpart = 0x80 + midpart lowpart = 0x80 + lowpart

-- -- Check for an invalid character (thanks Andy R. at Adobe). -- See table 3.7, page 93, in http://www.unicode.org/versions/Unicode5.2.0/ch03.pdf#G28070 -- if (highpart

0xE0 and midpart < 0xA0) or (highpart

0xED and midpart > 0x9F) or (highpart

0xF0 and midpart < 0x90) or (highpart

0xF4 and midpart > 0x8F) then return "?" else return string.char(highpart, midpart, lowpart) end

else -- -- 11110zzz 10zzyyyy 10yyyyxx 10xxxxxx -- local highpart = math.floor(codepoint / 0x40000) local remainder = codepoint - 0x40000 * highpart local midA = math.floor(remainder / 0x1000) remainder = remainder - 0x1000 * midA local midB = math.floor(remainder / 0x40) local lowpart = remainder - 0x40 * midB

return string.char(0xF0 + highpart, 0x80 + midA, 0x80 + midB, 0x80 + lowpart) endend

function OBJDEF:onDecodeError(message, text, location, etc) if text then if location then message = string.format("%s at byte %d of: %s", message, location, text) else message = string.format("%s: %s", message, text) end end

if etc ~= nil then message = message .. " (" .. OBJDEF:encode(etc) .. ")" end

if self.assert then self.assert(false, message) else assert(false, message) endend

function OBJDEF:onTrailingGarbage(json_text, location, parsed_value, etc) return self:onDecodeError("trailing garbage", json_text, location, etc)end

OBJDEF.onDecodeOfNilError = OBJDEF.onDecodeErrorOBJDEF.onDecodeOfHTMLError = OBJDEF.onDecodeError

function OBJDEF:onEncodeError(message, etc) if etc ~= nil then message = message .. " (" .. OBJDEF:encode(etc) .. ")" end

if self.assert then self.assert(false, message) else assert(false, message) endend

local function grok_number(self, text, start, options) -- -- Grab the integer part -- local integer_part = text:match('^-?[1-9]%d*', start) or text:match("^-?0", start)

if not integer_part then self:onDecodeError("expected number", text, start, options.etc) return nil, start -- in case the error method doesn't abort, return something sensible end

local i = start + integer_part:len

-- -- Grab an optional decimal part -- local decimal_part = text:match('^%.%d+', i) or ""

i = i + decimal_part:len

-- -- Grab an optional exponential part -- local exponent_part = text:match('^[eE][-+]?%d+', i) or ""

i = i + exponent_part:len

local full_number_text = integer_part .. decimal_part .. exponent_part

if options.decodeNumbersAsObjects then return OBJDEF:asNumber(full_number_text), i end

-- -- If we're told to stringify under certain conditions, so do. -- We punt a bit when there's an exponent by just stringifying no matter what. -- I suppose we should really look to see whether the exponent is actually big enough one -- way or the other to trip stringification, but I'll be lazy about it until someone asks. -- if (options.decodeIntegerStringificationLength and (integer_part:len >= options.decodeIntegerStringificationLength or exponent_part:len > 0))

or

(options.decodeDecimalStringificationLength and (decimal_part:len >= options.decodeDecimalStringificationLength or exponent_part:len > 0)) then return full_number_text, i -- this returns the exact string representation seen in the original JSON end

local as_number = tonumber(full_number_text)

if not as_number then self:onDecodeError("bad number", text, start, options.etc) return nil, start -- in case the error method doesn't abort, return something sensible end

return as_number, iend

local function grok_string(self, text, start, options)

if text:sub(start,start) ~= '"' then self:onDecodeError("expected string's opening quote", text, start, options.etc) return nil, start -- in case the error method doesn't abort, return something sensible end

local i = start + 1 -- +1 to bypass the initial quote local text_len = text:len local VALUE = "" while i <= text_len do local c = text:sub(i,i) if c

'"' then return VALUE, i + 1 end if c ~= '\\' then VALUE = VALUE .. c i = i + 1 elseif text:match('^\\b', i) then VALUE = VALUE .. "\b" i = i + 2 elseif text:match('^\\f', i) then VALUE = VALUE .. "\f" i = i + 2 elseif text:match('^\\n', i) then VALUE = VALUE .. "\n" i = i + 2 elseif text:match('^\\r', i) then VALUE = VALUE .. "\r" i = i + 2 elseif text:match('^\\t', i) then VALUE = VALUE .. "\t" i = i + 2 else local hex = text:match('^\\u([0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF])', i) if hex then i = i + 6 -- bypass what we just read

-- We have a Unicode codepoint. It could be standalone, or if in the proper range and -- followed by another in a specific range, it'll be a two-code surrogate pair. local codepoint = tonumber(hex, 16) if codepoint >= 0xD800 and codepoint <= 0xDBFF then -- it's a hi surrogate... see whether we have a following low local lo_surrogate = text:match('^\\u([dD][cdefCDEF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF])', i) if lo_surrogate then i = i + 6 -- bypass the low surrogate we just read codepoint = 0x2400 + (codepoint - 0xD800) * 0x400 + tonumber(lo_surrogate, 16) else -- not a proper low, so we'll just leave the first codepoint as is and spit it out. end end VALUE = VALUE .. unicode_codepoint_as_utf8(codepoint)

else

-- just pass through what's escaped VALUE = VALUE .. text:match('^\\(.)', i) i = i + 2 end end end

self:onDecodeError("unclosed string", text, start, options.etc) return nil, start -- in case the error method doesn't abort, return something sensibleend

local function skip_whitespace(text, start)

local _, match_end = text:find("^[\n\r\t]+", start) -- http://www.ietf.org/rfc/rfc4627.txt Section 2 if match_end then return match_end + 1 else return start endend

local grok_one -- assigned later

local function grok_object(self, text, start, options)

if text:sub(start,start) ~= '' then return VALUE, i + 1 end

if text:sub(i, i) ~= ',' then self:onDecodeError("expected comma or '}'", text, i, options.etc) return nil, i -- in case the error method doesn't abort, return something sensible end

i = skip_whitespace(text, i + 1) end

self:onDecodeError("unclosed '