1use std::path::Path;
2
3use changeset_core::types::ManifestFormat;
4use jsonc_parser::ParseOptions;
5use jsonc_parser::cst::{CstInputValue, CstObject, CstRootNode};
6use semver::Version;
7use toml_edit::DocumentMut;
8use yaml_edit::Document;
9
10use crate::error::ManifestError;
11
12pub fn write_external_version(
16 path: &Path,
17 format: ManifestFormat,
18 version_field_path: &str,
19 version: &Version,
20) -> Result<(), ManifestError> {
21 let content = std::fs::read_to_string(path).map_err(|source| ManifestError::Read {
22 path: path.to_path_buf(),
23 source,
24 })?;
25
26 let new_content = match format {
27 ManifestFormat::Toml => write_toml_version(&content, path, version_field_path, version)?,
28 ManifestFormat::Yaml => write_yaml_version(&content, path, version_field_path, version)?,
29 ManifestFormat::Json => write_json_version(&content, path, version_field_path, version)?,
30 };
31
32 std::fs::write(path, new_content).map_err(|source| ManifestError::Write {
33 path: path.to_path_buf(),
34 source,
35 })
36}
37
38pub fn restore_external_version(
42 path: &Path,
43 format: ManifestFormat,
44 version_field_path: &str,
45 version_str: &str,
46) -> Result<(), ManifestError> {
47 let content = std::fs::read_to_string(path).map_err(|source| ManifestError::Read {
48 path: path.to_path_buf(),
49 source,
50 })?;
51
52 let new_content = match format {
53 ManifestFormat::Toml => {
54 restore_toml_version(&content, path, version_field_path, version_str)?
55 }
56 ManifestFormat::Yaml => {
57 restore_yaml_version(&content, path, version_field_path, version_str)?
58 }
59 ManifestFormat::Json => {
60 restore_json_version(&content, path, version_field_path, version_str)?
61 }
62 };
63
64 std::fs::write(path, new_content).map_err(|source| ManifestError::Write {
65 path: path.to_path_buf(),
66 source,
67 })
68}
69
70pub fn read_external_version_string(
75 path: &Path,
76 format: ManifestFormat,
77 version_field_path: &str,
78) -> Result<String, ManifestError> {
79 let content = std::fs::read_to_string(path).map_err(|source| ManifestError::Read {
80 path: path.to_path_buf(),
81 source,
82 })?;
83
84 match format {
85 ManifestFormat::Toml => read_toml_version(&content, path, version_field_path),
86 ManifestFormat::Yaml => read_yaml_version(&content, path, version_field_path),
87 ManifestFormat::Json => read_json_version(&content, path, version_field_path),
88 }
89}
90
91pub fn verify_external_version(
95 path: &Path,
96 format: ManifestFormat,
97 version_field_path: &str,
98 expected: &Version,
99) -> Result<(), ManifestError> {
100 let content = std::fs::read_to_string(path).map_err(|source| ManifestError::Read {
101 path: path.to_path_buf(),
102 source,
103 })?;
104
105 let version_str = match format {
106 ManifestFormat::Toml => read_toml_version(&content, path, version_field_path)?,
107 ManifestFormat::Yaml => read_yaml_version(&content, path, version_field_path)?,
108 ManifestFormat::Json => read_json_version(&content, path, version_field_path)?,
109 };
110
111 let actual =
112 version_str
113 .parse::<Version>()
114 .map_err(|source| ManifestError::InvalidVersion {
115 path: path.to_path_buf(),
116 version: version_str,
117 source,
118 })?;
119
120 if actual != *expected {
121 return Err(ManifestError::VerificationFailed {
122 path: path.to_path_buf(),
123 expected: expected.to_string(),
124 actual: actual.to_string(),
125 });
126 }
127
128 Ok(())
129}
130
131fn write_toml_version(
132 content: &str,
133 path: &Path,
134 version_field_path: &str,
135 version: &Version,
136) -> Result<String, ManifestError> {
137 let mut doc = content
138 .parse::<DocumentMut>()
139 .map_err(|source| ManifestError::Parse {
140 path: path.to_path_buf(),
141 source,
142 })?;
143
144 let segments: Vec<&str> = version_field_path.split('.').collect();
145 let (leaf_key, parent_segments) =
146 segments
147 .split_last()
148 .ok_or_else(|| ManifestError::VersionPathNotFound {
149 path: path.to_path_buf(),
150 version_field_path: version_field_path.to_string(),
151 })?;
152
153 let mut current = doc.as_item_mut();
154 for segment in parent_segments {
155 current = current
156 .get_mut(*segment)
157 .ok_or_else(|| ManifestError::VersionPathNotFound {
158 path: path.to_path_buf(),
159 version_field_path: version_field_path.to_string(),
160 })?;
161 }
162
163 let table = current
164 .as_table_like_mut()
165 .ok_or_else(|| ManifestError::VersionPathNotFound {
166 path: path.to_path_buf(),
167 version_field_path: version_field_path.to_string(),
168 })?;
169
170 if table.get(leaf_key).is_none() {
171 return Err(ManifestError::VersionPathNotFound {
172 path: path.to_path_buf(),
173 version_field_path: version_field_path.to_string(),
174 });
175 }
176
177 table.insert(leaf_key, toml_edit::value(version.to_string()));
178
179 Ok(doc.to_string())
180}
181
182fn read_toml_version(
183 content: &str,
184 path: &Path,
185 version_field_path: &str,
186) -> Result<String, ManifestError> {
187 let doc = content
188 .parse::<DocumentMut>()
189 .map_err(|source| ManifestError::Parse {
190 path: path.to_path_buf(),
191 source,
192 })?;
193
194 let mut current = doc.as_item();
195 for segment in version_field_path.split('.') {
196 current = current
197 .get(segment)
198 .ok_or_else(|| ManifestError::VersionPathNotFound {
199 path: path.to_path_buf(),
200 version_field_path: version_field_path.to_string(),
201 })?;
202 }
203
204 current
205 .as_str()
206 .map(String::from)
207 .ok_or_else(|| ManifestError::VersionNotString {
208 path: path.to_path_buf(),
209 version_field_path: version_field_path.to_string(),
210 })
211}
212
213fn write_yaml_version(
214 content: &str,
215 path: &Path,
216 version_field_path: &str,
217 version: &Version,
218) -> Result<String, ManifestError> {
219 let doc = content
220 .parse::<Document>()
221 .map_err(|source| ManifestError::YamlParse {
222 path: path.to_path_buf(),
223 source,
224 })?;
225
226 let segments: Vec<&str> = version_field_path.split('.').collect();
227 let (leaf_key, parent_segments) =
228 segments
229 .split_last()
230 .ok_or_else(|| ManifestError::VersionPathNotFound {
231 path: path.to_path_buf(),
232 version_field_path: version_field_path.to_string(),
233 })?;
234
235 let mapping = doc
236 .as_mapping()
237 .ok_or_else(|| ManifestError::VersionPathNotFound {
238 path: path.to_path_buf(),
239 version_field_path: version_field_path.to_string(),
240 })?;
241
242 let mut current_mapping = mapping;
243 for segment in parent_segments {
244 current_mapping = current_mapping.get_mapping(*segment).ok_or_else(|| {
245 ManifestError::VersionPathNotFound {
246 path: path.to_path_buf(),
247 version_field_path: version_field_path.to_string(),
248 }
249 })?;
250 }
251
252 if !current_mapping.contains_key(*leaf_key) {
253 return Err(ManifestError::VersionPathNotFound {
254 path: path.to_path_buf(),
255 version_field_path: version_field_path.to_string(),
256 });
257 }
258
259 current_mapping.set(*leaf_key, version.to_string().as_str());
260
261 Ok(doc.to_string())
262}
263
264fn read_yaml_version(
265 content: &str,
266 path: &Path,
267 version_field_path: &str,
268) -> Result<String, ManifestError> {
269 let doc = content
270 .parse::<Document>()
271 .map_err(|source| ManifestError::YamlParse {
272 path: path.to_path_buf(),
273 source,
274 })?;
275
276 let segments: Vec<&str> = version_field_path.split('.').collect();
277 let (leaf_key, parent_segments) =
278 segments
279 .split_last()
280 .ok_or_else(|| ManifestError::VersionPathNotFound {
281 path: path.to_path_buf(),
282 version_field_path: version_field_path.to_string(),
283 })?;
284
285 let mapping = doc
286 .as_mapping()
287 .ok_or_else(|| ManifestError::VersionPathNotFound {
288 path: path.to_path_buf(),
289 version_field_path: version_field_path.to_string(),
290 })?;
291
292 let mut current_mapping = mapping;
293 for segment in parent_segments {
294 current_mapping = current_mapping.get_mapping(*segment).ok_or_else(|| {
295 ManifestError::VersionPathNotFound {
296 path: path.to_path_buf(),
297 version_field_path: version_field_path.to_string(),
298 }
299 })?;
300 }
301
302 let node =
303 current_mapping
304 .get(*leaf_key)
305 .ok_or_else(|| ManifestError::VersionPathNotFound {
306 path: path.to_path_buf(),
307 version_field_path: version_field_path.to_string(),
308 })?;
309
310 node.as_scalar()
311 .map(yaml_edit::Scalar::as_string)
312 .ok_or_else(|| ManifestError::VersionNotString {
313 path: path.to_path_buf(),
314 version_field_path: version_field_path.to_string(),
315 })
316}
317
318fn navigate_json_to_object(
319 root_obj: &CstObject,
320 path: &Path,
321 version_field_path: &str,
322 parent_segments: &[&str],
323) -> Result<CstObject, ManifestError> {
324 let mut current = root_obj.clone();
325 for segment in parent_segments {
326 current =
327 current
328 .object_value(segment)
329 .ok_or_else(|| ManifestError::VersionPathNotFound {
330 path: path.to_path_buf(),
331 version_field_path: version_field_path.to_string(),
332 })?;
333 }
334 Ok(current)
335}
336
337fn write_json_version(
338 content: &str,
339 path: &Path,
340 version_field_path: &str,
341 version: &Version,
342) -> Result<String, ManifestError> {
343 let root = CstRootNode::parse(content, &ParseOptions::default()).map_err(|source| {
344 ManifestError::JsonParse {
345 path: path.to_path_buf(),
346 source,
347 }
348 })?;
349
350 let root_obj = root
351 .object_value()
352 .ok_or_else(|| ManifestError::VersionPathNotFound {
353 path: path.to_path_buf(),
354 version_field_path: version_field_path.to_string(),
355 })?;
356
357 let segments: Vec<&str> = version_field_path.split('.').collect();
358 let (leaf_key, parent_segments) =
359 segments
360 .split_last()
361 .ok_or_else(|| ManifestError::VersionPathNotFound {
362 path: path.to_path_buf(),
363 version_field_path: version_field_path.to_string(),
364 })?;
365
366 let target_obj = navigate_json_to_object(&root_obj, path, version_field_path, parent_segments)?;
367
368 let prop = target_obj
369 .get(leaf_key)
370 .ok_or_else(|| ManifestError::VersionPathNotFound {
371 path: path.to_path_buf(),
372 version_field_path: version_field_path.to_string(),
373 })?;
374
375 prop.set_value(CstInputValue::String(version.to_string()));
376
377 Ok(root.to_string())
378}
379
380fn read_json_version(
381 content: &str,
382 path: &Path,
383 version_field_path: &str,
384) -> Result<String, ManifestError> {
385 let root = CstRootNode::parse(content, &ParseOptions::default()).map_err(|source| {
386 ManifestError::JsonParse {
387 path: path.to_path_buf(),
388 source,
389 }
390 })?;
391
392 let root_obj = root
393 .object_value()
394 .ok_or_else(|| ManifestError::VersionPathNotFound {
395 path: path.to_path_buf(),
396 version_field_path: version_field_path.to_string(),
397 })?;
398
399 let segments: Vec<&str> = version_field_path.split('.').collect();
400 let (leaf_key, parent_segments) =
401 segments
402 .split_last()
403 .ok_or_else(|| ManifestError::VersionPathNotFound {
404 path: path.to_path_buf(),
405 version_field_path: version_field_path.to_string(),
406 })?;
407
408 let target_obj = navigate_json_to_object(&root_obj, path, version_field_path, parent_segments)?;
409
410 let prop = target_obj
411 .get(leaf_key)
412 .ok_or_else(|| ManifestError::VersionPathNotFound {
413 path: path.to_path_buf(),
414 version_field_path: version_field_path.to_string(),
415 })?;
416
417 let node = prop
418 .value()
419 .ok_or_else(|| ManifestError::VersionNotString {
420 path: path.to_path_buf(),
421 version_field_path: version_field_path.to_string(),
422 })?;
423
424 let string_lit = node
425 .as_string_lit()
426 .ok_or_else(|| ManifestError::VersionNotString {
427 path: path.to_path_buf(),
428 version_field_path: version_field_path.to_string(),
429 })?;
430
431 string_lit
432 .decoded_value()
433 .map_err(|source| ManifestError::JsonStringDecode {
434 path: path.to_path_buf(),
435 source,
436 })
437}
438
439fn restore_toml_version(
440 content: &str,
441 path: &Path,
442 version_field_path: &str,
443 version_str: &str,
444) -> Result<String, ManifestError> {
445 let mut doc = content
446 .parse::<DocumentMut>()
447 .map_err(|source| ManifestError::Parse {
448 path: path.to_path_buf(),
449 source,
450 })?;
451
452 let segments: Vec<&str> = version_field_path.split('.').collect();
453 let (leaf_key, parent_segments) =
454 segments
455 .split_last()
456 .ok_or_else(|| ManifestError::VersionPathNotFound {
457 path: path.to_path_buf(),
458 version_field_path: version_field_path.to_string(),
459 })?;
460
461 let mut current = doc.as_item_mut();
462 for segment in parent_segments {
463 current = current
464 .get_mut(*segment)
465 .ok_or_else(|| ManifestError::VersionPathNotFound {
466 path: path.to_path_buf(),
467 version_field_path: version_field_path.to_string(),
468 })?;
469 }
470
471 let table = current
472 .as_table_like_mut()
473 .ok_or_else(|| ManifestError::VersionPathNotFound {
474 path: path.to_path_buf(),
475 version_field_path: version_field_path.to_string(),
476 })?;
477
478 if table.get(leaf_key).is_none() {
479 return Err(ManifestError::VersionPathNotFound {
480 path: path.to_path_buf(),
481 version_field_path: version_field_path.to_string(),
482 });
483 }
484
485 table.insert(leaf_key, toml_edit::value(version_str));
486
487 Ok(doc.to_string())
488}
489
490fn restore_yaml_version(
491 content: &str,
492 path: &Path,
493 version_field_path: &str,
494 version_str: &str,
495) -> Result<String, ManifestError> {
496 let doc = content
497 .parse::<Document>()
498 .map_err(|source| ManifestError::YamlParse {
499 path: path.to_path_buf(),
500 source,
501 })?;
502
503 let segments: Vec<&str> = version_field_path.split('.').collect();
504 let (leaf_key, parent_segments) =
505 segments
506 .split_last()
507 .ok_or_else(|| ManifestError::VersionPathNotFound {
508 path: path.to_path_buf(),
509 version_field_path: version_field_path.to_string(),
510 })?;
511
512 let mapping = doc
513 .as_mapping()
514 .ok_or_else(|| ManifestError::VersionPathNotFound {
515 path: path.to_path_buf(),
516 version_field_path: version_field_path.to_string(),
517 })?;
518
519 let mut current_mapping = mapping;
520 for segment in parent_segments {
521 current_mapping = current_mapping.get_mapping(*segment).ok_or_else(|| {
522 ManifestError::VersionPathNotFound {
523 path: path.to_path_buf(),
524 version_field_path: version_field_path.to_string(),
525 }
526 })?;
527 }
528
529 if !current_mapping.contains_key(*leaf_key) {
530 return Err(ManifestError::VersionPathNotFound {
531 path: path.to_path_buf(),
532 version_field_path: version_field_path.to_string(),
533 });
534 }
535
536 current_mapping.set(*leaf_key, version_str);
537
538 Ok(doc.to_string())
539}
540
541fn restore_json_version(
542 content: &str,
543 path: &Path,
544 version_field_path: &str,
545 version_str: &str,
546) -> Result<String, ManifestError> {
547 let root = CstRootNode::parse(content, &ParseOptions::default()).map_err(|source| {
548 ManifestError::JsonParse {
549 path: path.to_path_buf(),
550 source,
551 }
552 })?;
553
554 let root_obj = root
555 .object_value()
556 .ok_or_else(|| ManifestError::VersionPathNotFound {
557 path: path.to_path_buf(),
558 version_field_path: version_field_path.to_string(),
559 })?;
560
561 let segments: Vec<&str> = version_field_path.split('.').collect();
562 let (leaf_key, parent_segments) =
563 segments
564 .split_last()
565 .ok_or_else(|| ManifestError::VersionPathNotFound {
566 path: path.to_path_buf(),
567 version_field_path: version_field_path.to_string(),
568 })?;
569
570 let target_obj = navigate_json_to_object(&root_obj, path, version_field_path, parent_segments)?;
571
572 let prop = target_obj
573 .get(leaf_key)
574 .ok_or_else(|| ManifestError::VersionPathNotFound {
575 path: path.to_path_buf(),
576 version_field_path: version_field_path.to_string(),
577 })?;
578
579 prop.set_value(CstInputValue::String(version_str.to_string()));
580
581 Ok(root.to_string())
582}
583
584#[cfg(test)]
585mod tests {
586 use semver::Version;
587 use tempfile::NamedTempFile;
588
589 use super::*;
590
591 #[test]
592 fn writes_toml_version_preserving_comments() {
593 let content = "# package info\n[package]\nname = \"foo\"\nversion = \"1.0.0\"\n# after version\nedition = \"2024\"\n";
594 let file = NamedTempFile::new().expect("create temp file");
595 std::fs::write(file.path(), content).expect("write file");
596
597 write_external_version(
598 file.path(),
599 ManifestFormat::Toml,
600 "package.version",
601 &Version::new(2, 0, 0),
602 )
603 .expect("write version");
604
605 let result = std::fs::read_to_string(file.path()).expect("read file");
606 assert!(result.contains("# package info"));
607 assert!(result.contains("# after version"));
608 assert!(result.contains(r#"version = "2.0.0""#));
609 }
610
611 #[test]
612 fn writes_yaml_version_preserving_comments() {
613 let content = "name: my-chart # chart name\nversion: \"1.0.0\" # current version\n";
614 let file = NamedTempFile::new().expect("create temp file");
615 std::fs::write(file.path(), content).expect("write file");
616
617 write_external_version(
618 file.path(),
619 ManifestFormat::Yaml,
620 "version",
621 &Version::new(2, 0, 0),
622 )
623 .expect("write version");
624
625 let result = std::fs::read_to_string(file.path()).expect("read file");
626 assert!(result.contains("# chart name"));
627 assert!(result.contains("2.0.0"));
628 }
629
630 #[test]
631 fn writes_json_version_preserving_formatting() {
632 let content = "{\n \"version\": \"1.0.0\",\n \"name\": \"my-pkg\"\n}\n";
633 let file = NamedTempFile::new().expect("create temp file");
634 std::fs::write(file.path(), content).expect("write file");
635
636 write_external_version(
637 file.path(),
638 ManifestFormat::Json,
639 "version",
640 &Version::new(2, 0, 0),
641 )
642 .expect("write version");
643
644 let result = std::fs::read_to_string(file.path()).expect("read file");
645 assert!(result.contains(" \"version\""));
646 assert!(result.contains("\"2.0.0\""));
647 }
648
649 #[test]
650 fn writes_jsonc_version_preserving_comments() {
651 let content = "{\n // version field\n \"version\": \"1.0.0\"\n}\n";
652 let file = NamedTempFile::new().expect("create temp file");
653 std::fs::write(file.path(), content).expect("write file");
654
655 write_external_version(
656 file.path(),
657 ManifestFormat::Json,
658 "version",
659 &Version::new(2, 0, 0),
660 )
661 .expect("write version");
662
663 let result = std::fs::read_to_string(file.path()).expect("read file");
664 assert!(result.contains("// version field"));
665 assert!(result.contains("\"2.0.0\""));
666 }
667
668 #[test]
669 fn writes_yaml_version_flat_path() {
670 let content = "version: \"1.0.0\"\nname: my-chart\n";
671 let file = NamedTempFile::new().expect("create temp file");
672 std::fs::write(file.path(), content).expect("write file");
673
674 write_external_version(
675 file.path(),
676 ManifestFormat::Yaml,
677 "version",
678 &Version::new(3, 1, 4),
679 )
680 .expect("write version");
681
682 let result = std::fs::read_to_string(file.path()).expect("read file");
683 assert!(result.contains("3.1.4"));
684 }
685
686 #[test]
687 fn writes_toml_version_nested_path() {
688 let content = "[package]\nname = \"my-crate\"\nversion = \"1.0.0\"\n";
689 let file = NamedTempFile::new().expect("create temp file");
690 std::fs::write(file.path(), content).expect("write file");
691
692 write_external_version(
693 file.path(),
694 ManifestFormat::Toml,
695 "package.version",
696 &Version::new(1, 2, 3),
697 )
698 .expect("write version");
699
700 let result = std::fs::read_to_string(file.path()).expect("read file");
701 assert!(result.contains(r#"version = "1.2.3""#));
702 }
703
704 #[test]
705 fn writes_json_version_nested_path() {
706 let content = "{\n \"metadata\": {\n \"version\": \"1.0.0\"\n }\n}\n";
707 let file = NamedTempFile::new().expect("create temp file");
708 std::fs::write(file.path(), content).expect("write file");
709
710 write_external_version(
711 file.path(),
712 ManifestFormat::Json,
713 "metadata.version",
714 &Version::new(2, 0, 0),
715 )
716 .expect("write version");
717
718 let result = std::fs::read_to_string(file.path()).expect("read file");
719 assert!(result.contains("\"2.0.0\""));
720 }
721
722 #[test]
723 fn verifies_matching_version() {
724 let content = "{\n \"version\": \"1.2.3\"\n}\n";
725 let file = NamedTempFile::new().expect("create temp file");
726 std::fs::write(file.path(), content).expect("write file");
727
728 verify_external_version(
729 file.path(),
730 ManifestFormat::Json,
731 "version",
732 &Version::new(1, 2, 3),
733 )
734 .expect("verify version");
735 }
736
737 #[test]
738 fn verifies_returns_error_on_mismatch() {
739 let content = "{\n \"version\": \"1.0.0\"\n}\n";
740 let file = NamedTempFile::new().expect("create temp file");
741 std::fs::write(file.path(), content).expect("write file");
742
743 let result = verify_external_version(
744 file.path(),
745 ManifestFormat::Json,
746 "version",
747 &Version::new(2, 0, 0),
748 );
749 assert!(matches!(
750 result,
751 Err(ManifestError::VerificationFailed { .. })
752 ));
753 }
754
755 #[test]
756 fn returns_error_for_missing_version_field_path() {
757 let content = "{\n \"name\": \"my-pkg\"\n}\n";
758 let file = NamedTempFile::new().expect("create temp file");
759 std::fs::write(file.path(), content).expect("write file");
760
761 let result = write_external_version(
762 file.path(),
763 ManifestFormat::Json,
764 "version",
765 &Version::new(1, 0, 0),
766 );
767 assert!(matches!(
768 result,
769 Err(ManifestError::VersionPathNotFound { .. })
770 ));
771 }
772}