tom 0.0.1

Yet another format-preserving TOML parser/manipulator.
Documentation
extern crate tom;

use std::iter;
use tom::{
    TomlDoc, CstNode, Symbol, TextRange, Position::*,
    ast::{self, EntryOwner},
    symbol::*,
};

fn main() {
    let text = "\
title = \"TOML Example\"

[owner]
name = \"Tom Preston-Werner\"
dob = 1979-05-27T07:32:00-08:00 # First class dates
";

    // `TomlDoc` is the API entry point.
    let doc = TomlDoc::new(text);

    // Internally, `TomlDoc` is represented as a homogeneous
    // Concrete Syntax Tree (CST) which includes whitespace and
    // comments explicitely. `.debug` method can be used to see
    // this representation:
    assert_eq!(doc.debug().trim(), r##"
DOC@[0; 112)
  ENTRY@[0; 22)
    KEY@[0; 5)
      BARE_KEY@[0; 5) "title"
    WHITESPACE@[5; 6)
    EQ@[6; 7) "="
    WHITESPACE@[7; 8)
    VALUE@[8; 22)
      BASIC_STRING@[8; 22) "\"TOML Example\""
  WHITESPACE@[22; 24)
  TABLE@[24; 111)
    TABLE_HEADER@[24; 31)
      L_BRACK@[24; 25) "["
      KEY@[25; 30)
        BARE_KEY@[25; 30) "owner"
      R_BRACK@[30; 31) "]"
    WHITESPACE@[31; 32)
    ENTRY@[32; 59)
      KEY@[32; 36)
        BARE_KEY@[32; 36) "name"
      WHITESPACE@[36; 37)
      EQ@[37; 38) "="
      WHITESPACE@[38; 39)
      VALUE@[39; 59)
        BASIC_STRING@[39; 59) "\"Tom Preston-Werner\""
    WHITESPACE@[59; 60)
    ENTRY@[60; 111)
      KEY@[60; 63)
        BARE_KEY@[60; 63) "dob"
      WHITESPACE@[63; 64)
      EQ@[64; 65) "="
      WHITESPACE@[65; 66)
      VALUE@[66; 91)
        DATE_TIME@[66; 91) "1979-05-27T07:32:00-08:00"
      WHITESPACE@[91; 92)
      COMMENT@[92; 111) "# First class dates"
  WHITESPACE@[111; 112)
"##.trim());

    // Note that `new` method does not return a `Result`.
    // This is because the library can parse even partially
    // invalid toml documents. Use `.errors` method to check
    // if file has any syntax errors;
    let invalid_toml = TomlDoc::new(":-)\nfoo=1");
    assert_eq!(invalid_toml.debug().trim(), r#"
DOC@[0; 9)
  ERROR@[0; 1)
    ERROR@[0; 1) ":"
  ENTRY@[1; 9)
    KEY@[1; 2)
      BARE_KEY@[1; 2) "-"
    ERROR@[2; 3)
      ERROR@[2; 3) ")"
    WHITESPACE@[3; 4)
    KEY@[4; 7)
      BARE_KEY@[4; 7) "foo"
    EQ@[7; 8) "="
    VALUE@[8; 9)
      NUMBER@[8; 9) "1"

error@[0; 1) ":": expected a key
error@[2; 3) ")": expected `.`
error@[1; 8) "-)\nfoo=": newlines are forbidden in entries"#.trim());
    assert_eq!(invalid_toml.errors().len(), 3);

    // To access CST, use `.cst` method.
    let root: CstNode = doc.cst();

    // A CST node has a `Symbol`, which says what kind
    // of syntactic construct this node represents, a
    // range in the text and links to children, parent,
    // and siblings.
    //
    // Note that every method on `CstNode` needs a `&TomlDoc` argument.
    // This is because internally a `CstNode` is just an index, and it
    // needs the document to extract information.
    assert_eq!(root.symbol(&doc), DOC);
    assert_eq!(root.children(&doc).iter().count(), 4);

    let trailing_newline = root.children(&doc).last().unwrap();
    let ws_symbol: Symbol = trailing_newline.symbol(&doc);
    assert_eq!(ws_symbol, WHITESPACE);
    assert_eq!(trailing_newline.get_text(&doc), "\n");

    let title = root.children(&doc).first().unwrap();
    assert_eq!(title.symbol(&doc), ENTRY);
    assert_eq!(title.range(&doc), TextRange::from_to(0.into(), 22.into()));
    assert_eq!(title.parent(&doc), Some(root));
    assert_eq!(title.get_text(&doc), "title = \"TOML Example\"");

    // Working with CST directly is powerful, because you get access to
    // all comments, punctuation and whitespace, but it is not convenient
    // if you need the meaning of the toml document. For that purpose,
    // a higher-level `ast` API exists.
    let root: ast::Doc = doc.ast();
    assert_eq!(root.entries(&doc).count(), 1);
    assert_eq!(root.tables(&doc).count(), 1);
    let table: ast::Table = root.tables(&doc).next().unwrap();
    assert_eq!(table.entries(&doc).count(), 2);
    let entry: ast::Entry = table.entries(&doc).next().unwrap();
    match entry.value(&doc).kind(&doc) {
        ast::ValueKind::StringLit(lit) => {
            assert_eq!(lit.value(&doc), "Tom Preston-Werner");
        },
        _ => panic!("unexpected entry")
    }

    // Internally, each AST node is a wrapper of the corresponding CST node,
    // and you can use `.cst()` and `::cast()` methods to convert between the two:
    let entry_raw = entry.cst();
    assert_eq!(entry_raw.get_text(&doc), "name = \"Tom Preston-Werner\"");
    match ast::Entry::cast(entry_raw, &doc) {
        Some(entry) => assert_eq!(entry.keys(&doc).next().unwrap().name(&doc), "name"),
        None => panic!("can't cast a node to entry"),
    }

    // Let's see how the API for modifying the document works.
    // First, we need to call `.start_edit` method. This is required
    // for two reasons:
    //   * A small edit can change the ranges of a large number of CST nodes
    //     which makes maintaining correct ranges costly. Thus, in edit mode
    //     ranges are assumed to be invalid and `CstNode::range` panics.
    //   * By design, arbitrary CST structure can be created during edits.
    //     That means that methods that assume that, for example, `ast::Value`'s
    //     parent is always an `ast::Entry` may panic as well.
    let mut doc = doc;
    doc.start_edit();

    // The primary way to create new elements is the family of
    // `TomlDoc::new_foo_from_text` methods. Using raw text, you
    // have full control of formatting, whitespace and comments:
    let new_entry: ast::Entry = doc.new_entry_from_text("foo= 92 #comments are preserved");
    assert_eq!(new_entry.cst().get_text(&doc), "foo= 92 #comments are preserved");

    // If the text can not be parsed as a requested syntactic construct,
    // the call will panic:
    //
    // doc.new_entry_from_text(":("); // panics!

    // You can also create new nodes in a typed way from components,
    // using `TomlDoc::new_foo` family of methods.
    let new_entry: ast::Entry = {
        let key: ast::Key = doc.new_key("foo");
        let value: ast::Value = doc.new_value(92);
        doc.new_entry(iter::once(key), value)
    };
    assert_eq!(new_entry.cst().get_text(&doc), "foo = 92");

    // `TomlDoc` has several methods which manipulate trees:
    // `replace`, `detach`, `insert`.
    // Because each node is an index, and not a reference, we
    // are able mutate document without invalidating existing
    // nodes.
    doc.replace(entry, new_entry);
    assert_eq!(doc.cst().get_text(&doc), "\
title = \"TOML Example\"

[owner]
foo = 92
dob = 1979-05-27T07:32:00-08:00 # First class dates
");

    doc.detach(new_entry);
    assert_eq!(doc.cst().get_text(&doc), "\
title = \"TOML Example\"

[owner]
dob = 1979-05-27T07:32:00-08:00 # First class dates
");

    doc.insert(new_entry, PrependTo(root.cst()));
    assert_eq!(doc.cst().get_text(&doc), "\
foo = 92
title = \"TOML Example\"

[owner]
dob = 1979-05-27T07:32:00-08:00 # First class dates
");

    // AST nodes have type-safe mutating methods as well,
    // although currently only few are actually implemented :)
    let e1 = doc.new_entry_from_text("foo = 1");
    let e2 = doc.new_entry_from_text("bar = 2");

    let d = doc.new_dict_from_text("{}");
    assert_eq!(d.cst().get_text(&doc), "{}");

    d.append_entry(&mut doc, e1);
    assert_eq!(d.cst().get_text(&doc), "{ foo = 1 }");

    d.append_entry(&mut doc, e2);
    assert_eq!(d.cst().get_text(&doc), "{ foo = 1, bar = 2 }");

    // Remember that we've begun the editing with `.start_edit` call?
    // We have two ways to finish edit, with different tradeoffs.
    //
    // `doc.finish_edit_no_reparse();` will recalculate correct
    // ranges for elements, but it **will not** check that CST
    // structure is valid. However, all existing CST and AST
    // nodes (which, as a reminder, are indices), remain valid.
    //
    // `doc.finish_edit_full_reparse();` will reparse the document,
    // creating a fresh, valid CST. Because the CST structure might
    // change after the reparse, old indices become invalid.
    doc.finish_edit_no_reparse();
}