//! Example for how to use `VisitMut` to iterate over a table. use std::collections::BTreeSet; use toml_edit::visit::{visit_table_like_kv, Visit}; use toml_edit::visit_mut::{visit_table_like_kv_mut, visit_table_mut, VisitMut}; use toml_edit::{Array, DocumentMut, 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: &DocumentMut) -> BTreeSet<&str> { let mut visitor = DependencyNameVisitor { state: VisitState::Root, names: BTreeSet::new(), }; visitor.visit_document(document); visitor.names } fn visit_mut_example(document: &mut DocumentMut) { let mut visitor = NormalizeDependencyTablesVisitor { state: VisitState::Root, }; visitor.visit_document_mut(document); } fn main() { let mut document: DocumentMut = 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: DocumentMut = 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: DocumentMut = INPUT.parse().expect("input is valid TOML"); visit_mut_example(&mut document); assert_eq!(format!("{}", document), VISIT_MUT_OUTPUT); }