Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/tanelso2/yanyl

YAML library for Nim
https://github.com/tanelso2/yanyl

Last synced: 7 days ago
JSON representation

YAML library for Nim

Awesome Lists containing this project

README

        

# 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](https://tanelso2.github.io/yanyl/index.html)

# Example

```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"
```

# 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.

```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"
```

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

```nim
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: `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:

```nim
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:

```nim
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:

```nim
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](https://github.com/flyx/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

```nim
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
%YAML 1.2
%TAG !n! tag:nimyaml.org,2016:
--- !n!custom:Obj
i: 42
s: Hello galaxy
```

Yanyl:

```yaml
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.