1use anyhow::{Result, anyhow};
2use regex::Regex;
3use semver::VersionReq;
4use std::sync::LazyLock;
5
6#[derive(Debug)]
11pub struct Dependency<'a> {
12 pub manager: &'a str,
14 pub package: &'a str,
16 pub req: Option<VersionReq>,
18 pub version_str: Option<String>,
20 pub description: Option<&'a str>,
22}
23
24static DEP_RE: LazyLock<Regex> = LazyLock::new(|| {
25 Regex::new(r"^(?P<pkg_and_ver>.+?)(?::(?P<desc>[^:].*))?$")
26 .expect("Static DEP_RE regex is valid")
27});
28
29static VER_RE: LazyLock<Regex> = LazyLock::new(|| {
30 Regex::new(r"^(?P<pkg>@[^@/]+/[^@]+|[^@=><~^]+)(?P<ver>@.+|[=><~^].+)?$")
33 .expect("Static VER_RE regex is valid")
34});
35
36pub fn parse_dependency_string<'a>(
45 dep_str: &'a str,
46 is_known_manager: impl Fn(&str) -> bool,
47) -> Result<Dependency<'a>> {
48 let (manager, rest) = match dep_str.split_once(':') {
49 Some((m, r)) if !m.is_empty() && is_known_manager(m) => (m, r),
50 _ => ("zoi", dep_str),
51 };
52
53 let rest = rest.trim();
54 if rest.is_empty() {
55 return Err(anyhow!("Invalid dependency string: {}", dep_str));
56 }
57
58 let caps = DEP_RE
59 .captures(rest)
60 .ok_or_else(|| anyhow!("Failed to parse dependency string: {}", rest))?;
61
62 let package_and_version = caps
63 .name("pkg_and_ver")
64 .ok_or_else(|| {
65 anyhow!(
66 "Regex matched but pkg_and_ver group not found in '{}'",
67 rest
68 )
69 })?
70 .as_str()
71 .trim();
72 let description = caps.name("desc").map(|m| m.as_str().trim());
73
74 let ver_caps = VER_RE.captures(package_and_version).ok_or_else(|| {
75 anyhow!(
76 "Failed to parse package and version from: {}",
77 package_and_version
78 )
79 })?;
80
81 let package = ver_caps
82 .name("pkg")
83 .ok_or_else(|| {
84 anyhow!(
85 "Regex matched but pkg group not found in '{}'",
86 package_and_version
87 )
88 })?
89 .as_str()
90 .trim();
91 let mut version_str = ver_caps.name("ver").map(|m| m.as_str().to_string());
92
93 if let Some(v) = &version_str
94 && v.starts_with('@')
95 {
96 version_str = Some(v[1..].to_string());
97 }
98
99 let req = if let Some(v_str) = &version_str {
100 let req_parse_str = if v_str
101 .chars()
102 .next()
103 .ok_or_else(|| anyhow!("Empty version string"))?
104 .is_ascii_digit()
105 {
106 format!("={}", v_str)
107 } else {
108 v_str.to_string()
109 };
110
111 if manager == "zoi" && VersionReq::parse(&req_parse_str).is_err() {
112 None
113 } else {
114 Some(VersionReq::parse(&req_parse_str)?)
115 }
116 } else {
117 None
118 };
119
120 Ok(Dependency {
121 manager,
122 package,
123 req,
124 version_str,
125 description,
126 })
127}