Brodur

json.lua

Apr 13th, 2019
99
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 7.18 KB | None | 0 0
  1. --[[ json.lua
  2.  
  3. A compact pure-Lua JSON library.
  4. The main functions are: json.stringify, json.parse.
  5.  
  6. ## json.stringify:
  7.  
  8. This expects the following to be true of any tables being encoded:
  9.  * They only have string or number keys. Number keys must be represented as
  10.    strings in json; this is part of the json spec.
  11.  * They are not recursive. Such a structure cannot be specified in json.
  12.  
  13. A Lua table is considered to be an array if and only if its set of keys is a
  14. consecutive sequence of positive integers starting at 1. Arrays are encoded like
  15. so: `[2, 3, false, "hi"]`. Any other type of Lua table is encoded as a json
  16. object, encoded like so: `{"key1": 2, "key2": false}`.
  17.  
  18. Because the Lua nil value cannot be a key, and as a table value is considerd
  19. equivalent to a missing key, there is no way to express the json "null" value in
  20. a Lua table. The only way this will output "null" is if your entire input obj is
  21. nil itself.
  22.  
  23. An empty Lua table, {}, could be considered either a json object or array -
  24. it's an ambiguous edge case. We choose to treat this as an object as it is the
  25. more general type.
  26.  
  27. To be clear, none of the above considerations is a limitation of this code.
  28. Rather, it is what we get when we completely observe the json specification for
  29. as arbitrary a Lua object as json is capable of expressing.
  30.  
  31. ## json.parse:
  32.  
  33. This function parses json, with the exception that it does not pay attention to
  34. \u-escaped unicode code points in strings.
  35.  
  36. It is difficult for Lua to return null as a value. In order to prevent the loss
  37. of keys with a null value in a json string, this function uses the one-off
  38. table value json.null (which is just an empty table) to indicate null values.
  39. This way you can check if a value is null with the conditional
  40. `val == json.null`.
  41.  
  42. If you have control over the data and are using Lua, I would recommend just
  43. avoiding null values in your data to begin with.
  44.  
  45. --]]
  46.  
  47.  
  48. local json = {}
  49.  
  50.  
  51. -- Internal functions.
  52.  
  53. local function kind_of(obj)
  54.   if type(obj) ~= 'table' then return type(obj) end
  55.   local i = 1
  56.   for _ in pairs(obj) do
  57.     if obj[i] ~= nil then i = i + 1 else return 'table' end
  58.   end
  59.   if i == 1 then return 'table' else return 'array' end
  60. end
  61.  
  62. local function escape_str(s)
  63.   local in_char  = {'\\', '"', '/', '\b', '\f', '\n', '\r', '\t'}
  64.   local out_char = {'\\', '"', '/',  'b',  'f',  'n',  'r',  't'}
  65.   for i, c in ipairs(in_char) do
  66.     s = s:gsub(c, '\\' .. out_char[i])
  67.   end
  68.   return s
  69. end
  70.  
  71. -- Returns pos, did_find; there are two cases:
  72. -- 1. Delimiter found: pos = pos after leading space + delim; did_find = true.
  73. -- 2. Delimiter not found: pos = pos after leading space;     did_find = false.
  74. -- This throws an error if err_if_missing is true and the delim is not found.
  75. local function skip_delim(str, pos, delim, err_if_missing)
  76.   pos = pos + #str:match('^%s*', pos)
  77.   if str:sub(pos, pos) ~= delim then
  78.     if err_if_missing then
  79.       error('Expected ' .. delim .. ' near position ' .. pos)
  80.     end
  81.     return pos, false
  82.   end
  83.   return pos + 1, true
  84. end
  85.  
  86. -- Expects the given pos to be the first character after the opening quote.
  87. -- Returns val, pos; the returned pos is after the closing quote character.
  88. local function parse_str_val(str, pos, val)
  89.   val = val or ''
  90.   local early_end_error = 'End of input found while parsing string.'
  91.   if pos > #str then error(early_end_error) end
  92.   local c = str:sub(pos, pos)
  93.   if c == '"'  then return val, pos + 1 end
  94.   if c ~= '\\' then return parse_str_val(str, pos + 1, val .. c) end
  95.   -- We must have a \ character.
  96.   local esc_map = {b = '\b', f = '\f', n = '\n', r = '\r', t = '\t'}
  97.   local nextc = str:sub(pos + 1, pos + 1)
  98.   if not nextc then error(early_end_error) end
  99.   return parse_str_val(str, pos + 2, val .. (esc_map[nextc] or nextc))
  100. end
  101.  
  102. -- Returns val, pos; the returned pos is after the number's final character.
  103. local function parse_num_val(str, pos)
  104.   local num_str = str:match('^-?%d+%.?%d*[eE]?[+-]?%d*', pos)
  105.   local val = tonumber(num_str)
  106.   if not val then error('Error parsing number at position ' .. pos .. '.') end
  107.   return val, pos + #num_str
  108. end
  109.  
  110.  
  111. -- Public values and functions.
  112.  
  113. function json.stringify(obj, as_key)
  114.   local s = {}  -- We'll build the string as an array of strings to be concatenated.
  115.   local kind = kind_of(obj)  -- This is 'array' if it's an array or type(obj) otherwise.
  116.   if kind == 'array' then
  117.     if as_key then error('Can\'t encode array as key.') end
  118.     s[#s + 1] = '['
  119.     for i, val in ipairs(obj) do
  120.       if i > 1 then s[#s + 1] = ', ' end
  121.       s[#s + 1] = json.stringify(val)
  122.     end
  123.     s[#s + 1] = ']'
  124.   elseif kind == 'table' then
  125.     if as_key then error('Can\'t encode table as key.') end
  126.     s[#s + 1] = '{'
  127.     for k, v in pairs(obj) do
  128.       if #s > 1 then s[#s + 1] = ', ' end
  129.       s[#s + 1] = json.stringify(k, true)
  130.       s[#s + 1] = ':'
  131.       s[#s + 1] = json.stringify(v)
  132.     end
  133.     s[#s + 1] = '}'
  134.   elseif kind == 'string' then
  135.     return '"' .. escape_str(obj) .. '"'
  136.   elseif kind == 'number' then
  137.     if as_key then return '"' .. tostring(obj) .. '"' end
  138.     return tostring(obj)
  139.   elseif kind == 'boolean' then
  140.     return tostring(obj)
  141.   elseif kind == 'nil' then
  142.     return 'null'
  143.   else
  144.     error('Unjsonifiable type: ' .. kind .. '.')
  145.   end
  146.   return table.concat(s)
  147. end
  148.  
  149. json.null = {}  -- This is a one-off table to represent the null value.
  150.  
  151. function json.parse(str, pos, end_delim)
  152.   pos = pos or 1
  153.   if pos > #str then error('Reached unexpected end of input.') end
  154.   local pos = pos + #str:match('^%s*', pos)  -- Skip whitespace.
  155.   local first = str:sub(pos, pos)
  156.   if first == '{' then  -- Parse an object.
  157.     local obj, key, delim_found = {}, true, true
  158.     pos = pos + 1
  159.     while true do
  160.       key, pos = json.parse(str, pos, '}')
  161.       if key == nil then return obj, pos end
  162.       if not delim_found then error('Comma missing between object items.') end
  163.       pos = skip_delim(str, pos, ':', true)  -- true -> error if missing.
  164.       obj[key], pos = json.parse(str, pos)
  165.       pos, delim_found = skip_delim(str, pos, ',')
  166.     end
  167.   elseif first == '[' then  -- Parse an array.
  168.     local arr, val, delim_found = {}, true, true
  169.     pos = pos + 1
  170.     while true do
  171.       val, pos = json.parse(str, pos, ']')
  172.       if val == nil then return arr, pos end
  173.       if not delim_found then error('Comma missing between array items.') end
  174.       arr[#arr + 1] = val
  175.       pos, delim_found = skip_delim(str, pos, ',')
  176.     end
  177.   elseif first == '"' then  -- Parse a string.
  178.     return parse_str_val(str, pos + 1)
  179.   elseif first == '-' or first:match('%d') then  -- Parse a number.
  180.     return parse_num_val(str, pos)
  181.   elseif first == end_delim then  -- End of an object or array.
  182.     return nil, pos + 1
  183.   else  -- Parse true, false, or null.
  184.     local literals = {['true'] = true, ['false'] = false, ['null'] = json.null}
  185.     for lit_str, lit_val in pairs(literals) do
  186.       local lit_end = pos + #lit_str - 1
  187.       if str:sub(pos, lit_end) == lit_str then return lit_val, lit_end + 1 end
  188.     end
  189.     local pos_info_str = 'position ' .. pos .. ': ' .. str:sub(pos, pos + 10)
  190.     error('Invalid json syntax starting at ' .. pos_info_str)
  191.   end
  192. end
  193.  
  194. return json
Add Comment
Please, Sign In to add comment