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 package = source
401 .strip_prefix("https://pypi.debian.net/")?
402 .trim_end_matches('/');
403
404 let version_type = if let Some(pattern) = matching_pattern {
406 extract_version_type(pattern)
407 } else {
408 None
409 };
410
411 Some(Template::PyPI {
412 package: package.to_string(),
413 version_type,
414 })
415}
416
417fn detect_npmregistry_template(
419 source: &str,
420 matching_pattern: Option<&str>,
421 searchmode: Option<&str>,
422) -> Option<Template> {
423 if searchmode != Some("plain") && searchmode.is_some() {
425 return None;
426 }
427
428 if !source.starts_with("https://registry.npmjs.org/") {
430 return None;
431 }
432
433 let package = source.strip_prefix("https://registry.npmjs.org/")?;
434
435 let version_type = if let Some(pattern) = matching_pattern {
437 extract_version_type(pattern)
438 } else {
439 None
440 };
441
442 Some(Template::Npmregistry {
443 package: package.to_string(),
444 version_type,
445 })
446}
447
448fn detect_metacpan_template(
450 source: &str,
451 matching_pattern: Option<&str>,
452 searchmode: Option<&str>,
453) -> Option<Template> {
454 if searchmode != Some("plain") && searchmode.is_some() {
456 return None;
457 }
458
459 if source == "https://cpan.metacpan.org/authors/id/" {
460 let pattern = matching_pattern?;
462
463 if !pattern.starts_with(".*/") {
466 return None;
467 }
468
469 let after_prefix = pattern.strip_prefix(".*/").unwrap();
470
471 let version_type = extract_version_type(pattern);
473
474 let dist = if let Some(idx) = after_prefix.find('@') {
477 after_prefix[..idx].trim_end_matches("-v?").trim_end_matches('-')
478 } else {
479 return None;
480 };
481
482 Some(Template::Metacpan {
483 dist: dist.to_string(),
484 version_type,
485 })
486 } else if let Some(dist) = source
487 .strip_prefix("https://metacpan.org/release/")
488 .or_else(|| source.strip_prefix("https://metacpan.org/dist/"))
489 {
490 let dist = dist.trim_end_matches('/');
491 if dist.is_empty() {
492 return None;
493 }
494
495 let version_type = matching_pattern.and_then(extract_version_type);
496
497 Some(Template::Metacpan {
498 dist: dist.to_string(),
499 version_type,
500 })
501 } else {
502 None
503 }
504}
505
506fn extract_version_type(pattern: &str) -> Option<String> {
509 if pattern.contains("@ANY_VERSION@") {
511 None
512 } else if let Some(start) = pattern.find('@') {
513 if let Some(end) = pattern[start + 1..].find('@') {
514 let version_str = &pattern[start + 1..start + 1 + end];
515 if version_str.ends_with("_VERSION") {
516 let type_str = version_str.strip_suffix("_VERSION")?;
517 Some(type_str.to_lowercase())
518 } else {
519 None
520 }
521 } else {
522 None
523 }
524 } else {
525 None
526 }
527}
528
529pub fn parse_github_url(url: &str) -> Result<(String, String), TemplateError> {
531 let url = url.trim_end_matches('/');
532
533 if let Ok(parsed) = url::Url::parse(url) {
535 if parsed.host_str() == Some("github.com") {
536 let path = parsed.path().trim_start_matches('/').trim_end_matches('/');
537 let parts: Vec<&str> = path.split('/').collect();
538 if parts.len() >= 2 {
539 return Ok((parts[0].to_string(), parts[1].to_string()));
540 }
541 }
542 }
543
544 let parts: Vec<&str> = url.split('/').collect();
546 if parts.len() == 2 {
547 return Ok((parts[0].to_string(), parts[1].to_string()));
548 }
549
550 Err(TemplateError::InvalidValue {
551 field: "Dist".to_string(),
552 reason: format!("Could not parse GitHub URL: {}", url),
553 })
554}
555
556#[cfg(test)]
557mod tests {
558 use super::*;
559
560 #[test]
561 fn test_github_template_with_owner_repository() {
562 let template = Template::GitHub {
563 owner: "torvalds".to_string(),
564 repository: "linux".to_string(),
565 release_only: false,
566 version_type: None,
567 };
568
569 let result = expand_template(template);
570 assert_eq!(
571 result.source,
572 Some("https://github.com/torvalds/linux/tags".to_string())
573 );
574 assert_eq!(
575 result.matching_pattern,
576 Some(r".*/(?:refs/tags/)?v?@ANY_VERSION@@ARCHIVE_EXT@".to_string())
577 );
578 }
579
580 #[test]
581 fn test_github_template_release_only() {
582 let template = Template::GitHub {
583 owner: "test".to_string(),
584 repository: "project".to_string(),
585 release_only: true,
586 version_type: None,
587 };
588
589 let result = expand_template(template);
590 assert_eq!(
591 result.source,
592 Some("https://github.com/test/project/releases".to_string())
593 );
594 }
595
596 #[test]
597 fn test_parse_github_url() {
598 let (owner, repo) = parse_github_url("https://github.com/guimard/llng-docker").unwrap();
599 assert_eq!(owner, "guimard");
600 assert_eq!(repo, "llng-docker");
601
602 let (owner, repo) = parse_github_url("torvalds/linux").unwrap();
603 assert_eq!(owner, "torvalds");
604 assert_eq!(repo, "linux");
605 }
606
607 #[test]
608 fn test_pypi_template() {
609 let template = Template::PyPI {
610 package: "bitbox02".to_string(),
611 version_type: None,
612 };
613
614 let result = expand_template(template);
615 assert_eq!(
616 result.source,
617 Some("https://pypi.debian.net/bitbox02/".to_string())
618 );
619 assert_eq!(result.searchmode, Some("plain".to_string()));
620 }
621
622 #[test]
623 fn test_npmregistry_template() {
624 let template = Template::Npmregistry {
625 package: "@lemonldapng/handler".to_string(),
626 version_type: None,
627 };
628
629 let result = expand_template(template);
630 assert_eq!(
631 result.source,
632 Some("https://registry.npmjs.org/@lemonldapng/handler".to_string())
633 );
634 assert_eq!(result.searchmode, Some("plain".to_string()));
635 }
636
637 #[test]
638 fn test_gitlab_template() {
639 let template = Template::GitLab {
640 dist: "https://salsa.debian.org/debian/devscripts".to_string(),
641 release_only: false,
642 version_type: None,
643 };
644
645 let result = expand_template(template);
646 assert_eq!(
647 result.source,
648 Some("https://salsa.debian.org/debian/devscripts".to_string())
649 );
650 assert_eq!(result.mode, Some("gitlab".to_string()));
651 }
652
653 #[test]
654 fn test_metacpan_template() {
655 let template = Template::Metacpan {
656 dist: "MetaCPAN-Client".to_string(),
657 version_type: None,
658 };
659
660 let result = expand_template(template);
661 assert_eq!(
662 result.source,
663 Some("https://cpan.metacpan.org/authors/id/".to_string())
664 );
665 }
666
667 #[test]
668 fn test_detect_github_template() {
669 let template = detect_template(
670 Some("https://github.com/torvalds/linux/tags"),
671 Some(r".*/(?:refs/tags/)?v?@ANY_VERSION@@ARCHIVE_EXT@"),
672 Some("html"),
673 None,
674 );
675
676 assert_eq!(
677 template,
678 Some(Template::GitHub {
679 owner: "torvalds".to_string(),
680 repository: "linux".to_string(),
681 release_only: false,
682 version_type: None,
683 })
684 );
685 }
686
687 #[test]
688 fn test_detect_github_template_releases() {
689 let template = detect_template(
690 Some("https://github.com/test/project/releases"),
691 Some(r".*/(?:refs/tags/)?v?@ANY_VERSION@@ARCHIVE_EXT@"),
692 Some("html"),
693 None,
694 );
695
696 assert_eq!(
697 template,
698 Some(Template::GitHub {
699 owner: "test".to_string(),
700 repository: "project".to_string(),
701 release_only: true,
702 version_type: None,
703 })
704 );
705 }
706
707 #[test]
708 fn test_detect_github_template_with_version_type() {
709 let template = detect_template(
710 Some("https://github.com/foo/bar/tags"),
711 Some(r".*/(?:refs/tags/)?v?@SEMANTIC_VERSION@@ARCHIVE_EXT@"),
712 Some("html"),
713 None,
714 );
715
716 assert_eq!(
717 template,
718 Some(Template::GitHub {
719 owner: "foo".to_string(),
720 repository: "bar".to_string(),
721 release_only: false,
722 version_type: Some("semantic".to_string()),
723 })
724 );
725 }
726
727 #[test]
728 fn test_detect_pypi_template() {
729 let template = detect_template(
730 Some("https://pypi.debian.net/bitbox02/"),
731 Some(r"https://pypi\.debian\.net/bitbox02/[^/]+\.tar\.gz#/.*-@ANY_VERSION@\.tar\.gz"),
732 Some("plain"),
733 None,
734 );
735
736 assert_eq!(
737 template,
738 Some(Template::PyPI {
739 package: "bitbox02".to_string(),
740 version_type: None,
741 })
742 );
743 }
744
745 #[test]
746 fn test_detect_gitlab_template() {
747 let template = detect_template(
748 Some("https://salsa.debian.org/debian/devscripts"),
749 Some(r".*/v?@ANY_VERSION@@ARCHIVE_EXT@"),
750 None,
751 Some("gitlab"),
752 );
753
754 assert_eq!(
755 template,
756 Some(Template::GitLab {
757 dist: "https://salsa.debian.org/debian/devscripts".to_string(),
758 release_only: false,
759 version_type: None,
760 })
761 );
762 }
763
764 #[test]
765 fn test_detect_npmregistry_template() {
766 let template = detect_template(
767 Some("https://registry.npmjs.org/@lemonldapng/handler"),
768 Some(
769 r"https://registry\.npmjs\.org/lemonldapng/handler/-/.*-@ANY_VERSION@@ARCHIVE_EXT@",
770 ),
771 Some("plain"),
772 None,
773 );
774
775 assert_eq!(
776 template,
777 Some(Template::Npmregistry {
778 package: "@lemonldapng/handler".to_string(),
779 version_type: None,
780 })
781 );
782 }
783
784 #[test]
785 fn test_detect_metacpan_template() {
786 let template = detect_template(
787 Some("https://cpan.metacpan.org/authors/id/"),
788 Some(r".*/MetaCPAN-Client@ANY_VERSION@@ARCHIVE_EXT@"),
789 Some("plain"),
790 None,
791 );
792
793 assert_eq!(
794 template,
795 Some(Template::Metacpan {
796 dist: "MetaCPAN-Client".to_string(),
797 version_type: None,
798 })
799 );
800 }
801
802 #[test]
803 fn test_detect_no_template() {
804 let template = detect_template(
806 Some("https://example.com/downloads/"),
807 Some(r".*/v?(\d+\.\d+)\.tar\.gz"),
808 Some("html"),
809 None,
810 );
811
812 assert_eq!(template, None);
813 }
814
815 #[test]
816 fn test_roundtrip_github_template() {
817 let original = Template::GitHub {
819 owner: "torvalds".to_string(),
820 repository: "linux".to_string(),
821 release_only: false,
822 version_type: None,
823 };
824 let expanded = expand_template(original.clone());
825
826 let detected = detect_template(
828 expanded.source.as_deref(),
829 expanded.matching_pattern.as_deref(),
830 expanded.searchmode.as_deref(),
831 expanded.mode.as_deref(),
832 );
833
834 assert_eq!(detected, Some(original));
835 }
836
837 #[test]
838 fn test_extract_version_type() {
839 assert_eq!(extract_version_type("@ANY_VERSION@"), None);
840 assert_eq!(
841 extract_version_type("@SEMANTIC_VERSION@"),
842 Some("semantic".to_string())
843 );
844 assert_eq!(
845 extract_version_type("@STABLE_VERSION@"),
846 Some("stable".to_string())
847 );
848 assert_eq!(extract_version_type("no-template-here"), None);
849 }
850
851 #[test]
852 fn test_detect_github_wrong_searchmode() {
853 let template = detect_template(
855 Some("https://github.com/torvalds/linux/tags"),
856 Some(r".*/(?:refs/tags/)?v?@ANY_VERSION@@ARCHIVE_EXT@"),
857 Some("plain"), None,
859 );
860
861 assert_eq!(template, None);
862 }
863
864 #[test]
865 fn test_detect_github_invalid_url() {
866 let template = detect_template(
868 Some("https://github.com/torvalds/linux"),
869 Some(r".*/(?:refs/tags/)?v?@ANY_VERSION@@ARCHIVE_EXT@"),
870 Some("html"),
871 None,
872 );
873
874 assert_eq!(template, None);
875 }
876
877 #[test]
878 fn test_detect_github_wrong_host() {
879 let template = detect_template(
881 Some("https://gitlab.com/foo/bar/tags"),
882 Some(r".*/(?:refs/tags/)?v?@ANY_VERSION@@ARCHIVE_EXT@"),
883 Some("html"),
884 None,
885 );
886
887 assert_eq!(template, None);
888 }
889
890 #[test]
891 fn test_detect_gitlab_without_mode() {
892 let template = detect_template(
894 Some("https://salsa.debian.org/debian/devscripts"),
895 Some(r".*/v?@ANY_VERSION@@ARCHIVE_EXT@"),
896 None,
897 None, );
899
900 assert_eq!(template, None);
901 }
902
903 #[test]
904 fn test_detect_pypi_wrong_searchmode() {
905 let template = detect_template(
906 Some("https://pypi.debian.net/bitbox02/"),
907 Some(r"https://pypi\.debian\.net/bitbox02/[^/]+\.tar\.gz#/.*-@ANY_VERSION@\.tar\.gz"),
908 Some("html"), None,
910 );
911
912 assert_eq!(template, None);
913 }
914
915 #[test]
916 fn test_detect_pypi_wrong_url() {
917 let template = detect_template(
918 Some("https://pypi.org/bitbox02/"), Some(r"https://pypi\.debian\.net/bitbox02/[^/]+\.tar\.gz#/.*-@ANY_VERSION@\.tar\.gz"),
920 Some("plain"),
921 None,
922 );
923
924 assert_eq!(template, None);
925 }
926
927 #[test]
928 fn test_detect_npmregistry_wrong_url() {
929 let template = detect_template(
930 Some("https://npm.example.com/@lemonldapng/handler"), Some(
932 r"https://registry\.npmjs\.org/lemonldapng/handler/-/.*-@ANY_VERSION@@ARCHIVE_EXT@",
933 ),
934 Some("plain"),
935 None,
936 );
937
938 assert_eq!(template, None);
939 }
940
941 #[test]
942 fn test_detect_metacpan_wrong_source() {
943 let template = detect_template(
944 Some("https://cpan.example.com/authors/id/"), Some(r".*/MetaCPAN-Client@ANY_VERSION@@ARCHIVE_EXT@"),
946 Some("plain"),
947 None,
948 );
949
950 assert_eq!(template, None);
951 }
952
953 #[test]
954 fn test_detect_metacpan_missing_pattern() {
955 let template = detect_template(
956 Some("https://cpan.metacpan.org/authors/id/"),
957 None, Some("plain"),
959 None,
960 );
961
962 assert_eq!(template, None);
963 }
964
965 #[test]
966 fn test_detect_metacpan_release_url() {
967 let template = detect_template(
968 Some("https://metacpan.org/release/Time-ParseDate"),
969 Some(r".*/Time-ParseDate-v?@ANY_VERSION@@ARCHIVE_EXT@$"),
970 None,
971 None,
972 );
973
974 assert_eq!(
975 template,
976 Some(Template::Metacpan {
977 dist: "Time-ParseDate".to_string(),
978 version_type: None,
979 })
980 );
981 }
982
983 #[test]
984 fn test_detect_metacpan_dist_url() {
985 let template = detect_template(
986 Some("https://metacpan.org/dist/Mail-AuthenticationResults"),
987 Some(r".*/Mail-AuthenticationResults-v?@ANY_VERSION@@ARCHIVE_EXT@$"),
988 None,
989 None,
990 );
991
992 assert_eq!(
993 template,
994 Some(Template::Metacpan {
995 dist: "Mail-AuthenticationResults".to_string(),
996 version_type: None,
997 })
998 );
999 }
1000
1001 #[test]
1002 fn test_detect_metacpan_cpan_url_with_v_prefix() {
1003 let template = detect_template(
1005 Some("https://cpan.metacpan.org/authors/id/"),
1006 Some(r".*/Time-ParseDate-v?@ANY_VERSION@@ARCHIVE_EXT@"),
1007 Some("plain"),
1008 None,
1009 );
1010
1011 assert_eq!(
1012 template,
1013 Some(Template::Metacpan {
1014 dist: "Time-ParseDate".to_string(),
1015 version_type: None,
1016 })
1017 );
1018 }
1019
1020 #[test]
1021 fn test_detect_metacpan_release_url_wrong_domain() {
1022 let template = detect_template(
1023 Some("https://example.org/release/Time-ParseDate"),
1024 Some(r".*/Time-ParseDate-v?@ANY_VERSION@@ARCHIVE_EXT@$"),
1025 None,
1026 None,
1027 );
1028
1029 assert_eq!(template, None);
1030 }
1031
1032 #[test]
1033 fn test_roundtrip_gitlab_template() {
1034 let original = Template::GitLab {
1035 dist: "https://salsa.debian.org/debian/devscripts".to_string(),
1036 release_only: false,
1037 version_type: None,
1038 };
1039 let expanded = expand_template(original.clone());
1040
1041 let detected = detect_template(
1042 expanded.source.as_deref(),
1043 expanded.matching_pattern.as_deref(),
1044 expanded.searchmode.as_deref(),
1045 expanded.mode.as_deref(),
1046 );
1047
1048 assert_eq!(detected, Some(original));
1049 }
1050
1051 #[test]
1052 fn test_roundtrip_pypi_template() {
1053 let original = Template::PyPI {
1054 package: "bitbox02".to_string(),
1055 version_type: None,
1056 };
1057 let expanded = expand_template(original.clone());
1058
1059 let detected = detect_template(
1060 expanded.source.as_deref(),
1061 expanded.matching_pattern.as_deref(),
1062 expanded.searchmode.as_deref(),
1063 expanded.mode.as_deref(),
1064 );
1065
1066 assert_eq!(detected, Some(original));
1067 }
1068
1069 #[test]
1070 fn test_roundtrip_npmregistry_template() {
1071 let original = Template::Npmregistry {
1072 package: "@scope/package".to_string(),
1073 version_type: None,
1074 };
1075 let expanded = expand_template(original.clone());
1076
1077 let detected = detect_template(
1078 expanded.source.as_deref(),
1079 expanded.matching_pattern.as_deref(),
1080 expanded.searchmode.as_deref(),
1081 expanded.mode.as_deref(),
1082 );
1083
1084 assert_eq!(detected, Some(original));
1085 }
1086
1087 #[test]
1088 fn test_roundtrip_metacpan_template() {
1089 let original = Template::Metacpan {
1090 dist: "MetaCPAN-Client".to_string(),
1091 version_type: None,
1092 };
1093 let expanded = expand_template(original.clone());
1094
1095 let detected = detect_template(
1096 expanded.source.as_deref(),
1097 expanded.matching_pattern.as_deref(),
1098 expanded.searchmode.as_deref(),
1099 expanded.mode.as_deref(),
1100 );
1101
1102 assert_eq!(detected, Some(original));
1103 }
1104
1105 #[test]
1106 fn test_roundtrip_github_with_version_type() {
1107 let original = Template::GitHub {
1108 owner: "foo".to_string(),
1109 repository: "bar".to_string(),
1110 release_only: true,
1111 version_type: Some("stable".to_string()),
1112 };
1113 let expanded = expand_template(original.clone());
1114
1115 let detected = detect_template(
1116 expanded.source.as_deref(),
1117 expanded.matching_pattern.as_deref(),
1118 expanded.searchmode.as_deref(),
1119 expanded.mode.as_deref(),
1120 );
1121
1122 assert_eq!(detected, Some(original));
1123 }
1124
1125 #[test]
1126 fn test_detect_with_none_source() {
1127 let template = detect_template(
1129 None,
1130 Some(r".*/v?@ANY_VERSION@@ARCHIVE_EXT@"),
1131 Some("html"),
1132 None,
1133 );
1134
1135 assert_eq!(template, None);
1136 }
1137
1138 #[test]
1139 fn test_detect_github_partial_match() {
1140 let template = detect_template(
1142 Some("https://github.com/torvalds/linux/tags"),
1143 Some(r".*/v?(\d+\.\d+)\.tar\.gz"), Some("html"),
1145 None,
1146 );
1147
1148 assert_eq!(
1150 template,
1151 Some(Template::GitHub {
1152 owner: "torvalds".to_string(),
1153 repository: "linux".to_string(),
1154 release_only: false,
1155 version_type: None,
1156 })
1157 );
1158 }
1159
1160 #[test]
1161 fn test_extract_version_type_edge_cases() {
1162 assert_eq!(extract_version_type("@FOO@@BAR@"), None);
1164
1165 assert_eq!(extract_version_type("@INCOMPLETE"), None);
1167
1168 assert_eq!(extract_version_type("@SOMETHING@"), None);
1170
1171 assert_eq!(extract_version_type("@@"), None);
1173 }
1174}