mirror of https://github.com/status-im/NimYAML.git
Check for custom object errors when loading
* Ensure no duplicate fields * Ensure no missing fields * Ensure no unknown fields * Implemented for both tuples and objects, including variant objects
This commit is contained in:
parent
6bb110b185
commit
d987b607e5
|
@ -302,6 +302,24 @@ suite "Serialization":
|
||||||
var output = dump(input, tsNone)
|
var output = dump(input, tsNone)
|
||||||
assertStringEqual "%YAML 1.2\n--- \nstr: value\ni: 42\nb: y", output
|
assertStringEqual "%YAML 1.2\n--- \nstr: value\ni: 42\nb: y", output
|
||||||
|
|
||||||
|
test "Load Tuple - unknown field":
|
||||||
|
let input = "str: value\nfoo: bar\ni: 42\nb: true"
|
||||||
|
var result: MyTuple
|
||||||
|
expect(YamlConstructionError):
|
||||||
|
load(input, result)
|
||||||
|
|
||||||
|
test "Load Tuple - missing field":
|
||||||
|
let input = "str: value\nb: true"
|
||||||
|
var result: MyTuple
|
||||||
|
expect(YamlConstructionError):
|
||||||
|
load(input, result)
|
||||||
|
|
||||||
|
test "Load Tuple - duplicate field":
|
||||||
|
let input = "str: value\ni: 42\nb: true\nb: true"
|
||||||
|
var result: MyTuple
|
||||||
|
expect(YamlConstructionError):
|
||||||
|
load(input, result)
|
||||||
|
|
||||||
test "Load Multiple Documents":
|
test "Load Multiple Documents":
|
||||||
let input = newStringStream("1\n---\n2")
|
let input = newStringStream("1\n---\n2")
|
||||||
var result: seq[int]
|
var result: seq[int]
|
||||||
|
@ -331,6 +349,24 @@ suite "Serialization":
|
||||||
assertStringEqual(
|
assertStringEqual(
|
||||||
"%YAML 1.2\n--- \nfirstnamechar: P\nsurname: Pan\nage: 12", output)
|
"%YAML 1.2\n--- \nfirstnamechar: P\nsurname: Pan\nage: 12", output)
|
||||||
|
|
||||||
|
test "Load custom object - unknown field":
|
||||||
|
let input = "firstnamechar: P\nsurname: Pan\nage: 12\noccupation: free"
|
||||||
|
var result: Person
|
||||||
|
expect(YamlConstructionError):
|
||||||
|
load(input, result)
|
||||||
|
|
||||||
|
test "Load custom object - missing field":
|
||||||
|
let input = "surname: Pan\nage: 12"
|
||||||
|
var result: Person
|
||||||
|
expect(YamlConstructionError):
|
||||||
|
load(input, result)
|
||||||
|
|
||||||
|
test "Load custom object - duplicate field":
|
||||||
|
let input = "firstnamechar: P\nsurname: Pan\nage: 12\nsurname: Pan"
|
||||||
|
var result: Person
|
||||||
|
expect(YamlConstructionError):
|
||||||
|
load(input, result)
|
||||||
|
|
||||||
test "Load sequence with explicit tags":
|
test "Load sequence with explicit tags":
|
||||||
let input = newStringStream("--- !nim:system:seq(" &
|
let input = newStringStream("--- !nim:system:seq(" &
|
||||||
"tag:yaml.org,2002:str)\n- !!str one\n- !!str two")
|
"tag:yaml.org,2002:str)\n- !!str one\n- !!str two")
|
||||||
|
@ -396,6 +432,12 @@ suite "Serialization":
|
||||||
-
|
-
|
||||||
barkometer: 13""", output
|
barkometer: 13""", output
|
||||||
|
|
||||||
|
test "Load custom variant object - missing field":
|
||||||
|
let input = "{name: Bastet, kind: akCat}"
|
||||||
|
var result: Animal
|
||||||
|
expect(YamlConstructionError):
|
||||||
|
load(input, result)
|
||||||
|
|
||||||
test "Dump cyclic data structure":
|
test "Dump cyclic data structure":
|
||||||
var
|
var
|
||||||
a = newNode("a")
|
a = newNode("a")
|
||||||
|
|
|
@ -468,10 +468,84 @@ proc yamlTag*(T: typedesc[tuple]):
|
||||||
try: serializationTagLibrary.tags[uri]
|
try: serializationTagLibrary.tags[uri]
|
||||||
except KeyError: serializationTagLibrary.registerUri(uri)
|
except KeyError: serializationTagLibrary.registerUri(uri)
|
||||||
|
|
||||||
macro constructFieldValue(t: typedesc, stream: untyped, context: untyped,
|
proc fieldAnalyzer(t: typedesc): tuple[sections, maxlen: int] {.compileTime.} =
|
||||||
name: untyped, o: untyped): typed =
|
result = (1, 0)
|
||||||
let tDesc = getType(getType(t)[1])
|
let tDesc = getType(getType(t)[1])
|
||||||
|
echo "fieldAnalyzer: " & tDesc.treeRepr
|
||||||
|
if tDesc.kind == nnkBracketExpr:
|
||||||
|
# tuple
|
||||||
|
result.maxlen = tDesc.len - 1
|
||||||
|
else:
|
||||||
|
# object
|
||||||
|
var outerLen = 0
|
||||||
|
for child in tDesc[2].children:
|
||||||
|
inc(outerLen)
|
||||||
|
if child.kind == nnkRecCase:
|
||||||
|
inc(result.sections)
|
||||||
|
var innerLen = 0
|
||||||
|
for bIndex in 1..<len(child):
|
||||||
|
inc(innerLen, child[bIndex][1].len)
|
||||||
|
result.maxlen = max(result.maxlen, innerLen)
|
||||||
|
result.maxlen = max(result.maxlen, outerLen)
|
||||||
|
|
||||||
|
macro matchMatrix(t: typedesc): untyped =
|
||||||
|
result = newNimNode(nnkBracket)
|
||||||
|
let details = fieldAnalyzer(t)
|
||||||
|
echo "details of " & typetraits.name(t) & ": " & $details
|
||||||
|
for section in 0..<details.sections:
|
||||||
|
for item in 0..<details.maxlen:
|
||||||
|
result.add(newLit(false))
|
||||||
|
|
||||||
|
proc checkDuplicate(t: typedesc, name: string, i: int, matched: NimNode):
|
||||||
|
NimNode {.compileTime.} =
|
||||||
|
result = newIfStmt((newNimNode(nnkBracketExpr).add(matched, newLit(i)),
|
||||||
|
newNimNode(nnkRaiseStmt).add(newCall("newException", newIdentNode(
|
||||||
|
"YamlConstructionError"), newLit("While constructing " &
|
||||||
|
typetraits.name(t) & ": Duplicate field: " & escape(name))))))
|
||||||
|
|
||||||
|
proc checkMissing(t: typedesc, name: string, i: int, matched: NimNode):
|
||||||
|
NimNode {.compileTime.} =
|
||||||
|
result = newIfStmt((newCall("not", newNimNode(nnkBracketExpr).add(matched,
|
||||||
|
newLit(i))), newNimNode(nnkRaiseStmt).add(newCall("newException",
|
||||||
|
newIdentNode("YamlConstructionError"), newLit("While constructing " &
|
||||||
|
typetraits.name(t) & ": Missing field: " & escape(name))))))
|
||||||
|
|
||||||
|
proc markAsFound(i: int, matched: NimNode): NimNode {.compileTime.} =
|
||||||
|
newAssignment(newNimNode(nnkBracketExpr).add(matched, newLit(i)),
|
||||||
|
newLit(true))
|
||||||
|
|
||||||
|
macro ensureAllFieldsPresent(t: typedesc, o: typed, matched: typed): typed =
|
||||||
|
result = newStmtList()
|
||||||
|
let
|
||||||
|
tDesc = getType(getType(t)[1])
|
||||||
|
details = fieldAnalyzer(t)
|
||||||
|
var outerField = 0
|
||||||
|
var section = 0
|
||||||
|
for child in tDesc[2].children:
|
||||||
|
if child.kind == nnkRecCase:
|
||||||
|
result.add(checkMissing(t, $child[0], outerField, matched))
|
||||||
|
inc(section)
|
||||||
|
var innerField = 0
|
||||||
|
for bIndex in 1 .. len(child) - 1:
|
||||||
|
let discChecks = newStmtList()
|
||||||
|
for item in child[bIndex][1].children:
|
||||||
|
discChecks.add(checkMissing(t, $item, section * details.maxlen +
|
||||||
|
innerField, matched))
|
||||||
|
inc(innerField)
|
||||||
|
result.add(newIfStmt((infix(newDotExpr(o, newIdentNode($child[0])),
|
||||||
|
"==", child[bIndex][0]), discChecks)))
|
||||||
|
else:
|
||||||
|
result.add(checkMissing(t, $child, outerField, matched))
|
||||||
|
inc(outerField)
|
||||||
|
|
||||||
|
macro constructFieldValue(t: typedesc, stream: untyped, context: untyped,
|
||||||
|
name: untyped, o: untyped, matched: untyped): typed =
|
||||||
|
let
|
||||||
|
tDesc = getType(getType(t)[1])
|
||||||
|
details = fieldAnalyzer(t)
|
||||||
result = newNimNode(nnkCaseStmt).add(name)
|
result = newNimNode(nnkCaseStmt).add(name)
|
||||||
|
var fieldIndex = 0
|
||||||
|
var sectionIndex = 0
|
||||||
for child in tDesc[2].children:
|
for child in tDesc[2].children:
|
||||||
if child.kind == nnkRecCase:
|
if child.kind == nnkRecCase:
|
||||||
let
|
let
|
||||||
|
@ -479,12 +553,16 @@ macro constructFieldValue(t: typedesc, stream: untyped, context: untyped,
|
||||||
discType = newCall("type", discriminant)
|
discType = newCall("type", discriminant)
|
||||||
var disOb = newNimNode(nnkOfBranch).add(newStrLitNode($child[0]))
|
var disOb = newNimNode(nnkOfBranch).add(newStrLitNode($child[0]))
|
||||||
disOb.add(newStmtList(
|
disOb.add(newStmtList(
|
||||||
|
checkDuplicate(t, $child[0], fieldIndex, matched),
|
||||||
newNimNode(nnkVarSection).add(
|
newNimNode(nnkVarSection).add(
|
||||||
newNimNode(nnkIdentDefs).add(
|
newNimNode(nnkIdentDefs).add(
|
||||||
newIdentNode("value"), discType, newEmptyNode())),
|
newIdentNode("value"), discType, newEmptyNode())),
|
||||||
newCall("constructChild", stream, context, newIdentNode("value")),
|
newCall("constructChild", stream, context, newIdentNode("value")),
|
||||||
newAssignment(discriminant, newIdentNode("value"))))
|
newAssignment(discriminant, newIdentNode("value")),
|
||||||
|
markAsFound(fieldIndex, matched)))
|
||||||
result.add(disOb)
|
result.add(disOb)
|
||||||
|
inc(sectionIndex)
|
||||||
|
var innerFieldIndex = 0
|
||||||
for bIndex in 1 .. len(child) - 1:
|
for bIndex in 1 .. len(child) - 1:
|
||||||
let discTest = infix(discriminant, "==", child[bIndex][0])
|
let discTest = infix(discriminant, "==", child[bIndex][0])
|
||||||
for item in child[bIndex][1].children:
|
for item in child[bIndex][1].children:
|
||||||
|
@ -497,17 +575,27 @@ macro constructFieldValue(t: typedesc, stream: untyped, context: untyped,
|
||||||
newCall("newException", newIdentNode("YamlConstructionError"),
|
newCall("newException", newIdentNode("YamlConstructionError"),
|
||||||
infix(newStrLitNode("Field " & $item & " not allowed for " &
|
infix(newStrLitNode("Field " & $item & " not allowed for " &
|
||||||
$child[0] & " == "), "&", prefix(discriminant, "$"))))))
|
$child[0] & " == "), "&", prefix(discriminant, "$"))))))
|
||||||
ob.add(newStmtList(ifStmt))
|
ob.add(newStmtList(checkDuplicate(t, $item,
|
||||||
|
sectionIndex * details.maxlen + innerFieldIndex, matched), ifStmt,
|
||||||
|
markAsFound(sectionIndex * details.maxlen + innerFieldIndex,
|
||||||
|
matched)))
|
||||||
result.add(ob)
|
result.add(ob)
|
||||||
|
inc(innerFieldIndex)
|
||||||
else:
|
else:
|
||||||
yAssert child.kind == nnkSym
|
yAssert child.kind == nnkSym
|
||||||
var ob = newNimNode(nnkOfBranch).add(newStrLitNode($child))
|
var ob = newNimNode(nnkOfBranch).add(newStrLitNode($child))
|
||||||
let field = newDotExpr(o, newIdentNode($child))
|
let field = newDotExpr(o, newIdentNode($child))
|
||||||
ob.add(newStmtList(newCall("constructChild", stream, context, field)))
|
ob.add(newStmtList(
|
||||||
|
checkDuplicate(t, $child, fieldIndex, matched),
|
||||||
|
newCall("constructChild", stream, context, field),
|
||||||
|
markAsFound(fieldIndex, matched)))
|
||||||
result.add(ob)
|
result.add(ob)
|
||||||
# TODO: is this correct?
|
inc(fieldIndex)
|
||||||
result.add(newNimNode(nnkElse).add(newNimNode(nnkDiscardStmt).add(
|
result.add(newNimNode(nnkElse).add(newNimNode(nnkRaiseStmt).add(
|
||||||
newEmptyNode())))
|
newCall("newException", newIdentNode("YamlConstructionError"),
|
||||||
|
infix(newLit("While constructing " & typetraits.name(t) &
|
||||||
|
": Unknown field: "), "&", name)))))
|
||||||
|
echo result.repr
|
||||||
|
|
||||||
proc isVariantObject(t: typedesc): bool {.compileTime.} =
|
proc isVariantObject(t: typedesc): bool {.compileTime.} =
|
||||||
let tDesc = getType(t)
|
let tDesc = getType(t)
|
||||||
|
@ -520,7 +608,9 @@ proc constructObject*[O: object|tuple](
|
||||||
s: var YamlStream, c: ConstructionContext, result: var O)
|
s: var YamlStream, c: ConstructionContext, result: var O)
|
||||||
{.raises: [YamlConstructionError, YamlStreamError].} =
|
{.raises: [YamlConstructionError, YamlStreamError].} =
|
||||||
## constructs a Nim object or tuple from a YAML mapping
|
## constructs a Nim object or tuple from a YAML mapping
|
||||||
let e = s.next()
|
static: echo "constructOb[" & typetraits.name(O) & "]"
|
||||||
|
var matched = matchMatrix(O)
|
||||||
|
var e = s.next()
|
||||||
const
|
const
|
||||||
startKind = when isVariantObject(O): yamlStartSeq else: yamlStartMap
|
startKind = when isVariantObject(O): yamlStartSeq else: yamlStartMap
|
||||||
endKind = when isVariantObject(O): yamlEndSeq else: yamlEndMap
|
endKind = when isVariantObject(O): yamlEndSeq else: yamlEndMap
|
||||||
|
@ -531,7 +621,7 @@ proc constructObject*[O: object|tuple](
|
||||||
while s.peek.kind != endKind:
|
while s.peek.kind != endKind:
|
||||||
# todo: check for duplicates in input and raise appropriate exception
|
# todo: check for duplicates in input and raise appropriate exception
|
||||||
# also todo: check for missing items and raise appropriate exception
|
# also todo: check for missing items and raise appropriate exception
|
||||||
var e = s.next()
|
e = s.next()
|
||||||
when isVariantObject(O):
|
when isVariantObject(O):
|
||||||
if e.kind != yamlStartMap:
|
if e.kind != yamlStartMap:
|
||||||
raise newException(YamlConstructionError,
|
raise newException(YamlConstructionError,
|
||||||
|
@ -542,18 +632,37 @@ proc constructObject*[O: object|tuple](
|
||||||
"Expected field name, got " & $e.kind)
|
"Expected field name, got " & $e.kind)
|
||||||
let name = e.scalarContent
|
let name = e.scalarContent
|
||||||
when result is tuple:
|
when result is tuple:
|
||||||
|
var i = 0
|
||||||
|
var found = false
|
||||||
for fname, value in fieldPairs(result):
|
for fname, value in fieldPairs(result):
|
||||||
if fname == name:
|
if fname == name:
|
||||||
|
if matched[i]:
|
||||||
|
raise newException(YamlConstructionError, "While constructing " &
|
||||||
|
typetraits.name(O) & ": Duplicate field: " & escape(name))
|
||||||
constructChild(s, c, value)
|
constructChild(s, c, value)
|
||||||
|
matched[i] = true
|
||||||
|
found = true
|
||||||
break
|
break
|
||||||
|
inc(i)
|
||||||
|
if not found:
|
||||||
|
raise newException(YamlConstructionError, "While constructing " &
|
||||||
|
typetraits.name(O) & ": Unknown field: " & escape(name))
|
||||||
else:
|
else:
|
||||||
constructFieldValue(O, s, c, name, result)
|
constructFieldValue(O, s, c, name, result, matched)
|
||||||
when isVariantObject(O):
|
when isVariantObject(O):
|
||||||
e = s.next()
|
e = s.next()
|
||||||
if e.kind != yamlEndMap:
|
if e.kind != yamlEndMap:
|
||||||
raise newException(YamlConstructionError,
|
raise newException(YamlConstructionError,
|
||||||
"Expected end of single-pair map, got " & $e.kind)
|
"Expected end of single-pair map, got " & $e.kind)
|
||||||
discard s.next()
|
discard s.next()
|
||||||
|
when result is tuple:
|
||||||
|
var i = 0
|
||||||
|
for fname, value in fieldPairs(result):
|
||||||
|
if not matched[i]:
|
||||||
|
raise newException(YamlConstructionError, "While constructing " &
|
||||||
|
typetraits.name(O) & ": Field missing: " & escape(fname))
|
||||||
|
inc(i)
|
||||||
|
else: ensureAllFieldsPresent(O, result, matched)
|
||||||
|
|
||||||
proc representObject*[O: object|tuple](value: O, ts: TagStyle,
|
proc representObject*[O: object|tuple](value: O, ts: TagStyle,
|
||||||
c: SerializationContext, tag: TagId) {.raises: [YamlStreamError].} =
|
c: SerializationContext, tag: TagId) {.raises: [YamlStreamError].} =
|
||||||
|
|
Loading…
Reference in New Issue