Skip to content

tanelso2/yanyl

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

67 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

YANYL (Yet Another Nim Yaml Library)

A library for working with YAML in Nim. Can generate conversion functions for most types declared in Nim.

NimDocs can be found here

Example

import
  yanyl

type
  Obj = object
    i: int
    s: string

deriveYaml Obj

var sample: string = """
i: 99
s: hello world
"""
var o: Obj = ofYamlStr(sample, Obj)
assert o.i == 99
assert o.s == "hello world"

Install

Add the following to your .nimble file:

requires "yanyl"

Usage

deriveYaml

Macro that takes the name of a type and will generate ofYaml and toYaml procs for that type.

import
  yanyl

type
  Obj = object
    i: int
    s: string

deriveYaml Obj

var sample: string = """
i: 99
s: hello world
"""
var o: Obj = ofYamlStr(sample, Obj)
assert o.i == 99
assert o.s == "hello world"

loadNode

Proc that takes a string or Stream and returns a YNode, the internal representation of a YAML document.

import
  yanyl

let s = """
a: 1
b: 3
c:
  - a
  - b
  - c
"""

let y: YNode = s.loadNode()
assert y.kind == ynMap
assert y.get("a").kind == ynString
assert y.get("c").kind == ynList

toString

Proc that returns the YAML string of a YNode

toYaml

Proc that takes an object of type T and returns a YNode.

Generic proc, should be redefined for every type. Can be autogenerated by deriveYaml

ofYaml

Proc that takes a YNode and returns an object of type T.

Generic proc, should be redefined for every type. Can be autogenerated by deriveYaml

ofYamlStr

Shortcut proc.

ofYamlStr(s,t) is equivalent to s.loadNode().ofYaml(t)

toYamlStr

Shortcut proc.

toYamlStr(x) is equivalent to x.toYaml().toString()

Code Generation

Code generation is opt-in per type. You can also write custom ofYaml/toYaml if the autogenerated ones do not work for your use-case.

Code generation supports most types you can define in Nim: objects, ref objects, enums, and variant objects are all supported.

Here's an example with multiple types that get generated and parse the YAML as expected:

import
  yanyl,
  unittest

type
  CatBreed = enum
    cbMaineCoon = "MaineCoon" 
    cbPersian = "Persian"
    cbBengal = "Bengal"
  DogBreed = enum
    dbCorgi = "Corgi"
    dbMastiff = "Mastiff"
  PetType = enum
    ptCat = "cat"
    ptDog = "dog"
  Nameable = object of RootObj
    name: string
  Pet = ref object of Nameable
    vet: Nameable
    case kind: PetType
    of ptCat:
      catBreed: CatBreed
    of ptDog:
      dogBreed: DogBreed
  Owner = object of Nameable
    paid: bool
    pets: seq[Pet]

deriveYamls:
  CatBreed
  DogBreed
  PetType
  Nameable
  Pet
  Owner

var s: string = """
name: Duncan Indigo
paid: false
pets:
- name: Ginger
  vet:
    name: Maria Belmont
  kind: cat
  catBreed: MaineCoon
- name: Buttersnap
  vet:
    name: Maria Belmont
  kind: dog
  dogBreed: Corgi
"""

let duncan = ofYamlStr(s, Owner)

check duncan.name == "Duncan Indigo"
check duncan.paid == false
check duncan.pets.len() == 2

let ginger = duncan.pets[0]
check ginger.name == "Ginger"
check ginger.kind == ptCat
check ginger.catBreed == cbMaineCoon
check ginger.vet.name == "Maria Belmont"

let buttersnap = duncan.pets[1]
check buttersnap.name == "Buttersnap"
check buttersnap.kind == ptDog
check buttersnap.dogBreed == dbCorgi
check buttersnap.vet.name == "Maria Belmont"

Viewing the generated code

If you would like to view the code generated by yanyl, you can use macros.expandMacros from the standard library. This will print the result of expanding the macros at compile time.

For example:

import
  yanyl,
  macros

type
  E = enum
    eStr, eInt
  V = object
    c: string
    case kind: E
    of eStr:
      s: string
    of eInt:
      i: int

expandMacros:
  deriveYamls:
    E
    V

will output:

proc ofYaml(n: YNode; t: typedesc[E]): E =
  case n.kind
  of ynString:
    case n.strVal
    of $eStr:
      eStr
    of $eInt:
      eInt
    else:
      raise newException(ValueError, "unknown kind: " & n.strVal)
  else:
    raise newException(ValueError, "expected string YNode")

proc toYaml(x: E): YNode =
  result = newYString($x)

proc ofYaml(n: YNode; t: typedesc[V]): V =
  case n.kind
  of ynMap:
    let kind = n.get("kind", typedesc[E])
    case kind
    of eStr:
      result = V(kind: kind, c: n.get("c", typedesc[string]),
                 s: n.get("s", typedesc[string]))
    of eInt:
      result = V(kind: kind, c: n.get("c", typedesc[string]),
                 i: n.get("i", typedesc[int]))
  else:
    raise newException(ValueError, "expected map YNode")

proc toYaml(x: V): YNode =
  case x.kind
  of eStr:
    result = newYMapRemoveNils([("kind", toYaml(x.kind)), ("c", toYaml(x.c)),
                                ("s", toYaml(x.s))])
  of eInt:
    result = newYMapRemoveNils([("kind", toYaml(x.kind)), ("c", toYaml(x.c)),
                                ("i", toYaml(x.i))])

Comparison with NimYAML

If you're using yanyl, you're already using NimYAML. Yanyl uses NimYAML as a parser, and then translates from NimYAML's YamlNode to yanyl's YNode. Yanyl discards tag information in doing so, and yanyl's toYamlStr and related functions do not emit tags either.

Why would you use Yanyl instead of just using NimYAML directly?

Simplified Output

NimYAML's output contains tags that allow it to reconstruct the object that was deserialized. Yanyl's output is a lot simpler and doesn't include any tag information.

For example, the following code

import
  yaml,
  yanyl

type
  Obj = object of RootObj
    i*: int
    s*: string

deriveYaml Obj

var o = Obj(i: 42, s: "Hello galaxy")
# NimYAML
echo dump(o)
# Yanyl
echo toYamlStr(o)

will result in the outputs

NimYAML:

%YAML 1.2
%TAG !n! tag:nimyaml.org,2016:
--- !n!custom:Obj 
i: 42
s: Hello galaxy

Yanyl:

s: Hello galaxy
i: 42

Support for more Nim types

I had trouble when using NimYAML where I was structuring my types to better work around what NimYAML could parse. I built yanyl so that I could structure my types how I normally would, and then used macro programming to make it write the ofYaml/toYaml functions that I would have.

NimYAML is more strict about types whereas Yanyl is more akin to duck-typing. If an object has the fields expected, Yanyl will stuff it into the type you specified.

Note: some of the things I thought were NimYAML's intended behavior may actually be bugs, so I will work on reporting those properly instead of just spinning off my own library.

Explicit declarations

As a newbie to Nim macro programming, I wasn't sure how or where NimYAML was creating functions. Yanyl makes it clear with its derive macros. It is more boilerplate for the developer, but I think it makes it clear and easier to debug.