Skip to main content

quanttide_devops/source/roadmap/
mod.rs

1//! ROADMAP 解析:从 `ROADMAP.md` 提取版本规划进度。
2//!
3//! 遵循 Keep a Changelog 变体格式,支持解析、进度统计和格式验证。
4
5pub(crate) mod parse;
6pub(crate) mod validate;
7
8// ═══════════════════════════════════════════════════════════════════════
9// 错误类型
10// ═══════════════════════════════════════════════════════════════════════
11
12/// ROADMAP 操作错误。
13#[derive(thiserror::Error, Debug)]
14pub enum RoadmapError {
15    /// 文件读取失败。
16    #[error("读取 ROADMAP 失败: {0}")]
17    Io(#[from] std::io::Error),
18    /// 解析失败(格式不符合预期)。
19    #[error("解析 ROADMAP 失败: {0}")]
20    Parse(String),
21}
22
23// ═══════════════════════════════════════════════════════════════════════
24// 公共类型
25// ═══════════════════════════════════════════════════════════════════════
26
27/// 单个 checklist 条目。
28#[derive(Debug, Clone, PartialEq)]
29pub struct RoadmapChecklistItem {
30    /// 描述文本。
31    pub description: String,
32    /// 是否已勾选(`[x]`)。
33    pub completed: bool,
34}
35
36/// 进度统计。
37#[derive(Debug, Clone, PartialEq)]
38pub struct RoadmapProgress {
39    /// 总条目数。
40    pub total: usize,
41    /// 已完成条目数。
42    pub completed: usize,
43}
44
45impl RoadmapProgress {
46    /// 完成百分比(0.0 ~ 100.0)。无条目时返回 100.0。
47    pub fn percent(&self) -> f64 {
48        if self.total == 0 {
49            return 100.0;
50        }
51        (self.completed as f64 / self.total as f64) * 100.0
52    }
53}
54
55/// 单版本的规划进度。
56#[derive(Debug, Clone, PartialEq)]
57pub struct RoadmapVersion {
58    /// 版本号(如 `"0.1.5"`)。
59    pub version: String,
60    /// 状态标签(如 `"待实施"`、`"已发布"`)。
61    pub status: String,
62    /// 已完成条目数。
63    pub done: usize,
64    /// 总条目数。
65    pub total: usize,
66    /// 分类分组:`(分类名, 条目列表)`。
67    pub categories: Vec<(String, Vec<RoadmapChecklistItem>)>,
68}
69
70impl RoadmapVersion {
71    /// 完成百分比(0.0 ~ 100.0)。无条目时返回 100.0。
72    pub fn percent(&self) -> f64 {
73        if self.total == 0 {
74            return 100.0;
75        }
76        (self.done as f64 / self.total as f64) * 100.0
77    }
78}
79
80/// 格式验证发现的单个问题。
81#[derive(Debug, Clone, PartialEq)]
82pub struct RoadmapIssue {
83    /// 问题所在行号(1-based)。
84    pub line: usize,
85    /// 问题所属 scope。
86    pub scope: String,
87    /// 问题描述。
88    pub message: String,
89}
90
91/// 解析后的 ROADMAP.md 文档。
92#[derive(Debug, Clone, PartialEq)]
93pub struct Roadmap {
94    /// 原始文本。
95    #[allow(dead_code)]
96    pub(crate) raw: String,
97    /// 所有版本区块(自上而下 = 最新优先)。
98    pub(crate) versions: Vec<RoadmapVersion>,
99}
100
101// ═══════════════════════════════════════════════════════════════════════
102// 公共访问器
103// ═══════════════════════════════════════════════════════════════════════
104
105impl Roadmap {
106    /// 获取所有版本的规划进度。
107    pub fn versions(&self) -> &[RoadmapVersion] {
108        &self.versions
109    }
110
111    /// 总已完成条目数。
112    pub fn total_done(&self) -> usize {
113        self.versions.iter().map(|v| v.done).sum()
114    }
115
116    /// 总条目数。
117    pub fn total_all(&self) -> usize {
118        self.versions.iter().map(|v| v.total).sum()
119    }
120}
121
122// ═══════════════════════════════════════════════════════════════════════
123// 内部构建器
124// ═══════════════════════════════════════════════════════════════════════
125
126pub(crate) struct RoadmapVersionBuilder {
127    pub(crate) version: String,
128    pub(crate) status: String,
129    pub(crate) categories: Vec<(String, Vec<RoadmapChecklistItem>)>,
130}
131
132impl RoadmapVersionBuilder {
133    pub(crate) fn new(version: String, status: String) -> Self {
134        Self {
135            version,
136            status,
137            categories: Vec::new(),
138        }
139    }
140
141    pub(crate) fn add_category(&mut self, name: String) {
142        self.categories.push((name, Vec::new()));
143    }
144
145    pub(crate) fn add_issue(&mut self, completed: bool, description: String) {
146        if let Some(last) = self.categories.last_mut() {
147            last.1.push(RoadmapChecklistItem {
148                description,
149                completed,
150            });
151        }
152    }
153
154    pub(crate) fn build(self) -> RoadmapVersion {
155        let total: usize = self.categories.iter().map(|(_, items)| items.len()).sum();
156        let done: usize = self
157            .categories
158            .iter()
159            .flat_map(|(_, items)| items)
160            .filter(|i| i.completed)
161            .count();
162
163        RoadmapVersion {
164            version: self.version,
165            status: self.status,
166            done,
167            total,
168            categories: self.categories,
169        }
170    }
171}
172
173// ═══════════════════════════════════════════════════════════════════════
174// 测试
175// ═══════════════════════════════════════════════════════════════════════
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    // ── Error display ────────────────────────────────────────────
182
183    #[test]
184    fn test_roadmap_error_display() {
185        let err = RoadmapError::Io(std::io::Error::new(
186            std::io::ErrorKind::NotFound,
187            "not found",
188        ));
189        assert!(err.to_string().contains("读取 ROADMAP 失败"));
190
191        let err = RoadmapError::Parse("bad format".into());
192        assert!(err.to_string().contains("解析 ROADMAP 失败"));
193    }
194
195    // ── RoadmapVersion percent ───────────────────────────────────
196
197    #[test]
198    fn test_version_percent() {
199        let v = RoadmapVersion {
200            version: "0.1.0".into(),
201            status: "test".into(),
202            done: 2,
203            total: 4,
204            categories: Vec::new(),
205        };
206        assert!((v.percent() - 50.0).abs() < f64::EPSILON);
207    }
208
209    #[test]
210    fn test_version_percent_empty() {
211        let v = RoadmapVersion {
212            version: "0.1.0".into(),
213            status: "test".into(),
214            done: 0,
215            total: 0,
216            categories: Vec::new(),
217        };
218        assert!((v.percent() - 100.0).abs() < f64::EPSILON);
219    }
220
221    #[test]
222    fn test_version_percent_all_done() {
223        let v = RoadmapVersion {
224            version: "0.1.0".into(),
225            status: "test".into(),
226            done: 3,
227            total: 3,
228            categories: Vec::new(),
229        };
230        assert!((v.percent() - 100.0).abs() < f64::EPSILON);
231    }
232
233    #[test]
234    fn test_version_percent_none_done() {
235        let v = RoadmapVersion {
236            version: "0.1.0".into(),
237            status: "test".into(),
238            done: 0,
239            total: 5,
240            categories: Vec::new(),
241        };
242        assert!((v.percent() - 0.0).abs() < f64::EPSILON);
243    }
244
245    // ── RoadmapProgress percent ──────────────────────────────────
246
247    #[test]
248    fn test_progress_percent() {
249        let p = RoadmapProgress {
250            total: 4,
251            completed: 2,
252        };
253        assert!((p.percent() - 50.0).abs() < f64::EPSILON);
254    }
255
256    #[test]
257    fn test_progress_empty() {
258        let p = RoadmapProgress {
259            total: 0,
260            completed: 0,
261        };
262        assert!((p.percent() - 100.0).abs() < f64::EPSILON);
263    }
264}