summary refs log tree commit diff stats
path: root/lib/std
diff options
context:
space:
mode:
authorTimothee Cour <timothee.cour2@gmail.com>2020-06-08 01:35:23 -0700
committerGitHub <noreply@github.com>2020-06-08 10:35:23 +0200
commitc7a1a7b8bf38ab5c15decdf913dfc272ad922c21 (patch)
treed92128143dd308811633cf1ef5dc159d6ab43850 /lib/std
parent733bd76f6bc8253f255df4cec26099502090264b (diff)
downloadNim-c7a1a7b8bf38ab5c15decdf913dfc272ad922c21.tar.gz
`toJson`, `jsonTo`, json (de)serialization for custom types; remove dependency on strtabs thanks to a hooking mechanism (#14563)
* json custom serialization; application for strtabs
* serialize using nesting
* make toJson more feature complete
* add since
* Revert "Improve JSON serialisation of strtabs (#14549)"

This reverts commit 7cb4ef26addb3bb5ce2405d8396df6fd41664dae.

* better approach via mixin
* toJson, jsonTo
* fix test
* address comments
* move to jsonutils
* doc
* cleanups
* also test for js
* also test for vm
Diffstat (limited to 'lib/std')
-rw-r--r--lib/std/jsonutils.nim126
1 files changed, 126 insertions, 0 deletions
diff --git a/lib/std/jsonutils.nim b/lib/std/jsonutils.nim
new file mode 100644
index 000000000..bfa600fa9
--- /dev/null
+++ b/lib/std/jsonutils.nim
@@ -0,0 +1,126 @@
+##[
+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.
+
+]##
+
+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 direction:
+add a way to customize serialization, for eg allowing missing
+or extra fields in JsonNode, field renaming, and a way to handle cyclic references
+using a cache of already visited addresses.
+]#
+
+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) =
+  ## 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)
+    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 =
+  ## 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:
+    const isNamed = T is object or isNamedTuple(T)
+    when isNamed:
+      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 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))
+  elif T is distinct: result = toJson(a.distinctBase)
+  elif T is bool: result = %(a)
+  elif T is Ordinal: result = %(a.ord)
+  else: result = %a