// 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 = std::result::Result; /// 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), /// 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, }, /// 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, 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::>() .join("\n") } /// Renders the optional context of an unresolved reference. fn context_suffix(context: &Option) -> String { match context { Some(c) => format!(" (referenced by {c})"), None => String::new(), } }