pkgcraft/dep/
cpn.rs

1use std::fmt;
2use std::str::FromStr;
3
4use serde_with::{DeserializeFromStr, SerializeDisplay};
5
6use crate::Error;
7use crate::traits::Intersects;
8
9use super::parse;
10
11/// Unversioned package.
12#[derive(
13    SerializeDisplay, DeserializeFromStr, PartialEq, Eq, PartialOrd, Ord, Clone, Hash,
14)]
15pub struct Cpn {
16    pub(crate) category: String,
17    pub(crate) package: String,
18}
19
20impl FromStr for Cpn {
21    type Err = Error;
22
23    fn from_str(s: &str) -> crate::Result<Self> {
24        Self::try_new(s)
25    }
26}
27
28/// Try converting a (category, package) string tuple into a Cpn.
29impl<T1, T2> TryFrom<(T1, T2)> for Cpn
30where
31    T1: AsRef<str>,
32    T2: AsRef<str>,
33{
34    type Error = Error;
35
36    fn try_from((category, package): (T1, T2)) -> Result<Self, Self::Error> {
37        let category = parse::category(category.as_ref()).map(|s| s.to_string())?;
38        let package = parse::package(package.as_ref()).map(|s| s.to_string())?;
39        Ok(Self { category, package })
40    }
41}
42
43impl From<&Cpn> for Cpn {
44    fn from(value: &Cpn) -> Self {
45        value.clone()
46    }
47}
48
49impl fmt::Debug for Cpn {
50    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
51        write!(f, "Cpn {{ {self} }}")
52    }
53}
54
55impl fmt::Display for Cpn {
56    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
57        write!(f, "{}/{}", self.category, self.package)
58    }
59}
60
61impl PartialEq<str> for Cpn {
62    fn eq(&self, other: &str) -> bool {
63        other
64            .split_once('/')
65            .map(|(cat, pkg)| self.category == cat && self.package == pkg)
66            .unwrap_or_default()
67    }
68}
69
70impl Cpn {
71    /// Create a [`Cpn`] from a given string.
72    pub fn try_new<S: AsRef<str>>(s: S) -> crate::Result<Self> {
73        parse::cpn(s.as_ref())
74    }
75
76    /// Return a Cpn's category.
77    pub fn category(&self) -> &str {
78        &self.category
79    }
80
81    /// Return a Cpn's package.
82    pub fn package(&self) -> &str {
83        &self.package
84    }
85}
86
87/// Determine if two Cpns intersect.
88impl Intersects for Cpn {
89    fn intersects(&self, other: &Self) -> bool {
90        self == other
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn parse() {
100        // invalid
101        for s in ["", "a", "a/", "/b", "a/+b", "a/b-1"] {
102            assert!(Cpn::try_new(s).is_err(), "{s} is valid");
103        }
104
105        // valid
106        for s in ["_/_", "a/b"] {
107            let cpn = Cpn::try_new(s);
108            assert!(cpn.is_ok(), "{s} isn't valid");
109            assert!(format!("{cpn:?}").contains(s));
110            let cpn = cpn.unwrap();
111            assert!(cpn.intersects(&cpn));
112        }
113    }
114}