diff --git a/lib/core/macros.nim b/lib/core/macros.nim index 4e44b9b4f6e58..2486104c5da0c 100644 --- a/lib/core/macros.nim +++ b/lib/core/macros.nim @@ -1472,7 +1472,12 @@ proc customPragmaNode(n: NimNode): NimNode = if n.kind in {nnkDotExpr, nnkCheckedFieldExpr}: let name = $(if n.kind == nnkCheckedFieldExpr: n[0][1] else: n[1]) let typInst = getTypeInst(if n.kind == nnkCheckedFieldExpr or n[0].kind == nnkHiddenDeref: n[0][0] else: n[0]) - var typDef = getImpl(if typInst.kind == nnkVarTy: typInst[0] else: typInst) + var typDef = getImpl( + if typInst.kind == nnkVarTy or + typInst.kind == nnkBracketExpr: + typInst[0] + else: typInst + ) while typDef != nil: typDef.expectKind(nnkTypeDef) let typ = typDef[2] @@ -1493,12 +1498,12 @@ proc customPragmaNode(n: NimNode): NimNode = let varNode = identDefs[i] # if it is and empty branch, skip if varNode[0].kind == nnkNilLit: continue - if varNode[1].kind == nnkIdentDefs: - identDefsStack.add(varNode[1]) - else: # nnkRecList - for j in 0 ..< varNode[1].len: - identDefsStack.add(varNode[1][j]) - + for j in 1 ..< varNode.len: + if varNode[j].kind == nnkIdentDefs: + identDefsStack.add(varNode[j]) + else: # nnkRecList + for m in 0 ..< varNode[j].len: + identDefsStack.add(varNode[j][m]) else: for i in 0 .. identDefs.len - 3: let varNode = identDefs[i] diff --git a/lib/pure/json.nim b/lib/pure/json.nim index 23b23b4a4649f..f3a297cd4f09d 100644 --- a/lib/pure/json.nim +++ b/lib/pure/json.nim @@ -116,6 +116,37 @@ ## Creating JSON ## ============= ## +## Serialization of Nim types to ``JsonNode`` is handled via ``%`` procedures +## for each type. +## +## For instance serialization of basic types results in a ``JsonNode`` of the +## corresponding ``JsonNodeKind``. +## +## .. code-block:: nim +## import json +## let numJson = %10 +## doAssert numJson.kind == JInt +## +## For objects it may be desirable to avoid serialization of one or more object +## fields. This can be achieved by using the ``{.doNotSerialize.}`` pragma. +## On the other hand sometimes it is desired to (de)serialize fields, but under a +## different name. For this use the ``{.jsonName: "myName".}`` pragma. +## +## .. code-block:: nim +## import json +## +## type +## User = object +## name: string +## age {.jsonName: "userAge".}: int +## uid {.doNotSerialize.}: int +## +## let user = User(name: "Siri", age: 7, uid: 1234) +## let uJson = % user +## doAssert not uJson.hasKey("uid") +## doAssert uJson["userAge"].num == 7 +## echo uJson +## ## This module can also be used to comfortably create JSON using the ``%*`` ## operator: ## @@ -363,10 +394,38 @@ proc `[]=`*(obj: JsonNode, key: string, val: JsonNode) {.inline.} = assert(obj.kind == JObject) obj.fields[key] = val +template doNotSerialize*() {.pragma.} + ## The `{.doNotSerialize.}` pragma can be attached to a field of an object to avoid + ## serialization of said field to a `JsonNode` via `%`. See the `%` proc for + ## objects below for an example. + +template jsonName*(name: string) {.pragma.} + ## The `{.jsonName: "myName".}` pragma can be attached to a field of an object + ## to change the (de)serialization name of that field. See the `%` proc for + ## objects or the `to` macro below for examples. + proc `%`*[T: object](o: T): JsonNode = ## Construct JsonNode from tuples and objects. + ## + ## An object field annotated with the `{.doNotSerialize.}` pragma will not appear + ## in the serialized JsonNode. A field annotated with the `{.jsonName: "myName".}` + ## pragma however, will be serialized under that name instead. + runnableExamples: + type + User = object + name: string + age {.jsonName: "userAge".} : int + uid {.doNotSerialize.}: int + let user = User(name: "Siri", age: 7, uid: 1234) + let uJson = % user + doAssert not uJson.hasKey("uid") + doAssert uJson["userAge"].num == 7 result = newJObject() - for k, v in o.fieldPairs: result[k] = %v + for k, v in o.fieldPairs: + when hasCustomPragma(v, jsonName): + result[getCustomPragmaVal(v, jsonName)] = %v + elif not hasCustomPragma(v, doNotSerialize): + result[k] = %v proc `%`*(o: ref object): JsonNode = ## Generic constructor for JSON data. Creates a new `JObject JsonNode` @@ -1465,7 +1524,6 @@ proc postProcessExprColonExpr(exprColonExpr, resIdent: NimNode): NimNode = quote do: `resIdent`.`fieldName` = `fieldValue` - proc postProcess(node: NimNode): NimNode = ## The ``createConstructor`` proc creates a ObjConstr node which contains ## if statements for fields that may not be assignable (due to an object @@ -1502,9 +1560,80 @@ proc postProcess(node: NimNode): NimNode = # Return the `res` variable. result.add(resIdent) +proc extractJsonName(node: NimNode): tuple[field: string, fieldAs: string] = + ## extracts the field name and its replacement for the `jsonName` pragma + expectKind(node, nnkPragmaExpr) + doAssert node.len == 2 + doAssert node[1][0].kind == nnkExprColonExpr + doAssert node[1][0][0].strVal == "jsonName" + case node[0].kind + of nnkIdent, nnkSym: + result = (field: node[0].strVal, fieldAs: node[1][0][1].strVal) + of nnkPostfix: + # for exported fields + result = (field: node[0][1].strVal, fieldAs: node[1][0][1].strVal) + else: + assert false, "unsupported node kind " & $node.kind + +proc findJsonNamePragma(typeNode: NimNode): + seq[tuple[field: string, fieldAs: string]] = + ## recurses the whole `typeNode` and searches for appearences of the + ## `jsonName` pragma to rename fields for (de)serialization. + for ch in typeNode: + case ch.kind + of nnkPragmaExpr: + result.add extractJsonName(ch) + of nnkSym: + if typeNode.kind != nnkTypeDef: + # if it was an nnkTypeDef, we'd recurse on ourselves + let impl = getTypeImpl(ch) + if impl.kind == nnkObjectTy: + result.add findJsonNamePragma(ch.getImpl) + else: + result.add findJsonNamePragma(ch) + +proc findReplace(node: NimNode, + replace: seq[tuple[field: string, fieldAs: string]]): NimNode = + ## performs replacement according to `jsonName` values of `nnkBracketExpr` nodes + ## appearing in the `to` macro's generated code. + expectKind(node, nnkBracketExpr) + if node.len == 2: + result = nnkBracketExpr.newTree() + if node[0].kind == nnkBracketExpr: + # if child itself bracketExpr, recurse + result.add findReplace(node[0], replace) + else: + result.add node[0] + for i, el in replace: + # check if child 1 is literal string and matches an element of `replace` + if node[1].kind == nnkStrLit and + node[1].strVal == el[0]: + result.add newLit(el[1]) + return result + # not found, keep as is + result.add node[1] + else: + # for literal arrays, e.g. `array[2, float]` don't touch + result = node + +proc replaceNames(node: NimNode, + replace: seq[tuple[field: string, fieldAs: string]]): NimNode = + ## replaces all appearences of `field` by `fieldAs` in `node` if + ## `field` appears in `nnkBracketExpr` as `nnkStrLit` + result = node.kind.newTree() + for ch in node: + case ch.kind + of nnkBracketExpr: + result.add findReplace(ch, replace) + of nnkIdent, nnkSym, nnkLiterals, nnkEmpty: + result.add ch + else: + result.add replaceNames(ch, replace) macro to*(node: JsonNode, T: typedesc): untyped = ## `Unmarshals`:idx: the specified node into the object type specified. + ## If the JSON contains field names differing from the Nim object field + ## names, use the ``{.jsonName: "myName".}`` pragma as below. ## ## Known limitations: ## @@ -1519,7 +1648,7 @@ macro to*(node: JsonNode, T: typedesc): untyped = ## { ## "person": { ## "name": "Nimmer", - ## "age": 21 + ## "personAge": 21 ## }, ## "list": [1, 2, 3, 4] ## } @@ -1528,7 +1657,7 @@ macro to*(node: JsonNode, T: typedesc): untyped = ## type ## Person = object ## name: string - ## age: int + ## age {.jsonName: "personAge".}: int ## ## Data = object ## person: Person @@ -1550,9 +1679,13 @@ macro to*(node: JsonNode, T: typedesc): untyped = result.add quote do: let `temp` = `node` - let constructor = createConstructor(typeNode[1], temp) - result.add(postProcessValue(constructor)) + var constructor = createConstructor(typeNode[1], temp) + if typeNode[1].kind == nnkSym: + # if more complex type, check if implementation has pragmas attached + let pragmaReplace = findJsonNamePragma(getTypeImpl(T)[1].getImpl) + constructor = constructor.replaceNames(pragmaReplace) + result.add(postProcessValue(constructor)) # echo(treeRepr(result)) # echo(toStrLit(result)) diff --git a/tests/macros/tcprag.nim b/tests/macros/tcprag.nim index 71618883f593c..899cf08c20a9f 100644 --- a/tests/macros/tcprag.nim +++ b/tests/macros/tcprag.nim @@ -2,6 +2,9 @@ discard """ output: '''true true true +true +true +true ''' """ @@ -30,3 +33,56 @@ macro m2(T: typedesc): untyped = result = quote do: `T`.hasCustomPragma(table) echo m2(User) + + + +block: + template noserialize() {.pragma.} + + type + Point[T] = object + x, y: T + + ReplayEventKind = enum + FoodAppeared, FoodEaten, DirectionChanged + + # ref #11415 + # this works, since `foodPos` is inside of a variant kind with a + # single `of` element + block: + type + ReplayEvent = object + time: float + pos {.noserialize.}: Point[float] # works before fix + case kind: ReplayEventKind + of FoodEaten: + foodPos {.noserialize.}: Point[float] # also works, only in one branch + of DirectionChanged, FoodAppeared: + playerPos: float + + let ev = ReplayEvent( + pos: Point[float](x: 5.0, y: 1.0), + time: 1.2345, + kind: FoodEaten, + foodPos: Point[float](x: 5.0, y: 1.0) + ) + echo ev.pos.hasCustomPragma(noserialize) + echo ev.foodPos.hasCustomPragma(noserialize) + + # ref 11415 + # this did not work, since `foodPos` is inside of a variant kind with a + # two `of` elements + block: + type + ReplayEvent = object + case kind: ReplayEventKind + of FoodEaten, FoodAppeared: + foodPos {.noserialize.}: Point[float] # did not work, because in two branches + of DirectionChanged: + playerPos: float + + let ev = ReplayEvent( + kind: FoodEaten, + foodPos: Point[float](x: 5.0, y: 1.0) + ) + echo ev.foodPos.hasCustomPragma(noserialize) diff --git a/tests/stdlib/tjsonmacro.nim b/tests/stdlib/tjsonmacro.nim index 85530065d64f4..34714473f25fb 100644 --- a/tests/stdlib/tjsonmacro.nim +++ b/tests/stdlib/tjsonmacro.nim @@ -553,6 +553,113 @@ block: let cFromJson = (% c).to(RecoEvent[Pix]) doAssert c == cFromJson + # avoid serialization of specific field + block: + type + User = object + name: string + age: int + uid {.doNotSerialize.}: int + + let user = User(name: "Siri", age: 7, uid: 1234) + let uJson = % user + doAssert uJson["name"].getStr == "Siri" + doAssert uJson["age"].getInt == 7 + doAssert not uJson.hasKey("uid") + + # avoid serialization of specific field in variant object + # was broken pre #11415 + block: + type + ReplayEvent2 = object + time: float + anotherPos {.doNotSerialize.}: Point[float] + case kind: ReplayEventKind + of FoodEaten, FoodAppeared: + foodPos {.doNotSerialize.}: Point[float] + of DirectionChanged: + playerPos: float + + let ev = ReplayEvent2( + anotherPos: Point[float](x: 5.0, y: 1.0), + time: 1.2345, + kind: FoodEaten, + foodPos: Point[float](x: 5.0, y: 1.0) + ) + + let node = % ev + doAssert node["time"].getFloat == 1.2345 + doAssert node["kind"].getStr == "FoodEaten" + doAssert not hasKey(node, "anotherPos") + doAssert not hasKey(node, "foodPos") + + # deserialize into specific fieldname + block: + let exp = parseJson(""" + { + "_nodes": { + "total": 1, + "successful": 1, + "failed": 0 + } + }""") + type + Node = object + total: int + successful: int + failed: int + Response {.jsonName: "response".} = object + nodes {.jsonName: "_nodes".}: Node + let response = to(exp, Response) + doAssert response.nodes.total == 1 + doAssert response.nodes.successful == 1 + doAssert response.nodes.failed == 0 + # check serialization + let respJson = % response + doAssert respJson == exp + + # serialize into specific fieldname, nested with variant + block: + let exp = parseJson(""" + { + "response": { + "_foobar": 1 + }, + "myKind": "mkOne", + "one": 2 + }""") + type + KindEnum = enum + mkOne, mkTwo + Resp = object + myField {.jsonName: "_foobar".}: int + SerializeMe = object + someObj {.jsonName: "response".}: Resp + case kind {.jsonName: "myKind".}: KindEnum + of mkOne: + one: int + of mkTwo: + two: string + let obj = SerializeMe(someObj: Resp(myField: 1), kind: mkOne, one: 2) + doAssert (% to(exp, SerializeMe)) == exp + let objJson = % obj + doAssert objJson["response"]["_foobar"].num == 1 + doAssert objJson["myKind"].str == "mkOne" + doAssert objJson["one"].num == 2 + + # serialize a field with nnkPostfix node + block: + let exp = parseJson(""" + { + "customName": 3 + }""") + type + AndAnotherObject = object + afield* {.jsonName: "customName".}: int + let obj = AndAnotherObject(afield: 3) + doAssert (% obj) == exp + doAssert obj == to(exp, AndAnotherObject) + # TODO: when the issue with the limeted vm registers is solved, the # exact same test as above should be evaluated at compile time as # well, to ensure that the vm functionality won't diverge from the