diff options
Diffstat (limited to 'src/lib.rs')
-rw-r--r-- | src/lib.rs | 595 |
1 files changed, 9 insertions, 586 deletions
@@ -9,9 +9,8 @@ //! //! # Example //! ``` -//! # use roadmap::Step; //! # fn main() -> std::result::Result<(), Box<dyn std::error::Error>> { -//! 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<Status> { - 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<Status> 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<String>, -} - -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<Item = &String> { - 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<T> = Result<T, String>; - -/// All the steps to get to the end goal. -pub struct Roadmap { - steps: Vec<Step>, -} - -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<Roadmap, Box<dyn Error>> { - let mut roadmap = Roadmap::new(); - let map: HashMap<String, serde_yaml::Value> = 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<Step> { - 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<Status> { - 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<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? - 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<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 => "#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; |