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)