2022-07-17 23:32:44 +00:00
|
|
|
-- Copyright (c) 2022 David Vogel
|
|
|
|
--
|
|
|
|
-- This software is released under the MIT License.
|
|
|
|
-- https://opensource.org/licenses/MIT
|
|
|
|
|
2022-07-23 15:36:21 +00:00
|
|
|
-- Simple library to marshal JSON values. Mainly for Noita.
|
2022-07-17 23:32:44 +00:00
|
|
|
|
|
|
|
local lib = {}
|
|
|
|
|
|
|
|
---Maps single characters to escaped strings.
|
|
|
|
---
|
|
|
|
---Copyright (c) 2020 rxi
|
|
|
|
---@see [github.com/rxi/json.lua](https://github.com/rxi/json.lua/blob/master/json.lua)
|
|
|
|
local escapeCharacters = {
|
|
|
|
["\\"] = "\\",
|
|
|
|
["\""] = "\"",
|
|
|
|
["\b"] = "b",
|
|
|
|
["\f"] = "f",
|
|
|
|
["\n"] = "n",
|
|
|
|
["\r"] = "r",
|
|
|
|
["\t"] = "t",
|
|
|
|
}
|
|
|
|
|
|
|
|
---escapeRune returns the escaped string for a given rune.
|
|
|
|
---
|
|
|
|
---Copyright (c) 2020 rxi
|
|
|
|
---@see [github.com/rxi/json.lua](https://github.com/rxi/json.lua/blob/master/json.lua)
|
|
|
|
---@param rune string
|
|
|
|
---@return string
|
|
|
|
local function escapeCharacter(rune)
|
|
|
|
return "\\" .. (escapeCharacters[rune] or string.format("u%04x", rune:byte()))
|
|
|
|
end
|
|
|
|
|
|
|
|
---escapeString returns the escaped version of the given string.
|
|
|
|
---
|
|
|
|
---Copyright (c) 2020 rxi
|
|
|
|
---@see [github.com/rxi/json.lua](https://github.com/rxi/json.lua/blob/master/json.lua)
|
|
|
|
---@param str string
|
|
|
|
---@return string
|
|
|
|
local function escapeString(str)
|
|
|
|
local result, count = str:gsub('[%z\1-\31\\"]', escapeCharacter)
|
|
|
|
return result
|
|
|
|
end
|
|
|
|
|
|
|
|
---MarshalString returns the JSON representation of a string value.
|
|
|
|
---@param val string
|
|
|
|
---@return string
|
|
|
|
function lib.MarshalString(val)
|
2022-07-22 17:10:15 +00:00
|
|
|
return string.format("%q", escapeString(val))
|
2022-07-17 23:32:44 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
---MarshalNumber returns the JSON representation of a number value.
|
|
|
|
---@param val number
|
|
|
|
---@return string
|
|
|
|
function lib.MarshalNumber(val)
|
2022-08-08 20:07:00 +00:00
|
|
|
-- Edge cases, as there is no real solution to this.
|
|
|
|
-- JSON can't store special IEEE754 values, this is dumb as hell.
|
|
|
|
if val ~= val then
|
|
|
|
return "null" -- Alternatively we could output the string "NaN" (With quotes).
|
|
|
|
elseif val <= -math.huge then
|
|
|
|
return "-1E+600" -- Just output a stupidly large number.
|
|
|
|
elseif val >= math.huge then
|
|
|
|
return "1E+600" -- Just output a stupidly large number.
|
|
|
|
end
|
2022-07-17 23:32:44 +00:00
|
|
|
|
|
|
|
return tostring(val)
|
|
|
|
end
|
|
|
|
|
|
|
|
---MarshalBoolean returns the JSON representation of a boolean value.
|
2022-07-18 09:47:59 +00:00
|
|
|
---@param val boolean
|
2022-07-17 23:32:44 +00:00
|
|
|
---@return string
|
|
|
|
function lib.MarshalBoolean(val)
|
|
|
|
return tostring(val)
|
|
|
|
end
|
|
|
|
|
|
|
|
---MarshalObject returns the JSON representation of a table object.
|
|
|
|
---
|
|
|
|
---This only works with string keys. Number keys will be converted into strings.
|
|
|
|
---@param val table<string,any>
|
|
|
|
---@return string
|
|
|
|
function lib.MarshalObject(val)
|
|
|
|
local result = "{"
|
|
|
|
|
|
|
|
for k, v in pairs(val) do
|
|
|
|
result = result .. lib.MarshalString(k) .. ": " .. lib.Marshal(v)
|
|
|
|
-- Append character depending on whether this is the last element or not.
|
|
|
|
if next(val, k) == nil then
|
|
|
|
result = result .. "}"
|
|
|
|
else
|
|
|
|
result = result .. ", "
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
return result
|
|
|
|
end
|
|
|
|
|
|
|
|
---MarshalArray returns the JSON representation of an array object.
|
|
|
|
---
|
|
|
|
---@param val table<number,any>
|
|
|
|
---@param customMarshalFunction function|nil -- Custom function for marshalling the array values.
|
|
|
|
---@return string
|
|
|
|
function lib.MarshalArray(val, customMarshalFunction)
|
|
|
|
local result = "["
|
|
|
|
|
|
|
|
-- TODO: Check if the type of all array entries is the same.
|
|
|
|
|
|
|
|
local length = #val
|
|
|
|
for i, v in ipairs(val) do
|
|
|
|
if customMarshalFunction then
|
|
|
|
result = result .. customMarshalFunction(v)
|
|
|
|
else
|
|
|
|
result = result .. lib.Marshal(v)
|
|
|
|
end
|
|
|
|
-- Append character depending on whether this is the last element or not.
|
|
|
|
if i == length then
|
|
|
|
result = result .. "]"
|
|
|
|
else
|
|
|
|
result = result .. ", "
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
return result
|
|
|
|
end
|
|
|
|
|
|
|
|
---Marshal marshals any value into JSON representation.
|
|
|
|
---@param val any
|
|
|
|
---@return string
|
|
|
|
function lib.Marshal(val)
|
|
|
|
local t = type(val)
|
|
|
|
|
|
|
|
if t == "nil" then
|
|
|
|
return "null"
|
|
|
|
elseif t == "number" then
|
|
|
|
return lib.MarshalNumber(val)
|
|
|
|
elseif t == "string" then
|
|
|
|
return lib.MarshalString(val)
|
|
|
|
elseif t == "boolean" then
|
|
|
|
return lib.MarshalBoolean(val)
|
|
|
|
elseif t == "table" then
|
2022-07-18 09:47:59 +00:00
|
|
|
-- Check if object implements the JSON marshaler interface.
|
|
|
|
if val.MarshalJSON ~= nil and type(val.MarshalJSON) == "function" then
|
|
|
|
return val:MarshalJSON()
|
2022-07-17 23:32:44 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
-- If not, fall back to array or object handling.
|
|
|
|
local commonKeyType, commonValueType
|
|
|
|
for k, v in pairs(val) do
|
|
|
|
local keyType, valueType = type(k), type(v)
|
|
|
|
commonKeyType = commonKeyType or keyType
|
|
|
|
if commonKeyType ~= keyType then
|
|
|
|
-- Different types detected, abort.
|
|
|
|
commonKeyType = "mixed"
|
|
|
|
break
|
|
|
|
end
|
|
|
|
commonValueType = commonValueType or valueType
|
|
|
|
if commonValueType ~= valueType then
|
|
|
|
-- Different types detected, abort.
|
|
|
|
commonValueType = "mixed"
|
|
|
|
break
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Decide based on common types.
|
|
|
|
if commonKeyType == "number" and commonValueType ~= "mixed" then
|
|
|
|
return lib.MarshalArray(val) -- This will falsely detect sparse integer key maps as arrays. But meh.
|
|
|
|
elseif commonKeyType == "string" then
|
|
|
|
return lib.MarshalObject(val) -- This will not detect if there are number keys, which would work with MarshalObject.
|
|
|
|
elseif commonKeyType == nil and commonValueType == nil then
|
|
|
|
return "null" -- Fallback in case of empty table. There is no other way than using null, as we don't have type information without table elements.
|
|
|
|
end
|
|
|
|
|
|
|
|
error(string.format("unsupported table type. CommonKeyType = %s. CommonValueType = %s. MetaTable = %s", commonKeyType or "nil", commonValueType or "nil", getmetatable(val) or "nil"))
|
|
|
|
end
|
|
|
|
|
|
|
|
error(string.format("unsupported type %q", t))
|
|
|
|
end
|
|
|
|
|
|
|
|
return lib
|