feat: initial package draft
This commit is contained in:
+129
@@ -0,0 +1,129 @@
|
||||
//! One error type for the whole crate.
|
||||
//!
|
||||
//! Every fallible operation returns [`Error`]. Variants carry the path or
|
||||
//! identifier at fault so a message is actionable without a backtrace. The
|
||||
//! [`Error::Invalid`] variant carries a list of problems rather than one,
|
||||
//! because validation is meant to report everything wrong with a file in a
|
||||
//! single pass instead of making the author fix one issue per run.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// The crate result alias.
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Anything that can go wrong loading, validating, or transforming course data.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// A file could not be read or written.
|
||||
#[error("cannot read or write {path}: {source}")]
|
||||
Io {
|
||||
/// The offending path.
|
||||
path: PathBuf,
|
||||
/// The underlying I/O failure.
|
||||
source: std::io::Error,
|
||||
},
|
||||
|
||||
/// An I/O failure with no natural path to attach.
|
||||
#[error("i/o error: {0}")]
|
||||
BareIo(#[from] std::io::Error),
|
||||
|
||||
/// A YAML file did not match the schema.
|
||||
#[error("{path} is not valid coursebank YAML: {source}")]
|
||||
Yaml {
|
||||
/// The offending path.
|
||||
path: PathBuf,
|
||||
/// The parse failure, which includes a line and column.
|
||||
source: serde_yaml_ng::Error,
|
||||
},
|
||||
|
||||
/// A JSON file did not parse.
|
||||
#[error("{path} is not valid JSON: {source}")]
|
||||
Json {
|
||||
/// The offending path.
|
||||
path: PathBuf,
|
||||
/// The parse failure.
|
||||
source: serde_json::Error,
|
||||
},
|
||||
|
||||
/// A CSV file did not parse.
|
||||
#[error("{path} is not valid CSV: {source}")]
|
||||
Csv {
|
||||
/// The offending path.
|
||||
path: PathBuf,
|
||||
/// The parse failure.
|
||||
source: csv::Error,
|
||||
},
|
||||
|
||||
/// A file was structurally fine but semantically wrong. Holds every problem
|
||||
/// found so one run fixes one file.
|
||||
#[error("{} problem(s) found:\n{}", .0.len(), format_issues(.0))]
|
||||
Invalid(Vec<String>),
|
||||
|
||||
/// A reference did not resolve: an unknown objective, lecture, item, or bank.
|
||||
#[error("unknown {kind} `{id}`{}", context_suffix(.context))]
|
||||
Unresolved {
|
||||
/// What kind of thing was referenced, e.g. `"learning objective"`.
|
||||
kind: &'static str,
|
||||
/// The identifier that did not resolve.
|
||||
id: String,
|
||||
/// Where the dangling reference was found, if known.
|
||||
context: Option<String>,
|
||||
},
|
||||
|
||||
/// The requested selection could not be satisfied from the available items.
|
||||
#[error("cannot satisfy the blueprint: {0}")]
|
||||
Infeasible(String),
|
||||
|
||||
/// A date string was not `YYYY-MM-DD`, or was not a real calendar date.
|
||||
#[error("`{0}` is not a date in YYYY-MM-DD form")]
|
||||
BadDate(String),
|
||||
|
||||
/// A command-line argument was well-formed but unusable.
|
||||
#[error("{0}")]
|
||||
Usage(String),
|
||||
|
||||
/// A capability was compiled out.
|
||||
#[error("{0} support was not compiled in; rebuild with `--features {1}`")]
|
||||
FeatureDisabled(&'static str, &'static str),
|
||||
|
||||
/// Something went wrong that does not deserve its own variant.
|
||||
#[error("{0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
/// Wraps an I/O failure with the path that caused it.
|
||||
pub fn io(path: impl Into<PathBuf>, source: std::io::Error) -> Error {
|
||||
Error::Io {
|
||||
path: path.into(),
|
||||
source,
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds an [`Error::Other`] from anything displayable.
|
||||
pub fn other(msg: impl std::fmt::Display) -> Error {
|
||||
Error::Other(msg.to_string())
|
||||
}
|
||||
|
||||
/// Builds an [`Error::Usage`] from anything displayable.
|
||||
pub fn usage(msg: impl std::fmt::Display) -> Error {
|
||||
Error::Usage(msg.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders an issue list as an indented bullet list.
|
||||
fn format_issues(issues: &[String]) -> String {
|
||||
issues
|
||||
.iter()
|
||||
.map(|i| format!(" - {i}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
/// Renders the optional context of an unresolved reference.
|
||||
fn context_suffix(context: &Option<String>) -> String {
|
||||
match context {
|
||||
Some(c) => format!(" (referenced by {c})"),
|
||||
None => String::new(),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user