facti_lib/
changelog.rs

1use std::{
2    collections::{HashMap, HashSet},
3    fmt::{self, Display, Write},
4    str::FromStr,
5};
6
7use pest::Parser;
8use pest_derive::Parser;
9use serde::{Deserialize, Serialize};
10use thiserror::Error;
11
12use crate::version::Version;
13
14/// Version section start is a sequence of 99 dashes exactly.
15const SECTION_START: &str = "---------------------------------------------------------------------------------------------------";
16
17/// Contains all the sections part of a Factorio mod changelog.
18#[derive(Clone, Debug, Serialize, Deserialize)]
19pub struct Changelog {
20    pub sections: Vec<Section>,
21}
22
23#[derive(Clone, Debug, Serialize, Deserialize)]
24pub struct Section {
25    pub version: Version,
26    pub date: Option<String>,
27    pub categories: HashMap<CategoryType, HashSet<String>>,
28}
29
30#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
31pub enum CategoryType {
32    MajorFeatures,
33    Features,
34    MinorFeatures,
35    Graphics,
36    Sounds,
37    Optimizations,
38    Balancing,
39    CombatBalancing,
40    CircuitNetwork,
41    Changes,
42    Bugfixes,
43    Modding,
44    Scripting,
45    Gui,
46    Control,
47    Translation,
48    Debug,
49    EaseOfUse,
50    Info,
51    Locale,
52    Other(String),
53}
54
55#[derive(Parser)]
56#[grammar = "changelog/grammar.pest"]
57struct ChangelogParser;
58
59#[derive(Error, Debug)]
60pub enum ParseChangelogError {
61    #[error("Pest error when parsing")]
62    Pest(#[source] Box<pest::error::Error<Rule>>),
63}
64
65impl Changelog {
66    /// Parses a [`Changelog`] from a string.
67    ///
68    /// The given string must be a valid changelog as specified by
69    /// [the changelog format spec][spec].
70    ///
71    /// [spec]: https://wiki.factorio.com/Tutorial:Mod_changelog_format
72    pub fn parse<T: AsRef<str>>(s: T) -> Result<Self, ParseChangelogError> {
73        s.as_ref().parse()
74    }
75
76    /// Sorts the [`sections`][Changelog::sections] by version.
77    pub fn sort(&mut self) {
78        self.sections.sort_by(|a, b| b.version.cmp(&a.version));
79    }
80
81    /// Converts the [`Changelog`] to a string, with sorted sections according
82    /// to [`Section::version`].
83    pub fn to_string_sorted(&self) -> Result<String, fmt::Error> {
84        let mut sorted = self.to_owned();
85        sorted.sort();
86
87        let mut s = String::new();
88
89        for section in sorted.sections {
90            write!(s, "{}", section)?;
91        }
92
93        Ok(s)
94    }
95}
96
97impl FromStr for Changelog {
98    type Err = ParseChangelogError;
99
100    fn from_str(s: &str) -> Result<Self, Self::Err> {
101        let mut result = Self { sections: vec![] };
102
103        let changelog = ChangelogParser::parse(Rule::changelog, s)
104            .map_err(|e| ParseChangelogError::Pest(Box::new(e)))?
105            .next()
106            .unwrap();
107
108        for section_pair in changelog.into_inner() {
109            match section_pair.as_rule() {
110                Rule::section => {
111                    let mut inner_rules = section_pair.into_inner();
112                    let ver_str = inner_rules.next().unwrap().as_str();
113                    let version = Version::from_str(ver_str).unwrap();
114
115                    let mut section = Section {
116                        version,
117                        date: None,
118                        categories: HashMap::new(),
119                    };
120
121                    for remaining in inner_rules {
122                        match remaining.as_rule() {
123                            Rule::date => {
124                                section.date = Some(remaining.as_str().to_owned());
125                            }
126                            Rule::category => {
127                                let mut inner_rules = remaining.into_inner();
128                                let category_type =
129                                    CategoryType::from_str(inner_rules.next().unwrap().as_str())
130                                        .unwrap();
131                                let entries = section.categories.entry(category_type).or_default();
132
133                                for entry_pair in inner_rules {
134                                    match entry_pair.as_rule() {
135                                        Rule::entry => {
136                                            let str = entry_pair
137                                                .into_inner()
138                                                .map(|e| e.as_str())
139                                                .collect::<Vec<_>>()
140                                                .join("\n");
141
142                                            entries.insert(str);
143                                        }
144                                        _ => unreachable!(),
145                                    }
146                                }
147                            }
148                            _ => unreachable!(),
149                        }
150                    }
151
152                    result.sections.push(section);
153                }
154                Rule::EOI => (),
155                _ => unreachable!(),
156            }
157        }
158
159        result.sections.sort_by(|a, b| b.version.cmp(&a.version));
160
161        Ok(result)
162    }
163}
164
165impl Display for Changelog {
166    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167        for section in &self.sections {
168            write!(f, "{}", section)?;
169        }
170
171        Ok(())
172    }
173}
174
175impl Display for Section {
176    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177        writeln!(f, "{}\nVersion: {}", SECTION_START, self.version)?;
178
179        if let Some(date) = &self.date {
180            writeln!(f, "Date: {}", date)?;
181        }
182
183        for (category, entries) in &self.categories {
184            writeln!(f, "  {}:", category)?;
185
186            for entry in entries {
187                let mut lines = entry.lines();
188                if let Some(line) = lines.next() {
189                    writeln!(f, "    - {}", line)?;
190                }
191                for line in lines {
192                    writeln!(f, "      {}", line)?;
193                }
194            }
195        }
196
197        Ok(())
198    }
199}
200
201impl FromStr for CategoryType {
202    type Err = String;
203
204    fn from_str(s: &str) -> Result<Self, Self::Err> {
205        use CategoryType::*;
206        Ok(match s {
207            "Major Features" => MajorFeatures,
208            "Features" => Features,
209            "Minor Features" => MinorFeatures,
210            "Graphics" => Graphics,
211            "Sounds" => Sounds,
212            "Optimizations" => Optimizations,
213            "Balancing" => Balancing,
214            "Combat Balancing" => CombatBalancing,
215            "Circuit Network" => CircuitNetwork,
216            "Changes" => Changes,
217            "Bugfixes" => Bugfixes,
218            "Modding" => Modding,
219            "Scripting" => Scripting,
220            "Gui" => Gui,
221            "Control" => Control,
222            "Translation" => Translation,
223            "Debug" => Debug,
224            "Ease of use" => EaseOfUse,
225            "Info" => Info,
226            "Locale" => Locale,
227            o => Other(o.to_owned()),
228        })
229    }
230}
231
232impl Display for CategoryType {
233    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234        use CategoryType::*;
235        f.write_str(match self {
236            MajorFeatures => "Major Features",
237            Features => "Features",
238            MinorFeatures => "Minor Features",
239            Graphics => "Graphics",
240            Sounds => "Sounds",
241            Optimizations => "Optimizations",
242            Balancing => "Balancing",
243            CombatBalancing => "Combat Balancing",
244            CircuitNetwork => "Circuit Network",
245            Changes => "Changes",
246            Bugfixes => "Bugfixes",
247            Modding => "Modding",
248            Scripting => "Scripting",
249            Gui => "Gui",
250            Control => "Control",
251            Translation => "Translation",
252            Debug => "Debug",
253            EaseOfUse => "Ease of use",
254            Info => "Info",
255            Locale => "Locale",
256            Other(o) => o,
257        })
258    }
259}