rust_releases_rust_changelog/
lib.rs

1#![deny(missing_docs)]
2#![deny(clippy::all)]
3#![deny(unsafe_code)]
4//! Please, see the [`rust-releases`] for additional documentation on how this crate can be used.
5//!
6//! [`rust-releases`]: https://docs.rs/rust-releases
7use 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
22/// A [`Source`] which obtains release data from the official Rust changelog.
23///
24/// [`Source`]: rust_releases_core::Source
25pub struct RustChangelog {
26    source: Document,
27
28    /// Used to compare against the date of an unreleased version which does already exist in the
29    /// changelog. If this date is at least as late as the time found in a release registration, we
30    /// will say that such a version is released (i.e. published).
31    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
68/// Create a release from a `Version ...` header in the Rust changelog file (`RELEASES.md`).
69///
70/// We skip a few older versions which did not use full 3-component semver versions.
71/// While we could parse them as `SemverReq` requirements, adding those would not be worth the hassle
72///   (at least for now).
73///
74/// Versions which we should be able to parse, and are based on their release date available, are
75///   returned as `Some(Result<Release, Error>)`.
76/// If a version is not yet available based on their release date we return `None`.
77/// Versions we currently do not support are also returned as `None`.
78///
79/// The resulting releases can then be filtered on `Option::is_some`, to only keep relevant results.
80fn create_release(line: &str, today: &ReleaseDate) -> Option<RustChangelogResult<Release>> {
81    let parsed = parse_release(line.split_ascii_whitespace());
82
83    match parsed {
84        // If the version and date can be parsed, and the version has been released
85        Ok((version, date)) if date.is_available(today) && version.pre.is_empty() => {
86            Some(Ok(Release::new_stable(version)))
87        }
88        // If the version and date can be parsed, but the version is not yet released
89        Ok(_) => None,
90        // We skip versions 0.10, 0.9, etc. which require more lenient semver parsing
91        // Unfortunately we can't access the error kind, so we have to match the string instead
92        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        // In any ony other error case, we forward the error
99        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}