Skip to content

Commit f9aa74b

Browse files
author
Sheridan C Rawlins
committed
JSON: Support empty tables being objects; not arrays.
Fixes #44
1 parent 152828b commit f9aa74b

File tree

5 files changed

+95
-15
lines changed

5 files changed

+95
-15
lines changed

json/api.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,9 @@ func Encode(L *lua.LState) int {
3333
L.Push(lua.LString(string(data)))
3434
return 1
3535
}
36+
37+
func TableIsObject(L *lua.LState) int {
38+
table := L.CheckTable(1)
39+
L.SetMetatable(table, L.GetTypeMetatable(jsonTableIsObject))
40+
return 0
41+
}

json/decoder.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ func registerDecoder(L *lua.LState) {
6363
}))
6464
}
6565

66+
func registerJsonDecodedObject(L *lua.LState) {
67+
mt := L.NewTypeMetatable(jsonTableIsObject)
68+
mt.RawSetString(jsonTableIsObject, lua.LTrue)
69+
}
70+
6671
func newJSONDecoder(L *lua.LState) int {
6772
reader := io.CheckIOReader(L, 1)
6873
L.Pop(L.GetTop())

json/loader.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ func Preload(L *lua.LState) {
1616
func Loader(L *lua.LState) int {
1717
registerJSONEncoder(L)
1818
registerDecoder(L)
19+
registerJsonDecodedObject(L)
1920

2021
t := L.NewTable()
2122
L.SetFuncs(t, api)
@@ -24,8 +25,9 @@ func Loader(L *lua.LState) int {
2425
}
2526

2627
var api = map[string]lua.LGFunction{
27-
"decode": Decode,
28-
"encode": Encode,
29-
"new_encoder": newJSONEncoder,
30-
"new_decoder": newJSONDecoder,
28+
"tableIsObject": TableIsObject,
29+
"decode": Decode,
30+
"encode": Encode,
31+
"new_encoder": newJSONEncoder,
32+
"new_decoder": newJSONDecoder,
3133
}

json/test/test_api.lua

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,26 @@ function TestJson(t)
99

1010
local result, err = json.decode(jsonStringWithNull)
1111
t:Run("decode", function(t)
12-
if err then error(err) end
13-
if result["a"]["b"] ~= 1 then error("must be decode") end
14-
if result["a"]["c"] ~= nil then error("c is not nil") end
12+
if err then
13+
error(err)
14+
end
15+
if result["a"]["b"] ~= 1 then
16+
error("must be decode")
17+
end
18+
if result["a"]["c"] ~= nil then
19+
error("c is not nil")
20+
end
1521
print("done: json.decode()")
1622
end)
1723

1824
local result, err = json.encode(result)
1925
t:Run("encode omits null values", function(t)
20-
if err then error(err) end
21-
if result ~= jsonString then error("must be encode "..inspect(result)) end
26+
if err then
27+
error(err)
28+
end
29+
if result ~= jsonString then
30+
error("must be encode " .. inspect(result))
31+
end
2232
print("done: json.encode()")
2333
end)
2434
end
@@ -29,7 +39,7 @@ function TestEncoder(t)
2939
writer, err = io.open(temp_file, 'w')
3040
assert(not err, err)
3141
encoder = json.new_encoder(writer)
32-
err = encoder:encode({foo="bar", bar="baz"})
42+
err = encoder:encode({ foo = "bar", bar = "baz" })
3343
assert(not err, err)
3444
writer:close()
3545

@@ -44,7 +54,7 @@ end
4454
function TestEncoderWithStringsBuffer(t)
4555
builder = strings.new_builder()
4656
encoder = json.new_encoder(builder)
47-
err = encoder:encode({abc="def", num=123, arr={1,2,3}})
57+
err = encoder:encode({ abc = "def", num = 123, arr = { 1, 2, 3 } })
4858
s = strings.trim_suffix(builder:string(), "\n")
4959
expected = [[{"abc":"def","arr":[1,2,3],"num":123}]]
5060
assert(s == expected, string.format([['%s' ~= '%s']], expected, s))
@@ -54,7 +64,7 @@ function TestEncoderWithPrettyPrinting(t)
5464
builder = strings.new_builder()
5565
encoder = json.new_encoder(builder)
5666
encoder:set_indent('', " ")
57-
err = encoder:encode({abc="def", num=123, arr={1,2,3}})
67+
err = encoder:encode({ abc = "def", num = 123, arr = { 1, 2, 3 } })
5868
s = strings.trim_suffix(builder:string(), "\n")
5969
expected = [[{
6070
"abc": "def",
@@ -118,13 +128,58 @@ end
118128
function TestEncoder_writing_twice(t)
119129
writer = strings.new_builder()
120130
encoder = json.new_encoder(writer)
121-
err = encoder:encode({abc="def"})
131+
err = encoder:encode({ abc = "def" })
122132
assert(not err, err)
123-
err = encoder:encode({num=123})
133+
err = encoder:encode({ num = 123 })
124134
assert(not err, err)
125135
s = writer:string()
126136
expected = [[{"abc":"def"}
127137
{"num":123}
128138
]]
129139
assert(s == expected, string.format([['%s' ~= '%s']], s, expected))
130140
end
141+
142+
function TestEncodeDecodeEmpty(t)
143+
tests = {
144+
{
145+
name = [["{} should re-encode to {}"]],
146+
input = '{}'
147+
},
148+
{
149+
name = [["[] should re-encode to []"]],
150+
input = '[]'
151+
},
152+
{
153+
name = [["object with both {} and [] should re-encode to properly"]],
154+
input = [[{"emptyArr":[],"emptyObj":{},"s":"foo bar baz"}]]
155+
},
156+
}
157+
for _, tt in ipairs(tests) do
158+
t:Run(tt.name, function(t)
159+
t:Logf("input: %s", tt.input)
160+
local decoded = json.decode(tt.input)
161+
t:Logf("decoded: %s", inspect(decoded))
162+
local encoded, err = json.encode(decoded)
163+
t:Logf("encoded: %s, err = %s", encoded, tostring(err))
164+
assert(not err, err)
165+
assert(encoded == tt.input, string.format("expected %s; got %s", tt.input, encoded))
166+
end)
167+
end
168+
end
169+
170+
function TestTableIsObject(t)
171+
t:Run("empty table is []", function(t)
172+
local table = {}
173+
local encoded, err = json.encode(table)
174+
assert(not err, err)
175+
assert(encoded == "[]", string.format("expected []; got %s", encoded))
176+
end)
177+
178+
t:Run("empty table marked as object is {}", function(t)
179+
local table = {}
180+
json.tableIsObject(table)
181+
local encoded, err = json.encode(table)
182+
assert(not err, err)
183+
assert(encoded == "{}", string.format("expected {}; got %s", encoded))
184+
end)
185+
end

json/value.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
lua "github.com/yuin/gopher-lua"
88
)
99

10+
const jsonTableIsObject = "__jsonTableIsObject"
11+
1012
var (
1113
errNested = errors.New("cannot encode recursively nested tables to JSON")
1214
errSparseArray = errors.New("cannot encode sparse array")
@@ -24,6 +26,15 @@ type jsonValue struct {
2426
visited map[*lua.LTable]bool
2527
}
2628

29+
func marshalEmptyTable(table *lua.LTable) []byte {
30+
if mt, ok := table.Metatable.(*lua.LTable); ok {
31+
if lua.LVAsBool(mt.RawGetString(jsonTableIsObject)) {
32+
return []byte("{}")
33+
}
34+
}
35+
return []byte("[]")
36+
}
37+
2738
func (j jsonValue) MarshalJSON() (data []byte, err error) {
2839
switch converted := j.LValue.(type) {
2940
case lua.LBool:
@@ -44,7 +55,7 @@ func (j jsonValue) MarshalJSON() (data []byte, err error) {
4455

4556
switch key.Type() {
4657
case lua.LTNil: // empty table
47-
data = []byte(`[]`)
58+
data = marshalEmptyTable(converted)
4859
case lua.LTNumber:
4960
arr := make([]jsonValue, 0, converted.Len())
5061
expectedKey := lua.LNumber(1)
@@ -108,6 +119,7 @@ func decode(L *lua.LState, value interface{}) lua.LValue {
108119
return arr
109120
case map[string]interface{}:
110121
tbl := L.CreateTable(0, len(converted))
122+
L.SetMetatable(tbl, L.GetTypeMetatable(jsonTableIsObject))
111123
for key, item := range converted {
112124
tbl.RawSetH(lua.LString(key), decode(L, item))
113125
}

0 commit comments

Comments
 (0)