summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--compiler/extccomp.nim3
-rw-r--r--lib/pure/json.nim162
-rw-r--r--tests/stdlib/tjsonmacro.nim138
-rw-r--r--tests/stdlib/tjsonmacro_reject.nim18
-rw-r--r--tests/stdlib/tjsonmacro_reject2.nim21
5 files changed, 314 insertions, 28 deletions
diff --git a/compiler/extccomp.nim b/compiler/extccomp.nim
index e6b23aae5..42c341651 100644
--- a/compiler/extccomp.nim
+++ b/compiler/extccomp.nim
@@ -764,8 +764,9 @@ proc callCCompiler*(projectfile: string) =
       add(objfiles, quoteShell(
           addFileExt(objFile, CC[cCompiler].objExt)))
     for x in toCompile:
+      let objFile = if noAbsolutePaths(): x.obj.extractFilename else: x.obj
       add(objfiles, ' ')
-      add(objfiles, quoteShell(x.obj))
+      add(objfiles, quoteShell(objFile))
 
     linkCmd = getLinkCmd(projectfile, objfiles)
     if optCompileOnly notin gGlobalOptions:
diff --git a/lib/pure/json.nim b/lib/pure/json.nim
index cea485c43..b5b84863a 100644
--- a/lib/pure/json.nim
+++ b/lib/pure/json.nim
@@ -1346,6 +1346,16 @@ proc createJsonIndexer(jsonNode: NimNode,
     indexNode
   )
 
+proc transformJsonIndexer(jsonNode: NimNode): NimNode =
+  case jsonNode.kind
+  of nnkBracketExpr:
+    result = newNimNode(nnkCurlyExpr)
+  else:
+    result = jsonNode.copy()
+
+  for child in jsonNode:
+    result.add(transformJsonIndexer(child))
+
 template verifyJsonKind(node: JsonNode, kinds: set[JsonNodeKind],
                         ast: string) =
   if node.kind notin kinds:
@@ -1524,6 +1534,35 @@ proc processObjField(field, jsonNode: NimNode): seq[NimNode] =
 
   doAssert result.len > 0
 
+proc processFields(obj: NimNode,
+                   jsonNode: NimNode): seq[NimNode] {.compileTime.} =
+  ## Process all the fields of an ``ObjectTy`` and any of its
+  ## parent type's fields (via inheritance).
+  result = @[]
+  case obj.kind
+  of nnkObjectTy:
+    expectKind(obj[2], nnkRecList)
+    for field in obj[2]:
+      let nodes = processObjField(field, jsonNode)
+      result.add(nodes)
+
+    # process parent type fields
+    case obj[1].kind
+    of nnkBracketExpr:
+      assert $obj[1][0] == "ref"
+      result.add(processFields(getType(obj[1][1]), jsonNode))
+    of nnkSym:
+      result.add(processFields(getType(obj[1]), jsonNode))
+    else:
+      discard
+  of nnkTupleTy:
+    for identDefs in obj:
+      expectKind(identDefs, nnkIdentDefs)
+      let nodes = processObjField(identDefs[0], jsonNode)
+      result.add(nodes)
+  else:
+    doAssert false, "Unable to process field type: " & $obj.kind
+
 proc processType(typeName: NimNode, obj: NimNode,
                  jsonNode: NimNode, isRef: bool): NimNode {.compileTime.} =
   ## Process a type such as ``Sym "float"`` or ``ObjectTy ...``.
@@ -1533,20 +1572,21 @@ proc processType(typeName: NimNode, obj: NimNode,
   ## .. code-block::plain
   ##     ObjectTy
   ##       Empty
-  ##       Empty
+  ##       InheritanceInformation
   ##       RecList
   ##         Sym "events"
   case obj.kind
-  of nnkObjectTy:
+  of nnkObjectTy, nnkTupleTy:
     # Create object constructor.
-    result = newNimNode(nnkObjConstr)
-    result.add(typeName) # Name of the type to construct.
+    result =
+      if obj.kind == nnkObjectTy: newNimNode(nnkObjConstr)
+      else: newNimNode(nnkPar)
 
-    # Process each object field and add it as an exprColonExpr
-    expectKind(obj[2], nnkRecList)
-    for field in obj[2]:
-      let nodes = processObjField(field, jsonNode)
-      result.add(nodes)
+    if obj.kind == nnkObjectTy:
+      result.add(typeName) # Name of the type to construct.
+
+    # Process each object/tuple field and add it as an exprColonExpr
+    result.add(processFields(obj, jsonNode))
 
     # Object might be null. So we need to check for that.
     if isRef:
@@ -1569,25 +1609,14 @@ proc processType(typeName: NimNode, obj: NimNode,
         `getEnumCall`
       )
   of nnkSym:
-    case ($typeName).normalize
-    of "float":
-      result = quote do:
-        (
-          verifyJsonKind(`jsonNode`, {JFloat, JInt}, astToStr(`jsonNode`));
-          if `jsonNode`.kind == JFloat: `jsonNode`.fnum else: `jsonNode`.num.float
-        )
+    let name = ($typeName).normalize
+    case name
     of "string":
       result = quote do:
         (
           verifyJsonKind(`jsonNode`, {JString, JNull}, astToStr(`jsonNode`));
           if `jsonNode`.kind == JNull: nil else: `jsonNode`.str
         )
-    of "int":
-      result = quote do:
-        (
-          verifyJsonKind(`jsonNode`, {JInt}, astToStr(`jsonNode`));
-          `jsonNode`.num.int
-        )
     of "biggestint":
       result = quote do:
         (
@@ -1601,12 +1630,36 @@ proc processType(typeName: NimNode, obj: NimNode,
           `jsonNode`.bval
         )
     else:
-      doAssert false, "Unable to process nnkSym " & $typeName
+      if name.startsWith("int") or name.startsWith("uint"):
+        result = quote do:
+          (
+            verifyJsonKind(`jsonNode`, {JInt}, astToStr(`jsonNode`));
+            `jsonNode`.num.`obj`
+          )
+      elif name.startsWith("float"):
+        result = quote do:
+          (
+            verifyJsonKind(`jsonNode`, {JInt, JFloat}, astToStr(`jsonNode`));
+            if `jsonNode`.kind == JFloat: `jsonNode`.fnum.`obj` else: `jsonNode`.num.`obj`
+          )
+      else:
+        doAssert false, "Unable to process nnkSym " & $typeName
   else:
     doAssert false, "Unable to process type: " & $obj.kind
 
   doAssert(not result.isNil(), "processType not initialised.")
 
+import options
+proc workaroundMacroNone[T](): Option[T] =
+  none(T)
+
+proc depth(n: NimNode, current = 0): int =
+  result = 1
+  for child in n:
+    let d = 1 + child.depth(current + 1)
+    if d > result:
+      result = d
+
 proc createConstructor(typeSym, jsonNode: NimNode): NimNode =
   ## Accepts a type description, i.e. "ref Type", "seq[Type]", "Type" etc.
   ##
@@ -1616,10 +1669,50 @@ proc createConstructor(typeSym, jsonNode: NimNode): NimNode =
   # echo("--createConsuctor-- \n", treeRepr(typeSym))
   # echo()
 
+  if depth(jsonNode) > 150:
+    error("The `to` macro does not support ref objects with cycles.", jsonNode)
+
   case typeSym.kind
   of nnkBracketExpr:
     var bracketName = ($typeSym[0]).normalize
     case bracketName
+    of "option":
+      # TODO: Would be good to verify that this is Option[T] from
+      # options module I suppose.
+      let lenientJsonNode = transformJsonIndexer(jsonNode)
+
+      let optionGeneric = typeSym[1]
+      let value = createConstructor(typeSym[1], jsonNode)
+      let workaround = bindSym("workaroundMacroNone") # TODO: Nim Bug: This shouldn't be necessary.
+
+      result = quote do:
+        (
+          if `lenientJsonNode`.isNil: `workaround`[`optionGeneric`]() else: some[`optionGeneric`](`value`)
+        )
+    of "table", "orderedtable":
+      let tableKeyType = typeSym[1]
+      if ($tableKeyType).cmpIgnoreStyle("string") != 0:
+        error("JSON doesn't support keys of type " & $tableKeyType)
+      let tableValueType = typeSym[2]
+
+      let forLoopKey = genSym(nskForVar, "key")
+      let indexerNode = createJsonIndexer(jsonNode, forLoopKey)
+      let constructorNode = createConstructor(tableValueType, indexerNode)
+
+      let tableInit =
+        if bracketName == "table":
+          bindSym("initTable")
+        else:
+          bindSym("initOrderedTable")
+
+      # Create a statement expression containing a for loop.
+      result = quote do:
+        (
+          var map = `tableInit`[`tableKeyType`, `tableValueType`]();
+          verifyJsonKind(`jsonNode`, {JObject}, astToStr(`jsonNode`));
+          for `forLoopKey` in keys(`jsonNode`.fields): map[`forLoopKey`] = `constructorNode`;
+          map
+        )
     of "ref":
       # Ref type.
       var typeName = $typeSym[1]
@@ -1663,12 +1756,23 @@ proc createConstructor(typeSym, jsonNode: NimNode): NimNode =
       let obj = getType(typeSym)
       result = processType(typeSym, obj, jsonNode, false)
   of nnkSym:
+    # Handle JsonNode.
+    if ($typeSym).cmpIgnoreStyle("jsonnode") == 0:
+      return jsonNode
+
+    # Handle all other types.
     let obj = getType(typeSym)
     if obj.kind == nnkBracketExpr:
       # When `Sym "Foo"` turns out to be a `ref object`.
       result = createConstructor(obj, jsonNode)
     else:
       result = processType(typeSym, obj, jsonNode, false)
+  of nnkTupleTy:
+    result = processType(typeSym, typeSym, jsonNode, false)
+  of nnkPar:
+    # TODO: The fact that `jsonNode` here works to give a good line number
+    # is weird. Specifying typeSym should work but doesn't.
+    error("Use a named tuple instead of: " & $toStrLit(typeSym), jsonNode)
   else:
     doAssert false, "Unable to create constructor for: " & $typeSym.kind
 
@@ -1796,10 +1900,18 @@ macro to*(node: JsonNode, T: typedesc): untyped =
   expectKind(typeNode, nnkBracketExpr)
   doAssert(($typeNode[0]).normalize == "typedesc")
 
-  result = createConstructor(typeNode[1], node)
+  # Create `temp` variable to store the result in case the user calls this
+  # on `parseJson` (see bug #6604).
+  result = newNimNode(nnkStmtListExpr)
+  let temp = genSym(nskLet, "temp")
+  result.add quote do:
+    let `temp` = `node`
+
+  let constructor = createConstructor(typeNode[1], temp)
   # TODO: Rename postProcessValue and move it (?)
-  result = postProcessValue(result)
+  result.add(postProcessValue(constructor))
 
+  # echo(treeRepr(result))
   # echo(toStrLit(result))
 
 when false:
diff --git a/tests/stdlib/tjsonmacro.nim b/tests/stdlib/tjsonmacro.nim
index 153cf8556..2cdd82305 100644
--- a/tests/stdlib/tjsonmacro.nim
+++ b/tests/stdlib/tjsonmacro.nim
@@ -2,7 +2,7 @@ discard """
   file: "tjsonmacro.nim"
   output: ""
 """
-import json, strutils
+import json, strutils, options, tables
 
 when isMainModule:
   # Tests inspired by own use case (with some additional tests).
@@ -246,4 +246,138 @@ when isMainModule:
     var b = Bird(age: 3, height: 1.734, name: "bardo", colors: [red, blue])
     let jnode = %b
     let data = jnode.to(Bird)
-    doAssert data == b
\ No newline at end of file
+    doAssert data == b
+
+  block:
+    type
+      MsgBase = ref object of RootObj
+        name*: string
+
+      MsgChallenge = ref object of MsgBase
+        challenge*: string
+
+    let data = %*{"name": "foo", "challenge": "bar"}
+    let msg = data.to(MsgChallenge)
+    doAssert msg.name == "foo"
+    doAssert msg.challenge == "bar"
+
+  block:
+    type
+      Color = enum Red, Brown
+      Thing = object
+        animal: tuple[fur: bool, legs: int]
+        color: Color
+
+    var j = parseJson("""
+      {"animal":{"fur":true,"legs":6},"color":"Red"}
+    """)
+
+    let parsed = to(j, Thing)
+    doAssert parsed.animal.fur
+    doAssert parsed.animal.legs == 6
+    doAssert parsed.color == Red
+
+  block:
+    type
+      Car = object
+        engine: tuple[name: string, capacity: float]
+        model: string
+
+    let j = """
+      {"engine": {"name": "V8", "capacity": 5.5}, "model": "Skyline"}
+    """
+
+    var i = 0
+    proc mulTest: JsonNode =
+      i.inc()
+      return parseJson(j)
+
+    let parsed = mulTest().to(Car)
+    doAssert parsed.engine.name == "V8"
+
+    doAssert i == 1
+
+  block:
+    # Option[T] support!
+    type
+      Car1 = object # TODO: Codegen bug when `Car`
+        engine: tuple[name: string, capacity: Option[float]]
+        model: string
+        year: Option[int]
+
+    let noYear = """
+      {"engine": {"name": "V8", "capacity": 5.5}, "model": "Skyline"}
+    """
+
+    let noYearParsed = parseJson(noYear)
+    let noYearDeser = to(noYearParsed, Car1)
+    doAssert noYearDeser.engine.capacity == some(5.5)
+    doAssert noYearDeser.year.isNone
+    doAssert noYearDeser.engine.name == "V8"
+
+  # Table[T, Y] support.
+  block:
+    type
+      Friend = object
+        name: string
+        age: int
+
+      Dynamic = object
+        name: string
+        friends: Table[string, Friend]
+
+    let data = """
+      {"friends": {
+                    "John": {"name": "John", "age": 35},
+                    "Elizabeth": {"name": "Elizabeth", "age": 23}
+                  }, "name": "Dominik"}
+    """
+
+    let dataParsed = parseJson(data)
+    let dataDeser = to(dataParsed, Dynamic)
+    doAssert dataDeser.name == "Dominik"
+    doAssert dataDeser.friends["John"].age == 35
+    doAssert dataDeser.friends["Elizabeth"].age == 23
+
+  # JsonNode support
+  block:
+    type
+      Test = object
+        name: string
+        fallback: JsonNode
+
+    let data = """
+      {"name": "FooBar", "fallback": 56.42}
+    """
+
+    let dataParsed = parseJson(data)
+    let dataDeser = to(dataParsed, Test)
+    doAssert dataDeser.name == "FooBar"
+    doAssert dataDeser.fallback.kind == JFloat
+    doAssert dataDeser.fallback.getFloat() == 56.42
+
+  # int64, float64 etc support.
+  block:
+    type
+      Test1 = object
+        a: int8
+        b: int16
+        c: int32
+        d: int64
+        e: uint8
+        f: uint16
+        g: uint32
+        h: uint64
+        i: float32
+        j: float64
+
+    let data = """
+      {"a": 1, "b": 2, "c": 3, "d": 4, "e": 5, "f": 6, "g": 7,
+       "h": 8, "i": 9.9, "j": 10.10}
+    """
+
+    let dataParsed = parseJson(data)
+    let dataDeser = to(dataParsed, Test1)
+    doAssert dataDeser.a == 1
+    doAssert dataDeser.f == 6
+    doAssert dataDeser.i == 9.9'f32
\ No newline at end of file
diff --git a/tests/stdlib/tjsonmacro_reject.nim b/tests/stdlib/tjsonmacro_reject.nim
new file mode 100644
index 000000000..00506449f
--- /dev/null
+++ b/tests/stdlib/tjsonmacro_reject.nim
@@ -0,0 +1,18 @@
+discard """
+  file: "tjsonmacro_reject.nim"
+  line: 11
+  errormsg: "Use a named tuple instead of: (string, float)"
+"""
+
+import json
+
+type
+  Car = object
+    engine: (string, float)
+    model: string
+
+let j = """
+  {"engine": {"name": "V8", "capacity": 5.5}, model: "Skyline"}
+"""
+let parsed = parseJson(j)
+echo(to(parsed, Car))
\ No newline at end of file
diff --git a/tests/stdlib/tjsonmacro_reject2.nim b/tests/stdlib/tjsonmacro_reject2.nim
new file mode 100644
index 000000000..b01153553
--- /dev/null
+++ b/tests/stdlib/tjsonmacro_reject2.nim
@@ -0,0 +1,21 @@
+discard """
+  file: "tjsonmacro_reject2.nim"
+  line: 10
+  errormsg: "The `to` macro does not support ref objects with cycles."
+"""
+import json
+
+type
+  Misdirection = object
+    cycle: Cycle
+
+  Cycle = ref object
+    foo: string
+    cycle: Misdirection
+
+let data = """
+  {"cycle": null}
+"""
+
+let dataParsed = parseJson(data)
+let dataDeser = to(dataParsed, Cycle)
\ No newline at end of file