1#![warn(missing_docs)]
2
3use anyhow::anyhow;
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9use serde_with::{KeyValueMap, serde_as};
10
11#[serde_as]
14#[derive(Debug, Deserialize, Serialize, JsonSchema)]
15#[serde(deny_unknown_fields)]
16pub struct Changelog {
17 pub title: String,
19 pub description: String,
22 pub repository: String,
24 pub unreleased: Changes,
26 #[serde_as(as = "KeyValueMap<_>")]
28 pub versions: Vec<Version>,
29}
30
31#[derive(Debug, Default, Deserialize, Serialize, JsonSchema)]
33#[serde(deny_unknown_fields)]
34pub struct Version {
35 #[serde(rename = "$key$")]
37 pub version: String,
38 pub tag: String,
40 #[schemars(regex(pattern = r"^\d{4}-[01]\d-[0-3]\d$"))]
42 pub date: String,
43 #[serde(skip_serializing_if = "Option::is_none")]
45 pub description: Option<String>,
46 #[serde(default, skip_serializing_if = "Option::is_none")]
48 pub yanked: Option<String>,
49 #[serde(flatten)]
51 pub changes: Changes,
52}
53
54#[derive(Debug, Default, Deserialize, Serialize, JsonSchema, PartialEq)]
56#[serde(deny_unknown_fields)]
57pub struct Changes {
58 #[serde(default, skip_serializing_if = "Vec::is_empty")]
60 pub added: Vec<String>,
61 #[serde(default, skip_serializing_if = "Vec::is_empty")]
63 pub changed: Vec<String>,
64 #[serde(default, skip_serializing_if = "Vec::is_empty")]
66 pub deprecated: Vec<String>,
67 #[serde(default, skip_serializing_if = "Vec::is_empty")]
69 pub removed: Vec<String>,
70 #[serde(default, skip_serializing_if = "Vec::is_empty")]
72 pub fixed: Vec<String>,
73 #[serde(default, skip_serializing_if = "Vec::is_empty")]
75 pub security: Vec<String>,
76}
77
78impl Changes {
79 pub fn push_added(&mut self, change: String) {
81 self.added.push(change)
82 }
83 pub fn push_changed(&mut self, change: String) {
85 self.changed.push(change)
86 }
87 pub fn push_deprecated(&mut self, change: String) {
89 self.deprecated.push(change)
90 }
91 pub fn push_fixed(&mut self, change: String) {
93 self.fixed.push(change)
94 }
95 pub fn push_removed(&mut self, change: String) {
97 self.removed.push(change)
98 }
99 pub fn push_security(&mut self, change: String) {
101 self.security.push(change)
102 }
103
104 pub fn is_empty(&self) -> bool {
106 self.added.is_empty()
107 && self.changed.is_empty()
108 && self.deprecated.is_empty()
109 && self.fixed.is_empty()
110 && self.removed.is_empty()
111 && self.security.is_empty()
112 }
113
114 fn write_changes_if_exist(
116 &self,
117 f: &mut std::fmt::Formatter<'_>,
118 title: &str,
119 changes: &Vec<String>,
120 ) -> std::fmt::Result {
121 if !changes.is_empty() {
122 writeln!(f)?;
123 writeln!(f, "### {}", title)?;
124 writeln!(f)?;
125 for change in changes {
126 writeln!(f, "- {}", change)?;
127 }
128 }
129 Ok(())
130 }
131}
132
133impl std::fmt::Display for Changelog {
134 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135 writeln!(f, "# {}", self.title)?;
136 writeln!(f)?;
137 writeln!(f, "{}", self.description)?;
138 if !self.description.ends_with("\n") {
139 writeln!(f)?;
140 }
141 if !self.unreleased.is_empty() {
142 writeln!(f, "## [Unreleased]")?;
143 writeln!(f, "{}", self.unreleased)?;
144 }
145
146 for version in &self.versions {
147 write!(f, "{}", version)?;
148 }
149
150 writeln!(f)?;
151 writeln!(f, "# Revisions")?;
152 writeln!(f)?;
153 match &self.versions[..] {
154 [] => writeln!(f, "- [unreleased] <{}/commits/>", self.repository)?,
156
157 versions @ [.., last] => {
158 writeln!(
159 f,
160 "- [unreleased] <{}/compare/{}...HEAD>",
161 self.repository, versions[0].tag
162 )?;
163 for idx in 0..(versions.len() - 1) {
164 writeln!(
165 f,
166 "- [{}] <{}/compare/{}..{}>",
167 versions[idx].version,
168 self.repository,
169 versions[idx + 1].tag,
170 versions[idx].tag,
171 )?;
172 }
173 writeln!(
175 f,
176 "- [{}] <{}/commits/{}>",
177 last.version, self.repository, last.tag
178 )?;
179 }
180 };
181
182 Ok(())
183 }
184}
185
186impl std::fmt::Display for Version {
187 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188 write!(f, "## {} - {}", self.version, self.date)?;
189 if let Some(reason) = &self.yanked {
190 write!(f, " [YANKED] {}", reason)?;
191 }
192 writeln!(f)?;
193 writeln!(f)?;
194 if let Some(desc) = &self.description {
195 writeln!(f, "{}", desc.trim())?;
196 }
197 if !self.changes.is_empty() {
198 writeln!(f, "{}", self.changes)?;
199 }
200
201 Ok(())
202 }
203}
204
205impl std::fmt::Display for Changes {
206 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
207 self.write_changes_if_exist(f, "Added", &self.added)?;
208 self.write_changes_if_exist(f, "Changed", &self.changed)?;
209 self.write_changes_if_exist(f, "Deprecated", &self.deprecated)?;
210 self.write_changes_if_exist(f, "Removed", &self.removed)?;
211 self.write_changes_if_exist(f, "Fixed", &self.fixed)?;
212 self.write_changes_if_exist(f, "Security", &self.security)?;
213
214 Ok(())
215 }
216}
217
218impl Changelog {
219 pub fn from_path(path: impl Into<std::path::PathBuf>) -> anyhow::Result<Changelog> {
223 let path = path.into();
224
225 if !path.exists() {
226 return Err(anyhow!("no such file {}", path.display()));
227 }
228
229 match path.extension().map(|e| e.to_ascii_lowercase()) {
230 Some(e) if e == "yml" || e == "yaml" => {
231 let s = &std::fs::read_to_string(path)?;
232 Self::from_yaml(s)
233 }
234 Some(e) if e == "toml" => {
235 let s = &std::fs::read_to_string(path)?;
236 Self::from_toml(s)
237 }
238 Some(e) if e == "json" => {
239 let s = &std::fs::read_to_string(path)?;
240 Self::from_json(s)
241 }
242 Some(e) => Err(anyhow!("Invalid file extension {}", e.to_string_lossy())),
243 None => Err(anyhow!(
244 "Unable to read {} without an extension",
245 path.display()
246 )),
247 }
248 }
249
250 pub fn from_yaml(s: &str) -> anyhow::Result<Changelog> {
252 let de = serde_yml::Deserializer::from_str(s);
253 Ok(serde_path_to_error::deserialize(de)?)
254 }
255
256 pub fn from_json(s: &str) -> anyhow::Result<Changelog> {
258 let mut de = serde_json::Deserializer::from_str(s);
259 Ok(serde_path_to_error::deserialize(&mut de)?)
260 }
261
262 pub fn from_toml(s: &str) -> anyhow::Result<Changelog> {
264 let de = toml::Deserializer::new(s);
265 Ok(serde_path_to_error::deserialize(de)?)
266 }
267
268 pub fn to_yaml(&self) -> anyhow::Result<String> {
270 Ok(serde_yml::to_string(&self)?)
271 }
272
273 pub fn to_toml(&self) -> anyhow::Result<String> {
275 Ok(toml::to_string_pretty(&self)?)
276 }
277
278 pub fn to_json(&self) -> anyhow::Result<String> {
280 Ok(serde_json::to_string_pretty(&self)? + "\n")
281 }
282}
283
284impl Default for Changelog {
285 fn default() -> Self {
286 Self {
287 title: "Changelog".into(),
288 description: r#"All notable changes to this project will be documented in this file.
289
290The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
291and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
292"#
293 .into(),
294 repository: "https://github.com/me/my-swanky-project".into(),
295 unreleased: Changes {
296 added: vec"
298 .to_string(),
299 ],
300 ..Default::default()
301 },
302 versions: vec![],
303 }
304 }
305}