Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(errors)!: added proper error handling #262

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 16 additions & 28 deletions src/io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
/// ```
Expand All @@ -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<Library, serde_yaml::Error> {
serde_yaml::from_str(s)
pub fn from_yaml_str(s: &str) -> Result<Library, Error> {
serde_yaml::from_str(s).map_err(Error::from)
}

/// Serialize a bibliography to a YAML string.
pub fn to_yaml_str(entries: &Library) -> Result<String, serde_yaml::Error> {
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<String, Error> {
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<Library, Vec<BibLaTeXError>> {
let bibliography =
Bibliography::parse(biblatex).map_err(|e| vec![BibLaTeXError::Parse(e)])?;
pub fn from_biblatex_str(biblatex: &str) -> Result<Library, Error> {
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::<Vec<_>>())
.map_err(BibLaTeXErrors)
.map_err(Error::from)
}

/// Parse a bibliography from a BibLaTeX [`Bibliography`].
Expand Down
44 changes: 44 additions & 0 deletions src/macros.rs
Original file line number Diff line number Diff line change
@@ -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)
};
}
137 changes: 66 additions & 71 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -52,14 +55,15 @@ 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 <[email protected]>")
.about("Format references and citations for your YAML-encoded or BibLaTeX bibliography files and query bibliographies using selectors.")
.arg(
Arg::new("INPUT")
.help("Sets the bibliography file to use")
.value_parser(value_parser!(PathBuf))
.required(true)
.index(1)
).arg(
Expand Down Expand Up @@ -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(
Expand All @@ -188,7 +193,7 @@ fn main() {
)
.get_matches();

let input = Path::new(matches.get_one::<String>("INPUT").unwrap());
let input = matches.get_one::<PathBuf>("INPUT").unwrap().to_owned();

let format = matches.get_one("format").cloned().unwrap_or_else(|| {
#[allow(unused_mut)]
Expand All @@ -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::<String>("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::<String>("key") {
let mut res = vec![];
Expand Down Expand Up @@ -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::<String>("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();
Expand Down Expand Up @@ -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::<String>("locales").map(|s| s.split(','));
let locale_str: Option<&String> = sub_matches.get_one("locale");
Expand All @@ -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);
Expand Down Expand Up @@ -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<impl Iterator<Item = &'a str>>,
locale_str: Option<&String>,
) -> (IndependentStyle, Vec<Locale>, Option<LocaleCode>) {
let locale: Option<_> = locale_str.map(|l: &String| LocaleCode(l.into()));
) -> Result<(IndependentStyle, Vec<Locale>, Option<LocaleCode>), 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<Locale> = 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::<Result<_, _>>()?,
None => locales(),
};

(style, locales, locale)
Ok((style, locales, locale))
}
Loading
Loading