use textwrap::fill; pub use crate::from_yaml; pub use crate::Status; pub use crate::Step; /// Represent a full project roadmap. /// /// This stores all the steps needed to reach the end goal. See the /// crate leve documentation for an example. #[derive(Clone,Debug)] pub struct Roadmap { steps: Vec, } impl Roadmap { /// Create a new, empty roadmap. /// /// You probably want the `from_yaml` function instead. 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() } /// Iterate over step names. pub fn step_names(&self) -> impl Iterator { 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. pub fn add_step(&mut self, step: &Step) { self.steps.push(step.clone()); } // Get iterator over refs to steps. pub fn iter(&self) -> impl Iterator { self.steps.iter() } // Get iterator over mut refs to steps. pub fn iter_mut(&mut self) -> impl Iterator { 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 = 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> { 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); 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); 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); roadmap.add_step(&second); 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; } " ); } }