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