mirror of
https://github.com/nim-lang/Nim.git
synced 2025-12-29 17:34:43 +00:00
201 lines
6.3 KiB
Nim
201 lines
6.3 KiB
Nim
##[
|
|
This module implements a hookable (de)serialization for arbitrary types.
|
|
Design goal: avoid importing modules where a custom serialization is needed;
|
|
see strtabs.fromJsonHook,toJsonHook for an example.
|
|
]##
|
|
|
|
runnableExamples:
|
|
import std/[strtabs,json]
|
|
type Foo = ref object
|
|
t: bool
|
|
z1: int8
|
|
let a = (1.5'f32, (b: "b2", a: "a2"), 'x', @[Foo(t: true, z1: -3), nil], [{"name": "John"}.newStringTable])
|
|
let j = a.toJson
|
|
doAssert j.jsonTo(type(a)).toJson == j
|
|
|
|
import std/[json,tables,strutils]
|
|
|
|
#[
|
|
xxx
|
|
use toJsonHook,fromJsonHook for Table|OrderedTable
|
|
add Options support also using toJsonHook,fromJsonHook and remove `json=>options` dependency
|
|
|
|
Future directions:
|
|
add a way to customize serialization, for eg:
|
|
* allowing missing or extra fields in JsonNode
|
|
* field renaming
|
|
* allow serializing `enum` and `char` as `string` instead of `int`
|
|
(enum is more compact/efficient, and robust to enum renamings, but string
|
|
is more human readable)
|
|
* handle cyclic references, using a cache of already visited addresses
|
|
]#
|
|
|
|
import std/macros
|
|
|
|
proc isNamedTuple(T: typedesc): bool {.magic: "TypeTrait".}
|
|
proc distinctBase(T: typedesc): typedesc {.magic: "TypeTrait".}
|
|
template distinctBase[T](a: T): untyped = distinctBase(type(a))(a)
|
|
|
|
macro getDiscriminants(a: typedesc): seq[string] =
|
|
## return the discriminant keys
|
|
# candidate for std/typetraits
|
|
var a = a.getTypeImpl
|
|
doAssert a.kind == nnkBracketExpr
|
|
let sym = a[1]
|
|
let t = sym.getTypeImpl
|
|
let t2 = t[2]
|
|
doAssert t2.kind == nnkRecList
|
|
result = newTree(nnkBracket)
|
|
for ti in t2:
|
|
if ti.kind == nnkRecCase:
|
|
let key = ti[0][0]
|
|
let typ = ti[0][1]
|
|
result.add newLit key.strVal
|
|
if result.len > 0:
|
|
result = quote do:
|
|
@`result`
|
|
else:
|
|
result = quote do:
|
|
seq[string].default
|
|
|
|
macro initCaseObject(a: typedesc, fun: untyped): untyped =
|
|
## does the minimum to construct a valid case object, only initializing
|
|
## the discriminant fields; see also `getDiscriminants`
|
|
# maybe candidate for std/typetraits
|
|
var a = a.getTypeImpl
|
|
doAssert a.kind == nnkBracketExpr
|
|
let sym = a[1]
|
|
let t = sym.getTypeImpl
|
|
var t2: NimNode
|
|
case t.kind
|
|
of nnkObjectTy: t2 = t[2]
|
|
of nnkRefTy: t2 = t[0].getTypeImpl[2]
|
|
else: doAssert false, $t.kind # xxx `nnkPtrTy` could be handled too
|
|
doAssert t2.kind == nnkRecList
|
|
result = newTree(nnkObjConstr)
|
|
result.add sym
|
|
for ti in t2:
|
|
if ti.kind == nnkRecCase:
|
|
let key = ti[0][0]
|
|
let typ = ti[0][1]
|
|
let key2 = key.strVal
|
|
let val = quote do:
|
|
`fun`(`key2`, typedesc[`typ`])
|
|
result.add newTree(nnkExprColonExpr, key, val)
|
|
|
|
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)
|
|
|
|
template fromJsonFields(a, b, T, keys) =
|
|
checkJson b.kind == JObject, $(b.kind) # we could customize whether to allow JNull
|
|
var num = 0
|
|
for key, val in fieldPairs(a):
|
|
num.inc
|
|
when key notin keys:
|
|
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
|
|
|
|
proc fromJson*[T](a: var T, b: JsonNode) =
|
|
## inplace version of `jsonTo`
|
|
#[
|
|
adding "json path" leading to `b` can be added in future work.
|
|
]#
|
|
checkJson b != nil, $($T, b)
|
|
when 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:
|
|
when nimvm:
|
|
# bug, potentially related to https://github.com/nim-lang/Nim/issues/12282
|
|
a = T(jsonTo(b, distinctBase(T)))
|
|
else:
|
|
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)
|
|
var i = 0
|
|
for ai in mitems(a):
|
|
fromJson(ai, b[i])
|
|
i.inc
|
|
elif T is seq:
|
|
a.setLen b.len
|
|
for i, val in b.getElems:
|
|
fromJson(a[i], val)
|
|
elif T is object:
|
|
template fun(key, typ): untyped =
|
|
jsonTo(b[key], typ)
|
|
a = initCaseObject(T, fun)
|
|
const keys = getDiscriminants(T)
|
|
fromJsonFields(a, b, T, keys)
|
|
elif T is tuple:
|
|
when isNamedTuple(T):
|
|
fromJsonFields(a, b, T, seq[string].default)
|
|
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
|
|
checkJson b.len == i, $(b.len, i, $T, b) # could customize
|
|
else:
|
|
# checkJson not appropriate here
|
|
static: doAssert false, "not yet implemented: " & $T
|
|
|
|
proc jsonTo*(b: JsonNode, T: typedesc): T =
|
|
## reverse of `toJson`
|
|
fromJson(result, b)
|
|
|
|
proc toJson*[T](a: T): JsonNode =
|
|
## serializes `a` to json; uses `toJsonHook(a: T)` if it's in scope to
|
|
## customize serialization, see strtabs.toJsonHook for an example.
|
|
when 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:
|
|
when T is object or isNamedTuple(T):
|
|
result = newJObject()
|
|
for k, v in a.fieldPairs: result[k] = toJson(v)
|
|
else:
|
|
result = newJArray()
|
|
for v in a.fields: result.add toJson(v)
|
|
elif T is ref | ptr:
|
|
if system.`==`(a, nil): result = newJNull()
|
|
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))
|
|
# edge case: `a == nil` could've also led to `newJNull()`, but this results
|
|
# in simpler code for `toJson` and `fromJson`.
|
|
elif T is distinct: result = toJson(a.distinctBase)
|
|
elif T is bool: result = %(a)
|
|
elif T is Ordinal: result = %(a.ord)
|
|
else: result = %a
|