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
14const SECTION_START: &str = "---------------------------------------------------------------------------------------------------";
16
17#[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 pub fn parse<T: AsRef<str>>(s: T) -> Result<Self, ParseChangelogError> {
73 s.as_ref().parse()
74 }
75
76 pub fn sort(&mut self) {
78 self.sections.sort_by(|a, b| b.version.cmp(&a.version));
79 }
80
81 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}