initial impl, not compiling

This commit is contained in:
Eric 2024-02-07 09:40:48 +11:00
parent 5a8e85449d
commit d0a67a8eb0
No known key found for this signature in database
18 changed files with 1173 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
*
!*/
!*.*
nimble.develop
nimble.paths
.idea
vendor/
.vscode/

7
config.nims Normal file
View File

@ -0,0 +1,7 @@
--styleCheck:usages
--styleCheck:error
# begin Nimble config (version 1)
when fileExists("nimble.paths"):
include "nimble.paths"
# end Nimble config

5
serde.nim Normal file
View File

@ -0,0 +1,5 @@
import ./serde/serialize
import ./serde/deserialize
export serialize
export deserialize

20
serde.nimble Normal file
View File

@ -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"

19
serde/common.nim Normal file
View File

@ -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)

323
serde/deserialize.nim Normal file
View File

@ -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)

71
serde/pragmas.nim Normal file
View File

@ -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

164
serde/serialize.nim Normal file
View File

@ -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)

11
serde/types.nim Normal file
View File

@ -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

5
tests/config.nims Normal file
View File

@ -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")

5
tests/helpers.nim Normal file
View File

@ -0,0 +1,5 @@
import std/strutils
func flatten*(s: string): string =
s.replace(" ")
.replace("\n")

7
tests/test.nim Normal file
View File

@ -0,0 +1,7 @@
import ./testPragmas
import ./testSerialize
import ./testSerializeModes
import ./testDeserialize
import ./testDeserializeModes
{.warning[UnusedImport]:off.}

136
tests/testDeserialize.nim Normal file
View File

@ -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

View File

@ -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)

67
tests/testPragmas.nim Normal file
View File

@ -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())

96
tests/testSerialize.nim Normal file
View File

@ -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

View File

@ -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}"""

11
tests/tests.nimble Normal file
View File

@ -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"