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