/ yaml / data.nim
data.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/data
  9  ## ================
 10  ##
 11  ## This module defines the event data structure which serves as internal
 12  ## representation of YAML structures. Every YAML document can be described
 13  ## as stream of events.
 14  
 15  import hashes
 16  import style, private/escaping
 17  export style
 18  
 19  type
 20    Anchor* = distinct string ## \
 21      ## An ``Anchor`` identifies an anchor in the current document.
 22      ## It is not necessarily unique and references to an anchor must be
 23      ## resolved immediately on occurrence.
 24      ##
 25      ## Anchor provides the operator `$` for converting to string, `==` for
 26      ## comparison, and `hash` for usage in a hashmap.
 27  
 28    Tag* = distinct string ## \
 29      ## A ``Tag`` contains an URI, like for example ``"tag:yaml.org,2002:str"``.
 30  
 31    EventKind* = enum
 32      ## Kinds of YAML events that may occur in an ``YamlStream``. Event kinds
 33      ## are discussed in `YamlStreamEvent <#YamlStreamEvent>`_.
 34      yamlStartStream, yamlEndStream,
 35      yamlStartDoc, yamlEndDoc, yamlStartMap, yamlEndMap,
 36      yamlStartSeq, yamlEndSeq, yamlScalar, yamlAlias
 37  
 38    Event* = object
 39      ## An element from a `YamlStream <#YamlStream>`_. Events that start an
 40      ## object (``yamlStartMap``, ``yamlStartSeq``, ``yamlScalar``) have
 41      ## an optional anchor and a tag associated with them. The anchor will be
 42      ## set to ``yAnchorNone`` if it doesn't exist.
 43      ##
 44      ## A missing tag in the YAML character stream generates
 45      ## the non-specific tags ``?`` or ``!`` according to the YAML
 46      ## specification. These are by convention mapped to the ``TagId`` s
 47      ## ``yTagQuestionMark`` and ``yTagExclamationMark`` respectively.
 48      ## Mapping is done by a `TagLibrary <#TagLibrary>`_.
 49      ##
 50      ## ``startPos`` and ``endPos`` are only relevant for events from an input
 51      ## stream - they are generally ignored if used with events that generate
 52      ## output.
 53      startPos*, endPos*: Mark
 54      case kind*: EventKind
 55      of yamlStartStream, yamlEndStream: discard
 56      of yamlStartMap:
 57        mapProperties*: Properties
 58        mapStyle*: CollectionStyle
 59      of yamlStartSeq:
 60        seqProperties*: Properties
 61        seqStyle*: CollectionStyle
 62      of yamlScalar:
 63        scalarProperties*: Properties
 64        scalarStyle*  : ScalarStyle
 65        scalarContent*: string
 66      of yamlStartDoc:
 67        explicitDirectivesEnd*: bool
 68        version*: string
 69        handles*: seq[tuple[handle, uriPrefix: string]]
 70      of yamlEndDoc:
 71        explicitDocumentEnd*: bool
 72      of yamlEndMap, yamlEndSeq: discard
 73      of yamlAlias:
 74        aliasTarget* : Anchor
 75  
 76    Mark* = object
 77      line*  : Positive = 1
 78      column*: Positive = 1
 79  
 80    Properties* = tuple[anchor: Anchor, tag: Tag]
 81    
 82    YamlLoadingError* = object of ValueError
 83      ## Base class for all exceptions that may be raised during the process
 84      ## of loading a YAML character stream.
 85      mark*: Mark ## position at which the error has occurred.
 86      lineContent*: string ## \
 87        ## content of the line where the error was encountered. Includes a
 88        ## second line with a marker ``^`` at the position where the error
 89        ## was encountered.
 90  
 91  const
 92    yamlTagRepositoryPrefix* = "tag:yaml.org,2002:"
 93    nimyamlTagRepositoryPrefix* = "tag:nimyaml.org,2016:"
 94  
 95  proc defineTag*(uri: string): Tag =
 96    ## defines a tag. Use this to optimize away copies of globally defined
 97    ## Tags.
 98    result = uri.Tag
 99    #shallow(result.string) # doesn't work at compile-time
100  
101  proc defineCoreTag*(name: string): Tag =
102    ## defines a tag in YAML's core namespace, ``tag:yaml.org,2002:``
103    result = defineTag(yamlTagRepositoryPrefix & name)
104  
105  const
106    yAnchorNone*: Anchor = "".Anchor ## \
107      ## yielded when no anchor was defined for a YAML node
108  
109    yTagExclamationMark*: Tag = defineTag("!")
110    yTagQuestionMark*   : Tag = defineTag("?")
111  
112    # failsafe schema
113  
114    yTagString*   = defineCoreTag("str")
115    yTagSequence* = defineCoreTag("seq")
116    yTagMapping*  = defineCoreTag("map")
117  
118    # json & core schema
119  
120    yTagNull*    = defineCoreTag("null")
121    yTagBoolean* = defineCoreTag("bool")
122    yTagInteger* = defineCoreTag("int")
123    yTagFloat*   = defineCoreTag("float")
124  
125    # other language-independent YAML types (from http://yaml.org/type/ )
126  
127    yTagOrderedMap* = defineCoreTag("omap")
128    yTagPairs*      = defineCoreTag("pairs")
129    yTagSet*        = defineCoreTag("set")
130    yTagBinary*     = defineCoreTag("binary")
131    yTagMerge*      = defineCoreTag("merge")
132    yTagTimestamp*  = defineCoreTag("timestamp")
133    yTagValue*      = defineCoreTag("value")
134    yTagYaml*       = defineCoreTag("yaml")
135  
136    # NimYAML specific tags
137  
138    yTagNimField*   = defineTag(nimyamlTagRepositoryPrefix & "field")
139  
140  proc collectionStyle*(event: Event): CollectionStyle =
141    ## returns the style of the given collection start event
142    case event.kind
143    of yamlStartMap: result = event.mapStyle
144    of yamlStartSeq: result = event.seqStyle
145    else: raise (ref FieldDefect)(msg: "Event " & $event.kind & " has no collectionStyle")
146  
147  proc startStreamEvent*(): Event =
148    return Event(kind: yamlStartStream)
149  
150  proc endStreamEvent*(): Event =
151    return Event(kind: yamlEndStream)
152  
153  proc startDocEvent*(
154    explicit = false,
155    version  = "",
156    handles  : seq[tuple[handle, uriPrefix: string]] = @[],
157    startPos : Mark = Mark(),
158    endPos   : Mark = Mark(),
159  ): Event
160      {.inline, raises: [].} =
161    ## creates a new event that marks the start of a YAML document
162    result = Event(startPos: startPos, endPos: endPos,
163                   kind: yamlStartDoc, version: version, handles: handles,
164                   explicitDirectivesEnd: explicit)
165  
166  proc endDocEvent*(
167    explicit = false,
168    startPos : Mark = Mark(),
169    endPos   : Mark = Mark(),
170  ): Event
171      {.inline, raises: [].} =
172    ## creates a new event that marks the end of a YAML document
173    result = Event(startPos: startPos, endPos: endPos,
174                   kind: yamlEndDoc, explicitDocumentEnd: explicit)
175  
176  proc startMapEvent*(
177    style   : CollectionStyle,
178    props   : Properties,
179    startPos: Mark = Mark(),
180    endPos  : Mark = Mark(),
181  ): Event {.inline, raises: [].} =
182    ## creates a new event that marks the start of a YAML mapping
183    result = Event(startPos: startPos, endPos: endPos,
184                   kind: yamlStartMap, mapProperties: props,
185                   mapStyle: style)
186  
187  proc startMapEvent*(
188    style   : CollectionStyle = csAny,
189    tag     : Tag             = yTagQuestionMark,
190    anchor  : Anchor          = yAnchorNone,
191    startPos: Mark            = Mark(),
192    endPos  : Mark            = Mark(),
193  ): Event {.inline.} =
194    return startMapEvent(style, (anchor, tag), startPos, endPos)
195  
196  proc endMapEvent*(
197    startPos : Mark = Mark(),
198    endPos   : Mark = Mark(),
199  ): Event {.inline, raises: [].} =
200    ## creates a new event that marks the end of a YAML mapping
201    result = Event(startPos: startPos, endPos: endPos, kind: yamlEndMap)
202  
203  proc startSeqEvent*(
204    style   : CollectionStyle,
205    props   : Properties,
206    startPos: Mark = Mark(),
207    endPos  : Mark = Mark(),
208  ): Event {.inline, raises: [].} =
209    ## creates a new event that marks the beginning of a YAML sequence
210    result = Event(startPos: startPos, endPos: endPos,
211                   kind: yamlStartSeq, seqProperties: props,
212                   seqStyle: style)
213  
214  proc startSeqEvent*(
215    style   : CollectionStyle = csAny,
216    tag     : Tag             = yTagQuestionMark,
217    anchor  : Anchor          = yAnchorNone,
218    startPos: Mark            = Mark(),
219    endPos  : Mark            = Mark(),
220  ): Event {.inline.} =
221    return startSeqEvent(style, (anchor, tag), startPos, endPos)
222  
223  proc endSeqEvent*(
224    startPos: Mark = Mark(),
225    endPos  : Mark = Mark(),
226  ): Event {.inline, raises: [].} =
227    ## creates a new event that marks the end of a YAML sequence
228    result = Event(startPos: startPos, endPos: endPos, kind: yamlEndSeq)
229  
230  proc scalarEvent*(
231    content : string,
232    props   : Properties,
233    style   : ScalarStyle = ssAny,
234    startPos: Mark        = Mark(),
235    endPos  : Mark        = Mark(),
236  ): Event {.inline, raises: [].} =
237    ## creates a new event that represents a YAML scalar
238    result = Event(startPos: startPos, endPos: endPos,
239                   kind: yamlScalar, scalarProperties: props,
240                   scalarContent: content, scalarStyle: style)
241  
242  proc scalarEvent*(
243    content : string      = "",
244    tag     : Tag         = yTagQuestionMark,
245    anchor  : Anchor      = yAnchorNone,
246    style   : ScalarStyle = ssAny,
247    startPos: Mark        = Mark(),
248    endPos  : Mark        = Mark(),
249  ): Event {.inline.} =
250    return scalarEvent(content, (anchor, tag), style, startPos, endPos)
251  
252  proc aliasEvent*(
253    target  : Anchor,
254    startPos: Mark = Mark(),
255    endPos  : Mark = Mark(),
256  ): Event {.inline, raises: [].} =
257    ## creates a new event that represents a YAML alias
258    result = Event(startPos: startPos, endPos: endPos, kind: yamlAlias, aliasTarget: target)
259  
260  proc `==`*(left, right: Anchor): bool {.borrow.}
261  proc `$`*(id: Anchor): string {.borrow.}
262  proc hash*(id: Anchor): Hash {.borrow.}
263  
264  proc `==`*(left, right: Tag): bool {.borrow.}
265  proc `$`*(tag: Tag): string {.borrow.}
266  proc hash*(tag: Tag): Hash {.borrow.}
267  
268  proc `==`*(left: Event, right: Event): bool {.raises: [].} =
269    ## compares all existing fields of the given items
270    if left.kind != right.kind: return false
271    case left.kind
272    of yamlStartStream, yamlEndStream, yamlStartDoc, yamlEndDoc, yamlEndMap, yamlEndSeq:
273      result = true
274    of yamlStartMap:
275      result = left.mapProperties == right.mapProperties
276    of yamlStartSeq:
277      result = left.seqProperties == right.seqProperties
278    of yamlScalar:
279      result = left.scalarProperties == right.scalarProperties and
280               left.scalarContent == right.scalarContent
281    of yamlAlias: result = left.aliasTarget == right.aliasTarget
282  
283  proc renderAttrs*(props: Properties, isPlain: bool = true): string =
284    result = ""
285    if props.anchor != yAnchorNone: result &= " &" & $props.anchor
286    case props.tag
287    of yTagQuestionMark: discard
288    of yTagExclamationMark:
289      if isPlain: result &= " <!>"
290    else:
291      result &= " <" & $props.tag & ">"
292  
293  proc `$`*(event: Event): string {.raises: [].} =
294    ## outputs a human-readable string describing the given event.
295    ## This string is compatible to the format used in the yaml test suite.
296    case event.kind
297    of yamlStartStream: result = "+STR"
298    of yamlEndStream: result = "-STR"
299    of yamlEndMap: result = "-MAP"
300    of yamlEndSeq: result = "-SEQ"
301    of yamlStartDoc:
302      result = "+DOC"
303      if event.explicitDirectivesEnd: result &= " ---"
304    of yamlEndDoc:
305      result = "-DOC"
306      if event.explicitDocumentEnd: result &= " ..."
307    of yamlStartMap:
308      result = "+MAP" & (if event.mapStyle == csFlow: " {}" else: "") &
309        renderAttrs(event.mapProperties)
310    of yamlStartSeq:
311      result = "+SEQ" & (if event.seqStyle == csFlow: " []" else: "") &
312        renderAttrs(event.seqProperties)
313    of yamlScalar:
314      result = "=VAL" & renderAttrs(event.scalarProperties,
315                                    event.scalarStyle == ssPlain or
316                                    event.scalarStyle == ssAny)
317      case event.scalarStyle
318      of ssPlain, ssAny: result &= " :"
319      of ssSingleQuoted: result &= " \'"
320      of ssDoubleQuoted: result &= " \""
321      of ssLiteral: result &= " |"
322      of ssFolded: result &= " >"
323      result &= yamlTestSuiteEscape(event.scalarContent)
324    of yamlAlias: result = "=ALI *" & $event.aliasTarget
325  
326  proc properties*(event: Event): Properties =
327    ## returns the tag of the given event
328    case event.kind
329    of yamlStartMap: result = event.mapProperties
330    of yamlStartSeq: result = event.seqProperties
331    of yamlScalar: result = event.scalarProperties
332    else: raise newException(FieldDefect, "Event " & $event.kind & " has no properties")
333  
334  proc emptyProperties*(event: Event): bool =
335    ## returns true if the given event does not need to render
336    ## any properties if presented into a YAML character stream.
337    let props = event.properties()
338    case props.tag
339    of yTagExclamationMark:
340      result = event.kind == yamlScalar and props.anchor == yAnchorNone
341    of yTagQuestionMark:
342      result = props.anchor == yAnchorNone
343    else: result = false
344  
345  proc hasSpecificTag*(event: Event): bool =
346    var props: Properties
347    case event.kind
348    of yamlStartMap: props = event.mapProperties
349    of yamlStartSeq: props = event.seqProperties
350    of yamlScalar: props = event.scalarProperties
351    else: return false
352    case props.tag
353    of yTagExclamationMark, yTagQuestionMark: return false
354    else: return true