From 10271bd4947802f222f409c75487ba9c81fe0df9 Mon Sep 17 00:00:00 2001 From: Eric <5089238+emizzle@users.noreply.github.com> Date: Thu, 16 May 2024 17:57:42 +1000 Subject: [PATCH] feat: improve deserialization from string (#23) Improves how types handle deserialization from string, including Option[T], seq[T], and Option[seq[T]] Empty and null strings are no longer deserialized to 0, instead an error Result is returned If an error occurs when parsing json, a JsonParseError is returned. --- serde/json/deserializer.nim | 66 ++++++++++++- serde/json/parser.nim | 5 +- tests/json/deserialize/std.nim | 161 +++++++++++++++++++++++++++++++ tests/json/deserialize/stint.nim | 18 +++- 4 files changed, 241 insertions(+), 9 deletions(-) diff --git a/serde/json/deserializer.nim b/serde/json/deserializer.nim index dee909a..f63dcaa 100644 --- a/serde/json/deserializer.nim +++ b/serde/json/deserializer.nim @@ -79,6 +79,12 @@ proc fromJson*[T: SomeInteger](_: type T, json: JsonNode): ?!T = expectJsonKind(T, {JInt, JString}, json) case json.kind of JString: + if json.isNullString: + let err = newSerdeError("Cannot deserialize 'null' into type " & $T) + return failure(err) + elif json.isEmptyString: + return success T(0) + without x =? parseBiggestUInt(json.str).catch, error: return failure newSerdeError(error.msg) return success cast[T](x) @@ -125,16 +131,15 @@ proc fromJson*[T: distinct](_: type T, json: JsonNode): ?!T = success T(?T.distinctBase.fromJson(json)) proc fromJson*(T: typedesc[StUint or StInt], json: JsonNode): ?!T = - expectJsonKind(T, {JString, JInt, JNull}, json) + expectJsonKind(T, {JString, JInt}, json) case json.kind - of JNull: # return 0, optional values are handled up the call stack - return catch parse("0", T) of JInt: return catch parse($json, T) else: # JString (only other kind allowed) if json.isNullString: - return catch parse("0", T) + let err = newSerdeError("Cannot deserialize 'null' into type " & $T) + return failure(err) let jsonStr = json.getStr let prefix = @@ -253,9 +258,62 @@ proc fromJson*[T: ref object or object](_: type T, bytes: openArray[byte]): ?!T T.fromJson(json) proc fromJson*[T: ref object or object](_: type T, json: string): ?!T = + echo "here1, T: ", T + when T is Option: + echo " we have an option!" let jsn = ?JsonNode.parse(json) # full qualification required in-module only T.fromJson(jsn) +proc fromJson*[T: enum](_: type T, json: string): ?!T = + T.fromJson(newJString(json)) + +proc fromJson*[T: SomeInteger or SomeFloat or openArray[byte] or bool]( + _: type T, json: string +): ?!T = + if json == "" or json == "null": + let err = newSerdeError("Cannot deserialize '' or 'null' into type " & $T) + failure err + else: + let jsn = ?JsonNode.parse(json) + T.fromJson(jsn) + +proc fromJson*[T: SomeInteger or SomeFloat or openArray[byte] or bool or enum]( + _: type Option[T], json: string +): ?!Option[T] = + if json == "" or json == "null": + success T.none + else: + when T is enum: + let jsn = newJString(json) + else: + let jsn = ?JsonNode.parse(json) + Option[T].fromJson(jsn) + +proc fromJson*[T: SomeInteger or SomeFloat or openArray[byte] or bool or enum]( + _: type seq[T], json: string +): ?!seq[T] = + if json == "" or json == "null": + success newSeq[T]() + else: + if T is enum: + let err = newSerdeError("Cannot deserialize a seq[enum]: not yet implemented, PRs welcome") + return failure err + + let jsn = ?JsonNode.parse(json) + seq[T].fromJson(jsn) + +proc fromJson*[T: SomeInteger or SomeFloat or openArray[byte] or bool or enum]( + _: type ?seq[T], json: string +): ?!Option[seq[T]] = + if json == "" or json == "null": + success seq[T].none + else: + if T is enum: + let err = newSerdeError("Cannot deserialize a seq[enum]: not yet implemented, PRs welcome") + return failure err + let jsn = ?JsonNode.parse(json) + Option[seq[T]].fromJson(jsn) + proc fromJson*[T: ref object or object](_: type seq[T], json: string): ?!seq[T] = let jsn = ?JsonNode.parse(json) # full qualification required in-module only seq[T].fromJson(jsn) diff --git a/serde/json/parser.nim b/serde/json/parser.nim index ee09c23..5f790e1 100644 --- a/serde/json/parser.nim +++ b/serde/json/parser.nim @@ -2,6 +2,7 @@ import std/json as stdjson import pkg/questionable/results +import ./errors import ./types {.push raises: [].} @@ -10,6 +11,8 @@ proc parse*(_: type JsonNode, json: string): ?!JsonNode = # Used as a replacement for `std/json.parseJson`. Will not raise Exception like in the # standard library try: - return stdjson.parseJson(json).catch + without val =? stdjson.parseJson(json).catch, error: + return failure error.mapErrTo(JsonParseError) + return success val except Exception as e: return failure newException(JsonParseError, e.msg, e) diff --git a/tests/json/deserialize/std.nim b/tests/json/deserialize/std.nim index fd68a69..bff1c91 100644 --- a/tests/json/deserialize/std.nim +++ b/tests/json/deserialize/std.nim @@ -20,6 +20,20 @@ suite "json - deserialize std types": let json = newJInt(1) check (!fromJson(?int, json) == some 1) + test "deserializes Option[T] when has a string value": + check (!fromJson(?int, "1") == some 1) + + test "deserializes Option[T] from empty string": + check (!fromJson(?int, "") == int.none) + + test "deserializes Option[T] from empty string": + check (!fromJson(?int, "") == int.none) + + test "cannot deserialize T from null string": + let res = fromJson(int, "null") + check res.error of SerdeError + check res.error.msg == "Cannot deserialize '' or 'null' into type int" + test "deserializes Option[T] when doesn't have a value": let json = newJNull() check !fromJson(?int, json) == none int @@ -28,6 +42,19 @@ suite "json - deserialize std types": let json = newJFloat(1.234) check !float.fromJson(json) == 1.234 + test "deserializes float from string": + check !float.fromJson("1.234") == 1.234 + + test "cannot deserialize float from empty string": + let res = float.fromJson("") + check res.error of SerdeError + check res.error.msg == "Cannot deserialize '' or 'null' into type float" + + test "cannot deserialize float from null string": + let res = float.fromJson("null") + check res.error of SerdeError + check res.error.msg == "Cannot deserialize '' or 'null' into type float" + test "deserializes Inf float": let json = newJString("inf") check !float.fromJson(json) == Inf @@ -54,3 +81,137 @@ suite "json - deserialize std types": let largeUInt: uint = uint(int.high) + 1'u let json = newJString($BiggestUInt(largeUInt)) check !uint.fromJson(json) == largeUInt + + test "deserializes bool from JBool": + let json = newJBool(true) + check !bool.fromJson(json) + + test "deserializes bool from string": + check !bool.fromJson("true") + + test "cannot deserialize bool from empty string": + let res = bool.fromJson("") + check res.error of SerdeError + check res.error.msg == "Cannot deserialize '' or 'null' into type bool" + + test "cannot deserialize bool from null string": + let res = bool.fromJson("null") + check res.error of SerdeError + check res.error.msg == "Cannot deserialize '' or 'null' into type bool" + + test "deserializes ?bool from string": + check Option[bool].fromJson("true") == success true.some + + test "deserializes ?bool from empty string": + check !Option[bool].fromJson("") == bool.none + + test "deserializes ?bool from null string": + check !Option[bool].fromJson("null") == bool.none + + test "deserializes seq[bool] from JArray": + let json = newJArray() + json.add(newJBool(true)) + json.add(newJBool(false)) + check !seq[bool].fromJson(json) == @[true, false] + + test "deserializes seq[bool] from string": + check !seq[bool].fromJson("[true, false]") == @[true, false] + + test "deserializes seq[bool] from empty string": + check !seq[bool].fromJson("") == newSeq[bool]() + + test "deserializes seq[bool] from null string": + check !seq[bool].fromJson("null") == newSeq[bool]() + + test "cannot deserialize seq[bool] from unknown string": + let res = seq[bool].fromJson("blah") + check res.error of JsonParseError + check res.error.msg == "input(1, 4) Error: { expected" + + test "deserializes ?seq[bool] from string": + check Option[seq[bool]].fromJson("[true, false]") == success @[true, false].some + + test "deserializes ?seq[bool] from empty string": + check !Option[seq[bool]].fromJson("") == seq[bool].none + + test "deserializes ?seq[bool] from null string": + check !Option[seq[bool]].fromJson("null") == seq[bool].none + + test "deserializes enum from JString": + type MyEnum = enum + one + + let json = newJString("one") + check !MyEnum.fromJson(json) == MyEnum.one + + test "deserializes enum from string": + type MyEnum = enum + one + + check !MyEnum.fromJson("one") == MyEnum.one + + test "cannot deserialize enum from empty string": + type MyEnum = enum + one + + let res = MyEnum.fromJson("") + check res.error of SerdeError + check res.error.msg == "Invalid enum value: " + + test "cannot deserialize enum from null string": + type MyEnum = enum + one + + let res = MyEnum.fromJson("null") + check res.error of SerdeError + check res.error.msg == "Invalid enum value: null" + + test "deserializes ?enum from string": + type MyEnum = enum + one + + check Option[MyEnum].fromJson("one") == success MyEnum.one.some + + test "deserializes ?enum from empty string": + type MyEnum = enum + one + + check !Option[MyEnum].fromJson("") == MyEnum.none + + test "deserializes ?enum from null string": + type MyEnum = enum + one + + check !Option[MyEnum].fromJson("null") == MyEnum.none + + test "deserializes seq[enum] from string": + type MyEnum = enum + one + two + + let res = seq[MyEnum].fromJson("[one,two]") + check res.error of SerdeError + check res.error.msg == + "Cannot deserialize a seq[enum]: not yet implemented, PRs welcome" + + test "deserializes ?seq[enum] from string": + type MyEnum = enum + one + two + + let res = Option[seq[MyEnum]].fromJson("[one,two]") + check res.error of SerdeError + check res.error.msg == + "Cannot deserialize a seq[enum]: not yet implemented, PRs welcome" + + test "deserializes ?seq[MyEnum] from empty string": + type MyEnum = enum + one + + check !Option[seq[MyEnum]].fromJson("") == seq[MyEnum].none + + test "deserializes ?seq[MyEnum] from null string": + type MyEnum = enum + one + + check !Option[seq[MyEnum]].fromJson("null") == seq[MyEnum].none diff --git a/tests/json/deserialize/stint.nim b/tests/json/deserialize/stint.nim index 3546f0c..497a244 100644 --- a/tests/json/deserialize/stint.nim +++ b/tests/json/deserialize/stint.nim @@ -14,10 +14,15 @@ suite "json - deserialize stint": check !UInt256.fromJson("") == 0.u256 test "deserializes UInt256 from null string": - check !UInt256.fromJson("null") == 0.u256 + let res = UInt256.fromJson("null") + check res.error of SerdeError + check res.error.msg == "Cannot deserialize 'null' into type UInt256" test "deserializes UInt256 from JNull": - check !UInt256.fromJson(newJNull()) == 0.u256 + let res = UInt256.fromJson(newJNull()) + check res.error of UnexpectedKindError + check res.error.msg == + "deserialization to UInt256 failed: expected {JInt, JString} but got JNull" test "deserializes ?UInt256 from an empty JString": let json = newJString("") @@ -39,10 +44,15 @@ suite "json - deserialize stint": check seq[UInt256].fromJson("[1,2,\"\"]") == success @[1.u256, 2.u256, 0.u256] test "deserializes seq[UInt256] from string with null item": - check seq[UInt256].fromJson("[1,2,null]") == success @[1.u256, 2.u256, 0.u256] + let res = seq[UInt256].fromJson("[1,2,null]") + check res.error of UnexpectedKindError + check res.error.msg == + "deserialization to UInt256 failed: expected {JInt, JString} but got JNull" test "deserializes seq[UInt256] from string with null string item": - check seq[UInt256].fromJson("[1,2,\"null\"]") == success @[1.u256, 2.u256, 0.u256] + let res = seq[UInt256].fromJson("[1,2,\"null\"]") + check res.error of SerdeError + check res.error.msg == "Cannot deserialize 'null' into type UInt256" test "deserializes seq[?UInt256] from string": check seq[?UInt256].fromJson("[1,2,3]") ==