rust_releases_rust_changelog/
lib.rs1#![deny(missing_docs)]
2#![deny(clippy::all)]
3#![deny(unsafe_code)]
4use rust_releases_core::{semver, Channel, Release, ReleaseIndex, Source};
8use rust_releases_io::Document;
9#[cfg(test)]
10#[macro_use]
11extern crate rust_releases_io;
12
13pub(crate) mod errors;
14pub(crate) mod fetch;
15
16use crate::fetch::fetch;
17
18pub use errors::{RustChangelogError, RustChangelogResult};
19use std::str::FromStr;
20use time::macros::format_description;
21
22pub struct RustChangelog {
26 source: Document,
27
28 today: ReleaseDate,
32}
33
34impl RustChangelog {
35 pub(crate) fn from_document(source: Document) -> Self {
36 Self {
37 source,
38 today: ReleaseDate::today(),
39 }
40 }
41
42 #[cfg(test)]
43 pub(crate) fn from_document_with_date(source: Document, date: ReleaseDate) -> Self {
44 Self {
45 source,
46 today: date,
47 }
48 }
49}
50
51impl Source for RustChangelog {
52 type Error = RustChangelogError;
53
54 fn build_index(&self) -> Result<ReleaseIndex, Self::Error> {
55 let buffer = self.source.buffer();
56 let content = std::str::from_utf8(buffer).map_err(RustChangelogError::UnrecognizedText)?;
57
58 let releases = content
59 .lines()
60 .filter(|s| s.starts_with("Version"))
61 .filter_map(|line| create_release(line, &self.today))
62 .collect::<Result<ReleaseIndex, Self::Error>>()?;
63
64 Ok(releases)
65 }
66}
67
68fn create_release(line: &str, today: &ReleaseDate) -> Option<RustChangelogResult<Release>> {
81 let parsed = parse_release(line.split_ascii_whitespace());
82
83 match parsed {
84 Ok((version, date)) if date.is_available(today) && version.pre.is_empty() => {
86 Some(Ok(Release::new_stable(version)))
87 }
88 Ok(_) => None,
90 Err(RustChangelogError::SemverError(err, _))
93 if err.to_string().as_str()
94 == "unexpected end of input while parsing minor version number" =>
95 {
96 None
97 }
98 Err(err) => Some(Err(err)),
100 }
101}
102
103impl RustChangelog {
104 pub fn fetch_channel(channel: Channel) -> Result<Self, RustChangelogError> {
106 if let Channel::Stable = channel {
107 let document = fetch()?;
108 Ok(Self::from_document(document))
109 } else {
110 Err(RustChangelogError::ChannelNotAvailable(channel))
111 }
112 }
113}
114
115fn parse_release<'line>(
116 mut parts: impl Iterator<Item = &'line str>,
117) -> Result<(semver::Version, ReleaseDate), RustChangelogError> {
118 let version_number = parts
119 .nth(1)
120 .ok_or(RustChangelogError::NoVersionInChangelogItem)?;
121 let release_date = parts
122 .next()
123 .ok_or(RustChangelogError::NoDateInChangelogItem)?;
124
125 let version = semver::Version::parse(version_number)
126 .map_err(|err| RustChangelogError::SemverError(err, version_number.to_string()))?;
127
128 let date = ReleaseDate::parse(&release_date[1..release_date.len() - 1])?;
129
130 Ok((version, date))
131}
132
133#[derive(Debug)]
134struct ReleaseDate(time::Date);
135
136impl ReleaseDate {
137 fn today() -> Self {
138 let date = time::OffsetDateTime::now_utc().date();
139
140 Self(date)
141 }
142
143 fn parse(from: &str) -> Result<Self, RustChangelogError> {
144 from.parse::<ReleaseDate>()
145 }
146
147 fn is_available(&self, today: &Self) -> bool {
148 today.0 >= self.0
149 }
150}
151
152impl FromStr for ReleaseDate {
153 type Err = crate::RustChangelogError;
154
155 fn from_str(item: &str) -> Result<Self, Self::Err> {
156 let format = format_description!("[year]-[month]-[day]");
157
158 let result = time::Date::parse(item.trim(), &format)
159 .map_err(|err| RustChangelogError::TimeParseError(item.to_string(), err))?;
160
161 Ok(Self(result))
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::ReleaseDate;
168 use crate::RustChangelog;
169 use rust_releases_core::{semver, Channel, Release, ReleaseIndex};
170 use rust_releases_io::Document;
171 use std::fs;
172 use time::macros::date;
173 use yare::parameterized;
174
175 #[test]
176 fn source_dist_index() {
177 let path = [
178 env!("CARGO_MANIFEST_DIR"),
179 "/../../resources/rust_changelog/RELEASES.md",
180 ]
181 .join("");
182
183 let buffer = fs::read(path).unwrap();
184 let document = Document::new(buffer);
185
186 let strategy = RustChangelog::from_document(document);
187 let index = ReleaseIndex::from_source(strategy).unwrap();
188
189 assert!(index.releases().len() > 50);
190 assert_eq!(
191 index.releases()[0],
192 Release::new_stable(semver::Version::new(1, 50, 0))
193 );
194 }
195
196 #[test]
197 fn parse_date() {
198 let date = ReleaseDate::parse("2021-09-01").unwrap();
199 let expected = date!(2021 - 09 - 01);
200 assert_eq!(date.0, expected);
201 }
202
203 #[test]
204 fn with_unreleased_version() {
205 let path = [
206 env!("CARGO_MANIFEST_DIR"),
207 "/../../resources/rust_changelog/RELEASES_with_unreleased.md",
208 ]
209 .join("");
210 let buffer = fs::read(path).unwrap();
211 let document = Document::new(buffer);
212
213 let date = ReleaseDate::parse("2021-09-01").unwrap();
214 let strategy = RustChangelog::from_document_with_date(document, date);
215 let index = ReleaseIndex::from_source(strategy).unwrap();
216
217 let mut releases = index.releases().iter();
218
219 assert_eq!(
220 releases.next().unwrap().version(),
221 &semver::Version::new(1, 54, 0)
222 );
223 }
224
225 #[parameterized(
226 beta = { Channel::Beta },
227 nightly = { Channel::Nightly },
228 )]
229 fn fetch_unsupported_channel(channel: Channel) {
230 __internal_dl_test!({
231 let file = RustChangelog::fetch_channel(channel);
232 assert!(file.is_err());
233 })
234 }
235
236 #[test]
237 fn fetch_supported_channel() {
238 __internal_dl_test!({
239 let file = RustChangelog::fetch_channel(Channel::Stable);
240 assert!(file.is_ok());
241 })
242 }
243}