# 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.serialization ## ========================= ## ## This is the most high-level API of NimYAML. It enables you to parse YAML ## character streams directly into native YAML types and vice versa. It builds ## on top of the low-level parser and presenter APIs. ## ## It is possible to define custom construction and serialization procs for any ## type. Please consult the serialization guide on the NimYAML website for more ## information. import tables, typetraits, strutils, macros, streams import parser, taglib, presenter, stream, ../private/internal, hints export stream # *something* in here needs externally visible `==`(x,y: AnchorId), # but I cannot figure out what. binding it would be the better option. type SerializationContext* = ref object ## Context information for the process of serializing YAML from Nim values. refs*: Table[pointer, AnchorId] style: AnchorStyle nextAnchorId*: AnchorId put*: proc(e: YamlStreamEvent) {.raises: [], closure.} ConstructionContext* = ref object ## Context information for the process of constructing Nim values from YAML. refs*: Table[AnchorId, pointer] YamlConstructionError* = object of YamlLoadingError ## Exception that may be raised when constructing data objects from a ## `YamlStream <#YamlStream>`_. The fields ``line``, ``column`` and ## ``lineContent`` are only available if the costructing proc also does ## parsing, because otherwise this information is not available to the ## costruction proc. # forward declares proc constructChild*[T](s: var YamlStream, c: ConstructionContext, result: var T) {.raises: [YamlConstructionError, YamlStreamError].} ## Constructs an arbitrary Nim value from a part of a YAML stream. ## The stream will advance until after the finishing token that was used ## for constructing the value. The ``ConstructionContext`` is needed for ## potential child objects which may be refs. proc constructChild*(s: var YamlStream, c: ConstructionContext, result: var string) {.raises: [YamlConstructionError, YamlStreamError].} ## Constructs a Nim value that is a string from a part of a YAML stream. ## This specialization takes care of possible nil strings. proc constructChild*[T](s: var YamlStream, c: ConstructionContext, result: var seq[T]) {.raises: [YamlConstructionError, YamlStreamError].} ## Constructs a Nim value that is a string from a part of a YAML stream. ## This specialization takes care of possible nil seqs. proc constructChild*[O](s: var YamlStream, c: ConstructionContext, result: var ref O) {.raises: [YamlStreamError].} ## Constructs an arbitrary Nim value from a part of a YAML stream. ## The stream will advance until after the finishing token that was used ## for constructing the value. The object may be constructed from an alias ## node which will be resolved using the ``ConstructionContext``. proc representChild*[O](value: ref O, ts: TagStyle, c: SerializationContext) {.raises: [].} ## Represents an arbitrary Nim reference value as YAML object. The object ## may be represented as alias node if it is already present in the ## ``SerializationContext``. proc representChild*(value: string, ts: TagStyle, c: SerializationContext) {.inline, raises: [].} ## Represents a Nim string. Supports nil strings. proc representChild*[O](value: O, ts: TagStyle, c: SerializationContext) ## Represents an arbitrary Nim object as YAML object. proc newConstructionContext*(): ConstructionContext = new(result) result.refs = initTable[AnchorId, pointer]() proc newSerializationContext*(s: AnchorStyle, putImpl: proc(e: YamlStreamEvent) {.raises: [], closure.}): SerializationContext = SerializationContext(refs: initTable[pointer, AnchorId](), style: s, nextAnchorId: 0.AnchorId, put: putImpl) template presentTag*(t: typedesc, ts: TagStyle): TagId = ## Get the TagId that represents the given type in the given style if ts == tsNone: yTagQuestionMark else: yamlTag(t) proc lazyLoadTag(uri: string): TagId {.inline, raises: [].} = try: result = serializationTagLibrary.tags[uri] except KeyError: result = serializationTagLibrary.registerUri(uri) proc safeTagUri(id: TagId): string {.raises: [].} = try: let uri = serializationTagLibrary.uri(id) if uri.len > 0 and uri[0] == '!': return uri[1..uri.len - 1] else: return uri except KeyError: internalError("Unexpected KeyError for TagId " & $id) proc constructionError(s: YamlStream, msg: string): ref YamlConstructionError = result = newException(YamlConstructionError, msg) if not s.getLastTokenContext(result.line, result.column, result.lineContent): (result.line, result.column) = (-1, -1) result.lineContent = "" template constructScalarItem*(s: var YamlStream, i: untyped, t: typedesc, content: untyped) = ## Helper template for implementing ``constructObject`` for types that ## are constructed from a scalar. ``i`` is the identifier that holds ## the scalar as ``YamlStreamEvent`` in the content. Exceptions raised in ## the content will be automatically catched and wrapped in ## ``YamlConstructionError``, which will then be raised. bind constructionError let i = s.next() if i.kind != yamlScalar: raise constructionError(s, "Expected scalar") try: content except YamlConstructionError: raise except Exception: var e = constructionError(s, "Cannot construct to " & name(t) & ": " & item.scalarContent) e.parent = getCurrentException() raise e proc yamlTag*(T: typedesc[string]): TagId {.inline, noSideEffect, raises: [].} = yTagString proc constructObject*(s: var YamlStream, c: ConstructionContext, result: var string) {.raises: [YamlConstructionError, YamlStreamError].} = ## costructs a string from a YAML scalar constructScalarItem(s, item, string): result = item.scalarContent proc representObject*(value: string, ts: TagStyle, c: SerializationContext, tag: TagId) {.raises: [].} = ## represents a string as YAML scalar c.put(scalarEvent(value, tag, yAnchorNone)) proc parseHex[T: int8|int16|int32|int64|uint8|uint16|uint32|uint64]( s: YamlStream, val: string): T = result = 0 for i in 2.. 0: representChild(field, if ts == tsAll: tsAll else: tsRootOnly, c) inc(count) if count == 1: c.put(scalarEvent("~", yTagNull)) else: representObject(value, ts, c, if ts == tsNone: yTagQuestionMark else: yamlTag(O)) proc construct*[T](s: var YamlStream, target: var T) {.raises: [YamlStreamError].} = ## Constructs a Nim value from a YAML stream. var context = newConstructionContext() try: var e = s.next() yAssert(e.kind == yamlStartDoc) constructChild(s, context, target) e = s.next() yAssert(e.kind == yamlEndDoc) except YamlConstructionError: raise (ref YamlConstructionError)(getCurrentException()) except YamlStreamError: let cur = getCurrentException() var e = newException(YamlStreamError, cur.msg) e.parent = cur.parent raise e except Exception: # may occur while calling s() var ex = newException(YamlStreamError, "") ex.parent = getCurrentException() raise ex proc load*[K](input: Stream | string, target: var K) {.raises: [YamlConstructionError, IOError, YamlParserError].} = ## Loads a Nim value from a YAML character stream. var parser = newYamlParser(serializationTagLibrary) events = parser.parse(input) try: construct(events, target) except YamlStreamError: let e = (ref YamlStreamError)(getCurrentException()) if e.parent of IOError: raise (ref IOError)(e.parent) elif e.parent of YamlParserError: raise (ref YamlParserError)(e.parent) else: internalError("Unexpected exception: " & e.parent.repr) proc loadMultiDoc*[K](input: Stream | string, target: var seq[K]) = if target.isNil: target = newSeq[K]() var parser = newYamlParser(serializationTagLibrary) events = parser.parse(input) try: while not events.finished(): var item: K construct(events, item) target.add(item) except YamlConstructionError: var e = (ref YamlConstructionError)(getCurrentException()) discard events.getLastTokenContext(e.line, e.column, e.lineContent) raise e except YamlStreamError: let e = (ref YamlStreamError)(getCurrentException()) if e.parent of IOError: raise (ref IOError)(e.parent) elif e.parent of YamlParserError: raise (ref YamlParserError)(e.parent) else: internalError("Unexpected exception: " & e.parent.repr) proc setAnchor(a: var AnchorId, q: var Table[pointer, AnchorId]) {.inline.} = if a != yAnchorNone: a = q.getOrDefault(cast[pointer](a)) proc represent*[T](value: T, ts: TagStyle = tsRootOnly, a: AnchorStyle = asTidy): YamlStream = ## Represents a Nim value as ``YamlStream`` var bys = newBufferYamlStream() var context = newSerializationContext(a, proc(e: YamlStreamEvent) = bys.put(e) ) bys.put(startDocEvent()) representChild(value, ts, context) bys.put(endDocEvent()) if a == asTidy: for item in bys.mitems(): case item.kind of yamlStartMap: item.mapAnchor.setAnchor(context.refs) of yamlStartSeq: item.seqAnchor.setAnchor(context.refs) of yamlScalar: item.scalarAnchor.setAnchor(context.refs) else: discard result = bys proc dump*[K](value: K, target: Stream, tagStyle: TagStyle = tsRootOnly, anchorStyle: AnchorStyle = asTidy, options: PresentationOptions = defaultPresentationOptions) {.raises: [YamlPresenterJsonError, YamlPresenterOutputError, YamlStreamError].} = ## Dump a Nim value as YAML character stream. var events = represent(value, if options.style == psCanonical: tsAll else: tagStyle, if options.style == psJson: asNone else: anchorStyle) try: present(events, target, serializationTagLibrary, options) except YamlStreamError: internalError("Unexpected exception: " & getCurrentException().repr) proc dump*[K](value: K, tagStyle: TagStyle = tsRootOnly, anchorStyle: AnchorStyle = asTidy, options: PresentationOptions = defaultPresentationOptions): string = ## Dump a Nim value as YAML into a string var events = represent(value, if options.style == psCanonical: tsAll else: tagStyle, if options.style == psJson: asNone else: anchorStyle) try: result = present(events, serializationTagLibrary, options) except YamlStreamError: internalError("Unexpected exception: " & getCurrentException().repr) proc canBeImplicit(t: typedesc): bool {.compileTime.} = let tDesc = getType(t) if tDesc.kind != nnkObjectTy: return false if tDesc[2].len != 1: return false if tDesc[2][0].kind != nnkRecCase: return false var foundEmptyBranch = false for i in 1.. tDesc[2][0].len - 1: case tDesc[2][0][i][1].len # branch contents of 0: if foundEmptyBranch: return false else: foundEmptyBranch = true of 1: discard else: return false return true macro setImplicitVariantObjectMarker(t: typedesc): untyped = result = quote do: proc `implicitVariantObjectMarker`*(unused: `t`) = discard template markAsImplicit*(t: typedesc): typed = ## Mark a variant object type as implicit. This requires the type to consist ## of nothing but a case expression and each branch of the case expression ## containing exactly one field - with the exception that one branch may ## contain zero fields. when canBeImplicit(t): # this will be checked by means of compiles(implicitVariantObject(...)) setImplicitVariantObjectMarker(t) else: {. fatal: "This type cannot be marked as implicit" .}