summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2019-09-28 10:31:50 +0300
committerLars Wirzenius <liw@liw.fi>2019-09-28 11:36:36 +0300
commit6d4af8bd7fbfa88a7dd3820af2b557fe072a8311 (patch)
treec677aafd7fc44ffe763b2b25610bdfab0c059219
parent03b9822809b71fe302c83df2ddc832afd58777e0 (diff)
downloadroadmap-6d4af8bd7fbfa88a7dd3820af2b557fe072a8311.tar.gz
Change: move Status, Step, Roadmap, and parser to separate modules
-rw-r--r--src/bin/roadmap2dot.rs4
-rw-r--r--src/lib.rs595
-rw-r--r--src/parser.rs196
-rw-r--r--src/roadmap.rs280
-rw-r--r--src/status.rs65
-rw-r--r--src/step.rs92
6 files changed, 644 insertions, 588 deletions
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<dyn std::error::Error>> {
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<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;
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<T> = Result<T, String>;
+
+/// 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 = 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<Step> {
+ 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<Status> {
+ 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<Step>,
+}
+
+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<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?
+ 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<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, 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<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,
+ }
+ }
+}
+
+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
+ }
+}
+
+
+#[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<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. 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"]);
+ }
+}