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