Improve error messages and add tests for the JSON macro.

This commit is contained in:
Dominik Picheta 2017-04-08 22:06:57 +02:00
commit 658467a31f
2 changed files with 100 additions and 35 deletions

View file

@ -124,6 +124,9 @@ type
state: seq[ParserState] state: seq[ParserState]
filename: string filename: string
JsonKindError* = object of ValueError ## raised by the ``to`` macro if the
## JSON kind is incorrect.
{.deprecated: [TJsonEventKind: JsonEventKind, TJsonError: JsonError, {.deprecated: [TJsonEventKind: JsonEventKind, TJsonError: JsonError,
TJsonParser: JsonParser, TTokKind: TokKind].} TJsonParser: JsonParser, TTokKind: TokKind].}
@ -1289,14 +1292,24 @@ proc createJsonIndexer(jsonNode: NimNode,
indexNode indexNode
) )
proc getEnum(node: JsonNode, T: typedesc): T = template verifyJsonKind(node: JsonNode, kinds: set[JsonNodeKind],
# TODO: Exceptions. ast: string) =
if node.kind notin kinds:
let msg = "Incorrect JSON kind. Wanted '$1' in '$2' but got '$3'." % [
$kinds,
ast,
$node.kind
]
raise newException(JsonKindError, msg)
proc getEnum(node: JsonNode, ast: string, T: typedesc): T =
verifyJsonKind(node, {JString}, ast)
return parseEnum[T](node.getStr()) return parseEnum[T](node.getStr())
proc toIdentNode(typeNode: NimNode): NimNode = proc toIdentNode(typeNode: NimNode): NimNode =
## Converts a Sym type node (returned by getType et al.) into an ## Converts a Sym type node (returned by getType et al.) into an
## Ident node. Placing Sym type nodes is unsound (according to @Araq) ## Ident node. Placing Sym type nodes inside the resulting code AST is
## so this is necessary. ## unsound (according to @Araq) so this is necessary.
case typeNode.kind case typeNode.kind
of nnkSym: of nnkSym:
return newIdentNode($typeNode) return newIdentNode($typeNode)
@ -1317,7 +1330,8 @@ proc createIfStmtForOf(ofBranch, jsonNode, kindType,
# -> getEnum(`jsonNode`, `kindType`) # -> getEnum(`jsonNode`, `kindType`)
let getEnumSym = bindSym("getEnum") let getEnumSym = bindSym("getEnum")
let getEnumCall = newCall(getEnumSym, jsonNode, kindType) let astStrLit = toStrLit(jsonNode)
let getEnumCall = newCall(getEnumSym, jsonNode, astStrLit, kindType)
var cond = newEmptyNode() var cond = newEmptyNode()
for ofCond in ofBranch: for ofCond in ofBranch:
@ -1398,7 +1412,8 @@ proc processObjField(field, jsonNode: NimNode): seq[NimNode] =
# Add the "case" field's value. # Add the "case" field's value.
let kindType = toIdentNode(getTypeInst(field[0])) let kindType = toIdentNode(getTypeInst(field[0]))
let getEnumSym = bindSym("getEnum") let getEnumSym = bindSym("getEnum")
let getEnumCall = newCall(getEnumSym, kindJsonNode, kindType) let astStrLit = toStrLit(kindJsonNode)
let getEnumCall = newCall(getEnumSym, kindJsonNode, astStrLit, kindType)
exprColonExpr.add(getEnumCall) exprColonExpr.add(getEnumCall)
# Iterate through each `of` branch. # Iterate through each `of` branch.
@ -1439,25 +1454,25 @@ proc processType(typeName: NimNode, obj: NimNode,
of "float": of "float":
result = quote do: result = quote do:
( (
assert `jsonNode`.kind == JFloat; verifyJsonKind(`jsonNode`, {JFloat}, astToStr(`jsonNode`));
`jsonNode`.fnum `jsonNode`.fnum
) )
of "string": of "string":
result = quote do: result = quote do:
( (
assert `jsonNode`.kind in {JString, JNull}; verifyJsonKind(`jsonNode`, {JString, JNull}, astToStr(`jsonNode`));
if `jsonNode`.kind == JNull: nil else: `jsonNode`.str if `jsonNode`.kind == JNull: nil else: `jsonNode`.str
) )
of "int": of "int":
result = quote do: result = quote do:
( (
assert `jsonNode`.kind == JInt; verifyJsonKind(`jsonNode`, {JInt}, astToStr(`jsonNode`));
`jsonNode`.num.int `jsonNode`.num.int
) )
of "bool": of "bool":
result = quote do: result = quote do:
( (
assert `jsonNode`.kind == JBool; verifyJsonKind(`jsonNode`, {JBool}, astToStr(`jsonNode`));
`jsonNode`.bval `jsonNode`.bval
) )
else: else:

View file

@ -2,33 +2,33 @@ discard """
file: "tjsonmacro.nim" file: "tjsonmacro.nim"
output: "" output: ""
""" """
import json, macros, strutils import json, strutils
type
Point[T] = object
x, y: T
ReplayEventKind* = enum
FoodAppeared, FoodEaten, DirectionChanged
ReplayEvent* = object
time*: float
case kind*: ReplayEventKind
of FoodAppeared, FoodEaten:
foodPos*: Point[float]
of DirectionChanged:
playerPos*: float
Replay* = ref object
events*: seq[ReplayEvent]
test: int
test2: string
test3: bool
testNil: string
when isMainModule: when isMainModule:
# Tests inspired by own use case (with some additional tests). # Tests inspired by own use case (with some additional tests).
# This should succeed. # This should succeed.
type
Point[T] = object
x, y: T
ReplayEventKind* = enum
FoodAppeared, FoodEaten, DirectionChanged
ReplayEvent* = object
time*: float
case kind*: ReplayEventKind
of FoodAppeared, FoodEaten:
foodPos*: Point[float]
of DirectionChanged:
playerPos*: float
Replay* = ref object
events*: seq[ReplayEvent]
test: int
test2: string
test3: bool
testNil: string
var x = Replay( var x = Replay(
events: @[ events: @[
ReplayEvent( ReplayEvent(
@ -53,7 +53,57 @@ when isMainModule:
doAssert y.test == 18827361 doAssert y.test == 18827361
doAssert y.test2 == "hello world" doAssert y.test2 == "hello world"
doAssert y.test3 doAssert y.test3
doAssert y.testNil == nil doAssert y.testNil.isNil
# TODO: Test for custom object variants (without an enum).
# TODO: Test for object variant with an else branch.
# Tests that verify the error messages for invalid data. # Tests that verify the error messages for invalid data.
# TODO: block:
type
Person = object
name: string
age: int
var node = %{
"name": %"Dominik"
}
try:
discard to(node, Person)
doAssert false
except KeyError as exc:
doAssert("age" in exc.msg)
except:
doAssert false
node["age"] = %false
try:
discard to(node, Person)
doAssert false
except JsonKindError as exc:
doAssert("age" in exc.msg)
except:
doAssert false
type
PersonAge = enum
Fifteen, Sixteen
PersonCase = object
name: string
case age: PersonAge
of Fifteen:
discard
of Sixteen:
id: string
try:
discard to(node, PersonCase)
doAssert false
except JsonKindError as exc:
doAssert("age" in exc.msg)
except:
doAssert false