1use std::collections::HashMap;
2
3use textwrap::fill;
4
5pub use crate::RoadmapError;
6pub use crate::Status;
7pub use crate::Step;
8
9pub type RoadmapResult<T> = Result<T, RoadmapError>;
11
12#[derive(Clone, Debug, Default)]
17pub struct Roadmap {
18 steps: Vec<Step>,
19}
20
21impl Roadmap {
22 pub fn new(map: HashMap<String, Step>) -> Self {
26 Self {
27 steps: map.values().cloned().collect(),
28 }
29 }
30
31 fn goals(&self) -> Vec<&Step> {
33 self.steps
34 .iter()
35 .filter(|step| self.is_goal(step))
36 .collect()
37 }
38
39 pub fn count_goals(&self) -> usize {
41 self.goals().len()
42 }
43
44 pub fn step_names(&self) -> impl Iterator<Item = &str> {
46 self.steps.iter().map(|step| step.name())
47 }
48
49 pub fn get_step(&self, name: &str) -> Option<&Step> {
51 self.steps.iter().find(|step| step.name() == name)
52 }
53
54 pub fn add_step(&mut self, step: Step) {
56 self.steps.push(step);
57 }
58
59 pub fn iter(&self) -> impl Iterator<Item = &Step> {
61 self.steps.iter()
62 }
63
64 pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Step> {
66 self.steps.iter_mut()
67 }
68
69 pub fn set_missing_statuses(&mut self) {
72 let new_steps: Vec<Step> = self
73 .steps
74 .iter()
75 .map(|step| {
76 let mut step = step.clone();
77 if step.status() == Status::Unknown {
78 if self.is_goal(&step) {
79 step.set_status(Status::Goal);
80 } else if self.is_blocked(&step) {
81 step.set_status(Status::Blocked);
82 } else if self.is_ready(&step) {
83 step.set_status(Status::Ready);
84 }
85 }
86 step
87 })
88 .collect();
89
90 if self.steps != new_steps {
91 self.steps = new_steps;
92 self.set_missing_statuses();
93 }
94 }
95
96 pub fn is_ready(&self, step: &Step) -> bool {
99 self.dep_statuses(step)
100 .iter()
101 .all(|&status| status == Status::Finished)
102 }
103
104 pub fn is_blocked(&self, step: &Step) -> bool {
107 self.dep_statuses(step)
108 .iter()
109 .any(|&status| status != Status::Finished)
110 }
111
112 fn dep_statuses(&self, step: &Step) -> Vec<Status> {
114 step.dependencies()
115 .map(|depname| {
116 if let Some(step) = self.get_step(depname) {
117 step.status()
118 } else {
119 Status::Unknown
120 }
121 })
122 .collect()
123 }
124
125 pub fn is_goal(&self, step: &Step) -> bool {
128 self.steps.iter().all(|other| !other.depends_on(step))
129 }
130
131 pub fn validate(&self) -> RoadmapResult<()> {
133 let goals = self.goals();
135 let n = goals.len();
136 match n {
137 0 => return Err(RoadmapError::NoGoals),
138 1 => (),
139 _ => {
140 let names: Vec<String> = goals.iter().map(|s| s.name().into()).collect();
141 return Err(RoadmapError::ManyGoals { count: n, names });
142 }
143 }
144
145 for step in self.iter() {
147 for depname in step.dependencies() {
148 if self.get_step(depname).is_none() {
149 return Err(RoadmapError::MissingDep {
150 name: step.name().into(),
151 missing: depname.into(),
152 });
153 }
154 }
155 }
156
157 Ok(())
158 }
159
160 pub fn format_as_dot(&self, label_width: usize) -> RoadmapResult<String> {
164 self.validate()?;
165
166 let labels = self.steps.iter().map(|step| {
167 format!(
168 "{} [label=\"{}\" style=filled fillcolor=\"{}\" shape=\"{}\"];\n",
169 step.name(),
170 fill(step.label(), label_width).replace('\n', "\\n"),
171 Roadmap::get_status_color(step),
172 Roadmap::get_status_shape(step),
173 )
174 });
175
176 let mut dot = String::new();
177 dot.push_str("digraph \"roadmap\" {\n");
178 for line in labels {
179 dot.push_str(&line);
180 }
181
182 for step in self.iter() {
183 for dep in step.dependencies() {
184 let line = format!("{} -> {};\n", dep, step.name());
185 dot.push_str(&line);
186 }
187 }
188
189 dot.push_str("}\n");
190
191 Ok(dot)
192 }
193
194 fn get_status_color(step: &Step) -> &str {
195 match step.status() {
196 Status::Blocked => "#f4bada",
197 Status::Finished => "#eeeeee",
198 Status::Ready => "#ffffff",
199 Status::Next => "#0cc00",
200 Status::Goal => "#00eeee",
201 Status::Unknown => "#ff0000",
202 }
203 }
204
205 fn get_status_shape(step: &Step) -> &str {
206 match step.status() {
207 Status::Blocked => "rectangle",
208 Status::Finished => "octagon",
209 Status::Ready => "ellipse",
210 Status::Next => "ellipse",
211 Status::Goal => "diamond",
212 Status::Unknown => "house",
213 }
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::{Roadmap, Status, Step};
220 use crate::from_yaml;
221
222 #[test]
223 fn new_roadmap() {
224 let roadmap = Roadmap::default();
225 assert_eq!(roadmap.step_names().count(), 0);
226 }
227
228 #[test]
229 fn add_step_to_roadmap() {
230 let mut roadmap = Roadmap::default();
231 let first = Step::new("first", "the first step");
232 roadmap.add_step(first);
233 let names: Vec<&str> = roadmap.step_names().collect();
234 assert_eq!(names, vec!["first"]);
235 }
236
237 #[test]
238 fn get_step_from_roadmap() {
239 let mut roadmap = Roadmap::default();
240 let first = Step::new("first", "the first step");
241 roadmap.add_step(first);
242 let gotit = roadmap.get_step("first").unwrap();
243 assert_eq!(gotit.name(), "first");
244 assert_eq!(gotit.label(), "the first step");
245 }
246
247 #[test]
248 fn set_missing_goal_status() {
249 let mut r = from_yaml(
250 "
251goal:
252 depends:
253 - finished
254 - blocked
255
256finished:
257 status: finished
258
259ready:
260 depends:
261 - finished
262
263next:
264 status: next
265
266blocked:
267 depends:
268 - ready
269 - next
270",
271 )
272 .unwrap();
273 r.set_missing_statuses();
274 assert_eq!(r.get_step("goal").unwrap().status(), Status::Goal);
275 assert_eq!(r.get_step("finished").unwrap().status(), Status::Finished);
276 assert_eq!(r.get_step("ready").unwrap().status(), Status::Ready);
277 assert_eq!(r.get_step("next").unwrap().status(), Status::Next);
278 assert_eq!(r.get_step("blocked").unwrap().status(), Status::Blocked);
279 }
280
281 #[test]
282 fn empty_dot() {
283 let roadmap = Roadmap::default();
284 match roadmap.format_as_dot(999) {
285 Err(_) => (),
286 _ => panic!("expected error for empty roadmap"),
287 }
288 }
289
290 #[test]
291 fn simple_dot() {
292 let mut roadmap = Roadmap::default();
293 let mut first = Step::new("first", "");
294 first.set_status(Status::Ready);
295 let mut second = Step::new("second", "");
296 second.add_dependency("first");
297 second.set_status(Status::Goal);
298 roadmap.add_step(first);
299 roadmap.add_step(second);
300 assert_eq!(
301 roadmap.format_as_dot(999).unwrap(),
302 "digraph \"roadmap\" {
303first [label=\"\" style=filled fillcolor=\"#ffffff\" shape=\"ellipse\"];
304second [label=\"\" style=filled fillcolor=\"#00eeee\" shape=\"diamond\"];
305first -> second;
306}
307"
308 );
309 }
310}