alpm_types/
pkg.rs

1use std::{convert::Infallible, fmt::Display, str::FromStr};
2
3use serde::{Deserialize, Serialize};
4use serde_with::{DeserializeFromStr, SerializeDisplay};
5use strum::{Display, EnumString};
6
7use crate::{Error, Name};
8
9/// The type of a package
10///
11/// ## Examples
12/// ```
13/// use std::str::FromStr;
14///
15/// use alpm_types::PackageType;
16///
17/// // create PackageType from str
18/// assert_eq!(PackageType::from_str("pkg"), Ok(PackageType::Package));
19///
20/// // format as String
21/// assert_eq!("debug", format!("{}", PackageType::Debug));
22/// assert_eq!("pkg", format!("{}", PackageType::Package));
23/// assert_eq!("src", format!("{}", PackageType::Source));
24/// assert_eq!("split", format!("{}", PackageType::Split));
25/// ```
26#[derive(Clone, Copy, Debug, Display, EnumString, Eq, PartialEq, Serialize)]
27pub enum PackageType {
28    /// a debug package
29    #[strum(to_string = "debug")]
30    Debug,
31    /// a single (non-split) package
32    #[strum(to_string = "pkg")]
33    Package,
34    /// a source-only package
35    #[strum(to_string = "src")]
36    Source,
37    /// one split package out of a set of several
38    #[strum(to_string = "split")]
39    Split,
40}
41
42/// Description of a package
43///
44/// This type enforces the following invariants on the contained string:
45/// - No leading/trailing spaces
46/// - Tabs and newlines are substituted with spaces.
47/// - Multiple, consecutive spaces are substituted with a single space.
48///
49/// This is a type alias for [`String`].
50///
51/// ## Examples
52///
53/// ```
54/// use alpm_types::PackageDescription;
55///
56/// # fn main() {
57/// // Create PackageDescription from a string slice
58/// let description = PackageDescription::from("my special package ");
59///
60/// assert_eq!(&description.to_string(), "my special package");
61/// # }
62/// ```
63#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
64pub struct PackageDescription(String);
65
66impl PackageDescription {
67    /// Create a new `PackageDescription` from a given `String`.
68    pub fn new(description: &str) -> Self {
69        Self::from(description)
70    }
71}
72
73impl Default for PackageDescription {
74    /// Returns the default [`PackageDescription`].
75    ///
76    /// Following the default for [`String`], this returns a [`PackageDescription`] wrapping an
77    /// empty string.
78    fn default() -> Self {
79        Self::new("")
80    }
81}
82
83impl FromStr for PackageDescription {
84    type Err = Infallible;
85
86    fn from_str(s: &str) -> Result<Self, Self::Err> {
87        Ok(Self::from(s))
88    }
89}
90
91impl AsRef<str> for PackageDescription {
92    /// Returns a reference to the inner [`String`].
93    fn as_ref(&self) -> &str {
94        &self.0
95    }
96}
97
98impl From<&str> for PackageDescription {
99    /// Creates a new [`PackageDescription`] from a string slice.
100    ///
101    /// Trims leading and trailing whitespace.
102    /// Replaces any new lines and tabs with a space.
103    /// Replaces any consecutive spaces with a single space.
104    fn from(value: &str) -> Self {
105        // Trim front and back and replace unwanted whitespace chars.
106        let mut description = value.trim().replace(['\n', '\r', '\t'], " ");
107
108        // Remove all spaces that follow a space.
109        let mut previous = ' ';
110        description.retain(|ch| {
111            if ch == ' ' && previous == ' ' {
112                return false;
113            };
114            previous = ch;
115            true
116        });
117
118        Self(description)
119    }
120}
121
122impl Display for PackageDescription {
123    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124        write!(f, "{}", self.0)
125    }
126}
127
128/// Name of the base package information that one or more packages are built from.
129///
130/// This is a type alias for [`Name`].
131///
132/// ## Examples
133/// ```
134/// use std::str::FromStr;
135///
136/// use alpm_types::{Error, Name};
137///
138/// # fn main() -> Result<(), alpm_types::Error> {
139/// // create PackageBaseName from &str
140/// let pkgbase = Name::from_str("test-123@.foo_+")?;
141///
142/// // format as String
143/// let pkgbase = Name::from_str("foo")?;
144/// assert_eq!("foo", pkgbase.to_string());
145/// # Ok(())
146/// # }
147/// ```
148pub type PackageBaseName = Name;
149
150/// Extra data entry associated with a package
151///
152/// This type wraps a key-value pair of data as String, which is separated by an equal sign (`=`).
153#[derive(Clone, Debug, DeserializeFromStr, PartialEq, SerializeDisplay)]
154pub struct ExtraDataEntry {
155    key: String,
156    value: String,
157}
158
159impl ExtraDataEntry {
160    /// Create a new extra_data
161    pub fn new(key: String, value: String) -> Self {
162        Self { key, value }
163    }
164
165    /// Return the key of the extra_data
166    pub fn key(&self) -> &str {
167        &self.key
168    }
169
170    /// Return the value of the extra_data
171    pub fn value(&self) -> &str {
172        &self.value
173    }
174}
175
176impl FromStr for ExtraDataEntry {
177    type Err = Error;
178
179    /// Parses an `extra_data` from string.
180    ///
181    /// The string is expected to be in the format `key=value`.
182    ///
183    /// ## Errors
184    ///
185    /// This function returns an error if the string is missing the key or value component.
186    ///
187    /// ## Examples
188    ///
189    /// ```
190    /// use std::str::FromStr;
191    ///
192    /// use alpm_types::{ExtraDataEntry, PackageType};
193    ///
194    /// # fn main() -> Result<(), alpm_types::Error> {
195    /// // create ExtraDataEntry from str
196    /// let extra_data: ExtraDataEntry = ExtraDataEntry::from_str("pkgtype=debug")?;
197    /// assert_eq!(extra_data.key(), "pkgtype");
198    /// assert_eq!(extra_data.value(), "debug");
199    /// # Ok(())
200    /// # }
201    /// ```
202    fn from_str(s: &str) -> Result<Self, Self::Err> {
203        const DELIMITER: char = '=';
204        let mut parts = s.splitn(2, DELIMITER);
205        let key = parts
206            .next()
207            .map(|v| v.trim())
208            .filter(|v| !v.is_empty())
209            .ok_or(Error::MissingComponent { component: "key" })?;
210        let value = parts
211            .next()
212            .map(|v| v.trim())
213            .filter(|v| !v.is_empty())
214            .ok_or(Error::MissingComponent { component: "value" })?;
215        Ok(Self::new(key.to_string(), value.to_string()))
216    }
217}
218
219impl Display for ExtraDataEntry {
220    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
221        write!(f, "{}={}", self.key, self.value)
222    }
223}
224
225/// Extra data associated with a package.
226///
227/// This type wraps a vector of [`ExtraDataEntry`] items enforcing that it includes a valid
228/// `pkgtype` entry.
229///
230/// Can be created from a [`Vec<ExtraDataEntry>`] or [`ExtraDataEntry`] using [`TryFrom::try_from`].
231#[derive(Clone, Debug, PartialEq, Serialize)]
232pub struct ExtraData(Vec<ExtraDataEntry>);
233
234impl ExtraData {
235    /// Returns the package type.
236    pub fn pkg_type(&self) -> PackageType {
237        self.0
238            .iter()
239            .find(|v| v.key() == "pkgtype")
240            .map(|v| PackageType::from_str(v.value()).expect("Invalid package type"))
241            .unwrap_or_else(|| unreachable!("Valid xdata should always contain a pkgtype entry."))
242    }
243
244    /// Returns the number of extra data entries.
245    pub fn len(&self) -> usize {
246        self.0.len()
247    }
248
249    /// Returns true if there are no extra data entries.
250    ///
251    /// Due to the invariant enforced in [`TryFrom`], this will always return `false` and is only
252    /// included for consistency with [`Vec::is_empty`] in the standard library.
253    pub fn is_empty(&self) -> bool {
254        self.0.is_empty()
255    }
256}
257
258impl TryFrom<Vec<ExtraDataEntry>> for ExtraData {
259    type Error = Error;
260
261    /// Creates an [`ExtraData`] from a vector of [`ExtraDataEntry`].
262    ///
263    /// ## Errors
264    ///
265    /// Returns an error in the following cases:
266    ///
267    /// - if the `value` does not contain a `pkgtype` key.
268    /// - if the `pkgtype` entry does not contain a valid package type.
269    fn try_from(value: Vec<ExtraDataEntry>) -> Result<Self, Self::Error> {
270        if let Some(pkg_type) = value.iter().find(|v| v.key() == "pkgtype") {
271            let _ = PackageType::from_str(pkg_type.value())?;
272            Ok(Self(value))
273        } else {
274            Err(Error::MissingComponent {
275                component: "extra_data with a valid \"pkgtype\" entry",
276            })
277        }
278    }
279}
280
281impl TryFrom<ExtraDataEntry> for ExtraData {
282    type Error = Error;
283
284    /// Creates an [`ExtraData`] from a single [`ExtraDataEntry`].
285    ///
286    /// Delegates to [`TryFrom::try_from`] for [`Vec<ExtraDataEntry>`].
287    ///
288    /// ## Errors
289    ///
290    /// If the [`TryFrom::try_from`] for [`Vec<ExtraDataEntry>`] returns an error.
291    fn try_from(value: ExtraDataEntry) -> Result<Self, Self::Error> {
292        Self::try_from(vec![value])
293    }
294}
295
296impl From<ExtraData> for Vec<ExtraDataEntry> {
297    /// Converts the [`ExtraData`] into a [`Vec<ExtraDataEntry>`].
298    fn from(value: ExtraData) -> Self {
299        value.0
300    }
301}
302
303impl IntoIterator for ExtraData {
304    type Item = ExtraDataEntry;
305    type IntoIter = std::vec::IntoIter<ExtraDataEntry>;
306
307    /// Consumes the [`ExtraData`] and returns an iterator over [`ExtraDataEntry`] items.
308    fn into_iter(self) -> Self::IntoIter {
309        self.0.into_iter()
310    }
311}
312
313impl AsRef<[ExtraDataEntry]> for ExtraData {
314    /// Returns a reference to the inner [`Vec<ExtraDataEntry>`].
315    fn as_ref(&self) -> &[ExtraDataEntry] {
316        &self.0
317    }
318}
319
320#[cfg(test)]
321mod tests {
322    use std::str::FromStr;
323
324    use rstest::rstest;
325    use testresult::TestResult;
326
327    use super::*;
328
329    #[rstest]
330    #[case("debug", Ok(PackageType::Debug))]
331    #[case("pkg", Ok(PackageType::Package))]
332    #[case("src", Ok(PackageType::Source))]
333    #[case("split", Ok(PackageType::Split))]
334    #[case("foo", Err(strum::ParseError::VariantNotFound))]
335    fn pkgtype_from_string(
336        #[case] from_str: &str,
337        #[case] result: Result<PackageType, strum::ParseError>,
338    ) {
339        assert_eq!(PackageType::from_str(from_str), result);
340    }
341
342    #[rstest]
343    #[case(PackageType::Debug, "debug")]
344    #[case(PackageType::Package, "pkg")]
345    #[case(PackageType::Source, "src")]
346    #[case(PackageType::Split, "split")]
347    fn pkgtype_format_string(#[case] pkgtype: PackageType, #[case] pkgtype_str: &str) {
348        assert_eq!(pkgtype_str, format!("{pkgtype}"));
349    }
350
351    #[rstest]
352    #[case("key=value", "key", "value")]
353    #[case("pkgtype=debug", "pkgtype", "debug")]
354    #[case("test-123@.foo_+=1000", "test-123@.foo_+", "1000")]
355    fn extra_data_entry_from_str(
356        #[case] data: &str,
357        #[case] key: &str,
358        #[case] value: &str,
359    ) -> TestResult {
360        let extra_data = ExtraDataEntry::from_str(data)?;
361        assert_eq!(extra_data.key(), key);
362        assert_eq!(extra_data.value(), value);
363        assert_eq!(extra_data.to_string(), data);
364        Ok(())
365    }
366
367    #[rstest]
368    #[case("key", Err(Error::MissingComponent { component: "value" }))]
369    #[case("key=", Err(Error::MissingComponent { component: "value" }))]
370    #[case("=value", Err(Error::MissingComponent { component: "key" }))]
371    fn extra_data_entry_from_str_error(
372        #[case] extra_data: &str,
373        #[case] result: Result<ExtraDataEntry, Error>,
374    ) {
375        assert_eq!(ExtraDataEntry::from_str(extra_data), result);
376    }
377
378    #[rstest]
379    #[case::empty_list(vec![])]
380    #[case::invalid_pkgtype(vec![ExtraDataEntry::from_str("pkgtype=foo")?])]
381    fn extra_data_invalid(#[case] xdata: Vec<ExtraDataEntry>) -> TestResult {
382        assert!(ExtraData::try_from(xdata).is_err());
383        Ok(())
384    }
385
386    #[rstest]
387    #[case::only_pkgtype(vec![ExtraDataEntry::from_str("pkgtype=pkg")?])]
388    #[case::with_additional_xdata_entry(vec![ExtraDataEntry::from_str("pkgtype=pkg")?, ExtraDataEntry::from_str("foo=bar")?])]
389    fn extra_data_valid(#[case] xdata: Vec<ExtraDataEntry>) -> TestResult {
390        let xdata = ExtraData::try_from(xdata)?;
391        assert_eq!(xdata.pkg_type(), PackageType::Package);
392        Ok(())
393    }
394
395    #[rstest]
396    #[case("  trailing  ", "trailing")]
397    #[case("in    between    words", "in between words")]
398    #[case("\nsome\t whitespace\n chars\n", "some whitespace chars")]
399    #[case("  \neverything\t   combined\n yeah \n   ", "everything combined yeah")]
400    fn package_description(#[case] input: &str, #[case] result: &str) {
401        assert_eq!(PackageDescription::new(input).to_string(), result);
402    }
403}