Anki/rslib/src/error/mod.rs

327 lines
10 KiB
Rust

// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
mod db;
mod filtered;
mod invalid_input;
pub(crate) mod network;
mod not_found;
mod search;
#[cfg(windows)]
pub mod windows;
use anki_i18n::I18n;
use anki_io::FileIoError;
use anki_io::FileOp;
pub use db::DbError;
pub use db::DbErrorKind;
pub use filtered::CustomStudyError;
pub use filtered::FilteredDeckError;
pub use network::NetworkError;
pub use network::NetworkErrorKind;
pub use network::SyncError;
pub use network::SyncErrorKind;
pub use search::ParseError;
pub use search::SearchErrorKind;
use snafu::Snafu;
pub use self::invalid_input::InvalidInputError;
pub use self::invalid_input::OrInvalid;
pub use self::not_found::NotFoundError;
pub use self::not_found::OrNotFound;
use crate::import_export::ImportError;
use crate::links::HelpPage;
pub type Result<T, E = AnkiError> = std::result::Result<T, E>;
#[derive(Debug, PartialEq, Snafu)]
pub enum AnkiError {
#[snafu(context(false))]
InvalidInput {
source: InvalidInputError,
},
TemplateError {
info: String,
},
#[snafu(context(false))]
CardTypeError {
source: CardTypeError,
},
#[snafu(context(false))]
FileIoError {
source: FileIoError,
},
#[snafu(context(false))]
DbError {
source: DbError,
},
#[snafu(context(false))]
NetworkError {
source: NetworkError,
},
#[snafu(context(false))]
SyncError {
source: SyncError,
},
JsonError {
info: String,
},
ProtoError {
info: String,
},
ParseNumError,
Interrupted,
CollectionNotOpen,
CollectionAlreadyOpen,
#[snafu(context(false))]
NotFound {
source: NotFoundError,
},
/// Indicates an absent card or note, but (unlike [AnkiError::NotFound]) in
/// a non-critical context like the browser table, where deleted ids are
/// deliberately not removed.
Deleted,
Existing,
#[snafu(context(false))]
FilteredDeckError {
source: FilteredDeckError,
},
#[snafu(context(false))]
SearchError {
source: SearchErrorKind,
},
InvalidRegex {
info: String,
},
UndoEmpty,
MultipleNotetypesSelected,
DatabaseCheckRequired,
MediaCheckRequired,
#[snafu(context(false))]
CustomStudyError {
source: CustomStudyError,
},
#[snafu(context(false))]
ImportError {
source: ImportError,
},
InvalidId,
#[cfg(windows)]
#[snafu(context(false))]
WindowsError {
source: windows::WindowsError,
},
InvalidMethodIndex,
InvalidServiceIndex,
FsrsWeightsInvalid,
/// Returned by fsrs-rs; may happen even if 400+ reviews
FsrsInsufficientData,
/// Generated by our backend if count < 400
FsrsInsufficientReviews {
count: usize,
},
FsrsUnableToDetermineDesiredRetention,
SchedulerUpgradeRequired,
InvalidCertificateFormat,
}
// error helpers
impl AnkiError {
pub fn message(&self, tr: &I18n) -> String {
match self {
AnkiError::SyncError { source } => source.message(tr),
AnkiError::NetworkError { source } => source.message(tr),
AnkiError::TemplateError { info: source } => {
// already localized
source.into()
}
AnkiError::CardTypeError { source } => {
let header =
tr.card_templates_invalid_template_number(source.ordinal + 1, &source.notetype);
let details = match &source.source {
CardTypeErrorDetails::TemplateParseError => tr.card_templates_see_preview(),
CardTypeErrorDetails::NoSuchField { field } => {
tr.card_templates_field_not_found(field)
}
CardTypeErrorDetails::NoFrontField => tr.card_templates_no_front_field(),
CardTypeErrorDetails::Duplicate { index } => {
tr.card_templates_identical_front(index + 1)
}
CardTypeErrorDetails::MissingCloze => tr.card_templates_missing_cloze(),
CardTypeErrorDetails::ExtraneousCloze => tr.card_templates_extraneous_cloze(),
};
format!("{}<br>{}", header, details)
}
AnkiError::DbError { source } => source.message(tr),
AnkiError::SearchError { source } => source.message(tr),
AnkiError::ParseNumError => tr.errors_parse_number_fail().into(),
AnkiError::FilteredDeckError { source } => source.message(tr),
AnkiError::InvalidRegex { info: source } => format!("<pre>{}</pre>", source),
AnkiError::MultipleNotetypesSelected => tr.errors_multiple_notetypes_selected().into(),
AnkiError::DatabaseCheckRequired => tr.errors_please_check_database().into(),
AnkiError::MediaCheckRequired => tr.errors_please_check_media().into(),
AnkiError::CustomStudyError { source } => source.message(tr),
AnkiError::ImportError { source } => source.message(tr),
AnkiError::Deleted => tr.browsing_row_deleted().into(),
AnkiError::InvalidId => tr.errors_please_check_database().into(),
AnkiError::JsonError { .. }
| AnkiError::ProtoError { .. }
| AnkiError::Interrupted
| AnkiError::CollectionNotOpen
| AnkiError::CollectionAlreadyOpen
| AnkiError::Existing
| AnkiError::InvalidServiceIndex
| AnkiError::InvalidMethodIndex
| AnkiError::UndoEmpty
| AnkiError::InvalidCertificateFormat => format!("{:?}", self),
AnkiError::FileIoError { source } => source.message(),
AnkiError::InvalidInput { source } => source.message(),
AnkiError::NotFound { source } => source.message(tr),
AnkiError::FsrsInsufficientData => tr.deck_config_not_enough_history().into(),
AnkiError::FsrsInsufficientReviews { count } => {
tr.deck_config_must_have_400_reviews(*count).into()
}
AnkiError::FsrsWeightsInvalid => tr.deck_config_invalid_weights().into(),
AnkiError::SchedulerUpgradeRequired => {
tr.scheduling_update_required().replace("V2", "v3")
}
#[cfg(windows)]
AnkiError::WindowsError { source } => format!("{source:?}"),
AnkiError::FsrsUnableToDetermineDesiredRetention => tr
.deck_config_unable_to_determine_desired_retention()
.into(),
}
}
pub fn help_page(&self) -> Option<HelpPage> {
match self {
Self::CardTypeError {
source: CardTypeError { source, .. },
} => Some(match source {
CardTypeErrorDetails::TemplateParseError => HelpPage::CardTypeTemplateError,
CardTypeErrorDetails::NoSuchField { field: _ } => HelpPage::CardTypeTemplateError,
CardTypeErrorDetails::Duplicate { .. } => HelpPage::CardTypeDuplicate,
CardTypeErrorDetails::NoFrontField => HelpPage::CardTypeNoFrontField,
CardTypeErrorDetails::MissingCloze => HelpPage::CardTypeMissingCloze,
CardTypeErrorDetails::ExtraneousCloze => HelpPage::CardTypeExtraneousCloze,
}),
_ => None,
}
}
pub fn context(&self) -> String {
match self {
Self::InvalidInput { source } => source.context(),
Self::NotFound { source } => source.context(),
_ => String::new(),
}
}
pub fn backtrace(&self) -> String {
match self {
Self::InvalidInput { source } => {
if let Some(bt) = snafu::ErrorCompat::backtrace(source) {
return format!("{bt}");
}
}
Self::NotFound { source } => {
if let Some(bt) = snafu::ErrorCompat::backtrace(source) {
return format!("{bt}");
}
}
_ => (),
}
String::new()
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum TemplateError {
NoClosingBrackets(String),
ConditionalNotClosed(String),
ConditionalNotOpen {
closed: String,
currently_open: Option<String>,
},
FieldNotFound {
filters: String,
field: String,
},
NoSuchConditional(String),
}
impl From<serde_json::Error> for AnkiError {
fn from(err: serde_json::Error) -> Self {
AnkiError::JsonError {
info: err.to_string(),
}
}
}
impl From<prost::EncodeError> for AnkiError {
fn from(err: prost::EncodeError) -> Self {
AnkiError::ProtoError {
info: err.to_string(),
}
}
}
impl From<prost::DecodeError> for AnkiError {
fn from(err: prost::DecodeError) -> Self {
AnkiError::ProtoError {
info: err.to_string(),
}
}
}
impl From<tempfile::PathPersistError> for AnkiError {
fn from(e: tempfile::PathPersistError) -> Self {
FileIoError::from(e).into()
}
}
impl From<tempfile::PersistError> for AnkiError {
fn from(e: tempfile::PersistError) -> Self {
FileIoError::from(e).into()
}
}
impl From<regex::Error> for AnkiError {
fn from(err: regex::Error) -> Self {
AnkiError::InvalidRegex {
info: err.to_string(),
}
}
}
// stopgap; implicit mapping should be phased out in favor of manual
// context attachment
impl From<std::io::Error> for AnkiError {
fn from(source: std::io::Error) -> Self {
FileIoError {
path: std::path::PathBuf::new(),
op: FileOp::Unknown,
source,
}
.into()
}
}
#[derive(Debug, PartialEq, Eq, Snafu)]
#[snafu(visibility(pub))]
pub struct CardTypeError {
pub notetype: String,
pub ordinal: usize,
pub source: CardTypeErrorDetails,
}
#[derive(Debug, PartialEq, Eq, Snafu)]
#[snafu(visibility(pub))]
pub enum CardTypeErrorDetails {
TemplateParseError,
Duplicate { index: usize },
NoFrontField,
NoSuchField { field: String },
MissingCloze,
ExtraneousCloze,
}