Skip to main content

logicaffeine_web/ui/pages/
roadmap_history.rs

1//! Release-history data for the roadmap page.
2//!
3//! Generated from `CHANGELOG.md` + git tags by `scripts/generate-roadmap.sh`
4//! into `roadmap_history.json`, then baked at compile time — mirroring how
5//! [`benchmarks`](super::benchmarks) bakes `latest.json`. The roadmap renders
6//! one terse row per release; clicking a release opens its news article when
7//! one exists ([`news_slug_for`]).
8
9use std::sync::LazyLock;
10use serde::Deserialize;
11
12/// One released (or prepared) version. Newest-first in [`get_history`].
13#[derive(Deserialize, Clone, Debug, PartialEq)]
14pub struct Release {
15    pub version: String,
16    pub date: String,
17    pub title: String,
18    /// Whether a `v<version>` git tag exists. `false` = prepared, not yet cut.
19    pub tagged: bool,
20    /// Release-plumbing release (CI, benchmark infra, deploy, tooling, skipped).
21    /// Collapsed by default on the roadmap.
22    #[serde(default)]
23    pub maintenance: bool,
24}
25
26static HISTORY: LazyLock<Vec<Release>> =
27    LazyLock::new(|| serde_json::from_str(include_str!("roadmap_history.json")).unwrap());
28
29/// All releases, newest-first.
30pub fn get_history() -> &'static [Release] {
31    &HISTORY
32}
33
34/// The news-article slug for a release, if one exists.
35///
36/// Release articles are titled `v<version> — …`, so the first whitespace token
37/// is matched exactly — `v0.9.1` must not prefix-match `v0.9.16`.
38pub fn news_slug_for(version: &str) -> Option<&'static str> {
39    let token = format!("v{version}");
40    crate::ui::pages::news::get_articles()
41        .into_iter()
42        .find(|a| a.title.split_whitespace().next() == Some(token.as_str()))
43        .map(|a| a.slug)
44}
45
46#[cfg(test)]
47mod tests {
48    use super::*;
49
50    #[test]
51    fn history_parses_and_is_nonempty() {
52        assert!(!get_history().is_empty(), "roadmap_history.json deserialized to no releases");
53    }
54
55    #[test]
56    fn every_release_is_well_formed() {
57        for r in get_history() {
58            assert!(!r.version.is_empty(), "release with empty version");
59            assert!(!r.date.is_empty(), "release {} has empty date", r.version);
60            assert!(!r.title.is_empty(), "release {} has empty title", r.version);
61        }
62    }
63
64    #[test]
65    fn newest_first_ordering() {
66        let h = get_history();
67        assert!(h.len() >= 2, "expected at least two releases");
68        let parse = |v: &str| -> Vec<u32> { v.split('.').filter_map(|p| p.parse().ok()).collect() };
69        assert!(
70            parse(&h[0].version) >= parse(&h[1].version),
71            "history not newest-first: {} before {}",
72            h[0].version,
73            h[1].version
74        );
75    }
76
77    #[test]
78    fn tagged_releases_with_articles_resolve_a_slug() {
79        let any = get_history().iter().any(|r| news_slug_for(&r.version).is_some());
80        assert!(any, "no release resolved a news-article slug — version→slug matcher is broken");
81    }
82}