From d0a67a8eb09aa7015d767b781007ad59fd721c60 Mon Sep 17 00:00:00 2001 From: Eric <5089238+emizzle@users.noreply.github.com> Date: Wed, 7 Feb 2024 09:40:48 +1100 Subject: [PATCH] initial impl, not compiling --- .gitignore | 8 + config.nims | 7 + serde.nim | 5 + serde.nimble | 20 ++ serde/common.nim | 19 ++ serde/deserialize.nim | 323 +++++++++++++++++++++++++++++++++ serde/pragmas.nim | 71 ++++++++ serde/serialize.nim | 164 +++++++++++++++++ serde/types.nim | 11 ++ tests/config.nims | 5 + tests/helpers.nim | 5 + tests/test.nim | 7 + tests/testDeserialize.nim | 136 ++++++++++++++ tests/testDeserializeModes.nim | 116 ++++++++++++ tests/testPragmas.nim | 67 +++++++ tests/testSerialize.nim | 96 ++++++++++ tests/testSerializeModes.nim | 102 +++++++++++ tests/tests.nimble | 11 ++ 18 files changed, 1173 insertions(+) create mode 100644 .gitignore create mode 100644 config.nims create mode 100644 serde.nim create mode 100644 serde.nimble create mode 100644 serde/common.nim create mode 100644 serde/deserialize.nim create mode 100644 serde/pragmas.nim create mode 100644 serde/serialize.nim create mode 100644 serde/types.nim create mode 100644 tests/config.nims create mode 100644 tests/helpers.nim create mode 100644 tests/test.nim create mode 100644 tests/testDeserialize.nim create mode 100644 tests/testDeserializeModes.nim create mode 100644 tests/testPragmas.nim create mode 100644 tests/testSerialize.nim create mode 100644 tests/testSerializeModes.nim create mode 100644 tests/tests.nimble diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2681317 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +* +!*/ +!*.* +nimble.develop +nimble.paths +.idea +vendor/ +.vscode/ \ No newline at end of file diff --git a/config.nims b/config.nims new file mode 100644 index 0000000..e01d099 --- /dev/null +++ b/config.nims @@ -0,0 +1,7 @@ +--styleCheck:usages +--styleCheck:error + +# begin Nimble config (version 1) +when fileExists("nimble.paths"): + include "nimble.paths" +# end Nimble config diff --git a/serde.nim b/serde.nim new file mode 100644 index 0000000..aa90a81 --- /dev/null +++ b/serde.nim @@ -0,0 +1,5 @@ +import ./serde/serialize +import ./serde/deserialize + +export serialize +export deserialize diff --git a/serde.nimble b/serde.nimble new file mode 100644 index 0000000..d4c2385 --- /dev/null +++ b/serde.nimble @@ -0,0 +1,20 @@ +# Package + +version = "0.1.0" +author = "nim-json authors" +description = "Drop-in replacement for std/json, with easy-to-use json serialization capabilities." +license = "MIT" +srcDir = "src" + + +# Dependencies +requires "nim >= 1.6.14" +requires "chronicles >= 0.10.3 & < 0.11.0" +requires "questionable >= 0.10.13 & < 0.11.0" +requires "stint" +requires "stew" + +task test, "Run the test suite": + exec "nimble install -d -y" + withDir "tests": + exec "nimble test" diff --git a/serde/common.nim b/serde/common.nim new file mode 100644 index 0000000..14cf33b --- /dev/null +++ b/serde/common.nim @@ -0,0 +1,19 @@ +import std/json as stdjson except `%`, `%*` + +import pkg/questionable +import pkg/questionable/results + +export stdjson except `%`, `%*`, parseJson + +{.push raises: [].} + +type + SerdeError* = object of CatchableError + JsonParseError* = object of SerdeError + +proc parseJson*(json: string): ?!JsonNode = + ## fix for nim raising Exception + try: + return stdjson.parseJson(json).catch + except Exception as e: + return failure newException(JsonParseError, e.msg) diff --git a/serde/deserialize.nim b/serde/deserialize.nim new file mode 100644 index 0000000..de1aa40 --- /dev/null +++ b/serde/deserialize.nim @@ -0,0 +1,323 @@ + +# import std/json as stdjson except `%`, `%*` +import std/macros +import std/options +import std/sequtils +import std/sets +import std/strutils +import std/tables +import std/typetraits +import pkg/chronicles except toJson +import pkg/stew/byteutils +import pkg/stint +import pkg/questionable +import pkg/questionable/results + +import ./common # parseJson, std/json except `%`, `%*` +import ./pragmas +import ./types + +export common +export chronicles except toJson +export pragmas +export sets +export types + +{.push raises: [].} + +logScope: + topics = "json deserialization" + +proc mapErrTo[E1: ref CatchableError, E2: SerdeError]( + e1: E1, + _: type E2, + msg: string = e1.msg): ref E2 = + + return newException(E2, msg, e1) + +proc newSerdeError(msg: string): ref SerdeError = + newException(SerdeError, msg) + +proc newUnexpectedKindError( + expectedType: type, + expectedKinds: string, + json: JsonNode +): ref UnexpectedKindError = + let kind = if json.isNil: "nil" + else: $json.kind + newException(UnexpectedKindError, + "deserialization to " & $expectedType & " failed: expected " & + expectedKinds & " but got " & $kind) + +proc newUnexpectedKindError( + expectedType: type, + expectedKinds: set[JsonNodeKind], + json: JsonNode +): ref UnexpectedKindError = + newUnexpectedKindError(expectedType, $expectedKinds, json) + +proc newUnexpectedKindError( + expectedType: type, + expectedKind: JsonNodeKind, + json: JsonNode +): ref UnexpectedKindError = + newUnexpectedKindError(expectedType, {expectedKind}, json) + +template expectJsonKind( + expectedType: type, + expectedKinds: set[JsonNodeKind], + json: JsonNode +) = + if json.isNil or json.kind notin expectedKinds: + return failure(newUnexpectedKindError(expectedType, expectedKinds, json)) + +template expectJsonKind*( + expectedType: type, + expectedKind: JsonNodeKind, + json: JsonNode +) = + expectJsonKind(expectedType, {expectedKind}, json) + +proc fieldKeys[T](obj: T): seq[string] = + for name, _ in fieldPairs(when type(T) is ref: obj[] else: obj): + result.add name + +func keysNotIn[T](json: JsonNode, obj: T): HashSet[string] = + let jsonKeys = json.keys.toSeq.toHashSet + let objKeys = obj.fieldKeys.toHashSet + difference(jsonKeys, objKeys) + + +proc fromJson*( + T: type enum, + json: JsonNode +): ?!T = + expectJsonKind(string, JString, json) + without val =? parseEnum[T](json.str).catch, error: + return failure error.mapErrTo(SerdeError) + return success val + +proc fromJson*( + _: type string, + json: JsonNode +): ?!string = + if json.isNil: + return failure newSerdeError("'json' expected, but was nil") + elif json.kind == JNull: + return success("null") + elif json.isNil or json.kind != JString: + return failure newUnexpectedKindError(string, JString, json) + catch json.getStr + +proc fromJson*( + _: type bool, + json: JsonNode +): ?!bool = + expectJsonKind(bool, JBool, json) + catch json.getBool + +proc fromJson*( + _: type int, + json: JsonNode +): ?!int = + expectJsonKind(int, JInt, json) + catch json.getInt + +proc fromJson*[T: SomeInteger]( + _: type T, + json: JsonNode +): ?!T = + when T is uint|uint64 or (not defined(js) and int.sizeof == 4): + expectJsonKind(T, {JInt, JString}, json) + case json.kind + of JString: + without x =? parseBiggestUInt(json.str).catch, error: + return failure newSerdeError(error.msg) + return success cast[T](x) + else: + return success T(json.num) + else: + expectJsonKind(T, {JInt}, json) + return success cast[T](json.num) + +proc fromJson*[T: SomeFloat]( + _: type T, + json: JsonNode +): ?!T = + expectJsonKind(T, {JInt, JFloat, JString}, json) + if json.kind == JString: + case json.str + of "nan": + let b = NaN + return success T(b) + # dst = NaN # would fail some tests because range conversions would cause CT error + # in some cases; but this is not a hot-spot inside this branch and backend can optimize this. + of "inf": + let b = Inf + return success T(b) + of "-inf": + let b = -Inf + return success T(b) + else: + let err = newUnexpectedKindError(T, "'nan|inf|-inf'", json) + return failure(err) + else: + if json.kind == JFloat: + return success T(json.fnum) + else: + return success T(json.num) + +proc fromJson*( + _: type seq[byte], + json: JsonNode +): ?!seq[byte] = + expectJsonKind(seq[byte], JString, json) + hexToSeqByte(json.getStr).catch + +proc fromJson*[N: static[int], T: array[N, byte]]( + _: type T, + json: JsonNode +): ?!T = + expectJsonKind(T, JString, json) + T.fromHex(json.getStr).catch + +proc fromJson*[T: distinct]( + _: type T, + json: JsonNode +): ?!T = + success T(? T.distinctBase.fromJson(json)) + +proc fromJson*[N: static[int], T: StUint[N]]( + _: type T, + json: JsonNode +): ?!T = + expectJsonKind(T, JString, json) + let jsonStr = json.getStr + let prefix = jsonStr[0..1].toLowerAscii + case prefix: + of "0x": catch parse(jsonStr, T, 16) + of "0o": catch parse(jsonStr, T, 8) + of "0b": catch parse(jsonStr, T, 2) + else: catch parse(jsonStr, T) + +proc fromJson*[T]( + _: type Option[T], + json: JsonNode +): ?! Option[T] = + if json.isNil or json.kind == JNull: + return success(none T) + without val =? T.fromJson(json), error: + return failure(error) + success(val.some) + +proc fromJson*[T]( + _: type seq[T], + json: JsonNode +): ?! seq[T] = + expectJsonKind(seq[T], JArray, json) + var arr: seq[T] = @[] + for elem in json.elems: + arr.add(? T.fromJson(elem)) + success arr + +proc fromJson*[T: ref object or object]( + _: type T, + json: JsonNode +): ?!T = + + when T is JsonNode: + return success T(json) + + expectJsonKind(T, JObject, json) + var res = when type(T) is ref: T.new() else: T.default + let mode = T.getSerdeMode(deserialize) + + # ensure there's no extra fields in json + if mode == SerdeMode.Strict: + let extraFields = json.keysNotIn(res) + if extraFields.len > 0: + return failure newSerdeError("json field(s) missing in object: " & $extraFields) + + for name, value in fieldPairs(when type(T) is ref: res[] else: res): + + logScope: + field = $T & "." & name + mode + + let hasDeserializePragma = value.hasCustomPragma(deserialize) + let opts = getSerdeFieldOptions(deserialize, name, value) + let isOptionalValue = typeof(value) is Option + var skip = false # workaround for 'continue' not supported in a 'fields' loop + + case mode: + of Strict: + if opts.key notin json: + return failure newSerdeError("object field missing in json: " & opts.key) + elif opts.ignore: + # unable to figure out a way to make this a compile time check + warn "object field marked as 'ignore' while in Strict mode, field will be deserialized anyway" + + of OptIn: + if not hasDeserializePragma: + debug "object field not marked as 'deserialize', skipping" + skip = true + elif opts.ignore: + debug "object field marked as 'ignore', skipping" + skip = true + elif opts.key notin json and not isOptionalValue: + return failure newSerdeError("object field missing in json: " & opts.key) + + of OptOut: + if opts.ignore: + debug "object field is opted out of deserialization ('igore' is set), skipping" + skip = true + elif hasDeserializePragma and opts.key == name: + warn "object field marked as deserialize in OptOut mode, but 'ignore' not set, field will be deserialized" + + if not skip: + + if isOptionalValue: + + let jsonVal = json{opts.key} + without parsed =? typeof(value).fromJson(jsonVal), e: + debug "failed to deserialize field", + `type` = $typeof(value), + json = jsonVal, + error = e.msg + return failure(e) + value = parsed + + # not Option[T] + elif opts.key in json and + jsonVal =? json{opts.key}.catch and + not jsonVal.isNil: + + without parsed =? typeof(value).fromJson(jsonVal), e: + debug "failed to deserialize field", + `type` = $typeof(value), + json = jsonVal, + error = e.msg + return failure(e) + value = parsed + + success(res) + +proc fromJson*[T: ref object or object]( + _: type T, + bytes: seq[byte] +): ?!T = + let json = ? parse(string.fromBytes(bytes)) + T.fromJson(json) + +proc fromJson*( + _: type JsonNode, + jsn: string +): ?!JsonNode = + return common.parseJson(jsn) + +proc fromJson*[T: ref object or object]( + _: type T, + jsn: string +): ?!T = + let jsn = ? common.parseJson(jsn) # full qualification required in-module only + T.fromJson(jsn) diff --git a/serde/pragmas.nim b/serde/pragmas.nim new file mode 100644 index 0000000..4faa74c --- /dev/null +++ b/serde/pragmas.nim @@ -0,0 +1,71 @@ +import std/macros + +import ./types + +type + SerdeFieldOptions* = object + key*: string + ignore*: bool + +template serialize*(key = "", ignore = false, mode = SerdeMode.OptOut) {.pragma.} +template deserialize*(key = "", ignore = false, mode = SerdeMode.OptOut) {.pragma.} + +proc isDefault[T](paramValue: T): bool {.compileTime.} = + var result = paramValue == T.default + when T is SerdeMode: + return paramValue == SerdeMode.OptOut + return result + +template expectMissingPragmaParam*(value, pragma, name, msg) = + static: + when value.hasCustomPragma(pragma): + const params = value.getCustomPragmaVal(pragma) + for paramName, paramValue in params.fieldPairs: + + if paramName == name and not paramValue.isDefault: + raiseAssert(msg) + +template getSerdeFieldOptions*(pragma, fieldName, fieldValue): SerdeFieldOptions = + var opts = SerdeFieldOptions(key: fieldName, ignore: false) + when fieldValue.hasCustomPragma(pragma): + fieldValue.expectMissingPragmaParam(pragma, "mode", + "Cannot set " & astToStr(pragma) & " 'mode' on '" & fieldName & "' field defintion.") + let (key, ignore, _) = fieldValue.getCustomPragmaVal(pragma) + opts.ignore = ignore + if key != "": + opts.key = key + opts + +template getSerdeMode*(T, pragma): SerdeMode = + when T.hasCustomPragma(pragma): + T.expectMissingPragmaParam(pragma, "key", + "Cannot set " & astToStr(pragma) & " 'key' on '" & $T & + "' type definition.") + T.expectMissingPragmaParam(pragma, "ignore", + "Cannot set " & astToStr(pragma) & " 'ignore' on '" & $T & + "' type definition.") + let (_, _, mode) = T.getCustomPragmaVal(pragma) + mode + else: + # Default mode -- when the type is NOT annotated with a + # serialize/deserialize pragma. + # + # NOTE This may be different in the logic branch above, when the type is + # annotated with serialize/deserialize but doesn't specify a mode. The + # default in that case will fallback to the default mode specified in the + # pragma signature (currently OptOut for both serialize and deserialize) + # + # Examples: + # 1. type MyObj = object + # Type is not annotated, mode defaults to OptOut (as specified on the + # pragma signatures) for both serialization and deserializtion + # + # 2. type MyObj {.serialize, deserialize.} = object + # Type is annotated, mode defaults to OptIn for serialization and OptOut + # for deserialization + when astToStr(pragma) == "serialize": + static: echo "decided default mode for ", T, "serialize, OptIn" + SerdeMode.OptIn + elif astToStr(pragma) == "deserialize": + static: echo "decided default mode for ", T, " deserialize, OptOut" + SerdeMode.OptOut \ No newline at end of file diff --git a/serde/serialize.nim b/serde/serialize.nim new file mode 100644 index 0000000..09e7f94 --- /dev/null +++ b/serde/serialize.nim @@ -0,0 +1,164 @@ +# import std/json as stdjson except `%`, `%*` +import std/macros +import std/options +import std/sets +import std/strutils +import std/tables +import std/typetraits + +import pkg/chronicles except toJson +import pkg/questionable +import pkg/stew/byteutils +import pkg/stint + +import ./common +import ./pragmas +import ./types + +# export stdjson except `%`, `%*`, parseJson +export chronicles except toJson +export common +export pragmas +export sets +export types + +{.push raises: [].} + +logScope: + topics = "json serialization" + +func `%`*(s: string): JsonNode = newJString(s) + +func `%`*(n: uint): JsonNode = + if n > cast[uint](int.high): + newJString($n) + else: + newJInt(BiggestInt(n)) + +func `%`*(n: int): JsonNode = newJInt(n) + +func `%`*(n: BiggestUInt): JsonNode = + if n > cast[BiggestUInt](BiggestInt.high): + newJString($n) + else: + newJInt(BiggestInt(n)) + +func `%`*(n: BiggestInt): JsonNode = newJInt(n) + +func `%`*(n: float): JsonNode = + if n != n: newJString("nan") + elif n == Inf: newJString("inf") + elif n == -Inf: newJString("-inf") + else: newJFloat(n) + +func `%`*(b: bool): JsonNode = newJBool(b) + +func `%`*(keyVals: openArray[tuple[key: string, val: JsonNode]]): JsonNode = + if keyVals.len == 0: return newJArray() + let jObj = newJObject() + for key, val in items(keyVals): jObj.fields[key] = val + jObj + +template `%`*(j: JsonNode): JsonNode = j + +func `%`*[T](table: Table[string, T]|OrderedTable[string, T]): JsonNode = + let jObj = newJObject() + for k, v in table: jObj[k] = ? %v + jObj + +func `%`*[T](opt: Option[T]): JsonNode = + if opt.isSome: %(opt.get) else: newJNull() + +proc `%`*[T: object or ref object](obj: T): JsonNode = + + let jsonObj = newJObject() + let o = when T is ref object: obj[] + else: obj + + let mode = T.getSerdeMode(serialize) + + for name, value in o.fieldPairs: + + logScope: + field = $T & "." & name + mode + + let opts = getSerdeFieldOptions(serialize, name, value) + let hasSerialize = value.hasCustomPragma(serialize) + echo T, " hasSerialize: ", hasSerialize + var skip = false # workaround for 'continue' not supported in a 'fields' loop + + case mode: + of OptIn: + if not hasSerialize: + debug "object field not marked with serialize, skipping" + skip = true + elif opts.ignore: + skip = true + + of OptOut: + if opts.ignore: + debug "object field opted out of serialization ('ignore' is set), skipping" + skip = true + elif hasSerialize and opts.key == name: # all serialize params are default + warn "object field marked as serialize in OptOut mode, but 'ignore' not set, field will be serialized" + + of Strict: + if opts.ignore: + # unable to figure out a way to make this a compile time check + warn "object field marked as 'ignore' while in Strict mode, field will be serialized anyway" + + if not skip: + jsonObj[opts.key] = %value + + jsonObj + +proc `%`*(o: enum): JsonNode = % $o + +func `%`*(stint: StInt|StUint): JsonNode = %stint.toString + +func `%`*(cstr: cstring): JsonNode = % $cstr + +func `%`*(arr: openArray[byte]): JsonNode = % arr.to0xHex + +func `%`*[T](elements: openArray[T]): JsonNode = + let jObj = newJArray() + for elem in elements: jObj.add(%elem) + jObj + +func `%`*[T: distinct](id: T): JsonNode = + type baseType = T.distinctBase + % baseType(id) + +proc toJson*[T](item: T): string = $(%item) + +proc toJsnImpl(x: NimNode): NimNode = + case x.kind + of nnkBracket: # array + if x.len == 0: return newCall(bindSym"newJArray") + result = newNimNode(nnkBracket) + for i in 0 ..< x.len: + result.add(toJsnImpl(x[i])) + result = newCall(bindSym("%", brOpen), result) + of nnkTableConstr: # object + if x.len == 0: return newCall(bindSym"newJObject") + result = newNimNode(nnkTableConstr) + for i in 0 ..< x.len: + x[i].expectKind nnkExprColonExpr + result.add newTree(nnkExprColonExpr, x[i][0], toJsnImpl(x[i][1])) + result = newCall(bindSym("%", brOpen), result) + of nnkCurly: # empty object + x.expectLen(0) + result = newCall(bindSym"newJObject") + of nnkNilLit: + result = newCall(bindSym"newJNull") + of nnkPar: + if x.len == 1: result = toJsnImpl(x[0]) + else: result = newCall(bindSym("%", brOpen), x) + else: + result = newCall(bindSym("%", brOpen), x) + +macro `%*`*(x: untyped): JsonNode = + ## Convert an expression to a JsonNode directly, without having to specify + ## `%` for every element. + result = toJsnImpl(x) \ No newline at end of file diff --git a/serde/types.nim b/serde/types.nim new file mode 100644 index 0000000..2a5c1f0 --- /dev/null +++ b/serde/types.nim @@ -0,0 +1,11 @@ +import ./common + +type + UnexpectedKindError* = object of SerdeError + SerdeMode* = enum + OptOut, ## serialize: all object fields will be serialized, except fields marked with 'ignore' + ## deserialize: all json keys will be deserialized, no error if extra json field + OptIn, ## serialize: only object fields marked with serialize will be serialzied + ## deserialize: only fields marked with deserialize will be deserialized + Strict ## serialize: all object fields will be serialized, regardless if the field is marked with 'ignore' + ## deserialize: object fields and json fields must match exactly diff --git a/tests/config.nims b/tests/config.nims new file mode 100644 index 0000000..fd4b500 --- /dev/null +++ b/tests/config.nims @@ -0,0 +1,5 @@ +switch("path", "..") +when (NimMajor, NimMinor) >= (1, 4): + switch("hint", "XCannotRaiseY:off") +when (NimMajor, NimMinor, NimPatch) >= (1, 6, 11): + switch("warning", "BareExcept:off") \ No newline at end of file diff --git a/tests/helpers.nim b/tests/helpers.nim new file mode 100644 index 0000000..830c531 --- /dev/null +++ b/tests/helpers.nim @@ -0,0 +1,5 @@ +import std/strutils + +func flatten*(s: string): string = + s.replace(" ") + .replace("\n") \ No newline at end of file diff --git a/tests/test.nim b/tests/test.nim new file mode 100644 index 0000000..0707642 --- /dev/null +++ b/tests/test.nim @@ -0,0 +1,7 @@ +import ./testPragmas +import ./testSerialize +import ./testSerializeModes +import ./testDeserialize +import ./testDeserializeModes + +{.warning[UnusedImport]:off.} diff --git a/tests/testDeserialize.nim b/tests/testDeserialize.nim new file mode 100644 index 0000000..63c575f --- /dev/null +++ b/tests/testDeserialize.nim @@ -0,0 +1,136 @@ +import std/math +import std/options +import std/strformat +import std/strutils +import std/unittest +import pkg/stew/byteutils +import pkg/stint +import pkg/serde +import pkg/questionable +import pkg/questionable/results + +suite "json serialization - deserialize": + + test "deserializes NaN float": + check %NaN == newJString("nan") + + test "deserialize enum": + type MyEnum = enum + First, + Second + let json = newJString("Second") + check !MyEnum.fromJson(json) == Second + + test "deserializes UInt256 from non-hex string representation": + let json = newJString("100000") + check !UInt256.fromJson(json) == 100000.u256 + + test "deserializes Option[T] when has a value": + let json = newJInt(1) + check (!fromJson(?int, json) == some 1) + + test "deserializes Option[T] when doesn't have a value": + let json = newJNull() + check !fromJson(?int, json) == none int + + test "deserializes float": + let json = newJFloat(1.234) + check !float.fromJson(json) == 1.234 + + test "deserializes Inf float": + let json = newJString("inf") + check !float.fromJson(json) == Inf + + test "deserializes -Inf float": + let json = newJString("-inf") + check !float.fromJson(json) == -Inf + + test "deserializes NaN float": + let json = newJString("nan") + check (!float.fromJson(json)).isNaN + + test "deserializes array to sequence": + let expected = @[1, 2, 3] + let json = !"[1,2,3]".parseJson + check !seq[int].fromJson(json) == expected + + test "deserializes uints int.high or smaller": + let largeUInt: uint = uint(int.high) + let json = newJInt(BiggestInt(largeUInt)) + check !uint.fromJson(json) == largeUInt + + test "deserializes large uints": + let largeUInt: uint = uint(int.high) + 1'u + let json = newJString($BiggestUInt(largeUInt)) + check !uint.fromJson(json) == largeUInt + + test "can deserialize json objects": + type MyObj = object + mystring: string + myint: int + myoption: ?bool + + let expected = MyObj(mystring: "abc", myint: 123, myoption: some true) + + let json = !parseJson("""{ + "mystring": "abc", + "myint": 123, + "myoption": true + }""") + check !MyObj.fromJson(json) == expected + + test "ignores serialize pragma when deserializing": + type MyObj = object + mystring {.serialize.}: string + mybool: bool + + let expected = MyObj(mystring: "abc", mybool: true) + + let json = !parseJson("""{ + "mystring": "abc", + "mybool": true + }""") + + check !MyObj.fromJson(json) == expected + + test "deserializes objects with extra fields": + type MyObj = object + mystring: string + mybool: bool + + let expected = MyObj(mystring: "abc", mybool: true) + + let json = !"""{ + "mystring": "abc", + "mybool": true, + "extra": "extra" + }""".parseJson + check !MyObj.fromJson(json) == expected + + test "deserializes objects with less fields": + type MyObj = object + mystring: string + mybool: bool + + let expected = MyObj(mystring: "abc", mybool: false) + + let json = !"""{ + "mystring": "abc" + }""".parseJson + check !MyObj.fromJson(json) == expected + + test "deserializes ref objects": + type MyRef = ref object + mystring: string + myint: int + + let expected = MyRef(mystring: "abc", myint: 1) + + let json = !"""{ + "mystring": "abc", + "myint": 1 + }""".parseJson + + let deserialized = !MyRef.fromJson(json) + check deserialized.mystring == expected.mystring + check deserialized.myint == expected.myint \ No newline at end of file diff --git a/tests/testDeserializeModes.nim b/tests/testDeserializeModes.nim new file mode 100644 index 0000000..3484fb9 --- /dev/null +++ b/tests/testDeserializeModes.nim @@ -0,0 +1,116 @@ +import std/options +import std/unittest + +import pkg/stint +import pkg/serde +import pkg/questionable +import pkg/questionable/results + +suite "json deserialization, mode = OptIn": + + test "deserializes only fields marked as deserialize when mode is OptIn": + type MyObj {.deserialize(mode=OptIn).} = object + field1: int + field2 {.deserialize.}: bool + + let val = !MyObj.fromJson("""{"field1":true,"field2":true}""") + check val == MyObj(field1: 0, field2: true) + + test "deserializes Optional fields when mode is OptIn": + type MyObj {.deserialize(mode=OptIn).} = object + field1 {.deserialize.}: bool + field2 {.deserialize.}: Option[bool] + + let val = !MyObj.fromJson("""{"field1":true}""") + check val == MyObj(field1: true, field2: none bool) + + +suite "json deserialization, mode = OptOut": + + test "deserializes object in OptOut mode when not marked with deserialize": + type MyObj = object + field1: bool + field2: bool + + let val = !MyObj.fromJson("""{"field1":true,"field3":true}""") + check val == MyObj(field1: true, field2: false) + + test "deserializes object field with marked json key": + type MyObj = object + field1 {.deserialize("test").}: bool + field2: bool + + let val = !MyObj.fromJson("""{"test":true,"field2":true}""") + check val == MyObj(field1: true, field2: true) + + test "fails to deserialize object field with wrong type": + type MyObj = object + field1: int + field2: bool + + let r = MyObj.fromJson("""{"field1":true,"field2":true}""") + check r.isFailure + check r.error of UnexpectedKindError + check r.error.msg == "deserialization to int failed: expected {JInt} but got JBool" + + test "does not deserialize ignored fields in OptOut mode": + type MyObj = object + field1 {.deserialize(ignore=true).}: bool + field2: bool + + let val = !MyObj.fromJson("""{"field1":true,"field2":true}""") + check val == MyObj(field1: false, field2: true) + + test "deserializes fields when marked with deserialize but not ignored": + type MyObj = object + field1 {.deserialize.}: bool + field2: bool + + let val = !MyObj.fromJson("""{"field1":true,"field2":true}""") + check val == MyObj(field1: true, field2: true) + + test "deserializes Optional field": + type MyObj = object + field1: Option[bool] + field2: bool + + let val = !MyObj.fromJson("""{"field2":true}""") + check val == MyObj(field1: none bool, field2: true) + + +suite "json deserialization, mode = Strict": + + test "deserializes matching object and json fields when mode is Strict": + type MyObj {.deserialize(mode=Strict).} = object + field1: bool + field2: bool + + let val = !MyObj.fromJson("""{"field1":true,"field2":true}""") + check val == MyObj(field1: true, field2: true) + + test "fails to deserialize with missing json field when mode is Strict": + type MyObj {.deserialize(mode=Strict).} = object + field1: bool + field2: bool + + let r = MyObj.fromJson("""{"field2":true}""") + check r.isFailure + check r.error of SerdeError + check r.error.msg == "object field missing in json: field1" + + test "fails to deserialize with missing object field when mode is Strict": + type MyObj {.deserialize(mode=Strict).} = object + field2: bool + + let r = MyObj.fromJson("""{"field1":true,"field2":true}""") + check r.isFailure + check r.error of SerdeError + check r.error.msg == "json field(s) missing in object: {\"field1\"}" + + test "deserializes ignored fields in Strict mode": + type MyObj {.deserialize(mode=Strict).} = object + field1 {.deserialize(ignore=true).}: bool + field2: bool + + let val = !MyObj.fromJson("""{"field1":true,"field2":true}""") + check val == MyObj(field1: true, field2: true) diff --git a/tests/testPragmas.nim b/tests/testPragmas.nim new file mode 100644 index 0000000..942744f --- /dev/null +++ b/tests/testPragmas.nim @@ -0,0 +1,67 @@ +import std/math +import std/options +import std/strformat +import std/strutils +import std/unittest + +import pkg/serde +import pkg/stew/byteutils +import pkg/stint +import pkg/questionable +import pkg/questionable/results + +suite "json serialization pragmas": + + test "fails to compile when object marked with 'serialize' specifies options": + type + MyObj {.serialize(key="test", ignore=true).} = object + + check not compiles(%MyObj()) + + test "compiles when object marked with 'serialize' only": + type + MyObj {.serialize.} = object + + check compiles(%MyObj()) + + test "fails to compile when field marked with 'deserialize' specifies mode": + type + MyObj = object + field {.deserialize(mode=OptIn).}: bool + + check not compiles(MyObj.fromJson("""{"field":true}""")) + + test "compiles when object marked with 'deserialize' specifies mode": + type + MyObj {.deserialize(mode=OptIn).} = object + field: bool + + check compiles(MyObj.fromJson("""{"field":true}""")) + + test "fails to compile when object marked with 'deserialize' specifies key": + type + MyObj {.deserialize("test").} = object + field: bool + + check not compiles(MyObj.fromJson("""{"field":true}""")) + + test "compiles when field marked with 'deserialize' specifies key": + type + MyObj = object + field {.deserialize("test").}: bool + + check compiles(MyObj.fromJson("""{"field":true}""")) + + test "compiles when field marked with empty 'deserialize'": + type + MyObj = object + field {.deserialize.}: bool + + check compiles(MyObj.fromJson("""{"field":true}""")) + + test "compiles when field marked with 'serialize'": + type + MyObj = object + field {.serialize.}: bool + + check compiles(%MyObj()) \ No newline at end of file diff --git a/tests/testSerialize.nim b/tests/testSerialize.nim new file mode 100644 index 0000000..5d130bd --- /dev/null +++ b/tests/testSerialize.nim @@ -0,0 +1,96 @@ +import std/options +import std/strutils +import std/unittest +import pkg/stint +import pkg/serde +import pkg/questionable + +import ./helpers + +suite "json serialization - serialize": + + test "serializes UInt256 to non-hex string representation": + check (% 100000.u256) == newJString("100000") + + test "serializes sequence to an array": + let json = % @[1, 2, 3] + let expected = "[1,2,3]" + check $json == expected + + test "serializes Option[T] when has a value": + let obj = %(some 1) + let expected = "1" + check $obj == expected + + test "serializes Option[T] when doesn't have a value": + let obj = %(none int) + let expected = "null" + check $obj == expected + + test "serializes uints int.high or smaller": + let largeUInt: uint = uint(int.high) + check %largeUInt == newJInt(BiggestInt(largeUInt)) + + test "serializes large uints": + let largeUInt: uint = uint(int.high) + 1'u + check %largeUInt == newJString($largeUInt) + + test "serializes Inf float": + check %Inf == newJString("inf") + + test "serializes -Inf float": + check %(-Inf) == newJString("-inf") + + test "can construct json objects with %*": + type MyObj = object + mystring {.serialize.}: string + myint {.serialize.}: int + myoption {.serialize.}: ?bool + + let myobj = MyObj(mystring: "abc", myint: 123, myoption: some true) + let mystuint = 100000.u256 + + let json = %*{ + "myobj": myobj, + "mystuint": mystuint + } + + let expected = """{ + "myobj": { + "mystring": "abc", + "myint": 123, + "myoption": true + }, + "mystuint": "100000" + }""".flatten + + check $json == expected + + test "only serializes marked fields": + type MyObj = object + mystring {.serialize.}: string + myint {.serialize.}: int + mybool: bool + + let obj = % MyObj(mystring: "abc", myint: 1, mybool: true) + + let expected = """{ + "mystring": "abc", + "myint": 1 + }""".flatten + + check $obj == expected + + test "serializes ref objects": + type MyRef = ref object + mystring {.serialize.}: string + myint {.serialize.}: int + + let obj = % MyRef(mystring: "abc", myint: 1) + + let expected = """{ + "mystring": "abc", + "myint": 1 + }""".flatten + + check $obj == expected \ No newline at end of file diff --git a/tests/testSerializeModes.nim b/tests/testSerializeModes.nim new file mode 100644 index 0000000..a2b36c2 --- /dev/null +++ b/tests/testSerializeModes.nim @@ -0,0 +1,102 @@ +import std/unittest + +# import pkg/stint +import pkg/serde + +suite "json serialization, mode = OptIn": + + test "serializes with default mode OptIn when object not marked with serialize": + type MyObj = object + field1 {.serialize.}: bool + field2: bool + + let obj = MyObj(field1: true, field2: true) + check obj.toJson == """{"field1":true}""" + + test "not marking object with serialize is equivalent to marking it with serialize in OptIn mode": + type MyObj = object + field1 {.serialize.}: bool + field2: bool + + type MyObjMarked {.serialize(mode=OptIn).} = object + field1 {.serialize.}: bool + field2: bool + + let obj = MyObj(field1: true, field2: true) + let objMarked = MyObjMarked(field1: true, field2: true) + check obj.toJson == objMarked.toJson + + test "serializes field with key when specified": + type MyObj = object + field1 {.serialize("test").}: bool + field2 {.serialize.}: bool + + let obj = MyObj(field1: true, field2: true) + check obj.toJson == """{"test":true,"field2":true}""" + + test "does not serialize ignored field": + type MyObj = object + field1 {.serialize.}: bool + field2 {.serialize(ignore=true).}: bool + + let obj = MyObj(field1: true, field2: true) + check obj.toJson == """{"field1":true}""" + + +suite "json serialization, mode = OptOut": + + test "serialize on object definition defaults to OptOut mode, serializes all fields": + type MyObj {.serialize.} = object + field1: bool + field2: bool + + let obj = MyObj(field1: true, field2: true) + check obj.toJson == """{"field1":true,"field2":true}""" + + test "not specifying serialize mode is equivalent to specifying OptOut mode": + type MyObj {.serialize.} = object + field1: bool + field2: bool + + type MyObjMarked {.serialize(mode=OptOut).} = object + field1: bool + field2: bool + + let obj = MyObj(field1: true, field2: true) + let objMarked = MyObjMarked(field1: true, field2: true) + check obj.toJson == objMarked.toJson + + test "ignores field when marked with ignore": + type MyObj {.serialize.} = object + field1 {.serialize(ignore=true).}: bool + field2: bool + + let obj = MyObj(field1: true, field2: true) + check obj.toJson == """{"field2":true}""" + + test "serializes field with key instead of field name": + type MyObj {.serialize.} = object + field1 {.serialize("test").}: bool + field2: bool + + let obj = MyObj(field1: true, field2: true) + check obj.toJson == """{"test":true,"field2":true}""" + + +suite "json serialization - mode = Strict": + + test "serializes all fields in Strict mode": + type MyObj {.serialize(mode=Strict).} = object + field1: bool + field2: bool + + let obj = MyObj(field1: true, field2: true) + check obj.toJson == """{"field1":true,"field2":true}""" + + test "ignores ignored fields in Strict mode": + type MyObj {.serialize(mode=Strict).} = object + field1 {.serialize(ignore=true).}: bool + field2: bool + + let obj = MyObj(field1: true, field2: true) + check obj.toJson == """{"field1":true,"field2":true}""" diff --git a/tests/tests.nimble b/tests/tests.nimble new file mode 100644 index 0000000..ea43387 --- /dev/null +++ b/tests/tests.nimble @@ -0,0 +1,11 @@ +version = "0.1.0" +author = "nim-json authors" +description = "tests for nim-json library" +license = "MIT" + +requires "asynctest >= 0.5.1 & < 0.6.0" +requires "questionable >= 0.10.13 & < 0.11.0" + +task test, "Run the test suite": + exec "nimble install -d -y" + exec "nim c -r test"