Skip to main content

zoi_core/
dependency.rs

1use anyhow::{Result, anyhow};
2use regex::Regex;
3use semver::VersionReq;
4use std::sync::LazyLock;
5
6/// A parsed dependency specification.
7///
8/// This represents a requirement for another package, which may be managed by Zoi
9/// or an external package manager (e.g. `apt`, `brew`, `npm`).
10#[derive(Debug)]
11pub struct Dependency<'a> {
12    /// The name of the package manager responsible for this dependency (e.g. "zoi", "apt").
13    pub manager: &'a str,
14    /// The name of the package in the target ecosystem.
15    pub package: &'a str,
16    /// A semver version requirement, if applicable.
17    pub req: Option<VersionReq>,
18    /// The raw version string or channel name (e.g. "1.2.3", "stable").
19    pub version_str: Option<String>,
20    /// An optional description of the dependency.
21    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    // Matches package name and optional version.
31    // Supports scoped packages (@repo/name) and versions (@ver, =ver, etc.)
32    Regex::new(r"^(?P<pkg>@[^@/]+/[^@]+|[^@=><~^]+)(?P<ver>@.+|[=><~^].+)?$")
33        .expect("Static VER_RE regex is valid")
34});
35
36/// Parses a dependency string into its constituent parts.
37///
38/// The expected format is `manager:package[@version][:description]`.
39/// If the manager is omitted, it defaults to "zoi".
40///
41/// Arguments:
42/// - `dep_str`: The raw dependency string from a package definition.
43/// - `is_known_manager`: A closure that determines if a prefix should be treated as a manager name.
44pub 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}