Improve bin/gen error messages

Create an error enum with actual error messages.

type: development
This commit is contained in:
Casey Rodarmor 2020-04-29 00:20:04 -07:00
parent e396b7f071
commit 342266853e
No known key found for this signature in database
GPG Key ID: 556186B153EC6FE0
15 changed files with 218 additions and 108 deletions

9
Cargo.lock generated
View File

@ -27,12 +27,6 @@ dependencies = [
"winapi 0.3.8", "winapi 0.3.8",
] ]
[[package]]
name = "anyhow"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9a60d744a80c30fcb657dfe2c1b22bcb3e814c1a1e3674f32bf5820b570fbff"
[[package]] [[package]]
name = "array-init" name = "array-init"
version = "0.0.4" version = "0.0.4"
@ -364,18 +358,19 @@ checksum = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3"
name = "gen" name = "gen"
version = "0.0.0" version = "0.0.0"
dependencies = [ dependencies = [
"anyhow",
"askama", "askama",
"cargo_toml", "cargo_toml",
"chrono", "chrono",
"fehler", "fehler",
"git2", "git2",
"globset", "globset",
"libc",
"log", "log",
"pretty_env_logger", "pretty_env_logger",
"regex", "regex",
"serde", "serde",
"serde_yaml", "serde_yaml",
"snafu",
"structopt", "structopt",
"strum", "strum",
"strum_macros", "strum_macros",

View File

@ -6,17 +6,18 @@ edition = "2018"
publish = false publish = false
[dependencies] [dependencies]
anyhow = "1.0.28"
askama = "0.9.0" askama = "0.9.0"
cargo_toml = "0.8.0" cargo_toml = "0.8.0"
chrono = "0.4.11" chrono = "0.4.11"
fehler = "1.0.0" fehler = "1.0.0"
git2 = "0.13.1" git2 = "0.13.1"
globset = "0.4.5" globset = "0.4.5"
libc = "0.2.69"
log = "0.4.8" log = "0.4.8"
pretty_env_logger = "0.4.0" pretty_env_logger = "0.4.0"
regex = "1.3.6" regex = "1.3.6"
serde_yaml = "0.8.11" serde_yaml = "0.8.11"
snafu = "0.6.0"
structopt = "0.3.12" structopt = "0.3.12"
strum = "0.18.0" strum = "0.18.0"
strum_macros = "0.18.0" strum_macros = "0.18.0"

View File

@ -16,7 +16,7 @@ impl Changelog {
loop { loop {
let summary_bytes = current let summary_bytes = current
.summary_bytes() .summary_bytes()
.ok_or_else(|| anyhow!("Commit had no summary"))?; .ok_or_else(|| Error::CommitSummery { hash: current.id() })?;
let summary = String::from_utf8_lossy(summary_bytes); let summary = String::from_utf8_lossy(summary_bytes);
@ -47,7 +47,10 @@ impl Changelog {
match current.parent_count() { match current.parent_count() {
0 => break, 0 => break,
1 => current = current.parent(0)?, 1 => current = current.parent(0)?,
_ => throw!(anyhow!("Commit had multiple parents!")), other => throw!(Error::CommitParents {
hash: current.id(),
parents: other
}),
} }
} }

View File

@ -1,8 +1,12 @@
use crate::common::*; use crate::common::*;
#[allow(redundant_semicolons)]
pub(crate) trait CommandExt { pub(crate) trait CommandExt {
#[throws] #[throws]
fn out(&mut self) -> String; fn out(&mut self) -> String;
#[throws]
fn status_into_result(&mut self);
} }
impl CommandExt for Command { impl CommandExt for Command {
@ -13,12 +17,36 @@ impl CommandExt for Command {
let output = self let output = self
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.stderr(Stdio::inherit()) .stderr(Stdio::inherit())
.output()?; .output()
.context(error::CommandInvoke {
command: format!("{:?}", self),
})?;
output.status.into_result()?; if !output.status.success() {
throw!(Error::CommandStatus {
command: format!("{:?}", self),
exit_status: output.status,
});
}
let text = String::from_utf8(output.stdout)?; let text = String::from_utf8(output.stdout).context(error::CommandDecode {
command: format!("{:?}", self),
})?;
text text
} }
#[throws]
fn status_into_result(&mut self) {
let status = self.status().context(error::CommandInvoke {
command: format!("{:?}", self),
})?;
if !status.success() {
throw!(Error::CommandStatus {
command: format!("{:?}", self),
exit_status: status
});
}
}
} }

View File

@ -4,36 +4,40 @@ pub(crate) use std::{
env, env,
fmt::{self, Display, Formatter}, fmt::{self, Display, Formatter},
fs::{self, File}, fs::{self, File},
io,
ops::Deref,
path::{Path, PathBuf}, path::{Path, PathBuf},
process::{Command, ExitStatus, Stdio}, process::{self, Command, ExitStatus, Stdio},
str, str,
}; };
pub(crate) use anyhow::{anyhow, Error};
pub(crate) use askama::Template; pub(crate) use askama::Template;
pub(crate) use cargo_toml::Manifest; pub(crate) use cargo_toml::Manifest;
pub(crate) use chrono::{DateTime, NaiveDateTime, Utc}; pub(crate) use chrono::{DateTime, NaiveDateTime, Utc};
pub(crate) use fehler::{throw, throws}; pub(crate) use fehler::{throw, throws};
pub(crate) use git2::{Commit, Repository}; pub(crate) use git2::{Commit, Oid, Repository};
pub(crate) use libc::EXIT_FAILURE;
pub(crate) use log::info; pub(crate) use log::info;
pub(crate) use regex::Regex; pub(crate) use regex::Regex;
pub(crate) use serde::{Deserialize, Serialize}; pub(crate) use serde::{Deserialize, Serialize};
pub(crate) use snafu::{ResultExt, Snafu};
pub(crate) use std::string::FromUtf8Error;
pub(crate) use structopt::StructOpt; pub(crate) use structopt::StructOpt;
pub(crate) use strum::VariantNames; pub(crate) use strum::VariantNames;
pub(crate) use strum_macros::{EnumVariantNames, IntoStaticStr}; pub(crate) use strum_macros::{EnumVariantNames, IntoStaticStr};
pub(crate) use url::Url; pub(crate) use url::Url;
// modules
pub(crate) use crate::error;
// traits // traits
pub(crate) use crate::{ pub(crate) use crate::{command_ext::CommandExt, row::Row, slug::Slug, template_ext::TemplateExt};
command_ext::CommandExt, exit_status_ext::ExitStatusExt, row::Row, slug::Slug,
template_ext::TemplateExt,
};
// structs and enums // structs and enums
pub(crate) use crate::{ pub(crate) use crate::{
bin::Bin, changelog::Changelog, config::Config, entry::Entry, example::Example, faq::Faq, bin::Bin, changelog::Changelog, config::Config, entry::Entry, error::Error, example::Example,
faq_entry::FaqEntry, introduction::Introduction, kind::Kind, metadata::Metadata, opt::Opt, faq::Faq, faq_entry::FaqEntry, introduction::Introduction, kind::Kind, metadata::Metadata,
package::Package, project::Project, readme::Readme, reference::Reference, opt::Opt, package::Package, project::Project, readme::Readme, reference::Reference,
reference_section::ReferenceSection, release::Release, subcommand::Subcommand, summary::Summary, reference_section::ReferenceSection, release::Release, subcommand::Subcommand, summary::Summary,
table::Table, table::Table,
}; };

View File

@ -14,7 +14,8 @@ pub(crate) struct Config {
impl Config { impl Config {
#[throws] #[throws]
pub(crate) fn load(root: &Path) -> Config { pub(crate) fn load(root: &Path) -> Config {
let file = File::open(root.join(PATH))?; let path = root.join(PATH);
serde_yaml::from_reader(file)? let file = File::open(&path).context(error::Filesystem { path: &path })?;
serde_yaml::from_reader(file).context(error::ConfigDeserialize { path })?
} }
} }

88
bin/gen/src/error.rs Normal file
View File

@ -0,0 +1,88 @@
use crate::common::*;
#[derive(Debug, Snafu)]
#[snafu(visibility(pub(crate)))]
pub(crate) enum Error {
#[snafu(display("Failed to deserialize `Cargo.toml`: {}", source))]
CargoToml { source: cargo_toml::Error },
#[snafu(display("Failed to decode command `{}` output: {}", command, source))]
CommandDecode {
command: String,
source: FromUtf8Error,
},
#[snafu(display("Failed to invoke command `{}` output: {}", command, source))]
CommandInvoke { command: String, source: io::Error },
#[snafu(display("Command `{}` failed: {}", command, exit_status))]
CommandStatus {
command: String,
exit_status: ExitStatus,
},
#[snafu(display(
"Failed to deserialize commit metadata: {}\n{}\n{}",
source,
hash,
message
))]
CommitMetadataDeserialize {
hash: Oid,
message: String,
source: serde_yaml::Error,
},
#[snafu(display("Commit missing metadata:\n{}\n{}", hash, message))]
CommitMetadataMissing { hash: Oid, message: String },
#[snafu(display("Commit has `{}` parents: {}", hash, parents))]
CommitParents { hash: Oid, parents: usize },
#[snafu(display("Commit has no summery: {}", hash))]
CommitSummery { hash: Oid },
#[snafu(display("Failed to deserialize config from `{}`: {}", path.display(), source))]
ConfigDeserialize {
path: PathBuf,
source: serde_yaml::Error,
},
#[snafu(display("Failed to get current dir: {}", source))]
CurrentDir { source: io::Error },
#[snafu(display(
"Example commands `{}` don't match bin commands `{}`",
example.iter().map(|command| command.deref()).collect::<Vec<&str>>().join(","),
bin.iter().map(|command| command.deref()).collect::<Vec<&str>>().join(","),
))]
ExampleCommands {
example: BTreeSet<String>,
bin: BTreeSet<String>,
},
#[snafu(display("I/O error at `{}`: {}", path.display(), source))]
Filesystem { path: PathBuf, source: io::Error },
#[snafu(display("Git error: {}", source))]
Git { source: git2::Error },
#[snafu(display("Regex compilation error: {}", source))]
Regex { source: regex::Error },
#[snafu(display("Failed to find repository from `{}`: {}", start_dir.display(), source))]
RepositoryDiscover {
start_dir: PathBuf,
source: git2::Error,
},
#[snafu(display("Failed to create tempdir: {}", source))]
Tempdir { source: io::Error },
#[snafu(display("Failed to render template: {}", source))]
TemplateRender { source: askama::Error },
#[snafu(display("Failed to get workdir for repo at `{}`", repo.display()))]
Workdir { repo: PathBuf },
}
impl From<regex::Error> for Error {
fn from(source: regex::Error) -> Self {
Self::Regex { source }
}
}
impl From<git2::Error> for Error {
fn from(source: git2::Error) -> Self {
Self::Git { source }
}
}
impl From<cargo_toml::Error> for Error {
fn from(source: cargo_toml::Error) -> Self {
Self::CargoToml { source }
}
}

View File

@ -1,14 +0,0 @@
use crate::common::*;
pub(crate) trait ExitStatusExt {
fn into_result(self) -> anyhow::Result<()>;
}
impl ExitStatusExt for ExitStatus {
#[throws]
fn into_result(self) {
if !self.success() {
throw!(anyhow!(self));
}
}
}

View File

@ -9,8 +9,8 @@ mod command_ext;
mod common; mod common;
mod config; mod config;
mod entry; mod entry;
mod error;
mod example; mod example;
mod exit_status_ext;
mod faq; mod faq;
mod faq_entry; mod faq_entry;
mod introduction; mod introduction;
@ -30,11 +30,11 @@ mod summary;
mod table; mod table;
mod template_ext; mod template_ext;
#[throws]
fn main() { fn main() {
pretty_env_logger::init(); pretty_env_logger::init();
let project = Project::load()?; if let Err(error) = Opt::from_args().run() {
eprintln!("{}", error);
Opt::from_args().run(&project)?; process::exit(EXIT_FAILURE);
}
} }

View File

@ -19,11 +19,17 @@ impl Metadata {
let blank = message let blank = message
.rfind(BLANK) .rfind(BLANK)
.ok_or_else(|| anyhow!("Commit message missing metadata: {}", message))?; .ok_or_else(|| Error::CommitMetadataMissing {
hash: commit.id(),
message: message.to_string(),
})?;
let yaml = &message[blank + BLANK.len()..]; let yaml = &message[blank + BLANK.len()..];
let metadata = serde_yaml::from_str(yaml)?; let metadata = serde_yaml::from_str(yaml).context(error::CommitMetadataDeserialize {
hash: commit.id(),
message,
})?;
metadata metadata
} }

View File

@ -35,27 +35,29 @@ fn blank(path: impl AsRef<Path>, title: &str) {
title title
); );
fs::write(path, text)?; fs::write(&path, text).context(error::Filesystem { path })?;
} }
#[throws] #[throws]
fn clean_dir(dir: impl AsRef<Path>) { fn clean_dir(path: impl AsRef<Path>) {
let dir = dir.as_ref(); let path = path.as_ref();
info!("Cleaning `{}`…", dir.display()); info!("Cleaning `{}`…", path.display());
if dir.is_dir() { if path.is_dir() {
fs::remove_dir_all(dir)?; fs::remove_dir_all(path).context(error::Filesystem { path: &path })?;
} }
fs::create_dir_all(dir)?; fs::create_dir_all(path).context(error::Filesystem { path: &path })?;
} }
impl Opt { impl Opt {
#[throws] #[throws]
pub(crate) fn run(self, project: &Project) { pub(crate) fn run(self) {
let project = Project::load()?;
match self { match self {
Self::Changelog => Self::changelog(project)?, Self::Changelog => Self::changelog(&project)?,
Self::CommitTemplate => { Self::CommitTemplate => {
println!("{}", Metadata::default().to_string()); println!("{}", Metadata::default().to_string());
} }
@ -64,16 +66,16 @@ impl Opt {
println!("{}", kind) println!("{}", kind)
} }
} }
Self::CompletionScripts => Self::completion_scripts(project)?, Self::CompletionScripts => Self::completion_scripts(&project)?,
Self::Readme => Self::readme(project)?, Self::Readme => Self::readme(&project)?,
Self::Book => Self::book(project)?, Self::Book => Self::book(&project)?,
Self::Man => Self::man(project)?, Self::Man => Self::man(&project)?,
Self::All => { Self::All => {
Self::changelog(project)?; Self::changelog(&project)?;
Self::completion_scripts(project)?; Self::completion_scripts(&project)?;
Self::readme(project)?; Self::readme(&project)?;
Self::book(project)?; Self::book(&project)?;
Self::man(project)?; Self::man(&project)?;
} }
} }
} }
@ -83,9 +85,9 @@ impl Opt {
info!("Generating changelog…"); info!("Generating changelog…");
let changelog = Changelog::new(&project)?; let changelog = Changelog::new(&project)?;
let dst = project.root.join("CHANGELOG.md"); let path = project.root.join("CHANGELOG.md");
fs::write(dst, changelog.to_string())?; fs::write(&path, changelog.to_string()).context(error::Filesystem { path })?;
} }
#[throws] #[throws]
@ -104,20 +106,21 @@ impl Opt {
"--dir", "--dir",
completions completions
) )
.status()? .status_into_result()?
.into_result()?;
} }
#[throws] #[throws]
pub(crate) fn readme(project: &Project) { pub(crate) fn readme(project: &Project) {
info!("Generating readme…"); info!("Generating readme…");
let template = project.root.join("bin/gen/templates/README.md"); let template = project.root.join("bin/gen/templates/README.md");
let readme = Readme::load(&project.config, &template)?; let readme = Readme::load(&project.config, &template)?;
let text = readme.render_newline()?; let text = readme.render_newline()?;
fs::write(project.root.join("README.md"), text)?; let path = project.root.join("README.md");
fs::write(&path, text).context(error::Filesystem { path })?;
} }
#[throws] #[throws]
@ -137,35 +140,20 @@ impl Opt {
let dst = commands.join(format!("{}.md", subcommand.slug())); let dst = commands.join(format!("{}.md", subcommand.slug()));
fs::write(dst, page)?; fs::write(&dst, page).context(error::Filesystem { path: dst })?;
} }
let references = project.root.join("book/src/references/"); clean_dir(&project.root.join("book/src/references/"))?;
clean_dir(&references)?;
for section in &project.config.references { for section in &project.config.references {
let text = section.render_newline()?; section.render_to(project.root.join("book/src").join(section.path()))?;
let path = project.root.join("book/src").join(section.path());
fs::write(path, text)?;
} }
let faq = Faq::new(&project.config.faq); Faq::new(&project.config.faq).render_to(project.root.join("book/src/faq.md"))?;
fs::write(project.root.join("book/src/faq.md"), faq.render_newline()?)?; Summary::new(project).render_to(project.root.join("book/src/SUMMARY.md"))?;
let summary = Summary::new(project); Introduction::new(&project.config).render_to(project.root.join("book/src/introduction.md"))?;
let text = summary.render_newline()?;
fs::write(project.root.join("book/src/SUMMARY.md"), text)?;
let introduction = Introduction::new(&project.config);
let text = introduction.render_newline()?;
fs::write(project.root.join("book/src/introduction.md"), text)?;
} }
#[throws] #[throws]
@ -182,7 +170,7 @@ impl Opt {
info!("Writing man page to `{}`", dst.display()); info!("Writing man page to `{}`", dst.display());
fs::write(dst, man)?; fs::write(&dst, man).context(error::Filesystem { path: dst })?;
} }
} }
} }

View File

@ -10,11 +10,15 @@ pub(crate) struct Project {
impl Project { impl Project {
#[throws] #[throws]
pub(crate) fn load() -> Self { pub(crate) fn load() -> Self {
let repo = Repository::discover(env::current_dir()?)?; let start_dir = env::current_dir().context(error::CurrentDir)?;
let repo = Repository::discover(&start_dir).context(error::RepositoryDiscover { start_dir })?;
let root = repo let root = repo
.workdir() .workdir()
.ok_or_else(|| anyhow!("Repository at `{}` had no workdir", repo.path().display()))? .ok_or_else(|| Error::Workdir {
repo: repo.path().to_owned(),
})?
.to_owned(); .to_owned();
let config = Config::load(&root)?; let config = Config::load(&root)?;
@ -34,17 +38,10 @@ impl Project {
.collect::<BTreeSet<String>>(); .collect::<BTreeSet<String>>();
if example_commands != bin_commands { if example_commands != bin_commands {
println!("Example commands:"); throw!(Error::ExampleCommands {
for command in example_commands { example: example_commands,
println!("{}", command); bin: bin_commands
} });
println!("…don't match bin commands:");
for command in bin_commands {
println!("{}", command);
}
throw!(anyhow!(""));
} }
Project { Project {

View File

@ -12,7 +12,7 @@ const HEADING_PATTERN: &str = "(?m)^(?P<MARKER>#+) (?P<TEXT>.*)$";
impl Readme { impl Readme {
#[throws] #[throws]
pub(crate) fn load(config: &Config, template: &Path) -> Readme { pub(crate) fn load(config: &Config, template: &Path) -> Readme {
let text = fs::read_to_string(template)?; let text = fs::read_to_string(template).context(error::Filesystem { path: template })?;
let header_re = Regex::new(HEADING_PATTERN)?; let header_re = Regex::new(HEADING_PATTERN)?;

View File

@ -72,11 +72,13 @@ impl Subcommand {
name, description name, description
); );
let tmp = tempfile::tempdir()?; let tmp = tempfile::tempdir().context(error::Tempdir)?;
let include_path = tmp.path().join("include"); let include_path = tmp.path().join("include");
fs::write(&include_path, include)?; fs::write(&include_path, include).context(error::Filesystem {
path: &include_path,
})?;
let version = cmd!(&self.bin, "--version") let version = cmd!(&self.bin, "--version")
.out()? .out()?

View File

@ -3,12 +3,23 @@ use crate::common::*;
pub(crate) trait TemplateExt { pub(crate) trait TemplateExt {
#[throws] #[throws]
fn render_newline(&self) -> String; fn render_newline(&self) -> String;
#[throws]
fn render_to(&self, path: impl AsRef<Path>) {
let path = path.as_ref();
let text = self.render_newline()?;
fs::write(&path, text).context(error::Filesystem { path })?;
}
} }
impl<T: Template> TemplateExt for T { impl<T: Template> TemplateExt for T {
#[throws] #[throws]
fn render_newline(&self) -> String { fn render_newline(&self) -> String {
let mut text = self.render()?.trim().to_owned(); let mut text = self
.render()
.context(error::TemplateRender)?
.trim()
.to_owned();
text.push('\n'); text.push('\n');
text text
} }