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
|
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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue