logicaffeine_web/ui/pages/
roadmap_history.rs1use std::sync::LazyLock;
10use serde::Deserialize;
11
12#[derive(Deserialize, Clone, Debug, PartialEq)]
14pub struct Release {
15 pub version: String,
16 pub date: String,
17 pub title: String,
18 pub tagged: bool,
20 #[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
29pub fn get_history() -> &'static [Release] {
31 &HISTORY
32}
33
34pub 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}