/ yaml / native.nim
native.nim
   1  #            NimYAML - YAML implementation in Nim
   2  #        (c) Copyright 2016 - 2020 Felix Krause
   3  #
   4  #    See the file "copying.txt", included in this
   5  #    distribution, for details about the copyright.
   6  
   7  ## ==================
   8  ## Module yaml/native
   9  ## ==================
  10  ##
  11  ## This module transforms native Nim values into a stream of YAML events,
  12  ## and vice versa. The procs of this module must be available for name binding
  13  ## when using the loading and dumping APIs. A NimYAML consumer would rarely
  14  ## call this module's procs directly. The main entry points to this API are
  15  ## ``construct`` and ``represent``; all other procs are usually called via
  16  ## instantiations of those two procs.
  17  ##
  18  ## You can extend the procs defined here with own procs to define custom
  19  ## handling for native types. See the documentation on the NimYAML
  20  ## website for more information.
  21  
  22  import std / [tables, typetraits, strutils, macros, streams, times, parseutils, options]
  23  import data, taglib, stream, private/internal, hints, annotations
  24  export data, stream, macros, annotations, options
  25    # *something* in here needs externally visible `==`(x,y: AnchorId),
  26    # but I cannot figure out what. binding it would be the better option.
  27  
  28  type
  29    TagStyle* = enum
  30      ## Whether object should be serialized with explicit tags.
  31      ##
  32      ## - ``tsNone``: No tags will be outputted unless necessary.
  33      ## - ``tsRootOnly``: A tag will only be outputted for the root tag and
  34      ##   where necessary.
  35      ## - ``tsAll``: Tags will be outputted for every object.
  36      tsNone, tsRootOnly, tsAll
  37    
  38    AnchorStyle* = enum
  39      ## How ref object should be serialized.
  40      ##
  41      ## - ``asNone``: No anchors will be written. Values present at
  42      ##   multiple places in the content that is serialized will be
  43      ##   duplicated at every occurrence. If the content is cyclic, this
  44      ##   will raise a YamlSerializationError.
  45      ## - ``asTidy``: Anchors will only be generated for objects that
  46      ##   actually occur more than once in the content to be serialized.
  47      ##   This is a bit slower and needs more memory than ``asAlways``.
  48      ## - ``asAlways``: Achors will be generated for every ref object in the
  49      ##   content that is serialized, regardless of whether the object is
  50      ##   referenced again afterwards.
  51      asNone, asTidy, asAlways
  52  
  53    SerializationOptions* = object
  54      tagStyle*   : TagStyle    = tsNone
  55      anchorStyle*: AnchorStyle = asTidy
  56      handles*    : seq[tuple[handle, uriPrefix: string]]
  57  
  58    SerializationContext* = object
  59      ## Context information for the process of serializing YAML from Nim values.
  60      refs: Table[pointer, tuple[a: Anchor, referenced: bool]]
  61      emitTag: bool
  62      nextAnchorId: string
  63      options*: SerializationOptions
  64      putImpl*: proc(ctx: var SerializationContext, e: Event) {.raises: [], closure.}
  65      overridingScalarStyle*: ScalarStyle = ssAny
  66      overridingCollectionStyle*: CollectionStyle = csAny
  67  
  68    ConstructionContext* = object
  69      ## Context information for the process of constructing Nim values from YAML.
  70      input*: YamlStream
  71      refs* : Table[Anchor, tuple[tag: Tag, p: pointer]]
  72  
  73    YamlConstructionError* = object of YamlLoadingError
  74      ## Exception that may be raised when constructing data objects from a
  75      ## `YamlStream <#YamlStream>`_. The fields ``line``, ``column`` and
  76      ## ``lineContent`` are only available if the costructing proc also does
  77      ## parsing, because otherwise this information is not available to the
  78      ## costruction proc.
  79  
  80    YamlSerializationError* = object of ValueError
  81      ## Exception that may be raised when serializing Nim values into YAML
  82      ## stream events.
  83  
  84  proc put*(ctx: var SerializationContext, e: Event) {.raises: [].} =
  85    ctx.putImpl(ctx, e)
  86  
  87  proc scalarStyleFor(ctx: var SerializationContext, t: typedesc): ScalarStyle =
  88    if ctx.overridingScalarStyle != ssAny:
  89      result = ctx.overridingScalarStyle
  90      ctx.overridingScalarStyle = ssAny
  91    else:
  92      when compiles(t.hasCustomPragma(scalar)):
  93        when t.hasCustomPragma(scalar):
  94          result = t.getCustomPragmaVal(scalar)
  95        else: result = ssAny
  96      else: result = ssAny
  97    ctx.overridingCollectionStyle = csAny
  98  
  99  proc collectionStyleFor(ctx: var SerializationContext, t: typedesc): CollectionStyle =
 100    if ctx.overridingCollectionStyle != csAny:
 101      result = ctx.overridingCollectionStyle
 102      ctx.overridingCollectionStyle = csAny
 103    else:
 104      when compiles(t.hasCustomPragma(collection)):
 105        when t.hasCustomPragma(collection):
 106          result = t.getCustomPragmaVal(collection)
 107        else: result = csAny
 108      else: result = csAny
 109    ctx.overridingScalarStyle = ssAny
 110  
 111  # forward declares
 112  
 113  proc constructChild*[T](
 114    ctx   : var ConstructionContext,
 115    result: var T,
 116  ) {.raises: [YamlConstructionError, YamlStreamError].}
 117    ## Constructs an arbitrary Nim value from a part of a YAML stream.
 118    ## The stream will advance until after the finishing token that was used
 119    ## for constructing the value. The ``ConstructionContext`` is needed for
 120    ## potential child objects which may be refs.
 121  
 122  proc constructChild*(
 123    ctx   : var ConstructionContext,
 124    result: var string,
 125  ) {.raises: [YamlConstructionError, YamlStreamError].}
 126    ## Constructs a Nim value that is a string from a part of a YAML stream.
 127    ## This specialization takes care of possible nil strings.
 128  
 129  proc constructChild*[T](
 130    ctx   : var ConstructionContext,
 131    result: var seq[T],
 132  ) {.raises: [YamlConstructionError, YamlStreamError].}
 133    ## Constructs a Nim value that is a string from a part of a YAML stream.
 134    ## This specialization takes care of possible nil seqs.
 135  
 136  proc constructChild*[O](
 137    ctx   : var ConstructionContext,
 138    result: var ref O,
 139  ) {.raises: [YamlConstructionError, YamlStreamError].}
 140    ## Constructs an arbitrary Nim value from a part of a YAML stream.
 141    ## The stream will advance until after the finishing token that was used
 142    ## for constructing the value. The object may be constructed from an alias
 143    ## node which will be resolved using the ``ConstructionContext``.
 144  
 145  proc representChild*[O](
 146    ctx  : var SerializationContext,
 147    value: ref O,
 148  ) {.raises: [YamlSerializationError].}
 149    ## Represents an arbitrary Nim reference value as YAML object. The object
 150    ## may be represented as alias node if it is already present in the
 151    ## ``SerializationContext``.
 152  
 153  proc representChild*(
 154    ctx  : var SerializationContext,
 155    value: string,
 156  ) {.inline, raises: [].}
 157    ## Represents a Nim string. Supports nil strings.
 158  
 159  proc representChild*[O](
 160    ctx: var SerializationContext,
 161    value: O,
 162  ) {.raises: [YamlSerializationError].}
 163    ## Represents an arbitrary Nim object as YAML object.
 164  
 165  proc initConstructionContext*(input: YamlStream): ConstructionContext =
 166    result = ConstructionContext(
 167      input: input,
 168      refs : initTable[Anchor, tuple[tag: Tag, p: pointer]](),
 169    )
 170  
 171  proc initSerializationContext*(
 172    options: SerializationOptions,
 173    putImpl: proc(ctx: var SerializationContext, e: Event) {.raises: [], closure.}
 174  ): SerializationContext =
 175    result = SerializationContext(
 176      refs: initTable[pointer, tuple[a: Anchor, referenced: bool]](),
 177      emitTag: options.tagStyle != tsNone,
 178      nextAnchorId: "a",
 179      options: options,
 180      putImpl: putImpl
 181    )
 182  
 183  proc presentTag*(ctx: var SerializationContext, t: typedesc): Tag {.inline.} =
 184    ## Get the Tag that represents the given type in the given style
 185    if ctx.emitTag:
 186      result = yamlTag(t)
 187      if ctx.options.tagStyle == tsRootOnly: ctx.emitTag = false
 188    else:
 189      result = yTagQuestionMark
 190  
 191  proc safeTagUri(tag: Tag): string {.raises: [].} =
 192    try:
 193      var uri = $tag
 194      # '!' is not allowed inside a tag handle
 195      if uri.len > 0 and uri[0] == '!': uri = uri[1..^1]
 196      # ',' is not allowed after a tag handle in the suffix because it's a flow
 197      # indicator
 198      for i in countup(0, uri.len - 1):
 199        if uri[i] == ',': uri[i] = ';'
 200      return uri
 201    except KeyError:
 202      internalError("Unexpected KeyError for Tag " & $tag)
 203  
 204  proc newYamlConstructionError*(
 205    s: YamlStream,
 206    mark: Mark,
 207    msg: string,
 208  ): ref YamlConstructionError =
 209    result = newException(YamlConstructionError, msg)
 210    result.mark = mark
 211    if not s.getLastTokenContext(result.lineContent):
 212      result.lineContent = ""
 213  
 214  proc constructionError*(s: YamlStream, mark: Mark, msg: string):
 215      ref YamlConstructionError =
 216    return newYamlConstructionError(s, mark, msg)
 217  
 218  template constructScalarItem*(
 219    s: var YamlStream,
 220    i: untyped,
 221    t: typedesc,
 222    content: untyped,
 223  ) =
 224    ## Helper template for implementing ``constructObject`` for types that
 225    ## are constructed from a scalar. ``i`` is the identifier that holds
 226    ## the scalar as ``Event`` in the content. Exceptions raised in
 227    ## the content will be automatically caught and wrapped in
 228    ## ``YamlConstructionError``, which will then be raised.
 229    bind constructionError
 230    let i = s.next()
 231    if i.kind != yamlScalar:
 232      raise constructionError(s, i.startPos, "Expected scalar")
 233    try: content
 234    except YamlConstructionError as e: raise e
 235    except CatchableError as e:
 236      var ce = constructionError(s, i.startPos,
 237          "Cannot construct to " & name(t) & ": " & item.scalarContent &
 238          "; error: " & e.msg)
 239      ce.parent = e
 240      raise ce
 241  
 242  proc yamlTag*(T: typedesc[string]): Tag {.inline, noSideEffect, raises: [].} =
 243    yTagString
 244  
 245  proc constructObject*(
 246    ctx   : var ConstructionContext,
 247    result: var string,
 248  ) {.raises: [YamlConstructionError, YamlStreamError].} =
 249    ## constructs a string from a YAML scalar
 250    ctx.input.constructScalarItem(item, string):
 251      result = item.scalarContent
 252  
 253  proc representObject*(
 254    ctx  : var SerializationContext,
 255    value: string,
 256    tag  : Tag,
 257  ) {.raises: [].} =
 258    ## represents a string as YAML scalar
 259    ctx.put(scalarEvent(value, tag, yAnchorNone, ctx.scalarStyleFor(string)))
 260  
 261  proc parseHex[T: int8|int16|int32|int64|uint8|uint16|uint32|uint64](
 262    s: YamlStream, mark: Mark, val: string
 263  ): T =
 264    result = 0
 265    for i in 2..<val.len:
 266      case val[i]
 267      of '_': discard
 268      of '0'..'9': result = result shl 4 or T(ord(val[i]) - ord('0'))
 269      of 'a'..'f': result = result shl 4 or T(ord(val[i]) - ord('a') + 10)
 270      of 'A'..'F': result = result shl 4 or T(ord(val[i]) - ord('A') + 10)
 271      else:
 272        raise s.constructionError(mark, "Invalid character in hex: " &
 273            escape("" & val[i]))
 274  
 275  proc parseOctal[T: int8|int16|int32|int64|uint8|uint16|uint32|uint64](
 276    s: YamlStream, mark: Mark, val: string
 277  ): T =
 278    for i in 2..<val.len:
 279      case val[i]
 280      of '_': discard
 281      of '0'..'7': result = result shl 3 + T((ord(val[i]) - ord('0')))
 282      else:
 283        raise s.constructionError(mark, "Invalid character in hex: " &
 284            escape("" & val[i]))
 285  
 286  type NumberStyle = enum
 287    nsHex
 288    nsOctal
 289    nsDecimal
 290  
 291  proc numberStyle(item: Event): NumberStyle =
 292    if item.scalarContent[0] == '0' and item.scalarContent.len > 1:
 293      if item.scalarContent[1] in {'x', 'X' }: return nsHex
 294      if item.scalarContent[1] in {'o', 'O'}: return nsOctal
 295    return nsDecimal
 296  
 297  proc constructObject*[T: int8|int16|int32|int64](
 298    ctx   : var ConstructionContext,
 299    result: var T,
 300  ) {.raises: [YamlConstructionError, YamlStreamError].} =
 301    ## constructs an integer value from a YAML scalar
 302    ctx.input.constructScalarItem(item, T):
 303      case item.numberStyle
 304      of nsHex:
 305        result = parseHex[T](ctx.input, item.startPos, item.scalarContent)
 306      of nsOctal:
 307        result = parseOctal[T](ctx.input, item.startPos, item.scalarContent)
 308      of nsDecimal:
 309        let nInt = parseBiggestInt(item.scalarContent)
 310        if nInt <= T.high:
 311          # make sure we don't produce a range error
 312          result = T(nInt)
 313        else:
 314          raise ctx.input.constructionError(
 315            item.startPos,
 316            "Cannot construct int; out of range: " &
 317            $nInt & " for type " & T.name & " with max of: " & $T.high
 318          )
 319  
 320  proc constructObject*(
 321    ctx   : var ConstructionContext,
 322    result: var int,
 323  ) {.raises: [YamlConstructionError, YamlStreamError], inline.} =
 324    ## constructs an integer of architecture-defined length by loading it into
 325    ## int32 and then converting it.
 326    var i32Result: int32
 327    ctx.constructObject(i32Result)
 328    result = int(i32Result)
 329  
 330  proc representObject*[T: int8|int16|int32|int64](
 331    ctx  : var SerializationContext,
 332    value: T,
 333    tag  : Tag,
 334  ) {.raises: [].} =
 335    ## represents an integer value as YAML scalar
 336    ctx.put(scalarEvent($value, tag, yAnchorNone, ctx.scalarStyleFor(T)))
 337  
 338  proc representObject*(
 339    ctx  : var SerializationContext,
 340    value: int,
 341    tag  : Tag,
 342  ) {.raises: [YamlSerializationError], inline.}=
 343    ## represent an integer of architecture-defined length by casting it to int32.
 344    ## on 64-bit systems, this may cause a RangeDefect.
 345  
 346    # currently, sizeof(int) is at least sizeof(int32).
 347    try:
 348      ctx.put(scalarEvent(
 349        $int32(value), tag, yAnchorNone, ctx.scalarStyleFor(int)))
 350    except RangeDefect as rd:
 351      var e = newException(YamlSerializationError, rd.msg)
 352      e.parent = rd
 353      raise e
 354  
 355  when defined(JS):
 356    type DefiniteUIntTypes = uint8 | uint16 | uint32
 357  else:
 358    type DefiniteUIntTypes = uint8 | uint16 | uint32 | uint64
 359  
 360  proc constructObject*[T: DefiniteUIntTypes](
 361    ctx   : var ConstructionContext,
 362    result: var T,
 363  ) {.raises: [YamlConstructionError, YamlStreamError].} =
 364    ## construct an unsigned integer value from a YAML scalar
 365    ctx.input.constructScalarItem(item, T):
 366      case item.numberStyle
 367      of nsHex:
 368        result = parseHex[T](ctx.input, item.startPos, item.scalarContent)
 369      of nsOctal:
 370        result = parseOctal[T](ctx.input, item.startPos, item.scalarContent)
 371      else:
 372        let nUInt = parseBiggestUInt(item.scalarContent)
 373        if nUInt <= T.high:
 374          # make sure we don't produce a range error
 375          result = T(nUInt)
 376        else:
 377          raise ctx.input.constructionError(
 378            item.startPos,
 379            "Cannot construct uint; out of range: " &
 380            $nUInt & " for type " & T.name & " with max of: " & $T.high
 381          )
 382  
 383  proc constructObject*(
 384    ctx   : var ConstructionContext,
 385    result: var uint,
 386  ) {.raises: [YamlConstructionError, YamlStreamError], inline.} =
 387    ## represent an unsigned integer of architecture-defined length by loading it
 388    ## into uint32 and then converting it.
 389    var u32Result: uint32
 390    ctx.constructObject(u32Result)
 391    result = uint(u32Result)
 392  
 393  when defined(JS):
 394    # TODO: this is a dirty hack and may lead to overflows!
 395    proc `$`(x: uint8|uint16|uint32|uint64|uint): string =
 396      result = $BiggestInt(x)
 397  
 398  proc representObject*[T: uint8|uint16|uint32|uint64](
 399    ctx  : var SerializationContext,
 400    value: T,
 401    tag  : Tag,
 402  ) {.raises: [].} =
 403    ## represents an unsigned integer value as YAML scalar
 404    ctx.put(scalarEvent($value, tag, yAnchorNone, ctx.scalarStyleFor(T)))
 405  
 406  proc representObject*(
 407    ctx  : var SerializationContext,
 408    value: uint,
 409    tag  : Tag,
 410  ) {.raises: [YamlSerializationError], inline.} =
 411    ## represent an unsigned integer of architecture-defined length by casting it
 412    ## to int32. on 64-bit systems, this may cause a RangeDefect.
 413    try:
 414      ctx.put(scalarEvent(
 415        $uint32(value), tag, yAnchorNone, ctx.scalarStyleFor(uint)))
 416    except RangeDefect as rd:
 417      var e = newException(YamlSerializationError, rd.msg)
 418      e.parent = rd
 419      raise e
 420  
 421  proc constructObject*[T: float|float32|float64](
 422    ctx   : var ConstructionContext,
 423    result: var T,
 424  ) {.raises: [YamlConstructionError, YamlStreamError].} =
 425    ## construct a float value from a YAML scalar
 426    ctx.input.constructScalarItem(item, T):
 427      let hint = guessType(item.scalarContent)
 428      case hint
 429      of yTypeFloat:
 430        var res: BiggestFloat
 431        discard parseBiggestFloat(item.scalarContent, res)
 432        result = res
 433      of yTypeInteger:
 434        var res: BiggestFloat
 435        discard parseBiggestFloat(item.scalarContent, res)
 436        result = res
 437      of yTypeFloatInf:
 438          if item.scalarContent[0] == '-': result = NegInf
 439          else: result = Inf
 440      of yTypeFloatNaN: result = NaN
 441      else:
 442        raise ctx.input.constructionError(
 443          item.startPos,
 444          "Cannot construct to float: " & escape(item.scalarContent)
 445        )
 446  
 447  proc representObject*[T: float|float32|float64](
 448    ctx  : var SerializationContext,
 449    value: T,
 450    tag  : Tag,
 451  ) {.raises: [].} =
 452    ## represents a float value as YAML scalar
 453    case value
 454    of Inf: ctx.put(scalarEvent(".inf", tag, yAnchorNone, ctx.scalarStyleFor(T)))
 455    of NegInf: ctx.put(scalarEvent("-.inf", tag, yAnchorNone, ctx.scalarStyleFor(T)))
 456    of NaN: ctx.put(scalarEvent(".nan", tag, yAnchorNone, ctx.scalarStyleFor(T)))
 457    else: ctx.put(scalarEvent($value, tag, yAnchorNone, ctx.scalarStyleFor(T)))
 458  
 459  proc yamlTag*(T: typedesc[bool]): Tag {.inline, raises: [].} = yTagBoolean
 460  
 461  proc constructObject*(
 462    ctx   : var ConstructionContext,
 463    result: var bool,
 464  ) {.raises: [YamlConstructionError, YamlStreamError].} =
 465    ## constructs a bool value from a YAML scalar
 466    ctx.input.constructScalarItem(item, bool):
 467      case guessType(item.scalarContent)
 468      of yTypeBoolTrue: result = true
 469      of yTypeBoolFalse: result = false
 470      else:
 471        raise ctx.input.constructionError(
 472          item.startPos,
 473          "Cannot construct to bool: " & escape(item.scalarContent)
 474        )
 475  
 476  proc representObject*(
 477    ctx  : var SerializationContext,
 478    value: bool,
 479    tag  : Tag,
 480  )  {.raises: [].} =
 481    ## represents a bool value as a YAML scalar
 482    ctx.put(scalarEvent(if value: "true" else: "false",
 483      tag, yAnchorNone, ctx.scalarStyleFor(bool)))
 484  
 485  proc constructObject*(
 486    ctx   : var ConstructionContext,
 487    result: var char,
 488  ) {.raises: [YamlConstructionError, YamlStreamError].} =
 489    ## constructs a char value from a YAML scalar
 490    ctx.input.constructScalarItem(item, char):
 491      if item.scalarContent.len != 1:
 492        raise ctx.input.constructionError(
 493          item.startPos,
 494          "Cannot construct to char (length != 1): " & escape(item.scalarContent)
 495        )
 496      else: result = item.scalarContent[0]
 497  
 498  proc representObject*(
 499    ctx  : var SerializationContext,
 500    value: char,
 501    tag  : Tag
 502  ) {.raises: [].} =
 503    ## represents a char value as YAML scalar
 504    ctx.put(scalarEvent("" & value, tag, yAnchorNone, ctx.scalarStyleFor(char)))
 505  
 506  proc yamlTag*(T: typedesc[Time]): Tag {.inline, raises: [].} = yTagTimestamp
 507  
 508  proc constructObject*(
 509    ctx   : var ConstructionContext,
 510    result: var Time,
 511  ) {.raises: [YamlConstructionError, YamlStreamError].} =
 512    ctx.input.constructScalarItem(item, Time):
 513      if guessType(item.scalarContent) == yTypeTimestamp:
 514        var
 515          tmp = newStringOfCap(60)
 516          pos = 8
 517          c: char
 518        while pos < item.scalarContent.len():
 519          c = item.scalarContent[pos]
 520          if c in {' ', '\t', 'T', 't'}: break
 521          inc(pos)
 522        if pos == item.scalarContent.len():
 523          tmp.add(item.scalarContent)
 524          tmp.add("T00:00:00+00:00")
 525        else:
 526          tmp.add(item.scalarContent[0 .. pos - 1])
 527          if c in {' ', '\t'}:
 528            while true:
 529              inc(pos)
 530              c = item.scalarContent[pos]
 531              if c notin {' ', '\t'}: break
 532          else: inc(pos)
 533          tmp.add("T")
 534          let timeStart = pos
 535          inc(pos, 7)
 536          var fractionStart = -1
 537          while pos < item.scalarContent.len():
 538            c = item.scalarContent[pos]
 539            if c in {'+', '-', 'Z', ' ', '\t'}: break
 540            elif c == '.': fractionStart = pos
 541            inc(pos)
 542          if fractionStart == -1:
 543            tmp.add(item.scalarContent[timeStart .. pos - 1])
 544          else:
 545            tmp.add(item.scalarContent[timeStart .. fractionStart - 1])
 546          if c in {'Z', ' ', '\t'}: tmp.add("+00:00")
 547          else:
 548            tmp.add(c)
 549            inc(pos)
 550            let tzStart = pos
 551            inc(pos)
 552            if pos < item.scalarContent.len() and item.scalarContent[pos] != ':':
 553              inc(pos)
 554            if pos - tzStart == 1: tmp.add('0')
 555            tmp.add(item.scalarContent[tzStart .. pos - 1])
 556            if pos == item.scalarContent.len(): tmp.add(":00")
 557            elif pos + 2 == item.scalarContent.len():
 558              tmp.add(":0")
 559              tmp.add(item.scalarContent[pos + 1])
 560            else:
 561              tmp.add(item.scalarContent[pos .. pos + 2])
 562        let info = tmp.parse("yyyy-M-d'T'H:mm:sszzz")
 563        result = info.toTime()
 564      else:
 565        raise ctx.input.constructionError(
 566          item.startPos,
 567          "Not a parsable timestamp: " & escape(item.scalarContent)
 568        )
 569  
 570  proc representObject*(
 571    ctx  : var SerializationContext,
 572    value: Time,
 573    tag  : Tag,
 574  ) {.raises: [].} =
 575    let tmp = value.utc()
 576    ctx.put(scalarEvent(tmp.format(
 577      "yyyy-MM-dd'T'HH:mm:ss'Z'"), tag, yAnchorNone, ctx.scalarStyleFor(Time)))
 578  
 579  proc yamlTag*[I](T: typedesc[seq[I]]): Tag {.inline, raises: [].} =
 580    return nimTag("system:seq(" & safeTagUri(yamlTag(I)) & ')')
 581  
 582  proc yamlTag*[I](T: typedesc[set[I]]): Tag {.inline, raises: [].} =
 583    return nimTag("system:set(" & safeTagUri(yamlTag(I)) & ')')
 584  
 585  proc constructObject*[T](
 586    ctx   : var ConstructionContext,
 587    result: var seq[T],
 588  ) {.raises: [YamlConstructionError, YamlStreamError].} =
 589    ## constructs a Nim seq from a YAML sequence
 590    let event = ctx.input.next()
 591    if event.kind != yamlStartSeq:
 592      raise ctx.input.constructionError(event.startPos, "Expected sequence start")
 593    result = newSeq[T]()
 594    while ctx.input.peek().kind != yamlEndSeq:
 595      var item: T
 596      ctx.constructChild(item)
 597      result.add(move(item))
 598    discard ctx.input.next()
 599  
 600  proc constructObject*[T](
 601    ctx   : var ConstructionContext,
 602    result: var set[T],
 603  ) {.raises: [YamlConstructionError, YamlStreamError].} =
 604    ## constructs a Nim seq from a YAML sequence
 605    let event = ctx.input.next()
 606    if event.kind != yamlStartSeq:
 607      raise ctx.input.constructionError(event.startPos, "Expected sequence start")
 608    result = {}
 609    while ctx.input.peek().kind != yamlEndSeq:
 610      var item: T
 611      ctx.constructChild(item)
 612      result.incl(item)
 613    discard ctx.input.next()
 614  
 615  proc representObject*[T](
 616    ctx  : var SerializationContext,
 617    value: seq[T]|set[T],
 618    tag  : Tag,
 619  ) {.raises: [YamlSerializationError].} =
 620    ## represents a Nim seq as YAML sequence
 621    ctx.put(
 622      startSeqEvent(tag = tag, style = ctx.collectionStyleFor(type(value))))
 623    for item in value: ctx.representChild(item)
 624    ctx.put(endSeqEvent())
 625  
 626  proc yamlTag*[I, V](T: typedesc[array[I, V]]): Tag {.inline, raises: [].} =
 627    const rangeName = name(I)
 628    return nimTag("system:array(" & rangeName[6..rangeName.high()] & ';' &
 629        safeTagUri(yamlTag(V)) & ')')
 630  
 631  proc constructObject*[I, T](
 632    ctx   : var ConstructionContext,
 633    result: var array[I, T],
 634  ) {.raises: [YamlConstructionError, YamlStreamError].} =
 635    ## constructs a Nim array from a YAML sequence
 636    var event = ctx.input.next()
 637    if event.kind != yamlStartSeq:
 638      raise ctx.input.constructionError(event.startPos, "Expected sequence start")
 639    for index in low(I)..high(I):
 640      event = ctx.input.peek()
 641      if event.kind == yamlEndSeq:
 642        raise ctx.input.constructionError(event.startPos, "Too few array values")
 643      ctx.constructChild(result[index])
 644    event = ctx.input.next()
 645    if event.kind != yamlEndSeq:
 646      raise ctx.input.constructionError(event.startPos, "Too many array values")
 647  
 648  proc representObject*[I, T](
 649    ctx  : var SerializationContext,
 650    value: array[I, T],
 651    tag  : Tag,
 652  ) {.raises: [YamlSerializationError].} =
 653    ## represents a Nim array as YAML sequence
 654    ctx.put(startSeqEvent(tag = tag, style = ctx.collectionStyleFor(array[I, T])))
 655    for item in value: ctx.representChild(item)
 656    ctx.put(endSeqEvent())
 657  
 658  proc yamlTag*[K, V](T: typedesc[Table[K, V]]): Tag {.inline, raises: [].} =
 659    return nimTag("tables:Table(" & safeTagUri(yamlTag(K)) & ';' &
 660        safeTagUri(yamlTag(V)) & ")")
 661  
 662  proc constructObject*[K, V](
 663    ctx   : var ConstructionContext,
 664    result: var Table[K, V],
 665  ) {.raises: [YamlConstructionError, YamlStreamError].} =
 666    ## constructs a Nim Table from a YAML mapping
 667    let event = ctx.input.next()
 668    if event.kind != yamlStartMap:
 669      raise ctx.input.constructionError(
 670        event.startPos, "Expected map start, got " & $event.kind
 671      )
 672    result = initTable[K, V]()
 673    while ctx.input.peek.kind != yamlEndMap:
 674      var
 675        key: K
 676        value: V
 677      ctx.constructChild(key)
 678      ctx.constructChild(value)
 679      if result.contains(key):
 680        raise ctx.input.constructionError(event.startPos, "Duplicate table key!")
 681      result[key] = value
 682    discard ctx.input.next()
 683  
 684  proc representObject*[K, V](
 685    ctx  : var SerializationContext,
 686    value: Table[K, V],
 687    tag  : Tag,
 688  ) {.raises: [YamlSerializationError].} =
 689    ## represents a Nim Table as YAML mapping
 690    ctx.put(
 691      startMapEvent(tag = tag, style = ctx.collectionStyleFor(Table[K, V])))
 692    for key, value in value.pairs:
 693      ctx.representChild(key)
 694      ctx.representChild(value)
 695    ctx.put(endMapEvent())
 696  
 697  proc yamlTag*[K, V](T: typedesc[OrderedTable[K, V]]): Tag
 698      {.inline, raises: [].} =
 699    return nimTag("tables:OrderedTable(" & safeTagUri(yamlTag(K)) & ';' &
 700        safeTagUri(yamlTag(V)) & ")")
 701  
 702  proc constructObject*[K, V](
 703    ctx   : var ConstructionContext,
 704    result: var OrderedTable[K, V],
 705  ) {.raises: [YamlConstructionError, YamlStreamError].} =
 706    ## constructs a Nim OrderedTable from a YAML mapping
 707    var event = ctx.input.next()
 708    if event.kind != yamlStartSeq:
 709      raise ctx.input.constructionError(
 710        event.startPos, "Expected seq start, got " & $event.kind
 711      )
 712    result = initOrderedTable[K, V]()
 713    while ctx.input.peek.kind != yamlEndSeq:
 714      var
 715        key: K
 716        value: V
 717      event = ctx.input.next()
 718      if event.kind != yamlStartMap:
 719        raise ctx.input.constructionError(
 720          event.startPos, "Expected map start, got " & $event.kind
 721        )
 722      ctx.constructChild(key)
 723      ctx.constructChild(value)
 724      event = ctx.input.next()
 725      if event.kind != yamlEndMap:
 726        raise ctx.input.constructionError(
 727          event.startPos, "Expected map end, got " & $event.kind
 728        )
 729      if result.contains(key):
 730        raise ctx.input.constructionError(event.startPos, "Duplicate table key!")
 731      result[move(key)] = move(value)
 732    discard ctx.input.next()
 733  
 734  proc representObject*[K, V](
 735    ctx  : var SerializationContext,
 736    value: OrderedTable[K, V],
 737    tag  : Tag,
 738  ) {.raises: [YamlSerializationError].} =
 739    ctx.put(startSeqEvent(
 740      tag = tag, style = ctx.collectionStyleFor(OrderedTable[K, V])))
 741    for key, value in value.pairs:
 742      ctx.put(startMapEvent())
 743      ctx.representChild(key)
 744      ctx.representChild(value)
 745      ctx.put(endMapEvent())
 746    ctx.put(endSeqEvent())
 747  
 748  proc yamlTag*(T: typedesc[object|enum]):
 749      Tag {.inline, raises: [].} =
 750    return nimTag("custom:" & (typetraits.name(type(T))))
 751  
 752  proc yamlTag*(T: typedesc[tuple]):
 753      Tag {.inline, raises: [].} =
 754    var
 755      i: T
 756      uri = nimyamlTagRepositoryPrefix & "tuple("
 757      first = true
 758    for name, value in fieldPairs(i):
 759      if first: first = false
 760      else: uri.add(",")
 761      uri.add(safeTagUri(yamlTag(type(value))))
 762    uri.add(")")
 763    return Tag(uri)
 764  
 765  iterator recListItems(n: NimNode): NimNode =
 766    if n.kind == nnkRecList:
 767      for item in n.children: yield item
 768    else: yield n
 769  
 770  proc recListLen(n: NimNode): int {.compileTime.} =
 771    if n.kind == nnkRecList: result = n.len
 772    else: result = 1
 773  
 774  proc recListNode(n: NimNode): NimNode {.compileTime.} =
 775    if n.kind == nnkRecList: result = n[0]
 776    else: result = n
 777  
 778  proc parentType(tDesc: NimNode): NimNode {.compileTime.} =
 779    var name: NimNode
 780    case tDesc[1].kind
 781    of nnkEmpty: return nil
 782    of nnkBracketExpr:
 783      # happens when parent type is `ref X`
 784      name = tDesc[1][1]
 785    of nnkObjectTy, nnkSym:
 786      name = tDesc[1]
 787    else:
 788      return nil
 789    result = newNimNode(nnkBracketExpr)
 790    result.add(bindSym("typeDesc"))
 791    result.add(name)
 792  
 793  proc fieldCount(t: NimNode): int {.compiletime.} =
 794    result = 0
 795    var tTypedesc: NimNode
 796    if t.kind == nnkSym:
 797      tTypedesc = getType(t)
 798    else:
 799      tTypedesc = t
 800  
 801    let tDesc = getType(tTypedesc[1])
 802    if tDesc.kind == nnkBracketExpr:
 803      # tuple
 804      result = tDesc.len - 1
 805    else:
 806      # object
 807      let tParent = parentType(tDesc)
 808      if tParent != nil:
 809        # inherited fields
 810        result += fieldCount(tParent)
 811      for child in tDesc[2].children:
 812        inc(result)
 813        if child.kind == nnkRecCase:
 814          for bIndex in 1..<len(child):
 815            var increment = 0
 816            case child[bIndex].kind
 817            of nnkOfBranch:
 818              let content = child[bIndex][len(child[bIndex])-1]
 819              # We cannot assume that child[bIndex][1] is a RecList due to
 820              # a one-liner like 'of akDog: barkometer' not resulting in a
 821              # RecList but in an Ident node.
 822              case content.kind
 823              of nnkRecList:
 824                increment = len(content)
 825              else:
 826                increment = 1
 827            of nnkElse:
 828              # Same goes for the else branch.
 829              case child[bIndex][0].kind
 830              of nnkRecList:
 831                increment = len(child[bIndex][0])
 832              else:
 833                increment = 1
 834            else:
 835              internalError("Unexpected child kind: " & $child[bIndex].kind)
 836            inc(result, increment)
 837  
 838  macro matchMatrix(t: typedesc): untyped =
 839    let numFields = fieldCount(t)
 840    if numFields == 0:
 841      result = quote do:
 842        (seq[bool])(@[])
 843      return
 844    
 845    result = newNimNode(nnkBracket)
 846    for i in 0..<numFields:
 847      result.add(newLit(false))
 848  
 849  proc checkDuplicate(
 850    s      : NimNode,
 851    tName  : string,
 852    name   : string,
 853    i      : int,
 854    matched: NimNode,
 855    m      : NimNode,
 856  ): NimNode {.compileTime.} =
 857    result = newIfStmt((newNimNode(nnkBracketExpr).add(matched, newLit(i)),
 858        newNimNode(nnkRaiseStmt).add(newCall(bindSym("constructionError"), s, m,
 859        newLit("While constructing " & tName & ": Duplicate field: " &
 860        escape(name))))))
 861  
 862  proc input(ctx: NimNode): NimNode {.compileTime.} =
 863    return newDotExpr(ctx, ident("input"))
 864  
 865  proc hasSparse(t: typedesc): bool {.compileTime.} =
 866    when compiles(t.hasCustomPragma(sparse)):
 867      return t.hasCustomPragma(sparse)
 868    else:
 869      return false
 870  
 871  proc getOptionInner(fType: NimNode): NimNode {.compileTime.} =
 872    if fType.kind == nnkBracketExpr and len(fType) == 2 and
 873        fType[0].kind == nnkSym:
 874      return fType[1]
 875    else: return nil
 876  
 877  proc checkMissing(
 878    s, t   : NimNode,
 879    tName  : string,
 880    field  : NimNode,
 881    i      : int,
 882    matched: NimNode,
 883    o, m   : NimNode,
 884  ):
 885      NimNode {.compileTime.} =
 886    let
 887      fType = getTypeInst(field)
 888      fName = escape($field)
 889      optionInner = getOptionInner(fType)
 890    result = quote do:
 891      when not `o`.`field`.hasCustomPragma(transient):
 892        if not `matched`[`i`]:
 893          when `o`.`field`.hasCustomPragma(defaultVal):
 894            `o`.`field` = `o`.`field`.getCustomPragmaVal(defaultVal)
 895          elif hasSparse(`t`) and `o`.`field` is Option:
 896            `o`.`field` = none[`optionInner`]()
 897          else:
 898            raise constructionError(`s`, `m`, "While constructing " & `tName` &
 899                ": Missing field: " & `fName`)
 900  
 901  proc markAsFound(i: int, matched: NimNode): NimNode {.compileTime.} =
 902    newAssignment(newNimNode(nnkBracketExpr).add(matched, newLit(i)),
 903        newLit(true))
 904  
 905  proc ifNotTransient(
 906    o, field : NimNode,
 907    content  : openarray[NimNode],
 908    elseError: bool,
 909    s, m : NimNode, 
 910    tName: string = "",
 911    fName: string = "",
 912  ):
 913      NimNode {.compileTime.} =
 914    var stmts = newStmtList(content)
 915    if elseError:
 916      result = quote do:
 917        when `o`.`field`.hasCustomPragma(transient):
 918          raise constructionError(`s`, `m`, "While constructing " & `tName` &
 919              ": Field \"" & `fName` & "\" is transient and may not occur in input")
 920        else:
 921          `stmts`
 922    else:
 923      result = quote do:
 924        when not `o`.`field`.hasCustomPragma(transient):
 925          `stmts`
 926  
 927  proc recEnsureAllFieldsPresent(
 928    s, tDecl, o: NimNode,
 929    matched, m : NimNode,
 930    tName: string,
 931    field: var int,
 932    stmt : NimNode,
 933  ) {.compileTime.} =
 934    var
 935      tDesc = getType(tDecl[1])
 936      tParent = parentType(tDesc)
 937    if tParent != nil:
 938      recEnsureAllFieldsPresent(s, tParent, o, matched, m, tName, field, stmt)
 939    for child in tDesc[2].children:
 940      if child.kind == nnkRecCase:
 941        stmt.add(checkMissing(
 942            s, tDecl, tName, child[0], field, matched, o, m))
 943        for bIndex in 1 .. len(child) - 1:
 944          let discChecks = newStmtList()
 945          var
 946            curValues = newNimNode(nnkCurly)
 947            recListIndex = 0
 948          case child[bIndex].kind
 949          of nnkOfBranch:
 950            while recListIndex < child[bIndex].len - 1:
 951              expectKind(child[bIndex][recListIndex], nnkIntLit)
 952              curValues.add(child[bIndex][recListIndex])
 953              inc(recListIndex)
 954          of nnkElse: discard
 955          else: internalError("Unexpected child kind: " & $child[bIndex].kind)
 956          for item in child[bIndex][recListIndex].recListItems:
 957            inc(field)
 958            discChecks.add(checkMissing(
 959                s, tDecl, tName, item, field, matched, o, m))
 960          stmt.add(newIfStmt((infix(newDotExpr(o, newIdentNode($child[0])),
 961              "in", curValues), discChecks)))
 962      else:
 963        stmt.add(checkMissing(s, tDecl, tName, child, field, matched, o, m))
 964      inc(field)
 965  
 966  proc getTypedescType(t: typedesc): NimNode {.compileTime.} =
 967    ## workaround for https://github.com/nim-lang/Nim/issues/23112
 968    result = getType(t)
 969    while result.kind == nnkBracketExpr and result[0].eqIdent"typeDesc":
 970      result = getType(result[1])
 971  
 972  macro ensureAllFieldsPresent(
 973    s      : YamlStream,
 974    t      : typedesc,
 975    o      : typed,
 976    matched: typed,
 977    m      : Mark,
 978  ) =
 979    result = newStmtList()
 980    let
 981      tDecl = getType(t)
 982      tName = $tDecl[1]
 983    var field = 0
 984    recEnsureAllFieldsPresent(s, tDecl, o, matched, m, tName, field, result)
 985  
 986  proc skipOverValue(s: var YamlStream) =
 987      var e = s.next()
 988      var depth = int(e.kind in {yamlStartMap, yamlStartSeq})
 989      while depth > 0:
 990        case s.next().kind
 991        of yamlStartMap, yamlStartSeq: inc(depth)
 992        of yamlEndMap, yamlEndSeq: dec(depth)
 993        of yamlScalar, yamlAlias: discard
 994        else: internalError("Unexpected event kind.")
 995  
 996  proc addFieldCases(
 997    tDecl, context  : NimNode,
 998    name, o, matched: NimNode,
 999    failOnUnknown, m: NimNode,
1000    tName     : string,
1001    caseStmt  : NimNode,
1002    fieldIndex: var int,
1003  ) {.compileTime.} =
1004    var
1005      tDesc = getType(tDecl[1])
1006      tParent = parentType(tDesc)
1007    if tParent != nil:
1008      addFieldCases(tParent, context, name, o, matched, failOnUnknown, m, tName, caseStmt, fieldIndex)
1009    for child in tDesc[2].children:
1010      if child.kind == nnkRecCase:
1011        let
1012          discriminant = newDotExpr(o, newIdentNode($child[0]))
1013          discType = newCall("type", discriminant)
1014        var disOb = newNimNode(nnkOfBranch).add(newStrLitNode($child[0]))
1015        var objConstr = newNimNode(nnkObjConstr).add(newCall("type", o))
1016        objConstr.add(newColonExpr(newIdentNode($child[0]), newIdentNode(
1017            "value")))
1018        for otherChild in tDesc[2].children:
1019          if otherChild == child:
1020            continue
1021          if otherChild.kind != nnkSym:
1022            error("Unexpected kind of field '" & $otherChild[0] &
1023                "': " & $otherChild.kind)
1024          objConstr.add(newColonExpr(newIdentNode($otherChild), newDotExpr(o,
1025              newIdentNode($otherChild))))
1026        disOb.add(newStmtList(
1027            checkDuplicate(input(context), tName, $child[0], fieldIndex, matched, m),
1028            newNimNode(nnkVarSection).add(
1029                newNimNode(nnkIdentDefs).add(
1030                    newIdentNode("value"), discType, newEmptyNode())),
1031            newCall("constructChild", context, newIdentNode("value")),
1032            newAssignment(o, objConstr),
1033            markAsFound(fieldIndex, matched)))
1034        caseStmt.add(disOb)
1035        var alreadyUsedSet = newNimNode(nnkCurly)
1036        for bIndex in 1 .. len(child) - 1:
1037          var recListIndex = 0
1038          var discTest: NimNode
1039          case child[bIndex].kind
1040          of nnkOfBranch:
1041            discTest = newNimNode(nnkCurly)
1042            while recListIndex < child[bIndex].len - 1:
1043              yAssert child[bIndex][recListIndex].kind == nnkIntLit
1044              discTest.add(child[bIndex][recListIndex])
1045              alreadyUsedSet.add(child[bIndex][recListIndex])
1046              inc(recListIndex)
1047            discTest = infix(discriminant, "in", discTest)
1048          of nnkElse:
1049            discTest = infix(discriminant, "notin", alreadyUsedSet)
1050          else:
1051            internalError("Unexpected child kind: " & $child[bIndex].kind)
1052  
1053          for item in child[bIndex][recListIndex].recListItems:
1054            inc(fieldIndex)
1055            yAssert item.kind == nnkSym
1056            var ob = newNimNode(nnkOfBranch).add(newStrLitNode($item))
1057            let field = newDotExpr(o, newIdentNode($item))
1058            var ifStmt = newIfStmt((cond: discTest, body: newStmtList(
1059                newCall("constructChild", context, field))))
1060            ifStmt.add(newNimNode(nnkElse).add(newNimNode(nnkRaiseStmt).add(
1061                newCall(bindSym("constructionError"), input(context), m,
1062                infix(newStrLitNode("Field " & $item & " not allowed for " &
1063                $child[0] & " == "), "&", prefix(discriminant, "$"))))))
1064            ob.add(ifNotTransient(o, item,
1065                [checkDuplicate(input(context), tName, $item, fieldIndex, matched, m),
1066                ifStmt, markAsFound(fieldIndex, matched)], true,
1067                input(context), m, tName, $item))
1068            caseStmt.add(ob)
1069      else:
1070        yAssert child.kind == nnkSym
1071        var ob = newNimNode(nnkOfBranch).add(newStrLitNode($child))
1072        let field = newDotExpr(o, newIdentNode($child))
1073        ob.add(ifNotTransient(o, child,
1074            [checkDuplicate(input(context), tName, $child, fieldIndex, matched, m),
1075            newCall("constructChild", context, field),
1076            markAsFound(fieldIndex, matched)], true, input(context), m, tName, $child))
1077        caseStmt.add(ob)
1078      inc(fieldIndex)
1079  
1080  macro constructFieldValue(
1081    t: typedesc, 
1082    context, name, o, matched: untyped,
1083    failOnUnknown: bool,
1084    m: untyped,
1085  ) =
1086    let
1087      tDecl = getType(t)
1088      tName = $tDecl[1]
1089    result = newStmtList()
1090    var caseStmt = newNimNode(nnkCaseStmt).add(name)
1091    var fieldIndex = 0
1092    addFieldCases(tDecl, context, name, o, matched, failOnUnknown, m, tName, caseStmt, fieldIndex)
1093    caseStmt.add(newNimNode(nnkElse).add(newNimNode(nnkWhenStmt).add(
1094      newNimNode(nnkElifBranch).add(failOnUnknown,
1095        newNimNode(nnkRaiseStmt).add(
1096          newCall(bindSym("constructionError"), input(context), m,
1097          infix(newLit("While constructing " & tName & ": Unknown field: "), "&",
1098          newCall(bindSym("escape"), name)))))
1099    ).add(newNimNode(nnkElse).add(
1100      newCall(bindSym("skipOverValue"), input(context))
1101    ))))
1102    result.add(caseStmt)
1103  
1104  proc isVariantObject(t: NimNode): bool {.compileTime.} =
1105    var
1106      tResolved: NimNode
1107      tDesc: NimNode
1108    if t.kind == nnkSym:
1109      tResolved = getType(t)
1110    else:
1111      tResolved = t
1112    if tResolved.kind == nnkBracketExpr and tResolved[0].strVal == "typeDesc":
1113      tDesc = getType(tResolved[1])
1114    else:
1115      tDesc = tResolved
1116    if tDesc.kind != nnkObjectTy: return false
1117    let tParent = parentType(tDesc)
1118    if tParent != nil:
1119      if isVariantObject(tParent): return true
1120    for child in tDesc[2].children:
1121      if child.kind == nnkRecCase: return true
1122    return false
1123  
1124  proc hasIgnore(t: typedesc): bool {.compileTime.} =
1125    when compiles(t.hasCustomPragma(ignore)):
1126      return t.hasCustomPragma(ignore)
1127    else:
1128      return false
1129  
1130  proc constructObjectDefault*(
1131    ctx   : var ConstructionContext,
1132    result: var RootObj,
1133  ) =
1134    # specialization of generic proc for RootObj, doesn't do anything
1135    return
1136  
1137  proc constructObjectDefault*[O: object|tuple](
1138    ctx   : var ConstructionContext,
1139    result: var O,
1140  ) {.raises: [YamlConstructionError, YamlStreamError].} =
1141    ## Constructs a Nim object or tuple from a YAML mapping.
1142    ## This is the default implementation for custom objects and tuples and should
1143    ## not be redefined. If you are adding a custom constructObject()
1144    ## implementation, you can use this proc to call the default implementation
1145    ## within it.
1146    var matched = matchMatrix(O)
1147    var e = ctx.input.next()
1148    const
1149      startKind = when isVariantObject(getType(O)): yamlStartSeq else: yamlStartMap
1150      endKind = when isVariantObject(getType(O)): yamlEndSeq else: yamlEndMap
1151    if e.kind != startKind:
1152      raise ctx.input.constructionError(
1153        e.startPos,
1154        "While constructing " & typetraits.name(O) &
1155        ": Expected " & $startKind & ", got " & $e.kind
1156      )
1157    let startPos = e.startPos
1158    when hasIgnore(O):
1159      const ignoredKeyList = O.getCustomPragmaVal(ignore)
1160      const failOnUnknown = len(ignoredKeyList) > 0
1161    else:
1162      const failOnUnknown = true
1163    while ctx.input.peek.kind != endKind:
1164      e = ctx.input.next()
1165      when isVariantObject(getType(O)):
1166        if e.kind != yamlStartMap:
1167          raise ctx.input.constructionError(
1168            e.startPos, "Expected single-pair map, got " & $e.kind
1169          )
1170        e = ctx.input.next()
1171      if e.kind != yamlScalar:
1172        raise ctx.input.constructionError(
1173          e.startPos, "Expected field name, got " & $e.kind
1174        )
1175      let name = e.scalarContent
1176      when result is tuple:
1177        var i = 0
1178        var found = false
1179        for fname, value in fieldPairs(result):
1180          if fname == name:
1181            if matched[i]:
1182              raise ctx.input.constructionError(
1183                e.startPos, "While constructing " &
1184                typetraits.name(O) & ": Duplicate field: " & escape(name)
1185              )
1186            ctx.constructChild(value)
1187            matched[i] = true
1188            found = true
1189            break
1190          inc(i)
1191        when failOnUnknown:
1192          if not found:
1193            raise ctx.input.constructionError(
1194              e.startPos, "While constructing " &
1195              typetraits.name(O) & ": Unknown field: " & escape(name)
1196            )
1197      else:
1198        when hasIgnore(O) and failOnUnknown:
1199          if name notin ignoredKeyList:
1200            constructFieldValue(O, ctx, name, result, matched, failOnUnknown, e.startPos)
1201          else:
1202            skipOverValue(ctx.input)
1203        else:
1204          constructFieldValue(O, ctx, name, result, matched, failOnUnknown, e.startPos)
1205      when isVariantObject(getType(O)):
1206        e = ctx.input.next()
1207        if e.kind != yamlEndMap:
1208          raise ctx.input.constructionError(
1209            e.startPos, "Expected end of single-pair map, got " & $e.kind
1210          )
1211    discard ctx.input.next()
1212    when result is tuple:
1213      var i = 0
1214      for fname, value in fieldPairs(result):
1215        if not matched[i]:
1216          raise ctx.input.constructionError(startPos, "While constructing " &
1217              typetraits.name(O) & ": Missing field: " & escape(fname))
1218        inc(i)
1219    else: ensureAllFieldsPresent(ctx.input, O, result, matched, startPos)
1220  
1221  proc constructObject*[O: object|tuple](
1222    ctx   : var ConstructionContext,
1223    result: var O,
1224  ) {.raises: [YamlConstructionError, YamlStreamError].} =
1225    ## Overridable default implementation for custom object and tuple types
1226    ctx.constructObjectDefault(result)
1227  
1228  proc recGenFieldRepresenters(
1229    tDecl, value: NimNode,
1230    isVO        : bool,
1231    fieldIndex  : var int16,
1232    result      : NimNode,
1233  ) {.compileTime.} =
1234    let
1235      tDesc = getType(tDecl[1])
1236      tParent = parentType(tDesc)
1237    if tParent != nil:
1238      recGenFieldRepresenters(tParent, value, isVO, fieldIndex, result)
1239    for child in tDesc[2].children:
1240      if child.kind == nnkRecCase:
1241        let
1242          fieldName = $child[0]
1243          fieldAccessor = newDotExpr(value, newIdentNode(fieldName))
1244        result.add(quote do:
1245          ctx.put(startMapEvent())
1246          ctx.put(scalarEvent(
1247            `fieldName`,
1248            tag = if ctx.emitTag: yTagNimField else: yTagQuestionMark
1249          ))
1250          when `fieldAccessor`.hasCustomPragma(scalar):
1251            ctx.overridingScalarStyle = `fieldAccessor`.getCustomPragmaVal(scalar)
1252          when `fieldAccessor`.hasCustomPragma(collection):
1253            ctx.overridingCollectionStyle = `fieldAccessor`.getCustomPragmaVal(collection)
1254          ctx.representChild(`fieldAccessor`)
1255          ctx.put(endMapEvent())
1256        )
1257        let enumName = $getTypeInst(child[0])
1258        var caseStmt = newNimNode(nnkCaseStmt).add(fieldAccessor)
1259        for bIndex in 1 .. len(child) - 1:
1260          var curBranch: NimNode
1261          var recListIndex = 0
1262          case child[bIndex].kind
1263          of nnkOfBranch:
1264            curBranch = newNimNode(nnkOfBranch)
1265            while recListIndex < child[bIndex].len - 1:
1266              expectKind(child[bIndex][recListIndex], nnkIntLit)
1267              curBranch.add(newCall(enumName, newLit(child[bIndex][recListIndex].intVal)))
1268              inc(recListIndex)
1269          of nnkElse:
1270            curBranch = newNimNode(nnkElse)
1271          else:
1272            internalError("Unexpected child kind: " & $child[bIndex].kind)
1273          var curStmtList = newStmtList()
1274          if child[bIndex][recListIndex].recListLen > 0:
1275            for item in child[bIndex][recListIndex].recListItems():
1276              inc(fieldIndex)
1277              let
1278                name = $item
1279                itemAccessor = newDotExpr(value, newIdentNode(name))
1280              curStmtList.add(quote do:
1281                when not `itemAccessor`.hasCustomPragma(transient):
1282                  ctx.put(startMapEvent())
1283                  ctx.put(scalarEvent(
1284                    `name`,
1285                    tag = if ctx.emitTag: yTagNimField else: yTagQuestionMark 
1286                  ))
1287                  when `itemAccessor`.hasCustomPragma(scalar):
1288                    ctx.overridingScalarStyle = `itemAccessor`.getCustomPragmaVal(scalar)
1289                  when `itemAccessor`.hasCustomPragma(collection):
1290                    ctx.overridingCollectionStyle = `itemAccessor`.getCustomPragmaVal(collection)
1291                  ctx.representChild(`itemAccessor`)
1292                  ctx.put(endMapEvent())
1293              )
1294          else:
1295            curStmtList.add(newNimNode(nnkDiscardStmt).add(newEmptyNode()))
1296          curBranch.add(curStmtList)
1297          caseStmt.add(curBranch)
1298        result.add(caseStmt)
1299      else:
1300        let
1301          name = $child
1302          templName = genSym(nskTemplate)
1303          childAccessor = newDotExpr(value, newIdentNode(name))
1304        result.add(quote do:
1305          template `templName` {.used.} =
1306            when bool(`isVO`): ctx.put(startMapEvent())
1307            ctx.put(scalarEvent(
1308              `name`,
1309              if ctx.emitTag: yTagNimField else: yTagQuestionMark,
1310              yAnchorNone
1311            ))
1312            when `childAccessor`.hasCustomPragma(scalar):
1313              ctx.overridingScalarStyle = `childAccessor`.getCustomPragmaVal(scalar)
1314            when `childAccessor`.hasCustomPragma(collection):
1315              ctx.overridingCollectionStyle = `childAccessor`.getCustomPragmaVal(collection)
1316            ctx.representChild(`childAccessor`)
1317            when bool(`isVO`): ctx.put(endMapEvent())
1318          when not `childAccessor`.hasCustomPragma(transient):
1319            when hasSparse(`tDecl`) and `child` is Option:
1320              if `childAccessor`.isSome: `templName`()
1321            else:
1322              `templName`()
1323        )
1324      inc(fieldIndex)
1325  
1326  macro genRepresentObject(t: typedesc, value) =
1327    result = newStmtList()
1328    let
1329      tDecl = getType(t)
1330      isVO  = isVariantObject(t)
1331    var fieldIndex = 0'i16
1332    recGenFieldRepresenters(tDecl, value, isVO, fieldIndex, result)
1333  
1334  proc representObject*[O: object](
1335    ctx  : var SerializationContext,
1336    value: O,
1337    tag  : Tag,
1338  ) {.raises: [YamlSerializationError].} =
1339    ## represents a Nim object or tuple as YAML mapping
1340    when isVariantObject(getType(O)):
1341      ctx.put(startSeqEvent(tag = tag, style = ctx.collectionStyleFor(O)))
1342    else:
1343      ctx.put(startMapEvent(tag = tag, style = ctx.collectionStyleFor(O)))
1344    genRepresentObject(O, value)
1345    when isVariantObject(getType(O)): ctx.put(endSeqEvent())
1346    else: ctx.put(endMapEvent())
1347  
1348  proc representObject*[O: tuple](
1349    ctx  : var SerializationContext,
1350    value: O,
1351    tag  : Tag,
1352  ) {.raises: [YamlSerializationError].} =
1353    var fieldIndex = 0'i16
1354    ctx.put(startMapEvent(tag = tag, style = ctx.collectionStyleFor(O)))
1355    for name, fvalue in fieldPairs(value):
1356      ctx.put(scalarEvent(
1357        name,
1358        tag = if ctx.emitTag: yTagNimField else: yTagQuestionMark
1359      ))
1360      ctx.representChild(fvalue)
1361      inc(fieldIndex)
1362    ctx.put(endMapEvent())
1363  
1364  proc constructObject*[O: enum](
1365    ctx   : var ConstructionContext,
1366    result: var O,
1367  ) {.raises: [YamlConstructionError, YamlStreamError].} =
1368    ## constructs a Nim enum from a YAML scalar
1369    let e = ctx.input.next()
1370    if e.kind != yamlScalar:
1371      raise ctx.input.constructionError(
1372        e.startPos, "Expected scalar, got " & $e.kind
1373      )
1374    try: result = parseEnum[O](e.scalarContent)
1375    except ValueError as ve:
1376      var ex = ctx.input.constructionError(e.startPos, "Cannot parse '" &
1377        escape(e.scalarContent) & "' as " & type(O).name
1378      )
1379      ex.parent = ve
1380      raise ex
1381  
1382  proc representObject*[O: enum](
1383    ctx  : var SerializationContext,
1384    value: O,
1385    tag  : Tag,
1386  ) {.raises: [].} =
1387    ## represents a Nim enum as YAML scalar
1388    ctx.put(scalarEvent($value, tag, yAnchorNone, ctx.scalarStyleFor(O)))
1389  
1390  proc yamlTag*[O](T: typedesc[ref O]): Tag {.inline, raises: [].} = yamlTag(O)
1391  
1392  macro constructImplicitVariantObject(
1393    m, c, r, possibleTags: untyped,
1394    t: typedesc,
1395  ) =
1396    let tDesc = getType(getType(t)[1])
1397    yAssert tDesc.kind == nnkObjectTy
1398    let recCase = tDesc[2][0]
1399    yAssert recCase.kind == nnkRecCase
1400    result = newNimNode(nnkIfStmt)
1401    for i in 1 .. recCase.len - 1:
1402      yAssert recCase[i].kind == nnkOfBranch
1403      var branch = newNimNode(nnkElifBranch)
1404      var branchContent = newStmtList(newAssignment(r,
1405          newNimNode(nnkObjConstr).add(
1406            newCall("type", r),
1407            newColonExpr(newIdentNode($recCase[0]), recCase[i][0])
1408      )))
1409      case recCase[i][1].recListLen
1410      of 0:
1411        branch.add(infix(newIdentNode("yTagNull"), "in", possibleTags))
1412        branchContent.add(
1413          newNimNode(nnkDiscardStmt).add(newCall("next", input(c))))
1414      of 1:
1415        let field = newDotExpr(r, newIdentNode($recCase[i][1].recListNode))
1416        branch.add(infix(
1417            newCall("yamlTag", newCall("type", field)), "in", possibleTags))
1418        branchContent.add(newCall("constructChild", c, field))
1419      else:
1420        block:
1421          internalError("Too many children: " & $recCase[i][1].recListlen)
1422      branch.add(branchContent)
1423      result.add(branch)
1424    let raiseStmt = newNimNode(nnkRaiseStmt).add(
1425        newCall(bindSym("constructionError"), input(c), m,
1426        infix(newStrLitNode("This value type does not map to any field in " &
1427                            getTypeImpl(t)[1].repr & ": "), "&",
1428              newCall("$", newNimNode(nnkBracketExpr).add(possibleTags, newIntLitNode(0)))
1429        )
1430    ))
1431    result.add(newNimNode(nnkElse).add(newNimNode(nnkTryStmt).add(
1432        newStmtList(raiseStmt), newNimNode(nnkExceptBranch).add(
1433          newIdentNode("KeyError"),
1434          newNimNode(nnkDiscardStmt).add(newEmptyNode())
1435    ))))
1436  
1437  proc isImplicitVariantObject(t: typedesc): bool {.compileTime.} =
1438    when compiles(t.hasCustomPragma(implicit)):
1439      return t.hasCustomPragma(implicit)
1440    else:
1441      return false
1442  
1443  proc canBeImplicit(t: typedesc): string {.compileTime.} =
1444    ## returns empty string if type can be implicit, else the reason why it can't
1445    let tDesc = getTypedescType(t)
1446    if tDesc.kind != nnkObjectTy: return "type is not an object"
1447    if tDesc[2].len != 1 or tDesc[2][0].kind != nnkRecCase:
1448      return "type doesn't exclusively contain record case"
1449    var foundEmptyBranch = false
1450    for i in 1.. tDesc[2][0].len - 1:
1451      case tDesc[2][0][i][1].recListlen # branch contents
1452      of 0:
1453        if foundEmptyBranch: return "record case has more than one empty branch"
1454        else: foundEmptyBranch = true
1455      of 1: discard
1456      else: return "record case contains more than one field"
1457  
1458  proc constructChild*[T](
1459    ctx   : var ConstructionContext,
1460    result: var T,
1461  ) =
1462    let item = ctx.input.peek()
1463    when isImplicitVariantObject(T):
1464      const checkRes = canBeImplicit(T)
1465      when len(checkRes) > 0:
1466        {. fatal: "cannot be marked as implicit: " & checkRes .}
1467      var possibleTags = newSeq[Tag]()
1468      case item.kind
1469      of yamlScalar:
1470        case item.scalarProperties.tag
1471        of yTagQuestionMark:
1472          case guessType(item.scalarContent)
1473          of yTypeInteger:
1474            possibleTags.add([yamlTag(int), yamlTag(int8), yamlTag(int16),
1475                                yamlTag(int32), yamlTag(int64)])
1476            if item.scalarContent[0] != '-':
1477              possibleTags.add([yamlTag(uint), yamlTag(uint8), yamlTag(uint16),
1478                                  yamlTag(uint32), yamlTag(uint64)])
1479          of yTypeFloat, yTypeFloatInf, yTypeFloatNaN:
1480            possibleTags.add([yamlTag(float), yamlTag(float32),
1481                                yamlTag(float64)])
1482          of yTypeBoolTrue, yTypeBoolFalse:
1483            possibleTags.add(yamlTag(bool))
1484          of yTypeNull:
1485            raise ctx.input.constructionError(item.startPos, "not implemented!")
1486          of yTypeUnknown:
1487            possibleTags.add(yamlTag(string))
1488          of yTypeTimestamp:
1489            possibleTags.add(yamlTag(Time))
1490        of yTagExclamationMark:
1491          possibleTags.add(yamlTag(string))
1492        else:
1493          possibleTags.add(item.scalarProperties.tag)
1494      of yamlStartMap:
1495        if item.mapProperties.tag in [yTagQuestionMark, yTagExclamationMark]:
1496          raise ctx.input.constructionError(item.startPos,
1497              "Complex value of implicit variant object type must have a tag.")
1498        possibleTags.add(item.mapProperties.tag)
1499      of yamlStartSeq:
1500        if item.seqProperties.tag in [yTagQuestionMark, yTagExclamationMark]:
1501          raise ctx.input.constructionError(item.startPos,
1502              "Complex value of implicit variant object type must have a tag.")
1503        possibleTags.add(item.seqProperties.tag)
1504      of yamlAlias:
1505        raise ctx.input.constructionError(item.startPos,
1506            "cannot load non-ref value from alias node")
1507      else: internalError("Unexpected item kind: " & $item.kind)
1508      constructImplicitVariantObject(item.startPos, ctx, result, possibleTags, T)
1509    else:
1510      case item.kind
1511      of yamlScalar:
1512        if item.scalarProperties.tag notin [yTagQuestionMark, yTagExclamationMark,
1513                                 yamlTag(T)]:
1514          raise ctx.input.constructionError(
1515            item.startPos, "Wrong tag for " & typetraits.name(T) & ": " & $item.scalarProperties.tag)
1516        elif item.scalarProperties.anchor != yAnchorNone:
1517          raise ctx.input.constructionError(item.startPos, "Anchor on non-ref type")
1518      of yamlStartMap:
1519        if item.mapProperties.tag notin [yTagQuestionMark, yamlTag(T)]:
1520          raise ctx.input.constructionError(
1521            item.startPos, "Wrong tag for " & typetraits.name(T) & ": " & $item.mapProperties.tag)
1522        elif item.mapProperties.anchor != yAnchorNone:
1523          raise ctx.input.constructionError(item.startPos, "Anchor on non-ref type")
1524      of yamlStartSeq:
1525        if item.seqProperties.tag notin [yTagQuestionMark, yamlTag(T)]:
1526          raise ctx.input.constructionError(
1527            item.startPos, "Wrong tag for " & typetraits.name(T) & ": " & $item.seqProperties.tag)
1528        elif item.seqProperties.anchor != yAnchorNone:
1529          raise ctx.input.constructionError(item.startPos, "Anchor on non-ref type")
1530      of yamlAlias:
1531        raise ctx.input.constructionError(item.startPos,
1532            "cannot load non-ref value from alias node")
1533      else: internalError("Unexpected item kind: " & $item.kind)
1534      ctx.constructObject(result)
1535  
1536  proc constructChild*(
1537    ctx   : var ConstructionContext,
1538    result: var string,
1539  ) =
1540    let item = ctx.input.peek()
1541    if item.kind == yamlScalar:
1542      if item.scalarProperties.tag notin
1543          [yTagQuestionMark, yTagExclamationMark, yamlTag(string)]:
1544        raise ctx.input.constructionError(
1545          item.startPos, "Wrong tag for string: " & $item.scalarProperties.tag)
1546      elif item.scalarProperties.anchor != yAnchorNone:
1547        raise ctx.input.constructionError(item.startPos, "Anchor on non-ref type")
1548    ctx.constructObject(result)
1549  
1550  proc constructChild*[T](
1551    ctx   : var ConstructionContext,
1552    result: var seq[T],
1553  ) =
1554    let item = ctx.input.peek()
1555    if item.kind == yamlStartSeq:
1556      if item.seqProperties.tag notin [yTagQuestionMark, yamlTag(seq[T])]:
1557        raise ctx.input.constructionError(
1558          item.startPos, "Wrong tag for " & typetraits.name(seq[T]) & ": " & $item.seqProperties.tag)
1559      elif item.seqProperties.anchor != yAnchorNone:
1560        raise ctx.input.constructionError(item.startPos, "Anchor on non-ref type")
1561    ctx.constructObject(result)
1562  
1563  proc constructChild*[I, T](
1564    ctx   : var ConstructionContext,
1565    result: var array[I, T],
1566  ) =
1567    let item = ctx.input.peek()
1568    if item.kind == yamlStartSeq:
1569      if item.seqProperties.tag notin [yTagQuestionMark, yamlTag(array[I, T])]:
1570        raise ctx.input.constructionError(
1571          item.startPos, "Wrong tag for " & typetraits.name(array[I, T]) & ": " & $item.seqProperties.tag)
1572      elif item.seqProperties.anchor != yAnchorNone:
1573        raise ctx.input.constructionError(item.startPos, "Anchor on non-ref type")
1574    ctx.constructObject(result)
1575  
1576  proc constructChild*[T](
1577    ctx   : var ConstructionContext,
1578    result: var Option[T],
1579  ) =
1580    ## constructs an optional value. A value with a !!null tag will be loaded
1581    ## an empty value.
1582    let event = ctx.input.peek()
1583    if event.kind == yamlScalar and event.scalarProperties.tag == yTagNull:
1584      result = none(T)
1585      discard ctx.input.next()
1586    else:
1587      var inner: T
1588      ctx.constructChild(inner)
1589      result = some(inner)
1590  
1591  when defined(JS):
1592    # in JS, Time is a ref type. Therefore, we need this specialization so that
1593    # it is not handled by the general ref-type handler.
1594    proc constructChild*(
1595      ctx   : var ConstructionContext,
1596      result: var Time,
1597    ) =
1598      let e = ctx.input.peek()
1599      if e.kind == yamlScalar:
1600        if e.scalarProperties.tag notin [yTagQuestionMark, yTagTimestamp]:
1601          raise ctx.input.constructionError(e.startPos, "Wrong tag for Time: " & $e.scalarProperties.tag)
1602        elif guessType(e.scalarContent) != yTypeTimestamp:
1603          raise ctx.input.constructionError(e.startPos, "Invalid timestamp")
1604        elif e.scalarProperties.anchor != yAnchorNone:
1605          raise ctx.input.constructionError(e.startPos, "Anchor on non-ref type")
1606        ctx.constructObject(result)
1607      else:
1608        raise ctx.input.constructionError(e.startPos, "Unexpected structure, expected timestamp")
1609  
1610  proc constructChild*[O](
1611    ctx   : var ConstructionContext,
1612    result: var ref O,
1613  ) =
1614    var e = ctx.input.peek()
1615    if e.kind == yamlScalar:
1616      let props = e.scalarProperties
1617      if props.tag == yTagNull or (props.tag == yTagQuestionMark and
1618          guessType(e.scalarContent) == yTypeNull):
1619        result = nil
1620        discard ctx.input.next()
1621        return
1622    elif e.kind == yamlAlias:
1623      when nimvm:
1624        raise ctx.input.constructionError(e.startPos,
1625          "aliases are not supported at compile time")
1626      else:
1627        let val = ctx.refs.getOrDefault(e.aliasTarget, (yTagNull, pointer(nil)))
1628        if val.p == nil:
1629          raise ctx.input.constructionError(e.startPos,
1630            "alias node refers to anchor in ignored scope")
1631        if val.tag != yamlTag(O):
1632          raise ctx.input.constructionError(e.startPos,
1633            "alias node refers to object of incompatible type")
1634        result = cast[ref O](val.p)
1635        discard ctx.input.next()
1636        return
1637    new(result)
1638    template removeAnchor(anchor: var Anchor) {.dirty.} =
1639      if anchor != yAnchorNone:
1640        yAssert(not ctx.refs.hasKey(anchor))
1641        when nimvm: discard # no aliases supported at compile time
1642        else: ctx.refs[anchor] = (yamlTag(O), cast[pointer](result))
1643        anchor = yAnchorNone
1644  
1645    case e.kind
1646    of yamlScalar: removeAnchor(e.scalarProperties.anchor)
1647    of yamlStartMap: removeAnchor(e.mapProperties.anchor)
1648    of yamlStartSeq: removeAnchor(e.seqProperties.anchor)
1649    else: internalError("Unexpected event kind: " & $e.kind)
1650    ctx.input.peek = e
1651    try: ctx.constructChild(result[])
1652    except YamlConstructionError as e: raise e
1653    except YamlStreamError as e: raise e
1654    except CatchableError as ce:
1655      var e = newException(YamlStreamError, ce.msg)
1656      e.parent = ce
1657      raise e
1658  
1659  proc representChild*(
1660    ctx  : var SerializationContext,
1661    value: string,
1662  ) =
1663    let tag = ctx.presentTag(string)
1664    ctx.representObject(
1665      value,
1666      if tag == yTagQuestionMark and guessType(value) != yTypeUnknown:
1667        yTagExclamationMark
1668      else:
1669        tag
1670    )
1671  
1672  proc representChild*[T](
1673    ctx  : var SerializationContext,
1674    value: seq[T],
1675  ) {.raises: [YamlSerializationError].} =
1676    ctx.representObject(value, ctx.presentTag(seq[T]))
1677  
1678  proc representChild*[I, T](
1679    ctx  : var SerializationContext,
1680    value: array[I, T],
1681  ) {.raises: [YamlSerializationError].} =
1682    ctx.representObject(value, ctx.presentTag(array[I, T]))
1683  
1684  proc representChild*[O](
1685    ctx  : var SerializationContext,
1686    value: ref O,
1687  ) =
1688    if isNil(value): ctx.put(scalarEvent("~", yTagNull, style = ctx.scalarStyleFor(O)))
1689    else:
1690      when nimvm: discard
1691      else:
1692        let p = cast[pointer](value)
1693        # when c.anchorStyle == asNone, `referenced` is used as indicator that we are
1694        # currently in the process of serializing this node. This enables us to
1695        # detect cycles and raise an error.
1696        var val = ctx.refs.getOrDefault(
1697          p, (ctx.nextAnchorId.Anchor, ctx.options.anchorStyle == asNone)
1698        )
1699        if val.a != ctx.nextAnchorId.Anchor:
1700          if ctx.options.anchorStyle == asNone:
1701            if val.referenced:
1702              raise newException(YamlSerializationError,
1703                  "tried to serialize cyclic graph with asNone")
1704          else:
1705            val = ctx.refs.getOrDefault(p)
1706            yAssert(val.a != yAnchorNone)
1707            if not val.referenced:
1708              ctx.refs[p] = (val.a, true)
1709            ctx.put(aliasEvent(val.a))
1710            ctx.overridingScalarStyle = ssAny
1711            ctx.overridingCollectionStyle = csAny
1712            return
1713        ctx.refs[p] = val
1714        nextAnchor(ctx.nextAnchorId, len(ctx.nextAnchorId) - 1)
1715        let origPut = ctx.putImpl
1716        ctx.putImpl = proc(ctx: var SerializationContext, e: Event) =
1717          var ex = e
1718          case ex.kind
1719          of yamlStartMap:
1720            if ctx.options.anchorStyle != asNone: ex.mapProperties.anchor = val.a
1721          of yamlStartSeq:
1722            if ctx.options.anchorStyle != asNone: ex.seqProperties.anchor = val.a
1723          of yamlScalar:
1724            if ctx.options.anchorStyle != asNone: ex.scalarProperties.anchor = val.a
1725            if not ctx.emitTag and guessType(ex.scalarContent) != yTypeNull:
1726              ex.scalarProperties.tag = yTagQuestionMark
1727          else: discard
1728          ctx.putImpl = origPut
1729          ctx.put(ex)
1730      ctx.representChild(value[])
1731      when nimvm: discard
1732      else:
1733        if ctx.options.anchorStyle == asNone: ctx.refs[p] = (val.a, false)
1734  
1735  proc representChild*[T](
1736    ctx  : var SerializationContext,
1737    value: Option[T],
1738  ) {.raises: [YamlSerializationError].} =
1739    ## represents an optional value. If the value is missing, a !!null scalar
1740    ## will be produced.
1741    if value.isSome:
1742      ctx.representChild(value.get())
1743    else:
1744      ctx.put(scalarEvent("~", yTagNull, style = ctx.scalarStyleFor(Option[T])))
1745  
1746  proc representChild*[O](
1747    ctx  : var SerializationContext,
1748    value: O,
1749  ) =
1750    when isImplicitVariantObject(O):
1751      # todo: this would probably be nicer if constructed with a macro
1752      var count = 0
1753      for name, field in fieldPairs(value):
1754        if count > 0: ctx.representChild(field)
1755        inc(count)
1756      if count == 1: ctx.put(scalarEvent("~", yTagNull))
1757    else:
1758      ctx.representObject(value, ctx.presentTag(O))
1759  
1760  proc construct*[T](
1761    input : var YamlStream,
1762    target: var T,
1763  )
1764      {.raises: [YamlStreamError, YamlConstructionError].} =
1765    ## Constructs a Nim value from a YAML stream.
1766    var context = initConstructionContext(input)
1767    try:
1768      var e = input.next()
1769      yAssert(e.kind == yamlStartDoc)
1770  
1771      context.constructChild(target)
1772      e = input.next()
1773      yAssert(e.kind == yamlEndDoc)
1774    except YamlConstructionError as e: raise e
1775    except YamlStreamError as e: raise e
1776    except CatchableError as ce:
1777      # may occur while calling ctx.input()
1778      var ex = newException(YamlStreamError, "error occurred while constructing")
1779      ex.parent = ce
1780      raise ex
1781  
1782  proc represent*[T](
1783    value  : T,
1784    options: SerializationOptions = SerializationOptions(),
1785  ): YamlStream =
1786    ## Represents a Nim value as ``YamlStream``
1787    var
1788      bys = newBufferYamlStream()
1789      context = initSerializationContext(
1790        options,
1791        proc(ctx: var SerializationContext, e: Event) = bys.put(e)
1792      )
1793    bys.put(startStreamEvent())
1794    bys.put(startDocEvent(handles = options.handles))
1795    context.representChild(value)
1796    bys.put(endDocEvent())
1797    bys.put(endStreamEvent())
1798    if options.anchorStyle == asTidy:
1799      var ctx = initAnchorContext()
1800      for item in bys.mitems():
1801        case item.kind
1802        of yamlStartMap: ctx.process(item.mapProperties, context.refs)
1803        of yamlStartSeq: ctx.process(item.seqProperties, context.refs)
1804        of yamlScalar: ctx.process(item.scalarProperties, context.refs)
1805        of yamlAlias: item.aliasTarget = ctx.map(item.aliasTarget)
1806        else: discard
1807    result = bys
1808