mirror of https://github.com/status-im/NimYAML.git
Improved DOM API
* yMapping is now a Table * changed names to match those of the json module * implemented procs to easier get and set values
This commit is contained in:
parent
a51befe30d
commit
84d4127caf
|
@ -5,7 +5,7 @@
|
|||
# distribution, for details about the copyright.
|
||||
|
||||
import "../yaml"
|
||||
import unittest, commonTestUtils, streams
|
||||
import unittest, commonTestUtils, streams, tables
|
||||
|
||||
suite "DOM":
|
||||
test "Composing simple Scalar":
|
||||
|
@ -25,13 +25,13 @@ suite "DOM":
|
|||
result = loadDOM(input)
|
||||
assert result.root.kind == ySequence
|
||||
assert result.root.tag == "?"
|
||||
assert result.root.children.len == 2
|
||||
assert result.root.children[0].kind == yScalar
|
||||
assert result.root.children[0].tag == "tag:yaml.org,2002:str"
|
||||
assert result.root.children[0].content == "a"
|
||||
assert result.root.children[1].kind == yScalar
|
||||
assert result.root.children[1].tag == "tag:yaml.org,2002:bool"
|
||||
assert result.root.children[1].content == "no"
|
||||
assert result.root.len == 2
|
||||
assert result.root[0].kind == yScalar
|
||||
assert result.root[0].tag == "tag:yaml.org,2002:str"
|
||||
assert result.root[0].content == "a"
|
||||
assert result.root[1].kind == yScalar
|
||||
assert result.root[1].tag == "tag:yaml.org,2002:bool"
|
||||
assert result.root[1].content == "no"
|
||||
test "Serializing sequence":
|
||||
let input = initYamlDoc(newYamlNode([
|
||||
newYamlNode("a", "tag:yaml.org,2002:str"),
|
||||
|
@ -46,12 +46,13 @@ suite "DOM":
|
|||
result = loadDOM(input)
|
||||
assert result.root.kind == yMapping
|
||||
assert result.root.tag == "tag:yaml.org,2002:map"
|
||||
assert result.root.pairs.len == 1
|
||||
assert result.root.pairs[0].key.kind == yScalar
|
||||
assert result.root.pairs[0].key.tag == "!foo"
|
||||
assert result.root.pairs[0].key.content == "bar"
|
||||
assert result.root.pairs[0].value.kind == ySequence
|
||||
assert result.root.pairs[0].value.children.len == 2
|
||||
assert result.root.fields.len == 1
|
||||
for key, value in result.root.fields.pairs:
|
||||
assert key.kind == yScalar
|
||||
assert key.tag == "!foo"
|
||||
assert key.content == "bar"
|
||||
assert value.kind == ySequence
|
||||
assert value.len == 2
|
||||
test "Serializing mapping":
|
||||
let input = initYamlDoc(newYamlNode([
|
||||
(key: newYamlNode("bar"), value: newYamlNode([newYamlNode("a"),
|
||||
|
@ -65,13 +66,13 @@ suite "DOM":
|
|||
input = newStringStream("- &a foo\n- &b bar\n- *a\n- *b")
|
||||
result = loadDOM(input)
|
||||
assert result.root.kind == ySequence
|
||||
assert result.root.children.len == 4
|
||||
assert result.root.children[0].kind == yScalar
|
||||
assert result.root.children[0].content == "foo"
|
||||
assert result.root.children[1].kind == yScalar
|
||||
assert result.root.children[1].content == "bar"
|
||||
assert result.root.children[0] == result.root.children[2]
|
||||
assert result.root.children[1] == result.root.children[3]
|
||||
assert result.root.len == 4
|
||||
assert result.root[0].kind == yScalar
|
||||
assert result.root[0].content == "foo"
|
||||
assert result.root[1].kind == yScalar
|
||||
assert result.root[1].content == "bar"
|
||||
assert cast[pointer](result.root[0]) == cast[pointer](result.root[2])
|
||||
assert cast[pointer](result.root[1]) == cast[pointer](result.root[3])
|
||||
test "Serializing with anchors":
|
||||
let
|
||||
a = newYamlNode("a")
|
||||
|
|
183
yaml/dom.nim
183
yaml/dom.nim
|
@ -12,11 +12,14 @@
|
|||
## structure. It can also dump the structure back to YAML. Formally, it
|
||||
## represents the *Representation Graph* as defined in the YAML specification.
|
||||
##
|
||||
## The main interface of this API are ``loadDOM`` and ``dumpDOM``. The other
|
||||
## The main interface of this API are ``loadDom`` and ``dumpDom``. The other
|
||||
## exposed procs are low-level and useful if you want to load or generate parts
|
||||
## of a ``YamlStream``.
|
||||
##
|
||||
## The ``YamlNode`` objects in the DOM can be used similarly to the ``JsonNode``
|
||||
## objects of Nim's `json module <http://nim-lang.org/docs/json.html>`_.
|
||||
|
||||
import tables, streams
|
||||
import tables, streams, hashes, sets, strutils
|
||||
import stream, taglib, serialization, ../private/internal, parser,
|
||||
presenter
|
||||
|
||||
|
@ -31,23 +34,98 @@ type
|
|||
tag*: string
|
||||
case kind*: YamlNodeKind
|
||||
of yScalar: content*: string
|
||||
of ySequence: children*: seq[YamlNode]
|
||||
of yMapping: pairs*: seq[tuple[key, value: YamlNode]]
|
||||
of ySequence: elems*: seq[YamlNode]
|
||||
of yMapping: fields*: TableRef[YamlNode, YamlNode]
|
||||
# compiler does not like Table[YamlNode, YamlNode]
|
||||
|
||||
YamlDocument* = object
|
||||
## Represents a YAML document.
|
||||
root*: YamlNode
|
||||
|
||||
proc hash*(o: YamlNode): Hash =
|
||||
result = o.tag.hash
|
||||
case o.kind
|
||||
of yScalar: result = result !& o.content.hash
|
||||
of yMapping:
|
||||
for key, value in o.fields.pairs:
|
||||
result = result !& key.hash !& value.hash
|
||||
of ySequence:
|
||||
for item in o.elems:
|
||||
result = result !& item.hash
|
||||
result = !$result
|
||||
|
||||
proc eqImpl(x, y: YamlNode, alreadyVisited: var HashSet[pointer]): bool =
|
||||
template compare(a, b: YamlNode) {.dirty.} =
|
||||
if cast[pointer](a) != cast[pointer](b):
|
||||
if cast[pointer](a) in alreadyVisited and
|
||||
cast[pointer](b) in alreadyVisited:
|
||||
# prevent infinite loop!
|
||||
return false
|
||||
elif a != b: return false
|
||||
|
||||
if x.kind != y.kind or x.tag != y.tag: return false
|
||||
alreadyVisited.incl(cast[pointer](x))
|
||||
alreadyVisited.incl(cast[pointer](y))
|
||||
case x.kind
|
||||
of yScalar: result = x.content == y.content
|
||||
of ySequence:
|
||||
if x.elems.len != y.elems.len: return false
|
||||
for i in 0..<x.elems.len:
|
||||
compare(x.elems[i], y.elems[i])
|
||||
of yMapping:
|
||||
if x.fields.len != y.fields.len: return false
|
||||
for xKey, xValue in x.fields.pairs:
|
||||
let xKeyVisited = cast[pointer](xKey) in alreadyVisited
|
||||
var matchingValue: ref YamlNodeObj = nil
|
||||
for yKey, yValue in y.fields.pairs:
|
||||
if cast[pointer](yKey) != cast[pointer](xKey):
|
||||
if cast[pointer](yKey) in alreadyVisited and xKeyVisited:
|
||||
# prevent infinite loop!
|
||||
continue
|
||||
if xKey == yKey:
|
||||
matchingValue = yValue
|
||||
break
|
||||
else:
|
||||
matchingValue = yValue
|
||||
break
|
||||
if isNil(matchingValue): return false
|
||||
compare(xValue, matchingValue)
|
||||
|
||||
proc `==`*(x, y: YamlNode): bool =
|
||||
var alreadyVisited = initSet[pointer]()
|
||||
result = eqImpl(x, y, alreadyVisited)
|
||||
|
||||
proc `$`*(n: YamlNode): string =
|
||||
result = "!<" & n.tag & "> "
|
||||
case n.kind
|
||||
of yScalar: result.add(escape(n.content))
|
||||
of ySequence:
|
||||
result.add('[')
|
||||
for item in n.elems:
|
||||
result.add($item)
|
||||
result.add(", ")
|
||||
result.setLen(result.len - 1)
|
||||
result[^1] = ']'
|
||||
of yMapping:
|
||||
result.add('{')
|
||||
for key, value in n.fields.pairs:
|
||||
result.add($key)
|
||||
result.add(": ")
|
||||
result.add($value)
|
||||
result.add(", ")
|
||||
result.setLen(result.len - 1)
|
||||
result[^1] = '}'
|
||||
|
||||
proc newYamlNode*(content: string, tag: string = "?"): YamlNode =
|
||||
YamlNode(kind: yScalar, content: content, tag: tag)
|
||||
|
||||
proc newYamlNode*(children: openarray[YamlNode], tag: string = "?"):
|
||||
proc newYamlNode*(elems: openarray[YamlNode], tag: string = "?"):
|
||||
YamlNode =
|
||||
YamlNode(kind: ySequence, children: @children, tag: tag)
|
||||
YamlNode(kind: ySequence, elems: @elems, tag: tag)
|
||||
|
||||
proc newYamlNode*(pairs: openarray[tuple[key, value: YamlNode]],
|
||||
proc newYamlNode*(fields: openarray[(YamlNode, YamlNode)],
|
||||
tag: string = "?"): YamlNode =
|
||||
YamlNode(kind: yMapping, pairs: @pairs, tag: tag)
|
||||
YamlNode(kind: yMapping, fields: newTable(fields), tag: tag)
|
||||
|
||||
proc initYamlDoc*(root: YamlNode): YamlDocument = result.root = root
|
||||
|
||||
|
@ -62,12 +140,14 @@ proc composeNode(s: var YamlStream, tagLib: TagLibrary,
|
|||
of yamlStartMap:
|
||||
result.tag = tagLib.uri(start.mapTag)
|
||||
result.kind = yMapping
|
||||
result.pairs = newSeq[tuple[key, value: YamlNode]]()
|
||||
result.fields = newTable[YamlNode, YamlNode]()
|
||||
while s.peek().kind != yamlEndMap:
|
||||
let
|
||||
key = composeNode(s, tagLib, c)
|
||||
value = composeNode(s, tagLib, c)
|
||||
result.pairs.add((key: key, value: value))
|
||||
if result.fields.hasKeyOrPut(key, value):
|
||||
raise newException(YamlConstructionError,
|
||||
"Duplicate key: " & $key)
|
||||
discard s.next()
|
||||
if start.mapAnchor != yAnchorNone:
|
||||
yAssert(not c.refs.hasKey(start.mapAnchor))
|
||||
|
@ -75,9 +155,9 @@ proc composeNode(s: var YamlStream, tagLib: TagLibrary,
|
|||
of yamlStartSeq:
|
||||
result.tag = tagLib.uri(start.seqTag)
|
||||
result.kind = ySequence
|
||||
result.children = newSeq[YamlNode]()
|
||||
result.elems = newSeq[YamlNode]()
|
||||
while s.peek().kind != yamlEndSeq:
|
||||
result.children.add(composeNode(s, tagLib, c))
|
||||
result.elems.add(composeNode(s, tagLib, c))
|
||||
if start.seqAnchor != yAnchorNone:
|
||||
yAssert(not c.refs.hasKey(start.seqAnchor))
|
||||
c.refs[start.seqAnchor] = cast[pointer](result)
|
||||
|
@ -106,7 +186,7 @@ proc compose*(s: var YamlStream, tagLib: TagLibrary): YamlDocument
|
|||
n = s.next()
|
||||
yAssert n.kind == yamlEndDoc
|
||||
|
||||
proc loadDOM*(s: Stream | string): YamlDocument
|
||||
proc loadDom*(s: Stream | string): YamlDocument
|
||||
{.raises: [IOError, YamlParserError, YamlConstructionError].} =
|
||||
var
|
||||
tagLib = initExtendedTagLibrary()
|
||||
|
@ -148,14 +228,14 @@ proc serializeNode(n: YamlNode, c: SerializationContext, a: AnchorStyle,
|
|||
of yScalar: c.put(scalarEvent(n.content, tagId, anchor))
|
||||
of ySequence:
|
||||
c.put(startSeqEvent(tagId, anchor))
|
||||
for item in n.children:
|
||||
for item in n.elems:
|
||||
serializeNode(item, c, a, tagLib)
|
||||
c.put(endSeqEvent())
|
||||
of yMapping:
|
||||
c.put(startMapEvent(tagId, anchor))
|
||||
for i in n.pairs:
|
||||
serializeNode(i.key, c, a, tagLib)
|
||||
serializeNode(i.value, c, a, tagLib)
|
||||
for key, value in n.fields.pairs:
|
||||
serializeNode(key, c, a, tagLib)
|
||||
serializeNode(value, c, a, tagLib)
|
||||
c.put(endMapEvent())
|
||||
|
||||
template processAnchoredEvent(target: untyped, c: SerializationContext): typed =
|
||||
|
@ -182,7 +262,7 @@ proc serialize*(doc: YamlDocument, tagLib: TagLibrary, a: AnchorStyle = asTidy):
|
|||
else: discard
|
||||
result = bys
|
||||
|
||||
proc dumpDOM*(doc: YamlDocument, target: Stream,
|
||||
proc dumpDom*(doc: YamlDocument, target: Stream,
|
||||
anchorStyle: AnchorStyle = asTidy,
|
||||
options: PresentationOptions = defaultPresentationOptions)
|
||||
{.raises: [YamlPresenterJsonError, YamlPresenterOutputError,
|
||||
|
@ -193,3 +273,70 @@ proc dumpDOM*(doc: YamlDocument, target: Stream,
|
|||
events = serialize(doc, tagLib,
|
||||
if options.style == psJson: asNone else: anchorStyle)
|
||||
present(events, target, tagLib, options)
|
||||
|
||||
proc `[]`*(node: YamlNode, i: int): YamlNode =
|
||||
## Get the node at index *i* from a sequence. *node* must be a *ySequence*.
|
||||
assert node.kind == ySequence
|
||||
node.elems[i]
|
||||
|
||||
proc `[]=`*(node: var YamlNode, i: int, val: YamlNode) =
|
||||
## Set the node at index *i* of a sequence. *node* must be a *ySequence*.
|
||||
assert node.kind == ySequence
|
||||
node.elems[i] = val
|
||||
|
||||
proc `[]`*(node: YamlNode, key: YamlNode): YamlNode =
|
||||
## Get the value for a key in a mapping. *node* must be a *yMapping*.
|
||||
assert node.kind == yMapping
|
||||
node.fields[key]
|
||||
|
||||
proc `[]=`*(node: YamlNode, key: YamlNode, value: YamlNode) =
|
||||
## Set the value for a key in a mapping. *node* must be a *yMapping*.
|
||||
node.fields[key] = value
|
||||
|
||||
proc `[]`*(node: YamlNode, key: string): YamlNode =
|
||||
## Get the value for a string key in a mapping. *node* must be a *yMapping*.
|
||||
## This searches for a scalar key with content *key* and either no explicit
|
||||
## tag or the explicit tag ``!!str``.
|
||||
assert node.kind == yMapping
|
||||
var keyNode = YamlNode(kind: yScalar, tag: "!", content: key)
|
||||
result = node.fields.getOrDefault(keyNode)
|
||||
if isNil(result):
|
||||
keyNode.tag = "?"
|
||||
result = node.fields.getOrDefault(keyNode)
|
||||
if isNil(result):
|
||||
keyNode.tag = nimTag(yamlTagRepositoryPrefix & "str")
|
||||
result = node.fields.getOrDefault(keyNode)
|
||||
if isNil(result):
|
||||
raise newException(KeyError, "No key " & escape(key) & " exists!")
|
||||
|
||||
proc len*(node: YamlNode): int =
|
||||
## If *node* is a *yMapping*, return the number of key-value pairs. If *node*
|
||||
## is a *ySequence*, return the number of elements. Else, return ``0``
|
||||
case node.kind
|
||||
of yMapping: result = node.fields.len
|
||||
of ySequence: result = node.elems.len
|
||||
of yScalar: result = 0
|
||||
|
||||
iterator items*(node: YamlNode): YamlNode =
|
||||
## Iterates over all items of a sequence. *node* must be a *ySequence*.
|
||||
assert node.kind == ySequence
|
||||
for item in node.elems: yield item
|
||||
|
||||
iterator mitems*(node: var YamlNode): YamlNode =
|
||||
## Iterates over all items of a sequence. *node* must be a *ySequence*.
|
||||
## Values can be modified.
|
||||
assert node.kind == ySequence
|
||||
for item in node.elems.mitems: yield item
|
||||
|
||||
iterator pairs*(node: YamlNode): tuple[key, value: YamlNode] =
|
||||
## Iterates over all key-value pairs of a mapping. *node* must be a
|
||||
## *yMapping*.
|
||||
assert node.kind == yMapping
|
||||
for key, value in node.fields: yield (key, value)
|
||||
|
||||
iterator mpairs*(node: var YamlNode):
|
||||
tuple[key: YamlNode, value: var YamlNode] =
|
||||
## Iterates over all key-value pairs of a mapping. *node* must be a
|
||||
## *yMapping*. Values can be modified.
|
||||
doAssert node.kind == yMapping
|
||||
for key, value in node.fields.mpairs: yield (key, value)
|
Loading…
Reference in New Issue