NimYAML/yaml/tojson.nim

218 lines
8.4 KiB
Nim
Raw Normal View History

# NimYAML - YAML implementation in Nim
# (c) Copyright 2015-2023 Felix Krause
#
# See the file "copying.txt", included in this
# distribution, for details about the copyright.
## ==================
2021-05-17 22:31:47 +00:00
## Module yaml/tojson
## ==================
##
## The tojson API enables you to parser a YAML character stream into the JSON
## structures provided by Nim's stdlib.
2016-09-20 19:53:38 +00:00
import json, streams, strutils, tables
import data, hints, native, stream, private/internal, parser
2016-09-20 19:53:38 +00:00
# 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]
2015-12-24 14:21:49 +00:00
proc initLevel(node: JsonNode): Level {.raises: [].} =
(node: node, key: "", expKey: true)
2015-12-24 14:21:49 +00:00
proc jsonFromScalar(
content: sink string,
tag : Tag,
): JsonNode {.raises: [YamlConstructionError].} =
2016-04-02 15:48:22 +00:00
new(result)
var mappedType: TypeHint
2016-04-02 15:48:22 +00:00
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
2016-04-02 15:48:22 +00:00
try:
case mappedType
of yTypeInteger:
result = JsonNode(kind: JInt, num: parseBiggestInt(content))
2016-04-02 15:48:22 +00:00
of yTypeFloat:
result = JsonNode(kind: JFloat, fnum: parseFloat(content))
2016-04-02 15:48:22 +00:00
of yTypeFloatInf:
result = JsonNode(kind: JFloat, fnum: if content[0] == '-': NegInf else: Inf)
2016-04-02 15:48:22 +00:00
of yTypeFloatNaN:
result = JsonNode(kind: JFloat, fnum: NaN)
2016-04-02 15:48:22 +00:00
of yTypeBoolTrue:
result = JsonNode(kind: JBool, bval: true)
2016-04-02 15:48:22 +00:00
of yTypeBoolFalse:
result = JsonNode(kind: JBool, bval: false)
2016-04-02 15:48:22 +00:00
of yTypeNull:
result = JsonNode(kind: JNull)
2016-04-02 15:48:22 +00:00
else:
result = JsonNode(kind: JString)
2022-08-29 13:11:11 +00:00
when defined(gcArc) or defined(gcOrc):
result.str = content
else:
shallowCopy(result.str, content)
2023-03-18 12:54:45 +00:00
except ValueError as ve:
2016-04-02 15:48:22 +00:00
var e = newException(YamlConstructionError, "Cannot parse numeric value")
2023-03-18 12:54:45 +00:00
e.parent = ve
2016-04-02 15:48:22 +00:00
raise e
proc constructJson*(s: var YamlStream): seq[JsonNode] {.raises: [
YamlConstructionError, YamlStreamError
].} =
2016-09-20 19:53:38 +00:00
## 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.
2016-04-02 15:48:22 +00:00
newSeq(result, 0)
2016-04-02 15:48:22 +00:00
var
levels = newSeq[Level]()
anchors = initTable[Anchor, JsonNode]()
2016-04-02 15:48:22 +00:00
for event in s:
case event.kind
of yamlStartStream, yamlEndStream: discard
2016-04-02 15:48:22 +00:00
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 safely assume that levels has e length of exactly 1.
2016-04-02 15:48:22 +00:00
result.add(levels.pop().node)
of yamlStartSeq:
levels.add(initLevel(newJArray()))
if event.seqProperties.anchor != yAnchorNone:
anchors[event.seqProperties.anchor] = levels[levels.high].node
2016-04-02 15:48:22 +00:00
of yamlStartMap:
levels.add(initLevel(newJObject()))
if event.mapProperties.anchor != yAnchorNone:
anchors[event.mapProperties.anchor] = levels[levels.high].node
2016-04-02 15:48:22 +00:00
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))
2016-04-02 15:48:22 +00:00
continue
2016-04-02 15:48:22 +00:00
case levels[levels.high].node.kind
of JArray:
let jsonScalar = jsonFromScalar(event.scalarContent,
event.scalarProperties.tag)
2016-04-02 15:48:22 +00:00
levels[levels.high].node.elems.add(jsonScalar)
if event.scalarProperties.anchor != yAnchorNone:
anchors[event.scalarProperties.anchor] = jsonScalar
2016-04-02 15:48:22 +00:00
of JObject:
if levels[levels.high].expKey:
levels[levels.high].expKey = false
2016-04-02 15:48:22 +00:00
# JSON only allows strings as keys
levels[levels.high].key = event.scalarContent
if event.scalarProperties.anchor != yAnchorNone:
raise newException(YamlConstructionError,
2016-04-02 15:48:22 +00:00
"scalar keys may not have anchors in JSON")
else:
2016-04-02 15:48:22 +00:00
let jsonScalar = jsonFromScalar(event.scalarContent,
event.scalarProperties.tag)
2016-04-02 15:48:22 +00:00
levels[levels.high].node[levels[levels.high].key] = jsonScalar
levels[levels.high].expKey = true
if event.scalarProperties.anchor != yAnchorNone:
anchors[event.scalarProperties.anchor] = jsonScalar
2016-08-09 19:20:25 +00:00
else:
internalError("Unexpected node kind: " & $levels[levels.high].node.kind)
2016-04-02 15:48:22 +00:00
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,
2016-04-02 15:48:22 +00:00
"non-scalar as key not allowed in JSON")
else:
levels[levels.high].node[levels[levels.high].key] = level.node
levels[levels.high].expKey = true
2016-08-09 19:20:25 +00:00
else:
internalError("Unexpected node kind: " &
$levels[levels.high].node.kind)
2016-04-02 15:48:22 +00:00
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))
2016-04-02 15:48:22 +00:00
of JObject:
if levels[levels.high].expKey:
2016-04-02 15:48:22 +00:00
raise newException(YamlConstructionError,
"cannot use alias node as key in JSON")
else:
levels[levels.high].node.fields[
levels[levels.high].key] = anchors.getOrDefault(event.aliasTarget)
levels[levels.high].expKey = true
2016-08-09 19:20:25 +00:00
else:
internalError("Unexpected node kind: " & $levels[levels.high].node.kind)
2017-03-29 15:14:50 +00:00
when not defined(JS):
proc loadToJson*(s: Stream): seq[JsonNode]
{.raises: [YamlParserError, YamlConstructionError, IOError, OSError].} =
2017-03-29 15:14:50 +00:00
## Uses `YamlParser <#YamlParser>`_ and
## `constructJson <#constructJson>`_ to construct an in-memory JSON tree
## from a YAML character stream.
var parser: YamlParser
parser.init()
var events = parser.parse(s)
2017-03-29 15:14:50 +00:00
try:
return constructJson(events)
2023-03-18 12:54:45 +00:00
except YamlStreamError as e:
2017-03-29 15:14:50 +00:00
if e.parent of IOError:
raise (ref IOError)(e.parent)
2020-11-10 20:28:56 +00:00
elif e.parent of OSError:
raise (ref OSError)(e.parent)
2017-03-29 15:14:50 +00:00
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()
var events = parser.parse(str)
try: return constructJson(events)
2023-03-18 12:54:45 +00:00
except YamlStreamError as e:
if e.parent of YamlParserError:
raise (ref YamlParserError)(e.parent)
else: internalError("Unexpected exception: " & e.parent.repr)