#markup-language #parser #data-structures #schema #human-readable #hobbit

keytree

Simple markup language designed to load config files and schemas directly to Rust types

5 releases

0.2.4 Jan 23, 2020
0.2.3 Jan 21, 2020
0.2.2 Jan 16, 2020
0.2.1 Jan 16, 2020
0.2.0 Jan 16, 2020

#2439 in Parser implementations

MIT license

76KB
1.5K SLoC

KeyTree

KeyTree is an elegant markup language designed to convert human readable information into Rust data-structures. It is designed to be fast, to reduce cognitive load and to be easy to implement for one's own types. It has no dependencies on other crates and so is fast to compile. The format looks like

hobbit:
    name:           Frodo Baggins
    age:            60
    friends:
        hobbit:
            name:   Bilbo Baggins
            age:    111
        hobbit:
            name:   Samwise Gamgee
            age:    38

so data can be recursive. Also, it is easy to refer to a set of data using a path such as hobbit::friends::hobbit refers to a collection of two hobbits.

This library does not follow the standard Rust error handling pattern. If there is a parsing error it will crash or if there is an error in converting a value into a Rust type it will crash (with a nice error message). If you don't want this to happen, you will need to run this in its own thread/process.

Data Format Rules

  • Indentation has meaning and is 4 spaces, relative to the top key. Since indenting is relative to the top key, then you can neatly align strings embedded in code.

  • Each line can be empty, have whitespace only, be a comment, be a key, or be a key/value pair.

  • There are keys and values. Key/Value pairs look like

name: Frodo

are used for struct fields and enum variants.

Keys refer to child keys or child key/value pairs indented on lines under it, for example

hobbit:
    name: Frodo

hobbit refers to the name of the struct or enum. In this way, the data maps simply to Rust data-structures.

  • If a key has many children with the same key, it forms a collection, for example
hobbit:
    name: Frodo
    name: Bilbo

is a collection of hobbits.

  • Keys must not include but must be followed by a colon :.

  • Values are all characters between the combination of ':' and whitespace and the end of the line. The value is trimmed of whitespace at both ends.

  • Comments require // at the start of the line. For example

// comment
hobbit:
    name: Frodo

Example

Into from KeyTree into Rust types is automatically implemented for Vec<T>, Option<T> and basic Rust types. KeyTree text can be automatically converted to these data types, making use of type inference. The at() function returns an iterator over KeyTree types that can be used to implement Into for your own types. The following example should cover 90 percent of use cases,

use keytree::KeyTree;
use keytree::parser::KeyTreeBuilder;

#[derive(Debug)]
struct Hobbit {
    name:    String,
    age:     u32,
    friends: Vec<Hobbit>,
    nick:    Option<String>,
}

impl<'a> Into<Hobbit> for KeyTree<'a> {
    fn into(self) -> Hobbit {
        Hobbit {
            name:       self.at("hobbit::name"),
            age:        self.at("hobbit::age"),
            friends:    self.at("hobbit::friends::hobbit"),
            nick:       self.op("hobbit::nick"),
        }
    }
}

fn main() {
    let s = r#"
         hobbit:
             name:         Frodo Baggins
             age:          98
             friends:
                 hobbit:
                     name: Bilbo Baggins
                     age:  176
                 hobbit:
                     name: Samwise Gamgee
                     age:  66
                     nick: Sam"#;

    
    let core = KeyTreeBuilder::parse(s);
    let hobbit: Hobbit = KeyTree::from_core(&core).into();
    dbg!(&hobbit);
}

Details

We'll have a look in detail at what is happening in the example on the line

friends:    self.at("hobbit::friends::hobbit"),

In the Into trait impl, we want Bilbo Baggins and Samwise Gamgee to be Frodo's friends. We specify their location in the KeyTree string using a path "hobbit::friends::hobbit" which refers to two branches in the tree (two hobbits). The at() function, unlike the the op() function, requires that the branches exist. Rust infers that they need to be converted into the type Vec<Hobbit> as specified in the Hobbit struct. The Vec<T> impl of Into is supplied by KeyTree. In fact the at() function supplies an iterator over the Hobbits, which the Vec<T> impl uses.

No runtime deps