From 1710ad9861612e8bcffa92af5564454d2f1fe704 Mon Sep 17 00:00:00 2001 From: FensieRenaud <65811543+FensieRenaud@users.noreply.github.com> Date: Mon, 18 Jan 2021 17:44:39 +0100 Subject: [PATCH] Serialise sparse arrays into JSON (#685) --- .../computercraft/lua/rom/apis/textutils.lua | 12 ++- .../test-rom/spec/apis/textutils_spec.lua | 99 +++++++++++++++++++ 2 files changed, 109 insertions(+), 2 deletions(-) diff --git a/src/main/resources/data/computercraft/lua/rom/apis/textutils.lua b/src/main/resources/data/computercraft/lua/rom/apis/textutils.lua index c80f8c3c9..d6b4c5705 100644 --- a/src/main/resources/data/computercraft/lua/rom/apis/textutils.lua +++ b/src/main/resources/data/computercraft/lua/rom/apis/textutils.lua @@ -381,6 +381,7 @@ local function serializeJSONImpl(t, tTracking, bNBTStyle) local sArrayResult = "[" local nObjectSize = 0 local nArraySize = 0 + local largestArrayIndex = 0 for k, v in pairs(t) do if type(k) == "string" then local sEntry @@ -395,10 +396,17 @@ local function serializeJSONImpl(t, tTracking, bNBTStyle) sObjectResult = sObjectResult .. "," .. sEntry end nObjectSize = nObjectSize + 1 + elseif type(k) == "number" and k > largestArrayIndex then --the largest index is kept to avoid losing half the array if there is any single nil in that array + largestArrayIndex = k end end - for _, v in ipairs(t) do - local sEntry = serializeJSONImpl(v, tTracking, bNBTStyle) + for k = 1, largestArrayIndex, 1 do --the array is read up to the very last valid array index, ipairs() would stop at the first nil value and we would lose any data after. + local sEntry + if t[k] == nil then --if the array is nil at index k the value is "null" as to keep the unused indexes in between used ones. + sEntry = "null" + else -- if the array index does not point to a nil we serialise it's content. + sEntry = serializeJSONImpl(t[k], tTracking, bNBTStyle) + end if nArraySize == 0 then sArrayResult = sArrayResult .. sEntry else diff --git a/src/test/resources/test-rom/spec/apis/textutils_spec.lua b/src/test/resources/test-rom/spec/apis/textutils_spec.lua index ab881d60b..6e1ba2bb7 100644 --- a/src/test/resources/test-rom/spec/apis/textutils_spec.lua +++ b/src/test/resources/test-rom/spec/apis/textutils_spec.lua @@ -70,6 +70,105 @@ describe("The textutils library", function() expect.error(textutils.serialiseJSON, nil):eq("bad argument #1 (expected table, string, number or boolean, got nil)") expect.error(textutils.serialiseJSON, "", 1):eq("bad argument #2 (expected boolean, got number)") end) + + it("serializes empty arrays", function() + expect(textutils.serializeJSON(textutils.empty_json_array)):eq("[]") + end) + + it("serializes null", function() + expect(textutils.serializeJSON(textutils.json_null)):eq("null") + end) + + it("serializes strings", function() + expect(textutils.serializeJSON('a')):eq('"a"') + expect(textutils.serializeJSON('"')):eq('"\\""') + expect(textutils.serializeJSON('\\')):eq('"\\\\"') + expect(textutils.serializeJSON('/')):eq('"/"') + expect(textutils.serializeJSON('\b')):eq('"\\b"') + expect(textutils.serializeJSON('\n')):eq('"\\n"') + expect(textutils.serializeJSON(string.char(0))):eq('"\\u0000"') + expect(textutils.serializeJSON(string.char(0x0A))):eq('"\\n"') + expect(textutils.serializeJSON(string.char(0x1D))):eq('"\\u001D"') + expect(textutils.serializeJSON(string.char(0x81))):eq('"\\u0081"') + expect(textutils.serializeJSON(string.char(0xFF))):eq('"\\u00FF"') + end) + + it("serializes arrays until the last index with content", function() + expect(textutils.serializeJSON({ 5, "test", nil, nil, 7 })):eq('[5,"test",null,null,7]') + expect(textutils.serializeJSON({ 5, "test", nil, nil, textutils.json_null })):eq('[5,"test",null,null,null]') + expect(textutils.serializeJSON({ nil, nil, nil, nil, "text" })):eq('[null,null,null,null,"text"]') + end) + end) + + describe("textutils.unserializeJSON", function() + describe("parses", function() + it("a list of primitives", function() + expect(textutils.unserializeJSON('[1, true, false, "hello"]')):same { 1, true, false, "hello" } + end) + + it("null when parse_null is true", function() + expect(textutils.unserializeJSON("null", { parse_null = true })):eq(textutils.json_null) + end) + + it("null when parse_null is false", function() + expect(textutils.unserializeJSON("null", { parse_null = false })):eq(nil) + end) + + it("an empty array", function() + expect(textutils.unserializeJSON("[]", { parse_null = false })):eq(textutils.empty_json_array) + end) + + it("basic objects", function() + expect(textutils.unserializeJSON([[{ "a": 1, "b":2 }]])):same { a = 1, b = 2 } + end) + end) + + describe("parses using NBT-style syntax", function() + local function exp(x) + local res, err = textutils.unserializeJSON(x, { nbt_style = true }) + if not res then error(err, 2) end + return expect(res) + end + it("basic objects", function() + exp([[{ a: 1, b:2 }]]):same { a = 1, b = 2 } + end) + + it("suffixed numbers", function() + exp("1b"):eq(1) + exp("1.1d"):eq(1.1) + end) + + it("strings", function() + exp("'123'"):eq("123") + exp("\"123\""):eq("123") + end) + + it("typed arrays", function() + exp("[B; 1, 2, 3]"):same { 1, 2, 3 } + exp("[B;]"):same {} + end) + end) + + describe("passes nst/JSONTestSuite", function() + local search_path = "test-rom/data/json-parsing" + local skip = dofile(search_path .. "/skip.lua") + for _, file in pairs(fs.find(search_path .. "/*.json")) do + local name = fs.getName(file):sub(1, -6); + (skip[name] and pending or it)(name, function() + local h = io.open(file, "r") + local contents = h:read("*a") + h:close() + + local res, err = textutils.unserializeJSON(contents) + local kind = fs.getName(file):sub(1, 1) + if kind == "n" then + expect(res):eq(nil) + elseif kind == "y" then + if err ~= nil then fail("Expected test to pass, but failed with " .. err) end + end + end) + end + end) end) describe("textutils.urlEncode", function()