soar_core/package/
query.rs

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