diff --git a/src/io.rs b/src/io.rs index e0df954..b283dd3 100644 --- a/src/io.rs +++ b/src/io.rs @@ -7,6 +7,8 @@ use biblatex::{Bibliography, TypeError}; use crate::Entry; use crate::Library; +use crate::error::Error; + /// Parse a bibliography from a YAML string. /// /// ``` @@ -24,43 +26,29 @@ use crate::Library; /// let bib = from_yaml_str(yaml).unwrap(); /// assert_eq!(bib.nth(0).unwrap().date().unwrap().year, 2014); /// ``` -pub fn from_yaml_str(s: &str) -> Result { - serde_yaml::from_str(s) +pub fn from_yaml_str(s: &str) -> Result { + serde_yaml::from_str(s).map_err(Error::from) } /// Serialize a bibliography to a YAML string. -pub fn to_yaml_str(entries: &Library) -> Result { - serde_yaml::to_string(&entries) -} - -/// Errors that may occur when parsing a BibLaTeX file. -#[cfg(feature = "biblatex")] -#[derive(Clone, Debug)] -pub enum BibLaTeXError { - /// An error occurred when parsing a BibLaTeX file. - Parse(biblatex::ParseError), - /// One of the BibLaTeX fields was malformed for its type. - Type(biblatex::TypeError), -} - -#[cfg(feature = "biblatex")] -impl std::fmt::Display for BibLaTeXError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Parse(err) => write!(f, "biblatex parse error: {}", err), - Self::Type(err) => write!(f, "biblatex type error: {}", err), - } - } +pub fn to_yaml_str(entries: &Library) -> Result { + serde_yaml::to_string(&entries).map_err(Error::from) } /// Parse a bibliography from a BibLaTeX source string. #[cfg(feature = "biblatex")] -pub fn from_biblatex_str(biblatex: &str) -> Result> { - let bibliography = - Bibliography::parse(biblatex).map_err(|e| vec![BibLaTeXError::Parse(e)])?; +pub fn from_biblatex_str(biblatex: &str) -> Result { + use crate::error::{BibLaTeXError, BibLaTeXErrors}; + + let bibliography = Bibliography::parse(biblatex) + .map_err(BibLaTeXError::Parse) + .map_err(|e| BibLaTeXErrors(vec![e])) + .map_err(Error::from)?; from_biblatex(&bibliography) - .map_err(|e| e.into_iter().map(BibLaTeXError::Type).collect()) + .map_err(|e| e.into_iter().map(BibLaTeXError::Type).collect::>()) + .map_err(BibLaTeXErrors) + .map_err(Error::from) } /// Parse a bibliography from a BibLaTeX [`Bibliography`]. diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 0000000..678e5c3 --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,44 @@ +/// A nice wrapper for handling errors in the `main()`. +/// Must be called from `main()`. +#[macro_export] +macro_rules! err { + (@private, $result:expr, $format_string:literal, $exit_code:expr) => { + match $result { + Ok(v) => v, + Err(err) => { + eprintln!($format_string, err); + return $exit_code; + } + } + }; + ($result:expr, $exit_code:expr) => { + err!(@private, $result, "{}", ExitCode::from($exit_code)) + }; + ($result:expr) => { + err!(@private, $result, "{}", ExitCode::FAILURE) + }; +} + +/// Like `err!()`, but requires a format string with `"{}"`. +/// Must be called from `main()`. +#[macro_export] +macro_rules! err_fmt { + ($result:expr, $format_string:literal, $exit_code:literal) => { + err!(@private, $result, $format_string, ExitCode::from($exit_code)) + }; + ($result:expr, $format_string:literal) => { + err!(@private, $result, $format_string, ExitCode::FAILURE) + }; +} + +/// Like `err!()`, but you can directly specify the error message with `&str`/`String`. +/// Must be called from `main()`. +#[macro_export] +macro_rules! err_str { + ($error_string:expr, $exit_code:expr) => { + err!(Err(Error::OtherError($error_string.into())), $exit_code) + }; + ($error_string:expr) => { + err_str!($error_string, ExitCode::FAILURE) + }; +} diff --git a/src/main.rs b/src/main.rs index 4da99c7..ff65213 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,24 +1,27 @@ use std::borrow::Cow; use std::fs::{self, read_to_string}; use std::io::ErrorKind as IoErrorKind; -use std::path::Path; -use std::process::exit; +use std::path::PathBuf; +use std::process::ExitCode; use citationberg::taxonomy::Locator; use citationberg::{ IndependentStyle, Locale, LocaleCode, LocaleFile, LongShortForm, Style, }; use clap::builder::PossibleValue; -use clap::{crate_version, Arg, ArgAction, Command, ValueEnum}; +use clap::{crate_version, value_parser, Arg, ArgAction, Command, ValueEnum}; use strum::VariantNames; use hayagriva::archive::{locales, ArchivedStyle}; +use hayagriva::types::error::{BibliographyError, Error}; use hayagriva::{ io, BibliographyDriver, CitationItem, CitationRequest, LocatorPayload, SpecificLocator, }; use hayagriva::{BibliographyRequest, Selector}; +mod macros; + #[derive(Debug, Copy, Clone, PartialEq, VariantNames)] #[strum(serialize_all = "kebab_case")] pub enum Format { @@ -52,7 +55,7 @@ impl ValueEnum for Format { } /// Main function of the Hayagriva CLI. -fn main() { +fn main() -> ExitCode { let matches = Command::new("Hayagriva CLI") .version(crate_version!()) .author("The Typst Project Developers ") @@ -60,6 +63,7 @@ fn main() { .arg( Arg::new("INPUT") .help("Sets the bibliography file to use") + .value_parser(value_parser!(PathBuf)) .required(true) .index(1) ).arg( @@ -167,6 +171,7 @@ fn main() { Arg::new("csl") .long("csl") .help("Set a CSL file to use the style therein") + .value_parser(value_parser!(PathBuf)) .num_args(1) ) .arg( @@ -188,7 +193,7 @@ fn main() { ) .get_matches(); - let input = Path::new(matches.get_one::("INPUT").unwrap()); + let input = matches.get_one::("INPUT").unwrap().to_owned(); let format = matches.get_one("format").cloned().unwrap_or_else(|| { #[allow(unused_mut)] @@ -207,49 +212,35 @@ fn main() { }); let bibliography = { - let input = match read_to_string(input) { - Ok(s) => s, - Err(e) => { - if e.kind() == IoErrorKind::NotFound { - eprintln!("Bibliography file \"{}\" not found.", input.display()); - exit(5); - } else if let Some(os) = e.raw_os_error() { - eprintln!( - "Error while reading the bibliography file \"{}\": {}", - input.display(), - os - ); - exit(6); - } else { - eprintln!( - "Error while reading the bibliography file \"{}\".", - input.display() - ); - exit(6); - } - } + use BibliographyError::*; + let input = match read_to_string(&input).map_err(|err| match err.kind() { + IoErrorKind::NotFound => (NotFound(input), 5), + _ => match err.raw_os_error() { + Some(os) => (ReadErrorWithCode(input, os), 6), + _ => (ReadError(input), 6), + }, + }) { + Ok(v) => v, + Err((err, exit_code)) => err!(Err(err), exit_code), }; - match format { - Format::Yaml => io::from_yaml_str(&input).unwrap(), + err!(match format { + Format::Yaml => io::from_yaml_str(&input), #[cfg(feature = "biblatex")] - Format::Biblatex | Format::Bibtex => io::from_biblatex_str(&input).unwrap(), - } + Format::Biblatex | Format::Bibtex => io::from_biblatex_str(&input), + }) }; let bib_len = bibliography.len(); - let selector = - matches - .get_one("selector") - .cloned() - .map(|src| match Selector::parse(src) { - Ok(selector) => selector, - Err(err) => { - eprintln!("Error while parsing selector: {}", err); - exit(7); - } - }); + let selector = match matches + .get_one::("selector") + .cloned() + .map(|src| Selector::parse(&src)) + { + Some(result) => Some(err_fmt!(result, "Error while parsing selector: {}", 7)), + _ => None, + }; let bibliography = if let Some(keys) = matches.get_one::("key") { let mut res = vec![]; @@ -306,23 +297,22 @@ fn main() { } } } - exit(0); + return ExitCode::SUCCESS; } match matches.subcommand() { Some(("reference", sub_matches)) => { let style: Option<&String> = sub_matches.get_one("style"); - let csl: Option<&String> = sub_matches.get_one("csl"); + let csl: Option<&PathBuf> = sub_matches.get_one("csl"); let locale_path = sub_matches.get_one::("locales").map(|s| s.split(',')); let locale_str: Option<&String> = sub_matches.get_one("locale"); let (style, locales, locale) = - retrieve_assets(style, csl, locale_path, locale_str); + err!(retrieve_assets(style, csl, locale_path, locale_str)); if style.bibliography.is_none() { - eprintln!("style has no bibliography"); - exit(4); + err_str!("style has no bibliography", 4); } let mut driver = BibliographyDriver::new(); @@ -361,7 +351,7 @@ fn main() { } Some(("cite", sub_matches)) => { let style: Option<&String> = sub_matches.get_one("style"); - let csl: Option<&String> = sub_matches.get_one("csl"); + let csl: Option<&PathBuf> = sub_matches.get_one("csl"); let locale_path = sub_matches.get_one::("locales").map(|s| s.split(',')); let locale_str: Option<&String> = sub_matches.get_one("locale"); @@ -373,7 +363,7 @@ fn main() { .collect(); let (style, locales, locale) = - retrieve_assets(style, csl, locale_path, locale_str); + err!(retrieve_assets(style, csl, locale_path, locale_str)); let assign_locator = |(i, e)| { let mut item = CitationItem::with_entry(e); @@ -452,43 +442,48 @@ fn main() { println!("{}", bib); } } + ExitCode::SUCCESS } fn retrieve_assets<'a>( style: Option<&String>, - csl: Option<&String>, + csl: Option<&PathBuf>, locale_paths: Option>, locale_str: Option<&String>, -) -> (IndependentStyle, Vec, Option) { - let locale: Option<_> = locale_str.map(|l: &String| LocaleCode(l.into())); +) -> Result<(IndependentStyle, Vec, Option), Error> { + use Error::OtherError; + let locale: Option<_> = locale_str.map(|l: &String| LocaleCode(l.into())); let style = match (style, csl) { (_, Some(csl)) => { - let file_str = fs::read_to_string(csl).expect("could not read CSL file"); - IndependentStyle::from_xml(&file_str).expect("CSL file malformed") - } - (Some(style), _) => { - let Style::Independent(indep) = - ArchivedStyle::by_name(style.as_str()).expect("no style found").get() - else { - panic!("dependent style in archive") - }; - indep + let xml_str = fs::read_to_string(csl).ok().ok_or(OtherError(format!( + r#"Could not read CSL file: "{}"{}"#, + csl.display(), + "\nMaybe you meant to use --style instead?" + )))?; + IndependentStyle::from_xml(&xml_str) + .map_err(|_| OtherError("CSL file malformed".into()))? } - (None, None) => panic!("must specify style or CSL file"), + (Some(style), _) => match ArchivedStyle::by_name(style.as_str()) + .ok_or(OtherError("no style found".into()))? + .get() + { + Style::Independent(indep) => indep, + _ => return Err("dependent style in archive".into()), + }, + (None, None) => return Err("must specify style or CSL file".into()), }; let locales: Vec = match locale_paths { - Some(locale_paths) => locale_paths - .into_iter() - .map(|locale_path| { - let file_str = - fs::read_to_string(locale_path).expect("could not read locale file"); - LocaleFile::from_xml(&file_str).expect("locale file malformed").into() + Some(paths) => paths + .map(|path| match fs::read_to_string(path) { + Ok(file_str) => LocaleFile::from_xml(&file_str) + .map(Into::into) + .map_err(|_| Error::from("locale file malformed")), + Err(_) => Err("could not read locale file".into()), }) - .collect(), + .collect::>()?, None => locales(), }; - - (style, locales, locale) + Ok((style, locales, locale)) } diff --git a/src/types/error.rs b/src/types/error.rs new file mode 100644 index 0000000..218e53d --- /dev/null +++ b/src/types/error.rs @@ -0,0 +1,82 @@ +use std::path::PathBuf; + +#[cfg(feature = "biblatex")] +pub use biblatex_module::*; + +/// Used once before all but the 1st errors. +const TOP_INDENT: &str = "\nCaused by:"; + +/// Used to indent next error. +const INDENT: &str = " "; + +/// The main error that is handled in the `main()` function. +#[derive(thiserror::Error, Debug)] +pub enum Error { + /// Bibliography error. + #[error("Bibliography error: {0}")] + BibliographyError(#[from] BibliographyError), + /// Invalid Bib(La)TeX input file format. + #[cfg(feature = "biblatex")] + #[error("Invalid format: expected Bib(La)TeX\n{TOP_INDENT}\n{INDENT}{0}")] + InvalidBiblatex(#[from] BibLaTeXErrors), + /// Invalid YAML input file format. + #[error("Invalid format: expected YAML\n{TOP_INDENT}\n{INDENT}{0}")] + InvalidYaml(#[from] serde_yaml::Error), + /// Other error. + #[error("{0}")] + OtherError(String), +} + +/// The error when reading bibliography file. +#[derive(thiserror::Error, Debug)] +pub enum BibliographyError { + /// Bibliography file not found. + #[error(r#"Bibliography file "{0}" not found."#)] + NotFound(PathBuf), + /// Error while reading the bibliography file (with OS error code). + #[error(r#"Error while reading the bibliography file "{0}": {1}"#)] + ReadErrorWithCode(PathBuf, i32), + /// Error while reading the bibliography file. + #[error(r#"Error while reading the bibliography file "{0}"."#)] + ReadError(PathBuf), +} + +impl From<&str> for Error { + fn from(value: &str) -> Self { + Self::OtherError(value.into()) + } +} + +#[cfg(feature = "biblatex")] +mod biblatex_module { + use super::INDENT; + use std::fmt::{Display, Formatter, Result}; + + /// Errors that may occur when parsing a BibLaTeX file. + #[derive(thiserror::Error, Clone, Debug)] + pub enum BibLaTeXError { + /// An error occurred when parsing a BibLaTeX file. + #[error("BibLaTeX parse error\n{INDENT}{0}")] + Parse(biblatex::ParseError), + /// One of the BibLaTeX fields was malformed for its type. + #[error("BibLaTeX type error\n{INDENT}{0}")] + Type(biblatex::TypeError), + } + + /// Wrapper over an array of `BibLaTeXError` elements. + #[derive(thiserror::Error, Clone, Debug)] + pub struct BibLaTeXErrors(pub Vec); + + impl Display for BibLaTeXErrors { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + let sources = self + .0 + .clone() + .into_iter() + .map(|x| x.to_string()) + .collect::>() + .join("\n\t"); + write!(f, "{sources}") + } + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs index 437ced6..2b12ad1 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -17,6 +17,8 @@ pub use persons::*; pub use strings::*; pub use time::*; +/// Provides error types. +pub mod error; mod numeric; mod page; mod persons;