soar_core/package/
query.rs1use std::sync::OnceLock;
2
3use regex::Regex;
4
5use crate::error::SoarError;
6
7#[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}