changesets/
versioning.rs

1use std::{
2    cmp::Ordering,
3    collections::HashMap,
4    convert::Infallible,
5    error::Error,
6    fmt::{Display, Formatter},
7};
8
9/// Describes how a [`crate::Change`] affects the version of relevant packages.
10///
11/// This is guaranteed to never be empty, as a changeset must always apply to at least one package.
12#[derive(Clone, Debug, Eq, PartialEq)]
13pub struct Versioning(HashMap<PackageName, ChangeType>);
14
15impl From<(&str, ChangeType)> for Versioning {
16    fn from(value: (&str, ChangeType)) -> Self {
17        let value = (PackageName::from(value.0), value.1);
18        Self::from(value)
19    }
20}
21
22impl From<(PackageName, ChangeType)> for Versioning {
23    fn from(value: (PackageName, ChangeType)) -> Self {
24        let mut map = HashMap::new();
25        map.insert(value.0, value.1);
26        Self(map)
27    }
28}
29
30impl Versioning {
31    /// Creates a new [`Versioning`] from an iterator of tuples.
32    ///
33    /// # Errors
34    ///
35    /// 1. If the iterator is empty, you'll get [`BuildVersioningError::EmptyVersioningError`].
36    pub fn try_from_iter<Key, Value, ParseError, Iter>(
37        iter: Iter,
38    ) -> Result<Self, BuildVersioningError>
39    where
40        Key: Into<PackageName>,
41        Value: TryInto<ChangeType, Error = ParseError>,
42        ParseError: Into<BuildVersioningError>,
43        Iter: IntoIterator<Item = (Key, Value)>,
44    {
45        let map = iter
46            .into_iter()
47            .map(|(key, value)| {
48                value
49                    .try_into()
50                    .map_err(Into::into)
51                    .map(|value| (key.into(), value))
52            })
53            .collect::<Result<HashMap<PackageName, ChangeType>, BuildVersioningError>>()?;
54        if map.is_empty() {
55            Err(BuildVersioningError::EmptyVersioningError)
56        } else {
57            Ok(Self(map))
58        }
59    }
60
61    pub fn iter(&self) -> impl Iterator<Item = (&PackageName, &ChangeType)> {
62        self.0.iter()
63    }
64
65    #[must_use]
66    pub fn len(&self) -> usize {
67        self.0.len()
68    }
69
70    #[must_use]
71    pub fn is_empty(&self) -> bool {
72        self.0.is_empty()
73    }
74}
75
76impl IntoIterator for Versioning {
77    type Item = (PackageName, ChangeType);
78    type IntoIter = std::collections::hash_map::IntoIter<PackageName, ChangeType>;
79
80    fn into_iter(self) -> Self::IntoIter {
81        self.0.into_iter()
82    }
83}
84
85impl FromIterator<(PackageName, ChangeType)> for Versioning {
86    fn from_iter<T: IntoIterator<Item = (PackageName, ChangeType)>>(iter: T) -> Self {
87        Self(iter.into_iter().collect())
88    }
89}
90
91/// The error that occurs if you try to create a [`Versioning`] out of an iterator which has no items.
92#[derive(Clone, Copy, Debug, Eq, PartialEq)]
93pub enum BuildVersioningError {
94    /// The iterator was empty.
95    EmptyVersioningError,
96}
97
98impl Display for BuildVersioningError {
99    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
100        match self {
101            Self::EmptyVersioningError => {
102                f.write_str("Versioning needs to contain at least one item.")
103            }
104        }
105    }
106}
107
108impl From<Infallible> for BuildVersioningError {
109    fn from(_: Infallible) -> Self {
110        unreachable!()
111    }
112}
113
114impl Error for BuildVersioningError {}
115
116/// An alias to [`String`] to encode semantic meaning in [`Change::versioning`]
117pub type PackageName = String;
118
119/// The [Semantic Versioning](https://semver.org/) component which should be incremented when a [`Change`]
120/// is applied.
121#[derive(Clone, Debug, Eq, PartialEq)]
122pub enum ChangeType {
123    Patch,
124    Minor,
125    Major,
126    Custom(String),
127}
128
129impl Display for ChangeType {
130    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
131        match self {
132            ChangeType::Custom(label) => write!(f, "{label}"),
133            ChangeType::Patch => write!(f, "patch"),
134            ChangeType::Minor => write!(f, "minor"),
135            ChangeType::Major => write!(f, "major"),
136        }
137    }
138}
139
140impl From<&str> for ChangeType {
141    fn from(s: &str) -> Self {
142        match s {
143            "patch" => ChangeType::Patch,
144            "minor" => ChangeType::Minor,
145            "major" => ChangeType::Major,
146            other => ChangeType::Custom(other.to_string()),
147        }
148    }
149}
150
151impl From<String> for ChangeType {
152    fn from(s: String) -> Self {
153        match s.as_str() {
154            "patch" => ChangeType::Patch,
155            "minor" => ChangeType::Minor,
156            "major" => ChangeType::Major,
157            _ => ChangeType::Custom(s),
158        }
159    }
160}
161
162impl Ord for ChangeType {
163    fn cmp(&self, other: &Self) -> Ordering {
164        match (self, other) {
165            (ChangeType::Custom(_), ChangeType::Custom(_))
166            | (ChangeType::Major, ChangeType::Major)
167            | (ChangeType::Patch, ChangeType::Patch)
168            | (ChangeType::Minor, ChangeType::Minor) => Ordering::Equal,
169            (ChangeType::Custom(_), _) => Ordering::Less,
170            (_, ChangeType::Custom(_)) => Ordering::Greater,
171            (ChangeType::Patch, _) => Ordering::Less,
172            (_, ChangeType::Patch) => Ordering::Greater,
173            (ChangeType::Minor, _) => Ordering::Less,
174            (_, ChangeType::Minor) => Ordering::Greater,
175        }
176    }
177}
178
179impl PartialOrd for ChangeType {
180    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
181        Some(self.cmp(other))
182    }
183}