use textwrap::fill; pub use crate::from_yaml; pub use crate::Status; pub use crate::Step; /// Error in Roadmap, from parsing or otherwise. pub type RoadmapError = Result; /// 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); } // 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 step.status() == Status::Unknown { 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(); } } /// 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 { 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 { self.steps.iter().all(|other| !other.depends_on(&step)) } // Validate that the parsed, constructed roadmap is valid. pub fn validate(&self) -> RoadmapError<()> { // Is there exactly one goal? match self.count_goals() { 0 => return Err(format!("the roadmap doesn't have a goal")), 1 => (), _ => return Err(format!("must have exactly one goal for roadmap")), } // Does every dependency exist? for step in self.iter() { for depname in step.dependencies() { match self.get_step(depname) { None => { return Err(format!( "step {} depends on missing {}", step.name(), depname )) } Some(_) => (), } } } Ok(()) } /// 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 format_as_dot(&self, label_width: usize) -> Result> { self.validate()?; 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(); match roadmap.format_as_dot(999) { Err(_) => (), _ => panic!("expected error for empty 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.format_as_dot(999).unwrap(), "digraph \"roadmap\" { first [label=\"\" style=filled fillcolor=\"#ffffff\" shape=\"ellipse\"]; second [label=\"\" style=filled fillcolor=\"#00eeee\" shape=\"diamond\"]; first -> second; } " ); } }