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 = fs::read_to_string(path)
44 .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!(out.contains("serde = \"1\""), "preserves existing dep: {out}");
146 assert!(
147 out.contains("flodl-hf = \"=0.5.2\""),
148 "appends new dep: {out}",
149 );
150 let header_pos = out.find("[dependencies]").unwrap();
152 let new_pos = out.find("flodl-hf").unwrap();
153 assert!(new_pos > header_pos);
154 }
155
156 #[test]
157 fn already_present_plain_version_is_noop() {
158 let input = "\
159[dependencies]
160flodl-hf = \"0.5.0\"
161serde = \"1\"
162";
163 let (out, outcome) = insert_dep(input, "flodl-hf", "=0.5.2").unwrap();
164 assert_eq!(outcome, AddDepOutcome::AlreadyPresent);
165 assert_eq!(out, input);
166 }
167
168 #[test]
169 fn already_present_inline_table_is_noop() {
170 let input = "\
171[dependencies]
172flodl-hf = { version = \"0.5.0\", features = [\"hub\"] }
173";
174 let (out, outcome) = insert_dep(input, "flodl-hf", "=0.5.2").unwrap();
175 assert_eq!(outcome, AddDepOutcome::AlreadyPresent);
176 assert_eq!(out, input);
177 }
178
179 #[test]
180 fn already_present_workspace_inheritance_is_noop() {
181 let input = "\
182[dependencies]
183flodl-hf = { workspace = true }
184";
185 let (out, outcome) = insert_dep(input, "flodl-hf", "=0.5.2").unwrap();
186 assert_eq!(outcome, AddDepOutcome::AlreadyPresent);
187 assert_eq!(out, input);
188 }
189
190 #[test]
191 fn missing_table_is_appended_at_eof() {
192 let input = "\
193[package]
194name = \"x\"
195";
196 let (out, outcome) = insert_dep(input, "flodl-hf", "=0.5.2").unwrap();
197 assert_eq!(outcome, AddDepOutcome::Added);
198 assert!(out.contains("[package]"));
199 assert!(out.contains("[dependencies]"));
200 assert!(out.contains("flodl-hf = \"=0.5.2\""));
201 let pkg = out.find("[package]").unwrap();
203 let dep = out.find("[dependencies]").unwrap();
204 assert!(dep > pkg);
205 }
206
207 #[test]
208 fn empty_dependencies_block_inserts_after_header() {
209 let input = "\
210[package]
211name = \"x\"
212
213[dependencies]
214
215[dev-dependencies]
216serde = \"1\"
217";
218 let (out, outcome) = insert_dep(input, "flodl-hf", "=0.5.2").unwrap();
219 assert_eq!(outcome, AddDepOutcome::Added);
220 let dep = out.find("[dependencies]").unwrap();
222 let dev = out.find("[dev-dependencies]").unwrap();
223 let new_dep = out.find("flodl-hf").unwrap();
224 assert!(
225 new_dep > dep && new_dep < dev,
226 "new dep must land inside [dependencies] block: {out}",
227 );
228 }
229
230 #[test]
231 fn neighbouring_crate_name_does_not_false_positive() {
232 let input = "\
234[dependencies]
235flodl-hf = \"=0.5.2\"
236";
237 let (out, outcome) = insert_dep(input, "flodl", "=0.5.2").unwrap();
238 assert_eq!(outcome, AddDepOutcome::Added);
239 assert!(out.contains("flodl = \"=0.5.2\""));
240 assert!(out.contains("flodl-hf = \"=0.5.2\""));
241 }
242
243 #[test]
244 fn dep_in_other_table_does_not_count_as_present() {
245 let input = "\
248[dependencies]
249serde = \"1\"
250
251[dev-dependencies]
252flodl-hf = \"0.5.0\"
253";
254 let (out, outcome) = insert_dep(input, "flodl-hf", "=0.5.2").unwrap();
255 assert_eq!(outcome, AddDepOutcome::Added);
256 let main_block_end = out.find("[dev-dependencies]").unwrap();
259 let new_dep = out[..main_block_end].find("flodl-hf").unwrap();
260 assert!(out[main_block_end..].contains("flodl-hf = \"0.5.0\""));
262 let _ = new_dep;
263 }
264
265 #[test]
266 fn preserves_trailing_newline() {
267 let input = "[dependencies]\nserde = \"1\"\n";
268 let (out, _) = insert_dep(input, "flodl-hf", "=0.5.2").unwrap();
269 assert!(out.ends_with('\n'), "trailing newline preserved: {out:?}");
270 }
271
272 #[test]
273 fn preserves_no_trailing_newline() {
274 let input = "[dependencies]\nserde = \"1\"";
275 let (out, _) = insert_dep(input, "flodl-hf", "=0.5.2").unwrap();
276 assert!(!out.ends_with("\n\n"));
277 }
278
279 #[test]
280 fn empty_name_errors() {
281 let err = insert_dep("[dependencies]\n", "", "=0.5.2").unwrap_err();
282 assert!(err.contains("name cannot be empty"));
283 }
284}