Skip to main content

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, 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 RustChangelog {
104    /// Fetch all known releases from the official rust changelog
105    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}