134 lines
4.2 KiB
Rust
134 lines
4.2 KiB
Rust
// SPDX-License-Identifier: Prosperity-3.0.0
|
|
// Copyright Scientific Computing Studio
|
|
// Source: https://git.scient.ing/education/coursebank
|
|
|
|
//! 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(),
|
|
}
|
|
}
|