throwawayrobot

lib/json.lua

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