From fe4b2fd63312f1b70c237dba89ce573427f49460 Mon Sep 17 00:00:00 2001 From: Thomas Harning Jr Date: Mon, 16 Jan 2017 16:42:38 -0500 Subject: [PATCH] decode+docs+tests: adds support for (array/object/calls).allowEmptyElement to address GH #38 --- docs/LuaJSON.txt | 9 +- lua/json/decode/composite.lua | 3 + lua/json/decode/state.lua | 29 +++-- tests/lunit-empties-decode.lua | 273 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 305 insertions(+), 9 deletions(-) create mode 100644 tests/lunit-empties-decode.lua diff --git a/docs/LuaJSON.txt b/docs/LuaJSON.txt index 83034ba..242d37c 100644 --- a/docs/LuaJSON.txt +++ b/docs/LuaJSON.txt @@ -112,7 +112,14 @@ number.inf = false unicodeWhitespace : boolean:: Specifies if unicode whitespace characters are counted -array.trailingComma / object.trailingComma : boolean:: +array.allowEmptyElement / calls.allowEmptyElement / object.allowEmptyElement : boolean:: + Specifies if missing values are allowed, replacing them with the value of 'undefined' or 'null' if undefined not allowed. + Example cases: + [1,, 3] ==> { 1, json.util.undefined, 3 } + { x: } ==> { x = json.util.undefined } + call(1,, 3) ==> call(1, json.util.undefined, 3) + +array.trailingComma / calls.trailingComma / object.trailingComma : boolean:: Specifies if extraneous trailing commas are ignored in declaration calls.defs : map:: diff --git a/lua/json/decode/composite.lua b/lua/json/decode/composite.lua index 7a61143..0dda1dd 100644 --- a/lua/json/decode/composite.lua +++ b/lua/json/decode/composite.lua @@ -22,15 +22,18 @@ local _ENV = nil local defaultOptions = { array = { + allowEmptyElement = false, trailingComma = true }, object = { + allowEmptyElement = false, trailingComma = true, number = true, identifier = true, setObjectKey = rawset }, calls = { + allowEmptyElement = false, defs = nil, -- By default, do not allow undefined calls to be de-serialized as call objects allowUndefined = false, diff --git a/lua/json/decode/state.lua b/lua/json/decode/state.lua index 9de693d..a269d7c 100644 --- a/lua/json/decode/state.lua +++ b/lua/json/decode/state.lua @@ -49,18 +49,20 @@ function state_ops.put_object_value(self, trailing) end end assert(self.active_key, "Missing key value") - object_options.setObjectKey(self.active, self.active_key, self:grab_value()) + object_options.setObjectKey(self.active, self.active_key, self:grab_value(object_options.allowEmptyElement)) self.active_key = nil end function state_ops.put_array_value(self, trailing) + local array_options = self.options.array -- Safety check - if trailing and not self.previous_set and self.options.array.trailingComma then + if trailing and not self.previous_set and array_options.trailingComma then return end local new_index = self.active_state + 1 self.active_state = new_index - self.active[new_index] = self:grab_value() + self.active[new_index] = self:grab_value(array_options.allowEmptyElement) +end function state_ops.put_call_value(self, trailing) local call_options = self.options.calls @@ -70,7 +72,7 @@ function state_ops.put_call_value(self, trailing) end local new_index = self.active_state + 1 self.active_state = new_index - self.active[new_index] = self:grab_value() + self.active[new_index] = self:grab_value(call_options.allowEmptyElement) end function state_ops.put_value(self, trailing) @@ -114,7 +116,7 @@ function state_ops.new_object(self) end function state_ops.end_object(self) - if self.previous_set or next(self.active) then + if self.active_key or self.previous_set or next(self.active) then -- Not an empty object self:put_value(true) end @@ -153,7 +155,11 @@ function state_ops.unset_value(self) self.previous = nil end -function state_ops.grab_value(self) +function state_ops.grab_value(self, allowEmptyValue) + if not self.previous_set and allowEmptyValue then + -- Calculate an appropriate empty-value + return self.emptyValue + end assert(self.previous_set, "Previous value not set") self.previous_set = false return self.previous @@ -179,6 +185,13 @@ end local function create(options) + local emptyValue + -- Calculate an empty value up front + if options.others.allowUndefined then + emptyValue = options.others.undefined or nil + else + emptyValue = options.others.null or nil + end local ret = { options = options, stack = {}, @@ -188,8 +201,8 @@ local function create(options) active = nil, active_key = nil, previous = nil, - active_state = nil - + active_state = nil, + emptyValue = emptyValue } return setmetatable(ret, state_mt) end diff --git a/tests/lunit-empties-decode.lua b/tests/lunit-empties-decode.lua new file mode 100644 index 0000000..61207cc --- /dev/null +++ b/tests/lunit-empties-decode.lua @@ -0,0 +1,273 @@ +local json = require("json") +local lunit = require("lunit") + +-- Test module for handling the decoding with 'empties' allowed +if not module then + _ENV = lunit.module("lunit-empties-decode", 'seeall') +else + module("lunit-empties-decode", lunit.testcase, package.seeall) +end + +local options = { + array = { + allowEmptyElement = true + }, + calls = { + allowEmptyElement = true, + allowUndefined = true + }, + object = { + allowEmptyElement = true, + } +} +local options_notrailing = { + array = { + allowEmptyElement = true, + trailingComma = false + }, + calls = { + allowEmptyElement = true, + allowUndefined = true, + trailingComma = false + }, + object = { + allowEmptyElement = true, + trailingComma = false + } +} +local options_simple_null = { + array = { + allowEmptyElement = true + }, + calls = { + allowEmptyElement = true, + allowUndefined = true + }, + object = { + allowEmptyElement = true, + }, + others = { + null = false, + undefined = false + } +} + +function test_decode_array_with_only_null() + local result = assert(json.decode('[null]', options_simple_null)) + assert_nil(result[1]) + assert_equal(1, result.n) + local result = assert(json.decode('[null]', options)) + assert_equal(json.util.null, result[1]) + assert_equal(1, #result) +end + +function test_decode_array_with_empties() + local result = assert(json.decode('[,]', options_simple_null)) + assert_nil(result[1]) + assert_equal(1, result.n) + local result = assert(json.decode('[,]', options)) + assert_equal(json.util.undefined, result[1]) + assert_equal(1, #result) + + local result = assert(json.decode('[,]', options_notrailing)) + assert_equal(json.util.undefined, result[1]) + assert_equal(json.util.undefined, result[2]) + assert_equal(2, #result) +end + +function test_decode_array_with_null() + local result = assert(json.decode('[1, null, 3]', options_simple_null)) + assert_equal(1, result[1]) + assert_nil(result[2]) + assert_equal(3, result[3]) + assert_equal(3, result.n) + local result = assert(json.decode('[1, null, 3]', options)) + assert_equal(1, result[1]) + assert_equal(json.util.null, result[2]) + assert_equal(3, result[3]) +end +function test_decode_array_with_empty() + local result = assert(json.decode('[1,, 3]', options_simple_null)) + assert_equal(1, result[1]) + assert_nil(result[2]) + assert_equal(3, result[3]) + assert_equal(3, result.n) + local result = assert(json.decode('[1,, 3]', options)) + assert_equal(1, result[1]) + assert_equal(json.util.undefined, result[2]) + assert_equal(3, result[3]) +end + +function test_decode_small_array_with_trailing_null() + local result = assert(json.decode('[1, null]', options_simple_null)) + assert_equal(1, result[1]) + assert_nil(result[2]) + assert_equal(2, result.n) + local result = assert(json.decode('[1, ]', options_simple_null)) + assert_equal(1, result[1]) + assert_equal(1, #result) + local result = assert(json.decode('[1, ]', options)) + assert_equal(1, result[1]) + assert_equal(1, #result) + local result = assert(json.decode('[1, ]', options_notrailing)) + assert_equal(1, result[1]) + assert_equal(json.util.undefined, result[2]) + assert_equal(2, #result) +end + +function test_decode_array_with_trailing_null() + local result = assert(json.decode('[1, null, 3, null]', options_simple_null)) + assert_equal(1, result[1]) + assert_nil(result[2]) + assert_equal(3, result[3]) + assert_nil(result[4]) + assert_equal(4, result.n) + local result = assert(json.decode('[1, null, 3, null]', options)) + assert_equal(1, result[1]) + assert_equal(json.util.null, result[2]) + assert_equal(3, result[3]) + assert_equal(json.util.null, result[4]) + assert_equal(4, #result) + local result = assert(json.decode('[1, , 3, ]', options)) + assert_equal(1, result[1]) + assert_equal(json.util.undefined, result[2]) + assert_equal(3, result[3]) + assert_equal(3, #result) + local result = assert(json.decode('[1, , 3, ]', options_notrailing)) + assert_equal(1, result[1]) + assert_equal(json.util.undefined, result[2]) + assert_equal(3, result[3]) + assert_equal(json.util.undefined, result[4]) + assert_equal(4, #result) +end + +function test_decode_object_with_null() + local result = assert(json.decode('{x: null}', options_simple_null)) + assert_nil(result.x) + assert_nil(next(result)) + + local result = assert(json.decode('{x: null}', options)) + assert_equal(json.util.null, result.x) + + local result = assert(json.decode('{x: }', options_simple_null)) + assert_nil(result.x) + assert_nil(next(result)) + + local result = assert(json.decode('{x: }', options)) + assert_equal(json.util.undefined, result.x) + + -- Handle the trailing comma case + local result = assert(json.decode('{x: ,}', options_simple_null)) + assert_nil(result.x) + assert_nil(next(result)) + + local result = assert(json.decode('{x: ,}', options)) + assert_equal(json.util.undefined, result.x) + + -- NOTE: Trailing comma must be allowed explicitly in this case + assert_error(function() + json.decode('{x: ,}', options_notrailing) + end) + + -- Standard setup doesn't allow empties + assert_error(function() + json.decode('{x: }') + end) +end +function test_decode_bigger_object_with_null() + local result = assert(json.decode('{y: 1, x: null}', options_simple_null)) + assert_equal(1, result.y) + assert_nil(result.x) + + local result = assert(json.decode('{y: 1, x: null}', options)) + assert_equal(1, result.y) + assert_equal(json.util.null, result.x) + + local result = assert(json.decode('{y: 1, x: }', options_simple_null)) + assert_equal(1, result.y) + assert_nil(result.x) + local result = assert(json.decode('{x: , y: 1}', options_simple_null)) + assert_equal(1, result.y) + assert_nil(result.x) + + local result = assert(json.decode('{y: 1, x: }', options)) + assert_equal(1, result.y) + assert_equal(json.util.undefined, result.x) + + local result = assert(json.decode('{x: , y: 1}', options)) + assert_equal(1, result.y) + assert_equal(json.util.undefined, result.x) + + -- Handle the trailing comma case + local result = assert(json.decode('{y: 1, x: , }', options_simple_null)) + assert_equal(1, result.y) + assert_nil(result.x) + local result = assert(json.decode('{x: , y: 1, }', options_simple_null)) + assert_equal(1, result.y) + assert_nil(result.x) + + local result = assert(json.decode('{y: 1, x: ,}', options)) + assert_equal(1, result.y) + assert_equal(json.util.undefined, result.x) + + local result = assert(json.decode('{x: , y: 1, }', options)) + assert_equal(1, result.y) + assert_equal(json.util.undefined, result.x) + + -- NOTE: Trailing comma must be allowed explicitly in this case as there is no such thing as an "empty" key:value pair + assert_error(function() + json.decode('{y: 1, x: ,}', options_notrailing) + end) + assert_error(function() + json.decode('{x: , y: 1, }', options_notrailing) + end) +end + +function test_decode_call_with_empties() + local result = assert(json.decode('call(,)', options_simple_null)) + result = result.parameters + assert_nil(result[1]) + assert_equal(1, result.n) + local result = assert(json.decode('call(,)', options)) + result = result.parameters + assert_equal(json.util.undefined, result[1]) + assert_equal(1, #result) + + local result = assert(json.decode('call(,)', options_notrailing)) + result = result.parameters + assert_equal(json.util.undefined, result[1]) + assert_equal(json.util.undefined, result[2]) + assert_equal(2, #result) +end + + + +function test_call_with_empties_and_trailing() + local result = assert(json.decode('call(1, null, 3, null)', options_simple_null)) + result = result.parameters + assert_equal(1, result[1]) + assert_nil(result[2]) + assert_equal(3, result[3]) + assert_nil(result[4]) + assert_equal(4, result.n) + local result = assert(json.decode('call(1, null, 3, null)', options)) + result = result.parameters + assert_equal(1, result[1]) + assert_equal(json.util.null, result[2]) + assert_equal(3, result[3]) + assert_equal(json.util.null, result[4]) + assert_equal(4, #result) + local result = assert(json.decode('call(1, , 3, )', options)) + result = result.parameters + assert_equal(1, result[1]) + assert_equal(json.util.undefined, result[2]) + assert_equal(3, result[3]) + assert_equal(3, #result) + local result = assert(json.decode('call(1, , 3, )', options_notrailing)) + result = result.parameters + assert_equal(1, result[1]) + assert_equal(json.util.undefined, result[2]) + assert_equal(3, result[3]) + assert_equal(json.util.undefined, result[4]) + assert_equal(4, #result) +end -- 2.11.4.GIT