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