1use std::path::{Path, PathBuf};
2
3use changeset_core::{AdditionalPackageDeclaration, ManifestFormat};
4use toml_edit::{ArrayOfTables, Item, Table, Value, value};
5
6use crate::config::MetadataSection;
7use crate::error::ManifestError;
8use crate::reader::read_document;
9use crate::writer::navigate_to_changeset_table;
10
11pub struct AdditionalPackageUpdate {
12 pub path: Option<PathBuf>,
13 pub influence: Option<Vec<String>>,
14 pub manifest_file_path: Option<PathBuf>,
15 pub manifest_format: Option<ManifestFormat>,
16 pub manifest_version_field_path: Option<String>,
17}
18
19pub fn add_additional_package(
24 path: &Path,
25 section: MetadataSection,
26 declaration: &AdditionalPackageDeclaration,
27) -> Result<(), ManifestError> {
28 let mut doc = read_document(path)?;
29 let changeset_table = navigate_to_changeset_table(&mut doc, section, path)?;
30
31 let aot = changeset_table
32 .entry("additional-packages")
33 .or_insert(Item::ArrayOfTables(ArrayOfTables::new()));
34
35 let Item::ArrayOfTables(aot) = aot else {
36 return Err(ManifestError::InvalidSectionType {
37 path: path.to_path_buf(),
38 section: "additional-packages".to_string(),
39 });
40 };
41
42 let mut table = Table::new();
43 table.insert("name", value(declaration.name().as_str()));
44 table.insert("path", value(declaration.path().to_string_lossy().as_ref()));
45
46 let mut influence_arr = toml_edit::Array::new();
47 for glob in declaration.influence() {
48 influence_arr.push(glob.as_str());
49 }
50 table.insert("influence", Item::Value(Value::Array(influence_arr)));
51
52 let mut manifest_table = Table::new();
53 manifest_table.insert(
54 "file-path",
55 value(
56 declaration
57 .manifest()
58 .file_path()
59 .to_string_lossy()
60 .as_ref(),
61 ),
62 );
63 manifest_table.insert(
64 "format",
65 value(declaration.manifest().format().to_string().as_str()),
66 );
67 manifest_table.insert(
68 "version-field-path",
69 value(declaration.manifest().version_field_path().as_str()),
70 );
71 table.insert("manifest", Item::Table(manifest_table));
72
73 aot.push(table);
74
75 std::fs::write(path, doc.to_string()).map_err(|source| ManifestError::Write {
76 path: path.to_path_buf(),
77 source,
78 })
79}
80
81pub fn remove_additional_package(
87 path: &Path,
88 section: MetadataSection,
89 name: &str,
90) -> Result<bool, ManifestError> {
91 let mut doc = read_document(path)?;
92 let changeset_table = navigate_to_changeset_table(&mut doc, section, path)?;
93
94 let Some(aot_item) = changeset_table.get_mut("additional-packages") else {
95 return Ok(false);
96 };
97
98 let Item::ArrayOfTables(aot) = aot_item else {
99 return Ok(false);
100 };
101
102 let original_len = aot.len();
103 let indices_to_remove: Vec<usize> = aot
104 .iter()
105 .enumerate()
106 .filter(|(_, t)| {
107 t.get("name")
108 .and_then(Item::as_str)
109 .is_some_and(|n| n == name)
110 })
111 .map(|(i, _)| i)
112 .collect();
113
114 if indices_to_remove.is_empty() {
115 return Ok(false);
116 }
117
118 for i in indices_to_remove.into_iter().rev() {
119 aot.remove(i);
120 }
121
122 let removed = aot.len() < original_len;
123 if removed {
124 std::fs::write(path, doc.to_string()).map_err(|source| ManifestError::Write {
125 path: path.to_path_buf(),
126 source,
127 })?;
128 }
129
130 Ok(removed)
131}
132
133pub fn update_additional_package(
140 path: &Path,
141 section: MetadataSection,
142 name: &str,
143 updates: &AdditionalPackageUpdate,
144) -> Result<bool, ManifestError> {
145 let mut doc = read_document(path)?;
146 let changeset_table = navigate_to_changeset_table(&mut doc, section, path)?;
147
148 let Some(aot_item) = changeset_table.get_mut("additional-packages") else {
149 return Ok(false);
150 };
151
152 let Item::ArrayOfTables(aot) = aot_item else {
153 return Ok(false);
154 };
155
156 let Some(table) = aot.iter_mut().find(|t| {
157 t.get("name")
158 .and_then(Item::as_str)
159 .is_some_and(|n| n == name)
160 }) else {
161 return Ok(false);
162 };
163
164 if let Some(ref new_path) = updates.path {
165 table.insert("path", value(new_path.to_string_lossy().as_ref()));
166 }
167
168 if let Some(ref new_influence) = updates.influence {
169 let mut arr = toml_edit::Array::new();
170 for glob in new_influence {
171 arr.push(glob.as_str());
172 }
173 table.insert("influence", Item::Value(Value::Array(arr)));
174 }
175
176 if updates.manifest_file_path.is_some()
177 || updates.manifest_format.is_some()
178 || updates.manifest_version_field_path.is_some()
179 {
180 let manifest_item = table
181 .entry("manifest")
182 .or_insert_with(|| Item::Table(Table::new()));
183
184 let manifest_table =
185 manifest_item
186 .as_table_mut()
187 .ok_or_else(|| ManifestError::InvalidSectionType {
188 path: path.to_path_buf(),
189 section: "additional-packages[].manifest".to_string(),
190 })?;
191
192 if let Some(ref new_file_path) = updates.manifest_file_path {
193 manifest_table.insert("file-path", value(new_file_path.to_string_lossy().as_ref()));
194 }
195
196 if let Some(new_format) = updates.manifest_format {
197 manifest_table.insert("format", value(new_format.to_string().as_str()));
198 }
199
200 if let Some(ref new_version_field_path) = updates.manifest_version_field_path {
201 manifest_table.insert("version-field-path", value(new_version_field_path.as_str()));
202 }
203 }
204
205 std::fs::write(path, doc.to_string()).map_err(|source| ManifestError::Write {
206 path: path.to_path_buf(),
207 source,
208 })?;
209
210 Ok(true)
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216 use changeset_core::{AdditionalPackageManifest, ManifestFormat};
217 use std::path::PathBuf;
218
219 fn make_declaration(name: &str) -> AdditionalPackageDeclaration {
220 AdditionalPackageDeclaration::new(
221 name.to_string(),
222 PathBuf::from(format!("charts/{name}")),
223 vec![format!("charts/{name}/**")],
224 AdditionalPackageManifest::new(
225 PathBuf::from(format!("charts/{name}/Chart.yaml")),
226 ManifestFormat::Yaml,
227 "version".to_string(),
228 ),
229 Vec::new(),
230 )
231 }
232
233 fn write_temp_toml(content: &str) -> (tempfile::TempDir, PathBuf) {
234 let dir = tempfile::tempdir().expect("create temp dir");
235 let path = dir.path().join("Cargo.toml");
236 std::fs::write(&path, content).expect("write test file");
237 (dir, path)
238 }
239
240 #[test]
241 fn add_creates_array_of_tables_entry() {
242 let (_dir, path) = write_temp_toml(
243 r#"
244[workspace]
245members = ["crates/*"]
246"#,
247 );
248 let decl = make_declaration("my-chart");
249
250 add_additional_package(&path, MetadataSection::Workspace, &decl)
251 .expect("add should succeed");
252
253 let content = std::fs::read_to_string(&path).expect("read file");
254 assert!(content.contains("[[workspace.metadata.changeset.additional-packages]]"));
255 assert!(content.contains(r#"name = "my-chart""#));
256 }
257
258 #[test]
259 fn add_appends_to_existing_array() {
260 let (_dir, path) = write_temp_toml(
261 r#"
262[workspace]
263members = ["crates/*"]
264"#,
265 );
266
267 add_additional_package(
268 &path,
269 MetadataSection::Workspace,
270 &make_declaration("chart-a"),
271 )
272 .expect("add first");
273 add_additional_package(
274 &path,
275 MetadataSection::Workspace,
276 &make_declaration("chart-b"),
277 )
278 .expect("add second");
279
280 let content = std::fs::read_to_string(&path).expect("read file");
281 assert!(content.contains(r#"name = "chart-a""#));
282 assert!(content.contains(r#"name = "chart-b""#));
283 }
284
285 #[test]
286 fn add_preserves_existing_comments() {
287 let (_dir, path) = write_temp_toml(
288 r#"# Workspace config
289[workspace]
290# Members
291members = ["crates/*"]
292"#,
293 );
294 let decl = make_declaration("my-chart");
295
296 add_additional_package(&path, MetadataSection::Workspace, &decl)
297 .expect("add should succeed");
298
299 let content = std::fs::read_to_string(&path).expect("read file");
300 assert!(content.contains("# Workspace config"));
301 assert!(content.contains("# Members"));
302 }
303
304 #[test]
305 fn add_creates_nested_manifest_table() {
306 let (_dir, path) = write_temp_toml(
307 r#"
308[workspace]
309members = ["crates/*"]
310"#,
311 );
312 let decl = AdditionalPackageDeclaration::new(
313 "my-chart".to_string(),
314 PathBuf::from("charts/my-chart"),
315 vec![],
316 AdditionalPackageManifest::new(
317 PathBuf::from("charts/my-chart/Chart.yaml"),
318 ManifestFormat::Yaml,
319 "version".to_string(),
320 ),
321 Vec::new(),
322 );
323
324 add_additional_package(&path, MetadataSection::Workspace, &decl)
325 .expect("add should succeed");
326
327 let content = std::fs::read_to_string(&path).expect("read file");
328 assert!(content.contains(r#"file-path = "charts/my-chart/Chart.yaml""#));
329 assert!(content.contains(r#"format = "yaml""#));
330 assert!(content.contains(r#"version-field-path = "version""#));
331 }
332
333 #[test]
334 fn add_serializes_influence_as_array() {
335 let (_dir, path) = write_temp_toml(
336 r#"
337[workspace]
338members = ["crates/*"]
339"#,
340 );
341 let decl = AdditionalPackageDeclaration::new(
342 "my-chart".to_string(),
343 PathBuf::from("charts/my-chart"),
344 vec!["charts/my-chart/**".to_string(), "helm/**".to_string()],
345 AdditionalPackageManifest::new(
346 PathBuf::from("charts/my-chart/Chart.yaml"),
347 ManifestFormat::Yaml,
348 "version".to_string(),
349 ),
350 Vec::new(),
351 );
352
353 add_additional_package(&path, MetadataSection::Workspace, &decl)
354 .expect("add should succeed");
355
356 let content = std::fs::read_to_string(&path).expect("read file");
357 assert!(content.contains("influence"));
358 assert!(content.contains(r#""charts/my-chart/**""#));
359 assert!(content.contains(r#""helm/**""#));
360 }
361
362 #[test]
363 fn remove_by_name_returns_true() {
364 let (_dir, path) = write_temp_toml(
365 r#"
366[workspace]
367members = ["crates/*"]
368"#,
369 );
370 add_additional_package(
371 &path,
372 MetadataSection::Workspace,
373 &make_declaration("my-chart"),
374 )
375 .expect("add should succeed");
376
377 let result = remove_additional_package(&path, MetadataSection::Workspace, "my-chart")
378 .expect("remove should succeed");
379
380 assert!(result);
381 let content = std::fs::read_to_string(&path).expect("read file");
382 assert!(!content.contains(r#"name = "my-chart""#));
383 }
384
385 #[test]
386 fn remove_nonexistent_returns_false() {
387 let (_dir, path) = write_temp_toml(
388 r#"
389[workspace]
390members = ["crates/*"]
391"#,
392 );
393
394 let result = remove_additional_package(&path, MetadataSection::Workspace, "nonexistent")
395 .expect("remove should succeed");
396
397 assert!(!result);
398 }
399
400 #[test]
401 fn remove_preserves_other_entries() {
402 let (_dir, path) = write_temp_toml(
403 r#"
404[workspace]
405members = ["crates/*"]
406"#,
407 );
408 add_additional_package(
409 &path,
410 MetadataSection::Workspace,
411 &make_declaration("chart-a"),
412 )
413 .expect("add first");
414 add_additional_package(
415 &path,
416 MetadataSection::Workspace,
417 &make_declaration("chart-b"),
418 )
419 .expect("add second");
420
421 remove_additional_package(&path, MetadataSection::Workspace, "chart-a")
422 .expect("remove should succeed");
423
424 let content = std::fs::read_to_string(&path).expect("read file");
425 assert!(!content.contains(r#"name = "chart-a""#));
426 assert!(content.contains(r#"name = "chart-b""#));
427 }
428
429 #[test]
430 fn remove_preserves_comments() {
431 let (_dir, path) = write_temp_toml(
432 r#"# Workspace config
433[workspace]
434# Members comment
435members = ["crates/*"]
436"#,
437 );
438 add_additional_package(
439 &path,
440 MetadataSection::Workspace,
441 &make_declaration("chart-a"),
442 )
443 .expect("add");
444
445 remove_additional_package(&path, MetadataSection::Workspace, "chart-a").expect("remove");
446
447 let content = std::fs::read_to_string(&path).expect("read file");
448 assert!(content.contains("# Workspace config"));
449 assert!(content.contains("# Members comment"));
450 }
451
452 #[test]
453 fn update_modifies_path_field() {
454 let (_dir, path) = write_temp_toml(
455 r#"
456[workspace]
457members = ["crates/*"]
458"#,
459 );
460 add_additional_package(
461 &path,
462 MetadataSection::Workspace,
463 &make_declaration("my-chart"),
464 )
465 .expect("add");
466
467 let updates = AdditionalPackageUpdate {
468 path: Some(PathBuf::from("new/chart/path")),
469 influence: None,
470 manifest_file_path: None,
471 manifest_format: None,
472 manifest_version_field_path: None,
473 };
474 let result =
475 update_additional_package(&path, MetadataSection::Workspace, "my-chart", &updates)
476 .expect("update should succeed");
477
478 assert!(result);
479 let content = std::fs::read_to_string(&path).expect("read file");
480 assert!(content.contains(r#"path = "new/chart/path""#));
481 }
482
483 #[test]
484 fn update_modifies_influence() {
485 let (_dir, path) = write_temp_toml(
486 r#"
487[workspace]
488members = ["crates/*"]
489"#,
490 );
491 add_additional_package(
492 &path,
493 MetadataSection::Workspace,
494 &make_declaration("my-chart"),
495 )
496 .expect("add");
497
498 let updates = AdditionalPackageUpdate {
499 path: None,
500 influence: Some(vec!["new/pattern/**".to_string()]),
501 manifest_file_path: None,
502 manifest_format: None,
503 manifest_version_field_path: None,
504 };
505 update_additional_package(&path, MetadataSection::Workspace, "my-chart", &updates)
506 .expect("update should succeed");
507
508 let content = std::fs::read_to_string(&path).expect("read file");
509 assert!(content.contains(r#""new/pattern/**""#));
510 }
511
512 #[test]
513 fn update_modifies_manifest_fields() {
514 let (_dir, path) = write_temp_toml(
515 r#"
516[workspace]
517members = ["crates/*"]
518"#,
519 );
520 add_additional_package(
521 &path,
522 MetadataSection::Workspace,
523 &make_declaration("my-chart"),
524 )
525 .expect("add");
526
527 let updates = AdditionalPackageUpdate {
528 path: None,
529 influence: None,
530 manifest_file_path: None,
531 manifest_format: Some(ManifestFormat::Json),
532 manifest_version_field_path: Some("info.version".to_string()),
533 };
534 update_additional_package(&path, MetadataSection::Workspace, "my-chart", &updates)
535 .expect("update should succeed");
536
537 let content = std::fs::read_to_string(&path).expect("read file");
538 assert!(content.contains(r#"format = "json""#));
539 assert!(content.contains(r#"version-field-path = "info.version""#));
540 }
541
542 #[test]
543 fn update_nonexistent_returns_false() {
544 let (_dir, path) = write_temp_toml(
545 r#"
546[workspace]
547members = ["crates/*"]
548"#,
549 );
550
551 let updates = AdditionalPackageUpdate {
552 path: Some(PathBuf::from("somewhere")),
553 influence: None,
554 manifest_file_path: None,
555 manifest_format: None,
556 manifest_version_field_path: None,
557 };
558 let result =
559 update_additional_package(&path, MetadataSection::Workspace, "nonexistent", &updates)
560 .expect("update should succeed");
561
562 assert!(!result);
563 }
564
565 #[test]
566 fn update_preserves_comments() {
567 let (_dir, path) = write_temp_toml(
568 r#"# My project
569[workspace]
570# Workspace members
571members = ["crates/*"]
572"#,
573 );
574 add_additional_package(
575 &path,
576 MetadataSection::Workspace,
577 &make_declaration("my-chart"),
578 )
579 .expect("add");
580
581 let updates = AdditionalPackageUpdate {
582 path: Some(PathBuf::from("new/path")),
583 influence: None,
584 manifest_file_path: None,
585 manifest_format: None,
586 manifest_version_field_path: None,
587 };
588 update_additional_package(&path, MetadataSection::Workspace, "my-chart", &updates)
589 .expect("update");
590
591 let content = std::fs::read_to_string(&path).expect("read file");
592 assert!(content.contains("# My project"));
593 assert!(content.contains("# Workspace members"));
594 }
595
596 #[test]
597 fn workspace_and_package_sections() {
598 let (_dir, path) = write_temp_toml(
599 r#"
600[package]
601name = "my-crate"
602version = "0.1.0"
603"#,
604 );
605 let decl = make_declaration("my-chart");
606
607 add_additional_package(&path, MetadataSection::Package, &decl).expect("add should succeed");
608
609 let content = std::fs::read_to_string(&path).expect("read file");
610 assert!(content.contains("[[package.metadata.changeset.additional-packages]]"));
611 assert!(!content.contains("[[workspace.metadata.changeset.additional-packages]]"));
612 }
613}