From 6d4af8bd7fbfa88a7dd3820af2b557fe072a8311 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 28 Sep 2019 10:31:50 +0300 Subject: Change: move Status, Step, Roadmap, and parser to separate modules --- src/bin/roadmap2dot.rs | 4 +- src/lib.rs | 595 +------------------------------------------------ src/parser.rs | 196 ++++++++++++++++ src/roadmap.rs | 280 +++++++++++++++++++++++ src/status.rs | 65 ++++++ src/step.rs | 92 ++++++++ 6 files changed, 644 insertions(+), 588 deletions(-) create mode 100644 src/parser.rs create mode 100644 src/roadmap.rs create mode 100644 src/status.rs create mode 100644 src/step.rs diff --git a/src/bin/roadmap2dot.rs b/src/bin/roadmap2dot.rs index e361efb..64b55d1 100644 --- a/src/bin/roadmap2dot.rs +++ b/src/bin/roadmap2dot.rs @@ -11,7 +11,7 @@ //! test the library crate. It is expected that serious use of the //! crate will be as a library. -use roadmap::Roadmap; +use roadmap::from_yaml; use std::fs::File; use std::io::Read; use std::path::PathBuf; @@ -36,7 +36,7 @@ fn main() -> Result<(), Box> { let mut f = File::open(opt.filename)?; f.read_to_string(&mut text)?; - let mut r = Roadmap::from_yaml(&text)?; + let mut r = from_yaml(&text)?; r.set_missing_statuses(); println!("{}", r.as_dot(LABEL_WIDTH).unwrap()); diff --git a/src/lib.rs b/src/lib.rs index 414e83a..c13943d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,9 +9,8 @@ //! //! # Example //! ``` -//! # use roadmap::Step; //! # fn main() -> std::result::Result<(), Box> { -//! let mut r = roadmap::Roadmap::from_yaml(" +//! let mut r = roadmap::from_yaml(" //! endgoal: //! label: The end goal //! depends: @@ -32,590 +31,14 @@ //! # } //! ``` -use serde_yaml; -use serde_yaml::Value; -use std::collections::HashMap; -use std::error::Error; -use textwrap::fill; +mod status; +pub use crate::status::Status; -#[derive(Clone,Debug,PartialEq)] -pub enum Status { - Unknown, - Goal, - Finished, - Ready, - Next, - Blocked, -} +mod step; +pub use crate::step::Step; -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, - } - } -} +mod roadmap; +pub use crate::roadmap::Roadmap; -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"]); - } -} +mod parser; +pub use crate::parser::from_yaml; diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..48c4c52 --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,196 @@ +use serde_yaml; +use serde_yaml::Value; +use std::collections::HashMap; +use std::error::Error; + +pub use crate::Status; +pub use crate::Step; +pub use crate::Roadmap; + +/// Result type for roadmap parsing. +type ParseResult = Result; + +/// 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 = step_from_value(&name, &value)?; + roadmap.add_step(&step)?; + } + + validate(&roadmap)?; + 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 = parse_label(&value); + let mut step = Step::new(name, label); + + let status = parse_status(&value)?; + step.set_status(status); + + for depname in 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 { + +parse_string("label", map) +} + +// Get status string from a Mapping element. Default to unknown status. +fn parse_status<'a>(map: &'a Value) -> ParseResult { + let text = parse_string("status", map); + let status = Status::from_text(text); + if let Some(status) = status { + Ok(status) + } else { + Err(format!("unknown 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(roadmap: &Roadmap) -> ParseResult<()> { + // Is there exactly one goal? + if roadmap.count_goals() != 1 { + return Err(format!("must have exactly one goal for roadmap")); + } + + // Does every dependency exist? + for step in roadmap.iter() { + for depname in step.dependencies() { + match roadmap.get_step(depname) { + None => { + return Err(format!( + "step {} depends on missing {}", + step.name(), + depname + )) + } + Some(_) => (), + } + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::from_yaml; + + #[test] + fn yaml_is_empty() { + let r = from_yaml(""); + match r { + Ok(_) => panic!("expected a parse error"), + _ => (), + } + } + + #[test] + fn yaml_is_list() { + let r = from_yaml("[]"); + match r { + Ok(_) => panic!("expected a parse error"), + _ => (), + } + } + + #[test] + fn yaml_map_entries_not_maps() { + let r = from_yaml("foo: []"); + match r { + Ok(_) => panic!("expected a parse error"), + _ => (), + } + } + + #[test] + fn yaml_unknown_dep() { + let r = from_yaml("foo: {depends: [bar]}"); + match r { + Ok(_) => panic!("expected a parse error"), + _ => (), + } + } + + #[test] + fn yaml_unknown_status() { + let r = from_yaml(r#"foo: {status: "bar"}"#); + match r { + Ok(_) => panic!("expected a parse error"), + _ => (), + } + } + + #[test] + fn yaml_happy() { + let 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"]); + } +} diff --git a/src/roadmap.rs b/src/roadmap.rs new file mode 100644 index 0000000..257441b --- /dev/null +++ b/src/roadmap.rs @@ -0,0 +1,280 @@ +use textwrap::fill; + +pub use crate::Status; +pub use crate::Step; +pub use crate::from_yaml; + +/// 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![] } + } + + /// Count number of steps that nothing depends on. + pub 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? + 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<'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? + 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 => "#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, from_yaml}; + + #[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 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; +} +" + ); + } +} diff --git a/src/status.rs b/src/status.rs new file mode 100644 index 0000000..247023c --- /dev/null +++ b/src/status.rs @@ -0,0 +1,65 @@ +/// Represent the status of a step in a roadmap. +/// +/// The unknown status allows the user to not specify the status, and +/// the roadmap to infer it from the structure of the graph. For +/// example, a step is inferred to be blocked if any of it +/// dependencies are not finished. + +#[derive(Clone,Debug,PartialEq)] +pub enum Status { + Unknown, + Goal, + Finished, + Ready, + Next, + Blocked, +} + +impl Status { + pub fn from_text(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 + } +} + + +#[cfg(test)] +mod test { + use super::Status; + + #[test] + fn happy_from_text() { + assert_eq!(Status::from_text("").unwrap(), Status::Unknown); + assert_eq!(Status::from_text("goal").unwrap(), Status::Goal); + assert_eq!(Status::from_text("finished").unwrap(), Status::Finished); + assert_eq!(Status::from_text("ready").unwrap(), Status::Ready); + assert_eq!(Status::from_text("next").unwrap(), Status::Next); + assert_eq!(Status::from_text("blocked").unwrap(), Status::Blocked); + } + + + #[test] + fn sad_from_text() { + let x = Status::from_text("x"); + assert_eq!(x, None); + } +} diff --git a/src/step.rs b/src/step.rs new file mode 100644 index 0000000..acbb078 --- /dev/null +++ b/src/step.rs @@ -0,0 +1,92 @@ +use super::Status; + +/// A roadmap step. +/// +/// See the crate documentation for an example. You +/// probably don't want to create steps manually, but via the roadmap +/// YAML parsing function. +#[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. Steps are referred by + /// name. Steps don't know about other steps, and can't validate + /// that the dependency exists, so this always works. + 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) + } +} + +#[cfg(test)] +mod tests { + use super::{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"]); + } +} -- cgit v1.2.1