alpm_types/
pkg.rs

1use std::{convert::Infallible, fmt::Display, str::FromStr};
2
3use serde::{Deserialize, Serialize};
4use strum::{Display, EnumString};
5
6use crate::{Error, Name};
7
8/// The type of a package
9///
10/// ## Examples
11/// ```
12/// use std::str::FromStr;
13///
14/// use alpm_types::PackageType;
15///
16/// // create PackageType from str
17/// assert_eq!(PackageType::from_str("pkg"), Ok(PackageType::Package));
18///
19/// // format as String
20/// assert_eq!("debug", format!("{}", PackageType::Debug));
21/// assert_eq!("pkg", format!("{}", PackageType::Package));
22/// assert_eq!("src", format!("{}", PackageType::Source));
23/// assert_eq!("split", format!("{}", PackageType::Split));
24/// ```
25#[derive(Clone, Copy, Debug, Display, EnumString, Eq, PartialEq, Serialize)]
26pub enum PackageType {
27    /// a debug package
28    #[strum(to_string = "debug")]
29    Debug,
30    /// a single (non-split) package
31    #[strum(to_string = "pkg")]
32    Package,
33    /// a source-only package
34    #[strum(to_string = "src")]
35    Source,
36    /// one split package out of a set of several
37    #[strum(to_string = "split")]
38    Split,
39}
40
41/// Description of a package
42///
43/// This type enforces the following invariants on the contained string:
44/// - No leading/trailing spaces
45/// - Tabs and newlines are substituted with spaces.
46/// - Multiple, consecutive spaces are substituted with a single space.
47///
48/// This is a type alias for [`String`].
49///
50/// ## Examples
51///
52/// ```
53/// use alpm_types::PackageDescription;
54///
55/// # fn main() {
56/// // Create PackageDescription from a string slice
57/// let description = PackageDescription::from("my special package ");
58///
59/// assert_eq!(&description.to_string(), "my special package");
60/// # }
61/// ```
62#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
63pub struct PackageDescription(String);
64
65impl PackageDescription {
66    /// Create a new `PackageDescription` from a given `String`.
67    pub fn new(description: &str) -> Self {
68        Self::from(description)
69    }
70}
71
72impl FromStr for PackageDescription {
73    type Err = Infallible;
74
75    fn from_str(s: &str) -> Result<Self, Self::Err> {
76        Ok(Self::from(s))
77    }
78}
79
80impl AsRef<str> for PackageDescription {
81    /// Returns a reference to the inner [`String`].
82    fn as_ref(&self) -> &str {
83        &self.0
84    }
85}
86
87impl From<&str> for PackageDescription {
88    /// Creates a new [`PackageDescription`] from a string slice.
89    ///
90    /// Trims leading and trailing whitespace.
91    /// Replaces any new lines and tabs with a space.
92    /// Replaces any consecutive spaces with a single space.
93    fn from(value: &str) -> Self {
94        // Trim front and back and replace unwanted whitespace chars.
95        let mut description = value.trim().replace(['\n', '\r', '\t'], " ");
96
97        // Remove all spaces that follow a space.
98        let mut previous = ' ';
99        description.retain(|ch| {
100            if ch == ' ' && previous == ' ' {
101                return false;
102            };
103            previous = ch;
104            true
105        });
106
107        Self(description)
108    }
109}
110
111impl Display for PackageDescription {
112    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113        write!(f, "{}", self.0)
114    }
115}
116
117/// Name of the base package information that one or more packages are built from.
118///
119/// This is a type alias for [`Name`].
120///
121/// ## Examples
122/// ```
123/// use std::str::FromStr;
124///
125/// use alpm_types::{Error, Name};
126///
127/// # fn main() -> Result<(), alpm_types::Error> {
128/// // create PackageBaseName from &str
129/// let pkgbase = Name::from_str("test-123@.foo_+")?;
130///
131/// // format as String
132/// let pkgbase = Name::from_str("foo")?;
133/// assert_eq!("foo", pkgbase.to_string());
134/// # Ok(())
135/// # }
136/// ```
137pub type PackageBaseName = Name;
138
139/// Extra data associated with a package
140///
141/// This type wraps a key-value pair of data as String, which is separated by an equal sign (`=`).
142#[derive(Clone, Debug, PartialEq, Serialize)]
143pub struct ExtraData {
144    key: String,
145    value: String,
146}
147
148impl ExtraData {
149    /// Create a new extra_data
150    pub fn new(key: String, value: String) -> Self {
151        Self { key, value }
152    }
153
154    /// Return the key of the extra_data
155    pub fn key(&self) -> &str {
156        &self.key
157    }
158
159    /// Return the value of the extra_data
160    pub fn value(&self) -> &str {
161        &self.value
162    }
163}
164
165impl FromStr for ExtraData {
166    type Err = Error;
167
168    /// Parses an `extra_data` from string.
169    ///
170    /// The string is expected to be in the format `key=value`.
171    ///
172    /// ## Errors
173    ///
174    /// This function returns an error if the string is missing the key or value component.
175    ///
176    /// ## Examples
177    ///
178    /// ```
179    /// use std::str::FromStr;
180    ///
181    /// use alpm_types::{Error, ExtraData, PackageType};
182    ///
183    /// # fn main() -> Result<(), alpm_types::Error> {
184    /// // create ExtraData from str
185    /// let extra_data: ExtraData = ExtraData::from_str("pkgtype=debug")?;
186    /// assert_eq!(extra_data.key(), "pkgtype");
187    /// assert_eq!(extra_data.value(), "debug");
188    /// # Ok(())
189    /// # }
190    /// ```
191    fn from_str(s: &str) -> Result<Self, Self::Err> {
192        const DELIMITER: char = '=';
193        let mut parts = s.splitn(2, DELIMITER);
194        let key = parts
195            .next()
196            .map(|v| v.trim())
197            .filter(|v| !v.is_empty())
198            .ok_or(Error::MissingComponent { component: "key" })?;
199        let value = parts
200            .next()
201            .map(|v| v.trim())
202            .filter(|v| !v.is_empty())
203            .ok_or(Error::MissingComponent { component: "value" })?;
204        Ok(Self::new(key.to_string(), value.to_string()))
205    }
206}
207
208impl Display for ExtraData {
209    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
210        write!(f, "{}={}", self.key, self.value)
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use std::str::FromStr;
217
218    use rstest::rstest;
219
220    use super::*;
221
222    #[rstest]
223    #[case("debug", Ok(PackageType::Debug))]
224    #[case("pkg", Ok(PackageType::Package))]
225    #[case("src", Ok(PackageType::Source))]
226    #[case("split", Ok(PackageType::Split))]
227    #[case("foo", Err(strum::ParseError::VariantNotFound))]
228    fn pkgtype_from_string(
229        #[case] from_str: &str,
230        #[case] result: Result<PackageType, strum::ParseError>,
231    ) {
232        assert_eq!(PackageType::from_str(from_str), result);
233    }
234
235    #[rstest]
236    #[case(PackageType::Debug, "debug")]
237    #[case(PackageType::Package, "pkg")]
238    #[case(PackageType::Source, "src")]
239    #[case(PackageType::Split, "split")]
240    fn pkgtype_format_string(#[case] pkgtype: PackageType, #[case] pkgtype_str: &str) {
241        assert_eq!(pkgtype_str, format!("{pkgtype}"));
242    }
243
244    #[rstest]
245    #[case("key=value", "key", "value")]
246    #[case("pkgtype=debug", "pkgtype", "debug")]
247    #[case("test-123@.foo_+=1000", "test-123@.foo_+", "1000")]
248    fn extra_data_from_str(
249        #[case] data: &str,
250        #[case] key: &str,
251        #[case] value: &str,
252    ) -> testresult::TestResult<()> {
253        let extra_data: ExtraData = ExtraData::from_str(data)?;
254        assert_eq!(extra_data.key(), key);
255        assert_eq!(extra_data.value(), value);
256        assert_eq!(extra_data.to_string(), data);
257        Ok(())
258    }
259
260    #[rstest]
261    #[case("key", Err(Error::MissingComponent { component: "value" }))]
262    #[case("key=", Err(Error::MissingComponent { component: "value" }))]
263    #[case("=value", Err(Error::MissingComponent { component: "key" }))]
264    fn extra_data_from_str_error(
265        #[case] extra_data: &str,
266        #[case] result: Result<ExtraData, Error>,
267    ) {
268        assert_eq!(ExtraData::from_str(extra_data), result);
269    }
270
271    #[rstest]
272    #[case("  trailing  ", "trailing")]
273    #[case("in    between    words", "in between words")]
274    #[case("\nsome\t whitespace\n chars\n", "some whitespace chars")]
275    #[case("  \neverything\t   combined\n yeah \n   ", "everything combined yeah")]
276    fn package_description(#[case] input: &str, #[case] result: &str) {
277        assert_eq!(PackageDescription::new(input).to_string(), result);
278    }
279}