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
62fn multiline_dep_insertion(arr: &Array) -> Option<(usize, String)> {
68 if !arr.to_string().contains('\n') {
69 return None;
70 }
71 let after = arr
72 .iter()
73 .enumerate()
74 .filter(|(_, v)| v.as_str().is_some_and(|s| s.starts_with("dep:")))
75 .map(|(i, _)| i + 1)
76 .last()
77 .unwrap_or_else(|| arr.iter().count());
78 let neighbor = arr
79 .iter()
80 .nth(after.saturating_sub(1))
81 .or_else(|| arr.iter().last());
82 let prefix = neighbor
83 .and_then(|v| v.decor().prefix().and_then(|p| p.as_str()))
84 .map(|prefix| match prefix.rfind('\n') {
85 Some(index) => &prefix[index..],
88 None => prefix,
89 })
90 .filter(|prefix| prefix.contains('\n'))
91 .unwrap_or("\n ")
92 .to_string();
93 Some((after, prefix))
94}
95
96pub fn add_dep_to_feature(doc: &mut DocumentMut, feature: &str, name: &str) -> Result<bool> {
98 let table = features_table(doc)?;
99 let existed = table.contains_key(feature);
100 let entry = table.entry(feature).or_insert_with(|| value(Array::new()));
101 let arr = entry
102 .as_array_mut()
103 .ok_or_else(|| Error::Manifest(format!("фича `{feature}` не является массивом")))?;
104 let token = format!("dep:{name}");
105 if !arr.iter().any(|v| v.as_str() == Some(token.as_str())) {
106 match multiline_dep_insertion(arr) {
107 Some((index, prefix)) => {
108 let mut new_value = Value::from(token);
109 new_value.decor_mut().set_prefix(prefix);
110 arr.insert_formatted(index, new_value);
113 }
114 None => arr.push(token),
115 }
116 }
117 Ok(!existed)
118}
119
120pub fn set_dependency_version(doc: &mut DocumentMut, name: &str, version: &str) -> Result<()> {
122 let entry = dependencies_table(doc)?
123 .get_mut(name)
124 .ok_or_else(|| Error::Manifest(format!("зависимость `{name}` не найдена")))?;
125 if entry.is_str() {
126 *entry = Item::Value(Value::from(version));
127 return Ok(());
128 }
129 if let Some(table) = entry.as_table_like_mut() {
130 table.insert("version", Item::Value(Value::from(version)));
131 return Ok(());
132 }
133 Err(Error::Manifest(format!(
134 "не удалось обновить версию зависимости `{name}`"
135 )))
136}
137
138pub fn has_feature(text: &str, feature: &str) -> Result<bool> {
140 let doc = parse(text)?;
141 Ok(doc
142 .get("features")
143 .and_then(Item::as_table_like)
144 .map(|t| t.contains_key(feature))
145 .unwrap_or(false))
146}
147
148pub fn features_containing(text: &str, name: &str) -> Result<Vec<String>> {
150 let doc = parse(text)?;
151 let token = format!("dep:{name}");
152 let mut out = Vec::new();
153 if let Some(features) = doc.get("features").and_then(Item::as_table_like) {
154 for (key, item) in features.iter() {
155 if let Some(arr) = item.as_array()
156 && arr.iter().any(|v| v.as_str() == Some(token.as_str()))
157 {
158 out.push(key.to_string());
159 }
160 }
161 }
162 out.sort();
163 Ok(out)
164}
165
166pub fn features_with_light_component_dep(text: &str) -> Result<Vec<String>> {
171 let doc = parse(text)?;
172 let mut out = Vec::new();
173 if let Some(features) = doc.get("features").and_then(Item::as_table_like) {
174 for (key, item) in features.iter() {
175 if let Some(arr) = item.as_array()
176 && arr
177 .iter()
178 .any(|v| v.as_str().is_some_and(|s| s.starts_with("dep:light-")))
179 {
180 out.push(key.to_string());
181 }
182 }
183 }
184 out.sort();
185 Ok(out)
186}
187
188pub fn remove_dependency(doc: &mut DocumentMut, name: &str) -> Result<Vec<String>> {
193 if let Some(deps) = doc.get_mut("dependencies").and_then(Item::as_table_mut) {
194 deps.remove(name);
195 }
196 let mut touched = Vec::new();
197 let token = format!("dep:{name}");
198 let Some(features) = doc.get_mut("features").and_then(Item::as_table_mut) else {
199 return Ok(touched);
200 };
201 let keys: Vec<String> = features.iter().map(|(k, _)| k.to_string()).collect();
202 for key in keys {
203 let Some(arr) = features.get_mut(&key).and_then(Item::as_array_mut) else {
204 continue;
205 };
206 let before = arr.len();
207 let mut i = 0;
208 while i < arr.len() {
209 if arr.get(i).and_then(Value::as_str) == Some(token.as_str()) {
210 arr.remove(i);
211 } else {
212 i += 1;
213 }
214 }
215 if arr.len() != before {
216 touched.push(key.clone());
217 }
218 if arr.len() != before && arr.is_empty() && key != "default" {
219 features.remove(&key);
220 }
221 }
222 Ok(touched)
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn adds_workspace_optional_dependency() {
231 let mut doc =
232 parse("[package]\nname = \"p\"\n\n[dependencies]\nlight = \"0.1\"\n").unwrap();
233 set_workspace_optional(&mut doc, "light-fw-element-form").unwrap();
234 let out = doc.to_string();
235 assert!(out.contains("light-fw-element-form = { workspace = true, optional = true }"));
236 assert!(out.contains("light = \"0.1\""), "форматирование сохранено");
237 }
238
239 #[test]
240 fn adds_version_optional_dependency() {
241 let mut doc = parse("[package]\nname = \"p\"\n").unwrap();
242 set_version_optional(&mut doc, "light-fw-element-form", "0.1.0").unwrap();
243 assert!(
244 doc.to_string()
245 .contains("light-fw-element-form = { version = \"0.1.0\", optional = true }")
246 );
247 }
248
249 #[test]
250 fn adds_dep_to_existing_and_new_feature() {
251 let mut doc = parse("[features]\ndefault = [\"full\"]\nfull = [\"light/x\"]\n").unwrap();
252 assert!(!add_dep_to_feature(&mut doc, "full", "light-fw-element-form").unwrap());
253 assert!(add_dep_to_feature(&mut doc, "element-form", "light-fw-element-form").unwrap());
254 assert!(!add_dep_to_feature(&mut doc, "full", "light-fw-element-form").unwrap());
255 let out = doc.to_string();
256 assert!(out.contains("\"dep:light-fw-element-form\""));
257 assert!(out.contains("element-form = [\"dep:light-fw-element-form\"]"));
258 }
259
260 #[test]
261 fn adds_dep_to_multiline_feature_keeps_lines_and_comments() {
262 let text = "[features]\nfull = [\n # формы\n \"dep:light-fw-element-form\",\n]\n";
263 let mut doc = parse(text).unwrap();
264 add_dep_to_feature(&mut doc, "full", "light-widget-datepicker").unwrap();
265 assert_eq!(
266 doc.to_string(),
267 "[features]\nfull = [\n # формы\n \"dep:light-fw-element-form\",\n \"dep:light-widget-datepicker\",\n]\n"
268 );
269 }
270
271 #[test]
272 fn add_then_remove_is_byte_perfect_in_multiline_feature() {
273 let text = "[features]\ndefault = [\"full\"]\nfull = [\n \"dep:light-fw-element-form\",\n \"dep:light-fw-behavior-validation\",\n]\n\
274 minimal = [\n \"std\",\n]\n";
275 let mut doc = parse(text).unwrap();
276 add_dep_to_feature(&mut doc, "full", "light-widget-datepicker").unwrap();
277 let added = doc.to_string();
278 assert!(
279 added.contains(
280 " \"dep:light-fw-element-form\",\n \"dep:light-fw-behavior-validation\",\n \"dep:light-widget-datepicker\",\n]"
281 ),
282 "соседние dep: не склеиваются: {added}"
283 );
284 let mut doc = parse(&added).unwrap();
285 remove_dependency(&mut doc, "light-widget-datepicker").unwrap();
286 assert_eq!(doc.to_string(), text, "add+remove восстанавливает файл");
287 }
288
289 #[test]
290 fn removes_dependency_and_feature_tokens() {
291 let mut doc = parse(
292 "[dependencies]\nlight = \"0.1\"\nlight-fw-element-form = { workspace = true, optional = true }\n\n\
293 [features]\ndefault = [\"full\"]\nfull = [\"dep:light-fw-element-form\", \"light/x\"]\n\
294 element-form = [\"dep:light-fw-element-form\"]\n",
295 )
296 .unwrap();
297 let touched = remove_dependency(&mut doc, "light-fw-element-form").unwrap();
298 let mut sorted = touched.clone();
299 sorted.sort();
300 assert_eq!(sorted, vec!["element-form", "full"]);
301 let out = doc.to_string();
302 assert!(!out.contains("light-fw-element-form"));
303 assert!(!out.contains("dep:"));
304 assert!(out.contains("light/x"));
305 assert!(out.contains("default"));
306 assert!(out.contains("full"));
307 }
308
309 #[test]
310 fn sets_dependency_version_string_and_table() {
311 let mut doc = parse(
312 "[dependencies]\nlight-fw-element-form = \"0.0.1\"\nlight = { version = \"0.1\", features = [\"x\"] }\n",
313 )
314 .unwrap();
315 set_dependency_version(&mut doc, "light-fw-element-form", "0.1.0").unwrap();
316 set_dependency_version(&mut doc, "light", "0.2.0").unwrap();
317 let out = doc.to_string();
318 assert!(out.contains("light-fw-element-form = \"0.1.0\""));
319 assert!(out.contains("version = \"0.2.0\""));
320 assert!(
321 out.contains("features = [\"x\"]"),
322 "features сохранены: {out}"
323 );
324 }
325
326 #[test]
327 fn remove_keeps_preexisting_empty_features() {
328 let mut doc = parse(
329 "[dependencies]\nlight-fw-element-form = { workspace = true, optional = true }\n\n\
330 [features]\nminimal = []\nelement-form = [\"dep:light-fw-element-form\"]\n",
331 )
332 .unwrap();
333 let touched = remove_dependency(&mut doc, "light-fw-element-form").unwrap();
334 assert_eq!(touched, vec!["element-form"]);
335 let out = doc.to_string();
336 assert!(out.contains("minimal = []"), "пустая фича сохранена: {out}");
337 assert!(
338 !out.contains("element-form"),
339 "опустевшая из-за удаления фича удалена: {out}"
340 );
341 }
342
343 #[test]
344 fn features_containing_reports_matches() {
345 let text = "[features]\nfull = [\"dep:light-fw-element-form\"]\nminimal = []\n";
346 assert_eq!(
347 features_containing(text, "light-fw-element-form").unwrap(),
348 vec!["full"]
349 );
350 assert!(has_feature(text, "full").unwrap());
351 assert!(!has_feature(text, "nope").unwrap());
352 }
353}