summaryrefslogtreecommitdiff
path: root/src/roadmap.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/roadmap.rs')
-rw-r--r--src/roadmap.rs280
1 files changed, 280 insertions, 0 deletions
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;
+}
+"
+ );
+ }
+}