summaryrefslogtreecommitdiff
path: root/src/lib.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib.rs')
-rw-r--r--src/lib.rs595
1 files changed, 9 insertions, 586 deletions
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;