mirror of https://github.com/status-im/NimYAML.git
Fixed and properly implemented variant objects
This commit is contained in:
parent
152a4f3bd3
commit
7cad7b5478
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
Loading…
Reference in New Issue