toml_edit 0.17.0

Yet another format-preserving TOML parser.
Documentation
//! Example for how to use `VisitMut` to iterate over a table.

use std::collections::BTreeSet;
use toml_edit::visit::*;
use toml_edit::visit_mut::*;
use toml_edit::{Array, Document, InlineTable, Item, KeyMut, Table, Value};

/// This models the visit state for dependency keys in a `Cargo.toml`.
///
/// Dependencies can be specified as:
///
/// ```toml
/// [dependencies]
/// dep1 = "0.2"
///
/// [build-dependencies]
/// dep2 = "0.3"
///
/// [dev-dependencies]
/// dep3 = "0.4"
///
/// [target.'cfg(windows)'.dependencies]
/// dep4 = "0.5"
///
/// # and target build- and dev-dependencies
/// ```
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
enum VisitState {
    /// Represents the root of the table.
    Root,
    /// Represents "dependencies", "build-dependencies" or "dev-dependencies", or the target
    /// forms of these.
    Dependencies,
    /// A table within dependencies.
    SubDependencies,
    /// Represents "target".
    Target,
    /// "target.[TARGET]".
    TargetWithSpec,
    /// Represents some other state.
    Other,
}

impl VisitState {
    /// Figures out the next visit state, given the current state and the given key.
    fn descend(self, key: &str) -> Self {
        match (self, key) {
            (
                VisitState::Root | VisitState::TargetWithSpec,
                "dependencies" | "build-dependencies" | "dev-dependencies",
            ) => VisitState::Dependencies,
            (VisitState::Root, "target") => VisitState::Target,
            (VisitState::Root | VisitState::TargetWithSpec, _) => VisitState::Other,
            (VisitState::Target, _) => VisitState::TargetWithSpec,
            (VisitState::Dependencies, _) => VisitState::SubDependencies,
            (VisitState::SubDependencies, _) => VisitState::SubDependencies,
            (VisitState::Other, _) => VisitState::Other,
        }
    }
}

/// Collect the names of every dependency key.
#[derive(Debug)]
struct DependencyNameVisitor<'doc> {
    state: VisitState,
    names: BTreeSet<&'doc str>,
}

impl<'doc> Visit<'doc> for DependencyNameVisitor<'doc> {
    fn visit_table_like_kv(&mut self, key: &'doc str, node: &'doc Item) {
        if self.state == VisitState::Dependencies {
            self.names.insert(key);
        } else {
            // Since we're only interested in collecting the top-level keys right under
            // [dependencies], don't recurse unconditionally.

            let old_state = self.state;

            // Figure out the next state given the key.
            self.state = self.state.descend(key);

            // Recurse further into the document tree.
            visit_table_like_kv(self, key, node);

            // Restore the old state after it's done.
            self.state = old_state;
        }
    }
}

/// Normalize all dependency tables into the format:
///
/// ```toml
/// [dependencies]
/// dep = { version = "1.0", features = ["foo", "bar"], ... }
/// ```
///
/// leaving other tables untouched.
#[derive(Debug)]
struct NormalizeDependencyTablesVisitor {
    state: VisitState,
}

impl VisitMut for NormalizeDependencyTablesVisitor {
    fn visit_table_mut(&mut self, node: &mut Table) {
        visit_table_mut(self, node);

        // The conversion from regular tables into inline ones might leave some explicit parent
        // tables hanging, so convert them to implicit.
        if matches!(self.state, VisitState::Target | VisitState::TargetWithSpec) {
            node.set_implicit(true);
        }
    }

    fn visit_table_like_kv_mut(&mut self, mut key: KeyMut<'_>, node: &mut Item) {
        let old_state = self.state;

        // Figure out the next state given the key.
        self.state = self.state.descend(key.get());

        match self.state {
            VisitState::Target | VisitState::TargetWithSpec | VisitState::Dependencies => {
                // Top-level dependency row, or above: turn inline tables into regular ones.
                if let Item::Value(Value::InlineTable(inline_table)) = node {
                    let inline_table = std::mem::replace(inline_table, InlineTable::new());
                    let table = inline_table.into_table();
                    key.fmt();
                    *node = Item::Table(table);
                }
            }
            VisitState::SubDependencies => {
                // Individual dependency: turn regular tables into inline ones.
                if let Item::Table(table) = node {
                    // Turn the table into an inline table.
                    let table = std::mem::replace(table, Table::new());
                    let inline_table = table.into_inline_table();
                    key.fmt();
                    *node = Item::Value(Value::InlineTable(inline_table));
                }
            }
            _ => {}
        }

        // Recurse further into the document tree.
        visit_table_like_kv_mut(self, key, node);

        // Restore the old state after it's done.
        self.state = old_state;
    }

    fn visit_array_mut(&mut self, node: &mut Array) {
        // Format any arrays within dependencies to be on the same line.
        if matches!(
            self.state,
            VisitState::Dependencies | VisitState::SubDependencies
        ) {
            node.fmt();
        }
    }
}

/// This is the input provided to visit_mut_example.
static INPUT: &str = r#"
[package]
name = "my-package"

[package.metadata.foo]
bar = 42

[dependencies]
atty = "0.2"
cargo-platform = { path = "crates/cargo-platform", version = "0.1.2" }

[dependencies.pretty_env_logger]
version = "0.4"
optional = true

[target.'cfg(windows)'.dependencies]
fwdansi = "1.1.0"

[target.'cfg(windows)'.dependencies.winapi]
version = "0.3"
features = [
"handleapi",
"jobapi",
]

[target.'cfg(unix)']
dev-dependencies = { miniz_oxide = "0.5" }

[dev-dependencies.cargo-test-macro]
path = "crates/cargo-test-macro"

[build-dependencies.flate2]
version = "0.4"
"#;

/// This is the output produced by visit_mut_example.
#[cfg(test)]
static VISIT_MUT_OUTPUT: &str = r#"
[package]
name = "my-package"

[package.metadata.foo]
bar = 42

[dependencies]
atty = "0.2"
cargo-platform = { path = "crates/cargo-platform", version = "0.1.2" }
pretty_env_logger = { version = "0.4", optional = true }

[target.'cfg(windows)'.dependencies]
fwdansi = "1.1.0"
winapi = { version = "0.3", features = ["handleapi", "jobapi"] }

[target.'cfg(unix)'.dev-dependencies]
miniz_oxide = "0.5"

[dev-dependencies]
cargo-test-macro = { path = "crates/cargo-test-macro" }

[build-dependencies]
flate2 = { version = "0.4" }
"#;

fn visit_example(document: &Document) -> BTreeSet<&str> {
    let mut visitor = DependencyNameVisitor {
        state: VisitState::Root,
        names: BTreeSet::new(),
    };

    visitor.visit_document(document);

    visitor.names
}

fn visit_mut_example(document: &mut Document) {
    let mut visitor = NormalizeDependencyTablesVisitor {
        state: VisitState::Root,
    };

    visitor.visit_document_mut(document);
}

fn main() {
    let mut document: Document = INPUT.parse().expect("input is valid TOML");

    println!("** visit example");
    println!("{:?}", visit_example(&document));

    println!("** visit_mut example");
    visit_mut_example(&mut document);
    println!("{}", document);
}

#[cfg(test)]
#[test]
fn visit_correct() {
    let document: Document = INPUT.parse().expect("input is valid TOML");

    let names = visit_example(&document);
    let expected = vec![
        "atty",
        "cargo-platform",
        "pretty_env_logger",
        "fwdansi",
        "winapi",
        "miniz_oxide",
        "cargo-test-macro",
        "flate2",
    ]
    .into_iter()
    .collect();
    assert_eq!(names, expected);
}

#[cfg(test)]
#[test]
fn visit_mut_correct() {
    let mut document: Document = INPUT.parse().expect("input is valid TOML");

    visit_mut_example(&mut document);
    assert_eq!(format!("{}", document), VISIT_MUT_OUTPUT);
}