fromJson: support object variants (#14694)
This commit is contained in:
parent
a3ae52de1a
commit
37081e2945
2 changed files with 126 additions and 15 deletions
|
|
@ -30,10 +30,59 @@ add a way to customize serialization, for eg:
|
||||||
* handle cyclic references, using a cache of already visited addresses
|
* handle cyclic references, using a cache of already visited addresses
|
||||||
]#
|
]#
|
||||||
|
|
||||||
|
import std/macros
|
||||||
|
|
||||||
proc isNamedTuple(T: typedesc): bool {.magic: "TypeTrait".}
|
proc isNamedTuple(T: typedesc): bool {.magic: "TypeTrait".}
|
||||||
proc distinctBase(T: typedesc): typedesc {.magic: "TypeTrait".}
|
proc distinctBase(T: typedesc): typedesc {.magic: "TypeTrait".}
|
||||||
template distinctBase[T](a: T): untyped = distinctBase(type(a))(a)
|
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 = "") =
|
proc checkJsonImpl(cond: bool, condStr: string, msg = "") =
|
||||||
if not cond:
|
if not cond:
|
||||||
# just pick 1 exception type for simplicity; other choices would be:
|
# just pick 1 exception type for simplicity; other choices would be:
|
||||||
|
|
@ -43,6 +92,19 @@ proc checkJsonImpl(cond: bool, condStr: string, msg = "") =
|
||||||
template checkJson(cond: untyped, msg = "") =
|
template checkJson(cond: untyped, msg = "") =
|
||||||
checkJsonImpl(cond, astToStr(cond), 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) =
|
proc fromJson*[T](a: var T, b: JsonNode) =
|
||||||
## inplace version of `jsonTo`
|
## inplace version of `jsonTo`
|
||||||
#[
|
#[
|
||||||
|
|
@ -85,25 +147,22 @@ proc fromJson*[T](a: var T, b: JsonNode) =
|
||||||
a.setLen b.len
|
a.setLen b.len
|
||||||
for i, val in b.getElems:
|
for i, val in b.getElems:
|
||||||
fromJson(a[i], val)
|
fromJson(a[i], val)
|
||||||
elif T is object | tuple:
|
elif T is object:
|
||||||
const isNamed = T is object or isNamedTuple(T)
|
template fun(key, typ): untyped =
|
||||||
when isNamed:
|
jsonTo(b[key], typ)
|
||||||
checkJson b.kind == JObject, $(b.kind) # we could customize whether to allow JNull
|
a = initCaseObject(T, fun)
|
||||||
var num = 0
|
const keys = getDiscriminants(T)
|
||||||
for key, val in fieldPairs(a):
|
fromJsonFields(a, b, T, keys)
|
||||||
num.inc
|
elif T is tuple:
|
||||||
if b.hasKey key:
|
when isNamedTuple(T):
|
||||||
fromJson(val, b[key])
|
fromJsonFields(a, b, T, seq[string].default)
|
||||||
else:
|
|
||||||
# we could customize to allow this
|
|
||||||
checkJson false, $($T, key, b)
|
|
||||||
checkJson b.len == num, $(b.len, num, $T, b) # could customize
|
|
||||||
else:
|
else:
|
||||||
checkJson b.kind == JArray, $(b.kind) # we could customize whether to allow JNull
|
checkJson b.kind == JArray, $(b.kind) # we could customize whether to allow JNull
|
||||||
var i = 0
|
var i = 0
|
||||||
for val in fields(a):
|
for val in fields(a):
|
||||||
fromJson(val, b[i])
|
fromJson(val, b[i])
|
||||||
i.inc
|
i.inc
|
||||||
|
checkJson b.len == i, $(b.len, i, $T, b) # could customize
|
||||||
else:
|
else:
|
||||||
# checkJson not appropriate here
|
# checkJson not appropriate here
|
||||||
static: doAssert false, "not yet implemented: " & $T
|
static: doAssert false, "not yet implemented: " & $T
|
||||||
|
|
@ -120,8 +179,7 @@ proc toJson*[T](a: T): JsonNode =
|
||||||
result = newJObject()
|
result = newJObject()
|
||||||
for k, v in pairs(a): result[k] = toJson(v)
|
for k, v in pairs(a): result[k] = toJson(v)
|
||||||
elif T is object | tuple:
|
elif T is object | tuple:
|
||||||
const isNamed = T is object or isNamedTuple(T)
|
when T is object or isNamedTuple(T):
|
||||||
when isNamed:
|
|
||||||
result = newJObject()
|
result = newJObject()
|
||||||
for k, v in a.fieldPairs: result[k] = toJson(v)
|
for k, v in a.fieldPairs: result[k] = toJson(v)
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,9 @@ proc testRoundtrip[T](t: T, expected: string) =
|
||||||
let j = t.toJson
|
let j = t.toJson
|
||||||
doAssert $j == expected, $j
|
doAssert $j == expected, $j
|
||||||
doAssert j.jsonTo(T).toJson == j
|
doAssert j.jsonTo(T).toJson == j
|
||||||
|
var t2: T
|
||||||
|
t2.fromJson(j)
|
||||||
|
doAssert t2.toJson == j
|
||||||
|
|
||||||
import tables
|
import tables
|
||||||
import strtabs
|
import strtabs
|
||||||
|
|
@ -66,5 +69,55 @@ template fn() =
|
||||||
doAssert b2.ord == 1 # explains the `1`
|
doAssert b2.ord == 1 # explains the `1`
|
||||||
testRoundtrip(a): """[1,2,3]"""
|
testRoundtrip(a): """[1,2,3]"""
|
||||||
|
|
||||||
|
block: # case object
|
||||||
|
type Foo = object
|
||||||
|
x0: float
|
||||||
|
case t1: bool
|
||||||
|
of true: z1: int8
|
||||||
|
of false: z2: uint16
|
||||||
|
x1: string
|
||||||
|
testRoundtrip(Foo(t1: true, z1: 5, x1: "bar")): """{"x0":0.0,"t1":true,"z1":5,"x1":"bar"}"""
|
||||||
|
testRoundtrip(Foo(x0: 1.5, t1: false, z2: 6)): """{"x0":1.5,"t1":false,"z2":6,"x1":""}"""
|
||||||
|
type PFoo = ref Foo
|
||||||
|
testRoundtrip(PFoo(x0: 1.5, t1: false, z2: 6)): """{"x0":1.5,"t1":false,"z2":6,"x1":""}"""
|
||||||
|
|
||||||
|
block: # ref case object
|
||||||
|
type Foo = ref object
|
||||||
|
x0: float
|
||||||
|
case t1: bool
|
||||||
|
of true: z1: int8
|
||||||
|
of false: z2: uint16
|
||||||
|
x1: string
|
||||||
|
testRoundtrip(Foo(t1: true, z1: 5, x1: "bar")): """{"x0":0.0,"t1":true,"z1":5,"x1":"bar"}"""
|
||||||
|
testRoundtrip(Foo(x0: 1.5, t1: false, z2: 6)): """{"x0":1.5,"t1":false,"z2":6,"x1":""}"""
|
||||||
|
|
||||||
|
block: # generic case object
|
||||||
|
type Foo[T] = ref object
|
||||||
|
x0: float
|
||||||
|
case t1: bool
|
||||||
|
of true: z1: int8
|
||||||
|
of false: z2: uint16
|
||||||
|
x1: string
|
||||||
|
testRoundtrip(Foo[float](t1: true, z1: 5, x1: "bar")): """{"x0":0.0,"t1":true,"z1":5,"x1":"bar"}"""
|
||||||
|
testRoundtrip(Foo[int](x0: 1.5, t1: false, z2: 6)): """{"x0":1.5,"t1":false,"z2":6,"x1":""}"""
|
||||||
|
# sanity check: nesting inside a tuple
|
||||||
|
testRoundtrip((Foo[int](x0: 1.5, t1: false, z2: 6), "foo")): """[{"x0":1.5,"t1":false,"z2":6,"x1":""},"foo"]"""
|
||||||
|
|
||||||
|
block: # case object: 2 discriminants, `when` branch, range discriminant
|
||||||
|
type Foo[T] = object
|
||||||
|
case t1: bool
|
||||||
|
of true:
|
||||||
|
z1: int8
|
||||||
|
of false:
|
||||||
|
z2: uint16
|
||||||
|
when T is float:
|
||||||
|
case t2: range[0..3]
|
||||||
|
of 0: z3: int8
|
||||||
|
of 2,3: z4: uint16
|
||||||
|
else: discard
|
||||||
|
testRoundtrip(Foo[float](t1: true, z1: 5, t2: 3, z4: 12)): """{"t1":true,"z1":5,"t2":3,"z4":12}"""
|
||||||
|
testRoundtrip(Foo[int](t1: false, z2: 7)): """{"t1":false,"z2":7}"""
|
||||||
|
# pending https://github.com/nim-lang/Nim/issues/14698, test with `type Foo[T] = ref object`
|
||||||
|
|
||||||
static: fn()
|
static: fn()
|
||||||
fn()
|
fn()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue