blob: cd7f851fe7e02da2891c3435fd04079f3daee6a7 [file] [log] [blame]
//! 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);
}