Skip to content

Commit

Permalink
toJson, jsonTo
Browse files Browse the repository at this point in the history
  • Loading branch information
timotheecour committed Jun 6, 2020
1 parent 5ad78f5 commit 5885916
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 41 deletions.
2 changes: 2 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@
- `osproc.execCmdEx` now takes an optional `input` for stdin, `workingDir` and `env`
parameters.

- added `json.jsonTo,toJson` for json serialization/deserialization from custom types.

## Language changes
- In the newruntime it is now allowed to assign discriminator field without restrictions as long as case object doesn't have custom destructor. Discriminator value doesn't have to be a constant either. If you have custom destructor for case object and you do want to freely assign discriminator fields, it is recommended to refactor object into 2 objects like this:

Expand Down
104 changes: 88 additions & 16 deletions lib/pure/json.nim
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,9 @@ runnableExamples:
doAssert $(%* Foo()) == """{"a1":0,"a2":0,"a0":0,"a3":0,"a4":0}"""

import
hashes, tables, strutils, lexbase, streams, macros, parsejson,
options
hashes, tables, strutils, lexbase, streams, macros, parsejson

import options # xxx remove this dependency using same approach as https://github.com/nim-lang/Nim/pull/14563
import std/private/since

export
Expand Down Expand Up @@ -1266,11 +1267,88 @@ when false:
# To get that we shall use, obj["json"]

proc isNamedTuple(T: typedesc): bool {.magic: "TypeTrait".}
proc distinctBase(T: typedesc): typedesc {.magic: "TypeTrait".}
template distinctBase[T](a: T): untyped = distinctBase(type(a))(a)

proc checkJsonImpl(cond: bool, condStr: string, msg = "") =
if not cond:
# just pick 1 exception type for simplicity; other choices would be:
# JsonError, JsonParser, JsonKindError
raise newException(ValueError, msg)

template checkJson(cond: untyped, msg = "") =
checkJsonImpl(cond, astToStr(cond), msg)

proc fromJson*[T](a: var T, b: JsonNode) {.since: (1,3,5).} =
## inplace version of `jsonTo`
#[
adding "json path" leading to `b` can be added in future work.
]#
checkJson b != nil, $($T, b)
when false: discard
elif compiles(fromJsonHook(a, b)): fromJsonHook(a, b)
elif T is bool: a = to(b,T)
elif T is Table | OrderedTable:
a.clear
for k,v in b:
a[k] = jsonTo(v, typeof(a[k]))
elif T is enum:
case b.kind
of JInt: a = T(b.getBiggestInt())
of JString: a = parseEnum[T](b.getStr())
else: checkJson false, $($T, " ", b)
elif T is Ordinal: a = T(to(b, int))
elif T is pointer: a = cast[pointer](to(b, int))
elif T is distinct: a.distinctBase.fromJson(b)
elif T is string|SomeNumber: a = to(b,T)
elif T is JsonNode: a = b
elif T is ref | ptr:
if b.kind == JNull: a = nil
else:
a = T()
fromJson(a[], b)
elif T is array:
checkJson a.len == b.len, $(a.len, b.len, $T)
for i, val in b.getElems:
fromJson(a[i], val)
elif T is seq:
a.setLen b.len
for i, val in b.getElems:
fromJson(a[i], val)
elif T is object | tuple:
const isNamed = T is object or isNamedTuple(T)
when isNamed:
checkJson b.kind == JObject, $(b.kind) # we could customize whether to allow JNull
var num = 0
for key, val in fieldPairs(a):
num.inc
if b.hasKey key:
fromJson(val, b[key])
else:
# we could customize to allow this
checkJson false, $($T, key, b)
checkJson b.len == num, $(b.len, num, $T, b) # could customize
else:
checkJson b.kind == JArray, $(b.kind) # we could customize whether to allow JNull
var i = 0
for val in fields(a):
fromJson(val, b[i])
i.inc
else:
# checkJson not appropriate here
static: doAssert false, "not yet implemented: " & $T

proc jsonTo*(b: JsonNode, T: typedesc): T {.since: (1,3,5).} =
## reverse of `toJson`
fromJson(result, b)

proc toJson*[T](a: T): JsonNode {.since: (1,3,5).} =
## like `%` but allows custom serialization hook if `serialize(a: T)` is in scope
when compiles(toJsonHook(a)):
result = toJsonHook(a)
when false: discard
elif compiles(toJsonHook(a)): result = toJsonHook(a)
elif T is Table | OrderedTable:
result = newJObject()
for k, v in pairs(a): result[k] = toJson(v)
elif T is object | tuple:
const isNamed = T is object or isNamedTuple(T)
when isNamed:
Expand All @@ -1284,18 +1362,12 @@ proc toJson*[T](a: T): JsonNode {.since: (1,3,5).} =
else: result = toJson(a[])
elif T is array | seq:
result = newJArray()
for ai in a:
result.add toJson(ai)
elif T is pointer:
result = toJson(cast[int](a))
elif T is distinct:
result = toJson(a.distinctBase)
elif T is bool:
result = %(a)
elif T is Ordinal:
result = %(cast[int](a))
else:
result = %a
for ai in a: result.add toJson(ai)
elif T is pointer: result = toJson(cast[int](a))
elif T is distinct: result = toJson(a.distinctBase)
elif T is bool: result = %(a)
elif T is Ordinal: result = %(cast[int](a))
else: result = %a

when isMainModule:
# Note: Macro tests are in tests/stdlib/tjsonmacro.nim
Expand Down
28 changes: 18 additions & 10 deletions lib/pure/strtabs.nim
Original file line number Diff line number Diff line change
Expand Up @@ -419,16 +419,24 @@ proc `%`*(f: string, t: StringTableRef, flags: set[FormatFlag] = {}): string {.
add(result, f[i])
inc(i)

proc toJsonHook*[](a: StringTableRef): auto =
## allows a custom serializer (eg json) to serialize this as we want.
mixin newJObject
mixin toJson
result = newJObject()
result["mode"] = toJson($a.mode)
let t = newJObject()
for k,v in a:
t[k] = toJson(v)
result["table"] = t
since (1,3,5):
proc fromJsonHook*[T](a: var StringTableRef, b: T) =
## for json.fromJson
mixin jsonTo
var mode = jsonTo(b["mode"], StringTableMode)
a = newStringTable(mode)
let b2 = b["table"]
for k,v in b2: a[k] = jsonTo(v, string)

proc toJsonHook*[](a: StringTableRef): auto =
## for json.toJson
mixin newJObject
mixin toJson
result = newJObject()
result["mode"] = toJson($a.mode)
let t = newJObject()
for k,v in a: t[k] = toJson(v)
result["table"] = t

when isMainModule:
var x = {"k": "v", "11": "22", "565": "67"}.newStringTable
Expand Down
37 changes: 22 additions & 15 deletions tests/stdlib/tjsonmacro.nim
Original file line number Diff line number Diff line change
Expand Up @@ -639,18 +639,25 @@ static:

import strtabs

proc testCustom()=
var t = {"name": "John", "city": "Monaco"}.newStringTable
let s = toJson(t)
doAssert $s == """{"mode":"modeCaseSensitive","table":{"city":"Monaco","name":"John"}}""", $s

proc testToJson() =
var t = {"z": "Z", "y": "Y"}.newStringTable
type A = ref object
a1: string
let a = (1.1, "fo", 'x', @[10,11], [true, false], [t,newStringTable()], (foo: 0.5'f32, bar: A(a1: "abc"), bar2: A.default))
let j = a.toJson
doAssert $j == """[1.1,"fo",120,[10,11],[true,false],[{"mode":"modeCaseSensitive","table":{"y":"Y","z":"Z"}},{"mode":"modeCaseSensitive","table":{}}],{"foo":0.5,"bar":{"a1":"abc"},"bar2":null}]"""

testCustom()
testToJson()
# xxx move to `tjson` pending https://github.com/nim-lang/Nim/pull/14572
proc testRoundtrip[T](t: T, expected: string) =
let j = t.toJson
doAssert $j == expected, $j
doAssert j.jsonTo(T).toJson == j

block: # toJson, jsonTo
type Foo = distinct float
testRoundtrip('x', """120""")
when not defined(js):
testRoundtrip(cast[pointer](12345)): """12345"""
testRoundtrip(Foo(1.5)): """1.5"""
testRoundtrip({"z": "Z", "y": "Y"}.toOrderedTable): """{"z":"Z","y":"Y"}"""
testRoundtrip({"z": (f1: 'f'), }.toTable): """{"z":{"f1":102}}"""
testRoundtrip({"name": "John", "city": "Monaco"}.newStringTable): """{"mode":"modeCaseSensitive","table":{"city":"Monaco","name":"John"}}"""
block: # complex example
let t = {"z": "Z", "y": "Y"}.newStringTable
type A = ref object
a1: string
let a = (1.1, "fo", 'x', @[10,11], [true, false], [t,newStringTable()], [0'u8,3'u8], (foo: 0.5'f32, bar: A(a1: "abc"), bar2: A.default))
testRoundtrip(a):
"""[1.1,"fo",120,[10,11],[true,false],[{"mode":"modeCaseSensitive","table":{"y":"Y","z":"Z"}},{"mode":"modeCaseSensitive","table":{}}],[0,3],{"foo":0.5,"bar":{"a1":"abc"},"bar2":null}]"""

0 comments on commit 5885916

Please sign in to comment.