-
Star
(174)
You must be signed in to star a gist -
Fork
(36)
You must be signed in to fork a gist
-
-
Save tylerneylon/59f4bcf316be525b30ab to your computer and use it in GitHub Desktop.
| --[[ json.lua | |
| A compact pure-Lua JSON library. | |
| The main functions are: json.stringify, json.parse. | |
| ## json.stringify: | |
| This expects the following to be true of any tables being encoded: | |
| * They only have string or number keys. Number keys must be represented as | |
| strings in json; this is part of the json spec. | |
| * They are not recursive. Such a structure cannot be specified in json. | |
| A Lua table is considered to be an array if and only if its set of keys is a | |
| consecutive sequence of positive integers starting at 1. Arrays are encoded like | |
| so: `[2, 3, false, "hi"]`. Any other type of Lua table is encoded as a json | |
| object, encoded like so: `{"key1": 2, "key2": false}`. | |
| Because the Lua nil value cannot be a key, and as a table value is considerd | |
| equivalent to a missing key, there is no way to express the json "null" value in | |
| a Lua table. The only way this will output "null" is if your entire input obj is | |
| nil itself. | |
| An empty Lua table, {}, could be considered either a json object or array - | |
| it's an ambiguous edge case. We choose to treat this as an object as it is the | |
| more general type. | |
| To be clear, none of the above considerations is a limitation of this code. | |
| Rather, it is what we get when we completely observe the json specification for | |
| as arbitrary a Lua object as json is capable of expressing. | |
| ## json.parse: | |
| This function parses json, with the exception that it does not pay attention to | |
| \u-escaped unicode code points in strings. | |
| It is difficult for Lua to return null as a value. In order to prevent the loss | |
| of keys with a null value in a json string, this function uses the one-off | |
| table value json.null (which is just an empty table) to indicate null values. | |
| This way you can check if a value is null with the conditional | |
| `val == json.null`. | |
| If you have control over the data and are using Lua, I would recommend just | |
| avoiding null values in your data to begin with. | |
| --]] | |
| local json = {} | |
| -- Internal functions. | |
| local function kind_of(obj) | |
| if type(obj) ~= 'table' then return type(obj) end | |
| local i = 1 | |
| for _ in pairs(obj) do | |
| if obj[i] ~= nil then i = i + 1 else return 'table' end | |
| end | |
| if i == 1 then return 'table' else return 'array' end | |
| end | |
| local function escape_str(s) | |
| local in_char = {'\\', '"', '/', '\b', '\f', '\n', '\r', '\t'} | |
| local out_char = {'\\', '"', '/', 'b', 'f', 'n', 'r', 't'} | |
| for i, c in ipairs(in_char) do | |
| s = s:gsub(c, '\\' .. out_char[i]) | |
| end | |
| return s | |
| end | |
| -- Returns pos, did_find; there are two cases: | |
| -- 1. Delimiter found: pos = pos after leading space + delim; did_find = true. | |
| -- 2. Delimiter not found: pos = pos after leading space; did_find = false. | |
| -- This throws an error if err_if_missing is true and the delim is not found. | |
| local function skip_delim(str, pos, delim, err_if_missing) | |
| pos = pos + #str:match('^%s*', pos) | |
| if str:sub(pos, pos) ~= delim then | |
| if err_if_missing then | |
| error('Expected ' .. delim .. ' near position ' .. pos) | |
| end | |
| return pos, false | |
| end | |
| return pos + 1, true | |
| end | |
| -- Expects the given pos to be the first character after the opening quote. | |
| -- Returns val, pos; the returned pos is after the closing quote character. | |
| local function parse_str_val(str, pos, val) | |
| val = val or '' | |
| local early_end_error = 'End of input found while parsing string.' | |
| if pos > #str then error(early_end_error) end | |
| local c = str:sub(pos, pos) | |
| if c == '"' then return val, pos + 1 end | |
| if c ~= '\\' then return parse_str_val(str, pos + 1, val .. c) end | |
| -- We must have a \ character. | |
| local esc_map = {b = '\b', f = '\f', n = '\n', r = '\r', t = '\t'} | |
| local nextc = str:sub(pos + 1, pos + 1) | |
| if not nextc then error(early_end_error) end | |
| return parse_str_val(str, pos + 2, val .. (esc_map[nextc] or nextc)) | |
| end | |
| -- Returns val, pos; the returned pos is after the number's final character. | |
| local function parse_num_val(str, pos) | |
| local num_str = str:match('^-?%d+%.?%d*[eE]?[+-]?%d*', pos) | |
| local val = tonumber(num_str) | |
| if not val then error('Error parsing number at position ' .. pos .. '.') end | |
| return val, pos + #num_str | |
| end | |
| -- Public values and functions. | |
| function json.stringify(obj, as_key) | |
| local s = {} -- We'll build the string as an array of strings to be concatenated. | |
| local kind = kind_of(obj) -- This is 'array' if it's an array or type(obj) otherwise. | |
| if kind == 'array' then | |
| if as_key then error('Can\'t encode array as key.') end | |
| s[#s + 1] = '[' | |
| for i, val in ipairs(obj) do | |
| if i > 1 then s[#s + 1] = ', ' end | |
| s[#s + 1] = json.stringify(val) | |
| end | |
| s[#s + 1] = ']' | |
| elseif kind == 'table' then | |
| if as_key then error('Can\'t encode table as key.') end | |
| s[#s + 1] = '{' | |
| for k, v in pairs(obj) do | |
| if #s > 1 then s[#s + 1] = ', ' end | |
| s[#s + 1] = json.stringify(k, true) | |
| s[#s + 1] = ':' | |
| s[#s + 1] = json.stringify(v) | |
| end | |
| s[#s + 1] = '}' | |
| elseif kind == 'string' then | |
| return '"' .. escape_str(obj) .. '"' | |
| elseif kind == 'number' then | |
| if as_key then return '"' .. tostring(obj) .. '"' end | |
| return tostring(obj) | |
| elseif kind == 'boolean' then | |
| return tostring(obj) | |
| elseif kind == 'nil' then | |
| return 'null' | |
| else | |
| error('Unjsonifiable type: ' .. kind .. '.') | |
| end | |
| return table.concat(s) | |
| end | |
| json.null = {} -- This is a one-off table to represent the null value. | |
| function json.parse(str, pos, end_delim) | |
| pos = pos or 1 | |
| if pos > #str then error('Reached unexpected end of input.') end | |
| local pos = pos + #str:match('^%s*', pos) -- Skip whitespace. | |
| local first = str:sub(pos, pos) | |
| if first == '{' then -- Parse an object. | |
| local obj, key, delim_found = {}, true, true | |
| pos = pos + 1 | |
| while true do | |
| key, pos = json.parse(str, pos, '}') | |
| if key == nil then return obj, pos end | |
| if not delim_found then error('Comma missing between object items.') end | |
| pos = skip_delim(str, pos, ':', true) -- true -> error if missing. | |
| obj[key], pos = json.parse(str, pos) | |
| pos, delim_found = skip_delim(str, pos, ',') | |
| end | |
| elseif first == '[' then -- Parse an array. | |
| local arr, val, delim_found = {}, true, true | |
| pos = pos + 1 | |
| while true do | |
| val, pos = json.parse(str, pos, ']') | |
| if val == nil then return arr, pos end | |
| if not delim_found then error('Comma missing between array items.') end | |
| arr[#arr + 1] = val | |
| pos, delim_found = skip_delim(str, pos, ',') | |
| end | |
| elseif first == '"' then -- Parse a string. | |
| return parse_str_val(str, pos + 1) | |
| elseif first == '-' or first:match('%d') then -- Parse a number. | |
| return parse_num_val(str, pos) | |
| elseif first == end_delim then -- End of an object or array. | |
| return nil, pos + 1 | |
| else -- Parse true, false, or null. | |
| local literals = {['true'] = true, ['false'] = false, ['null'] = json.null} | |
| for lit_str, lit_val in pairs(literals) do | |
| local lit_end = pos + #lit_str - 1 | |
| if str:sub(pos, lit_end) == lit_str then return lit_val, lit_end + 1 end | |
| end | |
| local pos_info_str = 'position ' .. pos .. ': ' .. str:sub(pos, pos + 10) | |
| error('Invalid json syntax starting at ' .. pos_info_str) | |
| end | |
| end | |
| return json |
@tylerneylon I have added support for all lua versions, lua5.1, lua5.3 and lua5.4 and a c api too. My goal here is not to replace JS or even challenge it on the front end. The goal is to bridge the gap between the front-end and back-end, so that a lua script or lua backend, can send, receive, and process json to and from the front end on the fly with nothing in between. The biggest benefit is that these new types play nice with all native lua types. This allows us to have objects and arrays of tables, functions, threads ..etc. Only printable items will render, the rest are silently ignored
local JSON = require "JSON"
local t1 = {1,2,3,4,5}
local t2 = {4,5,6,7,8}
local t3 = {"nine", "ten", "eleven", "twelve"}
local t_arr = JSON:array(t1, t2, t3)
print(t_arr:tojson()) --> [[1,2,3,4,5],[4,5,6,7,8],["nine","ten","eleven","twelve"]]
t_arr:move(0,1)
print(t_arr:tojson()) --> [[4,5,6,7,8],[1,2,3,4,5],["nine","ten","eleven","twelve"]]
t_arr:reverse(1,2)
print(t_arr:tojson()) --> [[4,5,6,7,8],["nine","ten","eleven","twelve"],[1,2,3,4,5]]
t_arr:reverse(0,1)
print(t_arr:tojson()) --> [["nine","ten","eleven","twelve"],[4,5,6,7,8],[1,2,3,4,5]]
t_arr:reverse()
print(t_arr:tojson()) --> [[1,2,3,4,5],[4,5,6,7,8],["nine","ten","eleven","twelve"]]
-- add some = data to t1
t1[#t1+1] = "some new data"
-- check it in the json
print(t_arr:tojson()) --> [[1,2,3,4,5,"some new data"],[4,5,6,7,8],["nine","ten","eleven","twelve"]]
--shift t1 from the array
shifted = t_arr:shift()
print(shifted) --> table: 0x59763d7a73b0
print(t1) --> table: 0x59763d7a73b0
-- check it in the json
print(t_arr:tojson()) --> [[4,5,6,7,8],["nine","ten","eleven","twelve"]]also its been pointed out my math was wrong when i referenced performance..
parse 30mb of json data (an array of 100 user objects) -->
Total Parse Time: 0.080 ms
Per Full Array Parse: (29939 bytes) 0.0801 ms
roughly 374 MB per second on an embedded processor (gl-inet-b3000) 🥱
here is the mixed tables example aswell
local JSON = require "JSON"
-- create a mixed table
local t = {1,2,3,4, some="data", age=99, root=false}
print(t) --> table: 0x4c14380
-- parse t with no args, defaults to type with most items (array in this case --> arr 4 | obj 3)
local arr = JSON.parse_table(arr, t) --> alt syntax JSON:parse_table(t)
print(arr) --> array: 0x4c14f00
print(arr:tojson()) --> [1,2,3,4,{"root":false,"age":99,"some":"data"}]
-- parse t as an object
local obj = JSON.parse_table(obj, t, "-o") --> alt syntax JSON:parse_table(t, "-o")
print(obj) --> object: 0x4c17aa0
print(obj:tojson()) --> {"root":false,"age":99,"some":"data","mixed_keys":[1,2,3,4]}
-- parse t as an object, user supplied key for mixed items ( "arr" in this case )
local obj = JSON.parse_table(obj, t, "-o", "arr") --> alt syntax JSON:parse_table(t, "-o", "arr")
print(obj) --> object: 0x4c1a790
print(obj:tojson()) --> {"root":false,"age":99,"some":"data","arr":[1,2,3,4]}
-- parse t as an array, set verbose output
arr = JSON.parse_table(arr, t, "-a-v") --> alt syntax JSON:parse_table(t, "-a-v")
--> Result:[ type: array | object len: 3 | array len: 4 | mixed: true | fixups: 0 ]
print(arr) --> array: 0x4c1a6d0
print(arr:tojson()) --> [1,2,3,4,{"root":false,"age":99,"some":"data"}]
-- parse t as array with no mixed items ( no keyed items)
arr = JSON.parse_table(arr, t, "-a", true) --> alt syntax JSON:parse_table(t, "-a", true)
print(arr) --> array: 0x4c1c200
print(arr:tojson()) --> [1,2,3,4]
-- parse t as an object no mixed items ( no keyless items)
local obj = JSON.parse_table(obj, t, "-o", true) --> alt syntax JSON:parse_table(t, "-o", true)
print(obj) --> object: 0x4c1cbf0
print(obj:tojson()) --> {"root":false,"age":99,"some":"data"}
Hi @TheRootED24 ! Thanks for the nice comments — I'm happy to hear the videos were useful!
I took a quick look at LuaJson5.1. This looks like a more serious JSON design than the small gist (json.lua) on this page! JSON is such an overwhelmingly useful standard that it opens doors to many implementations. Nice work on that.
Js and Lua are fairly similar languages. Js itself has evolved, but early versions (of js) were much worse than Lua, and years ago I wished that Lua were the default language for browsers instead. Nowadays I accept that js has taken its place in the world, somewhat by luck, and that it's nice that at least we have a browser-universal language at all. I mention all this because I appreciate efforts to bridge the two languages!