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 _ => return Err(TemplateError::UnknownTemplate(template_str)),
508 };
509
510 Ok(expand_template(template))
511 }
512
513 pub fn try_convert_to_template(&mut self) -> Option<crate::templates::Template> {
545 use crate::templates::detect_template;
546
547 let source = self.source().ok().flatten();
549 let matching_pattern = self.matching_pattern().ok().flatten();
550 let searchmode = self.get_field("Searchmode");
551 let mode = self.get_field("Mode");
552
553 let template = detect_template(
555 source.as_deref(),
556 matching_pattern.as_deref(),
557 searchmode.as_deref(),
558 mode.as_deref(),
559 )?;
560
561 self.paragraph.remove("Source");
563 self.paragraph.remove("Matching-Pattern");
564 self.paragraph.remove("Searchmode");
565 self.paragraph.remove("Mode");
566
567 match &template {
569 crate::templates::Template::GitHub {
570 owner,
571 repository,
572 release_only,
573 version_type,
574 } => {
575 self.paragraph.set("Template", "GitHub");
576 self.paragraph.set("Owner", owner);
577 self.paragraph.set("Project", repository);
578 if *release_only {
579 self.paragraph.set("Release-Only", "yes");
580 }
581 if let Some(vt) = version_type {
582 self.paragraph.set("Version-Type", vt);
583 }
584 }
585 crate::templates::Template::GitLab {
586 dist,
587 release_only: _,
588 version_type,
589 } => {
590 self.paragraph.set("Template", "GitLab");
591 self.paragraph.set("Dist", dist);
592 if let Some(vt) = version_type {
593 self.paragraph.set("Version-Type", vt);
594 }
595 }
596 crate::templates::Template::PyPI {
597 package,
598 version_type,
599 } => {
600 self.paragraph.set("Template", "PyPI");
601 self.paragraph.set("Dist", package);
602 if let Some(vt) = version_type {
603 self.paragraph.set("Version-Type", vt);
604 }
605 }
606 crate::templates::Template::Npmregistry {
607 package,
608 version_type,
609 } => {
610 self.paragraph.set("Template", "Npmregistry");
611 self.paragraph.set("Dist", package);
612 if let Some(vt) = version_type {
613 self.paragraph.set("Version-Type", vt);
614 }
615 }
616 crate::templates::Template::Metacpan { dist, version_type } => {
617 self.paragraph.set("Template", "Metacpan");
618 self.paragraph.set("Dist", dist);
619 if let Some(vt) = version_type {
620 self.paragraph.set("Version-Type", vt);
621 }
622 }
623 }
624
625 Some(template)
626 }
627}
628
629fn normalize_key(key: &str) -> String {
633 key.to_lowercase().replace(['-', '_'], "")
634}
635
636#[cfg(test)]
637mod tests {
638 use super::*;
639
640 #[test]
641 fn test_as_deb822() {
642 let input = r#"Version: 5
643
644Source: https://github.com/owner/repo/tags
645Matching-Pattern: .*/v?(\d\S+)\.tar\.gz
646"#;
647
648 let wf: WatchFile = input.parse().unwrap();
649 let deb822 = wf.as_deb822();
650
651 assert_eq!(deb822.paragraphs().count(), 2);
653 }
654
655 #[test]
656 fn test_create_v5_watchfile() {
657 let wf = WatchFile::new();
658 assert_eq!(wf.version(), 5);
659
660 let output = wf.to_string();
661 assert!(output.contains("Version"));
662 assert!(output.contains("5"));
663 }
664
665 #[test]
666 fn test_parse_v5_basic() {
667 let input = r#"Version: 5
668
669Source: https://github.com/owner/repo/tags
670Matching-Pattern: .*/v?(\d\S+)\.tar\.gz
671"#;
672
673 let wf: WatchFile = input.parse().unwrap();
674 assert_eq!(wf.version(), 5);
675
676 let entries: Vec<_> = wf.entries().collect();
677 assert_eq!(entries.len(), 1);
678
679 let entry = &entries[0];
680 assert_eq!(
681 entry.source().unwrap().as_deref(),
682 Some("https://github.com/owner/repo/tags")
683 );
684 assert_eq!(
685 entry.matching_pattern().unwrap(),
686 Some(".*/v?(\\d\\S+)\\.tar\\.gz".to_string())
687 );
688 }
689
690 #[test]
691 fn test_parse_v5_multiple_entries() {
692 let input = r#"Version: 5
693
694Source: https://github.com/owner/repo1/tags
695Matching-Pattern: .*/v?(\d\S+)\.tar\.gz
696
697Source: https://github.com/owner/repo2/tags
698Matching-Pattern: .*/release-(\d\S+)\.tar\.gz
699"#;
700
701 let wf: WatchFile = input.parse().unwrap();
702 let entries: Vec<_> = wf.entries().collect();
703 assert_eq!(entries.len(), 2);
704
705 assert_eq!(
706 entries[0].source().unwrap().as_deref(),
707 Some("https://github.com/owner/repo1/tags")
708 );
709 assert_eq!(
710 entries[1].source().unwrap().as_deref(),
711 Some("https://github.com/owner/repo2/tags")
712 );
713 }
714
715 #[test]
716 fn test_v5_case_insensitive_fields() {
717 let input = r#"Version: 5
718
719source: https://example.com/files
720matching-pattern: .*\.tar\.gz
721"#;
722
723 let wf: WatchFile = input.parse().unwrap();
724 let entries: Vec<_> = wf.entries().collect();
725 assert_eq!(entries.len(), 1);
726
727 let entry = &entries[0];
728 assert_eq!(
729 entry.source().unwrap().as_deref(),
730 Some("https://example.com/files")
731 );
732 assert_eq!(
733 entry.matching_pattern().unwrap().as_deref(),
734 Some(".*\\.tar\\.gz")
735 );
736 }
737
738 #[test]
739 fn test_v5_with_compression_option() {
740 let input = r#"Version: 5
741
742Source: https://example.com/files
743Matching-Pattern: .*\.tar\.gz
744Compression: xz
745"#;
746
747 let wf: WatchFile = input.parse().unwrap();
748 let entries: Vec<_> = wf.entries().collect();
749 assert_eq!(entries.len(), 1);
750
751 let entry = &entries[0];
752 let compression = entry.get_option("compression");
753 assert!(compression.is_some());
754 }
755
756 #[test]
757 fn test_v5_with_component() {
758 let input = r#"Version: 5
759
760Source: https://example.com/files
761Matching-Pattern: .*\.tar\.gz
762Component: foo
763"#;
764
765 let wf: WatchFile = input.parse().unwrap();
766 let entries: Vec<_> = wf.entries().collect();
767 assert_eq!(entries.len(), 1);
768
769 let entry = &entries[0];
770 assert_eq!(entry.component(), Some("foo".to_string()));
771 }
772
773 #[test]
774 fn test_v5_rejects_wrong_version() {
775 let input = r#"Version: 4
776
777Source: https://example.com/files
778Matching-Pattern: .*\.tar\.gz
779"#;
780
781 let result: Result<WatchFile, _> = input.parse();
782 assert!(result.is_err());
783 }
784
785 #[test]
786 fn test_v5_roundtrip() {
787 let input = r#"Version: 5
788
789Source: https://example.com/files
790Matching-Pattern: .*\.tar\.gz
791"#;
792
793 let wf: WatchFile = input.parse().unwrap();
794 let output = wf.to_string();
795
796 let wf2: WatchFile = output.parse().unwrap();
798 assert_eq!(wf2.version(), 5);
799
800 let entries: Vec<_> = wf2.entries().collect();
801 assert_eq!(entries.len(), 1);
802 }
803
804 #[test]
805 fn test_normalize_key() {
806 assert_eq!(normalize_key("Matching-Pattern"), "matchingpattern");
807 assert_eq!(normalize_key("matching_pattern"), "matchingpattern");
808 assert_eq!(normalize_key("MatchingPattern"), "matchingpattern");
809 assert_eq!(normalize_key("MATCHING-PATTERN"), "matchingpattern");
810 }
811
812 #[test]
813 fn test_defaults_paragraph() {
814 let input = r#"Version: 5
815
816Compression: xz
817User-Agent: Custom/1.0
818
819Source: https://example.com/repo1
820Matching-Pattern: .*\.tar\.gz
821
822Source: https://example.com/repo2
823Matching-Pattern: .*\.tar\.gz
824Compression: gz
825"#;
826
827 let wf: WatchFile = input.parse().unwrap();
828
829 let defaults = wf.defaults();
831 assert!(defaults.is_some());
832 let defaults = defaults.unwrap();
833 assert_eq!(defaults.get("Compression"), Some("xz".to_string()));
834 assert_eq!(defaults.get("User-Agent"), Some("Custom/1.0".to_string()));
835
836 let entries: Vec<_> = wf.entries().collect();
838 assert_eq!(entries.len(), 2);
839
840 assert_eq!(entries[0].get_option("Compression"), Some("xz".to_string()));
842 assert_eq!(
843 entries[0].get_option("User-Agent"),
844 Some("Custom/1.0".to_string())
845 );
846
847 assert_eq!(entries[1].get_option("Compression"), Some("gz".to_string()));
849 assert_eq!(
850 entries[1].get_option("User-Agent"),
851 Some("Custom/1.0".to_string())
852 );
853 }
854
855 #[test]
856 fn test_no_defaults_paragraph() {
857 let input = r#"Version: 5
858
859Source: https://example.com/repo1
860Matching-Pattern: .*\.tar\.gz
861"#;
862
863 let wf: WatchFile = input.parse().unwrap();
864
865 assert!(wf.defaults().is_none());
867
868 let entries: Vec<_> = wf.entries().collect();
869 assert_eq!(entries.len(), 1);
870 }
871
872 #[test]
873 fn test_set_source() {
874 let mut wf = WatchFile::new();
875 let mut entry = wf.add_entry("https://example.com/repo1", ".*\\.tar\\.gz");
876
877 assert_eq!(
878 entry.source().unwrap(),
879 Some("https://example.com/repo1".to_string())
880 );
881
882 entry.set_source("https://example.com/repo2");
883 assert_eq!(
884 entry.source().unwrap(),
885 Some("https://example.com/repo2".to_string())
886 );
887 }
888
889 #[test]
890 fn test_set_matching_pattern() {
891 let mut wf = WatchFile::new();
892 let mut entry = wf.add_entry("https://example.com/repo1", ".*\\.tar\\.gz");
893
894 assert_eq!(
895 entry.matching_pattern().unwrap(),
896 Some(".*\\.tar\\.gz".to_string())
897 );
898
899 entry.set_matching_pattern(".*/v?([\\d.]+)\\.tar\\.gz");
900 assert_eq!(
901 entry.matching_pattern().unwrap(),
902 Some(".*/v?([\\d.]+)\\.tar\\.gz".to_string())
903 );
904 }
905
906 #[test]
907 fn test_entry_line() {
908 let input = r#"Version: 5
909
910Source: https://example.com/repo1
911Matching-Pattern: .*\.tar\.gz
912
913Source: https://example.com/repo2
914Matching-Pattern: .*\.tar\.xz
915"#;
916
917 let wf: WatchFile = input.parse().unwrap();
918 let entries: Vec<_> = wf.entries().collect();
919
920 assert_eq!(entries[0].line(), 2);
922 assert_eq!(entries[1].line(), 5);
924 }
925
926 #[test]
927 fn test_defaults_with_case_variations() {
928 let input = r#"Version: 5
929
930compression: xz
931user-agent: Custom/1.0
932
933Source: https://example.com/repo1
934Matching-Pattern: .*\.tar\.gz
935"#;
936
937 let wf: WatchFile = input.parse().unwrap();
938
939 let entries: Vec<_> = wf.entries().collect();
941 assert_eq!(entries.len(), 1);
942
943 assert_eq!(entries[0].get_option("Compression"), Some("xz".to_string()));
945 assert_eq!(
946 entries[0].get_option("User-Agent"),
947 Some("Custom/1.0".to_string())
948 );
949 }
950
951 #[test]
952 fn test_v5_with_uversionmangle() {
953 let input = r#"Version: 5
954
955Source: https://pypi.org/project/foo/
956Matching-Pattern: foo-(\d+\.\d+)\.tar\.gz
957Uversionmangle: s/\.0+$//
958"#;
959
960 let wf: WatchFile = input.parse().unwrap();
961 let entries: Vec<_> = wf.entries().collect();
962 assert_eq!(entries.len(), 1);
963
964 let entry = &entries[0];
965 assert_eq!(
966 entry.get_option("Uversionmangle"),
967 Some("s/\\.0+$//".to_string())
968 );
969 }
970
971 #[test]
972 fn test_v5_with_filenamemangle() {
973 let input = r#"Version: 5
974
975Source: https://example.com/files
976Matching-Pattern: .*\.tar\.gz
977Filenamemangle: s/.*\///;s/@PACKAGE@-(.*)\.tar\.gz/foo_$1.orig.tar.gz/
978"#;
979
980 let wf: WatchFile = input.parse().unwrap();
981 let entries: Vec<_> = wf.entries().collect();
982 assert_eq!(entries.len(), 1);
983
984 let entry = &entries[0];
985 assert_eq!(
986 entry.get_option("Filenamemangle"),
987 Some("s/.*\\///;s/@PACKAGE@-(.*)\\.tar\\.gz/foo_$1.orig.tar.gz/".to_string())
988 );
989 }
990
991 #[test]
992 fn test_v5_with_searchmode() {
993 let input = r#"Version: 5
994
995Source: https://example.com/files
996Matching-Pattern: foo-(\d[\d.]*)\.tar\.gz
997Searchmode: plain
998"#;
999
1000 let wf: WatchFile = input.parse().unwrap();
1001 let entries: Vec<_> = wf.entries().collect();
1002 assert_eq!(entries.len(), 1);
1003
1004 let entry = &entries[0];
1005 assert_eq!(entry.get_field("Searchmode").as_deref(), Some("plain"));
1006 }
1007
1008 #[test]
1009 fn test_v5_with_version_policy() {
1010 let input = r#"Version: 5
1011
1012Source: https://example.com/files
1013Matching-Pattern: .*\.tar\.gz
1014Version-Policy: debian
1015"#;
1016
1017 let wf: WatchFile = input.parse().unwrap();
1018 let entries: Vec<_> = wf.entries().collect();
1019 assert_eq!(entries.len(), 1);
1020
1021 let entry = &entries[0];
1022 let policy = entry.version_policy();
1023 assert!(policy.is_ok());
1024 assert_eq!(format!("{:?}", policy.unwrap().unwrap()), "Debian");
1025 }
1026
1027 #[test]
1028 fn test_v5_multiple_mangles() {
1029 let input = r#"Version: 5
1030
1031Source: https://example.com/files
1032Matching-Pattern: .*\.tar\.gz
1033Uversionmangle: s/^v//;s/\.0+$//
1034Dversionmangle: s/\+dfsg\d*$//
1035Filenamemangle: s/.*/foo-$1.tar.gz/
1036"#;
1037
1038 let wf: WatchFile = input.parse().unwrap();
1039 let entries: Vec<_> = wf.entries().collect();
1040 assert_eq!(entries.len(), 1);
1041
1042 let entry = &entries[0];
1043 assert_eq!(
1044 entry.get_option("Uversionmangle"),
1045 Some("s/^v//;s/\\.0+$//".to_string())
1046 );
1047 assert_eq!(
1048 entry.get_option("Dversionmangle"),
1049 Some("s/\\+dfsg\\d*$//".to_string())
1050 );
1051 assert_eq!(
1052 entry.get_option("Filenamemangle"),
1053 Some("s/.*/foo-$1.tar.gz/".to_string())
1054 );
1055 }
1056
1057 #[test]
1058 fn test_v5_with_pgpmode() {
1059 let input = r#"Version: 5
1060
1061Source: https://example.com/files
1062Matching-Pattern: .*\.tar\.gz
1063Pgpmode: auto
1064"#;
1065
1066 let wf: WatchFile = input.parse().unwrap();
1067 let entries: Vec<_> = wf.entries().collect();
1068 assert_eq!(entries.len(), 1);
1069
1070 let entry = &entries[0];
1071 assert_eq!(entry.get_option("Pgpmode"), Some("auto".to_string()));
1072 }
1073
1074 #[test]
1075 fn test_v5_with_comments() {
1076 let input = r#"Version: 5
1077
1078# This is a comment about the entry
1079Source: https://example.com/files
1080Matching-Pattern: .*\.tar\.gz
1081"#;
1082
1083 let wf: WatchFile = input.parse().unwrap();
1084 let entries: Vec<_> = wf.entries().collect();
1085 assert_eq!(entries.len(), 1);
1086
1087 let output = wf.to_string();
1089 assert!(output.contains("# This is a comment about the entry"));
1090 }
1091
1092 #[test]
1093 fn test_v5_empty_after_version() {
1094 let input = "Version: 5\n";
1095
1096 let wf: WatchFile = input.parse().unwrap();
1097 assert_eq!(wf.version(), 5);
1098
1099 let entries: Vec<_> = wf.entries().collect();
1100 assert_eq!(entries.len(), 0);
1101 }
1102
1103 #[test]
1104 fn test_v5_trait_url() {
1105 let input = r#"Version: 5
1106
1107Source: https://example.com/files/@PACKAGE@
1108Matching-Pattern: .*\.tar\.gz
1109"#;
1110
1111 let wf: WatchFile = input.parse().unwrap();
1112 let entries: Vec<_> = wf.entries().collect();
1113 assert_eq!(entries.len(), 1);
1114
1115 let entry = &entries[0];
1116 assert_eq!(
1118 entry.source().unwrap().as_deref(),
1119 Some("https://example.com/files/@PACKAGE@")
1120 );
1121 }
1122
1123 #[test]
1124 fn test_github_template() {
1125 let input = r#"Version: 5
1126
1127Template: GitHub
1128Owner: torvalds
1129Project: linux
1130"#;
1131
1132 let wf: WatchFile = input.parse().unwrap();
1133 let entries: Vec<_> = wf.entries().collect();
1134 assert_eq!(entries.len(), 1);
1135
1136 let entry = &entries[0];
1137 assert_eq!(
1138 entry.source().unwrap(),
1139 Some("https://github.com/torvalds/linux/tags".to_string())
1140 );
1141 assert_eq!(
1142 entry.matching_pattern().unwrap(),
1143 Some(r".*/(?:refs/tags/)?v?@ANY_VERSION@@ARCHIVE_EXT@".to_string())
1144 );
1145 }
1146
1147 #[test]
1148 fn test_github_template_with_dist() {
1149 let input = r#"Version: 5
1150
1151Template: GitHub
1152Dist: https://github.com/guimard/llng-docker
1153"#;
1154
1155 let wf: WatchFile = input.parse().unwrap();
1156 let entries: Vec<_> = wf.entries().collect();
1157 assert_eq!(entries.len(), 1);
1158
1159 let entry = &entries[0];
1160 assert_eq!(
1161 entry.source().unwrap(),
1162 Some("https://github.com/guimard/llng-docker/tags".to_string())
1163 );
1164 }
1165
1166 #[test]
1167 fn test_pypi_template() {
1168 let input = r#"Version: 5
1169
1170Template: PyPI
1171Dist: bitbox02
1172"#;
1173
1174 let wf: WatchFile = input.parse().unwrap();
1175 let entries: Vec<_> = wf.entries().collect();
1176 assert_eq!(entries.len(), 1);
1177
1178 let entry = &entries[0];
1179 assert_eq!(
1180 entry.source().unwrap(),
1181 Some("https://pypi.debian.net/bitbox02/".to_string())
1182 );
1183 assert_eq!(
1184 entry.matching_pattern().unwrap(),
1185 Some(
1186 r"https://pypi\.debian\.net/bitbox02/[^/]+\.tar\.gz#/.*-@ANY_VERSION@\.tar\.gz"
1187 .to_string()
1188 )
1189 );
1190 }
1191
1192 #[test]
1193 fn test_gitlab_template() {
1194 let input = r#"Version: 5
1195
1196Template: GitLab
1197Dist: https://salsa.debian.org/debian/devscripts
1198"#;
1199
1200 let wf: WatchFile = input.parse().unwrap();
1201 let entries: Vec<_> = wf.entries().collect();
1202 assert_eq!(entries.len(), 1);
1203
1204 let entry = &entries[0];
1205 assert_eq!(
1206 entry.source().unwrap(),
1207 Some("https://salsa.debian.org/debian/devscripts".to_string())
1208 );
1209 assert_eq!(
1210 entry.matching_pattern().unwrap(),
1211 Some(r".*/v?@ANY_VERSION@@ARCHIVE_EXT@".to_string())
1212 );
1213 }
1214
1215 #[test]
1216 fn test_template_with_explicit_source() {
1217 let input = r#"Version: 5
1219
1220Template: GitHub
1221Owner: test
1222Project: project
1223Source: https://custom.example.com/
1224"#;
1225
1226 let wf: WatchFile = input.parse().unwrap();
1227 let entries: Vec<_> = wf.entries().collect();
1228 assert_eq!(entries.len(), 1);
1229
1230 let entry = &entries[0];
1231 assert_eq!(
1232 entry.source().unwrap(),
1233 Some("https://custom.example.com/".to_string())
1234 );
1235 }
1236
1237 #[test]
1238 fn test_convert_to_template_github() {
1239 let mut wf = WatchFile::new();
1240 let mut entry = wf.add_entry(
1241 "https://github.com/torvalds/linux/tags",
1242 r".*/(?:refs/tags/)?v?@ANY_VERSION@@ARCHIVE_EXT@",
1243 );
1244 entry.set_option_str("Searchmode", "html");
1245
1246 let template = entry.try_convert_to_template();
1248 assert_eq!(
1249 template,
1250 Some(crate::templates::Template::GitHub {
1251 owner: "torvalds".to_string(),
1252 repository: "linux".to_string(),
1253 release_only: false,
1254 version_type: None,
1255 })
1256 );
1257
1258 assert_eq!(entry.get_field("Template"), Some("GitHub".to_string()));
1260 assert_eq!(entry.get_field("Owner"), Some("torvalds".to_string()));
1261 assert_eq!(entry.get_field("Project"), Some("linux".to_string()));
1262 assert_eq!(entry.get_field("Source"), None);
1263 assert_eq!(entry.get_field("Matching-Pattern"), None);
1264 }
1265
1266 #[test]
1267 fn test_convert_to_template_pypi() {
1268 let mut wf = WatchFile::new();
1269 let mut entry = wf.add_entry(
1270 "https://pypi.debian.net/bitbox02/",
1271 r"https://pypi\.debian\.net/bitbox02/[^/]+\.tar\.gz#/.*-@ANY_VERSION@\.tar\.gz",
1272 );
1273 entry.set_option_str("Searchmode", "plain");
1274
1275 let template = entry.try_convert_to_template();
1277 assert_eq!(
1278 template,
1279 Some(crate::templates::Template::PyPI {
1280 package: "bitbox02".to_string(),
1281 version_type: None,
1282 })
1283 );
1284
1285 assert_eq!(entry.get_field("Template"), Some("PyPI".to_string()));
1287 assert_eq!(entry.get_field("Dist"), Some("bitbox02".to_string()));
1288 }
1289
1290 #[test]
1291 fn test_convert_to_template_no_match() {
1292 let mut wf = WatchFile::new();
1293 let mut entry = wf.add_entry(
1294 "https://example.com/downloads/",
1295 r".*/v?(\d+\.\d+)\.tar\.gz",
1296 );
1297
1298 let template = entry.try_convert_to_template();
1300 assert_eq!(template, None);
1301
1302 assert_eq!(
1304 entry.source().unwrap(),
1305 Some("https://example.com/downloads/".to_string())
1306 );
1307 }
1308
1309 #[test]
1310 fn test_convert_to_template_roundtrip() {
1311 let mut wf = WatchFile::new();
1312 let mut entry = wf.add_entry(
1313 "https://github.com/test/project/releases",
1314 r".*/(?:refs/tags/)?v?@ANY_VERSION@@ARCHIVE_EXT@",
1315 );
1316 entry.set_option_str("Searchmode", "html");
1317
1318 entry.try_convert_to_template().unwrap();
1320
1321 let source = entry.source().unwrap();
1323 let matching_pattern = entry.matching_pattern().unwrap();
1324
1325 assert_eq!(
1326 source,
1327 Some("https://github.com/test/project/releases".to_string())
1328 );
1329 assert_eq!(
1330 matching_pattern,
1331 Some(r".*/(?:refs/tags/)?v?@ANY_VERSION@@ARCHIVE_EXT@".to_string())
1332 );
1333 }
1334}