diff --git a/doc/serialization.txt b/doc/serialization.txt index e2dc7bc..4197d9a 100644 --- a/doc/serialization.txt +++ b/doc/serialization.txt @@ -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 diff --git a/private/serialization.nim b/private/serialization.nim index a9a3664..662cea4 100644 --- a/private/serialization.nim +++ b/private/serialization.nim @@ -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) diff --git a/test/serializing.nim b/test/serializing.nim index 45029b7..cdbb040 100644 --- a/test/serializing.nim +++ b/test/serializing.nim @@ -26,6 +26,17 @@ type next: ref Node 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.} @@ -303,6 +314,42 @@ suite "Serialization": assertStringEqual("%YAML 1.2\n" & "--- !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