mirror of https://github.com/status-im/NimYAML.git
214 lines
8.4 KiB
Nim
214 lines
8.4 KiB
Nim
# NimYAML - YAML implementation in Nim
|
|
# (c) Copyright 2016 Felix Krause
|
|
#
|
|
# See the file "copying.txt", included in this
|
|
# distribution, for details about the copyright.
|
|
|
|
## ==================
|
|
## Module yaml.tojson
|
|
## ==================
|
|
##
|
|
## The tojson API enables you to parser a YAML character stream into the JSON
|
|
## structures provided by Nim's stdlib.
|
|
|
|
import json, streams, strutils, tables
|
|
import data, taglib, hints, serialization, stream, private/internal, parser
|
|
|
|
# represents a single YAML level. The `node` with name `key`.
|
|
# `expKey` is used to indicate that an empty node shall be filled
|
|
type Level = tuple[node: JsonNode, key: string, expKey: bool]
|
|
|
|
proc initLevel(node: JsonNode): Level {.raises: [].} =
|
|
(node: node, key: "", expKey: true)
|
|
|
|
proc jsonFromScalar(content: string, tag: Tag): JsonNode
|
|
{.raises: [YamlConstructionError].}=
|
|
new(result)
|
|
var mappedType: TypeHint
|
|
|
|
case tag
|
|
of yTagQuestionMark: mappedType = guessType(content)
|
|
of yTagExclamationMark, yTagString: mappedType = yTypeUnknown
|
|
of yTagBoolean:
|
|
case guessType(content)
|
|
of yTypeBoolTrue: mappedType = yTypeBoolTrue
|
|
of yTypeBoolFalse: mappedType = yTypeBoolFalse
|
|
else:
|
|
raise newException(YamlConstructionError,
|
|
"Invalid boolean value: " & content)
|
|
of yTagInteger: mappedType = yTypeInteger
|
|
of yTagNull: mappedType = yTypeNull
|
|
of yTagFloat:
|
|
case guessType(content)
|
|
of yTypeFloat: mappedType = yTypeFloat
|
|
of yTypeFloatInf: mappedType = yTypeFloatInf
|
|
of yTypeFloatNaN: mappedType = yTypeFloatNaN
|
|
else:
|
|
raise newException(YamlConstructionError,
|
|
"Invalid float value: " & content)
|
|
else: mappedType = yTypeUnknown
|
|
|
|
try:
|
|
case mappedType
|
|
of yTypeInteger:
|
|
result = JsonNode(kind: JInt, num: parseBiggestInt(content))
|
|
of yTypeFloat:
|
|
result = JsonNode(kind: JFloat, fnum: parseFloat(content))
|
|
of yTypeFloatInf:
|
|
result = JsonNode(kind: JFloat, fnum: if content[0] == '-': NegInf else: Inf)
|
|
of yTypeFloatNaN:
|
|
result = JsonNode(kind: JFloat, fnum: NaN)
|
|
of yTypeBoolTrue:
|
|
result = JsonNode(kind: JBool, bval: true)
|
|
of yTypeBoolFalse:
|
|
result = JsonNode(kind: JBool, bval: false)
|
|
of yTypeNull:
|
|
result = JsonNode(kind: JNull)
|
|
else:
|
|
result = JsonNode(kind: JString)
|
|
shallowCopy(result.str, content)
|
|
except ValueError:
|
|
var e = newException(YamlConstructionError, "Cannot parse numeric value")
|
|
e.parent = getCurrentException()
|
|
raise e
|
|
|
|
proc constructJson*(s: var YamlStream): seq[JsonNode]
|
|
{.raises: [YamlConstructionError, YamlStreamError].} =
|
|
## Construct an in-memory JSON tree from a YAML event stream. The stream may
|
|
## not contain any tags apart from those in ``coreTagLibrary``. Anchors and
|
|
## aliases will be resolved. Maps in the input must not contain
|
|
## non-scalars as keys. Each element of the result represents one document
|
|
## in the YAML stream.
|
|
##
|
|
## **Warning:** The special float values ``[+-]Inf`` and ``NaN`` will be
|
|
## parsed into Nim's JSON structure without error. However, they cannot be
|
|
## rendered to a JSON character stream, because these values are not part
|
|
## of the JSON specification. Nim's JSON implementation currently does not
|
|
## check for these values and will output invalid JSON when rendering one
|
|
## of these values into a JSON character stream.
|
|
newSeq(result, 0)
|
|
|
|
var
|
|
levels = newSeq[Level]()
|
|
anchors = initTable[Anchor, JsonNode]()
|
|
for event in s:
|
|
case event.kind
|
|
of yamlStartStream, yamlEndStream: discard
|
|
of yamlStartDoc:
|
|
# we don't need to do anything here; root node will be created
|
|
# by first scalar, sequence or map event
|
|
discard
|
|
of yamlEndDoc:
|
|
# we can savely assume that levels has e length of exactly 1.
|
|
result.add(levels.pop().node)
|
|
of yamlStartSeq:
|
|
levels.add(initLevel(newJArray()))
|
|
if event.seqProperties.anchor != yAnchorNone:
|
|
anchors[event.seqProperties.anchor] = levels[levels.high].node
|
|
of yamlStartMap:
|
|
levels.add(initLevel(newJObject()))
|
|
if event.mapProperties.anchor != yAnchorNone:
|
|
anchors[event.mapProperties.anchor] = levels[levels.high].node
|
|
of yamlScalar:
|
|
if levels.len == 0:
|
|
# parser ensures that next event will be yamlEndDocument
|
|
levels.add((node: jsonFromScalar(event.scalarContent,
|
|
event.scalarProperties.tag),
|
|
key: "",
|
|
expKey: true))
|
|
continue
|
|
|
|
case levels[levels.high].node.kind
|
|
of JArray:
|
|
let jsonScalar = jsonFromScalar(event.scalarContent,
|
|
event.scalarProperties.tag)
|
|
levels[levels.high].node.elems.add(jsonScalar)
|
|
if event.scalarProperties.anchor != yAnchorNone:
|
|
anchors[event.scalarProperties.anchor] = jsonScalar
|
|
of JObject:
|
|
if levels[levels.high].expKey:
|
|
levels[levels.high].expKey = false
|
|
# JSON only allows strings as keys
|
|
levels[levels.high].key = event.scalarContent
|
|
if event.scalarProperties.anchor != yAnchorNone:
|
|
raise newException(YamlConstructionError,
|
|
"scalar keys may not have anchors in JSON")
|
|
else:
|
|
let jsonScalar = jsonFromScalar(event.scalarContent,
|
|
event.scalarProperties.tag)
|
|
levels[levels.high].node[levels[levels.high].key] = jsonScalar
|
|
levels[levels.high].expKey = true
|
|
if event.scalarProperties.anchor != yAnchorNone:
|
|
anchors[event.scalarProperties.anchor] = jsonScalar
|
|
else:
|
|
internalError("Unexpected node kind: " & $levels[levels.high].node.kind)
|
|
of yamlEndSeq, yamlEndMap:
|
|
if levels.len > 1:
|
|
let level = levels.pop()
|
|
case levels[levels.high].node.kind
|
|
of JArray: levels[levels.high].node.elems.add(level.node)
|
|
of JObject:
|
|
if levels[levels.high].expKey:
|
|
raise newException(YamlConstructionError,
|
|
"non-scalar as key not allowed in JSON")
|
|
else:
|
|
levels[levels.high].node[levels[levels.high].key] = level.node
|
|
levels[levels.high].expKey = true
|
|
else:
|
|
internalError("Unexpected node kind: " &
|
|
$levels[levels.high].node.kind)
|
|
else: discard # wait for yamlEndDocument
|
|
of yamlAlias:
|
|
# we can savely assume that the alias exists in anchors
|
|
# (else the parser would have already thrown an exception)
|
|
case levels[levels.high].node.kind
|
|
of JArray:
|
|
levels[levels.high].node.elems.add(
|
|
anchors.getOrDefault(event.aliasTarget))
|
|
of JObject:
|
|
if levels[levels.high].expKey:
|
|
raise newException(YamlConstructionError,
|
|
"cannot use alias node as key in JSON")
|
|
else:
|
|
levels[levels.high].node.fields.add(
|
|
levels[levels.high].key, anchors.getOrDefault(event.aliasTarget))
|
|
levels[levels.high].expKey = true
|
|
else:
|
|
internalError("Unexpected node kind: " & $levels[levels.high].node.kind)
|
|
|
|
when not defined(JS):
|
|
proc loadToJson*(s: Stream): seq[JsonNode]
|
|
{.raises: [YamlParserError, YamlConstructionError, IOError, OSError].} =
|
|
## Uses `YamlParser <#YamlParser>`_ and
|
|
## `constructJson <#constructJson>`_ to construct an in-memory JSON tree
|
|
## from a YAML character stream.
|
|
var parser: YamlParser
|
|
parser.init(initCoreTagLibrary())
|
|
var events = parser.parse(s)
|
|
try:
|
|
return constructJson(events)
|
|
except YamlStreamError:
|
|
let e = getCurrentException()
|
|
if e.parent of IOError:
|
|
raise (ref IOError)(e.parent)
|
|
elif e.parent of OSError:
|
|
raise (ref OSError)(e.parent)
|
|
elif e.parent of YamlParserError:
|
|
raise (ref YamlParserError)(e.parent)
|
|
else: internalError("Unexpected exception: " & e.parent.repr)
|
|
|
|
proc loadToJson*(str: string): seq[JsonNode]
|
|
{.raises: [YamlParserError, YamlConstructionError].} =
|
|
## Uses `YamlParser <#YamlParser>`_ and
|
|
## `constructJson <#constructJson>`_ to construct an in-memory JSON tree
|
|
## from a YAML character stream.
|
|
var parser: YamlParser
|
|
parser.init(initCoreTagLibrary())
|
|
var events = parser.parse(str)
|
|
try: return constructJson(events)
|
|
except YamlStreamError:
|
|
let e = getCurrentException()
|
|
if e.parent of YamlParserError:
|
|
raise (ref YamlParserError)(e.parent)
|
|
else: internalError("Unexpected exception: " & e.parent.repr)
|