Skip to main content

changeset_project/
release_state.rs

1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4
5/// Active prerelease configuration.
6/// File: `.changeset/pre-release.toml`
7/// Format:
8/// ```toml
9/// crate-a = "alpha"
10/// crate-b = "beta"
11/// ```
12#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
13pub struct PrereleaseState {
14    #[serde(flatten)]
15    packages: HashMap<String, String>,
16}
17
18impl PrereleaseState {
19    #[must_use]
20    pub fn new() -> Self {
21        Self::default()
22    }
23
24    #[must_use]
25    pub fn get(&self, crate_name: &str) -> Option<&str> {
26        self.packages.get(crate_name).map(String::as_str)
27    }
28
29    pub fn insert(&mut self, crate_name: String, tag: String) {
30        self.packages.insert(crate_name, tag);
31    }
32
33    #[must_use]
34    pub fn remove(&mut self, crate_name: &str) -> Option<String> {
35        self.packages.remove(crate_name)
36    }
37
38    pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
39        self.packages.iter().map(|(k, v)| (k.as_str(), v.as_str()))
40    }
41
42    #[must_use]
43    pub fn is_empty(&self) -> bool {
44        self.packages.is_empty()
45    }
46
47    #[must_use]
48    pub fn contains(&self, crate_name: &str) -> bool {
49        self.packages.contains_key(crate_name)
50    }
51
52    #[must_use]
53    pub fn len(&self) -> usize {
54        self.packages.len()
55    }
56}
57
58/// Graduation queue for 0.x packages.
59/// File: `.changeset/graduation.toml`
60/// Format:
61/// ```toml
62/// graduation = ["crate-a", "crate-b"]
63/// ```
64#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
65pub struct GraduationState {
66    #[serde(default)]
67    graduation: Vec<String>,
68}
69
70impl GraduationState {
71    #[must_use]
72    pub fn new() -> Self {
73        Self::default()
74    }
75
76    pub fn add(&mut self, crate_name: String) {
77        if !self.graduation.contains(&crate_name) {
78            self.graduation.push(crate_name);
79        }
80    }
81
82    #[must_use]
83    pub fn remove(&mut self, crate_name: &str) -> bool {
84        let len_before = self.graduation.len();
85        self.graduation.retain(|x| x != crate_name);
86        self.graduation.len() != len_before
87    }
88
89    #[must_use]
90    pub fn contains(&self, crate_name: &str) -> bool {
91        self.graduation.iter().any(|x| x == crate_name)
92    }
93
94    pub fn iter(&self) -> impl Iterator<Item = &str> {
95        self.graduation.iter().map(String::as_str)
96    }
97
98    #[must_use]
99    pub fn is_empty(&self) -> bool {
100        self.graduation.is_empty()
101    }
102
103    #[must_use]
104    pub fn len(&self) -> usize {
105        self.graduation.len()
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    mod prerelease_state {
114        use super::*;
115
116        #[test]
117        fn new_creates_empty_state() {
118            let state = PrereleaseState::new();
119
120            assert!(state.is_empty());
121            assert_eq!(state.len(), 0);
122        }
123
124        #[test]
125        fn insert_and_get() {
126            let mut state = PrereleaseState::new();
127
128            state.insert("my-crate".to_string(), "alpha".to_string());
129
130            assert_eq!(state.get("my-crate"), Some("alpha"));
131            assert_eq!(state.len(), 1);
132            assert!(!state.is_empty());
133        }
134
135        #[test]
136        fn insert_overwrites_existing() {
137            let mut state = PrereleaseState::new();
138            state.insert("my-crate".to_string(), "alpha".to_string());
139
140            state.insert("my-crate".to_string(), "beta".to_string());
141
142            assert_eq!(state.get("my-crate"), Some("beta"));
143            assert_eq!(state.len(), 1);
144        }
145
146        #[test]
147        fn get_nonexistent_returns_none() {
148            let state = PrereleaseState::new();
149
150            assert_eq!(state.get("nonexistent"), None);
151        }
152
153        #[test]
154        fn remove_existing() {
155            let mut state = PrereleaseState::new();
156            state.insert("my-crate".to_string(), "alpha".to_string());
157
158            let removed = state.remove("my-crate");
159
160            assert_eq!(removed, Some("alpha".to_string()));
161            assert!(state.is_empty());
162        }
163
164        #[test]
165        fn remove_nonexistent_returns_none() {
166            let mut state = PrereleaseState::new();
167
168            let removed = state.remove("nonexistent");
169
170            assert_eq!(removed, None);
171        }
172
173        #[test]
174        fn contains() {
175            let mut state = PrereleaseState::new();
176            state.insert("my-crate".to_string(), "alpha".to_string());
177
178            assert!(state.contains("my-crate"));
179            assert!(!state.contains("other-crate"));
180        }
181
182        #[test]
183        fn iter() {
184            let mut state = PrereleaseState::new();
185            state.insert("crate-a".to_string(), "alpha".to_string());
186            state.insert("crate-b".to_string(), "beta".to_string());
187
188            let items: Vec<_> = state.iter().collect();
189
190            assert_eq!(items.len(), 2);
191            assert!(items.contains(&("crate-a", "alpha")));
192            assert!(items.contains(&("crate-b", "beta")));
193        }
194
195        #[test]
196        fn serialize_deserialize_roundtrip() {
197            let mut state = PrereleaseState::new();
198            state.insert("crate-a".to_string(), "alpha".to_string());
199            state.insert("crate-b".to_string(), "beta".to_string());
200
201            let serialized = toml::to_string(&state).expect("serialization should succeed");
202            let deserialized: PrereleaseState =
203                toml::from_str(&serialized).expect("deserialization should succeed");
204
205            assert_eq!(state, deserialized);
206        }
207
208        #[test]
209        fn deserialize_from_toml() {
210            let toml_content = r#"
211crate-a = "alpha"
212crate-b = "beta"
213"#;
214
215            let state: PrereleaseState =
216                toml::from_str(toml_content).expect("deserialization should succeed");
217
218            assert_eq!(state.get("crate-a"), Some("alpha"));
219            assert_eq!(state.get("crate-b"), Some("beta"));
220            assert_eq!(state.len(), 2);
221        }
222
223        #[test]
224        fn deserialize_empty() {
225            let toml_content = "";
226
227            let state: PrereleaseState =
228                toml::from_str(toml_content).expect("deserialization should succeed");
229
230            assert!(state.is_empty());
231        }
232    }
233
234    mod graduation_state {
235        use super::*;
236
237        #[test]
238        fn new_creates_empty_state() {
239            let state = GraduationState::new();
240
241            assert!(state.is_empty());
242            assert_eq!(state.len(), 0);
243        }
244
245        #[test]
246        fn add_single() {
247            let mut state = GraduationState::new();
248
249            state.add("my-crate".to_string());
250
251            assert!(state.contains("my-crate"));
252            assert_eq!(state.len(), 1);
253        }
254
255        #[test]
256        fn add_duplicate_is_ignored() {
257            let mut state = GraduationState::new();
258            state.add("my-crate".to_string());
259
260            state.add("my-crate".to_string());
261
262            assert_eq!(state.len(), 1);
263        }
264
265        #[test]
266        fn add_multiple() {
267            let mut state = GraduationState::new();
268
269            state.add("crate-a".to_string());
270            state.add("crate-b".to_string());
271
272            assert_eq!(state.len(), 2);
273            assert!(state.contains("crate-a"));
274            assert!(state.contains("crate-b"));
275        }
276
277        #[test]
278        fn remove_existing() {
279            let mut state = GraduationState::new();
280            state.add("my-crate".to_string());
281
282            let removed = state.remove("my-crate");
283
284            assert!(removed);
285            assert!(state.is_empty());
286        }
287
288        #[test]
289        fn remove_nonexistent_returns_false() {
290            let mut state = GraduationState::new();
291
292            let removed = state.remove("nonexistent");
293
294            assert!(!removed);
295        }
296
297        #[test]
298        fn contains() {
299            let mut state = GraduationState::new();
300            state.add("my-crate".to_string());
301
302            assert!(state.contains("my-crate"));
303            assert!(!state.contains("other-crate"));
304        }
305
306        #[test]
307        fn iter() {
308            let mut state = GraduationState::new();
309            state.add("crate-a".to_string());
310            state.add("crate-b".to_string());
311
312            let items: Vec<_> = state.iter().collect();
313
314            assert_eq!(items.len(), 2);
315            assert!(items.contains(&"crate-a"));
316            assert!(items.contains(&"crate-b"));
317        }
318
319        #[test]
320        fn serialize_deserialize_roundtrip() {
321            let mut state = GraduationState::new();
322            state.add("crate-a".to_string());
323            state.add("crate-b".to_string());
324
325            let serialized = toml::to_string(&state).expect("serialization should succeed");
326            let deserialized: GraduationState =
327                toml::from_str(&serialized).expect("deserialization should succeed");
328
329            assert_eq!(state, deserialized);
330        }
331
332        #[test]
333        fn deserialize_from_toml() {
334            let toml_content = r#"
335graduation = ["crate-a", "crate-b"]
336"#;
337
338            let state: GraduationState =
339                toml::from_str(toml_content).expect("deserialization should succeed");
340
341            assert!(state.contains("crate-a"));
342            assert!(state.contains("crate-b"));
343            assert_eq!(state.len(), 2);
344        }
345
346        #[test]
347        fn deserialize_empty() {
348            let toml_content = "";
349
350            let state: GraduationState =
351                toml::from_str(toml_content).expect("deserialization should succeed");
352
353            assert!(state.is_empty());
354        }
355
356        #[test]
357        fn deserialize_empty_array() {
358            let toml_content = "graduation = []";
359
360            let state: GraduationState =
361                toml::from_str(toml_content).expect("deserialization should succeed");
362
363            assert!(state.is_empty());
364        }
365    }
366}