Module:JSON
Ang module na ito ay nag-aalok ng ilang mga pamamaraan ng palingkurang-bayan para sa pag-convert ng mga halaga ng Lua sa mga halaga ng JSON (sa mga string ng Lua na naka-encode ng UTF-8).
Sa kasamaang palad, medyo naiiba ang modelo ng data ni Lua sa JSON, kaya hindi posible na magsulat ng pangkalahatang punsyon na kumukuha ng anumang halaga ng Lua at nagbabalik ng halaga ng JSON, palaging "ginagawa ang tamang bagay". Sa halip, ang ilang mga halaga ay hindi maaaring ma-convert, at ang iba pang mga halaga ay may maraming posibleng hindi katumbas na representasyon.
Ang mga pagkakaiba ay:
- Ang Lua ay may tatlong uri na walang JSON analogues, katulad ng function, userdata, at thread, kaya walang suporta ang modulo na ito para sa mga halaga ng mga ganitong uri.
- Ang konsepto ng "metatables" ni Lua ay walang analogue sa JSON, kaya ganap na binabalewala ng module na ito ang mga metatable.
- Ang uri ng numero ni Lua, gaya ng ipinatupad sa Scribunto, ay binubuo ng mga double-precision na floating-point na halaga, samantalang ang uri ng numero ng JSON ay binubuo ng mga decimal na representasyon. (At malamang na iko-convert ng end-recipient ng data ng JSON ang mga value pabalik sa isang uri ng floating-point notation.) Nangangahulugan ito na, bukod sa mga integer, sa pangkalahatan ay hindi mo maaasahang eksaktong mako-convert ang mga value. (At kahit na may mga integer, maaari mo lang asahan ang perpektong conversion sa hanay na ±109 o higit pa.) Higit pa rito, nangangahulugan ito na ang Lua ay may ilang mga numeric na halaga na walang mga JSON analogue, katulad ng positibong kawalang-hanggan, negatibong kawalang-hanggan, at "not isang numero" na mga halaga; kaya, hindi sinusuportahan ng modyul na ito ang mga halagang iyon.
- Ang uri ng string ng Lua ay kumakatawan sa mga string ng walong-bit na byte, samantalang ang uri ng *string* ng JSON ay kumakatawan sa mga string ng mga Unicode na character. Ang module na ito ay nangangailangan ng mga string ng Lua na maging wastong mga pagkakasunod-sunod ng UTF-8.
- Samantalang ang Lua ay mayroon lamang iisang table na uri ng pagmamapa mula sa mga arbitrary na di-nil na mga halaga hanggang sa mga di-nil na halaga, ang JSON ay may hiwalay na array' ' at bagay na uri, kung saan ang isang array ay nagmamapa mula sa isang hanay ng mga integer {0,1,…,n} sa mga arbitraryo na halaga, at ang isang bagay ay nagmamapa mula sa mga arbitraryo na string patungo sa mga arbitraryo na halaga. Bilang resulta, ang modyul na ito [TBD]
(Tandaan: ang nasa itaas ay isang pagtatangka sa isang kumpletong listahan ng mga pagkakaiba, ngunit medyo posible na may napalampas ako.)
local export = {}
local m_table = require("Module:table")
local codepoint = require("Module:string utilities").codepoint
local concat = table.concat
local converter -- forward declaration
local format = string.format
local getmetatable = getmetatable
local index_ipairs = m_table.indexIpairs
local insert = table.insert
local is_array = m_table.isArray
local is_finite_real_number = require("Module:math").is_finite_real_number
local is_utf8 = mw.ustring.isutf8
local pairs = pairs
local pcall = pcall
local sorted_pairs = m_table.sortedPairs
local type = type
local ugsub = mw.ustring.gsub
-- Given a finite real number x, returns a string containing its JSON
-- representation, with enough precision that it *should* round-trip correctly
-- (depending on the well-behavedness of the system on the other end).
local function json_fromNumber(x, level)
if is_finite_real_number(x) then
return format("%.17g", x)
end
error(format("Cannot encode non-finite real number %g", x), level)
end
local escape_char_map = {
["\b"] = "\\b",
["\t"] = "\\t",
["\n"] = "\\n",
["\f"] = "\\f",
["\r"] = "\\r",
["\""] = "\\\"",
["\\"] = "\\\\",
}
local function escape_codepoint_utf16(c)
if c >= 0x10000 then
c = c - 0x10000
return format("\\u%04x\\u%04x", 0xD800 + (c / 1024), 0xDC00 + (c % 1024))
end
return format("\\u%04x", c)
end
local function escape_char(c)
return escape_char_map[c] or escape_codepoint_utf16(codepoint(c))
end
-- Given a string, escapes any illegal characters and wraps it in double-quotes.
-- Raises an error if the string is not valid UTF-8.
local function json_fromString(s, ascii, level)
if not is_utf8(s) then
error(format("Cannot encode non-UTF-8 string '%s'", s), level)
elseif ascii then
-- U+0080 = \194\128 in UTF-8, U+10FFFF = \244\143\191\191 in UTF-8
s = ugsub(s, '[%z\1-\31"\\\194\128-\244\143\191\191]', escape_char)
else
-- U+2029 (LINE SEPARATOR, \226\128\168 in UTF-8)
-- and U+2028 (PARAGRAPH SEPARATOR, \226\128\169 in UTF-8) are allowed
-- in JSON, but must be escaped for compatibility with JavaScript.
s = ugsub(s, '[%z\1-\31"\\\226\128\168\226\128\169]', escape_char)
end
return '"' .. s .. '"'
end
local function json_fromTable(t, opts, current, level)
local ret, open, close = {}
if is_array(t) then
for key, value in index_ipairs(t) do
ret[key] = converter(value, opts, current, level + 1) or "null"
end
open, close = "[", "]"
else
-- `seen_keys` memoizes keys already seen, to prevent collisions (e.g. 1
-- and "1").
local seen_keys, colon, ascii = {}, opts.compress and ":" or " : ", opts.ascii
for key, value in (opts.sort_keys and sorted_pairs or pairs)(t) do
local key_type = type(key)
if key_type == "number" then
key = json_fromNumber(key, level + 1)
elseif key_type ~= "string" then
error(format("Cannot use type '%s' as a table key", key_type), level)
end
key = json_fromString(key, ascii, level + 1)
if seen_keys[key] then
error(format("Collision for JSON key %s", key), level)
end
seen_keys[key] = true
insert(ret, key .. colon .. (converter(value, opts, current, level + 1) or "null"))
end
open, close = "{", "}"
end
ret = open .. (
opts.compress and concat(ret, ",") .. close or
" " .. concat(ret, ", ") .. (
#ret == 0 and "" or " "
) .. close
)
current[t] = nil
return ret
end
function converter(this, opts, current, level) -- local declared above
local val_type = type(this)
if val_type == "nil" then
return "null"
elseif val_type == "boolean" then
return this and "true" or "false"
elseif val_type == "number" then
return json_fromNumber(this, level + 1)
elseif val_type == "string" then
return json_fromString(this, opts.ascii, level + 1)
elseif val_type ~= "table" then
error(format("Cannot encode type '%s'", val_type), level)
elseif current[this] then
error("Cannot use recursive tables", level)
end
-- Memoize the table to enable recursion checking.
current[this] = true
if opts.ignore_toJSON then
return json_fromTable(this, opts, current, level + 1)
end
-- Check if a toJSON method can be used. Use the lua_table flag to get a Lua
-- table, as any options need to be applied to the output.
local to_json = this.toJSON
if to_json == nil then
return json_fromTable(this, opts, current, level + 1)
end
local to_json_type = type(to_json)
-- If it's a function, call it.
if to_json_type == "function" then
local ret = converter(to_json(this, {lua_table = true}), opts, current, level + 1)
current[this] = nil
return ret
-- If it's a table and there's a metatable, try to call it. If getmetatable
-- returns nil, there's definitely no metatable (so it can't be callable),
-- but otherwise the metatable could be protected with __metatable, so the
-- only reliable approach is to call it with pcall.
elseif to_json_type == "table" and getmetatable(to_json) ~= nil then
local success, new = pcall(to_json, this, {lua_table = true})
if success then
local ret = converter(new, opts, current, level + 1)
current[this] = nil
return ret
-- The error message will only take this exact form if it was thrown due
-- to `this` not being callable, as it will contain a traceback if
-- thrown in some other function, so raise the error if it's not a
-- match, since it's an error elsewhere.
elseif new ~= "attempt to call a table value" then
error(new)
end
-- Not a callable table.
end
-- Treat as a conventional value.
return json_fromTable(this, opts, current, level + 1)
end
-- This function makes an effort to convert an arbitrary Lua value to a string
-- containing a JSON representation of it. It's not intended to be very robust,
-- but may be useful for prototyping.
function export.toJSON(this, opts)
return converter(this, opts == nil and {} or opts, {}, 3)
end
return export