rust_releases_rust_changelog/
lib.rs1#![deny(missing_docs)]
2#![deny(clippy::all)]
3#![deny(unsafe_code)]
4use rust_releases_core::{semver, Channel, FetchResources, 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 FetchResources for RustChangelog {
104 type Error = RustChangelogError;
105
106 fn fetch_channel(channel: Channel) -> Result<Self, Self::Error> {
107 if let Channel::Stable = channel {
108 let document = fetch()?;
109 Ok(Self::from_document(document))
110 } else {
111 Err(RustChangelogError::ChannelNotAvailable(channel))
112 }
113 }
114}
115
116fn parse_release<'line>(
117 mut parts: impl Iterator<Item = &'line str>,
118) -> Result<(semver::Version, ReleaseDate), RustChangelogError> {
119 let version_number = parts
120 .nth(1)
121 .ok_or(RustChangelogError::NoVersionInChangelogItem)?;
122 let release_date = parts
123 .next()
124 .ok_or(RustChangelogError::NoDateInChangelogItem)?;
125
126 let version = semver::Version::parse(version_number)
127 .map_err(|err| RustChangelogError::SemverError(err, version_number.to_string()))?;
128
129 let date = ReleaseDate::parse(&release_date[1..release_date.len() - 1])?;
130
131 Ok((version, date))
132}
133
134#[derive(Debug)]
135struct ReleaseDate(time::Date);
136
137impl ReleaseDate {
138 fn today() -> Self {
139 let date = time::OffsetDateTime::now_utc().date();
140
141 Self(date)
142 }
143
144 fn parse(from: &str) -> Result<Self, RustChangelogError> {
145 from.parse::<ReleaseDate>()
146 }
147
148 fn is_available(&self, today: &Self) -> bool {
149 today.0 >= self.0
150 }
151}
152
153impl FromStr for ReleaseDate {
154 type Err = crate::RustChangelogError;
155
156 fn from_str(item: &str) -> Result<Self, Self::Err> {
157 let format = format_description!("[year]-[month]-[day]");
158
159 let result = time::Date::parse(item.trim(), &format)
160 .map_err(|err| RustChangelogError::TimeParseError(item.to_string(), err))?;
161
162 Ok(Self(result))
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::ReleaseDate;
169 use crate::RustChangelog;
170 use rust_releases_core::{semver, Channel, FetchResources, Release, ReleaseIndex};
171 use rust_releases_io::Document;
172 use std::fs;
173 use time::macros::date;
174 use yare::parameterized;
175
176 #[test]
177 fn source_dist_index() {
178 let path = [
179 env!("CARGO_MANIFEST_DIR"),
180 "/../../resources/rust_changelog/RELEASES.md",
181 ]
182 .join("");
183
184 let buffer = fs::read(path).unwrap();
185 let document = Document::new(buffer);
186
187 let strategy = RustChangelog::from_document(document);
188 let index = ReleaseIndex::from_source(strategy).unwrap();
189
190 assert!(index.releases().len() > 50);
191 assert_eq!(
192 index.releases()[0],
193 Release::new_stable(semver::Version::new(1, 50, 0))
194 );
195 }
196
197 #[test]
198 fn parse_date() {
199 let date = ReleaseDate::parse("2021-09-01").unwrap();
200 let expected = date!(2021 - 09 - 01);
201 assert_eq!(date.0, expected);
202 }
203
204 #[test]
205 fn with_unreleased_version() {
206 let path = [
207 env!("CARGO_MANIFEST_DIR"),
208 "/../../resources/rust_changelog/RELEASES_with_unreleased.md",
209 ]
210 .join("");
211 let buffer = fs::read(path).unwrap();
212 let document = Document::new(buffer);
213
214 let date = ReleaseDate::parse("2021-09-01").unwrap();
215 let strategy = RustChangelog::from_document_with_date(document, date);
216 let index = ReleaseIndex::from_source(strategy).unwrap();
217
218 let mut releases = index.releases().iter();
219
220 assert_eq!(
221 releases.next().unwrap().version(),
222 &semver::Version::new(1, 54, 0)
223 );
224 }
225
226 #[parameterized(
227 beta = { Channel::Beta },
228 nightly = { Channel::Nightly },
229 )]
230 fn fetch_unsupported_channel(channel: Channel) {
231 __internal_dl_test!({
232 let file = RustChangelog::fetch_channel(channel);
233 assert!(file.is_err());
234 })
235 }
236
237 #[test]
238 fn fetch_supported_channel() {
239 __internal_dl_test!({
240 let file = RustChangelog::fetch_channel(Channel::Stable);
241 assert!(file.is_ok());
242 })
243 }
244}