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

View File

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

View File

@ -16,7 +16,7 @@ impl Changelog {
loop {
let summary_bytes = current
.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);
@ -47,7 +47,10 @@ impl Changelog {
match current.parent_count() {
0 => break,
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::*;
#[allow(redundant_semicolons)]
pub(crate) trait CommandExt {
#[throws]
fn out(&mut self) -> String;
#[throws]
fn status_into_result(&mut self);
}
impl CommandExt for Command {
@ -13,12 +17,36 @@ impl CommandExt for Command {
let output = self
.stdout(Stdio::piped())
.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
}
#[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,
fmt::{self, Display, Formatter},
fs::{self, File},
io,
ops::Deref,
path::{Path, PathBuf},
process::{Command, ExitStatus, Stdio},
process::{self, Command, ExitStatus, Stdio},
str,
};
pub(crate) use anyhow::{anyhow, Error};
pub(crate) use askama::Template;
pub(crate) use cargo_toml::Manifest;
pub(crate) use chrono::{DateTime, NaiveDateTime, Utc};
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 regex::Regex;
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 strum::VariantNames;
pub(crate) use strum_macros::{EnumVariantNames, IntoStaticStr};
pub(crate) use url::Url;
// modules
pub(crate) use crate::error;
// traits
pub(crate) use crate::{
command_ext::CommandExt, exit_status_ext::ExitStatusExt, row::Row, slug::Slug,
template_ext::TemplateExt,
};
pub(crate) use crate::{command_ext::CommandExt, row::Row, slug::Slug, template_ext::TemplateExt};
// structs and enums
pub(crate) use crate::{
bin::Bin, changelog::Changelog, config::Config, entry::Entry, example::Example, faq::Faq,
faq_entry::FaqEntry, introduction::Introduction, kind::Kind, metadata::Metadata, opt::Opt,
package::Package, project::Project, readme::Readme, reference::Reference,
bin::Bin, changelog::Changelog, config::Config, entry::Entry, error::Error, example::Example,
faq::Faq, faq_entry::FaqEntry, introduction::Introduction, kind::Kind, metadata::Metadata,
opt::Opt, package::Package, project::Project, readme::Readme, reference::Reference,
reference_section::ReferenceSection, release::Release, subcommand::Subcommand, summary::Summary,
table::Table,
};

View File

@ -14,7 +14,8 @@ pub(crate) struct Config {
impl Config {
#[throws]
pub(crate) fn load(root: &Path) -> Config {
let file = File::open(root.join(PATH))?;
serde_yaml::from_reader(file)?
let path = root.join(PATH);
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 config;
mod entry;
mod error;
mod example;
mod exit_status_ext;
mod faq;
mod faq_entry;
mod introduction;
@ -30,11 +30,11 @@ mod summary;
mod table;
mod template_ext;
#[throws]
fn main() {
pretty_env_logger::init();
let project = Project::load()?;
Opt::from_args().run(&project)?;
if let Err(error) = Opt::from_args().run() {
eprintln!("{}", error);
process::exit(EXIT_FAILURE);
}
}

View File

@ -19,11 +19,17 @@ impl Metadata {
let blank = message
.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 metadata = serde_yaml::from_str(yaml)?;
let metadata = serde_yaml::from_str(yaml).context(error::CommitMetadataDeserialize {
hash: commit.id(),
message,
})?;
metadata
}

View File

@ -35,27 +35,29 @@ fn blank(path: impl AsRef<Path>, title: &str) {
title
);
fs::write(path, text)?;
fs::write(&path, text).context(error::Filesystem { path })?;
}
#[throws]
fn clean_dir(dir: impl AsRef<Path>) {
let dir = dir.as_ref();
fn clean_dir(path: impl AsRef<Path>) {
let path = path.as_ref();
info!("Cleaning `{}`…", dir.display());
info!("Cleaning `{}`…", path.display());
if dir.is_dir() {
fs::remove_dir_all(dir)?;
if path.is_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 {
#[throws]
pub(crate) fn run(self, project: &Project) {
pub(crate) fn run(self) {
let project = Project::load()?;
match self {
Self::Changelog => Self::changelog(project)?,
Self::Changelog => Self::changelog(&project)?,
Self::CommitTemplate => {
println!("{}", Metadata::default().to_string());
}
@ -64,16 +66,16 @@ impl Opt {
println!("{}", kind)
}
}
Self::CompletionScripts => Self::completion_scripts(project)?,
Self::Readme => Self::readme(project)?,
Self::Book => Self::book(project)?,
Self::Man => Self::man(project)?,
Self::CompletionScripts => Self::completion_scripts(&project)?,
Self::Readme => Self::readme(&project)?,
Self::Book => Self::book(&project)?,
Self::Man => Self::man(&project)?,
Self::All => {
Self::changelog(project)?;
Self::completion_scripts(project)?;
Self::readme(project)?;
Self::book(project)?;
Self::man(project)?;
Self::changelog(&project)?;
Self::completion_scripts(&project)?;
Self::readme(&project)?;
Self::book(&project)?;
Self::man(&project)?;
}
}
}
@ -83,9 +85,9 @@ impl Opt {
info!("Generating changelog…");
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]
@ -104,20 +106,21 @@ impl Opt {
"--dir",
completions
)
.status()?
.into_result()?;
.status_into_result()?
}
#[throws]
pub(crate) fn readme(project: &Project) {
info!("Generating readme…");
let template = project.root.join("bin/gen/templates/README.md");
let readme = Readme::load(&project.config, &template)?;
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]
@ -137,35 +140,20 @@ impl Opt {
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(&references)?;
clean_dir(&project.root.join("book/src/references/"))?;
for section in &project.config.references {
let text = section.render_newline()?;
let path = project.root.join("book/src").join(section.path());
fs::write(path, text)?;
section.render_to(project.root.join("book/src").join(section.path()))?;
}
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);
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)?;
Introduction::new(&project.config).render_to(project.root.join("book/src/introduction.md"))?;
}
#[throws]
@ -182,7 +170,7 @@ impl Opt {
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 {
#[throws]
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
.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();
let config = Config::load(&root)?;
@ -34,17 +38,10 @@ impl Project {
.collect::<BTreeSet<String>>();
if example_commands != bin_commands {
println!("Example commands:");
for command in example_commands {
println!("{}", command);
}
println!("…don't match bin commands:");
for command in bin_commands {
println!("{}", command);
}
throw!(anyhow!(""));
throw!(Error::ExampleCommands {
example: example_commands,
bin: bin_commands
});
}
Project {

View File

@ -12,7 +12,7 @@ const HEADING_PATTERN: &str = "(?m)^(?P<MARKER>#+) (?P<TEXT>.*)$";
impl Readme {
#[throws]
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)?;

View File

@ -72,11 +72,13 @@ impl Subcommand {
name, description
);
let tmp = tempfile::tempdir()?;
let tmp = tempfile::tempdir().context(error::Tempdir)?;
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")
.out()?

View File

@ -3,12 +3,23 @@ use crate::common::*;
pub(crate) trait TemplateExt {
#[throws]
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 {
#[throws]
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
}