1use toml_edit::{Array, DocumentMut, InlineTable, Item, Table, Value, value};
9
10use crate::error::{Error, Result};
11
12pub fn parse(text: &str) -> Result<DocumentMut> {
14 text.parse::<DocumentMut>()
15 .map_err(|e| Error::Manifest(format!("не удалось разобрать Cargo.toml: {e}")))
16}
17
18fn dependencies_table(doc: &mut DocumentMut) -> Result<&mut Table> {
19 doc.entry("dependencies")
20 .or_insert_with(|| Item::Table(Table::new()));
21 doc.get_mut("dependencies")
22 .and_then(Item::as_table_mut)
23 .ok_or_else(|| Error::Manifest("секция [dependencies] не является таблицей".to_string()))
24}
25
26fn features_table(doc: &mut DocumentMut) -> Result<&mut Table> {
27 doc.entry("features")
28 .or_insert_with(|| Item::Table(Table::new()));
29 doc.get_mut("features")
30 .and_then(Item::as_table_mut)
31 .ok_or_else(|| Error::Manifest("секция [features] не является таблицей".to_string()))
32}
33
34fn optional_dep(kind: (&str, &str), optional: bool) -> Item {
35 let mut inline = InlineTable::new();
36 inline.insert(kind.0, Value::from(kind.1));
37 inline.insert("optional", Value::from(optional));
38 Item::Value(Value::InlineTable(inline))
39}
40
41pub fn set_workspace_optional(doc: &mut DocumentMut, name: &str) -> Result<()> {
43 let mut inline = InlineTable::new();
44 inline.insert("workspace", Value::from(true));
45 inline.insert("optional", Value::from(true));
46 dependencies_table(doc)?.insert(name, Item::Value(Value::InlineTable(inline)));
47 Ok(())
48}
49
50pub fn set_path_optional(doc: &mut DocumentMut, name: &str, path: &str) -> Result<()> {
52 dependencies_table(doc)?.insert(name, optional_dep(("path", path), true));
53 Ok(())
54}
55
56pub fn set_version_optional(doc: &mut DocumentMut, name: &str, version: &str) -> Result<()> {
58 dependencies_table(doc)?.insert(name, optional_dep(("version", version), true));
59 Ok(())
60}
61
62pub fn has_dependency(doc: &DocumentMut, name: &str) -> bool {
64 doc.get("dependencies")
65 .and_then(Item::as_table_like)
66 .is_some_and(|table| table.contains_key(name))
67}
68
69pub fn add_features_to_dependency(
74 doc: &mut DocumentMut,
75 name: &str,
76 features: &[String],
77) -> Result<()> {
78 if features.is_empty() {
79 return Ok(());
80 }
81 let entry = dependencies_table(doc)?
82 .get_mut(name)
83 .ok_or_else(|| Error::Manifest(format!("зависимость `{name}` не найдена")))?;
84
85 let table = if entry.is_str() {
86 let version = entry.as_str().unwrap_or_default().to_string();
87 let mut inline = InlineTable::new();
88 inline.insert("version", Value::from(version));
89 inline.insert("features", Value::Array(Array::new()));
90 *entry = Item::Value(Value::InlineTable(inline));
91 entry
92 .as_table_like_mut()
93 .expect("только что создали inline-таблицу")
94 } else {
95 entry.as_table_like_mut().ok_or_else(|| {
96 Error::Manifest(format!("не удалось добавить фичи зависимости `{name}`"))
97 })?
98 };
99
100 if table.get_mut("features").is_none() {
101 table.insert("features", Item::Value(Value::Array(Array::new())));
102 }
103 let array = table
104 .get_mut("features")
105 .and_then(Item::as_array_mut)
106 .ok_or_else(|| Error::Manifest(format!("features зависимости `{name}` не массив")))?;
107 for feature in features {
108 if !array
109 .iter()
110 .any(|value| value.as_str() == Some(feature.as_str()))
111 {
112 array.push(feature.clone());
113 }
114 }
115 if let Item::Value(Value::InlineTable(inline)) = entry {
116 inline.fmt();
117 }
118 Ok(())
119}
120
121fn multiline_dep_insertion(arr: &Array) -> Option<(usize, String)> {
127 if !arr.to_string().contains('\n') {
128 return None;
129 }
130 let after = arr
131 .iter()
132 .enumerate()
133 .filter(|(_, v)| v.as_str().is_some_and(|s| s.starts_with("dep:")))
134 .map(|(i, _)| i + 1)
135 .last()
136 .unwrap_or_else(|| arr.iter().count());
137 let neighbor = arr
138 .iter()
139 .nth(after.saturating_sub(1))
140 .or_else(|| arr.iter().last());
141 let prefix = neighbor
142 .and_then(|v| v.decor().prefix().and_then(|p| p.as_str()))
143 .map(|prefix| match prefix.rfind('\n') {
144 Some(index) => &prefix[index..],
147 None => prefix,
148 })
149 .filter(|prefix| prefix.contains('\n'))
150 .unwrap_or("\n ")
151 .to_string();
152 Some((after, prefix))
153}
154
155pub fn add_dep_to_feature(doc: &mut DocumentMut, feature: &str, name: &str) -> Result<bool> {
157 let table = features_table(doc)?;
158 let existed = table.contains_key(feature);
159 let entry = table.entry(feature).or_insert_with(|| value(Array::new()));
160 let arr = entry
161 .as_array_mut()
162 .ok_or_else(|| Error::Manifest(format!("фича `{feature}` не является массивом")))?;
163 let token = format!("dep:{name}");
164 if !arr.iter().any(|v| v.as_str() == Some(token.as_str())) {
165 match multiline_dep_insertion(arr) {
166 Some((index, prefix)) => {
167 let mut new_value = Value::from(token);
168 new_value.decor_mut().set_prefix(prefix);
169 arr.insert_formatted(index, new_value);
172 }
173 None => arr.push(token),
174 }
175 }
176 Ok(!existed)
177}
178
179pub fn set_dependency_version(doc: &mut DocumentMut, name: &str, version: &str) -> Result<()> {
181 let entry = dependencies_table(doc)?
182 .get_mut(name)
183 .ok_or_else(|| Error::Manifest(format!("зависимость `{name}` не найдена")))?;
184 if entry.is_str() {
185 *entry = Item::Value(Value::from(version));
186 return Ok(());
187 }
188 if let Some(table) = entry.as_table_like_mut() {
189 table.insert("version", Item::Value(Value::from(version)));
190 return Ok(());
191 }
192 Err(Error::Manifest(format!(
193 "не удалось обновить версию зависимости `{name}`"
194 )))
195}
196
197pub fn has_feature(text: &str, feature: &str) -> Result<bool> {
199 let doc = parse(text)?;
200 Ok(doc
201 .get("features")
202 .and_then(Item::as_table_like)
203 .map(|t| t.contains_key(feature))
204 .unwrap_or(false))
205}
206
207pub fn features_containing(text: &str, name: &str) -> Result<Vec<String>> {
209 let doc = parse(text)?;
210 let token = format!("dep:{name}");
211 let mut out = Vec::new();
212 if let Some(features) = doc.get("features").and_then(Item::as_table_like) {
213 for (key, item) in features.iter() {
214 if let Some(arr) = item.as_array()
215 && arr.iter().any(|v| v.as_str() == Some(token.as_str()))
216 {
217 out.push(key.to_string());
218 }
219 }
220 }
221 out.sort();
222 Ok(out)
223}
224
225pub fn features_with_light_component_dep(text: &str) -> Result<Vec<String>> {
230 let doc = parse(text)?;
231 let mut out = Vec::new();
232 if let Some(features) = doc.get("features").and_then(Item::as_table_like) {
233 for (key, item) in features.iter() {
234 if let Some(arr) = item.as_array()
235 && arr
236 .iter()
237 .any(|v| v.as_str().is_some_and(|s| s.starts_with("dep:light-")))
238 {
239 out.push(key.to_string());
240 }
241 }
242 }
243 out.sort();
244 Ok(out)
245}
246
247pub fn remove_dependency(doc: &mut DocumentMut, name: &str) -> Result<Vec<String>> {
252 if let Some(deps) = doc.get_mut("dependencies").and_then(Item::as_table_mut) {
253 deps.remove(name);
254 }
255 let mut touched = Vec::new();
256 let token = format!("dep:{name}");
257 let Some(features) = doc.get_mut("features").and_then(Item::as_table_mut) else {
258 return Ok(touched);
259 };
260 let keys: Vec<String> = features.iter().map(|(k, _)| k.to_string()).collect();
261 for key in keys {
262 let Some(arr) = features.get_mut(&key).and_then(Item::as_array_mut) else {
263 continue;
264 };
265 let before = arr.len();
266 let mut i = 0;
267 while i < arr.len() {
268 if arr.get(i).and_then(Value::as_str) == Some(token.as_str()) {
269 arr.remove(i);
270 } else {
271 i += 1;
272 }
273 }
274 if arr.len() != before {
275 touched.push(key.clone());
276 }
277 if arr.len() != before && arr.is_empty() && key != "default" {
278 features.remove(&key);
279 }
280 }
281 Ok(touched)
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 #[test]
289 fn adds_workspace_optional_dependency() {
290 let mut doc =
291 parse("[package]\nname = \"p\"\n\n[dependencies]\nlight = \"0.1\"\n").unwrap();
292 set_workspace_optional(&mut doc, "light-fw-element-form").unwrap();
293 let out = doc.to_string();
294 assert!(out.contains("light-fw-element-form = { workspace = true, optional = true }"));
295 assert!(out.contains("light = \"0.1\""), "форматирование сохранено");
296 }
297
298 #[test]
299 fn adds_version_optional_dependency() {
300 let mut doc = parse("[package]\nname = \"p\"\n").unwrap();
301 set_version_optional(&mut doc, "light-fw-element-form", "0.1.0").unwrap();
302 assert!(
303 doc.to_string()
304 .contains("light-fw-element-form = { version = \"0.1.0\", optional = true }")
305 );
306 }
307
308 #[test]
309 fn adds_features_to_string_and_table_dependencies() {
310 let mut doc = parse(
311 "[dependencies]\nlight = \"0.5.0\"\nlight-fw-element-form = { version = \"0.5.0\", optional = true }\n",
312 )
313 .unwrap();
314 add_features_to_dependency(
315 &mut doc,
316 "light",
317 &["elements-form".to_string(), "validation".to_string()],
318 )
319 .unwrap();
320 add_features_to_dependency(&mut doc, "light-fw-element-form", &["default".to_string()])
321 .unwrap();
322 add_features_to_dependency(&mut doc, "light", &["validation".to_string()]).unwrap();
323 let out = doc.to_string();
324 assert!(
325 out.contains(
326 "light = { version = \"0.5.0\", features = [\"elements-form\", \"validation\"] }"
327 ),
328 "{out}"
329 );
330 assert!(
331 out.contains("light-fw-element-form = { version = \"0.5.0\", optional = true, features = [\"default\"] }"),
332 "{out}"
333 );
334 assert_eq!(out.matches("\"validation\"").count(), 1, "{out}");
335 }
336
337 #[test]
338 fn adds_dep_to_existing_and_new_feature() {
339 let mut doc = parse("[features]\ndefault = [\"full\"]\nfull = [\"light/x\"]\n").unwrap();
340 assert!(!add_dep_to_feature(&mut doc, "full", "light-fw-element-form").unwrap());
341 assert!(add_dep_to_feature(&mut doc, "element-form", "light-fw-element-form").unwrap());
342 assert!(!add_dep_to_feature(&mut doc, "full", "light-fw-element-form").unwrap());
343 let out = doc.to_string();
344 assert!(out.contains("\"dep:light-fw-element-form\""));
345 assert!(out.contains("element-form = [\"dep:light-fw-element-form\"]"));
346 }
347
348 #[test]
349 fn adds_dep_to_multiline_feature_keeps_lines_and_comments() {
350 let text = "[features]\nfull = [\n # формы\n \"dep:light-fw-element-form\",\n]\n";
351 let mut doc = parse(text).unwrap();
352 add_dep_to_feature(&mut doc, "full", "light-widget-datepicker").unwrap();
353 assert_eq!(
354 doc.to_string(),
355 "[features]\nfull = [\n # формы\n \"dep:light-fw-element-form\",\n \"dep:light-widget-datepicker\",\n]\n"
356 );
357 }
358
359 #[test]
360 fn add_then_remove_is_byte_perfect_in_multiline_feature() {
361 let text = "[features]\ndefault = [\"full\"]\nfull = [\n \"dep:light-fw-element-form\",\n \"dep:light-fw-behavior-validation\",\n]\n\
362 minimal = [\n \"std\",\n]\n";
363 let mut doc = parse(text).unwrap();
364 add_dep_to_feature(&mut doc, "full", "light-widget-datepicker").unwrap();
365 let added = doc.to_string();
366 assert!(
367 added.contains(
368 " \"dep:light-fw-element-form\",\n \"dep:light-fw-behavior-validation\",\n \"dep:light-widget-datepicker\",\n]"
369 ),
370 "соседние dep: не склеиваются: {added}"
371 );
372 let mut doc = parse(&added).unwrap();
373 remove_dependency(&mut doc, "light-widget-datepicker").unwrap();
374 assert_eq!(doc.to_string(), text, "add+remove восстанавливает файл");
375 }
376
377 #[test]
378 fn removes_dependency_and_feature_tokens() {
379 let mut doc = parse(
380 "[dependencies]\nlight = \"0.1\"\nlight-fw-element-form = { workspace = true, optional = true }\n\n\
381 [features]\ndefault = [\"full\"]\nfull = [\"dep:light-fw-element-form\", \"light/x\"]\n\
382 element-form = [\"dep:light-fw-element-form\"]\n",
383 )
384 .unwrap();
385 let touched = remove_dependency(&mut doc, "light-fw-element-form").unwrap();
386 let mut sorted = touched.clone();
387 sorted.sort();
388 assert_eq!(sorted, vec!["element-form", "full"]);
389 let out = doc.to_string();
390 assert!(!out.contains("light-fw-element-form"));
391 assert!(!out.contains("dep:"));
392 assert!(out.contains("light/x"));
393 assert!(out.contains("default"));
394 assert!(out.contains("full"));
395 }
396
397 #[test]
398 fn sets_dependency_version_string_and_table() {
399 let mut doc = parse(
400 "[dependencies]\nlight-fw-element-form = \"0.0.1\"\nlight = { version = \"0.1\", features = [\"x\"] }\n",
401 )
402 .unwrap();
403 set_dependency_version(&mut doc, "light-fw-element-form", "0.1.0").unwrap();
404 set_dependency_version(&mut doc, "light", "0.2.0").unwrap();
405 let out = doc.to_string();
406 assert!(out.contains("light-fw-element-form = \"0.1.0\""));
407 assert!(out.contains("version = \"0.2.0\""));
408 assert!(
409 out.contains("features = [\"x\"]"),
410 "features сохранены: {out}"
411 );
412 }
413
414 #[test]
415 fn remove_keeps_preexisting_empty_features() {
416 let mut doc = parse(
417 "[dependencies]\nlight-fw-element-form = { workspace = true, optional = true }\n\n\
418 [features]\nminimal = []\nelement-form = [\"dep:light-fw-element-form\"]\n",
419 )
420 .unwrap();
421 let touched = remove_dependency(&mut doc, "light-fw-element-form").unwrap();
422 assert_eq!(touched, vec!["element-form"]);
423 let out = doc.to_string();
424 assert!(out.contains("minimal = []"), "пустая фича сохранена: {out}");
425 assert!(
426 !out.contains("element-form"),
427 "опустевшая из-за удаления фича удалена: {out}"
428 );
429 }
430
431 #[test]
432 fn features_containing_reports_matches() {
433 let text = "[features]\nfull = [\"dep:light-fw-element-form\"]\nminimal = []\n";
434 assert_eq!(
435 features_containing(text, "light-fw-element-form").unwrap(),
436 vec!["full"]
437 );
438 assert!(has_feature(text, "full").unwrap());
439 assert!(!has_feature(text, "nope").unwrap());
440 }
441}