A library for working with YAML in Nim. Can generate conversion functions for most types declared in Nim.
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"
Add the following to your .nimble
file:
requires "yanyl"
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"
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
Proc that returns the YAML string of a YNode
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
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
Shortcut proc.
ofYamlStr(s,t)
is equivalent to s.loadNode().ofYaml(t)
Shortcut proc.
toYamlStr(x)
is equivalent to x.toYaml().toString()
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: object
s, ref object
s, enum
s, 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"
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?
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
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.
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.