diff options
Diffstat (limited to 'src/map.rs')
-rw-r--r-- | src/map.rs | 271 |
1 files changed, 271 insertions, 0 deletions
diff --git a/src/map.rs b/src/map.rs new file mode 100644 index 0000000..d131e7d --- /dev/null +++ b/src/map.rs @@ -0,0 +1,271 @@ +use textwrap::fill; + +pub use crate::from_yaml; +pub use crate::Status; +pub use crate::Step; + +/// All the steps to get to the end goal. +#[derive(Clone,Debug)] +pub struct Roadmap { + steps: Vec<Step>, +} + +impl Roadmap { + /// Create a new, empty roadmap. + pub fn new() -> Roadmap { + Roadmap { steps: vec![] } + } + + /// Count number of steps that nothing depends on. + pub fn count_goals(&self) -> usize { + self.steps + .iter() + .filter(|step| self.is_goal(step)) + .count() + } + + /// Return list of step names. + pub fn step_names(&self) -> impl Iterator<Item=&str> { + self.steps.iter().map(|step| step.name()) + } + + /// Get a step, given its name. + pub fn get_step(&self, name: &str) -> Option<&Step> { + self.steps.iter().filter(|step| step.name() == name).next() + } + + /// Add a step to the roadmap. This may fail, if there's a step + /// with that name already. + pub fn add_step(&mut self, step: &Step) -> Result<(), Box<dyn std::error::Error>> { + self.steps.push(step.clone()); + Ok(()) + } + + // Get iterator over refs to steps. + pub fn iter(&self) -> impl Iterator<Item = &Step> { + self.steps.iter() + } + + // Get iterator over mut refs to steps. + pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Step> { + self.steps.iter_mut() + } + + /// Compute status of any step for which it has not been specified + /// in the input. + pub fn set_missing_statuses(&mut self) { + let new_steps: Vec<Step> = self + .steps + .iter() + .map(|step| { + let mut step = step.clone(); + if self.is_unset(&step) { + if self.is_goal(&step) { + step.set_status(Status::Goal); + } else if self.is_blocked(&step) { + step.set_status(Status::Blocked); + } else if self.is_ready(&step) { + step.set_status(Status::Ready); + } + } + step + }) + .collect(); + + if self.steps != new_steps { + self.steps = new_steps; + self.set_missing_statuses(); + } + } + + /// Is status unset? + pub fn is_unset(&self, step: &Step) -> bool { + step.status() == Status::Unknown + } + + /// Should unset status be ready? In other words, if there are any + /// dependencies, they are all finished. + pub fn is_ready(&self, step: &Step) -> bool { + self.dep_statuses(step) + .iter() + .all(|&status| status == Status::Finished) + } + + /// Should unset status be blocked? In other words, if there are + /// any dependencies, that aren't finished. + pub fn is_blocked(&self, step: &Step) -> bool { + self.dep_statuses(step) + .iter() + .any(|&status| status != Status::Finished) + } + + // Return vector of all statuses of all dependencies + fn dep_statuses(&self, step: &Step) -> Vec<&Status> { + step.dependencies() + .map(|depname| { + if let Some(step) = self.get_step(depname) { + step.status() + } else { + &Status::Unknown + } + }) + .collect() + } + + /// Should status be goal? In other words, does any other step + /// depend on this one? + pub fn is_goal(&self, step: &Step) -> bool { + let has_parent = self.steps.iter().any(|other| other.depends_on(step.name())); + !has_parent + } + + /// Get a Graphviz dot language representation of a roadmap. This + /// is the textual representation, and the caller needs to use the + /// Graphviz dot(1) tool to create an image from it. + pub fn as_dot(&self, label_width: usize) -> Result<String, Box<dyn std::error::Error>> { + let labels = self.steps.iter().map(|step| { + format!( + "{} [label=\"{}\" style=filled fillcolor=\"{}\" shape=\"{}\"];\n", + step.name(), + fill(&step.label(), label_width).replace("\n", "\\n"), + Roadmap::get_status_color(step), + Roadmap::get_status_shape(step), + ) + }); + + let mut dot = String::new(); + dot.push_str("digraph \"roadmap\" {\n"); + for line in labels { + dot.push_str(&line); + } + + for step in self.iter() { + for dep in step.dependencies() { + let line = format!("{} -> {};\n", dep, step.name()); + dot.push_str(&line); + } + } + + dot.push_str("}\n"); + + Ok(dot) + } + + fn get_status_color(step: &Step) -> &str { + match step.status() { + Status::Blocked => "#f4bada", + Status::Finished => "#eeeeee", + Status::Ready => "#ffffff", + Status::Next => "#0cc00", + Status::Goal => "#00eeee", + Status::Unknown => "#ff0000", + } + } + + fn get_status_shape(step: &Step) -> &str { + match step.status() { + Status::Blocked => "rectangle", + Status::Finished => "circle", + Status::Ready => "ellipse", + Status::Next => "ellipse", + Status::Goal => "diamond", + Status::Unknown => "house", + } + } +} + +#[cfg(test)] +mod tests { + use super::{from_yaml, Roadmap, Status, Step}; + + #[test] + fn new_roadmap() { + let roadmap = Roadmap::new(); + assert_eq!(roadmap.step_names().count(), 0); + } + + #[test] + fn add_step_to_roadmap() { + let mut roadmap = Roadmap::new(); + let first = Step::new("first", "the first step"); + roadmap.add_step(&first).unwrap(); + let names: Vec<&str> = roadmap.step_names().collect(); + assert_eq!(names, vec!["first"]); + } + + #[test] + fn get_step_from_roadmap() { + let mut roadmap = Roadmap::new(); + let first = Step::new("first", "the first step"); + roadmap.add_step(&first).unwrap(); + let gotit = roadmap.get_step("first").unwrap(); + assert_eq!(gotit.name(), "first"); + assert_eq!(gotit.label(), "the first step"); + } + + #[test] + fn set_missing_goal_status() { + let mut r = from_yaml( + " +goal: + depends: + - finished + - blocked + +finished: + status: finished + +ready: + depends: + - finished + +next: + status: next + +blocked: + depends: + - ready + - next +", + ) + .unwrap(); + r.set_missing_statuses(); + assert_eq!(r.get_step("goal").unwrap().status(), Status::Goal); + assert_eq!(r.get_step("finished").unwrap().status(), Status::Finished); + assert_eq!(r.get_step("ready").unwrap().status(), Status::Ready); + assert_eq!(r.get_step("next").unwrap().status(), Status::Next); + assert_eq!(r.get_step("blocked").unwrap().status(), Status::Blocked); + } + + #[test] + fn empty_dot() { + let roadmap = Roadmap::new(); + assert_eq!( + roadmap.as_dot(999).unwrap(), + "digraph \"roadmap\" { +} +" + ); + } + + #[test] + fn simple_dot() { + let mut roadmap = Roadmap::new(); + let mut first = Step::new("first", ""); + first.set_status(Status::Ready); + let mut second = Step::new("second", ""); + second.add_dependency("first"); + second.set_status(Status::Goal); + roadmap.add_step(&first).unwrap(); + roadmap.add_step(&second).unwrap(); + assert_eq!( + roadmap.as_dot(999).unwrap(), + "digraph \"roadmap\" { +first [label=\"\" style=filled fillcolor=\"#ffffff\" shape=\"ellipse\"]; +second [label=\"\" style=filled fillcolor=\"#00eeee\" shape=\"diamond\"]; +first -> second; +} +" + ); + } +} |