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 Cran {
98 package: String,
100 version_type: Option<String>,
102 },
103 Bioconductor {
105 package: String,
107 version_type: Option<String>,
109 },
110}
111
112#[derive(Debug, Clone, Default)]
114pub struct ExpandedTemplate {
115 pub source: Option<String>,
117 pub matching_pattern: Option<String>,
119 pub searchmode: Option<String>,
121 pub mode: Option<String>,
123 pub pgpmode: Option<String>,
125 pub downloadurlmangle: Option<String>,
127}
128
129pub fn expand_template(template: Template) -> ExpandedTemplate {
131 match template {
132 Template::GitHub {
133 owner,
134 repository,
135 release_only,
136 version_type,
137 } => expand_github_template(owner, repository, release_only, version_type),
138 Template::GitLab {
139 dist,
140 release_only,
141 version_type,
142 } => expand_gitlab_template(dist, release_only, version_type),
143 Template::PyPI {
144 package,
145 version_type,
146 } => expand_pypi_template(package, version_type),
147 Template::Npmregistry {
148 package,
149 version_type,
150 } => expand_npmregistry_template(package, version_type),
151 Template::Metacpan { dist, version_type } => expand_metacpan_template(dist, version_type),
152 Template::Cran {
153 package,
154 version_type,
155 } => expand_cran_template(package, version_type),
156 Template::Bioconductor {
157 package,
158 version_type,
159 } => expand_bioconductor_template(package, version_type),
160 }
161}
162
163fn expand_github_template(
165 owner: String,
166 repository: String,
167 release_only: bool,
168 version_type: Option<String>,
169) -> ExpandedTemplate {
170 let version_pattern = version_type
171 .as_deref()
172 .map(|v| format!("@{}_VERSION@", v.to_uppercase()))
173 .unwrap_or_else(|| "@ANY_VERSION@".to_string());
174
175 let source = if release_only {
176 format!("https://github.com/{}/{}/releases", owner, repository)
177 } else {
178 format!("https://github.com/{}/{}/tags", owner, repository)
179 };
180
181 let matching_pattern = format!(
182 r".*/(?:refs/tags/)?v?{}{}",
183 version_pattern, "@ARCHIVE_EXT@"
184 );
185
186 ExpandedTemplate {
187 source: Some(source),
188 matching_pattern: Some(matching_pattern),
189 searchmode: Some("html".to_string()),
190 ..Default::default()
191 }
192}
193
194fn expand_gitlab_template(
196 dist: String,
197 _release_only: bool,
198 version_type: Option<String>,
199) -> ExpandedTemplate {
200 let version_pattern = version_type
201 .as_deref()
202 .map(|v| format!("@{}_VERSION@", v.to_uppercase()))
203 .unwrap_or_else(|| "@ANY_VERSION@".to_string());
204
205 ExpandedTemplate {
207 source: Some(dist),
208 matching_pattern: Some(format!(r".*/v?{}{}", version_pattern, "@ARCHIVE_EXT@")),
209 mode: Some("gitlab".to_string()),
210 ..Default::default()
211 }
212}
213
214fn expand_pypi_template(package: String, version_type: Option<String>) -> ExpandedTemplate {
216 let version_pattern = version_type
217 .as_deref()
218 .map(|v| format!("@{}_VERSION@", v.to_uppercase()))
219 .unwrap_or_else(|| "@ANY_VERSION@".to_string());
220
221 ExpandedTemplate {
222 source: Some(format!("https://pypi.debian.net/{}/", package)),
223 matching_pattern: Some(format!(
224 r"https://pypi\.debian\.net/{}/[^/]+\.tar\.gz#/.*-{}\.tar\.gz",
225 package, version_pattern
226 )),
227 searchmode: Some("plain".to_string()),
228 ..Default::default()
229 }
230}
231
232fn expand_npmregistry_template(package: 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 package_name = package.trim_start_matches('@');
241
242 ExpandedTemplate {
243 source: Some(format!("https://registry.npmjs.org/{}", package)),
244 matching_pattern: Some(format!(
245 r"https://registry\.npmjs\.org/{}/-/.*-{}@ARCHIVE_EXT@",
246 package_name.replace('/', r"\/"),
247 version_pattern
248 )),
249 searchmode: Some("plain".to_string()),
250 ..Default::default()
251 }
252}
253
254fn expand_metacpan_template(dist: String, version_type: Option<String>) -> ExpandedTemplate {
256 let version_pattern = version_type
257 .as_deref()
258 .map(|v| format!("@{}_VERSION@", v.to_uppercase()))
259 .unwrap_or_else(|| "@ANY_VERSION@".to_string());
260
261 let dist_name = dist.replace("::", "-");
263
264 ExpandedTemplate {
265 source: Some("https://cpan.metacpan.org/authors/id/".to_string()),
266 matching_pattern: Some(format!(r".*/{}{}@ARCHIVE_EXT@", dist_name, version_pattern)),
267 searchmode: Some("plain".to_string()),
268 ..Default::default()
269 }
270}
271
272fn expand_cran_template(package: String, version_type: Option<String>) -> ExpandedTemplate {
274 let version_pattern = version_type
275 .as_deref()
276 .map(|v| format!("@{}_VERSION@", v.to_uppercase()))
277 .unwrap_or_else(|| "@ANY_VERSION@".to_string());
278
279 ExpandedTemplate {
280 source: Some(format!("https://cran.r-project.org/package={}", package)),
281 matching_pattern: Some(format!(".*_{}.tar.gz", version_pattern)),
282 downloadurlmangle: Some(
283 "s%.*/src/contrib/%https://cran.r-project.org/src/contrib/%".to_string(),
284 ),
285 ..Default::default()
286 }
287}
288
289fn expand_bioconductor_template(package: String, version_type: Option<String>) -> ExpandedTemplate {
291 let version_pattern = version_type
292 .as_deref()
293 .map(|v| format!("@{}_VERSION@", v.to_uppercase()))
294 .unwrap_or_else(|| "@ANY_VERSION@".to_string());
295
296 ExpandedTemplate {
297 source: Some(format!("https://bioconductor.org/packages/{}", package)),
298 matching_pattern: Some(format!(".*_{}.tar.gz", version_pattern)),
299 downloadurlmangle: Some(
300 "s%.*/src/contrib/%https://bioconductor.org/packages/release/bioc/src/contrib/%"
301 .to_string(),
302 ),
303 ..Default::default()
304 }
305}
306
307pub fn detect_template(
325 source: Option<&str>,
326 matching_pattern: Option<&str>,
327 searchmode: Option<&str>,
328 mode: Option<&str>,
329) -> Option<Template> {
330 let source = source?;
331
332 if let Some(template) = detect_github_template(source, matching_pattern, searchmode) {
334 return Some(template);
335 }
336
337 if let Some(template) = detect_gitlab_template(source, matching_pattern, mode) {
339 return Some(template);
340 }
341
342 if let Some(template) = detect_pypi_template(source, matching_pattern, searchmode) {
344 return Some(template);
345 }
346
347 if let Some(template) = detect_npmregistry_template(source, matching_pattern, searchmode) {
349 return Some(template);
350 }
351
352 if let Some(template) = detect_metacpan_template(source, matching_pattern, searchmode) {
354 return Some(template);
355 }
356
357 if let Some(template) = detect_cran_template(source, matching_pattern) {
359 return Some(template);
360 }
361
362 if let Some(template) = detect_cran_from_source_url(source, matching_pattern) {
365 return Some(template);
366 }
367
368 if matching_pattern.is_none() {
371 if let Some(template) = detect_cran_from_inline_url(source) {
372 return Some(template);
373 }
374 if let Some(template) = detect_bioconductor_from_inline_url(source) {
375 return Some(template);
376 }
377 }
378
379 if let Some(template) = detect_bioconductor_template(source, matching_pattern) {
381 return Some(template);
382 }
383
384 None
385}
386
387fn detect_github_template(
389 source: &str,
390 matching_pattern: Option<&str>,
391 searchmode: Option<&str>,
392) -> Option<Template> {
393 if searchmode != Some("html") && searchmode.is_some() {
395 return None;
396 }
397
398 let release_only = if source.ends_with("/releases") {
400 true
401 } else if source.ends_with("/tags") {
402 false
403 } else {
404 return None;
405 };
406
407 let url_without_suffix = if release_only {
409 source.strip_suffix("/releases")?
410 } else {
411 source.strip_suffix("/tags")?
412 };
413
414 let (owner, repository) = if let Ok(parsed) = url::Url::parse(url_without_suffix) {
415 if parsed.host_str() != Some("github.com") {
416 return None;
417 }
418 let path = parsed.path().trim_start_matches('/').trim_end_matches('/');
419 let parts: Vec<&str> = path.split('/').collect();
420 if parts.len() != 2 {
421 return None;
422 }
423 (parts[0].to_string(), parts[1].to_string())
424 } else {
425 return None;
426 };
427
428 let version_type = if let Some(pattern) = matching_pattern {
430 extract_version_type(pattern)
431 } else {
432 None
433 };
434
435 Some(Template::GitHub {
436 owner,
437 repository,
438 release_only,
439 version_type,
440 })
441}
442
443fn detect_gitlab_template(
445 source: &str,
446 matching_pattern: Option<&str>,
447 mode: Option<&str>,
448) -> Option<Template> {
449 if mode != Some("gitlab") {
451 return None;
452 }
453
454 let version_type = if let Some(pattern) = matching_pattern {
456 extract_version_type(pattern)
457 } else {
458 None
459 };
460
461 Some(Template::GitLab {
462 dist: source.to_string(),
463 release_only: false, version_type,
465 })
466}
467
468fn detect_pypi_template(
470 source: &str,
471 matching_pattern: Option<&str>,
472 searchmode: Option<&str>,
473) -> Option<Template> {
474 if searchmode != Some("plain") && searchmode.is_some() {
476 return None;
477 }
478
479 if !source.starts_with("https://pypi.debian.net/") {
481 return None;
482 }
483
484 let remainder = source
485 .strip_prefix("https://pypi.debian.net/")?
486 .trim_end_matches('/');
487
488 let package = match remainder.split_once('/') {
490 Some((pkg, _)) => pkg,
491 None => remainder,
492 };
493
494 let version_type = if let Some(pattern) = matching_pattern {
496 extract_version_type(pattern)
497 } else {
498 None
499 };
500
501 Some(Template::PyPI {
502 package: package.to_string(),
503 version_type,
504 })
505}
506
507fn detect_npmregistry_template(
509 source: &str,
510 matching_pattern: Option<&str>,
511 searchmode: Option<&str>,
512) -> Option<Template> {
513 if searchmode != Some("plain") && searchmode.is_some() {
515 return None;
516 }
517
518 if !source.starts_with("https://registry.npmjs.org/") {
520 return None;
521 }
522
523 let package = source.strip_prefix("https://registry.npmjs.org/")?;
524
525 let version_type = if let Some(pattern) = matching_pattern {
527 extract_version_type(pattern)
528 } else {
529 None
530 };
531
532 Some(Template::Npmregistry {
533 package: package.to_string(),
534 version_type,
535 })
536}
537
538fn detect_metacpan_template(
540 source: &str,
541 matching_pattern: Option<&str>,
542 searchmode: Option<&str>,
543) -> Option<Template> {
544 if searchmode != Some("plain") && searchmode.is_some() {
546 return None;
547 }
548
549 if source == "https://cpan.metacpan.org/authors/id/" {
550 let pattern = matching_pattern?;
552
553 if !pattern.starts_with(".*/") {
556 return None;
557 }
558
559 let after_prefix = pattern.strip_prefix(".*/").unwrap();
560
561 let version_type = extract_version_type(pattern);
563
564 let dist = if let Some(idx) = after_prefix.find('@') {
567 after_prefix[..idx]
568 .trim_end_matches("-v?")
569 .trim_end_matches('-')
570 } else {
571 return None;
572 };
573
574 Some(Template::Metacpan {
575 dist: dist.to_string(),
576 version_type,
577 })
578 } else if let Some(dist) = source
579 .strip_prefix("https://metacpan.org/release/")
580 .or_else(|| source.strip_prefix("https://metacpan.org/dist/"))
581 {
582 let dist = dist.trim_end_matches('/');
583 if dist.is_empty() {
584 return None;
585 }
586
587 let version_type = matching_pattern.and_then(extract_version_type);
588
589 Some(Template::Metacpan {
590 dist: dist.to_string(),
591 version_type,
592 })
593 } else {
594 None
595 }
596}
597
598fn detect_cran_template(source: &str, matching_pattern: Option<&str>) -> Option<Template> {
600 let package = source.strip_prefix("https://cran.r-project.org/package=")?;
602
603 if package.is_empty() {
604 return None;
605 }
606
607 let version_type = matching_pattern.and_then(extract_version_type);
608
609 Some(Template::Cran {
610 package: package.to_string(),
611 version_type,
612 })
613}
614
615pub fn detect_cran_from_source_url(
621 source: &str,
622 matching_pattern: Option<&str>,
623) -> Option<Template> {
624 if source != "https://cran.r-project.org/src/contrib/"
626 && source != "https://cloud.r-project.org/src/contrib/"
627 {
628 return None;
629 }
630
631 let pattern = matching_pattern?;
633 let package = pattern.split('_').next()?;
634 if package.is_empty() {
635 return None;
636 }
637
638 let version_type = extract_version_type(pattern);
639
640 Some(Template::Cran {
641 package: package.to_string(),
642 version_type,
643 })
644}
645
646fn detect_cran_from_inline_url(source: &str) -> Option<Template> {
652 let remainder = source
653 .strip_prefix("https://cran.r-project.org/src/contrib/")
654 .or_else(|| source.strip_prefix("https://cloud.r-project.org/src/contrib/"))?;
655
656 let package = remainder.split('_').next()?;
658 if package.is_empty() {
659 return None;
660 }
661
662 Some(Template::Cran {
663 package: package.to_string(),
664 version_type: None,
665 })
666}
667
668fn detect_bioconductor_from_inline_url(source: &str) -> Option<Template> {
671 let remainder =
672 source.strip_prefix("https://bioconductor.org/packages/release/bioc/src/contrib/")?;
673
674 let package = remainder.split('_').next()?;
675 if package.is_empty() {
676 return None;
677 }
678
679 Some(Template::Bioconductor {
680 package: package.to_string(),
681 version_type: None,
682 })
683}
684
685fn detect_bioconductor_template(source: &str, matching_pattern: Option<&str>) -> Option<Template> {
687 let package = source.strip_prefix("https://bioconductor.org/packages/")?;
689
690 if package.is_empty() {
691 return None;
692 }
693
694 let version_type = matching_pattern.and_then(extract_version_type);
695
696 Some(Template::Bioconductor {
697 package: package.to_string(),
698 version_type,
699 })
700}
701
702fn extract_version_type(pattern: &str) -> Option<String> {
705 if pattern.contains("@ANY_VERSION@") {
707 None
708 } else if let Some(start) = pattern.find('@') {
709 if let Some(end) = pattern[start + 1..].find('@') {
710 let version_str = &pattern[start + 1..start + 1 + end];
711 if version_str.ends_with("_VERSION") {
712 let type_str = version_str.strip_suffix("_VERSION")?;
713 Some(type_str.to_lowercase())
714 } else {
715 None
716 }
717 } else {
718 None
719 }
720 } else {
721 None
722 }
723}
724
725pub fn parse_github_url(url: &str) -> Result<(String, String), TemplateError> {
727 let url = url.trim_end_matches('/');
728
729 if let Ok(parsed) = url::Url::parse(url) {
731 if parsed.host_str() == Some("github.com") {
732 let path = parsed.path().trim_start_matches('/').trim_end_matches('/');
733 let parts: Vec<&str> = path.split('/').collect();
734 if parts.len() >= 2 {
735 return Ok((parts[0].to_string(), parts[1].to_string()));
736 }
737 }
738 }
739
740 let parts: Vec<&str> = url.split('/').collect();
742 if parts.len() == 2 {
743 return Ok((parts[0].to_string(), parts[1].to_string()));
744 }
745
746 Err(TemplateError::InvalidValue {
747 field: "Dist".to_string(),
748 reason: format!("Could not parse GitHub URL: {}", url),
749 })
750}
751
752#[cfg(test)]
753mod tests {
754 use super::*;
755
756 #[test]
757 fn test_github_template_with_owner_repository() {
758 let template = Template::GitHub {
759 owner: "torvalds".to_string(),
760 repository: "linux".to_string(),
761 release_only: false,
762 version_type: None,
763 };
764
765 let result = expand_template(template);
766 assert_eq!(
767 result.source,
768 Some("https://github.com/torvalds/linux/tags".to_string())
769 );
770 assert_eq!(
771 result.matching_pattern,
772 Some(r".*/(?:refs/tags/)?v?@ANY_VERSION@@ARCHIVE_EXT@".to_string())
773 );
774 }
775
776 #[test]
777 fn test_github_template_release_only() {
778 let template = Template::GitHub {
779 owner: "test".to_string(),
780 repository: "project".to_string(),
781 release_only: true,
782 version_type: None,
783 };
784
785 let result = expand_template(template);
786 assert_eq!(
787 result.source,
788 Some("https://github.com/test/project/releases".to_string())
789 );
790 }
791
792 #[test]
793 fn test_parse_github_url() {
794 let (owner, repo) = parse_github_url("https://github.com/guimard/llng-docker").unwrap();
795 assert_eq!(owner, "guimard");
796 assert_eq!(repo, "llng-docker");
797
798 let (owner, repo) = parse_github_url("torvalds/linux").unwrap();
799 assert_eq!(owner, "torvalds");
800 assert_eq!(repo, "linux");
801 }
802
803 #[test]
804 fn test_pypi_template() {
805 let template = Template::PyPI {
806 package: "bitbox02".to_string(),
807 version_type: None,
808 };
809
810 let result = expand_template(template);
811 assert_eq!(
812 result.source,
813 Some("https://pypi.debian.net/bitbox02/".to_string())
814 );
815 assert_eq!(result.searchmode, Some("plain".to_string()));
816 }
817
818 #[test]
819 fn test_npmregistry_template() {
820 let template = Template::Npmregistry {
821 package: "@lemonldapng/handler".to_string(),
822 version_type: None,
823 };
824
825 let result = expand_template(template);
826 assert_eq!(
827 result.source,
828 Some("https://registry.npmjs.org/@lemonldapng/handler".to_string())
829 );
830 assert_eq!(result.searchmode, Some("plain".to_string()));
831 }
832
833 #[test]
834 fn test_gitlab_template() {
835 let template = Template::GitLab {
836 dist: "https://salsa.debian.org/debian/devscripts".to_string(),
837 release_only: false,
838 version_type: None,
839 };
840
841 let result = expand_template(template);
842 assert_eq!(
843 result.source,
844 Some("https://salsa.debian.org/debian/devscripts".to_string())
845 );
846 assert_eq!(result.mode, Some("gitlab".to_string()));
847 }
848
849 #[test]
850 fn test_metacpan_template() {
851 let template = Template::Metacpan {
852 dist: "MetaCPAN-Client".to_string(),
853 version_type: None,
854 };
855
856 let result = expand_template(template);
857 assert_eq!(
858 result.source,
859 Some("https://cpan.metacpan.org/authors/id/".to_string())
860 );
861 }
862
863 #[test]
864 fn test_detect_github_template() {
865 let template = detect_template(
866 Some("https://github.com/torvalds/linux/tags"),
867 Some(r".*/(?:refs/tags/)?v?@ANY_VERSION@@ARCHIVE_EXT@"),
868 Some("html"),
869 None,
870 );
871
872 assert_eq!(
873 template,
874 Some(Template::GitHub {
875 owner: "torvalds".to_string(),
876 repository: "linux".to_string(),
877 release_only: false,
878 version_type: None,
879 })
880 );
881 }
882
883 #[test]
884 fn test_detect_github_template_releases() {
885 let template = detect_template(
886 Some("https://github.com/test/project/releases"),
887 Some(r".*/(?:refs/tags/)?v?@ANY_VERSION@@ARCHIVE_EXT@"),
888 Some("html"),
889 None,
890 );
891
892 assert_eq!(
893 template,
894 Some(Template::GitHub {
895 owner: "test".to_string(),
896 repository: "project".to_string(),
897 release_only: true,
898 version_type: None,
899 })
900 );
901 }
902
903 #[test]
904 fn test_detect_github_template_with_version_type() {
905 let template = detect_template(
906 Some("https://github.com/foo/bar/tags"),
907 Some(r".*/(?:refs/tags/)?v?@SEMANTIC_VERSION@@ARCHIVE_EXT@"),
908 Some("html"),
909 None,
910 );
911
912 assert_eq!(
913 template,
914 Some(Template::GitHub {
915 owner: "foo".to_string(),
916 repository: "bar".to_string(),
917 release_only: false,
918 version_type: Some("semantic".to_string()),
919 })
920 );
921 }
922
923 #[test]
924 fn test_detect_pypi_template() {
925 let template = detect_template(
926 Some("https://pypi.debian.net/bitbox02/"),
927 Some(r"https://pypi\.debian\.net/bitbox02/[^/]+\.tar\.gz#/.*-@ANY_VERSION@\.tar\.gz"),
928 Some("plain"),
929 None,
930 );
931
932 assert_eq!(
933 template,
934 Some(Template::PyPI {
935 package: "bitbox02".to_string(),
936 version_type: None,
937 })
938 );
939 }
940
941 #[test]
942 fn test_detect_gitlab_template() {
943 let template = detect_template(
944 Some("https://salsa.debian.org/debian/devscripts"),
945 Some(r".*/v?@ANY_VERSION@@ARCHIVE_EXT@"),
946 None,
947 Some("gitlab"),
948 );
949
950 assert_eq!(
951 template,
952 Some(Template::GitLab {
953 dist: "https://salsa.debian.org/debian/devscripts".to_string(),
954 release_only: false,
955 version_type: None,
956 })
957 );
958 }
959
960 #[test]
961 fn test_detect_npmregistry_template() {
962 let template = detect_template(
963 Some("https://registry.npmjs.org/@lemonldapng/handler"),
964 Some(
965 r"https://registry\.npmjs\.org/lemonldapng/handler/-/.*-@ANY_VERSION@@ARCHIVE_EXT@",
966 ),
967 Some("plain"),
968 None,
969 );
970
971 assert_eq!(
972 template,
973 Some(Template::Npmregistry {
974 package: "@lemonldapng/handler".to_string(),
975 version_type: None,
976 })
977 );
978 }
979
980 #[test]
981 fn test_detect_metacpan_template() {
982 let template = detect_template(
983 Some("https://cpan.metacpan.org/authors/id/"),
984 Some(r".*/MetaCPAN-Client@ANY_VERSION@@ARCHIVE_EXT@"),
985 Some("plain"),
986 None,
987 );
988
989 assert_eq!(
990 template,
991 Some(Template::Metacpan {
992 dist: "MetaCPAN-Client".to_string(),
993 version_type: None,
994 })
995 );
996 }
997
998 #[test]
999 fn test_detect_no_template() {
1000 let template = detect_template(
1002 Some("https://example.com/downloads/"),
1003 Some(r".*/v?(\d+\.\d+)\.tar\.gz"),
1004 Some("html"),
1005 None,
1006 );
1007
1008 assert_eq!(template, None);
1009 }
1010
1011 #[test]
1012 fn test_roundtrip_github_template() {
1013 let original = Template::GitHub {
1015 owner: "torvalds".to_string(),
1016 repository: "linux".to_string(),
1017 release_only: false,
1018 version_type: None,
1019 };
1020 let expanded = expand_template(original.clone());
1021
1022 let detected = detect_template(
1024 expanded.source.as_deref(),
1025 expanded.matching_pattern.as_deref(),
1026 expanded.searchmode.as_deref(),
1027 expanded.mode.as_deref(),
1028 );
1029
1030 assert_eq!(detected, Some(original));
1031 }
1032
1033 #[test]
1034 fn test_extract_version_type() {
1035 assert_eq!(extract_version_type("@ANY_VERSION@"), None);
1036 assert_eq!(
1037 extract_version_type("@SEMANTIC_VERSION@"),
1038 Some("semantic".to_string())
1039 );
1040 assert_eq!(
1041 extract_version_type("@STABLE_VERSION@"),
1042 Some("stable".to_string())
1043 );
1044 assert_eq!(extract_version_type("no-template-here"), None);
1045 }
1046
1047 #[test]
1048 fn test_detect_github_wrong_searchmode() {
1049 let template = detect_template(
1051 Some("https://github.com/torvalds/linux/tags"),
1052 Some(r".*/(?:refs/tags/)?v?@ANY_VERSION@@ARCHIVE_EXT@"),
1053 Some("plain"), None,
1055 );
1056
1057 assert_eq!(template, None);
1058 }
1059
1060 #[test]
1061 fn test_detect_github_invalid_url() {
1062 let template = detect_template(
1064 Some("https://github.com/torvalds/linux"),
1065 Some(r".*/(?:refs/tags/)?v?@ANY_VERSION@@ARCHIVE_EXT@"),
1066 Some("html"),
1067 None,
1068 );
1069
1070 assert_eq!(template, None);
1071 }
1072
1073 #[test]
1074 fn test_detect_github_wrong_host() {
1075 let template = detect_template(
1077 Some("https://gitlab.com/foo/bar/tags"),
1078 Some(r".*/(?:refs/tags/)?v?@ANY_VERSION@@ARCHIVE_EXT@"),
1079 Some("html"),
1080 None,
1081 );
1082
1083 assert_eq!(template, None);
1084 }
1085
1086 #[test]
1087 fn test_detect_gitlab_without_mode() {
1088 let template = detect_template(
1090 Some("https://salsa.debian.org/debian/devscripts"),
1091 Some(r".*/v?@ANY_VERSION@@ARCHIVE_EXT@"),
1092 None,
1093 None, );
1095
1096 assert_eq!(template, None);
1097 }
1098
1099 #[test]
1100 fn test_detect_pypi_source_with_inline_pattern() {
1101 let template = detect_template(
1104 Some("https://pypi.debian.net/dulwich/dulwich-(.*).tar.gz"),
1105 None,
1106 None,
1107 None,
1108 );
1109
1110 assert_eq!(
1111 template,
1112 Some(Template::PyPI {
1113 package: "dulwich".to_string(),
1114 version_type: None,
1115 })
1116 );
1117 }
1118
1119 #[test]
1120 fn test_detect_pypi_wrong_searchmode() {
1121 let template = detect_template(
1122 Some("https://pypi.debian.net/bitbox02/"),
1123 Some(r"https://pypi\.debian\.net/bitbox02/[^/]+\.tar\.gz#/.*-@ANY_VERSION@\.tar\.gz"),
1124 Some("html"), None,
1126 );
1127
1128 assert_eq!(template, None);
1129 }
1130
1131 #[test]
1132 fn test_detect_pypi_wrong_url() {
1133 let template = detect_template(
1134 Some("https://pypi.org/bitbox02/"), Some(r"https://pypi\.debian\.net/bitbox02/[^/]+\.tar\.gz#/.*-@ANY_VERSION@\.tar\.gz"),
1136 Some("plain"),
1137 None,
1138 );
1139
1140 assert_eq!(template, None);
1141 }
1142
1143 #[test]
1144 fn test_detect_npmregistry_wrong_url() {
1145 let template = detect_template(
1146 Some("https://npm.example.com/@lemonldapng/handler"), Some(
1148 r"https://registry\.npmjs\.org/lemonldapng/handler/-/.*-@ANY_VERSION@@ARCHIVE_EXT@",
1149 ),
1150 Some("plain"),
1151 None,
1152 );
1153
1154 assert_eq!(template, None);
1155 }
1156
1157 #[test]
1158 fn test_detect_metacpan_wrong_source() {
1159 let template = detect_template(
1160 Some("https://cpan.example.com/authors/id/"), Some(r".*/MetaCPAN-Client@ANY_VERSION@@ARCHIVE_EXT@"),
1162 Some("plain"),
1163 None,
1164 );
1165
1166 assert_eq!(template, None);
1167 }
1168
1169 #[test]
1170 fn test_detect_metacpan_missing_pattern() {
1171 let template = detect_template(
1172 Some("https://cpan.metacpan.org/authors/id/"),
1173 None, Some("plain"),
1175 None,
1176 );
1177
1178 assert_eq!(template, None);
1179 }
1180
1181 #[test]
1182 fn test_detect_metacpan_release_url() {
1183 let template = detect_template(
1184 Some("https://metacpan.org/release/Time-ParseDate"),
1185 Some(r".*/Time-ParseDate-v?@ANY_VERSION@@ARCHIVE_EXT@$"),
1186 None,
1187 None,
1188 );
1189
1190 assert_eq!(
1191 template,
1192 Some(Template::Metacpan {
1193 dist: "Time-ParseDate".to_string(),
1194 version_type: None,
1195 })
1196 );
1197 }
1198
1199 #[test]
1200 fn test_detect_metacpan_dist_url() {
1201 let template = detect_template(
1202 Some("https://metacpan.org/dist/Mail-AuthenticationResults"),
1203 Some(r".*/Mail-AuthenticationResults-v?@ANY_VERSION@@ARCHIVE_EXT@$"),
1204 None,
1205 None,
1206 );
1207
1208 assert_eq!(
1209 template,
1210 Some(Template::Metacpan {
1211 dist: "Mail-AuthenticationResults".to_string(),
1212 version_type: None,
1213 })
1214 );
1215 }
1216
1217 #[test]
1218 fn test_detect_metacpan_cpan_url_with_v_prefix() {
1219 let template = detect_template(
1221 Some("https://cpan.metacpan.org/authors/id/"),
1222 Some(r".*/Time-ParseDate-v?@ANY_VERSION@@ARCHIVE_EXT@"),
1223 Some("plain"),
1224 None,
1225 );
1226
1227 assert_eq!(
1228 template,
1229 Some(Template::Metacpan {
1230 dist: "Time-ParseDate".to_string(),
1231 version_type: None,
1232 })
1233 );
1234 }
1235
1236 #[test]
1237 fn test_detect_metacpan_release_url_wrong_domain() {
1238 let template = detect_template(
1239 Some("https://example.org/release/Time-ParseDate"),
1240 Some(r".*/Time-ParseDate-v?@ANY_VERSION@@ARCHIVE_EXT@$"),
1241 None,
1242 None,
1243 );
1244
1245 assert_eq!(template, None);
1246 }
1247
1248 #[test]
1249 fn test_roundtrip_gitlab_template() {
1250 let original = Template::GitLab {
1251 dist: "https://salsa.debian.org/debian/devscripts".to_string(),
1252 release_only: false,
1253 version_type: None,
1254 };
1255 let expanded = expand_template(original.clone());
1256
1257 let detected = detect_template(
1258 expanded.source.as_deref(),
1259 expanded.matching_pattern.as_deref(),
1260 expanded.searchmode.as_deref(),
1261 expanded.mode.as_deref(),
1262 );
1263
1264 assert_eq!(detected, Some(original));
1265 }
1266
1267 #[test]
1268 fn test_roundtrip_pypi_template() {
1269 let original = Template::PyPI {
1270 package: "bitbox02".to_string(),
1271 version_type: None,
1272 };
1273 let expanded = expand_template(original.clone());
1274
1275 let detected = detect_template(
1276 expanded.source.as_deref(),
1277 expanded.matching_pattern.as_deref(),
1278 expanded.searchmode.as_deref(),
1279 expanded.mode.as_deref(),
1280 );
1281
1282 assert_eq!(detected, Some(original));
1283 }
1284
1285 #[test]
1286 fn test_roundtrip_npmregistry_template() {
1287 let original = Template::Npmregistry {
1288 package: "@scope/package".to_string(),
1289 version_type: None,
1290 };
1291 let expanded = expand_template(original.clone());
1292
1293 let detected = detect_template(
1294 expanded.source.as_deref(),
1295 expanded.matching_pattern.as_deref(),
1296 expanded.searchmode.as_deref(),
1297 expanded.mode.as_deref(),
1298 );
1299
1300 assert_eq!(detected, Some(original));
1301 }
1302
1303 #[test]
1304 fn test_roundtrip_metacpan_template() {
1305 let original = Template::Metacpan {
1306 dist: "MetaCPAN-Client".to_string(),
1307 version_type: None,
1308 };
1309 let expanded = expand_template(original.clone());
1310
1311 let detected = detect_template(
1312 expanded.source.as_deref(),
1313 expanded.matching_pattern.as_deref(),
1314 expanded.searchmode.as_deref(),
1315 expanded.mode.as_deref(),
1316 );
1317
1318 assert_eq!(detected, Some(original));
1319 }
1320
1321 #[test]
1322 fn test_roundtrip_github_with_version_type() {
1323 let original = Template::GitHub {
1324 owner: "foo".to_string(),
1325 repository: "bar".to_string(),
1326 release_only: true,
1327 version_type: Some("stable".to_string()),
1328 };
1329 let expanded = expand_template(original.clone());
1330
1331 let detected = detect_template(
1332 expanded.source.as_deref(),
1333 expanded.matching_pattern.as_deref(),
1334 expanded.searchmode.as_deref(),
1335 expanded.mode.as_deref(),
1336 );
1337
1338 assert_eq!(detected, Some(original));
1339 }
1340
1341 #[test]
1342 fn test_detect_with_none_source() {
1343 let template = detect_template(
1345 None,
1346 Some(r".*/v?@ANY_VERSION@@ARCHIVE_EXT@"),
1347 Some("html"),
1348 None,
1349 );
1350
1351 assert_eq!(template, None);
1352 }
1353
1354 #[test]
1355 fn test_detect_github_partial_match() {
1356 let template = detect_template(
1358 Some("https://github.com/torvalds/linux/tags"),
1359 Some(r".*/v?(\d+\.\d+)\.tar\.gz"), Some("html"),
1361 None,
1362 );
1363
1364 assert_eq!(
1366 template,
1367 Some(Template::GitHub {
1368 owner: "torvalds".to_string(),
1369 repository: "linux".to_string(),
1370 release_only: false,
1371 version_type: None,
1372 })
1373 );
1374 }
1375
1376 #[test]
1377 fn test_extract_version_type_edge_cases() {
1378 assert_eq!(extract_version_type("@FOO@@BAR@"), None);
1380
1381 assert_eq!(extract_version_type("@INCOMPLETE"), None);
1383
1384 assert_eq!(extract_version_type("@SOMETHING@"), None);
1386
1387 assert_eq!(extract_version_type("@@"), None);
1389 }
1390
1391 #[test]
1392 fn test_cran_template() {
1393 let template = Template::Cran {
1394 package: "forecast".to_string(),
1395 version_type: None,
1396 };
1397
1398 let result = expand_template(template);
1399 assert_eq!(
1400 result.source,
1401 Some("https://cran.r-project.org/package=forecast".to_string())
1402 );
1403 assert_eq!(
1404 result.matching_pattern,
1405 Some(".*_@ANY_VERSION@.tar.gz".to_string())
1406 );
1407 assert_eq!(
1408 result.downloadurlmangle,
1409 Some("s%.*/src/contrib/%https://cran.r-project.org/src/contrib/%".to_string())
1410 );
1411 }
1412
1413 #[test]
1414 fn test_bioconductor_template() {
1415 let template = Template::Bioconductor {
1416 package: "GenomicRanges".to_string(),
1417 version_type: None,
1418 };
1419
1420 let result = expand_template(template);
1421 assert_eq!(
1422 result.source,
1423 Some("https://bioconductor.org/packages/GenomicRanges".to_string())
1424 );
1425 assert_eq!(
1426 result.matching_pattern,
1427 Some(".*_@ANY_VERSION@.tar.gz".to_string())
1428 );
1429 assert_eq!(
1430 result.downloadurlmangle,
1431 Some(
1432 "s%.*/src/contrib/%https://bioconductor.org/packages/release/bioc/src/contrib/%"
1433 .to_string()
1434 )
1435 );
1436 }
1437
1438 #[test]
1439 fn test_detect_cran_template() {
1440 let template = detect_template(
1441 Some("https://cran.r-project.org/package=forecast"),
1442 Some(".*_@ANY_VERSION@.tar.gz"),
1443 None,
1444 None,
1445 );
1446
1447 assert_eq!(
1448 template,
1449 Some(Template::Cran {
1450 package: "forecast".to_string(),
1451 version_type: None,
1452 })
1453 );
1454 }
1455
1456 #[test]
1457 fn test_detect_bioconductor_template() {
1458 let template = detect_template(
1459 Some("https://bioconductor.org/packages/GenomicRanges"),
1460 Some(".*_@ANY_VERSION@.tar.gz"),
1461 None,
1462 None,
1463 );
1464
1465 assert_eq!(
1466 template,
1467 Some(Template::Bioconductor {
1468 package: "GenomicRanges".to_string(),
1469 version_type: None,
1470 })
1471 );
1472 }
1473
1474 #[test]
1475 fn test_detect_cran_from_source_url() {
1476 let template = detect_cran_from_source_url(
1477 "https://cran.r-project.org/src/contrib/",
1478 Some(r"forecast_([-.\d]*)\.tar\.gz"),
1479 );
1480
1481 assert_eq!(
1482 template,
1483 Some(Template::Cran {
1484 package: "forecast".to_string(),
1485 version_type: None,
1486 })
1487 );
1488 }
1489
1490 #[test]
1491 fn test_detect_cran_from_cloud_url() {
1492 let template = detect_cran_from_source_url(
1493 "https://cloud.r-project.org/src/contrib/",
1494 Some(r"forecast_([-.\d]*)\.tar\.gz"),
1495 );
1496
1497 assert_eq!(
1498 template,
1499 Some(Template::Cran {
1500 package: "forecast".to_string(),
1501 version_type: None,
1502 })
1503 );
1504 }
1505
1506 #[test]
1507 fn test_detect_cran_from_source_url_no_match() {
1508 let template = detect_cran_from_source_url(
1509 "https://example.com/src/contrib/",
1510 Some(r"forecast_([-.\d]*)\.tar\.gz"),
1511 );
1512
1513 assert_eq!(template, None);
1514 }
1515
1516 #[test]
1517 fn test_roundtrip_cran_template() {
1518 let original = Template::Cran {
1519 package: "forecast".to_string(),
1520 version_type: None,
1521 };
1522 let expanded = expand_template(original.clone());
1523
1524 let detected = detect_template(
1525 expanded.source.as_deref(),
1526 expanded.matching_pattern.as_deref(),
1527 expanded.searchmode.as_deref(),
1528 expanded.mode.as_deref(),
1529 );
1530
1531 assert_eq!(detected, Some(original));
1532 }
1533
1534 #[test]
1535 fn test_roundtrip_bioconductor_template() {
1536 let original = Template::Bioconductor {
1537 package: "GenomicRanges".to_string(),
1538 version_type: None,
1539 };
1540 let expanded = expand_template(original.clone());
1541
1542 let detected = detect_template(
1543 expanded.source.as_deref(),
1544 expanded.matching_pattern.as_deref(),
1545 expanded.searchmode.as_deref(),
1546 expanded.mode.as_deref(),
1547 );
1548
1549 assert_eq!(detected, Some(original));
1550 }
1551
1552 #[test]
1553 fn test_detect_cran_from_inline_url() {
1554 let template = detect_template(
1557 Some("https://cloud.r-project.org/src/contrib/forecast_([-\\d.]*)\\.tar\\.gz"),
1558 None,
1559 None,
1560 None,
1561 );
1562
1563 assert_eq!(
1564 template,
1565 Some(Template::Cran {
1566 package: "forecast".to_string(),
1567 version_type: None,
1568 })
1569 );
1570 }
1571
1572 #[test]
1573 fn test_detect_cran_from_inline_url_cran_domain() {
1574 let template = detect_template(
1575 Some("https://cran.r-project.org/src/contrib/gower_([-\\d.]*)\\.tar\\.gz"),
1576 None,
1577 None,
1578 None,
1579 );
1580
1581 assert_eq!(
1582 template,
1583 Some(Template::Cran {
1584 package: "gower".to_string(),
1585 version_type: None,
1586 })
1587 );
1588 }
1589}