Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

doNotSerialize, jsonName pragmas for JSON serialization closes #8104, #10718, also fixes #11415 #11416

Closed
wants to merge 9 commits into from
19 changes: 12 additions & 7 deletions lib/core/macros.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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]
Expand Down
145 changes: 139 additions & 6 deletions lib/pure/json.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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:
##
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
##
Expand All @@ -1519,7 +1648,7 @@ macro to*(node: JsonNode, T: typedesc): untyped =
## {
## "person": {
## "name": "Nimmer",
## "age": 21
## "personAge": 21
## },
## "list": [1, 2, 3, 4]
## }
Expand All @@ -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
Expand All @@ -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))

Expand Down
56 changes: 56 additions & 0 deletions tests/macros/tcprag.nim
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ discard """
output: '''true
true
true
true
true
true
'''
"""

Expand Down Expand Up @@ -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)
Loading