mirror of
https://github.com/status-im/nim-json-serialization.git
synced 2025-02-19 21:38:09 +00:00
Other changes: * Migrate many procs accepting JsonReader to JsonLexer in order to reduce the number of generic instantiations and the resulting code bloat
1013 lines
28 KiB
Nim
1013 lines
28 KiB
Nim
{.used.}
|
|
|
|
import
|
|
strutils, unittest2, json,
|
|
serialization/object_serialization,
|
|
serialization/testing/generic_suite,
|
|
../json_serialization, ./utils,
|
|
../json_serialization/lexer,
|
|
../json_serialization/std/[options, sets, tables],
|
|
../json_serialization/stew/results
|
|
|
|
type
|
|
Foo = object
|
|
i: int
|
|
b {.dontSerialize.}: Bar
|
|
s: string
|
|
|
|
Bar = object
|
|
sf: seq[Foo]
|
|
z: ref Simple
|
|
|
|
Invalid = object
|
|
distance: Mile
|
|
|
|
HasUnusualFieldNames = object
|
|
# Using Nim reserved keyword
|
|
`type`: string
|
|
renamedField {.serializedFieldName("renamed").}: string
|
|
|
|
MyKind = enum
|
|
Apple
|
|
Banana
|
|
|
|
MyCaseObject = object
|
|
name: string
|
|
case kind: MyKind
|
|
of Banana: banana: int
|
|
of Apple: apple: string
|
|
|
|
MyUseCaseObject = object
|
|
field: MyCaseObject
|
|
|
|
HasJsonString = object
|
|
name: string
|
|
data: JsonString
|
|
id: int
|
|
|
|
HasJsonNode = object
|
|
name: string
|
|
data: JsonNode
|
|
id: int
|
|
|
|
HasCstring = object
|
|
notNilStr: cstring
|
|
nilStr: cstring
|
|
|
|
# Customised parser tests
|
|
FancyInt = distinct int
|
|
FancyUInt = distinct uint
|
|
FancyText = distinct string
|
|
|
|
HasFancyInt = object
|
|
name: string
|
|
data: FancyInt
|
|
|
|
HasFancyUInt = object
|
|
name: string
|
|
data: FancyUInt
|
|
|
|
HasFancyText = object
|
|
name: string
|
|
data: FancyText
|
|
|
|
TokenRegistry = tuple
|
|
entry, exit: TokKind
|
|
dup: bool
|
|
|
|
HoldsResultOpt* = object
|
|
o*: Opt[Simple]
|
|
r*: ref Simple
|
|
|
|
WithCustomFieldRule* = object
|
|
str*: string
|
|
intVal*: int
|
|
|
|
OtherOptionTest* = object
|
|
a*: Option[Meter]
|
|
b*: Option[Meter]
|
|
|
|
NestedOptionTest* = object
|
|
c*: Option[OtherOptionTest]
|
|
d*: Option[OtherOptionTest]
|
|
|
|
SeqOptionTest* = object
|
|
a*: seq[Option[Meter]]
|
|
b*: Meter
|
|
|
|
OtherOptionTest2* = object
|
|
a*: Option[Meter]
|
|
b*: Option[Meter]
|
|
c*: Option[Meter]
|
|
|
|
proc readValue*(r: var JsonReader[DefaultFlavor], value: var CaseObject)
|
|
{.gcsafe, raises: [SerializationError, IOError].}
|
|
|
|
template readValueImpl(r: var JsonReader, value: var CaseObject) =
|
|
var
|
|
kindSpecified = false
|
|
valueSpecified = false
|
|
otherSpecified = false
|
|
|
|
for fieldName in readObjectFields(r):
|
|
case fieldName
|
|
of "kind":
|
|
value = CaseObject(kind: r.readValue(ObjectKind))
|
|
kindSpecified = true
|
|
case value.kind
|
|
of A:
|
|
discard
|
|
of B:
|
|
otherSpecified = true
|
|
|
|
of "a":
|
|
if kindSpecified:
|
|
case value.kind
|
|
of A:
|
|
r.readValue(value.a)
|
|
of B:
|
|
r.raiseUnexpectedValue(
|
|
"The 'a' field is only allowed for 'kind' = 'A'")
|
|
else:
|
|
r.raiseUnexpectedValue(
|
|
"The 'a' field must be specified after the 'kind' field")
|
|
valueSpecified = true
|
|
|
|
of "other":
|
|
if kindSpecified:
|
|
case value.kind
|
|
of A:
|
|
r.readValue(value.other)
|
|
of B:
|
|
r.raiseUnexpectedValue(
|
|
"The 'other' field is only allowed for 'kind' = 'A'")
|
|
else:
|
|
r.raiseUnexpectedValue(
|
|
"The 'other' field must be specified after the 'kind' field")
|
|
otherSpecified = true
|
|
|
|
of "b":
|
|
if kindSpecified:
|
|
case value.kind
|
|
of B:
|
|
r.readValue(value.b)
|
|
of A:
|
|
r.raiseUnexpectedValue(
|
|
"The 'b' field is only allowed for 'kind' = 'B'")
|
|
else:
|
|
r.raiseUnexpectedValue(
|
|
"The 'b' field must be specified after the 'kind' field")
|
|
valueSpecified = true
|
|
|
|
else:
|
|
r.raiseUnexpectedField(fieldName, "CaseObject")
|
|
|
|
if not (kindSpecified and valueSpecified and otherSpecified):
|
|
r.raiseUnexpectedValue(
|
|
"The CaseObject value should have sub-fields named " &
|
|
"'kind', and ('a' and 'other') or 'b' depending on 'kind'")
|
|
|
|
{.push warning[ProveField]:off.} # https://github.com/nim-lang/Nim/issues/22060
|
|
proc readValue*(r: var JsonReader[DefaultFlavor], value: var CaseObject)
|
|
{.raises: [SerializationError, IOError].} =
|
|
readValueImpl(r, value)
|
|
{.pop.}
|
|
|
|
template readValueImpl(r: var JsonReader, value: var MyCaseObject) =
|
|
var
|
|
nameSpecified = false
|
|
kindSpecified = false
|
|
valueSpecified = false
|
|
|
|
for fieldName in readObjectFields(r):
|
|
case fieldName
|
|
of "name":
|
|
r.readValue(value.name)
|
|
nameSpecified = true
|
|
|
|
of "kind":
|
|
value = MyCaseObject(kind: r.readValue(MyKind), name: value.name)
|
|
kindSpecified = true
|
|
|
|
of "banana":
|
|
if kindSpecified:
|
|
case value.kind
|
|
of Banana:
|
|
r.readValue(value.banana)
|
|
of Apple:
|
|
r.raiseUnexpectedValue(
|
|
"The 'banana' field is only allowed for 'kind' = 'Banana'")
|
|
else:
|
|
r.raiseUnexpectedValue(
|
|
"The 'banana' field must be specified after the 'kind' field")
|
|
valueSpecified = true
|
|
|
|
of "apple":
|
|
if kindSpecified:
|
|
case value.kind
|
|
of Apple:
|
|
r.readValue(value.apple)
|
|
of Banana:
|
|
r.raiseUnexpectedValue(
|
|
"The 'apple' field is only allowed for 'kind' = 'Apple'")
|
|
else:
|
|
r.raiseUnexpectedValue(
|
|
"The 'apple' field must be specified after the 'kind' field")
|
|
valueSpecified = true
|
|
|
|
else:
|
|
r.raiseUnexpectedField(fieldName, "MyCaseObject")
|
|
|
|
if not (nameSpecified and kindSpecified and valueSpecified):
|
|
r.raiseUnexpectedValue(
|
|
"The MyCaseObject value should have sub-fields named " &
|
|
"'name', 'kind', and 'banana' or 'apple' depending on 'kind'")
|
|
|
|
{.push warning[ProveField]:off.} # https://github.com/nim-lang/Nim/issues/22060
|
|
proc readValue*(r: var JsonReader[DefaultFlavor], value: var MyCaseObject)
|
|
{.raises: [SerializationError, IOError].} =
|
|
readValueImpl(r, value)
|
|
{.pop.}
|
|
|
|
var
|
|
customVisit: TokenRegistry
|
|
|
|
Json.useCustomSerialization(WithCustomFieldRule.intVal):
|
|
read:
|
|
try:
|
|
parseInt reader.readValue(string)
|
|
except ValueError:
|
|
reader.raiseUnexpectedValue("string encoded integer expected")
|
|
write:
|
|
writer.writeValue $value
|
|
|
|
template registerVisit(reader: var JsonReader; body: untyped): untyped =
|
|
if customVisit.entry == tkError:
|
|
customVisit.entry = reader.lexer.lazyTok
|
|
body
|
|
customVisit.exit = reader.lexer.lazyTok
|
|
else:
|
|
customVisit.dup = true
|
|
|
|
# Customised parser referring to other parser
|
|
proc readValue(reader: var JsonReader, value: var FancyInt) =
|
|
try:
|
|
reader.registerVisit:
|
|
value = reader.readValue(int).FancyInt
|
|
except ValueError:
|
|
reader.raiseUnexpectedValue("string encoded integer expected")
|
|
|
|
# Customised numeric parser for integer and stringified integer
|
|
proc readValue(reader: var JsonReader, value: var FancyUInt) =
|
|
try:
|
|
reader.registerVisit:
|
|
var accu = 0u
|
|
case reader.lexer.lazyTok
|
|
of tkNumeric:
|
|
reader.lexer.customIntValueIt:
|
|
accu = accu * 10u + it.uint
|
|
of tkQuoted:
|
|
var s = ""
|
|
reader.lexer.customTextValueIt:
|
|
s &= it
|
|
accu = s.parseUInt
|
|
else:
|
|
discard
|
|
value = accu.FancyUInt
|
|
reader.lexer.next
|
|
except ValueError:
|
|
reader.raiseUnexpectedValue("string encoded integer expected")
|
|
|
|
# Customised numeric parser for text, accepts embedded quote
|
|
proc readValue(reader: var JsonReader, value: var FancyText) =
|
|
try:
|
|
reader.registerVisit:
|
|
var (s, esc) = ("",false)
|
|
reader.lexer.customBlobValueIt:
|
|
let c = it.chr
|
|
if esc:
|
|
s &= c
|
|
esc = false
|
|
elif c == '\\':
|
|
esc = true
|
|
elif c != '"':
|
|
s &= c
|
|
else:
|
|
doNext = StopSwallowByte
|
|
value = s.FancyText
|
|
reader.lexer.next
|
|
except ValueError:
|
|
reader.raiseUnexpectedValue("string encoded integer expected")
|
|
|
|
|
|
# TODO `borrowSerialization` still doesn't work
|
|
# properly when it's placed in another module:
|
|
Meter.borrowSerialization int
|
|
|
|
template reject(code) {.used.} =
|
|
static: doAssert(not compiles(code))
|
|
|
|
func `==`(lhs, rhs: Meter): bool =
|
|
int(lhs) == int(rhs)
|
|
|
|
func `==`(lhs, rhs: ref Simple): bool =
|
|
if lhs.isNil: return rhs.isNil
|
|
if rhs.isNil: return false
|
|
lhs[] == rhs[]
|
|
|
|
executeReaderWriterTests Json
|
|
|
|
func newSimple(x: int, y: string, d: Meter): ref Simple =
|
|
(ref Simple)(x: x, y: y, distance: d)
|
|
|
|
var invalid = Invalid(distance: Mile(100))
|
|
# The compiler cannot handle this check at the moment
|
|
# {.fatal.} seems fatal even in `compiles` context
|
|
when false: reject invalid.toJson
|
|
else: discard invalid
|
|
|
|
type EnumTestX = enum
|
|
x0,
|
|
x1,
|
|
x2
|
|
|
|
type EnumTestY = enum
|
|
y1 = 1,
|
|
y3 = 3,
|
|
y4,
|
|
y6 = 6
|
|
EnumTestY.configureJsonDeserialization(
|
|
allowNumericRepr = true)
|
|
|
|
type EnumTestZ = enum
|
|
z1 = "aaa",
|
|
z2 = "bbb",
|
|
z3 = "ccc"
|
|
|
|
type EnumTestN = enum
|
|
n1 = "aaa",
|
|
n2 = "bbb",
|
|
n3 = "ccc"
|
|
EnumTestN.configureJsonDeserialization(
|
|
stringNormalizer = nimIdentNormalize)
|
|
|
|
type EnumTestO = enum
|
|
o1,
|
|
o2,
|
|
o3
|
|
EnumTestO.configureJsonDeserialization(
|
|
allowNumericRepr = true,
|
|
stringNormalizer = nimIdentNormalize)
|
|
|
|
createJsonFlavor MyJson
|
|
|
|
type
|
|
HasMyJsonDefaultBehavior = object
|
|
x*: int
|
|
y*: string
|
|
|
|
HasMyJsonOverride = object
|
|
x*: int
|
|
y*: string
|
|
|
|
HasMyJsonDefaultBehavior.useDefaultSerializationIn MyJson
|
|
|
|
proc readValue*(r: var JsonReader[MyJson], value: var HasMyJsonOverride) =
|
|
r.readRecordValue(value)
|
|
|
|
proc writeValue*(w: var JsonWriter[MyJson], value: HasMyJsonOverride) =
|
|
w.writeRecordValue(value)
|
|
|
|
suite "toJson tests":
|
|
test "encode primitives":
|
|
check:
|
|
1.toJson == "1"
|
|
"".toJson == "\"\""
|
|
"abc".toJson == "\"abc\""
|
|
|
|
test "enums":
|
|
Json.roundtripTest x0, "\"x0\""
|
|
Json.roundtripTest x1, "\"x1\""
|
|
Json.roundtripTest x2, "\"x2\""
|
|
expect UnexpectedTokenError:
|
|
discard Json.decode("0", EnumTestX)
|
|
expect UnexpectedTokenError:
|
|
discard Json.decode("1", EnumTestX)
|
|
expect UnexpectedTokenError:
|
|
discard Json.decode("2", EnumTestX)
|
|
expect UnexpectedTokenError:
|
|
discard Json.decode("3", EnumTestX)
|
|
expect UnexpectedValueError:
|
|
discard Json.decode("\"X0\"", EnumTestX)
|
|
expect UnexpectedValueError:
|
|
discard Json.decode("\"X1\"", EnumTestX)
|
|
expect UnexpectedValueError:
|
|
discard Json.decode("\"X2\"", EnumTestX)
|
|
expect UnexpectedValueError:
|
|
discard Json.decode("\"x_0\"", EnumTestX)
|
|
expect UnexpectedValueError:
|
|
discard Json.decode("\"\"", EnumTestX)
|
|
expect UnexpectedValueError:
|
|
discard Json.decode("\"0\"", EnumTestX)
|
|
|
|
Json.roundtripTest y1, "\"y1\""
|
|
Json.roundtripTest y3, "\"y3\""
|
|
Json.roundtripTest y4, "\"y4\""
|
|
Json.roundtripTest y6, "\"y6\""
|
|
check:
|
|
Json.decode("1", EnumTestY) == y1
|
|
Json.decode("3", EnumTestY) == y3
|
|
Json.decode("4", EnumTestY) == y4
|
|
Json.decode("6", EnumTestY) == y6
|
|
expect UnexpectedValueError:
|
|
discard Json.decode("0", EnumTestY)
|
|
expect UnexpectedValueError:
|
|
discard Json.decode("2", EnumTestY)
|
|
expect UnexpectedValueError:
|
|
discard Json.decode("5", EnumTestY)
|
|
expect UnexpectedValueError:
|
|
discard Json.decode("7", EnumTestY)
|
|
expect UnexpectedValueError:
|
|
discard Json.decode("\"Y1\"", EnumTestY)
|
|
expect UnexpectedValueError:
|
|
discard Json.decode("\"Y3\"", EnumTestY)
|
|
expect UnexpectedValueError:
|
|
discard Json.decode("\"Y4\"", EnumTestY)
|
|
expect UnexpectedValueError:
|
|
discard Json.decode("\"Y6\"", EnumTestY)
|
|
expect UnexpectedValueError:
|
|
discard Json.decode("\"y_1\"", EnumTestY)
|
|
expect UnexpectedValueError:
|
|
discard Json.decode("\"\"", EnumTestY)
|
|
expect UnexpectedValueError:
|
|
discard Json.decode("\"1\"", EnumTestY)
|
|
|
|
Json.roundtripTest z1, "\"aaa\""
|
|
Json.roundtripTest z2, "\"bbb\""
|
|
Json.roundtripTest z3, "\"ccc\""
|
|
expect UnexpectedTokenError:
|
|
discard Json.decode("0", EnumTestZ)
|
|
expect UnexpectedValueError:
|
|
discard Json.decode("\"AAA\"", EnumTestZ)
|
|
expect UnexpectedValueError:
|
|
discard Json.decode("\"BBB\"", EnumTestZ)
|
|
expect UnexpectedValueError:
|
|
discard Json.decode("\"CCC\"", EnumTestZ)
|
|
expect UnexpectedValueError:
|
|
discard Json.decode("\"z1\"", EnumTestZ)
|
|
expect UnexpectedValueError:
|
|
discard Json.decode("\"a_a_a\"", EnumTestZ)
|
|
expect UnexpectedValueError:
|
|
discard Json.decode("\"\"", EnumTestZ)
|
|
expect UnexpectedValueError:
|
|
discard Json.decode("\"\ud83d\udc3c\"", EnumTestZ)
|
|
|
|
Json.roundtripTest n1, "\"aaa\""
|
|
Json.roundtripTest n2, "\"bbb\""
|
|
Json.roundtripTest n3, "\"ccc\""
|
|
check:
|
|
Json.decode("\"aAA\"", EnumTestN) == n1
|
|
Json.decode("\"bBB\"", EnumTestN) == n2
|
|
Json.decode("\"cCC\"", EnumTestN) == n3
|
|
Json.decode("\"a_a_a\"", EnumTestN) == n1
|
|
Json.decode("\"b_b_b\"", EnumTestN) == n2
|
|
Json.decode("\"c_c_c\"", EnumTestN) == n3
|
|
expect UnexpectedTokenError:
|
|
discard Json.decode("0", EnumTestN)
|
|
expect UnexpectedValueError:
|
|
discard Json.decode("\"AAA\"", EnumTestN)
|
|
expect UnexpectedValueError:
|
|
discard Json.decode("\"BBB\"", EnumTestN)
|
|
expect UnexpectedValueError:
|
|
discard Json.decode("\"CCC\"", EnumTestN)
|
|
expect UnexpectedValueError:
|
|
discard Json.decode("\"Aaa\"", EnumTestN)
|
|
expect UnexpectedValueError:
|
|
discard Json.decode("\"Bbb\"", EnumTestN)
|
|
expect UnexpectedValueError:
|
|
discard Json.decode("\"Ccc\"", EnumTestN)
|
|
expect UnexpectedValueError:
|
|
discard Json.decode("\"n1\"", EnumTestN)
|
|
expect UnexpectedValueError:
|
|
discard Json.decode("\"_aaa\"", EnumTestN)
|
|
expect UnexpectedValueError:
|
|
discard Json.decode("\"\"", EnumTestN)
|
|
expect UnexpectedValueError:
|
|
discard Json.decode("\"\ud83d\udc3c\"", EnumTestN)
|
|
|
|
Json.roundtripTest o1, "\"o1\""
|
|
Json.roundtripTest o2, "\"o2\""
|
|
Json.roundtripTest o3, "\"o3\""
|
|
check:
|
|
Json.decode("\"o_1\"", EnumTestO) == o1
|
|
Json.decode("\"o_2\"", EnumTestO) == o2
|
|
Json.decode("\"o_3\"", EnumTestO) == o3
|
|
Json.decode("0", EnumTestO) == o1
|
|
Json.decode("1", EnumTestO) == o2
|
|
Json.decode("2", EnumTestO) == o3
|
|
expect UnexpectedValueError:
|
|
discard Json.decode("3", EnumTestO)
|
|
expect UnexpectedValueError:
|
|
discard Json.decode("\"O1\"", EnumTestO)
|
|
expect UnexpectedValueError:
|
|
discard Json.decode("\"O2\"", EnumTestO)
|
|
expect UnexpectedValueError:
|
|
discard Json.decode("\"O3\"", EnumTestO)
|
|
expect UnexpectedValueError:
|
|
discard Json.decode("\"_o1\"", EnumTestO)
|
|
expect UnexpectedValueError:
|
|
discard Json.decode("\"\"", EnumTestO)
|
|
expect UnexpectedValueError:
|
|
discard Json.decode("\"\ud83d\udc3c\"", EnumTestO)
|
|
|
|
test "simple objects":
|
|
var s = Simple(x: 10, y: "test", distance: Meter(20))
|
|
|
|
check:
|
|
s.toJson == """{"distance":20,"x":10,"y":"test"}"""
|
|
s.toJson(typeAnnotations = true) == """{"$type":"Simple","distance":20,"x":10,"y":"test"}"""
|
|
s.toJson(pretty = true) == test_dedent"""
|
|
{
|
|
"distance": 20,
|
|
"x": 10,
|
|
"y": "test"
|
|
}
|
|
"""
|
|
|
|
test "handle missing fields":
|
|
let json = test_dedent"""
|
|
{
|
|
"distance": 20,
|
|
"y": "test"
|
|
}
|
|
"""
|
|
|
|
let decoded = Json.decode(json, Simple)
|
|
|
|
check:
|
|
decoded.x == 0
|
|
decoded.y == "test"
|
|
decoded.distance.int == 20
|
|
|
|
test "Custom flavor with explicit serialization":
|
|
var s = Simple(x: 10, y: "test", distance: Meter(20))
|
|
|
|
reject:
|
|
discard MyJson.encode(s)
|
|
|
|
let hasDefaultBehavior = HasMyJsonDefaultBehavior(x: 10, y: "test")
|
|
let hasOverride = HasMyJsonOverride(x: 10, y: "test")
|
|
|
|
let json1 = MyJson.encode(hasDefaultBehavior)
|
|
let json2 = MyJson.encode(hasOverride)
|
|
|
|
reject:
|
|
let decodedAsMyJson = MyJson.decode(json2, Simple)
|
|
|
|
check:
|
|
json1 == """{"x":10,"y":"test"}"""
|
|
json2 == """{"x":10,"y":"test"}"""
|
|
|
|
MyJson.decode(json1, HasMyJsonDefaultBehavior) == hasDefaultBehavior
|
|
MyJson.decode(json2, HasMyJsonOverride) == hasOverride
|
|
|
|
test "handle additional fields":
|
|
let json = test_dedent"""
|
|
{
|
|
"x": -20,
|
|
"futureObject": {"a": -1, "b": [1, 2.0, 3.1], "c": null, "d": true},
|
|
"futureBool": false,
|
|
"y": "y value"
|
|
}
|
|
"""
|
|
|
|
let decoded = Json.decode(json, Simple, allowUnknownFields = true)
|
|
|
|
check:
|
|
decoded.x == -20
|
|
decoded.y == "y value"
|
|
decoded.distance.int == 0
|
|
|
|
expect UnexpectedField:
|
|
let shouldNotDecode = Json.decode(json, Simple)
|
|
echo "This should not have decoded ", shouldNotDecode
|
|
|
|
test "all fields are required and present":
|
|
let json = test_dedent"""
|
|
{
|
|
"x": 20,
|
|
"distance": 10,
|
|
"y": "y value"
|
|
}
|
|
"""
|
|
|
|
let decoded = Json.decode(json, Simple, requireAllFields = true)
|
|
|
|
check:
|
|
decoded.x == 20
|
|
decoded.y == "y value"
|
|
decoded.distance.int == 10
|
|
|
|
test "all fields were required, but not all were provided":
|
|
let json = test_dedent"""
|
|
{
|
|
"x": -20,
|
|
"distance": 10
|
|
}
|
|
"""
|
|
|
|
expect IncompleteObjectError:
|
|
let shouldNotDecode = Json.decode(json, Simple, requireAllFields = true)
|
|
echo "This should not have decoded ", shouldNotDecode
|
|
|
|
test "all fields were required, but not all were provided (additional fields present instead)":
|
|
let json = test_dedent"""
|
|
{
|
|
"futureBool": false,
|
|
"y": "y value",
|
|
"futureObject": {"a": -1, "b": [1, 2.0, 3.1], "c": null, "d": true},
|
|
"distance": 10
|
|
}
|
|
"""
|
|
|
|
expect IncompleteObjectError:
|
|
let shouldNotDecode = Json.decode(json, Simple,
|
|
requireAllFields = true,
|
|
allowUnknownFields = true)
|
|
echo "This should not have decoded ", shouldNotDecode
|
|
|
|
test "all fields were required, but none were provided":
|
|
let json = "{}"
|
|
|
|
expect IncompleteObjectError:
|
|
let shouldNotDecode = Json.decode(json, Simple, requireAllFields = true)
|
|
echo "This should not have decoded ", shouldNotDecode
|
|
|
|
test "all fields are required and provided, and additional ones are present":
|
|
let json = test_dedent"""
|
|
{
|
|
"x": 20,
|
|
"distance": 10,
|
|
"futureBool": false,
|
|
"y": "y value",
|
|
"futureObject": {"a": -1, "b": [1, 2.0, 3.1], "c": null, "d": true},
|
|
}
|
|
"""
|
|
|
|
let decoded = try:
|
|
Json.decode(json, Simple, requireAllFields = true, allowUnknownFields = true)
|
|
except SerializationError as err:
|
|
checkpoint "Unexpected deserialization failure: " & err.formatMsg("<input>")
|
|
raise
|
|
|
|
check:
|
|
decoded.x == 20
|
|
decoded.y == "y value"
|
|
decoded.distance.int == 10
|
|
|
|
expect UnexpectedField:
|
|
let shouldNotDecode = Json.decode(json, Simple,
|
|
requireAllFields = true,
|
|
allowUnknownFields = false)
|
|
echo "This should not have decoded ", shouldNotDecode
|
|
|
|
test "arrays are printed correctly":
|
|
var x = HoldsArray(data: @[1, 2, 3, 4])
|
|
|
|
check:
|
|
x.toJson(pretty = true) == test_dedent"""
|
|
{
|
|
"data": [
|
|
1,
|
|
2,
|
|
3,
|
|
4
|
|
]
|
|
}
|
|
"""
|
|
|
|
test "max unsigned value":
|
|
var uintVal = not BiggestUint(0)
|
|
let jsonValue = Json.encode(uintVal)
|
|
check:
|
|
jsonValue == "18446744073709551615"
|
|
Json.decode(jsonValue, BiggestUint) == uintVal
|
|
|
|
expect JsonReaderError:
|
|
discard Json.decode(jsonValue, BiggestUint, mode = Portable)
|
|
|
|
test "max signed value":
|
|
let intVal = BiggestInt.high
|
|
let validJsonValue = Json.encode(intVal)
|
|
let invalidJsonValue = "9223372036854775808"
|
|
check:
|
|
validJsonValue == "9223372036854775807"
|
|
Json.decode(validJsonValue, BiggestInt) == intVal
|
|
|
|
expect IntOverflowError:
|
|
discard Json.decode(invalidJsonValue, BiggestInt)
|
|
|
|
test "min signed value":
|
|
let intVal = BiggestInt.low
|
|
let validJsonValue = Json.encode(intVal)
|
|
let invalidJsonValue = "-9223372036854775809"
|
|
check:
|
|
validJsonValue == "-9223372036854775808"
|
|
Json.decode(validJsonValue, BiggestInt) == intVal
|
|
|
|
expect IntOverflowError:
|
|
discard Json.decode(invalidJsonValue, BiggestInt)
|
|
|
|
test "Unusual field names":
|
|
let r = HasUnusualFieldNames(`type`: "uint8", renamedField: "field")
|
|
check:
|
|
r.toJson == """{"type":"uint8","renamed":"field"}"""
|
|
r == Json.decode("""{"type":"uint8", "renamed":"field"}""", HasUnusualFieldNames)
|
|
|
|
test "Option types":
|
|
check:
|
|
2 == static(HoldsOption.totalSerializedFields)
|
|
1 == static(HoldsOption.totalExpectedFields)
|
|
|
|
2 == static(Foo.totalSerializedFields)
|
|
2 == static(Foo.totalExpectedFields)
|
|
|
|
let
|
|
h1 = HoldsOption(o: some Simple(x: 1, y: "2", distance: Meter(3)))
|
|
h2 = HoldsOption(r: newSimple(1, "2", Meter(3)))
|
|
h3 = Json.decode("""{"r":{"distance":3,"x":1,"y":"2"}}""",
|
|
HoldsOption, requireAllFields = true)
|
|
|
|
Json.roundtripTest h1, """{"r":null,"o":{"distance":3,"x":1,"y":"2"}}"""
|
|
Json.roundtripTest h2, """{"r":{"distance":3,"x":1,"y":"2"}}"""
|
|
|
|
check h3 == h2
|
|
|
|
expect SerializationError:
|
|
let h4 = Json.decode("""{"o":{"distance":3,"x":1,"y":"2"}}""",
|
|
HoldsOption, requireAllFields = true)
|
|
|
|
test "Nested option types":
|
|
let
|
|
h3 = OtherOptionTest()
|
|
h4 = OtherOptionTest(a: some Meter(1))
|
|
h5 = OtherOptionTest(b: some Meter(2))
|
|
h6 = OtherOptionTest(a: some Meter(3), b: some Meter(4))
|
|
|
|
Json.roundtripTest h3, """{}"""
|
|
Json.roundtripTest h4, """{"a":1}"""
|
|
Json.roundtripTest h5, """{"b":2}"""
|
|
Json.roundtripTest h6, """{"a":3,"b":4}"""
|
|
|
|
let
|
|
arr = @[some h3, some h4, some h5, some h6, none(OtherOptionTest)]
|
|
results = @[
|
|
"""{"c":{},"d":{}}""",
|
|
"""{"c":{},"d":{"a":1}}""",
|
|
"""{"c":{},"d":{"b":2}}""",
|
|
"""{"c":{},"d":{"a":3,"b":4}}""",
|
|
"""{"c":{}}""",
|
|
"""{"c":{"a":1},"d":{}}""",
|
|
"""{"c":{"a":1},"d":{"a":1}}""",
|
|
"""{"c":{"a":1},"d":{"b":2}}""",
|
|
"""{"c":{"a":1},"d":{"a":3,"b":4}}""",
|
|
"""{"c":{"a":1}}""",
|
|
"""{"c":{"b":2},"d":{}}""",
|
|
"""{"c":{"b":2},"d":{"a":1}}""",
|
|
"""{"c":{"b":2},"d":{"b":2}}""",
|
|
"""{"c":{"b":2},"d":{"a":3,"b":4}}""",
|
|
"""{"c":{"b":2}}""",
|
|
"""{"c":{"a":3,"b":4},"d":{}}""",
|
|
"""{"c":{"a":3,"b":4},"d":{"a":1}}""",
|
|
"""{"c":{"a":3,"b":4},"d":{"b":2}}""",
|
|
"""{"c":{"a":3,"b":4},"d":{"a":3,"b":4}}""",
|
|
"""{"c":{"a":3,"b":4}}""",
|
|
"""{"d":{}}""",
|
|
"""{"d":{"a":1}}""",
|
|
"""{"d":{"b":2}}""",
|
|
"""{"d":{"a":3,"b":4}}""",
|
|
"""{}"""
|
|
]
|
|
|
|
|
|
var r = 0
|
|
for a in arr:
|
|
for b in arr:
|
|
# lent iterator error
|
|
let a = a
|
|
let b = b
|
|
Json.roundtripTest NestedOptionTest(c: a, d: b), results[r]
|
|
r.inc
|
|
|
|
Json.roundtripTest SeqOptionTest(a: @[some 5.Meter, none Meter], b: Meter(5)), """{"a":[5,null],"b":5}"""
|
|
Json.roundtripTest OtherOptionTest2(a: some 5.Meter, b: none Meter, c: some 10.Meter), """{"a":5,"c":10}"""
|
|
|
|
test "Result Opt types":
|
|
check:
|
|
false == static(isFieldExpected Opt[Simple])
|
|
2 == static(HoldsResultOpt.totalSerializedFields)
|
|
1 == static(HoldsResultOpt.totalExpectedFields)
|
|
|
|
let
|
|
h1 = HoldsResultOpt(o: Opt[Simple].ok Simple(x: 1, y: "2", distance: Meter(3)))
|
|
h2 = HoldsResultOpt(r: newSimple(1, "2", Meter(3)))
|
|
|
|
Json.roundtripTest h1, """{"o":{"distance":3,"x":1,"y":"2"},"r":null}"""
|
|
Json.roundtripTest h2, """{"r":{"distance":3,"x":1,"y":"2"}}"""
|
|
|
|
let
|
|
h3 = Json.decode("""{"r":{"distance":3,"x":1,"y":"2"}}""",
|
|
HoldsResultOpt, requireAllFields = true)
|
|
|
|
check h3 == h2
|
|
|
|
expect SerializationError:
|
|
let h4 = Json.decode("""{"o":{"distance":3,"x":1,"y":"2"}}""",
|
|
HoldsResultOpt, requireAllFields = true)
|
|
|
|
test "Custom field serialization":
|
|
let obj = WithCustomFieldRule(str: "test", intVal: 10)
|
|
Json.roundtripTest obj, """{"str":"test","intVal":"10"}"""
|
|
|
|
test "Case object as field":
|
|
let
|
|
original = MyUseCaseObject(field: MyCaseObject(name: "hello",
|
|
kind: Apple,
|
|
apple: "world"))
|
|
decoded = Json.decode(Json.encode(original), MyUseCaseObject)
|
|
|
|
check:
|
|
$original == $decoded
|
|
|
|
test "stringLike":
|
|
check:
|
|
"abc" == Json.decode(Json.encode(['a', 'b', 'c']), string)
|
|
"abc" == Json.decode(Json.encode(@['a', 'b', 'c']), string)
|
|
['a', 'b', 'c'] == Json.decode(Json.encode(@['a', 'b', 'c']), seq[char])
|
|
['a', 'b', 'c'] == Json.decode(Json.encode("abc"), seq[char])
|
|
['a', 'b', 'c'] == Json.decode(Json.encode(@['a', 'b', 'c']), array[3, char])
|
|
|
|
expect JsonReaderError: # too short
|
|
discard Json.decode(Json.encode(@['a', 'b']), array[3, char])
|
|
|
|
expect JsonReaderError: # too long
|
|
discard Json.decode(Json.encode(@['a', 'b']), array[1, char])
|
|
|
|
proc testJsonHolders(HasJsonData: type) =
|
|
let
|
|
data1 = test_dedent"""
|
|
{
|
|
"name": "Data 1",
|
|
"data": [1, 2, 3, 4],
|
|
"id": 101
|
|
}
|
|
"""
|
|
let
|
|
data2 = test_dedent"""
|
|
{
|
|
"name": "Data 2",
|
|
"data": "some string",
|
|
"id": 1002
|
|
}
|
|
"""
|
|
let
|
|
data3 = test_dedent"""
|
|
{
|
|
"name": "Data 3",
|
|
"data": {"field1": 10, "field2": [1, 2, 3], "field3": "test"},
|
|
"id": 10003
|
|
}
|
|
"""
|
|
|
|
try:
|
|
let
|
|
d1 = Json.decode(data1, HasJsonData)
|
|
d2 = Json.decode(data2, HasJsonData)
|
|
d3 = Json.decode(data3, HasJsonData)
|
|
|
|
check:
|
|
d1.name == "Data 1"
|
|
$d1.data == "[1,2,3,4]"
|
|
d1.id == 101
|
|
|
|
d2.name == "Data 2"
|
|
$d2.data == "\"some string\""
|
|
d2.id == 1002
|
|
|
|
d3.name == "Data 3"
|
|
$d3.data == """{"field1":10,"field2":[1,2,3],"field3":"test"}"""
|
|
d3.id == 10003
|
|
|
|
let
|
|
d1Encoded = Json.encode(d1)
|
|
d2Encoded = Json.encode(d2)
|
|
d3Encoded = Json.encode(d3)
|
|
|
|
check:
|
|
d1Encoded == $parseJson(data1)
|
|
d2Encoded == $parseJson(data2)
|
|
d3Encoded == $parseJson(data3)
|
|
|
|
except SerializationError as e:
|
|
echo e.getStackTrace
|
|
echo e.formatMsg("<>")
|
|
raise e
|
|
|
|
test "Holders of JsonString":
|
|
testJsonHolders HasJsonString
|
|
|
|
test "Holders of JsonNode":
|
|
testJsonHolders HasJsonNode
|
|
|
|
test "Json with comments":
|
|
const jsonContent = staticRead "./cases/comments.json"
|
|
|
|
try:
|
|
let decoded = Json.decode(jsonContent, JsonNode)
|
|
check decoded["tasks"][0]["label"] == newJString("nim-beacon-chain build")
|
|
except SerializationError as err:
|
|
checkpoint err.formatMsg("./cases/comments.json")
|
|
check false
|
|
|
|
test "A nil cstring":
|
|
let
|
|
obj1 = HasCstring(notNilStr: "foo", nilStr: nil)
|
|
obj2 = HasCstring(notNilStr: "", nilStr: nil)
|
|
str: cstring = "some value"
|
|
|
|
check:
|
|
Json.encode(obj1) == """{"notNilStr":"foo","nilStr":null}"""
|
|
Json.encode(obj2) == """{"notNilStr":"","nilStr":null}"""
|
|
Json.encode(str) == "\"some value\""
|
|
Json.encode(cstring nil) == "null"
|
|
|
|
reject:
|
|
# Decoding cstrings is not supported due to lack of
|
|
# clarity regarding the memory allocation approach
|
|
Json.decode("null", cstring)
|
|
|
|
suite "Custom parser tests":
|
|
test "Fall back to int parser":
|
|
customVisit = TokenRegistry.default
|
|
|
|
let
|
|
jData = test_dedent"""
|
|
{
|
|
"name": "FancyInt",
|
|
"data": -12345
|
|
}
|
|
"""
|
|
dData = Json.decode(jData, HasFancyInt)
|
|
|
|
check dData.name == "FancyInt"
|
|
check dData.data.int == -12345
|
|
check customVisit == (tkNumeric, tkCurlyRi, false)
|
|
|
|
test "Uint parser on negative integer":
|
|
customVisit = TokenRegistry.default
|
|
|
|
let
|
|
jData = test_dedent"""
|
|
{
|
|
"name": "FancyUInt",
|
|
"data": -12345
|
|
}
|
|
"""
|
|
dData = Json.decode(jData, HasFancyUInt)
|
|
|
|
check dData.name == "FancyUInt"
|
|
check dData.data.uint == 12345u # abs value
|
|
check customVisit == (tkNumeric, tkExNegInt, false)
|
|
|
|
test "Uint parser on string integer":
|
|
customVisit = TokenRegistry.default
|
|
|
|
let
|
|
jData = test_dedent"""
|
|
{
|
|
"name": "FancyUInt",
|
|
"data": "12345"
|
|
}
|
|
"""
|
|
dData = Json.decode(jData, HasFancyUInt)
|
|
|
|
check dData.name == "FancyUInt"
|
|
check dData.data.uint == 12345u
|
|
check customVisit == (tkQuoted, tkExBlob, false)
|
|
|
|
test "Parser on text blob with embedded quote (backlash escape support)":
|
|
customVisit = TokenRegistry.default
|
|
|
|
let
|
|
jData = test_dedent"""
|
|
{
|
|
"name": "FancyText",
|
|
"data": "a\bc\"\\def"
|
|
}
|
|
"""
|
|
dData = Json.decode(jData, HasFancyText)
|
|
|
|
check dData.name == "FancyText"
|
|
check dData.data.string == "abc\"\\def"
|
|
check customVisit == (tkQuoted, tkExBlob, false)
|