1#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum TemplateError {
18 UnknownTemplate(String),
20 MissingField {
22 template: String,
24 field: String,
26 },
27 InvalidValue {
29 field: String,
31 reason: String,
33 },
34}
35
36impl std::fmt::Display for TemplateError {
37 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
38 match self {
39 TemplateError::UnknownTemplate(t) => write!(f, "Unknown template type: {}", t),
40 TemplateError::MissingField { template, field } => {
41 write!(f, "{} template requires '{}' field", template, field)
42 }
43 TemplateError::InvalidValue { field, reason } => {
44 write!(f, "Invalid value for '{}': {}", field, reason)
45 }
46 }
47 }
48}
49
50impl std::error::Error for TemplateError {}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
54pub enum Template {
55 GitHub {
57 owner: String,
59 repository: String,
61 release_only: bool,
63 version_type: Option<String>,
65 },
66 GitLab {
68 dist: String,
70 release_only: bool,
72 version_type: Option<String>,
74 },
75 PyPI {
77 package: String,
79 version_type: Option<String>,
81 },
82 Npmregistry {
84 package: String,
86 version_type: Option<String>,
88 },
89 Metacpan {
91 dist: String,
93 version_type: Option<String>,
95 },
96}
97
98#[derive(Debug, Clone, Default)]
100pub struct ExpandedTemplate {
101 pub source: Option<String>,
103 pub matching_pattern: Option<String>,
105 pub searchmode: Option<String>,
107 pub mode: Option<String>,
109 pub pgpmode: Option<String>,
111 pub downloadurlmangle: Option<String>,
113}
114
115pub fn expand_template(template: Template) -> ExpandedTemplate {
117 match template {
118 Template::GitHub {
119 owner,
120 repository,
121 release_only,
122 version_type,
123 } => expand_github_template(owner, repository, release_only, version_type),
124 Template::GitLab {
125 dist,
126 release_only,
127 version_type,
128 } => expand_gitlab_template(dist, release_only, version_type),
129 Template::PyPI {
130 package,
131 version_type,
132 } => expand_pypi_template(package, version_type),
133 Template::Npmregistry {
134 package,
135 version_type,
136 } => expand_npmregistry_template(package, version_type),
137 Template::Metacpan { dist, version_type } => expand_metacpan_template(dist, version_type),
138 }
139}
140
141fn expand_github_template(
143 owner: String,
144 repository: String,
145 release_only: bool,
146 version_type: Option<String>,
147) -> ExpandedTemplate {
148 let version_pattern = version_type
149 .as_deref()
150 .map(|v| format!("@{}_VERSION@", v.to_uppercase()))
151 .unwrap_or_else(|| "@ANY_VERSION@".to_string());
152
153 let source = if release_only {
154 format!("https://github.com/{}/{}/releases", owner, repository)
155 } else {
156 format!("https://github.com/{}/{}/tags", owner, repository)
157 };
158
159 let matching_pattern = format!(
160 r".*/(?:refs/tags/)?v?{}{}",
161 version_pattern, "@ARCHIVE_EXT@"
162 );
163
164 ExpandedTemplate {
165 source: Some(source),
166 matching_pattern: Some(matching_pattern),
167 searchmode: Some("html".to_string()),
168 ..Default::default()
169 }
170}
171
172fn expand_gitlab_template(
174 dist: String,
175 _release_only: bool,
176 version_type: Option<String>,
177) -> ExpandedTemplate {
178 let version_pattern = version_type
179 .as_deref()
180 .map(|v| format!("@{}_VERSION@", v.to_uppercase()))
181 .unwrap_or_else(|| "@ANY_VERSION@".to_string());
182
183 ExpandedTemplate {
185 source: Some(dist),
186 matching_pattern: Some(format!(r".*/v?{}{}", version_pattern, "@ARCHIVE_EXT@")),
187 mode: Some("gitlab".to_string()),
188 ..Default::default()
189 }
190}
191
192fn expand_pypi_template(package: String, version_type: Option<String>) -> ExpandedTemplate {
194 let version_pattern = version_type
195 .as_deref()
196 .map(|v| format!("@{}_VERSION@", v.to_uppercase()))
197 .unwrap_or_else(|| "@ANY_VERSION@".to_string());
198
199 ExpandedTemplate {
200 source: Some(format!("https://pypi.debian.net/{}/", package)),
201 matching_pattern: Some(format!(
202 r"https://pypi\.debian\.net/{}/[^/]+\.tar\.gz#/.*-{}\.tar\.gz",
203 package, version_pattern
204 )),
205 searchmode: Some("plain".to_string()),
206 ..Default::default()
207 }
208}
209
210fn expand_npmregistry_template(package: String, version_type: Option<String>) -> ExpandedTemplate {
212 let version_pattern = version_type
213 .as_deref()
214 .map(|v| format!("@{}_VERSION@", v.to_uppercase()))
215 .unwrap_or_else(|| "@ANY_VERSION@".to_string());
216
217 let package_name = package.trim_start_matches('@');
219
220 ExpandedTemplate {
221 source: Some(format!("https://registry.npmjs.org/{}", package)),
222 matching_pattern: Some(format!(
223 r"https://registry\.npmjs\.org/{}/-/.*-{}@ARCHIVE_EXT@",
224 package_name.replace('/', r"\/"),
225 version_pattern
226 )),
227 searchmode: Some("plain".to_string()),
228 ..Default::default()
229 }
230}
231
232fn expand_metacpan_template(dist: String, version_type: Option<String>) -> ExpandedTemplate {
234 let version_pattern = version_type
235 .as_deref()
236 .map(|v| format!("@{}_VERSION@", v.to_uppercase()))
237 .unwrap_or_else(|| "@ANY_VERSION@".to_string());
238
239 let dist_name = dist.replace("::", "-");
241
242 ExpandedTemplate {
243 source: Some("https://cpan.metacpan.org/authors/id/".to_string()),
244 matching_pattern: Some(format!(r".*/{}{}@ARCHIVE_EXT@", dist_name, version_pattern)),
245 searchmode: Some("plain".to_string()),
246 ..Default::default()
247 }
248}
249
250pub fn detect_template(
268 source: Option<&str>,
269 matching_pattern: Option<&str>,
270 searchmode: Option<&str>,
271 mode: Option<&str>,
272) -> Option<Template> {
273 let source = source?;
274
275 if let Some(template) = detect_github_template(source, matching_pattern, searchmode) {
277 return Some(template);
278 }
279
280 if let Some(template) = detect_gitlab_template(source, matching_pattern, mode) {
282 return Some(template);
283 }
284
285 if let Some(template) = detect_pypi_template(source, matching_pattern, searchmode) {
287 return Some(template);
288 }
289
290 if let Some(template) = detect_npmregistry_template(source, matching_pattern, searchmode) {
292 return Some(template);
293 }
294
295 if let Some(template) = detect_metacpan_template(source, matching_pattern, searchmode) {
297 return Some(template);
298 }
299
300 None
301}
302
303fn detect_github_template(
305 source: &str,
306 matching_pattern: Option<&str>,
307 searchmode: Option<&str>,
308) -> Option<Template> {
309 if searchmode != Some("html") && searchmode.is_some() {
311 return None;
312 }
313
314 let release_only = if source.ends_with("/releases") {
316 true
317 } else if source.ends_with("/tags") {
318 false
319 } else {
320 return None;
321 };
322
323 let url_without_suffix = if release_only {
325 source.strip_suffix("/releases")?
326 } else {
327 source.strip_suffix("/tags")?
328 };
329
330 let (owner, repository) = if let Ok(parsed) = url::Url::parse(url_without_suffix) {
331 if parsed.host_str() != Some("github.com") {
332 return None;
333 }
334 let path = parsed.path().trim_start_matches('/').trim_end_matches('/');
335 let parts: Vec<&str> = path.split('/').collect();
336 if parts.len() != 2 {
337 return None;
338 }
339 (parts[0].to_string(), parts[1].to_string())
340 } else {
341 return None;
342 };
343
344 let version_type = if let Some(pattern) = matching_pattern {
346 extract_version_type(pattern)
347 } else {
348 None
349 };
350
351 Some(Template::GitHub {
352 owner,
353 repository,
354 release_only,
355 version_type,
356 })
357}
358
359fn detect_gitlab_template(
361 source: &str,
362 matching_pattern: Option<&str>,
363 mode: Option<&str>,
364) -> Option<Template> {
365 if mode != Some("gitlab") {
367 return None;
368 }
369
370 let version_type = if let Some(pattern) = matching_pattern {
372 extract_version_type(pattern)
373 } else {
374 None
375 };
376
377 Some(Template::GitLab {
378 dist: source.to_string(),
379 release_only: false, version_type,
381 })
382}
383
384fn detect_pypi_template(
386 source: &str,
387 matching_pattern: Option<&str>,
388 searchmode: Option<&str>,
389) -> Option<Template> {
390 if searchmode != Some("plain") && searchmode.is_some() {
392 return None;
393 }
394
395 if !source.starts_with("https://pypi.debian.net/") {
397 return None;
398 }
399
400 let remainder = source
401 .strip_prefix("https://pypi.debian.net/")?
402 .trim_end_matches('/');
403
404 let package = match remainder.split_once('/') {
406 Some((pkg, _)) => pkg,
407 None => remainder,
408 };
409
410 let version_type = if let Some(pattern) = matching_pattern {
412 extract_version_type(pattern)
413 } else {
414 None
415 };
416
417 Some(Template::PyPI {
418 package: package.to_string(),
419 version_type,
420 })
421}
422
423fn detect_npmregistry_template(
425 source: &str,
426 matching_pattern: Option<&str>,
427 searchmode: Option<&str>,
428) -> Option<Template> {
429 if searchmode != Some("plain") && searchmode.is_some() {
431 return None;
432 }
433
434 if !source.starts_with("https://registry.npmjs.org/") {
436 return None;
437 }
438
439 let package = source.strip_prefix("https://registry.npmjs.org/")?;
440
441 let version_type = if let Some(pattern) = matching_pattern {
443 extract_version_type(pattern)
444 } else {
445 None
446 };
447
448 Some(Template::Npmregistry {
449 package: package.to_string(),
450 version_type,
451 })
452}
453
454fn detect_metacpan_template(
456 source: &str,
457 matching_pattern: Option<&str>,
458 searchmode: Option<&str>,
459) -> Option<Template> {
460 if searchmode != Some("plain") && searchmode.is_some() {
462 return None;
463 }
464
465 if source == "https://cpan.metacpan.org/authors/id/" {
466 let pattern = matching_pattern?;
468
469 if !pattern.starts_with(".*/") {
472 return None;
473 }
474
475 let after_prefix = pattern.strip_prefix(".*/").unwrap();
476
477 let version_type = extract_version_type(pattern);
479
480 let dist = if let Some(idx) = after_prefix.find('@') {
483 after_prefix[..idx]
484 .trim_end_matches("-v?")
485 .trim_end_matches('-')
486 } else {
487 return None;
488 };
489
490 Some(Template::Metacpan {
491 dist: dist.to_string(),
492 version_type,
493 })
494 } else if let Some(dist) = source
495 .strip_prefix("https://metacpan.org/release/")
496 .or_else(|| source.strip_prefix("https://metacpan.org/dist/"))
497 {
498 let dist = dist.trim_end_matches('/');
499 if dist.is_empty() {
500 return None;
501 }
502
503 let version_type = matching_pattern.and_then(extract_version_type);
504
505 Some(Template::Metacpan {
506 dist: dist.to_string(),
507 version_type,
508 })
509 } else {
510 None
511 }
512}
513
514fn extract_version_type(pattern: &str) -> Option<String> {
517 if pattern.contains("@ANY_VERSION@") {
519 None
520 } else if let Some(start) = pattern.find('@') {
521 if let Some(end) = pattern[start + 1..].find('@') {
522 let version_str = &pattern[start + 1..start + 1 + end];
523 if version_str.ends_with("_VERSION") {
524 let type_str = version_str.strip_suffix("_VERSION")?;
525 Some(type_str.to_lowercase())
526 } else {
527 None
528 }
529 } else {
530 None
531 }
532 } else {
533 None
534 }
535}
536
537pub fn parse_github_url(url: &str) -> Result<(String, String), TemplateError> {
539 let url = url.trim_end_matches('/');
540
541 if let Ok(parsed) = url::Url::parse(url) {
543 if parsed.host_str() == Some("github.com") {
544 let path = parsed.path().trim_start_matches('/').trim_end_matches('/');
545 let parts: Vec<&str> = path.split('/').collect();
546 if parts.len() >= 2 {
547 return Ok((parts[0].to_string(), parts[1].to_string()));
548 }
549 }
550 }
551
552 let parts: Vec<&str> = url.split('/').collect();
554 if parts.len() == 2 {
555 return Ok((parts[0].to_string(), parts[1].to_string()));
556 }
557
558 Err(TemplateError::InvalidValue {
559 field: "Dist".to_string(),
560 reason: format!("Could not parse GitHub URL: {}", url),
561 })
562}
563
564#[cfg(test)]
565mod tests {
566 use super::*;
567
568 #[test]
569 fn test_github_template_with_owner_repository() {
570 let template = Template::GitHub {
571 owner: "torvalds".to_string(),
572 repository: "linux".to_string(),
573 release_only: false,
574 version_type: None,
575 };
576
577 let result = expand_template(template);
578 assert_eq!(
579 result.source,
580 Some("https://github.com/torvalds/linux/tags".to_string())
581 );
582 assert_eq!(
583 result.matching_pattern,
584 Some(r".*/(?:refs/tags/)?v?@ANY_VERSION@@ARCHIVE_EXT@".to_string())
585 );
586 }
587
588 #[test]
589 fn test_github_template_release_only() {
590 let template = Template::GitHub {
591 owner: "test".to_string(),
592 repository: "project".to_string(),
593 release_only: true,
594 version_type: None,
595 };
596
597 let result = expand_template(template);
598 assert_eq!(
599 result.source,
600 Some("https://github.com/test/project/releases".to_string())
601 );
602 }
603
604 #[test]
605 fn test_parse_github_url() {
606 let (owner, repo) = parse_github_url("https://github.com/guimard/llng-docker").unwrap();
607 assert_eq!(owner, "guimard");
608 assert_eq!(repo, "llng-docker");
609
610 let (owner, repo) = parse_github_url("torvalds/linux").unwrap();
611 assert_eq!(owner, "torvalds");
612 assert_eq!(repo, "linux");
613 }
614
615 #[test]
616 fn test_pypi_template() {
617 let template = Template::PyPI {
618 package: "bitbox02".to_string(),
619 version_type: None,
620 };
621
622 let result = expand_template(template);
623 assert_eq!(
624 result.source,
625 Some("https://pypi.debian.net/bitbox02/".to_string())
626 );
627 assert_eq!(result.searchmode, Some("plain".to_string()));
628 }
629
630 #[test]
631 fn test_npmregistry_template() {
632 let template = Template::Npmregistry {
633 package: "@lemonldapng/handler".to_string(),
634 version_type: None,
635 };
636
637 let result = expand_template(template);
638 assert_eq!(
639 result.source,
640 Some("https://registry.npmjs.org/@lemonldapng/handler".to_string())
641 );
642 assert_eq!(result.searchmode, Some("plain".to_string()));
643 }
644
645 #[test]
646 fn test_gitlab_template() {
647 let template = Template::GitLab {
648 dist: "https://salsa.debian.org/debian/devscripts".to_string(),
649 release_only: false,
650 version_type: None,
651 };
652
653 let result = expand_template(template);
654 assert_eq!(
655 result.source,
656 Some("https://salsa.debian.org/debian/devscripts".to_string())
657 );
658 assert_eq!(result.mode, Some("gitlab".to_string()));
659 }
660
661 #[test]
662 fn test_metacpan_template() {
663 let template = Template::Metacpan {
664 dist: "MetaCPAN-Client".to_string(),
665 version_type: None,
666 };
667
668 let result = expand_template(template);
669 assert_eq!(
670 result.source,
671 Some("https://cpan.metacpan.org/authors/id/".to_string())
672 );
673 }
674
675 #[test]
676 fn test_detect_github_template() {
677 let template = detect_template(
678 Some("https://github.com/torvalds/linux/tags"),
679 Some(r".*/(?:refs/tags/)?v?@ANY_VERSION@@ARCHIVE_EXT@"),
680 Some("html"),
681 None,
682 );
683
684 assert_eq!(
685 template,
686 Some(Template::GitHub {
687 owner: "torvalds".to_string(),
688 repository: "linux".to_string(),
689 release_only: false,
690 version_type: None,
691 })
692 );
693 }
694
695 #[test]
696 fn test_detect_github_template_releases() {
697 let template = detect_template(
698 Some("https://github.com/test/project/releases"),
699 Some(r".*/(?:refs/tags/)?v?@ANY_VERSION@@ARCHIVE_EXT@"),
700 Some("html"),
701 None,
702 );
703
704 assert_eq!(
705 template,
706 Some(Template::GitHub {
707 owner: "test".to_string(),
708 repository: "project".to_string(),
709 release_only: true,
710 version_type: None,
711 })
712 );
713 }
714
715 #[test]
716 fn test_detect_github_template_with_version_type() {
717 let template = detect_template(
718 Some("https://github.com/foo/bar/tags"),
719 Some(r".*/(?:refs/tags/)?v?@SEMANTIC_VERSION@@ARCHIVE_EXT@"),
720 Some("html"),
721 None,
722 );
723
724 assert_eq!(
725 template,
726 Some(Template::GitHub {
727 owner: "foo".to_string(),
728 repository: "bar".to_string(),
729 release_only: false,
730 version_type: Some("semantic".to_string()),
731 })
732 );
733 }
734
735 #[test]
736 fn test_detect_pypi_template() {
737 let template = detect_template(
738 Some("https://pypi.debian.net/bitbox02/"),
739 Some(r"https://pypi\.debian\.net/bitbox02/[^/]+\.tar\.gz#/.*-@ANY_VERSION@\.tar\.gz"),
740 Some("plain"),
741 None,
742 );
743
744 assert_eq!(
745 template,
746 Some(Template::PyPI {
747 package: "bitbox02".to_string(),
748 version_type: None,
749 })
750 );
751 }
752
753 #[test]
754 fn test_detect_gitlab_template() {
755 let template = detect_template(
756 Some("https://salsa.debian.org/debian/devscripts"),
757 Some(r".*/v?@ANY_VERSION@@ARCHIVE_EXT@"),
758 None,
759 Some("gitlab"),
760 );
761
762 assert_eq!(
763 template,
764 Some(Template::GitLab {
765 dist: "https://salsa.debian.org/debian/devscripts".to_string(),
766 release_only: false,
767 version_type: None,
768 })
769 );
770 }
771
772 #[test]
773 fn test_detect_npmregistry_template() {
774 let template = detect_template(
775 Some("https://registry.npmjs.org/@lemonldapng/handler"),
776 Some(
777 r"https://registry\.npmjs\.org/lemonldapng/handler/-/.*-@ANY_VERSION@@ARCHIVE_EXT@",
778 ),
779 Some("plain"),
780 None,
781 );
782
783 assert_eq!(
784 template,
785 Some(Template::Npmregistry {
786 package: "@lemonldapng/handler".to_string(),
787 version_type: None,
788 })
789 );
790 }
791
792 #[test]
793 fn test_detect_metacpan_template() {
794 let template = detect_template(
795 Some("https://cpan.metacpan.org/authors/id/"),
796 Some(r".*/MetaCPAN-Client@ANY_VERSION@@ARCHIVE_EXT@"),
797 Some("plain"),
798 None,
799 );
800
801 assert_eq!(
802 template,
803 Some(Template::Metacpan {
804 dist: "MetaCPAN-Client".to_string(),
805 version_type: None,
806 })
807 );
808 }
809
810 #[test]
811 fn test_detect_no_template() {
812 let template = detect_template(
814 Some("https://example.com/downloads/"),
815 Some(r".*/v?(\d+\.\d+)\.tar\.gz"),
816 Some("html"),
817 None,
818 );
819
820 assert_eq!(template, None);
821 }
822
823 #[test]
824 fn test_roundtrip_github_template() {
825 let original = Template::GitHub {
827 owner: "torvalds".to_string(),
828 repository: "linux".to_string(),
829 release_only: false,
830 version_type: None,
831 };
832 let expanded = expand_template(original.clone());
833
834 let detected = detect_template(
836 expanded.source.as_deref(),
837 expanded.matching_pattern.as_deref(),
838 expanded.searchmode.as_deref(),
839 expanded.mode.as_deref(),
840 );
841
842 assert_eq!(detected, Some(original));
843 }
844
845 #[test]
846 fn test_extract_version_type() {
847 assert_eq!(extract_version_type("@ANY_VERSION@"), None);
848 assert_eq!(
849 extract_version_type("@SEMANTIC_VERSION@"),
850 Some("semantic".to_string())
851 );
852 assert_eq!(
853 extract_version_type("@STABLE_VERSION@"),
854 Some("stable".to_string())
855 );
856 assert_eq!(extract_version_type("no-template-here"), None);
857 }
858
859 #[test]
860 fn test_detect_github_wrong_searchmode() {
861 let template = detect_template(
863 Some("https://github.com/torvalds/linux/tags"),
864 Some(r".*/(?:refs/tags/)?v?@ANY_VERSION@@ARCHIVE_EXT@"),
865 Some("plain"), None,
867 );
868
869 assert_eq!(template, None);
870 }
871
872 #[test]
873 fn test_detect_github_invalid_url() {
874 let template = detect_template(
876 Some("https://github.com/torvalds/linux"),
877 Some(r".*/(?:refs/tags/)?v?@ANY_VERSION@@ARCHIVE_EXT@"),
878 Some("html"),
879 None,
880 );
881
882 assert_eq!(template, None);
883 }
884
885 #[test]
886 fn test_detect_github_wrong_host() {
887 let template = detect_template(
889 Some("https://gitlab.com/foo/bar/tags"),
890 Some(r".*/(?:refs/tags/)?v?@ANY_VERSION@@ARCHIVE_EXT@"),
891 Some("html"),
892 None,
893 );
894
895 assert_eq!(template, None);
896 }
897
898 #[test]
899 fn test_detect_gitlab_without_mode() {
900 let template = detect_template(
902 Some("https://salsa.debian.org/debian/devscripts"),
903 Some(r".*/v?@ANY_VERSION@@ARCHIVE_EXT@"),
904 None,
905 None, );
907
908 assert_eq!(template, None);
909 }
910
911 #[test]
912 fn test_detect_pypi_source_with_inline_pattern() {
913 let template = detect_template(
916 Some("https://pypi.debian.net/dulwich/dulwich-(.*).tar.gz"),
917 None,
918 None,
919 None,
920 );
921
922 assert_eq!(
923 template,
924 Some(Template::PyPI {
925 package: "dulwich".to_string(),
926 version_type: None,
927 })
928 );
929 }
930
931 #[test]
932 fn test_detect_pypi_wrong_searchmode() {
933 let template = detect_template(
934 Some("https://pypi.debian.net/bitbox02/"),
935 Some(r"https://pypi\.debian\.net/bitbox02/[^/]+\.tar\.gz#/.*-@ANY_VERSION@\.tar\.gz"),
936 Some("html"), None,
938 );
939
940 assert_eq!(template, None);
941 }
942
943 #[test]
944 fn test_detect_pypi_wrong_url() {
945 let template = detect_template(
946 Some("https://pypi.org/bitbox02/"), Some(r"https://pypi\.debian\.net/bitbox02/[^/]+\.tar\.gz#/.*-@ANY_VERSION@\.tar\.gz"),
948 Some("plain"),
949 None,
950 );
951
952 assert_eq!(template, None);
953 }
954
955 #[test]
956 fn test_detect_npmregistry_wrong_url() {
957 let template = detect_template(
958 Some("https://npm.example.com/@lemonldapng/handler"), Some(
960 r"https://registry\.npmjs\.org/lemonldapng/handler/-/.*-@ANY_VERSION@@ARCHIVE_EXT@",
961 ),
962 Some("plain"),
963 None,
964 );
965
966 assert_eq!(template, None);
967 }
968
969 #[test]
970 fn test_detect_metacpan_wrong_source() {
971 let template = detect_template(
972 Some("https://cpan.example.com/authors/id/"), Some(r".*/MetaCPAN-Client@ANY_VERSION@@ARCHIVE_EXT@"),
974 Some("plain"),
975 None,
976 );
977
978 assert_eq!(template, None);
979 }
980
981 #[test]
982 fn test_detect_metacpan_missing_pattern() {
983 let template = detect_template(
984 Some("https://cpan.metacpan.org/authors/id/"),
985 None, Some("plain"),
987 None,
988 );
989
990 assert_eq!(template, None);
991 }
992
993 #[test]
994 fn test_detect_metacpan_release_url() {
995 let template = detect_template(
996 Some("https://metacpan.org/release/Time-ParseDate"),
997 Some(r".*/Time-ParseDate-v?@ANY_VERSION@@ARCHIVE_EXT@$"),
998 None,
999 None,
1000 );
1001
1002 assert_eq!(
1003 template,
1004 Some(Template::Metacpan {
1005 dist: "Time-ParseDate".to_string(),
1006 version_type: None,
1007 })
1008 );
1009 }
1010
1011 #[test]
1012 fn test_detect_metacpan_dist_url() {
1013 let template = detect_template(
1014 Some("https://metacpan.org/dist/Mail-AuthenticationResults"),
1015 Some(r".*/Mail-AuthenticationResults-v?@ANY_VERSION@@ARCHIVE_EXT@$"),
1016 None,
1017 None,
1018 );
1019
1020 assert_eq!(
1021 template,
1022 Some(Template::Metacpan {
1023 dist: "Mail-AuthenticationResults".to_string(),
1024 version_type: None,
1025 })
1026 );
1027 }
1028
1029 #[test]
1030 fn test_detect_metacpan_cpan_url_with_v_prefix() {
1031 let template = detect_template(
1033 Some("https://cpan.metacpan.org/authors/id/"),
1034 Some(r".*/Time-ParseDate-v?@ANY_VERSION@@ARCHIVE_EXT@"),
1035 Some("plain"),
1036 None,
1037 );
1038
1039 assert_eq!(
1040 template,
1041 Some(Template::Metacpan {
1042 dist: "Time-ParseDate".to_string(),
1043 version_type: None,
1044 })
1045 );
1046 }
1047
1048 #[test]
1049 fn test_detect_metacpan_release_url_wrong_domain() {
1050 let template = detect_template(
1051 Some("https://example.org/release/Time-ParseDate"),
1052 Some(r".*/Time-ParseDate-v?@ANY_VERSION@@ARCHIVE_EXT@$"),
1053 None,
1054 None,
1055 );
1056
1057 assert_eq!(template, None);
1058 }
1059
1060 #[test]
1061 fn test_roundtrip_gitlab_template() {
1062 let original = Template::GitLab {
1063 dist: "https://salsa.debian.org/debian/devscripts".to_string(),
1064 release_only: false,
1065 version_type: None,
1066 };
1067 let expanded = expand_template(original.clone());
1068
1069 let detected = detect_template(
1070 expanded.source.as_deref(),
1071 expanded.matching_pattern.as_deref(),
1072 expanded.searchmode.as_deref(),
1073 expanded.mode.as_deref(),
1074 );
1075
1076 assert_eq!(detected, Some(original));
1077 }
1078
1079 #[test]
1080 fn test_roundtrip_pypi_template() {
1081 let original = Template::PyPI {
1082 package: "bitbox02".to_string(),
1083 version_type: None,
1084 };
1085 let expanded = expand_template(original.clone());
1086
1087 let detected = detect_template(
1088 expanded.source.as_deref(),
1089 expanded.matching_pattern.as_deref(),
1090 expanded.searchmode.as_deref(),
1091 expanded.mode.as_deref(),
1092 );
1093
1094 assert_eq!(detected, Some(original));
1095 }
1096
1097 #[test]
1098 fn test_roundtrip_npmregistry_template() {
1099 let original = Template::Npmregistry {
1100 package: "@scope/package".to_string(),
1101 version_type: None,
1102 };
1103 let expanded = expand_template(original.clone());
1104
1105 let detected = detect_template(
1106 expanded.source.as_deref(),
1107 expanded.matching_pattern.as_deref(),
1108 expanded.searchmode.as_deref(),
1109 expanded.mode.as_deref(),
1110 );
1111
1112 assert_eq!(detected, Some(original));
1113 }
1114
1115 #[test]
1116 fn test_roundtrip_metacpan_template() {
1117 let original = Template::Metacpan {
1118 dist: "MetaCPAN-Client".to_string(),
1119 version_type: None,
1120 };
1121 let expanded = expand_template(original.clone());
1122
1123 let detected = detect_template(
1124 expanded.source.as_deref(),
1125 expanded.matching_pattern.as_deref(),
1126 expanded.searchmode.as_deref(),
1127 expanded.mode.as_deref(),
1128 );
1129
1130 assert_eq!(detected, Some(original));
1131 }
1132
1133 #[test]
1134 fn test_roundtrip_github_with_version_type() {
1135 let original = Template::GitHub {
1136 owner: "foo".to_string(),
1137 repository: "bar".to_string(),
1138 release_only: true,
1139 version_type: Some("stable".to_string()),
1140 };
1141 let expanded = expand_template(original.clone());
1142
1143 let detected = detect_template(
1144 expanded.source.as_deref(),
1145 expanded.matching_pattern.as_deref(),
1146 expanded.searchmode.as_deref(),
1147 expanded.mode.as_deref(),
1148 );
1149
1150 assert_eq!(detected, Some(original));
1151 }
1152
1153 #[test]
1154 fn test_detect_with_none_source() {
1155 let template = detect_template(
1157 None,
1158 Some(r".*/v?@ANY_VERSION@@ARCHIVE_EXT@"),
1159 Some("html"),
1160 None,
1161 );
1162
1163 assert_eq!(template, None);
1164 }
1165
1166 #[test]
1167 fn test_detect_github_partial_match() {
1168 let template = detect_template(
1170 Some("https://github.com/torvalds/linux/tags"),
1171 Some(r".*/v?(\d+\.\d+)\.tar\.gz"), Some("html"),
1173 None,
1174 );
1175
1176 assert_eq!(
1178 template,
1179 Some(Template::GitHub {
1180 owner: "torvalds".to_string(),
1181 repository: "linux".to_string(),
1182 release_only: false,
1183 version_type: None,
1184 })
1185 );
1186 }
1187
1188 #[test]
1189 fn test_extract_version_type_edge_cases() {
1190 assert_eq!(extract_version_type("@FOO@@BAR@"), None);
1192
1193 assert_eq!(extract_version_type("@INCOMPLETE"), None);
1195
1196 assert_eq!(extract_version_type("@SOMETHING@"), None);
1198
1199 assert_eq!(extract_version_type("@@"), None);
1201 }
1202}