Skip to main content

zoi_core/
dependency.rs

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