diff options
author | Lars Wirzenius <liw@liw.fi> | 2020-03-20 07:32:34 +0000 |
---|---|---|
committer | Lars Wirzenius <liw@liw.fi> | 2020-03-20 07:32:34 +0000 |
commit | 931a8751d956741e29dae27c5bd2c7dbf9ff2d9a (patch) | |
tree | 889930448ab8764dc8f37840da4a07dd750ac3e1 | |
parent | 0cb84f03427e4159a08f18263784b907ccb87022 (diff) | |
parent | 5e5891771efd0530b1e648c1acb6d673b032e432 (diff) | |
download | roadmap-931a8751d956741e29dae27c5bd2c7dbf9ff2d9a.tar.gz |
Merge branch 'errors' into 'master'
Errors
See merge request larswirzenius/roadmap!1
-rw-r--r-- | Cargo.toml | 4 | ||||
-rw-r--r-- | legend.svg | 96 | ||||
-rw-r--r-- | src/bin/roadmap2dot.rs | 3 | ||||
-rw-r--r-- | src/err.rs | 33 | ||||
-rw-r--r-- | src/lib.rs | 3 | ||||
-rw-r--r-- | src/map.rs | 40 | ||||
-rw-r--r-- | src/parser.rs | 12 | ||||
-rw-r--r-- | src/step.rs | 7 |
8 files changed, 130 insertions, 68 deletions
@@ -1,6 +1,6 @@ [package] name = "roadmap" -version = "0.1.0" +version = "0.1.1" authors = ["Lars Wirzenius <liw@liw.fi>"] edition = "2018" license = "GPL-3.0-or-later" @@ -12,3 +12,5 @@ description = "model a project roadmap as a directed acyclic graph" serde_yaml = "0.8.9" structopt = "0.3" textwrap = "0.11.0" +thiserror = "1.0" +anyhow = "1.0" @@ -4,75 +4,75 @@ <!-- Generated by graphviz version 2.40.1 (20161225.0304) --> <!-- Title: roadmap Pages: 1 --> -<svg width="494pt" height="503pt" - viewBox="0.00 0.00 493.75 503.09" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> -<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 499.0854)"> +<svg width="511pt" height="395pt" + viewBox="0.00 0.00 511.49 395.04" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> +<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 391.0432)"> <title>roadmap</title> -<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-499.0854 489.7473,-499.0854 489.7473,4 -4,4"/> -<!-- next --> +<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-391.0432 507.4863,-391.0432 507.4863,4 -4,4"/> +<!-- goal --> <g id="node1" class="node"> +<title>goal</title> +<polygon fill="#00eeee" stroke="#000000" points="286.6812,-136 147.6812,-68 286.6812,0 425.6812,-68 286.6812,-136"/> +<text text-anchor="middle" x="286.6812" y="-86.8" font-family="Times,serif" font-size="14.00" fill="#000000">This is the end goal:</text> +<text text-anchor="middle" x="286.6812" y="-71.8" font-family="Times,serif" font-size="14.00" fill="#000000">if we reach here, there</text> +<text text-anchor="middle" x="286.6812" y="-56.8" font-family="Times,serif" font-size="14.00" fill="#000000">is nothing more to be</text> +<text text-anchor="middle" x="286.6812" y="-41.8" font-family="Times,serif" font-size="14.00" fill="#000000">done in the project</text> +</g> +<!-- next --> +<g id="node2" class="node"> <title>next</title> -<ellipse fill="#0cc000" stroke="#000000" cx="87.6812" cy="-457.6087" rx="87.8629" ry="26.7407"/> -<text text-anchor="middle" x="87.6812" y="-461.4087" font-family="Times,serif" font-size="14.00" fill="#000000">This task is chosen </text> -<text text-anchor="middle" x="87.6812" y="-446.4087" font-family="Times,serif" font-size="14.00" fill="#000000">to be done next</text> +<ellipse fill="#0cc000" stroke="#000000" cx="87.6812" cy="-349.5666" rx="87.8629" ry="26.7407"/> +<text text-anchor="middle" x="87.6812" y="-353.3666" font-family="Times,serif" font-size="14.00" fill="#000000">This task is chosen </text> +<text text-anchor="middle" x="87.6812" y="-338.3666" font-family="Times,serif" font-size="14.00" fill="#000000">to be done next</text> </g> <!-- blocked --> <g id="node4" class="node"> <title>blocked</title> -<polygon fill="#f4bada" stroke="#000000" points="255.6812,-304.566 115.6812,-304.566 115.6812,-251.566 255.6812,-251.566 255.6812,-304.566"/> -<text text-anchor="middle" x="185.6812" y="-289.366" font-family="Times,serif" font-size="14.00" fill="#000000">This task is blocked</text> -<text text-anchor="middle" x="185.6812" y="-274.366" font-family="Times,serif" font-size="14.00" fill="#000000">and can't be done until</text> -<text text-anchor="middle" x="185.6812" y="-259.366" font-family="Times,serif" font-size="14.00" fill="#000000">something happens</text> +<polygon fill="#f4bada" stroke="#000000" points="255.6812,-250.5449 115.6812,-250.5449 115.6812,-197.5449 255.6812,-197.5449 255.6812,-250.5449"/> +<text text-anchor="middle" x="185.6812" y="-235.3449" font-family="Times,serif" font-size="14.00" fill="#000000">This task is blocked</text> +<text text-anchor="middle" x="185.6812" y="-220.3449" font-family="Times,serif" font-size="14.00" fill="#000000">and can't be done until</text> +<text text-anchor="middle" x="185.6812" y="-205.3449" font-family="Times,serif" font-size="14.00" fill="#000000">something happens</text> </g> <!-- next->blocked --> <g id="edge4" class="edge"> <title>next->blocked</title> -<path fill="none" stroke="#000000" d="M102.3528,-430.7293C119.3613,-399.5686 147.4594,-348.0911 166.2652,-313.6375"/> -<polygon fill="#000000" stroke="#000000" points="169.3507,-315.29 171.0696,-304.8355 163.2063,-311.9362 169.3507,-315.29"/> +<path fill="none" stroke="#000000" d="M108.2253,-323.2531C122.8269,-304.5508 142.5877,-279.2406 158.4948,-258.8662"/> +<polygon fill="#000000" stroke="#000000" points="161.4315,-260.7922 164.8268,-250.7561 155.914,-256.4844 161.4315,-260.7922"/> </g> -<!-- finished --> -<g id="node2" class="node"> -<title>finished</title> -<ellipse fill="#eeeeee" stroke="#000000" cx="379.6812" cy="-278.066" rx="106.1321" ry="106.1321"/> -<text text-anchor="middle" x="379.6812" y="-296.866" font-family="Times,serif" font-size="14.00" fill="#000000">This task is finished;</text> -<text text-anchor="middle" x="379.6812" y="-281.866" font-family="Times,serif" font-size="14.00" fill="#000000">the arrow indicates what</text> -<text text-anchor="middle" x="379.6812" y="-266.866" font-family="Times,serif" font-size="14.00" fill="#000000">follows this task (unless</text> -<text text-anchor="middle" x="379.6812" y="-251.866" font-family="Times,serif" font-size="14.00" fill="#000000">it's blocked)</text> -</g> -<!-- goal --> +<!-- ready --> <g id="node3" class="node"> -<title>goal</title> -<polygon fill="#00eeee" stroke="#000000" points="282.6812,-136 143.6812,-68 282.6812,0 421.6812,-68 282.6812,-136"/> -<text text-anchor="middle" x="282.6812" y="-86.8" font-family="Times,serif" font-size="14.00" fill="#000000">This is the end goal:</text> -<text text-anchor="middle" x="282.6812" y="-71.8" font-family="Times,serif" font-size="14.00" fill="#000000">if we reach here, there</text> -<text text-anchor="middle" x="282.6812" y="-56.8" font-family="Times,serif" font-size="14.00" fill="#000000">is nothing more to be</text> -<text text-anchor="middle" x="282.6812" y="-41.8" font-family="Times,serif" font-size="14.00" fill="#000000">done in the project</text> +<title>ready</title> +<ellipse fill="#ffffff" stroke="#000000" cx="283.6812" cy="-349.5666" rx="90.5193" ry="37.4533"/> +<text text-anchor="middle" x="283.6812" y="-360.8666" font-family="Times,serif" font-size="14.00" fill="#000000">This task is ready </text> +<text text-anchor="middle" x="283.6812" y="-345.8666" font-family="Times,serif" font-size="14.00" fill="#000000">to be done: it is not</text> +<text text-anchor="middle" x="283.6812" y="-330.8666" font-family="Times,serif" font-size="14.00" fill="#000000">blocked by anything</text> </g> -<!-- finished->goal --> -<g id="edge1" class="edge"> -<title>finished->goal</title> -<path fill="none" stroke="#000000" d="M335.1558,-181.6404C327.5669,-165.2057 319.8473,-148.488 312.7471,-133.1114"/> -<polygon fill="#000000" stroke="#000000" points="315.7827,-131.3367 308.4128,-123.7251 309.4275,-134.2713 315.7827,-131.3367"/> +<!-- ready->blocked --> +<g id="edge3" class="edge"> +<title>ready->blocked</title> +<path fill="none" stroke="#000000" d="M255.5984,-313.5971C242.183,-296.4143 226.1925,-275.933 212.8917,-258.897"/> +<polygon fill="#000000" stroke="#000000" points="215.5142,-256.5685 206.6014,-250.8402 209.9966,-260.8763 215.5142,-256.5685"/> </g> <!-- blocked->goal --> <g id="edge2" class="edge"> <title>blocked->goal</title> -<path fill="none" stroke="#000000" d="M197.9664,-251.461C211.6819,-221.7581 234.3749,-172.6136 252.8233,-132.6612"/> -<polygon fill="#000000" stroke="#000000" points="256.0362,-134.052 257.0509,-123.5058 249.681,-131.1174 256.0362,-134.052"/> +<path fill="none" stroke="#000000" d="M203.0438,-197.2199C215.3004,-178.2833 232.2198,-152.1429 247.6358,-128.3251"/> +<polygon fill="#000000" stroke="#000000" points="250.7406,-129.9697 253.236,-119.6729 244.8641,-126.1661 250.7406,-129.9697"/> </g> -<!-- ready --> +<!-- finished --> <g id="node5" class="node"> -<title>ready</title> -<ellipse fill="#ffffff" stroke="#000000" cx="283.6812" cy="-457.6087" rx="90.5193" ry="37.4533"/> -<text text-anchor="middle" x="283.6812" y="-468.9087" font-family="Times,serif" font-size="14.00" fill="#000000">This task is ready </text> -<text text-anchor="middle" x="283.6812" y="-453.9087" font-family="Times,serif" font-size="14.00" fill="#000000">to be done: it is not</text> -<text text-anchor="middle" x="283.6812" y="-438.9087" font-family="Times,serif" font-size="14.00" fill="#000000">blocked by anything</text> +<title>finished</title> +<polygon fill="#eeeeee" stroke="#000000" points="503.2916,-202.4686 503.2916,-245.6213 436.1544,-276.1349 341.2081,-276.1349 274.0709,-245.6213 274.0709,-202.4686 341.2081,-171.955 436.1544,-171.955 503.2916,-202.4686"/> +<text text-anchor="middle" x="388.6812" y="-242.8449" font-family="Times,serif" font-size="14.00" fill="#000000">This task is finished;</text> +<text text-anchor="middle" x="388.6812" y="-227.8449" font-family="Times,serif" font-size="14.00" fill="#000000">the arrow indicates what</text> +<text text-anchor="middle" x="388.6812" y="-212.8449" font-family="Times,serif" font-size="14.00" fill="#000000">follows this task (unless</text> +<text text-anchor="middle" x="388.6812" y="-197.8449" font-family="Times,serif" font-size="14.00" fill="#000000">it's blocked)</text> </g> -<!-- ready->blocked --> -<g id="edge3" class="edge"> -<title>ready->blocked</title> -<path fill="none" stroke="#000000" d="M263.613,-420.8423C246.5107,-389.5098 222.0826,-344.7558 205.1534,-313.7404"/> -<polygon fill="#000000" stroke="#000000" points="208.02,-311.6868 200.1567,-304.5861 201.8757,-315.0405 208.02,-311.6868"/> +<!-- finished->goal --> +<g id="edge1" class="edge"> +<title>finished->goal</title> +<path fill="none" stroke="#000000" d="M354.433,-171.6501C345.3746,-157.7922 335.5231,-142.7208 326.2098,-128.4729"/> +<polygon fill="#000000" stroke="#000000" points="328.8936,-126.1816 320.4925,-119.7262 323.0343,-130.0116 328.8936,-126.1816"/> </g> </g> </svg> diff --git a/src/bin/roadmap2dot.rs b/src/bin/roadmap2dot.rs index 244a300..b180da8 100644 --- a/src/bin/roadmap2dot.rs +++ b/src/bin/roadmap2dot.rs @@ -16,6 +16,7 @@ use std::fs::File; use std::io::Read; use std::path::PathBuf; use structopt::StructOpt; +use anyhow::Result; const LABEL_WIDTH: usize = 30; @@ -30,7 +31,7 @@ struct Opt { filename: PathBuf, } -fn main() -> Result<(), Box<dyn std::error::Error>> { +fn main() -> Result<()> { let opt = Opt::from_args(); let mut text = String::new(); let mut f = File::open(opt.filename)?; diff --git a/src/err.rs b/src/err.rs new file mode 100644 index 0000000..6a224a1 --- /dev/null +++ b/src/err.rs @@ -0,0 +1,33 @@ +use serde_yaml; +use 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<String>, + }, + + #[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), +} @@ -31,6 +31,9 @@ //! # } //! ``` +mod err; +pub use err::RoadmapError; + mod status; pub use status::Status; @@ -1,12 +1,12 @@ -use std::error::Error; 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<T> = Result<T, Box<dyn Error>>; +pub type RoadmapResult<T> = Result<T, RoadmapError>; /// Represent a full project roadmap. /// @@ -25,9 +25,17 @@ impl Roadmap { Roadmap { steps: vec![] } } + // 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.steps.iter().filter(|step| self.is_goal(step)).count() + self.goals().len() } /// Iterate over step names. @@ -120,21 +128,29 @@ impl Roadmap { // Validate that the parsed, constructed roadmap is valid. pub fn validate(&self) -> RoadmapResult<()> { // Is there exactly one goal? - match self.count_goals() { - 0 => return Err(format!("the roadmap doesn't have a goal").into()), + let goals = self.goals(); + let n = goals.len(); + match n { + 0 => return Err(RoadmapError::NoGoals), 1 => (), - _ => return Err(format!("must have exactly one goal for roadmap").into()), + _ => { + let names: Vec<String> = goals.iter().map(|s| s.name().into()).collect(); + return Err(RoadmapError::ManyGoals { + count: n, + names: names, + }); + } } // Does every dependency exist? for step in self.iter() { for depname in step.dependencies() { match self.get_step(depname) { - None => { - return Err( - format!("step {} depends on missing {}", step.name(), depname).into(), - ) - } + None => + return Err(RoadmapError::MissingDep { + name: step.name().into(), + missing: depname.into(), + }), Some(_) => (), } } @@ -191,7 +207,7 @@ impl Roadmap { fn get_status_shape(step: &Step) -> &str { match step.status() { Status::Blocked => "rectangle", - Status::Finished => "circle", + Status::Finished => "octagon", Status::Ready => "ellipse", Status::Next => "ellipse", Status::Goal => "diamond", diff --git a/src/parser.rs b/src/parser.rs index 7da2254..7602b94 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3,6 +3,7 @@ use serde_yaml::Value; use std::collections::HashMap; pub use crate::Roadmap; +pub use crate::RoadmapError; pub use crate::RoadmapResult; pub use crate::Status; pub use crate::Step; @@ -37,7 +38,7 @@ fn step_from_value(name: &str, value: &Value) -> RoadmapResult<Step> { Ok(step) } - _ => Err("step is not a mapping".to_string().into()), + _ => Err(RoadmapError::StepNotMapping), } } @@ -46,19 +47,18 @@ fn parse_depends(map: &Value) -> RoadmapResult<Vec<&str>> { let key_name = "depends"; let key = Value::String(key_name.to_string()); let mut depends: Vec<&str> = vec![]; - let need_list_of_names = format!("'depends' must be a list of step names"); - + match map.get(&key) { None => (), Some(Value::Sequence(deps)) => { for depname in deps.iter() { match depname { Value::String(depname) => depends.push(depname), - _ => return Err(need_list_of_names.into()), + _ => return Err(RoadmapError::DependsNotNames), } } } - _ => return Err(need_list_of_names.into()), + _ => return Err(RoadmapError::DependsNotNames), } Ok(depends) @@ -74,7 +74,7 @@ fn parse_status<'a>(map: &'a Value) -> RoadmapResult<Status> { let text = parse_string("status", map); match Status::from_text(text) { Some(status) => Ok(status), - _ => Err(format!("unknown status: {:?}", text).into()), + _ => Err(RoadmapError::UnknownStatus(text.into())), } } diff --git a/src/step.rs b/src/step.rs index 1ed495b..92b8eec 100644 --- a/src/step.rs +++ b/src/step.rs @@ -1,4 +1,5 @@ use super::Status; +use std::fmt; /// A roadmap step. /// @@ -62,6 +63,12 @@ impl Step { } } +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}; |