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
out of the box.
**Important**: NimYAML currently does not support polymorphism or variant
object types. This may be added in the future.
**Important**: NimYAML currently does not support polymorphism. This may be
added in the future.
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
@ -125,18 +125,43 @@ Variant Object Types
....................
A *variant object type* is an object type that contains one or more ``case``
clauses. NimYAML currently supports variant object types. However, this feature
is **highly experimental**. Only the currently accessible fields of a variant
object type are dumped, and only those may be present when loading. The
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.
clauses. NimYAML supports variant object types. Only the currently accessible
fields of a variant object type are dumped, and only those may be present when
loading.
While dumping variant object types directly is currently not production ready,
you can use them for processing heterogeneous data sets. For example, if you
have a YAML document which contains differently typed values in the same list
like this:
The value of a discriminator field must be loaded before any value of a field
that depends on it. Therefore, a YAML mapping cannot be used to serialize
variant object types - the YAML specification explicitly states that the order
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
%YAML 1.2

View File

@ -422,7 +422,6 @@ macro constructFieldValue(t: typedesc, stream: expr, context: expr,
newNimNode(nnkIdentDefs).add(
newIdentNode("value"), discType, newEmptyNode())),
newCall("constructChild", stream, context, newIdentNode("value")),
newCall("reset", o),
newAssignment(discriminant, newIdentNode("value"))))
result.add(disOb)
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))
var ifStmt = newIfStmt((cond: discTest, body: newStmtList(
newCall("constructChild", stream, context, field))))
ifStmt.add(newNimNode(nnkElse).add(newNimNode(nnkDiscardStmt).add(
newEmptyNode()))) # todo: raise exception here
ifStmt.add(newNimNode(nnkElse).add(newNimNode(nnkRaiseStmt).add(
newCall("newException", newIdentNode("YamlConstructionError"),
infix(newStrLitNode("Field " & $item & " not allowed for " &
$child[0] & " == "), "&", prefix(discriminant, "$"))))))
ob.add(newStmtList(ifStmt))
result.add(ob)
else:
@ -444,18 +445,34 @@ macro constructFieldValue(t: typedesc, stream: expr, context: expr,
ob.add(newStmtList(newCall("constructChild", stream, context, field)))
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](
s: var YamlStream, c: ConstructionContext, result: var O)
{.raises: [YamlConstructionError, YamlStreamError].} =
## constructs a Nim object or tuple from a YAML mapping
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 " &
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
# 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:
raise newException(YamlConstructionError,
"Expected field name, got " & $e.kind)
@ -465,7 +482,13 @@ proc constructObject*[O: object|tuple](
if fname == name:
constructChild(s, c, value)
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()
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
result = iterator(): YamlStreamEvent =
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):
when isVariantObject(O):
yield startMapEvent(yTagQuestionMark, yAnchorNone)
yield scalarEvent(name,
if childTagStyle == tsNone: yTagQuestionMark else:
yTagNimField, yAnchorNone)
@ -483,7 +511,12 @@ proc representObject*[O: object|tuple](value: O, ts: TagStyle,
let event = events()
if finished(events): break
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,
result: var O)

View File

@ -27,6 +27,17 @@ type
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 `==`(l, r: BetterInt): bool {.borrow.}
@ -304,6 +315,42 @@ suite "Serialization":
"--- !nim:custom:Person \nfirstnamechar: P\nsurname: Pan\nage: 12",
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":
var
a = newNode("a")