/ yaml / dom.nim
dom.nim
  1  #            NimYAML - YAML implementation in Nim
  2  #        (c) Copyright 2015-2023 Felix Krause
  3  #
  4  #    See the file "copying.txt", included in this
  5  #    distribution, for details about the copyright.
  6  
  7  ## ===============
  8  ## Module yaml/dom
  9  ## ===============
 10  ##
 11  ## This is the DOM API, which enables you to load YAML into a tree-like
 12  ## structure. It can also dump the structure back to YAML. Formally, it
 13  ## represents the *Representation Graph* as defined in the YAML specification.
 14  ##
 15  ## You use this API by importing this module alongside loading and/or dumping,
 16  ## and then call load() / dump() on YamlNode values. A special function
 17  ## ``loadFlattened`` allows you to load aliases in the YAML input as if they
 18  ## were copies of the referenced subtree.
 19  ##
 20  ## The ``YamlNode`` objects in the DOM are structured similarly to the ``JsonNode``
 21  ## objects of Nim's `json module <http://nim-lang.org/docs/json.html>`_.
 22  
 23  import std / [tables, streams, hashes, sets, strutils]
 24  import data, stream, taglib, private/internal, parser, loading
 25  
 26  when defined(gcArc) and not defined(gcOrc):
 27    {.error: "NimYAML's DOM API only supports ORC because ARC can't deal with cycles".}
 28  
 29  when defined(nimNoNil):
 30      {.experimental: "notnil".}
 31  type
 32    YamlNodeKind* = enum
 33      yScalar, yMapping, ySequence
 34  
 35    YamlNode* = ref YamlNodeObj
 36      ## Represents a node in a YAML document
 37  
 38    YamlNodeObj* = object
 39      tag*: Tag
 40      startPos*, endPos*: Mark
 41      case kind*: YamlNodeKind
 42      of yScalar:
 43        content*    : string
 44        scalarStyle*: ScalarStyle
 45      of ySequence:
 46        elems*   : seq[YamlNode]
 47        seqStyle*: CollectionStyle
 48      of yMapping:
 49        fields*  : TableRef[YamlNode, YamlNode]
 50        mapStyle*: CollectionStyle
 51        # compiler does not like Table[YamlNode, YamlNode]
 52  
 53  proc hash*(o: YamlNode): Hash =
 54    result = o.tag.hash
 55    case o.kind
 56    of yScalar: result = result !& o.content.hash
 57    of yMapping:
 58      for key, value in o.fields.pairs:
 59        result = result !& key.hash !& value.hash
 60    of ySequence:
 61      for item in o.elems:
 62        result = result !& item.hash
 63    result = !$result
 64  
 65  proc eqImpl(x, y: YamlNode, alreadyVisited: var HashSet[pointer]): bool =
 66    template compare(a, b: YamlNode) {.dirty.} =
 67      if cast[pointer](a) != cast[pointer](b):
 68        if cast[pointer](a) in alreadyVisited and
 69            cast[pointer](b) in alreadyVisited:
 70          # prevent infinite loop!
 71          return false
 72        elif a != b: return false
 73  
 74    if x.kind != y.kind or x.tag != y.tag: return false
 75    alreadyVisited.incl(cast[pointer](x))
 76    alreadyVisited.incl(cast[pointer](y))
 77    case x.kind
 78    of yScalar: result = x.content == y.content
 79    of ySequence:
 80      if x.elems.len != y.elems.len: return false
 81      for i in 0..<x.elems.len:
 82        compare(x.elems[i], y.elems[i])
 83    of yMapping:
 84      if x.fields.len != y.fields.len: return false
 85      for xKey, xValue in x.fields.pairs:
 86        let xKeyVisited = cast[pointer](xKey) in alreadyVisited
 87        var matchingValue: ref YamlNodeObj = nil
 88        for yKey, yValue in y.fields.pairs:
 89          if cast[pointer](yKey) != cast[pointer](xKey):
 90            if cast[pointer](yKey) in alreadyVisited and xKeyVisited:
 91              # prevent infinite loop!
 92              continue
 93            if xKey == yKey:
 94              matchingValue = yValue
 95              break
 96          else:
 97            matchingValue = yValue
 98            break
 99        if isNil(matchingValue): return false
100        compare(xValue, matchingValue)
101  
102  proc `==`*(x, y: YamlNode): bool =
103    var alreadyVisited = initHashSet[pointer]()
104    result = eqImpl(x, y, alreadyVisited)
105  
106  proc `$`*(n: YamlNode): string =
107    result = "!<" & $n.tag & "> "
108    case n.kind
109    of yScalar: result.add(escape(n.content))
110    of ySequence:
111      result.add('[')
112      for item in n.elems:
113        result.add($item)
114        result.add(", ")
115      result.setLen(result.len - 1)
116      result[^1] = ']'
117    of yMapping:
118      result.add('{')
119      for key, value in n.fields.pairs:
120        result.add($key)
121        result.add(": ")
122        result.add($value)
123        result.add(", ")
124      result.setLen(result.len - 1)
125      result[^1] = '}'
126  
127  proc newYamlNode*(
128    content : string,
129    tag     : Tag         = yTagQuestionMark,
130    style   : ScalarStyle = ssAny,
131    startPos: Mark        = Mark(),
132    endPos  : Mark        = Mark(),
133  ): YamlNode =
134    YamlNode(kind: yScalar, content: content, tag: tag,
135        startPos: startPos, endPos: endPos)
136  
137  proc newYamlNode*(
138    elems   : openarray[YamlNode],
139    tag     : Tag  = yTagQuestionMark,
140    style   : CollectionStyle = csAny,
141    startPos: Mark = Mark(),
142    endPos  : Mark = Mark(),
143  ): YamlNode =
144    YamlNode(kind: ySequence, elems: @elems, tag: tag,
145        startPos: startPos, endPos: endPos)
146  
147  proc newYamlNode*(
148    fields  : openarray[(YamlNode, YamlNode)],
149    tag     : Tag  = yTagQuestionMark, 
150    style   : CollectionStyle = csAny,
151    startPos: Mark = Mark(),
152    endPos  : Mark = Mark(),
153  ): YamlNode =
154    YamlNode(kind: yMapping, fields: newTable(fields), tag: tag,
155        startPos: startPos, endPos: endPos)
156  
157  proc constructChild*(
158    ctx   : var ConstructionContext,
159    result: var YamlNode
160  ) {.raises: [YamlStreamError, YamlConstructionError].} =
161    template addAnchor(c: var ConstructionContext, target: Anchor) =
162      if target != yAnchorNone:
163        yAssert(not ctx.refs.hasKey(target))
164        c.refs[target] = (tag: yamlTag(YamlNode), p: cast[pointer](result))
165  
166    var start: Event
167    when defined(gcArc) or defined(gcOrc):
168      start = ctx.input.next()
169    else:
170      shallowCopy(start, ctx.input.next())
171  
172    case start.kind
173    of yamlStartMap:
174      result = YamlNode(
175        tag: start.mapProperties.tag,
176        kind: yMapping,
177        fields: newTable[YamlNode, YamlNode](),
178        mapStyle: start.mapStyle,
179        startPos: start.startPos,
180        endPos: start.endPos,
181      )
182      ctx.addAnchor(start.mapProperties.anchor)
183      while ctx.input.peek().kind != yamlEndMap:
184        var
185          key: YamlNode = nil
186          value: YamlNode = nil
187        ctx.constructChild(key)
188        ctx.constructChild(value)
189        if result.fields.hasKeyOrPut(key, value):
190          raise ctx.input.constructionError(key.startPos, "Duplicate key: " & $key)
191      discard ctx.input.next()
192    of yamlStartSeq:
193      result = YamlNode(
194        tag: start.seqProperties.tag,
195        kind: ySequence,
196        elems: newSeq[YamlNode](),
197        seqStyle: start.seqStyle,
198        startPos: start.startPos,
199        endPos: start.endPos,
200      )
201      ctx.addAnchor(start.seqProperties.anchor)
202      while ctx.input.peek().kind != yamlEndSeq:
203        var item: YamlNode = nil
204        ctx.constructChild(item)
205        result.elems.add(item)
206      discard ctx.input.next()
207    of yamlScalar:
208      result = YamlNode(
209        tag: start.scalarProperties.tag,
210        kind: yScalar,
211        scalarStyle: start.scalarStyle,
212        startPos: start.startPos,
213        endPos: start.endPos,
214      )
215      ctx.addAnchor(start.scalarProperties.anchor)
216      when defined(gcArc) or defined(gcOrc):
217        result.content = move start.scalarContent
218      else:
219        shallowCopy(result.content, start.scalarContent)
220    of yamlAlias:
221      result = cast[YamlNode](ctx.refs.getOrDefault(start.aliasTarget).p)
222    else: internalError("Malformed YamlStream")
223  
224  proc representChild*(
225    ctx  : var SerializationContext, 
226    value: YamlNodeObj,
227  ) {.raises: [YamlSerializationError].} =
228    case value.kind
229    of yScalar:
230      ctx.put(scalarEvent(value.content, value.tag, style = value.scalarStyle,
231          startPos = value.startPos, endPos = value.endPos))
232    of ySequence:
233      ctx.put(startSeqEvent(tag = value.tag, style = value.seqStyle,
234          startPos = value.startPos, endPos = value.endPos))
235      for item in value.elems: ctx.representChild(item)
236      ctx.put(endSeqEvent())
237    of yMapping:
238      ctx.put(startMapEvent(tag = value.tag, style = value.mapStyle,
239          startPos = value.startPos, endPos = value.endPos))
240      for key, value in value.fields.pairs:
241        ctx.representChild(key)
242        ctx.representChild(value)
243      ctx.put(endMapEvent())
244  
245  proc `[]`*(node: YamlNode, i: int): YamlNode =
246    ## Get the node at index *i* from a sequence. *node* must be a *ySequence*.
247    assert node.kind == ySequence
248    node.elems[i]
249  
250  proc `[]=`*(node: var YamlNode, i: int, val: YamlNode) =
251    ## Set the node at index *i* of a sequence. *node* must be a *ySequence*.
252    assert node.kind == ySequence
253    node.elems[i] = val
254  
255  proc `[]`*(node: YamlNode, key: YamlNode): YamlNode =
256    ## Get the value for a key in a mapping. *node* must be a *yMapping*.
257    assert node.kind == yMapping
258    node.fields[key]
259  
260  proc `[]=`*(node: YamlNode, key: YamlNode, value: YamlNode) =
261    ## Set the value for a key in a mapping. *node* must be a *yMapping*.
262    node.fields[key] = value
263  
264  proc `[]`*(node: YamlNode, key: string): YamlNode =
265    ## Get the value for a string key in a mapping. *node* must be a *yMapping*.
266    ## This searches for a scalar key with content *key* and either no explicit
267    ## tag or the explicit tag ``!!str``.
268    assert node.kind == yMapping
269    var keyNode = YamlNode(kind: yScalar, tag: yTagExclamationMark, content: key)
270    result = node.fields.getOrDefault(keyNode)
271    if isNil(result):
272      keyNode.tag = yTagQuestionMark
273      result = node.fields.getOrDefault(keyNode)
274      if isNil(result):
275        keyNode.tag = nimTag(yamlTagRepositoryPrefix & "str")
276        result = node.fields.getOrDefault(keyNode)
277        if isNil(result):
278          raise newException(KeyError, "No key " & escape(key) & " exists!")
279  
280  proc len*(node: YamlNode): int =
281    ## If *node* is a *yMapping*, return the number of key-value pairs. If *node*
282    ## is a *ySequence*, return the number of elements. Else, return ``0``
283    case node.kind
284    of yMapping: result = node.fields.len
285    of ySequence: result = node.elems.len
286    of yScalar: result = 0
287  
288  iterator items*(node: YamlNode): YamlNode =
289    ## Iterates over all items of a sequence. *node* must be a *ySequence*.
290    assert node.kind == ySequence
291    for item in node.elems: yield item
292  
293  iterator mitems*(node: var YamlNode): YamlNode =
294    ## Iterates over all items of a sequence. *node* must be a *ySequence*.
295    ## Values can be modified.
296    assert node.kind == ySequence
297    for item in node.elems.mitems: yield item
298  
299  iterator pairs*(node: YamlNode): tuple[key, value: YamlNode] =
300    ## Iterates over all key-value pairs of a mapping. *node* must be a
301    ## *yMapping*.
302    assert node.kind == yMapping
303    for key, value in node.fields: yield (key, value)
304  
305  iterator mpairs*(node: var YamlNode):
306      tuple[key: YamlNode, value: var YamlNode] =
307    ## Iterates over all key-value pairs of a mapping. *node* must be a
308    ## *yMapping*. Values can be modified.
309    doAssert node.kind == yMapping
310    for key, value in node.fields.mpairs: yield (key, value)
311  
312  proc loadFlattened*[K](
313    input : Stream | string,
314    target: var K,
315  ) {.raises: [
316    YamlConstructionError, YamlSerializationError,
317    IOError, OSError, YamlParserError
318  ].} =
319    ## Replaces all aliases with the referenced nodes in the input, then loads
320    ## the resulting YAML into K. Can be used when anchors & aliases are used like
321    ## variables in the input, to avoid having to define `ref` types for the
322    ## anchored data.
323    var node: YamlNode
324    load(input, node)
325    var stream = represent(node, tsNone, asNone)
326    try:
327      var e = stream.next()
328      yAssert(e.kind == yamlStartStream)
329      construct(stream, target)
330      e = stream.next()
331      yAssert(e.kind == yamlEndStream)
332    except YamlStreamError as e:
333      if e.parent of IOError: raise (ref IOError)(e.parent)
334      if e.parent of OSError: raise (ref OSError)(e.parent)
335      elif e.parent of YamlParserError: raise (ref YamlParserError)(e.parent)
336      else: internalError("Unexpected exception: " & $e.parent.name)