flodl_cli/util/
cargo_toml.rs1use std::fs;
14use std::path::Path;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum AddDepOutcome {
19 Added,
21 AlreadyPresent,
23}
24
25pub fn add_dep(path: &Path, name: &str, version: &str) -> Result<AddDepOutcome, String> {
43 let content =
44 fs::read_to_string(path).map_err(|e| format!("cannot read {}: {e}", path.display()))?;
45 let (new_content, outcome) = insert_dep(&content, name, version)?;
46 if outcome == AddDepOutcome::Added {
47 fs::write(path, new_content)
48 .map_err(|e| format!("cannot write {}: {e}", path.display()))?;
49 }
50 Ok(outcome)
51}
52
53fn insert_dep(content: &str, name: &str, version: &str) -> Result<(String, AddDepOutcome), String> {
56 if name.is_empty() {
57 return Err("dep name cannot be empty".into());
58 }
59
60 let lines: Vec<&str> = content.lines().collect();
61
62 let dep_header = lines.iter().position(|l| l.trim() == "[dependencies]");
65
66 if let Some(header_idx) = dep_header {
67 let block_end = lines[header_idx + 1..]
68 .iter()
69 .position(|l| l.trim_start().starts_with('['))
70 .map(|i| header_idx + 1 + i)
71 .unwrap_or(lines.len());
72
73 for line in &lines[header_idx + 1..block_end] {
75 if line_declares_dep(line, name) {
76 return Ok((content.to_string(), AddDepOutcome::AlreadyPresent));
77 }
78 }
79
80 let mut insert_at = header_idx + 1;
84 for (offset, line) in lines[header_idx + 1..block_end].iter().enumerate() {
85 if !line.trim().is_empty() {
86 insert_at = header_idx + 1 + offset + 1;
87 }
88 }
89
90 let new_line = format!("{name} = \"{version}\"");
91 let mut out = lines[..insert_at].join("\n");
92 if !out.is_empty() {
93 out.push('\n');
94 }
95 out.push_str(&new_line);
96 if insert_at < lines.len() {
97 out.push('\n');
98 out.push_str(&lines[insert_at..].join("\n"));
99 }
100 if content.ends_with('\n') && !out.ends_with('\n') {
101 out.push('\n');
102 }
103 return Ok((out, AddDepOutcome::Added));
104 }
105
106 let mut out = content.to_string();
108 if !out.is_empty() && !out.ends_with('\n') {
109 out.push('\n');
110 }
111 if !out.is_empty() && !out.ends_with("\n\n") {
112 out.push('\n');
113 }
114 out.push_str(&format!("[dependencies]\n{name} = \"{version}\"\n"));
115 Ok((out, AddDepOutcome::Added))
116}
117
118fn line_declares_dep(line: &str, name: &str) -> bool {
122 let t = line.trim_start();
123 let Some(after_key) = t.strip_prefix(name) else {
124 return false;
125 };
126 let rest = after_key.trim_start();
127 rest.starts_with('=')
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 #[test]
135 fn appends_to_existing_dependencies_block() {
136 let input = "\
137[package]
138name = \"x\"
139
140[dependencies]
141serde = \"1\"
142";
143 let (out, outcome) = insert_dep(input, "flodl-hf", "=0.5.2").unwrap();
144 assert_eq!(outcome, AddDepOutcome::Added);
145 assert!(
146 out.contains("serde = \"1\""),
147 "preserves existing dep: {out}"
148 );
149 assert!(
150 out.contains("flodl-hf = \"=0.5.2\""),
151 "appends new dep: {out}",
152 );
153 let header_pos = out.find("[dependencies]").unwrap();
155 let new_pos = out.find("flodl-hf").unwrap();
156 assert!(new_pos > header_pos);
157 }
158
159 #[test]
160 fn already_present_plain_version_is_noop() {
161 let input = "\
162[dependencies]
163flodl-hf = \"0.5.0\"
164serde = \"1\"
165";
166 let (out, outcome) = insert_dep(input, "flodl-hf", "=0.5.2").unwrap();
167 assert_eq!(outcome, AddDepOutcome::AlreadyPresent);
168 assert_eq!(out, input);
169 }
170
171 #[test]
172 fn already_present_inline_table_is_noop() {
173 let input = "\
174[dependencies]
175flodl-hf = { version = \"0.5.0\", features = [\"hub\"] }
176";
177 let (out, outcome) = insert_dep(input, "flodl-hf", "=0.5.2").unwrap();
178 assert_eq!(outcome, AddDepOutcome::AlreadyPresent);
179 assert_eq!(out, input);
180 }
181
182 #[test]
183 fn already_present_workspace_inheritance_is_noop() {
184 let input = "\
185[dependencies]
186flodl-hf = { workspace = true }
187";
188 let (out, outcome) = insert_dep(input, "flodl-hf", "=0.5.2").unwrap();
189 assert_eq!(outcome, AddDepOutcome::AlreadyPresent);
190 assert_eq!(out, input);
191 }
192
193 #[test]
194 fn missing_table_is_appended_at_eof() {
195 let input = "\
196[package]
197name = \"x\"
198";
199 let (out, outcome) = insert_dep(input, "flodl-hf", "=0.5.2").unwrap();
200 assert_eq!(outcome, AddDepOutcome::Added);
201 assert!(out.contains("[package]"));
202 assert!(out.contains("[dependencies]"));
203 assert!(out.contains("flodl-hf = \"=0.5.2\""));
204 let pkg = out.find("[package]").unwrap();
206 let dep = out.find("[dependencies]").unwrap();
207 assert!(dep > pkg);
208 }
209
210 #[test]
211 fn empty_dependencies_block_inserts_after_header() {
212 let input = "\
213[package]
214name = \"x\"
215
216[dependencies]
217
218[dev-dependencies]
219serde = \"1\"
220";
221 let (out, outcome) = insert_dep(input, "flodl-hf", "=0.5.2").unwrap();
222 assert_eq!(outcome, AddDepOutcome::Added);
223 let dep = out.find("[dependencies]").unwrap();
225 let dev = out.find("[dev-dependencies]").unwrap();
226 let new_dep = out.find("flodl-hf").unwrap();
227 assert!(
228 new_dep > dep && new_dep < dev,
229 "new dep must land inside [dependencies] block: {out}",
230 );
231 }
232
233 #[test]
234 fn neighbouring_crate_name_does_not_false_positive() {
235 let input = "\
237[dependencies]
238flodl-hf = \"=0.5.2\"
239";
240 let (out, outcome) = insert_dep(input, "flodl", "=0.5.2").unwrap();
241 assert_eq!(outcome, AddDepOutcome::Added);
242 assert!(out.contains("flodl = \"=0.5.2\""));
243 assert!(out.contains("flodl-hf = \"=0.5.2\""));
244 }
245
246 #[test]
247 fn dep_in_other_table_does_not_count_as_present() {
248 let input = "\
251[dependencies]
252serde = \"1\"
253
254[dev-dependencies]
255flodl-hf = \"0.5.0\"
256";
257 let (out, outcome) = insert_dep(input, "flodl-hf", "=0.5.2").unwrap();
258 assert_eq!(outcome, AddDepOutcome::Added);
259 let main_block_end = out.find("[dev-dependencies]").unwrap();
262 let new_dep = out[..main_block_end].find("flodl-hf").unwrap();
263 assert!(out[main_block_end..].contains("flodl-hf = \"0.5.0\""));
265 let _ = new_dep;
266 }
267
268 #[test]
269 fn preserves_trailing_newline() {
270 let input = "[dependencies]\nserde = \"1\"\n";
271 let (out, _) = insert_dep(input, "flodl-hf", "=0.5.2").unwrap();
272 assert!(out.ends_with('\n'), "trailing newline preserved: {out:?}");
273 }
274
275 #[test]
276 fn preserves_no_trailing_newline() {
277 let input = "[dependencies]\nserde = \"1\"";
278 let (out, _) = insert_dep(input, "flodl-hf", "=0.5.2").unwrap();
279 assert!(!out.ends_with("\n\n"));
280 }
281
282 #[test]
283 fn empty_name_errors() {
284 let err = insert_dep("[dependencies]\n", "", "=0.5.2").unwrap_err();
285 assert!(err.contains("name cannot be empty"));
286 }
287}