use std::collections::HashMap;
use textwrap::fill;
pub use crate::RoadmapError;
pub use crate::Status;
pub use crate::Step;
pub type RoadmapResult<T> = Result<T, RoadmapError>;
#[derive(Clone, Debug, Default)]
pub struct Roadmap {
steps: Vec<Step>,
}
impl Roadmap {
pub fn new(map: HashMap<String, Step>) -> Self {
Self {
steps: map.values().cloned().collect(),
}
}
fn goals(&self) -> Vec<&Step> {
self.steps
.iter()
.filter(|step| self.is_goal(step))
.collect()
}
pub fn count_goals(&self) -> usize {
self.goals().len()
}
pub fn step_names(&self) -> impl Iterator<Item = &str> {
self.steps.iter().map(|step| step.name())
}
pub fn get_step(&self, name: &str) -> Option<&Step> {
self.steps.iter().find(|step| step.name() == name)
}
pub fn add_step(&mut self, step: Step) {
self.steps.push(step);
}
pub fn iter(&self) -> impl Iterator<Item = &Step> {
self.steps.iter()
}
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Step> {
self.steps.iter_mut()
}
pub fn set_missing_statuses(&mut self) {
let new_steps: Vec<Step> = self
.steps
.iter()
.map(|step| {
let mut step = step.clone();
if step.status() == Status::Unknown {
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();
}
}
pub fn is_ready(&self, step: &Step) -> bool {
self.dep_statuses(step)
.iter()
.all(|&status| status == Status::Finished)
}
pub fn is_blocked(&self, step: &Step) -> bool {
self.dep_statuses(step)
.iter()
.any(|&status| status != Status::Finished)
}
fn dep_statuses(&self, step: &Step) -> Vec<Status> {
step.dependencies()
.map(|depname| {
if let Some(step) = self.get_step(depname) {
step.status()
} else {
Status::Unknown
}
})
.collect()
}
pub fn is_goal(&self, step: &Step) -> bool {
self.steps.iter().all(|other| !other.depends_on(step))
}
pub fn validate(&self) -> RoadmapResult<()> {
let goals = self.goals();
let n = goals.len();
match n {
0 => return Err(RoadmapError::NoGoals),
1 => (),
_ => {
let names: Vec<String> = goals.iter().map(|s| s.name().into()).collect();
return Err(RoadmapError::ManyGoals { count: n, names });
}
}
for step in self.iter() {
for depname in step.dependencies() {
if self.get_step(depname).is_none() {
return Err(RoadmapError::MissingDep {
name: step.name().into(),
missing: depname.into(),
});
}
}
}
Ok(())
}
pub fn format_as_dot(&self, label_width: usize) -> RoadmapResult<String> {
self.validate()?;
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 => "#ff0000",
}
}
fn get_status_shape(step: &Step) -> &str {
match step.status() {
Status::Blocked => "rectangle",
Status::Finished => "octagon",
Status::Ready => "ellipse",
Status::Next => "ellipse",
Status::Goal => "diamond",
Status::Unknown => "house",
}
}
}
#[cfg(test)]
mod tests {
use super::{Roadmap, Status, Step};
use crate::from_yaml;
#[test]
fn new_roadmap() {
let roadmap = Roadmap::default();
assert_eq!(roadmap.step_names().count(), 0);
}
#[test]
fn add_step_to_roadmap() {
let mut roadmap = Roadmap::default();
let first = Step::new("first", "the first step");
roadmap.add_step(first);
let names: Vec<&str> = roadmap.step_names().collect();
assert_eq!(names, vec!["first"]);
}
#[test]
fn get_step_from_roadmap() {
let mut roadmap = Roadmap::default();
let first = Step::new("first", "the first step");
roadmap.add_step(first);
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::default();
match roadmap.format_as_dot(999) {
Err(_) => (),
_ => panic!("expected error for empty roadmap"),
}
}
#[test]
fn simple_dot() {
let mut roadmap = Roadmap::default();
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);
roadmap.add_step(second);
assert_eq!(
roadmap.format_as_dot(999).unwrap(),
"digraph \"roadmap\" {
first [label=\"\" style=filled fillcolor=\"#ffffff\" shape=\"ellipse\"];
second [label=\"\" style=filled fillcolor=\"#00eeee\" shape=\"diamond\"];
first -> second;
}
"
);
}
}