diff --git a/doc/serialization.txt b/doc/serialization.txt index 6c1d4d8..3d91c1c 100644 --- a/doc/serialization.txt +++ b/doc/serialization.txt @@ -93,6 +93,15 @@ You can simply load it into a `seq[int]`. If your YAML file contains differently typed values in the same collection, you can use an implicit variant object, see below. +A special case is ``Option[T]``: This type will either contain a value or not. +NimYAML maps ``!!null`` YAML scalars to the option's ``none(T)`` value. +This also works for ``ref`` types because ``Option`` for those types will use +``nil`` as its ``none(T)`` value. + +By default, ``Option`` fields must be given even if they are ``none(T)``. +You can circumvent this by putting the annotation ``{.sparse.}`` on the type +containing the ``Option`` field. + Reference Types --------------- diff --git a/test/tannotations.nim b/test/tannotations.nim index 3cd352e..7b10b93 100644 --- a/test/tannotations.nim +++ b/test/tannotations.nim @@ -25,6 +25,10 @@ type strVal: string of ckInt: intVal: int + + Sparse {.sparse.} = ref object of RootObj + name*: Option[string] + description*: Option[string] suite "Serialization Annotations": test "load default value": @@ -59,4 +63,11 @@ suite "Serialization Annotations": load(input, result) assert len(result) == 2 assert result[0].kind == ckString - assert result[1].kind == ckInt \ No newline at end of file + assert result[1].kind == ckInt + + test "load sparse type": + let input = "{}" + var result: Sparse + load(input, result) + assert result.name.isNone + assert result.description.isNone \ No newline at end of file diff --git a/yaml/annotations.nim b/yaml/annotations.nim index 646d27b..490775c 100644 --- a/yaml/annotations.nim +++ b/yaml/annotations.nim @@ -12,7 +12,7 @@ ## (de)serialization behavior of those fields. template defaultVal*(value : typed) {.pragma.} - ## This annotation can be assigned to an object field. During deserialization, + ## This annotation can be put on an object field. During deserialization, ## if no value for this field is given, the ``value`` parameter of this ## annotation is used as value. ## @@ -23,6 +23,19 @@ template defaultVal*(value : typed) {.pragma.} ## a {.defaultVal: "foo".}: string ## c {.defaultVal: (1,2).}: tuple[x, y: int] +template sparse*() {.pragma.} + ## This annotation can be put on an object type. During deserialization, + ## the input may omit any field that has an ``Option[T]`` type (for any + ## concrete ``T``) and that field will be treated as if it had the annotation + ## ``{.defaultVal: none(T).}``. + ## + ## Example usage: + ## + ## .. code-block:: + ## type MyObject {.sparse.} = object + ## a: Option[string] + ## b: Option[int] + template transient*() {.pragma.} ## This annotation can be put on an object field. Any object field ## carrying this annotation will not be serialized to YAML and cannot be given diff --git a/yaml/serialization.nim b/yaml/serialization.nim index b43ecd5..128f80a 100644 --- a/yaml/serialization.nim +++ b/yaml/serialization.nim @@ -1,5 +1,5 @@ # NimYAML - YAML implementation in Nim -# (c) Copyright 2016 Felix Krause +# (c) Copyright 2016 - 2020 Felix Krause # # See the file "copying.txt", included in this # distribution, for details about the copyright. @@ -18,7 +18,7 @@ import tables, typetraits, strutils, macros, streams, times, parseutils, options import parser, taglib, presenter, stream, private/internal, hints, annotations -export stream, macros, annotations +export stream, macros, annotations, options # *something* in here needs externally visible `==`(x,y: AnchorId), # but I cannot figure out what. binding it would be the better option. @@ -656,15 +656,32 @@ proc addDefaultOr(tName: string, i: int, o: NimNode, `o`.`field` = `o`.`field`.getCustomPragmaVal(defaultVal) else: `elseBranch` +proc hasSparse(t: typedesc): bool {.compileTime.} = + when compiles(t.hasCustomPragma(sparse)): + return t.hasCustomPragma(sparse) + else: + return false + +proc getOptionInner(fType: NimNode): NimNode {.compileTime.} = + if fType.kind == nnkBracketExpr and len(fType) == 2 and + fType[1].kind == nnkSym: + return newIdentNode($fType[1]) + else: return nil + proc checkMissing(s: NimNode, t: NimNode, tName: string, field: NimNode, i: int, matched, o: NimNode): NimNode {.compileTime.} = - let fName = escape($field) + let + fType = getTypeInst(field) + fName = escape($field) + optionInner = getOptionInner(fType) result = quote do: when not `o`.`field`.hasCustomPragma(transient): if not `matched`[`i`]: when `o`.`field`.hasCustomPragma(defaultVal): `o`.`field` = `o`.`field`.getCustomPragmaVal(defaultVal) + elif hasSparse(`t`) and `o`.`field` is Option: + `o`.`field` = none(`optionInner`) else: raise constructionError(`s`, "While constructing " & `tName` & ": Missing field: " & `fName`) @@ -700,7 +717,8 @@ macro ensureAllFieldsPresent(s: YamlStream, t: typedesc, o: typed, var field = 0 for child in tDesc[2].children: if child.kind == nnkRecCase: - result.add(checkMissing(s, t, tName, child[0], field, matched, o)) + result.add(checkMissing( + s, t, tName, child[0], field, matched, o)) for bIndex in 1 .. len(child) - 1: let discChecks = newStmtList() var @@ -716,7 +734,8 @@ macro ensureAllFieldsPresent(s: YamlStream, t: typedesc, o: typed, else: internalError("Unexpected child kind: " & $child[bIndex].kind) for item in child[bIndex][recListIndex].recListItems: inc(field) - discChecks.add(checkMissing(s, t, tName, item, field, matched, o)) + discChecks.add(checkMissing( + s, t, tName, item, field, matched, o)) result.add(newIfStmt((infix(newDotExpr(o, newIdentNode($child[0])), "in", curValues), discChecks))) else: