1 //! Example for how to use `VisitMut` to iterate over a table.
2
3 use std::collections::BTreeSet;
4 use toml_edit::visit::*;
5 use toml_edit::visit_mut::*;
6 use toml_edit::{Array, Document, InlineTable, Item, KeyMut, Table, Value};
7
8 /// This models the visit state for dependency keys in a `Cargo.toml`.
9 ///
10 /// Dependencies can be specified as:
11 ///
12 /// ```toml
13 /// [dependencies]
14 /// dep1 = "0.2"
15 ///
16 /// [build-dependencies]
17 /// dep2 = "0.3"
18 ///
19 /// [dev-dependencies]
20 /// dep3 = "0.4"
21 ///
22 /// [target.'cfg(windows)'.dependencies]
23 /// dep4 = "0.5"
24 ///
25 /// # and target build- and dev-dependencies
26 /// ```
27 #[derive(Copy, Clone, Debug, Eq, PartialEq)]
28 enum VisitState {
29 /// Represents the root of the table.
30 Root,
31 /// Represents "dependencies", "build-dependencies" or "dev-dependencies", or the target
32 /// forms of these.
33 Dependencies,
34 /// A table within dependencies.
35 SubDependencies,
36 /// Represents "target".
37 Target,
38 /// "target.[TARGET]".
39 TargetWithSpec,
40 /// Represents some other state.
41 Other,
42 }
43
44 impl VisitState {
45 /// Figures out the next visit state, given the current state and the given key.
descend(self, key: &str) -> Self46 fn descend(self, key: &str) -> Self {
47 match (self, key) {
48 (
49 VisitState::Root | VisitState::TargetWithSpec,
50 "dependencies" | "build-dependencies" | "dev-dependencies",
51 ) => VisitState::Dependencies,
52 (VisitState::Root, "target") => VisitState::Target,
53 (VisitState::Root | VisitState::TargetWithSpec, _) => VisitState::Other,
54 (VisitState::Target, _) => VisitState::TargetWithSpec,
55 (VisitState::Dependencies, _) => VisitState::SubDependencies,
56 (VisitState::SubDependencies, _) => VisitState::SubDependencies,
57 (VisitState::Other, _) => VisitState::Other,
58 }
59 }
60 }
61
62 /// Collect the names of every dependency key.
63 #[derive(Debug)]
64 struct DependencyNameVisitor<'doc> {
65 state: VisitState,
66 names: BTreeSet<&'doc str>,
67 }
68
69 impl<'doc> Visit<'doc> for DependencyNameVisitor<'doc> {
visit_table_like_kv(&mut self, key: &'doc str, node: &'doc Item)70 fn visit_table_like_kv(&mut self, key: &'doc str, node: &'doc Item) {
71 if self.state == VisitState::Dependencies {
72 self.names.insert(key);
73 } else {
74 // Since we're only interested in collecting the top-level keys right under
75 // [dependencies], don't recurse unconditionally.
76
77 let old_state = self.state;
78
79 // Figure out the next state given the key.
80 self.state = self.state.descend(key);
81
82 // Recurse further into the document tree.
83 visit_table_like_kv(self, key, node);
84
85 // Restore the old state after it's done.
86 self.state = old_state;
87 }
88 }
89 }
90
91 /// Normalize all dependency tables into the format:
92 ///
93 /// ```toml
94 /// [dependencies]
95 /// dep = { version = "1.0", features = ["foo", "bar"], ... }
96 /// ```
97 ///
98 /// leaving other tables untouched.
99 #[derive(Debug)]
100 struct NormalizeDependencyTablesVisitor {
101 state: VisitState,
102 }
103
104 impl VisitMut for NormalizeDependencyTablesVisitor {
visit_table_mut(&mut self, node: &mut Table)105 fn visit_table_mut(&mut self, node: &mut Table) {
106 visit_table_mut(self, node);
107
108 // The conversion from regular tables into inline ones might leave some explicit parent
109 // tables hanging, so convert them to implicit.
110 if matches!(self.state, VisitState::Target | VisitState::TargetWithSpec) {
111 node.set_implicit(true);
112 }
113 }
114
visit_table_like_kv_mut(&mut self, mut key: KeyMut<'_>, node: &mut Item)115 fn visit_table_like_kv_mut(&mut self, mut key: KeyMut<'_>, node: &mut Item) {
116 let old_state = self.state;
117
118 // Figure out the next state given the key.
119 self.state = self.state.descend(key.get());
120
121 match self.state {
122 VisitState::Target | VisitState::TargetWithSpec | VisitState::Dependencies => {
123 // Top-level dependency row, or above: turn inline tables into regular ones.
124 if let Item::Value(Value::InlineTable(inline_table)) = node {
125 let inline_table = std::mem::replace(inline_table, InlineTable::new());
126 let table = inline_table.into_table();
127 key.fmt();
128 *node = Item::Table(table);
129 }
130 }
131 VisitState::SubDependencies => {
132 // Individual dependency: turn regular tables into inline ones.
133 if let Item::Table(table) = node {
134 // Turn the table into an inline table.
135 let table = std::mem::replace(table, Table::new());
136 let inline_table = table.into_inline_table();
137 key.fmt();
138 *node = Item::Value(Value::InlineTable(inline_table));
139 }
140 }
141 _ => {}
142 }
143
144 // Recurse further into the document tree.
145 visit_table_like_kv_mut(self, key, node);
146
147 // Restore the old state after it's done.
148 self.state = old_state;
149 }
150
visit_array_mut(&mut self, node: &mut Array)151 fn visit_array_mut(&mut self, node: &mut Array) {
152 // Format any arrays within dependencies to be on the same line.
153 if matches!(
154 self.state,
155 VisitState::Dependencies | VisitState::SubDependencies
156 ) {
157 node.fmt();
158 }
159 }
160 }
161
162 /// This is the input provided to visit_mut_example.
163 static INPUT: &str = r#"
164 [package]
165 name = "my-package"
166
167 [package.metadata.foo]
168 bar = 42
169
170 [dependencies]
171 atty = "0.2"
172 cargo-platform = { path = "crates/cargo-platform", version = "0.1.2" }
173
174 [dependencies.pretty_env_logger]
175 version = "0.4"
176 optional = true
177
178 [target.'cfg(windows)'.dependencies]
179 fwdansi = "1.1.0"
180
181 [target.'cfg(windows)'.dependencies.winapi]
182 version = "0.3"
183 features = [
184 "handleapi",
185 "jobapi",
186 ]
187
188 [target.'cfg(unix)']
189 dev-dependencies = { miniz_oxide = "0.5" }
190
191 [dev-dependencies.cargo-test-macro]
192 path = "crates/cargo-test-macro"
193
194 [build-dependencies.flate2]
195 version = "0.4"
196 "#;
197
198 /// This is the output produced by visit_mut_example.
199 #[cfg(test)]
200 static VISIT_MUT_OUTPUT: &str = r#"
201 [package]
202 name = "my-package"
203
204 [package.metadata.foo]
205 bar = 42
206
207 [dependencies]
208 atty = "0.2"
209 cargo-platform = { path = "crates/cargo-platform", version = "0.1.2" }
210 pretty_env_logger = { version = "0.4", optional = true }
211
212 [target.'cfg(windows)'.dependencies]
213 fwdansi = "1.1.0"
214 winapi = { version = "0.3", features = ["handleapi", "jobapi"] }
215
216 [target.'cfg(unix)'.dev-dependencies]
217 miniz_oxide = "0.5"
218
219 [dev-dependencies]
220 cargo-test-macro = { path = "crates/cargo-test-macro" }
221
222 [build-dependencies]
223 flate2 = { version = "0.4" }
224 "#;
225
visit_example(document: &Document) -> BTreeSet<&str>226 fn visit_example(document: &Document) -> BTreeSet<&str> {
227 let mut visitor = DependencyNameVisitor {
228 state: VisitState::Root,
229 names: BTreeSet::new(),
230 };
231
232 visitor.visit_document(document);
233
234 visitor.names
235 }
236
visit_mut_example(document: &mut Document)237 fn visit_mut_example(document: &mut Document) {
238 let mut visitor = NormalizeDependencyTablesVisitor {
239 state: VisitState::Root,
240 };
241
242 visitor.visit_document_mut(document);
243 }
244
main()245 fn main() {
246 let mut document: Document = INPUT.parse().expect("input is valid TOML");
247
248 println!("** visit example");
249 println!("{:?}", visit_example(&document));
250
251 println!("** visit_mut example");
252 visit_mut_example(&mut document);
253 println!("{}", document);
254 }
255
256 #[cfg(test)]
257 #[test]
visit_correct()258 fn visit_correct() {
259 let document: Document = INPUT.parse().expect("input is valid TOML");
260
261 let names = visit_example(&document);
262 let expected = vec![
263 "atty",
264 "cargo-platform",
265 "pretty_env_logger",
266 "fwdansi",
267 "winapi",
268 "miniz_oxide",
269 "cargo-test-macro",
270 "flate2",
271 ]
272 .into_iter()
273 .collect();
274 assert_eq!(names, expected);
275 }
276
277 #[cfg(test)]
278 #[test]
visit_mut_correct()279 fn visit_mut_correct() {
280 let mut document: Document = INPUT.parse().expect("input is valid TOML");
281
282 visit_mut_example(&mut document);
283 assert_eq!(format!("{}", document), VISIT_MUT_OUTPUT);
284 }
285