summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2020-03-20 07:32:34 +0000
committerLars Wirzenius <liw@liw.fi>2020-03-20 07:32:34 +0000
commit931a8751d956741e29dae27c5bd2c7dbf9ff2d9a (patch)
tree889930448ab8764dc8f37840da4a07dd750ac3e1
parent0cb84f03427e4159a08f18263784b907ccb87022 (diff)
parent5e5891771efd0530b1e648c1acb6d673b032e432 (diff)
downloadroadmap-931a8751d956741e29dae27c5bd2c7dbf9ff2d9a.tar.gz
Merge branch 'errors' into 'master'
Errors See merge request larswirzenius/roadmap!1
-rw-r--r--Cargo.toml4
-rw-r--r--legend.svg96
-rw-r--r--src/bin/roadmap2dot.rs3
-rw-r--r--src/err.rs33
-rw-r--r--src/lib.rs3
-rw-r--r--src/map.rs40
-rw-r--r--src/parser.rs12
-rw-r--r--src/step.rs7
8 files changed, 130 insertions, 68 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 7ada85b..3a6827a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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"
diff --git a/legend.svg b/legend.svg
index a7856d6..b358188 100644
--- a/legend.svg
+++ b/legend.svg
@@ -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&#39;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&#39;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&#45;&gt;blocked -->
<g id="edge4" class="edge">
<title>next&#45;&gt;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&#39;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&#45;&gt;goal -->
-<g id="edge1" class="edge">
-<title>finished&#45;&gt;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&#45;&gt;blocked -->
+<g id="edge3" class="edge">
+<title>ready&#45;&gt;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&#45;&gt;goal -->
<g id="edge2" class="edge">
<title>blocked&#45;&gt;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&#39;s blocked)</text>
</g>
-<!-- ready&#45;&gt;blocked -->
-<g id="edge3" class="edge">
-<title>ready&#45;&gt;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&#45;&gt;goal -->
+<g id="edge1" class="edge">
+<title>finished&#45;&gt;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),
+}
diff --git a/src/lib.rs b/src/lib.rs
index d3f672b..63497bb 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -31,6 +31,9 @@
//! # }
//! ```
+mod err;
+pub use err::RoadmapError;
+
mod status;
pub use status::Status;
diff --git a/src/map.rs b/src/map.rs
index be81216..671318e 100644
--- a/src/map.rs
+++ b/src/map.rs
@@ -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};