nim-json-serialization/tests/test_serialization.nim

1007 lines
28 KiB
Nim

# json-serialization
# Copyright (c) 2019-2023 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
# at your option.
# This file may not be copied, modified, or distributed except according to
# those terms.
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: JsonValueKind
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 =
customVisit.entry = reader.tokKind
body
# 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.tokKind
of JsonValueKind.Number:
reader.customIntValueIt:
accu = accu * 10u + it.uint
of JsonValueKind.String:
var s = ""
reader.customStringValueIt:
s &= it
accu = s.parseUInt
else:
discard
value = accu.FancyUInt
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 = ""
reader.customStringValueIt:
s.add it
value = s.FancyText
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, flags = {JsonReaderFlag.portableInt})
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)
discard h4
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)
discard h4
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("nimbus-eth2 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.entry == JsonValueKind.Number
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.entry == JsonValueKind.Number
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.entry == JsonValueKind.String
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 == "a\bc\"\\def"
check customVisit.entry == JsonValueKind.String