1use crate::types::ParseError as TypesParseError;
3use crate::VersionPolicy;
4use deb822_lossless::{Deb822, Paragraph};
5use std::str::FromStr;
6
7fn watch_option_to_key(option: &crate::types::WatchOption) -> &'static str {
9 use crate::types::WatchOption;
10
11 match option {
12 WatchOption::Component(_) => "Component",
13 WatchOption::Compression(_) => "Compression",
14 WatchOption::UserAgent(_) => "User-Agent",
15 WatchOption::Pagemangle(_) => "Pagemangle",
16 WatchOption::Uversionmangle(_) => "Uversionmangle",
17 WatchOption::Dversionmangle(_) => "Dversionmangle",
18 WatchOption::Dirversionmangle(_) => "Dirversionmangle",
19 WatchOption::Oversionmangle(_) => "Oversionmangle",
20 WatchOption::Downloadurlmangle(_) => "Downloadurlmangle",
21 WatchOption::Pgpsigurlmangle(_) => "Pgpsigurlmangle",
22 WatchOption::Filenamemangle(_) => "Filenamemangle",
23 WatchOption::VersionPolicy(_) => "Version-Policy",
24 WatchOption::Searchmode(_) => "Searchmode",
25 WatchOption::Mode(_) => "Mode",
26 WatchOption::Pgpmode(_) => "Pgpmode",
27 WatchOption::Gitexport(_) => "Gitexport",
28 WatchOption::Gitmode(_) => "Gitmode",
29 WatchOption::Pretty(_) => "Pretty",
30 WatchOption::Ctype(_) => "Ctype",
31 WatchOption::Repacksuffix(_) => "Repacksuffix",
32 WatchOption::Unzipopt(_) => "Unzipopt",
33 WatchOption::Script(_) => "Script",
34 WatchOption::Decompress => "Decompress",
35 WatchOption::Bare => "Bare",
36 WatchOption::Repack => "Repack",
37 }
38}
39
40#[derive(Debug)]
41pub struct ParseError(String);
43
44impl std::error::Error for ParseError {}
45
46impl std::fmt::Display for ParseError {
47 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
48 write!(f, "ParseError: {}", self.0)
49 }
50}
51
52#[derive(Debug, Clone)]
54pub struct WatchFile(Deb822);
55
56#[derive(Debug, Clone)]
58pub struct Entry {
59 paragraph: Paragraph,
60 defaults: Option<Paragraph>,
61}
62
63impl WatchFile {
64 pub fn as_deb822(&self) -> &Deb822 {
66 &self.0
67 }
68
69 pub fn snapshot(&self) -> Self {
72 WatchFile(self.0.snapshot())
73 }
74
75 pub fn tree_eq(&self, other: &Self) -> bool {
79 self.0.tree_eq(&other.0)
80 }
81
82 pub(crate) fn from_deb822(deb822: Deb822) -> Self {
88 WatchFile(deb822)
89 }
90
91 pub fn new() -> Self {
93 let content = "Version: 5\n";
95 WatchFile::from_str(content).expect("Failed to create empty watch file")
96 }
97
98 pub fn version(&self) -> u32 {
100 5
101 }
102
103 pub fn defaults(&self) -> Option<Paragraph> {
106 let paragraphs: Vec<_> = self.0.paragraphs().collect();
107
108 if paragraphs.len() > 1 {
109 if !paragraphs[1].contains_key("Source") && !paragraphs[1].contains_key("source") {
111 return Some(paragraphs[1].clone());
112 }
113 }
114
115 None
116 }
117
118 pub fn entries(&self) -> impl Iterator<Item = Entry> + '_ {
121 let paragraphs: Vec<_> = self.0.paragraphs().collect();
122 let defaults = self.defaults();
123
124 let start_index = if paragraphs.len() > 1 {
128 let has_source =
130 paragraphs[1].contains_key("Source") || paragraphs[1].contains_key("source");
131 let has_template =
132 paragraphs[1].contains_key("Template") || paragraphs[1].contains_key("template");
133
134 if !has_source && !has_template {
135 2 } else {
137 1 }
139 } else {
140 1
141 };
142
143 paragraphs
144 .into_iter()
145 .skip(start_index)
146 .map(move |p| Entry {
147 paragraph: p,
148 defaults: defaults.clone(),
149 })
150 }
151
152 pub fn inner(&self) -> &Deb822 {
154 &self.0
155 }
156
157 pub fn inner_mut(&mut self) -> &mut Deb822 {
159 &mut self.0
160 }
161
162 pub fn add_entry(&mut self, source: &str, matching_pattern: &str) -> Entry {
179 let mut para = self.0.add_paragraph();
180 para.set("Source", source);
181 para.set("Matching-Pattern", matching_pattern);
182
183 let defaults = self.defaults();
186
187 Entry {
188 paragraph: para.clone(),
189 defaults,
190 }
191 }
192}
193
194impl Default for WatchFile {
195 fn default() -> Self {
196 Self::new()
197 }
198}
199
200impl FromStr for WatchFile {
201 type Err = ParseError;
202
203 fn from_str(s: &str) -> Result<Self, Self::Err> {
204 match Deb822::from_str(s) {
205 Ok(deb822) => {
206 let version = deb822
208 .paragraphs()
209 .next()
210 .and_then(|p| p.get("Version"))
211 .unwrap_or_else(|| "1".to_string());
212
213 if version != "5" {
214 return Err(ParseError(format!("Expected version 5, got {}", version)));
215 }
216
217 Ok(WatchFile(deb822))
218 }
219 Err(e) => Err(ParseError(e.to_string())),
220 }
221 }
222}
223
224impl std::fmt::Display for WatchFile {
225 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
226 write!(f, "{}", self.0)
227 }
228}
229
230impl Entry {
231 pub(crate) fn get_field(&self, key: &str) -> Option<String> {
234 if let Some(value) = self.paragraph.get(key) {
236 return Some(value);
237 }
238
239 let normalized_key = normalize_key(key);
242
243 for (k, v) in self.paragraph.items() {
245 if normalize_key(&k) == normalized_key {
246 return Some(v);
247 }
248 }
249
250 if let Some(ref defaults) = self.defaults {
252 if let Some(value) = defaults.get(key) {
254 return Some(value);
255 }
256
257 for (k, v) in defaults.items() {
259 if normalize_key(&k) == normalized_key {
260 return Some(v);
261 }
262 }
263 }
264
265 None
266 }
267
268 pub fn source(&self) -> Result<Option<String>, crate::templates::TemplateError> {
273 if let Some(source) = self.get_field("Source") {
275 return Ok(Some(source));
276 }
277
278 if self.get_field("Template").is_none() {
280 return Ok(None);
281 }
282
283 self.expand_template().map(|t| t.source)
285 }
286
287 pub fn matching_pattern(&self) -> Result<Option<String>, crate::templates::TemplateError> {
292 if let Some(pattern) = self.get_field("Matching-Pattern") {
294 return Ok(Some(pattern));
295 }
296
297 if self.get_field("Template").is_none() {
299 return Ok(None);
300 }
301
302 self.expand_template().map(|t| t.matching_pattern)
304 }
305
306 pub fn as_deb822(&self) -> &Paragraph {
308 &self.paragraph
309 }
310
311 pub fn component(&self) -> Option<String> {
313 self.get_field("Component")
314 }
315
316 pub fn get_option(&self, key: &str) -> Option<String> {
318 match key {
319 "Source" => None, "Matching-Pattern" => None, "Component" => None, "Version" => None, key => self.get_field(key),
324 }
325 }
326
327 pub fn set_option(&mut self, option: crate::types::WatchOption) {
329 use crate::types::WatchOption;
330
331 let (key, value) = match option {
332 WatchOption::Component(v) => ("Component", Some(v)),
333 WatchOption::Compression(v) => ("Compression", Some(v.to_string())),
334 WatchOption::UserAgent(v) => ("User-Agent", Some(v)),
335 WatchOption::Pagemangle(v) => ("Pagemangle", Some(v)),
336 WatchOption::Uversionmangle(v) => ("Uversionmangle", Some(v)),
337 WatchOption::Dversionmangle(v) => ("Dversionmangle", Some(v)),
338 WatchOption::Dirversionmangle(v) => ("Dirversionmangle", Some(v)),
339 WatchOption::Oversionmangle(v) => ("Oversionmangle", Some(v)),
340 WatchOption::Downloadurlmangle(v) => ("Downloadurlmangle", Some(v)),
341 WatchOption::Pgpsigurlmangle(v) => ("Pgpsigurlmangle", Some(v)),
342 WatchOption::Filenamemangle(v) => ("Filenamemangle", Some(v)),
343 WatchOption::VersionPolicy(v) => ("Version-Policy", Some(v.to_string())),
344 WatchOption::Searchmode(v) => ("Searchmode", Some(v.to_string())),
345 WatchOption::Mode(v) => ("Mode", Some(v.to_string())),
346 WatchOption::Pgpmode(v) => ("Pgpmode", Some(v.to_string())),
347 WatchOption::Gitexport(v) => ("Gitexport", Some(v.to_string())),
348 WatchOption::Gitmode(v) => ("Gitmode", Some(v.to_string())),
349 WatchOption::Pretty(v) => ("Pretty", Some(v.to_string())),
350 WatchOption::Ctype(v) => ("Ctype", Some(v.to_string())),
351 WatchOption::Repacksuffix(v) => ("Repacksuffix", Some(v)),
352 WatchOption::Unzipopt(v) => ("Unzipopt", Some(v)),
353 WatchOption::Script(v) => ("Script", Some(v)),
354 WatchOption::Decompress => ("Decompress", None),
355 WatchOption::Bare => ("Bare", None),
356 WatchOption::Repack => ("Repack", None),
357 };
358
359 if let Some(v) = value {
360 self.paragraph.set(key, &v);
361 } else {
362 self.paragraph.set(key, "");
364 }
365 }
366
367 pub fn set_option_str(&mut self, key: &str, value: &str) {
369 self.paragraph.set(key, value);
370 }
371
372 pub fn delete_option(&mut self, option: crate::types::WatchOption) {
374 let key = watch_option_to_key(&option);
375 self.paragraph.remove(key);
376 }
377
378 pub fn delete_option_str(&mut self, key: &str) {
380 self.paragraph.remove(key);
381 }
382
383 pub fn url(&self) -> String {
385 self.source().unwrap_or(None).unwrap_or_default()
386 }
387
388 pub fn version_policy(&self) -> Result<Option<VersionPolicy>, TypesParseError> {
390 match self.get_field("Version-Policy") {
391 Some(policy) => Ok(Some(policy.parse()?)),
392 None => Ok(None),
393 }
394 }
395
396 pub fn script(&self) -> Option<String> {
398 self.get_field("Script")
399 }
400
401 pub fn set_source(&mut self, url: &str) {
403 self.paragraph.set("Source", url);
404 }
405
406 pub fn set_matching_pattern(&mut self, pattern: &str) {
408 self.paragraph.set("Matching-Pattern", pattern);
409 }
410
411 pub fn line(&self) -> usize {
413 self.paragraph.line()
414 }
415
416 pub fn mode(&self) -> Result<crate::types::Mode, TypesParseError> {
418 Ok(self
419 .get_field("Mode")
420 .map(|s| s.parse())
421 .transpose()?
422 .unwrap_or_default())
423 }
424
425 fn expand_template(
427 &self,
428 ) -> Result<crate::templates::ExpandedTemplate, crate::templates::TemplateError> {
429 use crate::templates::{expand_template, parse_github_url, Template, TemplateError};
430
431 let template_str =
433 self.get_field("Template")
434 .ok_or_else(|| TemplateError::MissingField {
435 template: "any".to_string(),
436 field: "Template".to_string(),
437 })?;
438
439 let release_only = self
440 .get_field("Release-Only")
441 .map(|v| v.to_lowercase() == "yes")
442 .unwrap_or(false);
443
444 let version_type = self.get_field("Version-Type");
445
446 let template = match template_str.to_lowercase().as_str() {
448 "github" => {
449 let (owner, repository) = if let (Some(o), Some(p)) =
451 (self.get_field("Owner"), self.get_field("Project"))
452 {
453 (o, p)
454 } else if let Some(dist) = self.get_field("Dist") {
455 parse_github_url(&dist)?
456 } else {
457 return Err(TemplateError::MissingField {
458 template: "GitHub".to_string(),
459 field: "Dist or Owner+Project".to_string(),
460 });
461 };
462
463 Template::GitHub {
464 owner,
465 repository,
466 release_only,
467 version_type,
468 }
469 }
470 "gitlab" => {
471 let dist = self
472 .get_field("Dist")
473 .ok_or_else(|| TemplateError::MissingField {
474 template: "GitLab".to_string(),
475 field: "Dist".to_string(),
476 })?;
477
478 Template::GitLab {
479 dist,
480 release_only,
481 version_type,
482 }
483 }
484 "pypi" => {
485 let package =
486 self.get_field("Dist")
487 .ok_or_else(|| TemplateError::MissingField {
488 template: "PyPI".to_string(),
489 field: "Dist".to_string(),
490 })?;
491
492 Template::PyPI {
493 package,
494 version_type,
495 }
496 }
497 "npmregistry" => {
498 let package =
499 self.get_field("Dist")
500 .ok_or_else(|| TemplateError::MissingField {
501 template: "Npmregistry".to_string(),
502 field: "Dist".to_string(),
503 })?;
504
505 Template::Npmregistry {
506 package,
507 version_type,
508 }
509 }
510 "metacpan" => {
511 let dist = self
512 .get_field("Dist")
513 .ok_or_else(|| TemplateError::MissingField {
514 template: "Metacpan".to_string(),
515 field: "Dist".to_string(),
516 })?;
517
518 Template::Metacpan { dist, version_type }
519 }
520 "cran" => {
521 let package =
522 self.get_field("Package")
523 .ok_or_else(|| TemplateError::MissingField {
524 template: "CRAN".to_string(),
525 field: "Package".to_string(),
526 })?;
527
528 Template::Cran {
529 package,
530 version_type,
531 }
532 }
533 "bioconductor" => {
534 let package =
535 self.get_field("Package")
536 .ok_or_else(|| TemplateError::MissingField {
537 template: "Bioconductor".to_string(),
538 field: "Package".to_string(),
539 })?;
540
541 Template::Bioconductor {
542 package,
543 version_type,
544 }
545 }
546 _ => return Err(TemplateError::UnknownTemplate(template_str)),
547 };
548
549 Ok(expand_template(template))
550 }
551
552 pub fn try_convert_to_template(&mut self) -> Option<crate::templates::Template> {
584 use crate::templates::detect_template;
585
586 let source = self.source().ok().flatten();
588 let matching_pattern = self.matching_pattern().ok().flatten();
589 let searchmode = self.get_field("Searchmode");
590 let mode = self.get_field("Mode");
591
592 let template = detect_template(
594 source.as_deref(),
595 matching_pattern.as_deref(),
596 searchmode.as_deref(),
597 mode.as_deref(),
598 )?;
599
600 self.paragraph.remove("Source");
602 self.paragraph.remove("Matching-Pattern");
603 self.paragraph.remove("Searchmode");
604 self.paragraph.remove("Mode");
605
606 match &template {
608 crate::templates::Template::GitHub {
609 owner,
610 repository,
611 release_only,
612 version_type,
613 } => {
614 self.paragraph.set("Template", "GitHub");
615 self.paragraph.set("Owner", owner);
616 self.paragraph.set("Project", repository);
617 if *release_only {
618 self.paragraph.set("Release-Only", "yes");
619 }
620 if let Some(vt) = version_type {
621 self.paragraph.set("Version-Type", vt);
622 }
623 }
624 crate::templates::Template::GitLab {
625 dist,
626 release_only: _,
627 version_type,
628 } => {
629 self.paragraph.set("Template", "GitLab");
630 self.paragraph.set("Dist", dist);
631 if let Some(vt) = version_type {
632 self.paragraph.set("Version-Type", vt);
633 }
634 }
635 crate::templates::Template::PyPI {
636 package,
637 version_type,
638 } => {
639 self.paragraph.set("Template", "PyPI");
640 self.paragraph.set("Dist", package);
641 if let Some(vt) = version_type {
642 self.paragraph.set("Version-Type", vt);
643 }
644 }
645 crate::templates::Template::Npmregistry {
646 package,
647 version_type,
648 } => {
649 self.paragraph.set("Template", "Npmregistry");
650 self.paragraph.set("Dist", package);
651 if let Some(vt) = version_type {
652 self.paragraph.set("Version-Type", vt);
653 }
654 }
655 crate::templates::Template::Metacpan { dist, version_type } => {
656 self.paragraph.set("Template", "Metacpan");
657 self.paragraph.set("Dist", dist);
658 if let Some(vt) = version_type {
659 self.paragraph.set("Version-Type", vt);
660 }
661 }
662 crate::templates::Template::Cran {
663 package,
664 version_type,
665 } => {
666 self.paragraph.set("Template", "CRAN");
667 self.paragraph.set("Package", &package);
668 if let Some(vt) = version_type {
669 self.paragraph.set("Version-Type", vt);
670 }
671 }
672 crate::templates::Template::Bioconductor {
673 package,
674 version_type,
675 } => {
676 self.paragraph.set("Template", "Bioconductor");
677 self.paragraph.set("Package", &package);
678 if let Some(vt) = version_type {
679 self.paragraph.set("Version-Type", vt);
680 }
681 }
682 }
683
684 Some(template)
685 }
686}
687
688fn normalize_key(key: &str) -> String {
692 key.to_lowercase().replace(['-', '_'], "")
693}
694
695#[cfg(test)]
696mod tests {
697 use super::*;
698
699 #[test]
700 fn test_as_deb822() {
701 let input = r#"Version: 5
702
703Source: https://github.com/owner/repo/tags
704Matching-Pattern: .*/v?(\d\S+)\.tar\.gz
705"#;
706
707 let wf: WatchFile = input.parse().unwrap();
708 let deb822 = wf.as_deb822();
709
710 assert_eq!(deb822.paragraphs().count(), 2);
712 }
713
714 #[test]
715 fn test_create_v5_watchfile() {
716 let wf = WatchFile::new();
717 assert_eq!(wf.version(), 5);
718
719 let output = wf.to_string();
720 assert!(output.contains("Version"));
721 assert!(output.contains("5"));
722 }
723
724 #[test]
725 fn test_parse_v5_basic() {
726 let input = r#"Version: 5
727
728Source: https://github.com/owner/repo/tags
729Matching-Pattern: .*/v?(\d\S+)\.tar\.gz
730"#;
731
732 let wf: WatchFile = input.parse().unwrap();
733 assert_eq!(wf.version(), 5);
734
735 let entries: Vec<_> = wf.entries().collect();
736 assert_eq!(entries.len(), 1);
737
738 let entry = &entries[0];
739 assert_eq!(
740 entry.source().unwrap().as_deref(),
741 Some("https://github.com/owner/repo/tags")
742 );
743 assert_eq!(
744 entry.matching_pattern().unwrap(),
745 Some(".*/v?(\\d\\S+)\\.tar\\.gz".to_string())
746 );
747 }
748
749 #[test]
750 fn test_parse_v5_multiple_entries() {
751 let input = r#"Version: 5
752
753Source: https://github.com/owner/repo1/tags
754Matching-Pattern: .*/v?(\d\S+)\.tar\.gz
755
756Source: https://github.com/owner/repo2/tags
757Matching-Pattern: .*/release-(\d\S+)\.tar\.gz
758"#;
759
760 let wf: WatchFile = input.parse().unwrap();
761 let entries: Vec<_> = wf.entries().collect();
762 assert_eq!(entries.len(), 2);
763
764 assert_eq!(
765 entries[0].source().unwrap().as_deref(),
766 Some("https://github.com/owner/repo1/tags")
767 );
768 assert_eq!(
769 entries[1].source().unwrap().as_deref(),
770 Some("https://github.com/owner/repo2/tags")
771 );
772 }
773
774 #[test]
775 fn test_v5_case_insensitive_fields() {
776 let input = r#"Version: 5
777
778source: https://example.com/files
779matching-pattern: .*\.tar\.gz
780"#;
781
782 let wf: WatchFile = input.parse().unwrap();
783 let entries: Vec<_> = wf.entries().collect();
784 assert_eq!(entries.len(), 1);
785
786 let entry = &entries[0];
787 assert_eq!(
788 entry.source().unwrap().as_deref(),
789 Some("https://example.com/files")
790 );
791 assert_eq!(
792 entry.matching_pattern().unwrap().as_deref(),
793 Some(".*\\.tar\\.gz")
794 );
795 }
796
797 #[test]
798 fn test_v5_with_compression_option() {
799 let input = r#"Version: 5
800
801Source: https://example.com/files
802Matching-Pattern: .*\.tar\.gz
803Compression: xz
804"#;
805
806 let wf: WatchFile = input.parse().unwrap();
807 let entries: Vec<_> = wf.entries().collect();
808 assert_eq!(entries.len(), 1);
809
810 let entry = &entries[0];
811 let compression = entry.get_option("compression");
812 assert!(compression.is_some());
813 }
814
815 #[test]
816 fn test_v5_with_component() {
817 let input = r#"Version: 5
818
819Source: https://example.com/files
820Matching-Pattern: .*\.tar\.gz
821Component: foo
822"#;
823
824 let wf: WatchFile = input.parse().unwrap();
825 let entries: Vec<_> = wf.entries().collect();
826 assert_eq!(entries.len(), 1);
827
828 let entry = &entries[0];
829 assert_eq!(entry.component(), Some("foo".to_string()));
830 }
831
832 #[test]
833 fn test_v5_rejects_wrong_version() {
834 let input = r#"Version: 4
835
836Source: https://example.com/files
837Matching-Pattern: .*\.tar\.gz
838"#;
839
840 let result: Result<WatchFile, _> = input.parse();
841 assert!(result.is_err());
842 }
843
844 #[test]
845 fn test_v5_roundtrip() {
846 let input = r#"Version: 5
847
848Source: https://example.com/files
849Matching-Pattern: .*\.tar\.gz
850"#;
851
852 let wf: WatchFile = input.parse().unwrap();
853 let output = wf.to_string();
854
855 let wf2: WatchFile = output.parse().unwrap();
857 assert_eq!(wf2.version(), 5);
858
859 let entries: Vec<_> = wf2.entries().collect();
860 assert_eq!(entries.len(), 1);
861 }
862
863 #[test]
864 fn test_normalize_key() {
865 assert_eq!(normalize_key("Matching-Pattern"), "matchingpattern");
866 assert_eq!(normalize_key("matching_pattern"), "matchingpattern");
867 assert_eq!(normalize_key("MatchingPattern"), "matchingpattern");
868 assert_eq!(normalize_key("MATCHING-PATTERN"), "matchingpattern");
869 }
870
871 #[test]
872 fn test_defaults_paragraph() {
873 let input = r#"Version: 5
874
875Compression: xz
876User-Agent: Custom/1.0
877
878Source: https://example.com/repo1
879Matching-Pattern: .*\.tar\.gz
880
881Source: https://example.com/repo2
882Matching-Pattern: .*\.tar\.gz
883Compression: gz
884"#;
885
886 let wf: WatchFile = input.parse().unwrap();
887
888 let defaults = wf.defaults();
890 assert!(defaults.is_some());
891 let defaults = defaults.unwrap();
892 assert_eq!(defaults.get("Compression"), Some("xz".to_string()));
893 assert_eq!(defaults.get("User-Agent"), Some("Custom/1.0".to_string()));
894
895 let entries: Vec<_> = wf.entries().collect();
897 assert_eq!(entries.len(), 2);
898
899 assert_eq!(entries[0].get_option("Compression"), Some("xz".to_string()));
901 assert_eq!(
902 entries[0].get_option("User-Agent"),
903 Some("Custom/1.0".to_string())
904 );
905
906 assert_eq!(entries[1].get_option("Compression"), Some("gz".to_string()));
908 assert_eq!(
909 entries[1].get_option("User-Agent"),
910 Some("Custom/1.0".to_string())
911 );
912 }
913
914 #[test]
915 fn test_no_defaults_paragraph() {
916 let input = r#"Version: 5
917
918Source: https://example.com/repo1
919Matching-Pattern: .*\.tar\.gz
920"#;
921
922 let wf: WatchFile = input.parse().unwrap();
923
924 assert!(wf.defaults().is_none());
926
927 let entries: Vec<_> = wf.entries().collect();
928 assert_eq!(entries.len(), 1);
929 }
930
931 #[test]
932 fn test_set_source() {
933 let mut wf = WatchFile::new();
934 let mut entry = wf.add_entry("https://example.com/repo1", ".*\\.tar\\.gz");
935
936 assert_eq!(
937 entry.source().unwrap(),
938 Some("https://example.com/repo1".to_string())
939 );
940
941 entry.set_source("https://example.com/repo2");
942 assert_eq!(
943 entry.source().unwrap(),
944 Some("https://example.com/repo2".to_string())
945 );
946 }
947
948 #[test]
949 fn test_set_matching_pattern() {
950 let mut wf = WatchFile::new();
951 let mut entry = wf.add_entry("https://example.com/repo1", ".*\\.tar\\.gz");
952
953 assert_eq!(
954 entry.matching_pattern().unwrap(),
955 Some(".*\\.tar\\.gz".to_string())
956 );
957
958 entry.set_matching_pattern(".*/v?([\\d.]+)\\.tar\\.gz");
959 assert_eq!(
960 entry.matching_pattern().unwrap(),
961 Some(".*/v?([\\d.]+)\\.tar\\.gz".to_string())
962 );
963 }
964
965 #[test]
966 fn test_entry_line() {
967 let input = r#"Version: 5
968
969Source: https://example.com/repo1
970Matching-Pattern: .*\.tar\.gz
971
972Source: https://example.com/repo2
973Matching-Pattern: .*\.tar\.xz
974"#;
975
976 let wf: WatchFile = input.parse().unwrap();
977 let entries: Vec<_> = wf.entries().collect();
978
979 assert_eq!(entries[0].line(), 2);
981 assert_eq!(entries[1].line(), 5);
983 }
984
985 #[test]
986 fn test_defaults_with_case_variations() {
987 let input = r#"Version: 5
988
989compression: xz
990user-agent: Custom/1.0
991
992Source: https://example.com/repo1
993Matching-Pattern: .*\.tar\.gz
994"#;
995
996 let wf: WatchFile = input.parse().unwrap();
997
998 let entries: Vec<_> = wf.entries().collect();
1000 assert_eq!(entries.len(), 1);
1001
1002 assert_eq!(entries[0].get_option("Compression"), Some("xz".to_string()));
1004 assert_eq!(
1005 entries[0].get_option("User-Agent"),
1006 Some("Custom/1.0".to_string())
1007 );
1008 }
1009
1010 #[test]
1011 fn test_v5_with_uversionmangle() {
1012 let input = r#"Version: 5
1013
1014Source: https://pypi.org/project/foo/
1015Matching-Pattern: foo-(\d+\.\d+)\.tar\.gz
1016Uversionmangle: s/\.0+$//
1017"#;
1018
1019 let wf: WatchFile = input.parse().unwrap();
1020 let entries: Vec<_> = wf.entries().collect();
1021 assert_eq!(entries.len(), 1);
1022
1023 let entry = &entries[0];
1024 assert_eq!(
1025 entry.get_option("Uversionmangle"),
1026 Some("s/\\.0+$//".to_string())
1027 );
1028 }
1029
1030 #[test]
1031 fn test_v5_with_filenamemangle() {
1032 let input = r#"Version: 5
1033
1034Source: https://example.com/files
1035Matching-Pattern: .*\.tar\.gz
1036Filenamemangle: s/.*\///;s/@PACKAGE@-(.*)\.tar\.gz/foo_$1.orig.tar.gz/
1037"#;
1038
1039 let wf: WatchFile = input.parse().unwrap();
1040 let entries: Vec<_> = wf.entries().collect();
1041 assert_eq!(entries.len(), 1);
1042
1043 let entry = &entries[0];
1044 assert_eq!(
1045 entry.get_option("Filenamemangle"),
1046 Some("s/.*\\///;s/@PACKAGE@-(.*)\\.tar\\.gz/foo_$1.orig.tar.gz/".to_string())
1047 );
1048 }
1049
1050 #[test]
1051 fn test_v5_with_searchmode() {
1052 let input = r#"Version: 5
1053
1054Source: https://example.com/files
1055Matching-Pattern: foo-(\d[\d.]*)\.tar\.gz
1056Searchmode: plain
1057"#;
1058
1059 let wf: WatchFile = input.parse().unwrap();
1060 let entries: Vec<_> = wf.entries().collect();
1061 assert_eq!(entries.len(), 1);
1062
1063 let entry = &entries[0];
1064 assert_eq!(entry.get_field("Searchmode").as_deref(), Some("plain"));
1065 }
1066
1067 #[test]
1068 fn test_v5_with_version_policy() {
1069 let input = r#"Version: 5
1070
1071Source: https://example.com/files
1072Matching-Pattern: .*\.tar\.gz
1073Version-Policy: debian
1074"#;
1075
1076 let wf: WatchFile = input.parse().unwrap();
1077 let entries: Vec<_> = wf.entries().collect();
1078 assert_eq!(entries.len(), 1);
1079
1080 let entry = &entries[0];
1081 let policy = entry.version_policy();
1082 assert!(policy.is_ok());
1083 assert_eq!(format!("{:?}", policy.unwrap().unwrap()), "Debian");
1084 }
1085
1086 #[test]
1087 fn test_v5_multiple_mangles() {
1088 let input = r#"Version: 5
1089
1090Source: https://example.com/files
1091Matching-Pattern: .*\.tar\.gz
1092Uversionmangle: s/^v//;s/\.0+$//
1093Dversionmangle: s/\+dfsg\d*$//
1094Filenamemangle: s/.*/foo-$1.tar.gz/
1095"#;
1096
1097 let wf: WatchFile = input.parse().unwrap();
1098 let entries: Vec<_> = wf.entries().collect();
1099 assert_eq!(entries.len(), 1);
1100
1101 let entry = &entries[0];
1102 assert_eq!(
1103 entry.get_option("Uversionmangle"),
1104 Some("s/^v//;s/\\.0+$//".to_string())
1105 );
1106 assert_eq!(
1107 entry.get_option("Dversionmangle"),
1108 Some("s/\\+dfsg\\d*$//".to_string())
1109 );
1110 assert_eq!(
1111 entry.get_option("Filenamemangle"),
1112 Some("s/.*/foo-$1.tar.gz/".to_string())
1113 );
1114 }
1115
1116 #[test]
1117 fn test_v5_with_pgpmode() {
1118 let input = r#"Version: 5
1119
1120Source: https://example.com/files
1121Matching-Pattern: .*\.tar\.gz
1122Pgpmode: auto
1123"#;
1124
1125 let wf: WatchFile = input.parse().unwrap();
1126 let entries: Vec<_> = wf.entries().collect();
1127 assert_eq!(entries.len(), 1);
1128
1129 let entry = &entries[0];
1130 assert_eq!(entry.get_option("Pgpmode"), Some("auto".to_string()));
1131 }
1132
1133 #[test]
1134 fn test_v5_with_comments() {
1135 let input = r#"Version: 5
1136
1137# This is a comment about the entry
1138Source: https://example.com/files
1139Matching-Pattern: .*\.tar\.gz
1140"#;
1141
1142 let wf: WatchFile = input.parse().unwrap();
1143 let entries: Vec<_> = wf.entries().collect();
1144 assert_eq!(entries.len(), 1);
1145
1146 let output = wf.to_string();
1148 assert!(output.contains("# This is a comment about the entry"));
1149 }
1150
1151 #[test]
1152 fn test_v5_empty_after_version() {
1153 let input = "Version: 5\n";
1154
1155 let wf: WatchFile = input.parse().unwrap();
1156 assert_eq!(wf.version(), 5);
1157
1158 let entries: Vec<_> = wf.entries().collect();
1159 assert_eq!(entries.len(), 0);
1160 }
1161
1162 #[test]
1163 fn test_v5_trait_url() {
1164 let input = r#"Version: 5
1165
1166Source: https://example.com/files/@PACKAGE@
1167Matching-Pattern: .*\.tar\.gz
1168"#;
1169
1170 let wf: WatchFile = input.parse().unwrap();
1171 let entries: Vec<_> = wf.entries().collect();
1172 assert_eq!(entries.len(), 1);
1173
1174 let entry = &entries[0];
1175 assert_eq!(
1177 entry.source().unwrap().as_deref(),
1178 Some("https://example.com/files/@PACKAGE@")
1179 );
1180 }
1181
1182 #[test]
1183 fn test_github_template() {
1184 let input = r#"Version: 5
1185
1186Template: GitHub
1187Owner: torvalds
1188Project: linux
1189"#;
1190
1191 let wf: WatchFile = input.parse().unwrap();
1192 let entries: Vec<_> = wf.entries().collect();
1193 assert_eq!(entries.len(), 1);
1194
1195 let entry = &entries[0];
1196 assert_eq!(
1197 entry.source().unwrap(),
1198 Some("https://github.com/torvalds/linux/tags".to_string())
1199 );
1200 assert_eq!(
1201 entry.matching_pattern().unwrap(),
1202 Some(r".*/(?:refs/tags/)?v?@ANY_VERSION@@ARCHIVE_EXT@".to_string())
1203 );
1204 }
1205
1206 #[test]
1207 fn test_github_template_with_dist() {
1208 let input = r#"Version: 5
1209
1210Template: GitHub
1211Dist: https://github.com/guimard/llng-docker
1212"#;
1213
1214 let wf: WatchFile = input.parse().unwrap();
1215 let entries: Vec<_> = wf.entries().collect();
1216 assert_eq!(entries.len(), 1);
1217
1218 let entry = &entries[0];
1219 assert_eq!(
1220 entry.source().unwrap(),
1221 Some("https://github.com/guimard/llng-docker/tags".to_string())
1222 );
1223 }
1224
1225 #[test]
1226 fn test_pypi_template() {
1227 let input = r#"Version: 5
1228
1229Template: PyPI
1230Dist: bitbox02
1231"#;
1232
1233 let wf: WatchFile = input.parse().unwrap();
1234 let entries: Vec<_> = wf.entries().collect();
1235 assert_eq!(entries.len(), 1);
1236
1237 let entry = &entries[0];
1238 assert_eq!(
1239 entry.source().unwrap(),
1240 Some("https://pypi.debian.net/bitbox02/".to_string())
1241 );
1242 assert_eq!(
1243 entry.matching_pattern().unwrap(),
1244 Some(
1245 r"https://pypi\.debian\.net/bitbox02/[^/]+\.tar\.gz#/.*-@ANY_VERSION@\.tar\.gz"
1246 .to_string()
1247 )
1248 );
1249 }
1250
1251 #[test]
1252 fn test_gitlab_template() {
1253 let input = r#"Version: 5
1254
1255Template: GitLab
1256Dist: https://salsa.debian.org/debian/devscripts
1257"#;
1258
1259 let wf: WatchFile = input.parse().unwrap();
1260 let entries: Vec<_> = wf.entries().collect();
1261 assert_eq!(entries.len(), 1);
1262
1263 let entry = &entries[0];
1264 assert_eq!(
1265 entry.source().unwrap(),
1266 Some("https://salsa.debian.org/debian/devscripts".to_string())
1267 );
1268 assert_eq!(
1269 entry.matching_pattern().unwrap(),
1270 Some(r".*/v?@ANY_VERSION@@ARCHIVE_EXT@".to_string())
1271 );
1272 }
1273
1274 #[test]
1275 fn test_template_with_explicit_source() {
1276 let input = r#"Version: 5
1278
1279Template: GitHub
1280Owner: test
1281Project: project
1282Source: https://custom.example.com/
1283"#;
1284
1285 let wf: WatchFile = input.parse().unwrap();
1286 let entries: Vec<_> = wf.entries().collect();
1287 assert_eq!(entries.len(), 1);
1288
1289 let entry = &entries[0];
1290 assert_eq!(
1291 entry.source().unwrap(),
1292 Some("https://custom.example.com/".to_string())
1293 );
1294 }
1295
1296 #[test]
1297 fn test_convert_to_template_github() {
1298 let mut wf = WatchFile::new();
1299 let mut entry = wf.add_entry(
1300 "https://github.com/torvalds/linux/tags",
1301 r".*/(?:refs/tags/)?v?@ANY_VERSION@@ARCHIVE_EXT@",
1302 );
1303 entry.set_option_str("Searchmode", "html");
1304
1305 let template = entry.try_convert_to_template();
1307 assert_eq!(
1308 template,
1309 Some(crate::templates::Template::GitHub {
1310 owner: "torvalds".to_string(),
1311 repository: "linux".to_string(),
1312 release_only: false,
1313 version_type: None,
1314 })
1315 );
1316
1317 assert_eq!(entry.get_field("Template"), Some("GitHub".to_string()));
1319 assert_eq!(entry.get_field("Owner"), Some("torvalds".to_string()));
1320 assert_eq!(entry.get_field("Project"), Some("linux".to_string()));
1321 assert_eq!(entry.get_field("Source"), None);
1322 assert_eq!(entry.get_field("Matching-Pattern"), None);
1323 }
1324
1325 #[test]
1326 fn test_convert_to_template_pypi() {
1327 let mut wf = WatchFile::new();
1328 let mut entry = wf.add_entry(
1329 "https://pypi.debian.net/bitbox02/",
1330 r"https://pypi\.debian\.net/bitbox02/[^/]+\.tar\.gz#/.*-@ANY_VERSION@\.tar\.gz",
1331 );
1332 entry.set_option_str("Searchmode", "plain");
1333
1334 let template = entry.try_convert_to_template();
1336 assert_eq!(
1337 template,
1338 Some(crate::templates::Template::PyPI {
1339 package: "bitbox02".to_string(),
1340 version_type: None,
1341 })
1342 );
1343
1344 assert_eq!(entry.get_field("Template"), Some("PyPI".to_string()));
1346 assert_eq!(entry.get_field("Dist"), Some("bitbox02".to_string()));
1347 }
1348
1349 #[test]
1350 fn test_convert_to_template_no_match() {
1351 let mut wf = WatchFile::new();
1352 let mut entry = wf.add_entry(
1353 "https://example.com/downloads/",
1354 r".*/v?(\d+\.\d+)\.tar\.gz",
1355 );
1356
1357 let template = entry.try_convert_to_template();
1359 assert_eq!(template, None);
1360
1361 assert_eq!(
1363 entry.source().unwrap(),
1364 Some("https://example.com/downloads/".to_string())
1365 );
1366 }
1367
1368 #[test]
1369 fn test_convert_to_template_roundtrip() {
1370 let mut wf = WatchFile::new();
1371 let mut entry = wf.add_entry(
1372 "https://github.com/test/project/releases",
1373 r".*/(?:refs/tags/)?v?@ANY_VERSION@@ARCHIVE_EXT@",
1374 );
1375 entry.set_option_str("Searchmode", "html");
1376
1377 entry.try_convert_to_template().unwrap();
1379
1380 let source = entry.source().unwrap();
1382 let matching_pattern = entry.matching_pattern().unwrap();
1383
1384 assert_eq!(
1385 source,
1386 Some("https://github.com/test/project/releases".to_string())
1387 );
1388 assert_eq!(
1389 matching_pattern,
1390 Some(r".*/(?:refs/tags/)?v?@ANY_VERSION@@ARCHIVE_EXT@".to_string())
1391 );
1392 }
1393}