pax_global_header00006660000000000000000000000064145505060230014512gustar00rootroot0000000000000052 comment=1998303f31df616bd952457cbf5f44fe391f586b roadmap-1998303f31df616bd952457cbf5f44fe391f586b/000077500000000000000000000000001455050602300200275ustar00rootroot00000000000000roadmap-1998303f31df616bd952457cbf5f44fe391f586b/.gitignore000066400000000000000000000000231455050602300220120ustar00rootroot00000000000000/target **/*.rs.bk roadmap-1998303f31df616bd952457cbf5f44fe391f586b/Cargo.lock000066400000000000000000000126411455050602300217400ustar00rootroot00000000000000# This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "aho-corasick" version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" dependencies = [ "memchr", ] [[package]] name = "anyhow" version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704" [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "hashbrown" version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "607c8a29735385251a339424dd462993c0fed8fa09d378f259377df08c126022" [[package]] name = "indexmap" version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" dependencies = [ "autocfg", "hashbrown", ] [[package]] name = "linked-hash-map" version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "memchr" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "proc-macro2" version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" dependencies = [ "proc-macro2", ] [[package]] name = "regex" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.6.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" [[package]] name = "roadmap" version = "0.5.0" dependencies = [ "anyhow", "serde", "serde_yaml", "textwrap", "thiserror", ] [[package]] name = "ryu" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" [[package]] name = "serde" version = "1.0.139" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0171ebb889e45aa68b44aee0859b3eede84c6f5f5c228e6f140c0b2a0a46cad6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.139" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc1d3230c1de7932af58ad8ffbe1d784bd55efd5a9d84ac24f69c72d83543dfb" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_yaml" version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" dependencies = [ "indexmap", "ryu", "serde", "yaml-rust", ] [[package]] name = "smawk" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" [[package]] name = "syn" version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "textwrap" version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" dependencies = [ "smawk", "unicode-linebreak", "unicode-width", ] [[package]] name = "thiserror" version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "unicode-ident" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7" [[package]] name = "unicode-linebreak" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a52dcaab0c48d931f7cc8ef826fa51690a08e1ea55117ef26f89864f532383f" dependencies = [ "regex", ] [[package]] name = "unicode-width" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" [[package]] name = "yaml-rust" version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" dependencies = [ "linked-hash-map", ] roadmap-1998303f31df616bd952457cbf5f44fe391f586b/Cargo.toml000066400000000000000000000007411455050602300217610ustar00rootroot00000000000000[package] name = "roadmap" version = "0.6.0" authors = ["Lars Wirzenius "] edition = "2018" license = "MIT-0" description = "model a project roadmap as a directed acyclic graph" repository = "https://gitlab.com/larswirzenius/roadmap" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] anyhow = "1" serde = { version = "1.0.134", features = ["derive"] } serde_yaml = "0.8" textwrap = "0.15" thiserror = "1" roadmap-1998303f31df616bd952457cbf5f44fe391f586b/README.md000066400000000000000000000041451455050602300213120ustar00rootroot00000000000000# Produce graphical road maps for projects Produce directed acyclic graph representations of a project roadmap. The idea is to show the steps needed to reach a goal, and the order they need to be taken, but ignore due dates and other irrelevant details. # Example ~~~yaml goal: label: | This is the end goal: if we reach here, there is nothing more to be done in the project depends: - finished - blocked finished: status: finished label: | This task is finished; the arrow indicates what follows this task (unless it's blocked) ready: status: ready label: | This task is ready to be done: it is not blocked by anything next: status: next label: | This task is chosen to be done next blocked: status: blocked label: | This task is blocked and can't be done until something happens depends: - ready - next ~~~ To run: ~~~sh cargo run --bin roadmap2dot legend.yaml | dot -Tsvg > legend.svg ~~~ This will produce a graph, which is visible below if this README is rendered from the source tree; or you can it [in the repository](https://gitlab.com/larswirzenius/roadmap/-/blob/main/legend.svg). ![](legend.svg "See yaml for textual version") # Legalese Copyright 2019 Lars Wirzenius Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. roadmap-1998303f31df616bd952457cbf5f44fe391f586b/check000077500000000000000000000001611455050602300210300ustar00rootroot00000000000000#!/bin/bash set -euo pipefail cargo clippy -q -- --deny=clippy::all cargo build -q --all-targets cargo test -q roadmap-1998303f31df616bd952457cbf5f44fe391f586b/legend.svg000066400000000000000000000122451455050602300220120ustar00rootroot00000000000000 roadmap goal This is the end goal: if we reach here, there is nothing more to be done in the project next This task is chosen to be done next blocked This task is blocked and can't be done until something happens next->blocked ready This task is ready to be done: it is not blocked by anything ready->blocked blocked->goal finished This task is finished; the arrow indicates what follows this task (unless it's blocked) finished->goal roadmap-1998303f31df616bd952457cbf5f44fe391f586b/legend.yaml000066400000000000000000000011271455050602300221520ustar00rootroot00000000000000goal: label: | This is the end goal: if we reach here, there is nothing more to be done in the project depends: - finished - blocked finished: status: finished label: | This task is finished; the arrow indicates what follows this task (unless it's blocked) ready: label: | This task is ready to be done: it is not blocked by anything next: status: next label: | This task is chosen to be done next blocked: label: | This task is blocked and can't be done until something happens depends: - ready - next roadmap-1998303f31df616bd952457cbf5f44fe391f586b/src/000077500000000000000000000000001455050602300206165ustar00rootroot00000000000000roadmap-1998303f31df616bd952457cbf5f44fe391f586b/src/err.rs000066400000000000000000000013161455050602300217550ustar00rootroot00000000000000use thiserror::Error; /// Errors that can be returned for roadmaps. #[derive(Error, Debug)] pub enum RoadmapError { #[error("roadmap has no goals, must have exactly one")] NoGoals, #[error("too many goals, must have exactly one: found {count:}: {}", .names.join(", "))] ManyGoals { count: usize, names: Vec }, #[error("step {name:} depends on missing {missing:}")] MissingDep { name: String, missing: String }, #[error("step is not a mapping")] StepNotMapping, #[error("'depends' must be a list of step names")] DependsNotNames, #[error("unknown status: {0}")] UnknownStatus(String), #[error(transparent)] SerdeError(#[from] serde_yaml::Error), } roadmap-1998303f31df616bd952457cbf5f44fe391f586b/src/lib.rs000066400000000000000000000021671455050602300217400ustar00rootroot00000000000000//! Model project roadmaps and step dependencies //! //! This crate models a roadmap as steps, which may depend on each //! other, and a directed acyclic graph (DAG) of said steps, which //! leads to the end goal. There is no support for due dates, //! estimates, or other such project management features. These roadmaps only //! care about what steps need to be take, in what order, to reach the //! goal. //! //! # Example //! ``` //! # fn main() -> std::result::Result<(), Box> { //! let mut r = roadmap::from_yaml(" //! endgoal: //! label: The end goal //! depends: //! - first //! first: //! label: The first step //! ").unwrap(); //! //! let n: Vec<&str> = r.step_names().collect(); //! assert_eq!(n.len(), 2); //! assert!(n.contains(&"first")); //! assert!(n.contains(&"endgoal")); //! //! r.set_missing_statuses(); //! println!("{}", r.format_as_dot(30).unwrap()); //! //! # Ok(()) //! # } //! ``` mod err; pub use err::RoadmapError; mod status; pub use status::Status; mod step; pub use step::Step; mod map; pub use map::Roadmap; pub use map::RoadmapResult; mod parser; pub use parser::from_yaml; roadmap-1998303f31df616bd952457cbf5f44fe391f586b/src/map.rs000066400000000000000000000216031455050602300217430ustar00rootroot00000000000000use std::collections::HashMap; use textwrap::fill; pub use crate::from_yaml; pub use crate::RoadmapError; pub use crate::Status; pub use crate::Step; /// Error in Roadmap, from parsing or otherwise. pub type RoadmapResult = Result; /// Represent a full project roadmap. /// /// This stores all the steps needed to reach the end goal. See the /// crate leve documentation for an example. #[derive(Clone, Debug, Default)] pub struct Roadmap { steps: Vec, } impl Roadmap { /// Create a new, empty roadmap. /// /// You probably want the `from_yaml` function instead. pub fn new(map: HashMap) -> Self { Self { steps: map.values().cloned().collect(), } } // Find steps that nothing depends on. fn goals(&self) -> Vec<&Step> { self.steps .iter() .filter(|step| self.is_goal(step)) .collect() } /// Count number of steps that nothing depends on. pub fn count_goals(&self) -> usize { self.goals().len() } /// Iterate over step names. pub fn step_names(&self) -> impl Iterator { self.steps.iter().map(|step| step.name()) } /// Get a step, given its name. pub fn get_step(&self, name: &str) -> Option<&Step> { self.steps.iter().find(|step| step.name() == name) } /// Add a step to the roadmap. pub fn add_step(&mut self, step: Step) { self.steps.push(step); } // Get iterator over refs to steps. pub fn iter(&self) -> impl Iterator { self.steps.iter() } // Get iterator over mut refs to steps. pub fn iter_mut(&mut self) -> impl Iterator { 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 = 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(); } } /// 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(&self, step: &Step) -> Vec { 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 { self.steps.iter().all(|other| !other.depends_on(step)) } // Validate that the parsed, constructed roadmap is valid. pub fn validate(&self) -> RoadmapResult<()> { // Is there exactly one goal? let goals = self.goals(); let n = goals.len(); match n { 0 => return Err(RoadmapError::NoGoals), 1 => (), _ => { let names: Vec = goals.iter().map(|s| s.name().into()).collect(); return Err(RoadmapError::ManyGoals { count: n, names }); } } // Does every dependency exist? for step in self.iter() { for depname in step.dependencies() { match self.get_step(depname) { None => { return Err(RoadmapError::MissingDep { name: step.name().into(), missing: depname.into(), }) } Some(_) => (), } } } Ok(()) } /// 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 format_as_dot(&self, label_width: usize) -> RoadmapResult { 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::{from_yaml, Roadmap, Status, Step}; #[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; } " ); } } roadmap-1998303f31df616bd952457cbf5f44fe391f586b/src/parser.rs000066400000000000000000000034721455050602300224660ustar00rootroot00000000000000use std::collections::HashMap; pub use crate::Roadmap; pub use crate::RoadmapError; pub use crate::RoadmapResult; pub use crate::Status; pub use crate::Step; /// Create a new roadmap from a textual YAML representation. pub fn from_yaml(yaml: &str) -> RoadmapResult { let mut roadmap: HashMap = serde_yaml::from_str(yaml)?; for (name, step) in roadmap.iter_mut() { step.set_name(name); } let roadmap = Roadmap::new(roadmap); roadmap.validate()?; Ok(roadmap) } #[cfg(test)] mod tests { use super::from_yaml; #[test] fn yaml_is_empty() { assert!(from_yaml("").is_err()); } #[test] fn yaml_is_list() { assert!(from_yaml("[]").is_err()); } #[test] fn yaml_unknown_dep() { assert!(from_yaml("foo: {depends: [bar]}").is_err()); } #[test] fn yaml_unknown_status() { assert!(from_yaml(r#"foo: {status: "bar"}"#).is_err()); } #[test] fn yaml_happy() { let roadmap = from_yaml( " first: label: the first step second: label: the second step depends: - first ", ) .unwrap(); let names: Vec<&str> = roadmap.step_names().collect(); 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 = first.dependencies().count(); assert_eq!(deps, 0); let second = roadmap.get_step("second").unwrap(); assert_eq!(second.name(), "second"); assert_eq!(second.label(), "the second step"); let deps: Vec<&str> = second.dependencies().collect(); assert_eq!(deps, vec!["first"]); } } roadmap-1998303f31df616bd952457cbf5f44fe391f586b/src/status.rs000066400000000000000000000031211455050602300225040ustar00rootroot00000000000000use serde::Deserialize; /// 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, Copy, Debug, Eq, PartialEq, Deserialize)] #[serde(rename_all = "lowercase")] pub enum Status { Unknown, Goal, Finished, Ready, Next, Blocked, } impl Default for Status { fn default() -> Self { Self::Unknown } } impl Status { pub fn from_text(text: &str) -> Option { 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, } } } #[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); } } roadmap-1998303f31df616bd952457cbf5f44fe391f586b/src/step.rs000066400000000000000000000055171455050602300221470ustar00rootroot00000000000000use super::Status; use serde::Deserialize; use std::fmt; /// 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, Eq, Deserialize)] #[serde(deny_unknown_fields)] pub struct Step { #[serde(skip)] name: String, #[serde(default)] label: String, #[serde(default)] status: Status, #[serde(default)] depends: Vec, } 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![], } } /// Set the name of a step. pub fn set_name(&mut self, name: &str) { self.name = name.to_string(); } /// Return the name of a step. pub fn name(&self) -> &str { &self.name } /// Return the label of a step. pub fn label(&self) -> &str { &self.label } /// Return the status of a step. pub fn status(&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 { self.depends.iter().map(|s| s.as_str()) } /// 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(name.to_owned()); } /// Does this step depend on given other step? pub fn depends_on(&self, other: &Step) -> bool { self.depends.iter().any(|depname| depname == other.name()) } } impl fmt::Display for Step { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.name) } } #[cfg(test)] mod tests { use super::{Status, Step}; #[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<_> = second.dependencies().collect(); assert_eq!(deps, vec!["first"]); } }