mol_core/
changeset.rs

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}