roadmap/
map.rs

1use std::collections::HashMap;
2
3use textwrap::fill;
4
5pub use crate::RoadmapError;
6pub use crate::Status;
7pub use crate::Step;
8
9/// Error in Roadmap, from parsing or otherwise.
10pub type RoadmapResult<T> = Result<T, RoadmapError>;
11
12/// Represent a full project roadmap.
13///
14/// This stores all the steps needed to reach the end goal. See the
15/// crate leve documentation for an example.
16#[derive(Clone, Debug, Default)]
17pub struct Roadmap {
18    steps: Vec<Step>,
19}
20
21impl Roadmap {
22    /// Create a new, empty roadmap.
23    ///
24    /// You probably want the `from_yaml` function instead.
25    pub fn new(map: HashMap<String, Step>) -> Self {
26        Self {
27            steps: map.values().cloned().collect(),
28        }
29    }
30
31    // Find steps that nothing depends on.
32    fn goals(&self) -> Vec<&Step> {
33        self.steps
34            .iter()
35            .filter(|step| self.is_goal(step))
36            .collect()
37    }
38
39    /// Count number of steps that nothing depends on.
40    pub fn count_goals(&self) -> usize {
41        self.goals().len()
42    }
43
44    /// Iterate over step names.
45    pub fn step_names(&self) -> impl Iterator<Item = &str> {
46        self.steps.iter().map(|step| step.name())
47    }
48
49    /// Get a step, given its name.
50    pub fn get_step(&self, name: &str) -> Option<&Step> {
51        self.steps.iter().find(|step| step.name() == name)
52    }
53
54    /// Add a step to the roadmap.
55    pub fn add_step(&mut self, step: Step) {
56        self.steps.push(step);
57    }
58
59    // Get iterator over refs to steps.
60    pub fn iter(&self) -> impl Iterator<Item = &Step> {
61        self.steps.iter()
62    }
63
64    // Get iterator over mut refs to steps.
65    pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Step> {
66        self.steps.iter_mut()
67    }
68
69    /// Compute status of any step for which it has not been specified
70    /// in the input.
71    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    /// Should unset status be ready? In other words, if there are any
97    /// dependencies, they are all finished.
98    pub fn is_ready(&self, step: &Step) -> bool {
99        self.dep_statuses(step)
100            .iter()
101            .all(|&status| status == Status::Finished)
102    }
103
104    /// Should unset status be blocked? In other words, if there are
105    /// any dependencies, that aren't finished.
106    pub fn is_blocked(&self, step: &Step) -> bool {
107        self.dep_statuses(step)
108            .iter()
109            .any(|&status| status != Status::Finished)
110    }
111
112    // Return vector of all statuses of all dependencies
113    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    /// Should status be goal? In other words, does any other step
126    /// depend on this one?
127    pub fn is_goal(&self, step: &Step) -> bool {
128        self.steps.iter().all(|other| !other.depends_on(step))
129    }
130
131    // Validate that the parsed, constructed roadmap is valid.
132    pub fn validate(&self) -> RoadmapResult<()> {
133        // Is there exactly one goal?
134        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        // Does every dependency exist?
146        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    /// Get a Graphviz dot language representation of a roadmap. This
161    /// is the textual representation, and the caller needs to use the
162    /// Graphviz dot(1) tool to create an image from it.
163    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}