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