//! Model project roadmaps and step dependencies //! //! This crate models a roadmap as steps, which may depend on each //! other, and a directed acyclic graph (DAG) of said steps, which //! leads to the end goal. There is no support for due dates, //! estimates, or other such project management features. These roadmaps only //! care about what steps need to be take, in what order, to reach the //! goal. //! //! # Example //! ``` //! # use roadmap::Step; //! # fn main() -> std::result::Result<(), Box> { //! let mut r = roadmap::Roadmap::from_yaml(" //! endgoal: //! label: The end goal //! depends: //! - first //! first: //! label: The first step //! ").unwrap(); //! //! let n = r.step_names(); //! assert_eq!(n.len(), 2); //! assert!(n.contains(&"first")); //! assert!(n.contains(&"endgoal")); //! //! r.set_missing_statuses(); //! println!("{}", r.as_dot(30).unwrap()); //! //! # Ok(()) //! # } //! ``` use serde_yaml; use serde_yaml::Value; use std::collections::HashMap; use std::error::Error; use textwrap::fill; #[derive(Clone,Debug,PartialEq)] pub enum Status { Unknown, Goal, Finished, Ready, Next, Blocked, } impl Status { fn from(text: &str) -> Option { match text { "" => Some(Status::Unknown), "goal" => Some(Status::Goal), "finished" => Some(Status::Finished), "ready" => Some(Status::Ready), "next" => Some(Status::Next), "blocked" => Some(Status::Blocked), _ => None, } } } impl PartialEq for &Status { fn eq(&self, other: &Status) -> bool { **self == *other } } impl PartialEq<&Status> for Status { fn eq(&self, other: &&Status) -> bool { **other == *self } } /// A step in a roadmap. #[derive(Clone, Debug, PartialEq)] pub struct Step { name: String, status: Status, label: String, depends: Vec, } impl Step { /// Create a new step with a name and a label. pub fn new(name: &str, label: &str) -> Step { Step { name: name.to_string(), status: Status::Unknown, label: label.to_string(), depends: vec![], } } /// Return the name of a step. pub fn name<'a>(&'a self) -> &'a str { &self.name } /// Return the label of a step. pub fn label<'a>(&'a self) -> &'a str { &self.label } /// Return the status of a step. pub fn status<'a>(&'a self) -> &Status { &self.status } /// Set the status of a step. pub fn set_status(&mut self, status: Status) { self.status = status } /// Return vector of names of dependencies for a step. pub fn dependencies(&self) -> impl Iterator { self.depends.iter() } /// Add the name of a dependency to step. pub fn add_dependency(&mut self, name: &str) { self.depends.push(String::from(name)); } /// Does this step depend on given other step? pub fn depends_on(&self, other_name: &str) -> bool { self.depends.iter().any(|depname| depname == other_name) } } /// Result type for roadmap parsing. type ParseResult = Result; /// All the steps to get to the end goal. pub struct Roadmap { steps: Vec, } impl Roadmap { /// Create a new, empty roadmap. pub fn new() -> Roadmap { Roadmap { steps: vec![] } } /// Create a new roadmap from a YAML representation. pub fn from_yaml(yaml: &str) -> Result> { let mut roadmap = Roadmap::new(); let map: HashMap = serde_yaml::from_str(yaml)?; for (name, value) in map { let step = Roadmap::step_from_value(&name, &value)?; roadmap.add_step(&step)?; } roadmap.validate()?; Ok(roadmap) } // Convert a Value into a Step, if possible. fn step_from_value(name: &str, value: &Value) -> ParseResult { match value { Value::Mapping(_) => { let label = Roadmap::parse_label(&value); let mut step = Step::new(name, label); let status = Roadmap::parse_status(&value)?; step.set_status(status); for depname in Roadmap::parse_depends(&value).iter() { step.add_dependency(depname); } Ok(step) } _ => Err("step is not a mapping".to_string()), } } // Get a sequence of depenencies. fn parse_depends(map: &Value) -> Vec<&str> { let key_name = "depends"; let key = Value::String(key_name.to_string()); let mut depends: Vec<&str> = vec![]; if let Some(Value::Sequence(deps)) = map.get(&key) { for depname in deps.iter() { if let Value::String(depname) = depname { depends.push(depname); } } } depends } // Get label string from a Mapping element, or empty string. fn parse_label<'a>(map: &'a Value) -> &'a str { Roadmap::parse_string("label", map) } // Get status string from a Mapping element. Default to unknown status. fn parse_status<'a>(map: &'a Value) -> ParseResult { let status_text = Roadmap::parse_string("status", map); let status = Status::from(status_text); if let Some(status) = status { Ok(status) } else { Err(format!("unknown status: {:?}", status_text)) } } // Get string value from a Mapping element, or empty string. fn parse_string<'a>(key_name: &str, map: &'a Value) -> &'a str { let key = Value::String(key_name.to_string()); match map.get(&key) { Some(Value::String(s)) => s, _ => "", } } // Validate that the parsed, constructed roadmap is valid. fn validate(&self) -> ParseResult<()> { // Is there exactly one goal? if self.count_goals() != 1 { return Err(format!("must have exactly one goal for roadmap")); } // Does every dependency exist? for step in self.steps.iter() { for depname in step.dependencies() { match self.get_step(depname) { None => { return Err(format!( "step {} depends on missing {}", step.name(), depname )) } Some(_) => (), } } } Ok(()) } // Count number of steps that nothing depends on. fn count_goals(&self) -> usize { self.steps .iter() .map(|step| self.is_goal(step)) .filter(|b| *b) .count() } /// Return list of step names. pub fn step_names<'a>(&'a self) -> Vec<&'a str> { let mut names = vec![]; for step in self.steps.iter() { names.push(step.name()); } names } /// Get a step, given its name. pub fn get_step<'a>(&'a self, name: &str) -> Option<&'a Step> { for step in self.steps.iter() { if step.name() == name { return Some(step); } } None } /// 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> { self.steps.push(step.clone()); Ok(()) } // 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? 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. 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. 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<'a>(&'a 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? 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 => "#rr0000", } } 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::{Roadmap, Step, Status}; #[test] fn new_step() { let step = Step::new("myname", "my label"); assert_eq!(step.name(), "myname"); assert_eq!(step.status(), Status::Unknown); assert_eq!(step.label(), "my label"); assert_eq!(step.dependencies().count(), 0); } #[test] fn set_status() { let mut step = Step::new("myname", "my label"); step.set_status(Status::Next); assert_eq!(step.status(), Status::Next); } #[test] fn add_step_dependency() { let mut second = Step::new("second", "the second step"); second.add_dependency("first"); let deps: Vec<&String> = second.dependencies().collect(); assert_eq!(deps, vec!["first"]); } #[test] fn new_roadmap() { let roadmap = Roadmap::new(); assert_eq!(roadmap.step_names().len(), 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(); assert_eq!(roadmap.step_names().len(), 1); assert_eq!(roadmap.step_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 parse_yaml_is_list() { let r = Roadmap::from_yaml("[]"); match r { Ok(_) => panic!("expected a parse error"), _ => (), } } #[test] fn parse_yaml_is_empty() { let r = Roadmap::from_yaml(""); match r { Ok(_) => panic!("expected a parse error"), _ => (), } } #[test] fn parse_yaml_map_entries_not_maps() { let r = Roadmap::from_yaml("foo: []"); match r { Ok(_) => panic!("expected a parse error"), _ => (), } } #[test] fn parse_yaml_unknown_dep() { let r = Roadmap::from_yaml("foo: {depends: [bar]}"); match r { Ok(_) => panic!("expected a parse error"), _ => (), } } #[test] fn parse_yaml_unknown_status() { let r = Roadmap::from_yaml(r#"foo: {status: "bar"}"#); match r { Ok(_) => panic!("expected a parse error"), _ => (), } } #[test] fn set_missing_goal_status() { let mut r = Roadmap::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; } " ); } #[test] fn from_empty_yaml() { let roadmap = Roadmap::from_yaml("{}"); match roadmap { Ok(_) => panic!("expected error for empty dict"), _ => (), } } #[test] fn from_nonempty_yaml() { let roadmap = Roadmap::from_yaml( " first: label: the first step second: label: the second step depends: - first ", ) .unwrap(); let names = roadmap.step_names(); assert_eq!(names.len(), 2); assert!(names.contains(&"first")); assert!(names.contains(&"second")); let first = roadmap.get_step("first").unwrap(); assert_eq!(first.name(), "first"); assert_eq!(first.label(), "the first step"); let deps: Vec<&String> = first.dependencies().collect(); assert_eq!(deps.len(), 0); let second = roadmap.get_step("second").unwrap(); assert_eq!(second.name(), "second"); assert_eq!(second.label(), "the second step"); let deps: Vec<&String> = second.dependencies().collect(); assert_eq!(deps, vec!["first"]); } }