quanttide_devops/source/roadmap/
mod.rs1pub(crate) mod parse;
6pub(crate) mod validate;
7
8#[derive(thiserror::Error, Debug)]
14pub enum RoadmapError {
15 #[error("读取 ROADMAP 失败: {0}")]
17 Io(#[from] std::io::Error),
18 #[error("解析 ROADMAP 失败: {0}")]
20 Parse(String),
21}
22
23#[derive(Debug, Clone, PartialEq)]
29pub struct RoadmapChecklistItem {
30 pub description: String,
32 pub completed: bool,
34}
35
36#[derive(Debug, Clone, PartialEq)]
38pub struct RoadmapProgress {
39 pub total: usize,
41 pub completed: usize,
43}
44
45impl RoadmapProgress {
46 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#[derive(Debug, Clone, PartialEq)]
57pub struct RoadmapVersion {
58 pub version: String,
60 pub status: String,
62 pub done: usize,
64 pub total: usize,
66 pub categories: Vec<(String, Vec<RoadmapChecklistItem>)>,
68}
69
70impl RoadmapVersion {
71 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#[derive(Debug, Clone, PartialEq)]
82pub struct RoadmapIssue {
83 pub line: usize,
85 pub scope: String,
87 pub message: String,
89}
90
91#[derive(Debug, Clone, PartialEq)]
93pub struct Roadmap {
94 #[allow(dead_code)]
96 pub(crate) raw: String,
97 pub(crate) versions: Vec<RoadmapVersion>,
99}
100
101impl Roadmap {
106 pub fn versions(&self) -> &[RoadmapVersion] {
108 &self.versions
109 }
110
111 pub fn total_done(&self) -> usize {
113 self.versions.iter().map(|v| v.done).sum()
114 }
115
116 pub fn total_all(&self) -> usize {
118 self.versions.iter().map(|v| v.total).sum()
119 }
120}
121
122pub(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#[cfg(test)]
178mod tests {
179 use super::*;
180
181 #[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 #[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 #[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}