soar_core/package/
query.rs

1use std::sync::OnceLock;
2
3use regex::Regex;
4
5use crate::error::SoarError;
6
7/// Parsed package query string.
8/// Supports format: `name#pkg_id@version:repo`
9#[derive(Debug)]
10pub struct PackageQuery {
11    pub name: Option<String>,
12    pub repo_name: Option<String>,
13    pub pkg_id: Option<String>,
14    pub version: Option<String>,
15}
16
17impl TryFrom<&str> for PackageQuery {
18    type Error = SoarError;
19
20    fn try_from(value: &str) -> Result<Self, Self::Error> {
21        static PACKAGE_RE: OnceLock<Regex> = OnceLock::new();
22        let re = PACKAGE_RE.get_or_init(|| {
23            Regex::new(
24                r"(?x)
25            (?P<name>[^\/\#\@:]+)?              # optional package name
26            (?:\#(?P<pkg_id>[^@:]+))?           # optional pkg_id after #
27            (?:@(?P<version>[^:]+))?            # optional version after @
28            (?::(?P<repo>[^:]+))?$              # optional repo after :
29            ",
30            )
31            .unwrap()
32        });
33
34        let query = value.trim().to_lowercase();
35        if query.is_empty() {
36            return Err(SoarError::InvalidPackageQuery(
37                "Package query can't be empty".into(),
38            ));
39        }
40
41        let caps = re.captures(&query).ok_or(SoarError::InvalidPackageQuery(
42            "Invalid package query format".into(),
43        ))?;
44
45        let name = caps.name("name").map(|m| m.as_str().to_string());
46        let pkg_id = caps.name("pkg_id").map(|m| m.as_str().to_string());
47        if pkg_id.is_none() && name.is_none() {
48            return Err(SoarError::InvalidPackageQuery(
49                "Either package name or pkg_id is required".into(),
50            ));
51        }
52
53        if let Some(ref pkg_id) = pkg_id {
54            if pkg_id == "all" && name.is_none() {
55                return Err(SoarError::InvalidPackageQuery(
56                    "For all, package name is required.".into(),
57                ));
58            }
59        }
60
61        Ok(PackageQuery {
62            repo_name: caps.name("repo").map(|m| m.as_str().to_string()),
63            pkg_id,
64            name,
65            version: caps.name("version").map(|m| m.as_str().to_string()),
66        })
67    }
68}