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#[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
28impl<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 pub fn try_new<S: AsRef<str>>(s: S) -> crate::Result<Self> {
73 parse::cpn(s.as_ref())
74 }
75
76 pub fn category(&self) -> &str {
78 &self.category
79 }
80
81 pub fn package(&self) -> &str {
83 &self.package
84 }
85}
86
87impl 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 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 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}