-- modules/json.lua - High-performance JSON module local json = {} function json.encode(value) local buffer = {} local pos = 1 local function encode_string(s) buffer[pos] = '"' pos = pos + 1 local start = 1 for i = 1, #s do local c = s:byte(i) if c == 34 then -- " if i > start then buffer[pos] = s:sub(start, i - 1) pos = pos + 1 end buffer[pos] = '\\"' pos = pos + 1 start = i + 1 elseif c == 92 then -- \ if i > start then buffer[pos] = s:sub(start, i - 1) pos = pos + 1 end buffer[pos] = '\\\\' pos = pos + 1 start = i + 1 elseif c < 32 then if i > start then buffer[pos] = s:sub(start, i - 1) pos = pos + 1 end if c == 8 then buffer[pos] = '\\b' elseif c == 9 then buffer[pos] = '\\t' elseif c == 10 then buffer[pos] = '\\n' elseif c == 12 then buffer[pos] = '\\f' elseif c == 13 then buffer[pos] = '\\r' else buffer[pos] = ('\\u%04x'):format(c) end pos = pos + 1 start = i + 1 end end if start <= #s then buffer[pos] = s:sub(start) pos = pos + 1 end buffer[pos] = '"' pos = pos + 1 end local function encode_value(v, depth) local t = type(v) if t == 'string' then encode_string(v) elseif t == 'number' then if v ~= v then -- NaN buffer[pos] = 'null' elseif v == 1/0 or v == -1/0 then -- Infinity buffer[pos] = 'null' else buffer[pos] = tostring(v) end pos = pos + 1 elseif t == 'boolean' then buffer[pos] = v and 'true' or 'false' pos = pos + 1 elseif t == 'table' then if depth > 100 then error('circular reference') end local is_array = true local max_index = 0 local count = 0 for k, _ in pairs(v) do count = count + 1 if type(k) ~= 'number' or k <= 0 or k % 1 ~= 0 then is_array = false break end if k > max_index then max_index = k end end if is_array and count == max_index then buffer[pos] = '[' pos = pos + 1 for i = 1, max_index do if i > 1 then buffer[pos] = ',' pos = pos + 1 end encode_value(v[i], depth + 1) end buffer[pos] = ']' pos = pos + 1 else buffer[pos] = '{' pos = pos + 1 local first = true for k, val in pairs(v) do if not first then buffer[pos] = ',' pos = pos + 1 end first = false encode_string(tostring(k)) buffer[pos] = ':' pos = pos + 1 encode_value(val, depth + 1) end buffer[pos] = '}' pos = pos + 1 end else buffer[pos] = 'null' pos = pos + 1 end end encode_value(value, 0) return table.concat(buffer) end function json.decode(str) local pos = 1 local len = #str local function skip_whitespace() while pos <= len do local c = str:byte(pos) if c ~= 32 and c ~= 9 and c ~= 10 and c ~= 13 then break end pos = pos + 1 end end local function decode_string() local start = pos + 1 pos = pos + 1 while pos <= len do local c = str:byte(pos) if c == 34 then -- " local result = str:sub(start, pos - 1) pos = pos + 1 if result:find('\\') then result = result:gsub('\\(.)', { ['"'] = '"', ['\\'] = '\\', ['/'] = '/', ['b'] = '\b', ['f'] = '\f', ['n'] = '\n', ['r'] = '\r', ['t'] = '\t' }) result = result:gsub('\\u(%x%x%x%x)', function(hex) return string.char(tonumber(hex, 16)) end) end return result elseif c == 92 then -- \ pos = pos + 2 else pos = pos + 1 end end error('unterminated string') end local function decode_number() local start = pos local c = str:byte(pos) if c == 45 then pos = pos + 1 end -- - c = str:byte(pos) if not c or c < 48 or c > 57 then error('invalid number') end if c == 48 then pos = pos + 1 else while pos <= len do c = str:byte(pos) if c < 48 or c > 57 then break end pos = pos + 1 end end if pos <= len and str:byte(pos) == 46 then -- . pos = pos + 1 local found_digit = false while pos <= len do c = str:byte(pos) if c < 48 or c > 57 then break end found_digit = true pos = pos + 1 end if not found_digit then error('invalid number') end end if pos <= len then c = str:byte(pos) if c == 101 or c == 69 then -- e or E pos = pos + 1 if pos <= len then c = str:byte(pos) if c == 43 or c == 45 then pos = pos + 1 end -- + or - end local found_digit = false while pos <= len do c = str:byte(pos) if c < 48 or c > 57 then break end found_digit = true pos = pos + 1 end if not found_digit then error('invalid number') end end end return tonumber(str:sub(start, pos - 1)) end local function decode_value() skip_whitespace() if pos > len then error('unexpected end') end local c = str:byte(pos) if c == 34 then -- " return decode_string() elseif c == 123 then -- { local result = {} pos = pos + 1 skip_whitespace() if pos <= len and str:byte(pos) == 125 then -- } pos = pos + 1 return result end while true do skip_whitespace() if pos > len or str:byte(pos) ~= 34 then error('expected string key') end local key = decode_string() skip_whitespace() if pos > len or str:byte(pos) ~= 58 then error('expected :') end pos = pos + 1 result[key] = decode_value() skip_whitespace() if pos > len then error('unexpected end') end c = str:byte(pos) if c == 125 then -- } pos = pos + 1 return result elseif c == 44 then -- , pos = pos + 1 else error('expected , or }') end end elseif c == 91 then -- [ local result = {} local index = 1 pos = pos + 1 skip_whitespace() if pos <= len and str:byte(pos) == 93 then -- ] pos = pos + 1 return result end while true do result[index] = decode_value() index = index + 1 skip_whitespace() if pos > len then error('unexpected end') end c = str:byte(pos) if c == 93 then -- ] pos = pos + 1 return result elseif c == 44 then -- , pos = pos + 1 else error('expected , or ]') end end elseif c == 116 then -- true if str:sub(pos, pos + 3) == 'true' then pos = pos + 4 return true end error('invalid literal') elseif c == 102 then -- false if str:sub(pos, pos + 4) == 'false' then pos = pos + 5 return false end error('invalid literal') elseif c == 110 then -- null if str:sub(pos, pos + 3) == 'null' then pos = pos + 4 return nil end error('invalid literal') elseif (c >= 48 and c <= 57) or c == 45 then -- 0-9 or - return decode_number() else error('unexpected character') end end local result = decode_value() skip_whitespace() if pos <= len then error('unexpected content after JSON') end return result end function json.load_file(filename) local file = io.open(filename, "r") if not file then error("Cannot open file: " .. filename) end local content = file:read("*all") file:close() return json.decode(content) end function json.save_file(filename, data) local file = io.open(filename, "w") if not file then error("Cannot write to file: " .. filename) end file:write(json.encode(data)) file:close() end function json.merge(...) local result = {} local n = select("#", ...) for i = 1, n do local obj = select(i, ...) if type(obj) == "table" then for k, v in pairs(obj) do result[k] = v end end end return result end function json.extract(data, path) local current = data local start = 1 local len = #path while start <= len do local dot_pos = path:find(".", start, true) local part = dot_pos and path:sub(start, dot_pos - 1) or path:sub(start) if type(current) ~= "table" then return nil end local bracket_start, bracket_end = part:find("^%[(%d+)%]$") if bracket_start then local index = tonumber(part:sub(2, -2)) + 1 current = current[index] else current = current[part] end if current == nil then return nil end start = dot_pos and dot_pos + 1 or len + 1 end return current end function json.pretty(value, indent) local buffer = {} local pos = 1 indent = indent or " " local function encode_string(s) buffer[pos] = '"' pos = pos + 1 local start = 1 for i = 1, #s do local c = s:byte(i) if c == 34 then -- " if i > start then buffer[pos] = s:sub(start, i - 1) pos = pos + 1 end buffer[pos] = '\\"' pos = pos + 1 start = i + 1 elseif c == 92 then -- \ if i > start then buffer[pos] = s:sub(start, i - 1) pos = pos + 1 end buffer[pos] = '\\\\' pos = pos + 1 start = i + 1 elseif c < 32 then if i > start then buffer[pos] = s:sub(start, i - 1) pos = pos + 1 end if c == 8 then buffer[pos] = '\\b' elseif c == 9 then buffer[pos] = '\\t' elseif c == 10 then buffer[pos] = '\\n' elseif c == 12 then buffer[pos] = '\\f' elseif c == 13 then buffer[pos] = '\\r' else buffer[pos] = ('\\u%04x'):format(c) end pos = pos + 1 start = i + 1 end end if start <= #s then buffer[pos] = s:sub(start) pos = pos + 1 end buffer[pos] = '"' pos = pos + 1 end local function encode_value(v, depth) local t = type(v) local current_indent = string.rep(indent, depth) local next_indent = string.rep(indent, depth + 1) if t == 'string' then encode_string(v) elseif t == 'number' then if v ~= v then -- NaN buffer[pos] = 'null' elseif v == 1/0 or v == -1/0 then -- Infinity buffer[pos] = 'null' else buffer[pos] = tostring(v) end pos = pos + 1 elseif t == 'boolean' then buffer[pos] = v and 'true' or 'false' pos = pos + 1 elseif t == 'table' then if depth > 100 then error('circular reference') end local is_array = true local max_index = 0 local count = 0 for k, _ in pairs(v) do count = count + 1 if type(k) ~= 'number' or k <= 0 or k % 1 ~= 0 then is_array = false break end if k > max_index then max_index = k end end if is_array and count == max_index then buffer[pos] = '[\n' pos = pos + 1 for i = 1, max_index do buffer[pos] = next_indent pos = pos + 1 encode_value(v[i], depth + 1) if i < max_index then buffer[pos] = ',' pos = pos + 1 end buffer[pos] = '\n' pos = pos + 1 end buffer[pos] = current_indent .. ']' pos = pos + 1 else buffer[pos] = '{\n' pos = pos + 1 local keys = {} for k in pairs(v) do keys[#keys + 1] = k end for i, k in ipairs(keys) do buffer[pos] = next_indent pos = pos + 1 encode_string(tostring(k)) buffer[pos] = ': ' pos = pos + 1 encode_value(v[k], depth + 1) if i < #keys then buffer[pos] = ',' pos = pos + 1 end buffer[pos] = '\n' pos = pos + 1 end buffer[pos] = current_indent .. '}' pos = pos + 1 end else buffer[pos] = 'null' pos = pos + 1 end end encode_value(value, 0) return table.concat(buffer) end function json.validate(data, schema) local function validate_value(value, schema_value) local value_type = type(value) local schema_type = schema_value.type if schema_type and value_type ~= schema_type then return false, "Expected " .. schema_type .. ", got " .. value_type end if schema_type == "table" and schema_value.properties then local required = schema_value.required for prop, prop_schema in pairs(schema_value.properties) do local prop_value = value[prop] if required and required[prop] and prop_value == nil then return false, "Missing required property: " .. prop end if prop_value ~= nil then local valid, err = validate_value(prop_value, prop_schema) if not valid then return false, "Property " .. prop .. ": " .. err end end end end return true end return validate_value(data, schema) end return json