1use std::collections::HashMap;
2use std::path::Path;
3use std::str::FromStr;
4
5use itertools::Itertools;
6use tokio::{fs::File, io::AsyncWriteExt};
7
8use crate::error::ChangesetParseError;
9use crate::version::{VersionMod, Versioned};
10
11#[derive(Debug, Default)]
12pub struct Changeset<T> {
13 pub packages: HashMap<String, VersionMod<T>>,
14 pub message: String,
15}
16
17impl<T> Changeset<T> {
18 fn find_changeset_start(
19 lines: &mut dyn Iterator<Item = &str>,
20 ) -> Result<(), ChangesetParseError> {
21 for line in lines {
22 match line {
23 "" => {}
24 "---" => return Ok(()),
25 _ => return Err(ChangesetParseError::HeaderNotFound),
26 }
27 }
28
29 Err(ChangesetParseError::HeaderNotFound)
30 }
31
32 fn parse_package_name(value: &str) -> &str {
33 if value.starts_with('\"') {
34 let mut chars = value.chars();
35 chars.next();
36 chars.next_back();
37 chars.as_str()
38 } else {
39 value
40 }
41 }
42}
43
44impl<T> Changeset<T>
45where
46 T: FromStr + Ord + Versioned,
47{
48 pub fn parse(value: &str) -> Result<Self, <Self as FromStr>::Err> {
49 Changeset::from_str(value)
50 }
51
52 pub async fn save<P: AsRef<Path>>(self, output: P) -> std::io::Result<()> {
53 let mut file = File::create(output).await?;
54
55 file.write_all(self.to_string().as_bytes()).await?;
56
57 Ok(())
58 }
59}
60
61impl<T> FromStr for Changeset<T>
62where
63 T: FromStr,
64{
65 type Err = ChangesetParseError;
66 fn from_str(value: &str) -> Result<Self, Self::Err> {
67 let mut packages = HashMap::new();
68 let mut lines = value.split('\n').map(|line| line.trim_end());
69
70 Self::find_changeset_start(&mut lines)?;
71
72 for line in &mut lines {
73 match line {
74 "---" => break,
75 value => {
76 let change_value: Vec<&str> = value.split(':').map(|val| val.trim()).collect();
77
78 match change_value.len() {
79 2 => {
80 let (package, version) = (
81 Self::parse_package_name(change_value[0]),
82 VersionMod::from_str(change_value[1]),
83 );
84
85 if let Ok(version) = version {
86 packages.insert(package.to_string(), version);
87 } else {
88 return Err(ChangesetParseError::HeaderParsing);
89 }
90 }
91 _ => return Err(ChangesetParseError::HeaderParsing),
92 }
93 }
94 }
95 }
96
97 Ok(Self {
98 packages,
99 message: lines.collect::<Vec<&str>>().join("\n").trim().to_owned(),
100 })
101 }
102}
103
104impl<T> ToString for Changeset<T>
105where
106 T: Versioned + Ord + ToString,
107{
108 fn to_string(&self) -> String {
109 let mut output = vec![];
110
111 output.extend(b"---\n");
112 for (package, version) in self.packages.iter().sorted() {
113 output.extend(format!("\"{}\": {}\n", package, version.to_string()).as_bytes())
114 }
115 output.extend(b"---\n\n");
116 output.extend(self.message.as_bytes());
117 output.push(b'\n');
118
119 String::from_utf8(output).unwrap()
120 }
121}
122
123#[cfg(test)]
124mod tests {
125
126 use super::*;
127 use crate::semantic::Semantic;
128
129 #[test]
130 fn from_str() {
131 let changeset = Changeset::from_str(
132 "
133---
134\"mol\": minor
135---
136
137Do cool stuff
138",
139 );
140
141 assert!(changeset.is_ok());
142
143 let changeset = changeset.unwrap();
144
145 assert_eq!(
146 changeset.packages,
147 vec![("mol".to_string(), VersionMod::new(Semantic::minor()))]
148 .into_iter()
149 .collect()
150 );
151 assert_eq!(changeset.message, "Do cool stuff");
152 }
153
154 #[test]
155 fn from_str_multiple() {
156 let changeset = Changeset::from_str(
157 "
158---
159\"mol\": minor
160\"mol-core\": major
161---
162
163Do cool stuff
164",
165 )
166 .unwrap();
167
168 assert_eq!(
169 changeset.packages,
170 vec![
171 ("mol".to_string(), VersionMod::new(Semantic::minor())),
172 ("mol-core".to_string(), VersionMod::new(Semantic::major()))
173 ]
174 .into_iter()
175 .collect()
176 );
177 }
178
179 #[test]
180 fn to_str() {
181 let changeset = Changeset {
182 packages: vec![("mol".to_owned(), VersionMod::new(Semantic::minor()))]
183 .into_iter()
184 .collect(),
185 message: "Do cool stuff".to_string(),
186 };
187
188 assert_eq!(
189 changeset.to_string(),
190 "---
191\"mol\": minor
192---
193
194Do cool stuff
195"
196 )
197 }
198
199 #[test]
200 fn to_str_multiple() {
201 let changeset = Changeset {
202 packages: vec![
203 ("mol".to_owned(), VersionMod::new(Semantic::minor())),
204 ("mol-core".to_owned(), VersionMod::new(Semantic::major())),
205 ]
206 .into_iter()
207 .collect(),
208 message: "Do cool stuff".to_string(),
209 };
210
211 assert_eq!(
212 changeset.to_string(),
213 "---
214\"mol\": minor
215\"mol-core\": major
216---
217
218Do cool stuff
219"
220 )
221 }
222}