Files
coursebank/src/error.rs
T
2026-08-07 11:47:30 -04:00

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(),
}
}