Fixed and properly implemented variant objects

This commit is contained in:
Felix Krause 2016-06-08 19:15:50 +02:00
parent 152a4f3bd3
commit 7cad7b5478
3 changed files with 127 additions and 22 deletions

View File

@ -37,8 +37,8 @@ NimYAML supports a growing number of types of Nim's ``system`` module and
standard library, and it also supports user-defined object, tuple and enum types standard library, and it also supports user-defined object, tuple and enum types
out of the box. out of the box.
**Important**: NimYAML currently does not support polymorphism or variant **Important**: NimYAML currently does not support polymorphism. This may be
object types. This may be added in the future. added in the future.
This also means that NimYAML is generally able to work with object, tuple and This also means that NimYAML is generally able to work with object, tuple and
enum types defined in the standard library or a third-party library without enum types defined in the standard library or a third-party library without
@ -125,18 +125,43 @@ Variant Object Types
.................... ....................
A *variant object type* is an object type that contains one or more ``case`` A *variant object type* is an object type that contains one or more ``case``
clauses. NimYAML currently supports variant object types. However, this feature clauses. NimYAML supports variant object types. Only the currently accessible
is **highly experimental**. Only the currently accessible fields of a variant fields of a variant object type are dumped, and only those may be present when
object type are dumped, and only those may be present when loading. The loading.
discriminator field(s) are treated like all other fields. The value of a
discriminator field must occur before any value of a field that depends on it.
This violates the YAML specification and therefore will be changed in the
future.
While dumping variant object types directly is currently not production ready, The value of a discriminator field must be loaded before any value of a field
you can use them for processing heterogeneous data sets. For example, if you that depends on it. Therefore, a YAML mapping cannot be used to serialize
have a YAML document which contains differently typed values in the same list variant object types - the YAML specification explicitly states that the order
like this: of key-value pairs in a mapping must not be used to convey content information.
So, any variant object type is serialized as a list of key-value pairs.
For example, this type:
.. code-block:: nim
type
AnimalKind = enum
akCat, akDog
Animal = object
name: string
case kind: AnimalKind
of akCat:
purringIntensity: int
of akDog:
barkometer: int
will be serialized as:
.. code-block:: yaml
%YAML 1.2
--- !nim:custom:Animal
- name: Bastet
- kind: akCat
- purringIntensity: 7
You can also use variant object types for processing heterogeneous data sets.
For example, if you have a YAML document which contains differently typed values
in the same list like this:
.. code-block:: yaml .. code-block:: yaml
%YAML 1.2 %YAML 1.2

View File

@ -422,7 +422,6 @@ macro constructFieldValue(t: typedesc, stream: expr, context: expr,
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")),
newCall("reset", o),
newAssignment(discriminant, newIdentNode("value")))) newAssignment(discriminant, newIdentNode("value"))))
result.add(disOb) result.add(disOb)
for bIndex in 1 .. len(child) - 1: for bIndex in 1 .. len(child) - 1:
@ -433,8 +432,10 @@ macro constructFieldValue(t: typedesc, stream: expr, context: expr,
let field = newDotExpr(o, newIdentNode($item)) let field = newDotExpr(o, newIdentNode($item))
var ifStmt = newIfStmt((cond: discTest, body: newStmtList( var ifStmt = newIfStmt((cond: discTest, body: newStmtList(
newCall("constructChild", stream, context, field)))) newCall("constructChild", stream, context, field))))
ifStmt.add(newNimNode(nnkElse).add(newNimNode(nnkDiscardStmt).add( ifStmt.add(newNimNode(nnkElse).add(newNimNode(nnkRaiseStmt).add(
newEmptyNode()))) # todo: raise exception here newCall("newException", newIdentNode("YamlConstructionError"),
infix(newStrLitNode("Field " & $item & " not allowed for " &
$child[0] & " == "), "&", prefix(discriminant, "$"))))))
ob.add(newStmtList(ifStmt)) ob.add(newStmtList(ifStmt))
result.add(ob) result.add(ob)
else: else:
@ -444,18 +445,34 @@ macro constructFieldValue(t: typedesc, stream: expr, context: expr,
ob.add(newStmtList(newCall("constructChild", stream, context, field))) ob.add(newStmtList(newCall("constructChild", stream, context, field)))
result.add(ob) result.add(ob)
proc isVariantObject(t: typedesc): bool {.compileTime.} =
let tDesc = getType(t)
if tDesc.kind != nnkObjectTy: return false
for child in tDesc[2].children:
if child.kind == nnkRecCase: return true
return false
proc constructObject*[O: object|tuple]( 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() let e = s.next()
if e.kind != yamlStartMap: const
startKind = when isVariantObject(O): yamlStartSeq else: yamlStartMap
endKind = when isVariantObject(O): yamlEndSeq else: yamlEndMap
if e.kind != startKind:
raise newException(YamlConstructionError, "While constructing " & raise newException(YamlConstructionError, "While constructing " &
typetraits.name(O) & ": Expected map start, got " & $e.kind) typetraits.name(O) & ": Expected map start, got " & $e.kind)
while s.peek.kind != yamlEndMap: when isVariantObject(O): reset(result) # make discriminants writeable
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
let e = s.next() var e = s.next()
when isVariantObject(O):
if e.kind != yamlStartMap:
raise newException(YamlConstructionError,
"Expected single-pair map, got " & $e.kind)
e = s.next()
if e.kind != yamlScalar: if e.kind != yamlScalar:
raise newException(YamlConstructionError, raise newException(YamlConstructionError,
"Expected field name, got " & $e.kind) "Expected field name, got " & $e.kind)
@ -465,7 +482,13 @@ proc constructObject*[O: object|tuple](
if fname == name: if fname == name:
constructChild(s, c, value) constructChild(s, c, value)
break break
else: constructFieldValue(O, s, c, name, result) else:
constructFieldValue(O, s, c, name, result)
when isVariantObject(O):
e = s.next()
if e.kind != yamlEndMap:
raise newException(YamlConstructionError,
"Expected end of single-pair map, got " & $e.kind)
discard s.next() discard s.next()
proc representObject*[O: object|tuple](value: O, ts: TagStyle, proc representObject*[O: object|tuple](value: O, ts: TagStyle,
@ -473,8 +496,13 @@ proc representObject*[O: object|tuple](value: O, ts: TagStyle,
## represents a Nim object or tuple as YAML mapping ## represents a Nim object or tuple as YAML mapping
result = iterator(): YamlStreamEvent = result = iterator(): YamlStreamEvent =
let childTagStyle = if ts == tsRootOnly: tsNone else: ts let childTagStyle = if ts == tsRootOnly: tsNone else: ts
yield startMapEvent(tag, yAnchorNone) when isVariantObject(O):
yield startSeqEvent(tag, yAnchorNone)
else:
yield startMapEvent(tag, yAnchorNone)
for name, value in fieldPairs(value): for name, value in fieldPairs(value):
when isVariantObject(O):
yield startMapEvent(yTagQuestionMark, yAnchorNone)
yield scalarEvent(name, yield scalarEvent(name,
if childTagStyle == tsNone: yTagQuestionMark else: if childTagStyle == tsNone: yTagQuestionMark else:
yTagNimField, yAnchorNone) yTagNimField, yAnchorNone)
@ -483,7 +511,12 @@ proc representObject*[O: object|tuple](value: O, ts: TagStyle,
let event = events() let event = events()
if finished(events): break if finished(events): break
yield event yield event
yield endMapEvent() when isVariantObject(O):
yield endMapEvent()
when isVariantObject(O):
yield endSeqEvent()
else:
yield endMapEvent()
proc constructObject*[O: enum](s: var YamlStream, c: ConstructionContext, proc constructObject*[O: enum](s: var YamlStream, c: ConstructionContext,
result: var O) result: var O)

View File

@ -26,6 +26,17 @@ type
next: ref Node next: ref Node
BetterInt = distinct int BetterInt = distinct int
AnimalKind = enum
akCat, akDog
Animal = object
name: string
case kind: AnimalKind
of akCat:
purringIntensity: int
of akDog:
barkometer: int
proc `$`(v: BetterInt): string {.borrow.} proc `$`(v: BetterInt): string {.borrow.}
proc `==`(l, r: BetterInt): bool {.borrow.} proc `==`(l, r: BetterInt): bool {.borrow.}
@ -303,6 +314,42 @@ suite "Serialization":
assertStringEqual("%YAML 1.2\n" & assertStringEqual("%YAML 1.2\n" &
"--- !nim:custom:Person \nfirstnamechar: P\nsurname: Pan\nage: 12", "--- !nim:custom:Person \nfirstnamechar: P\nsurname: Pan\nage: 12",
output.data) output.data)
test "Serialization: Load custom variant object":
let input = newStringStream(
"---\n- - name: Bastet\n - kind: akCat\n - purringIntensity: 7\n" &
"- - name: Anubis\n - kind: akDog\n - barkometer: 13")
var result: seq[Animal]
load(input, result)
assert result.len == 2
assert result[0].name == "Bastet"
assert result[0].kind == akCat
assert result[0].purringIntensity == 7
assert result[1].name == "Anubis"
assert result[1].kind == akDog
assert result[1].barkometer == 13
test "Serialization: Dump custom variant object":
let input = @[Animal(name: "Bastet", kind: akCat, purringIntensity: 7),
Animal(name: "Anubis", kind: akDog, barkometer: 13)]
var output = newStringStream()
dump(input, output, tsNone, asTidy, blockOnly)
assertStringEqual """%YAML 1.2
---
-
-
name: Bastet
-
kind: akCat
-
purringIntensity: 7
-
-
name: Anubis
-
kind: akDog
-
barkometer: 13""", output.data
test "Serialization: Dump cyclic data structure": test "Serialization: Dump cyclic data structure":
var var