From 702d37490cafe1755a6c684d667cb7915e376252 Mon Sep 17 00:00:00 2001 From: Kan-Ru Chen Date: Wed, 11 Feb 2026 15:12:19 +0900 Subject: [PATCH] refactor(rust): improve error reporting by adopting exn inspired extension --- src/dictionary/loader.rs | 132 ++++++++++++++++------------ src/dictionary/mod.rs | 102 ++++++++++----------- src/dictionary/sqlite.rs | 63 ++++++------- src/dictionary/trie.rs | 13 +-- src/editor/mod.rs | 124 ++++++++++++++------------ src/editor/selection/phrase.rs | 6 +- src/exn.rs | 123 ++++++++++++++++++++++++++ src/lib.rs | 3 + src/path.rs | 8 +- src/zhuyin/bopomofo.rs | 156 ++++++++++++++++----------------- src/zhuyin/syllable.rs | 48 +++++----- tools/src/info.rs | 9 +- tools/src/init_database.rs | 110 ++++++----------------- 13 files changed, 496 insertions(+), 401 deletions(-) create mode 100644 src/exn.rs diff --git a/src/dictionary/loader.rs b/src/dictionary/loader.rs index 849eb535..cb0df4cd 100644 --- a/src/dictionary/loader.rs +++ b/src/dictionary/loader.rs @@ -12,6 +12,7 @@ use log::{error, info}; #[cfg(feature = "sqlite")] use super::SqliteDictionary; use super::{Dictionary, TrieBuf, uhash}; +use crate::exn::{Exn, ResultExt}; use crate::{ dictionary::DictionaryUsage, editor::{AbbrevTable, SymbolSelector}, @@ -33,27 +34,6 @@ pub struct AssetLoader { search_path: Option, } -/// Errors during loading system or user dictionaries. -#[derive(Debug)] -pub enum LoadDictionaryError { - /// Cannot find any system or user dictionary. - NotFound, - /// IO Error. - IoError(io::Error), -} - -impl Display for LoadDictionaryError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Unable to load system dictionary: {self:?}") - } -} - -impl Error for LoadDictionaryError {} - -fn io_err(err: io::Error) -> LoadDictionaryError { - LoadDictionaryError::IoError(err) -} - impl AssetLoader { /// Creates a new dictionary loader. pub fn new() -> AssetLoader { @@ -112,29 +92,33 @@ impl AssetLoader { } /// Loads the abbrev table. pub fn load_abbrev(&self) -> Result { + let error = || LoadDictionaryError::new("failed to load abbrev table"); + let not_found = || error().with_source(io::Error::from(io::ErrorKind::NotFound)); let search_path = if let Some(path) = &self.search_path { path.to_owned() } else { search_path_from_env_var() }; - let parent_path = find_path_by_files(&search_path, &[ABBREV_FILE_NAME]) - .ok_or(LoadDictionaryError::NotFound)?; + let parent_path = + find_path_by_files(&search_path, &[ABBREV_FILE_NAME]).or_raise(not_found)?; let abbrev_path = parent_path.join(ABBREV_FILE_NAME); info!("Loading {ABBREV_FILE_NAME}"); - AbbrevTable::open(abbrev_path).map_err(io_err) + AbbrevTable::open(abbrev_path).or_raise(error) } /// Loads the symbol table. pub fn load_symbol_selector(&self) -> Result { + let error = || LoadDictionaryError::new("failed to load symbol table"); + let not_found = || error().with_source(io::Error::from(io::ErrorKind::NotFound)); let search_path = if let Some(path) = &self.search_path { path.to_owned() } else { search_path_from_env_var() }; - let parent_path = find_path_by_files(&search_path, &[SYMBOLS_FILE_NAME]) - .ok_or(LoadDictionaryError::NotFound)?; + let parent_path = + find_path_by_files(&search_path, &[SYMBOLS_FILE_NAME]).or_raise(not_found)?; let symbol_path = parent_path.join(SYMBOLS_FILE_NAME); info!("Loading {SYMBOLS_FILE_NAME}"); - SymbolSelector::open(symbol_path).map_err(io_err) + SymbolSelector::open(symbol_path).or_raise(error) } } @@ -171,34 +155,39 @@ impl UserDictionaryManager { /// /// If no user dictionary were found, a new dictionary will be created at /// the default path. - pub fn init(&self) -> io::Result> { + pub fn init(&self) -> Result, LoadDictionaryError> { + let error = || LoadDictionaryError::new("failed to init user dictionary"); + let not_found = || error().with_source(io::Error::from(io::ErrorKind::NotFound)); let mut loader = SingleDictionaryLoader::new(); loader.migrate_sqlite(true); let data_path = self .data_path .clone() .or_else(userphrase_path) - .ok_or(io::Error::from(io::ErrorKind::NotFound))?; + .or_raise(not_found)?; if data_path.ends_with(UD_MEM_FILE_NAME) { return Ok(Self::in_memory()); } if data_path.exists() { info!("Use existing user dictionary {}", data_path.display()); - return loader.guess_format_and_load(&data_path).map(|mut dict| { - dict.set_usage(DictionaryUsage::User); - dict - }); + return loader + .guess_format_and_load(&data_path) + .map(|mut dict| { + dict.set_usage(DictionaryUsage::User); + dict + }) + .or_raise(error); } let userdata_dir = data_path.parent().expect("path should contain a filename"); if !userdata_dir.exists() { info!("Creating userdata_dir: {}", userdata_dir.display()); - fs::create_dir_all(userdata_dir)?; + fs::create_dir_all(userdata_dir).or_raise(error)?; } info!( "Creating a fresh user dictionary at {}", data_path.display() ); - let mut fresh_dict = loader.guess_format_and_load(&data_path)?; + let mut fresh_dict = loader.guess_format_and_load(&data_path).or_raise(error)?; let user_dict_path = userdata_dir.join(UD_SQLITE_FILE_NAME); if cfg!(feature = "sqlite") && user_dict_path.exists() { @@ -208,18 +197,15 @@ impl UserDictionaryManager { "Importing existing sqlite dictionary at {}", user_dict_path.display() ); - let dict = SqliteDictionary::open(user_dict_path) - .map_err(|e| io::Error::new(io::ErrorKind::Other, Box::new(e)))?; + let dict = SqliteDictionary::open(user_dict_path).or_raise(error)?; for (syllables, phrase) in dict.entries() { let freq = phrase.freq(); let last_used = phrase.last_used().unwrap_or_default(); fresh_dict .update_phrase(&syllables, phrase, freq, last_used) - .map_err(|e| io::Error::new(io::ErrorKind::Other, Box::new(e)))?; + .or_raise(error)?; } - fresh_dict - .flush() - .map_err(|e| io::Error::new(io::ErrorKind::Other, Box::new(e)))?; + fresh_dict.flush().or_raise(error)?; } } else { let uhash_path = userdata_dir.join(UD_UHASH_FILE_NAME); @@ -228,7 +214,7 @@ impl UserDictionaryManager { "Importing existing uhash dictionary at {}", user_dict_path.display() ); - let mut input = File::open(uhash_path)?; + let mut input = File::open(uhash_path).or_raise(error)?; if let Ok(phrases) = uhash::try_load_bin(&input).or_else(|_| { input.rewind()?; uhash::try_load_text(&input) @@ -238,11 +224,9 @@ impl UserDictionaryManager { let last_used = phrase.last_used().unwrap_or_default(); fresh_dict .update_phrase(&syllables, phrase, freq, last_used) - .map_err(|e| io::Error::other(Box::new(e)))?; + .or_raise(error)?; } - fresh_dict - .flush() - .map_err(|e| io::Error::other(Box::new(e)))?; + fresh_dict.flush().or_raise(error)?; } } } @@ -254,20 +238,24 @@ impl UserDictionaryManager { /// /// If no user exclusion dictionary were found, a new dictionary /// will be created at the default path. - pub fn init_deleted(&self) -> io::Result> { + pub fn init_deleted(&self) -> Result, LoadDictionaryError> { + let error = || LoadDictionaryError::new("failed to init user exclusion dictionary"); + let not_found = || error().with_source(io::Error::from(io::ErrorKind::NotFound)); let loader = SingleDictionaryLoader::new(); let data_path = self .data_path .clone() .or_else(userphrase_path) - .ok_or(io::Error::from(io::ErrorKind::NotFound))?; + .or_raise(not_found)?; let userdata_dir = data_path.parent().expect("path should contain a filename"); if !userdata_dir.exists() { info!("Creating userdata_dir: {}", userdata_dir.display()); - fs::create_dir_all(&userdata_dir)?; + fs::create_dir_all(&userdata_dir).or_raise(error)?; } let exclude_dict_path = userdata_dir.join("chewing-deleted.dat"); - Ok(loader.guess_format_and_load(&exclude_dict_path)?) + loader + .guess_format_and_load(&exclude_dict_path) + .or_raise(error) } /// Load a in-memory user dictionary. pub fn in_memory() -> Box { @@ -290,12 +278,16 @@ impl SingleDictionaryLoader { pub fn migrate_sqlite(&mut self, migrate: bool) { self.migrate_sqlite = migrate; } - pub fn guess_format_and_load(&self, dict_path: &PathBuf) -> io::Result> { + pub fn guess_format_and_load( + &self, + dict_path: &PathBuf, + ) -> Result, LoadDictionaryError> { + let error = || LoadDictionaryError::new("failed to parse and load dictionary"); info!("Loading dictionary {}", dict_path.display()); if self.migrate_sqlite && dict_path.is_file() { - let metadata = dict_path.metadata()?; + let metadata = dict_path.metadata().or_raise(error)?; if metadata.permissions().readonly() { - return Err(io::Error::from(io::ErrorKind::PermissionDenied)); + return Err(error().with_source(io::Error::from(io::ErrorKind::PermissionDenied))); } } @@ -306,23 +298,47 @@ impl SingleDictionaryLoader { if self.migrate_sqlite { SqliteDictionary::open(dict_path) .map(|dict| Box::new(dict) as Box) - .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, Box::new(e))) + .or_raise(error) } else { SqliteDictionary::open_readonly(dict_path) .map(|dict| Box::new(dict) as Box) - .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, Box::new(e))) + .or_raise(error) } } #[cfg(not(feature = "sqlite"))] { - Err(io::Error::from(io::ErrorKind::Unsupported)) + Err(error().with_source(io::Error::from(io::ErrorKind::Unsupported))) } } else if ext.eq_ignore_ascii_case("dat") { TrieBuf::open(dict_path) .map(|dict| Box::new(dict) as Box) - .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, Box::new(e))) + .or_raise(error) } else { - Err(io::Error::from(io::ErrorKind::Other)) + Err(error()) } } } + +/// Errors during loading system or user dictionaries. +#[derive(Debug)] +pub struct LoadDictionaryError { + msg: String, + source: Option>, +} + +impl LoadDictionaryError { + fn new(msg: &str) -> LoadDictionaryError { + LoadDictionaryError { + msg: msg.to_string(), + source: None, + } + } +} + +impl Display for LoadDictionaryError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.msg) + } +} + +impl_exn!(LoadDictionaryError); diff --git a/src/dictionary/mod.rs b/src/dictionary/mod.rs index 2f347cdb..397b0136 100644 --- a/src/dictionary/mod.rs +++ b/src/dictionary/mod.rs @@ -6,7 +6,6 @@ use std::{ cmp::Ordering, error::Error, fmt::{Debug, Display}, - io, path::Path, }; @@ -20,6 +19,7 @@ pub use self::sqlite::{SqliteDictionary, SqliteDictionaryBuilder, SqliteDictiona pub use self::trie::{Trie, TrieBuilder, TrieOpenOptions, TrieStatistics}; pub use self::trie_buf::TrieBuf; pub use self::usage::DictionaryUsage; +use crate::exn::Exn; use crate::zhuyin::Syllable; mod layered; @@ -31,35 +31,6 @@ mod trie_buf; mod uhash; mod usage; -/// The error type which is returned from updating a dictionary. -#[derive(Debug)] -pub struct UpdateDictionaryError { - /// TODO: doc - message: &'static str, - source: Option>, -} - -impl UpdateDictionaryError { - pub(crate) fn new(message: &'static str) -> UpdateDictionaryError { - UpdateDictionaryError { - message, - source: None, - } - } -} - -impl Display for UpdateDictionaryError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "update dictionary failed: {}", self.message) - } -} - -impl Error for UpdateDictionaryError { - fn source(&self) -> Option<&(dyn Error + 'static)> { - self.source.as_ref().map(|err| err.as_ref() as &dyn Error) - } -} - /// A collection of metadata of a dictionary. /// /// The dictionary version and copyright information can be used in @@ -408,46 +379,69 @@ pub trait Dictionary: Debug { } } -/// Errors during dictionary construction. +/// TODO: doc +pub trait DictionaryBuilder: Any { + /// TODO: doc + fn set_info(&mut self, info: DictionaryInfo) -> Result<(), BuildDictionaryError>; + /// TODO: doc + fn insert( + &mut self, + syllables: &[Syllable], + phrase: Phrase, + ) -> Result<(), BuildDictionaryError>; + /// TODO: doc + fn build(&mut self, path: &Path) -> Result<(), BuildDictionaryError>; +} + +/// The error type which is returned from updating a dictionary. #[derive(Debug)] -pub struct BuildDictionaryError { - source: Box, +pub struct UpdateDictionaryError { + /// TODO: doc + message: &'static str, + source: Option>, } -impl Display for BuildDictionaryError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "build dictionary error") +impl UpdateDictionaryError { + pub(crate) fn new(message: &'static str) -> UpdateDictionaryError { + UpdateDictionaryError { + message, + source: None, + } } } -impl Error for BuildDictionaryError { - fn source(&self) -> Option<&(dyn Error + 'static)> { - Some(self.source.as_ref()) +impl Display for UpdateDictionaryError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "update dictionary failed: {}", self.message) } } -impl From for BuildDictionaryError { - fn from(source: io::Error) -> Self { +impl_exn!(UpdateDictionaryError); + +/// Errors during dictionary construction. +#[derive(Debug)] +pub struct BuildDictionaryError { + msg: String, + source: Option>, +} + +impl BuildDictionaryError { + fn new(msg: &str) -> BuildDictionaryError { BuildDictionaryError { - source: Box::new(source), + msg: msg.to_string(), + source: None, } } } -/// TODO: doc -pub trait DictionaryBuilder: Any { - /// TODO: doc - fn set_info(&mut self, info: DictionaryInfo) -> Result<(), BuildDictionaryError>; - /// TODO: doc - fn insert( - &mut self, - syllables: &[Syllable], - phrase: Phrase, - ) -> Result<(), BuildDictionaryError>; - /// TODO: doc - fn build(&mut self, path: &Path) -> Result<(), BuildDictionaryError>; +impl Display for BuildDictionaryError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "build dictionary error: {}", self.msg) + } } +impl_exn!(BuildDictionaryError); + #[cfg(test)] mod tests { use crate::dictionary::{Dictionary, DictionaryBuilder}; diff --git a/src/dictionary/sqlite.rs b/src/dictionary/sqlite.rs index 72d68e20..e6e01811 100644 --- a/src/dictionary/sqlite.rs +++ b/src/dictionary/sqlite.rs @@ -12,7 +12,7 @@ use super::{ BuildDictionaryError, Dictionary, DictionaryBuilder, DictionaryInfo, Entries, LookupStrategy, Phrase, UpdateDictionaryError, }; -use crate::{dictionary::DictionaryUsage, zhuyin::Syllable}; +use crate::{dictionary::DictionaryUsage, exn::ResultExt, zhuyin::Syllable}; const APPLICATION_ID: u32 = 0x43484557; // 'CHEW' in big-endian const USER_VERSION: u32 = 0; @@ -561,35 +561,21 @@ impl Default for SqliteDictionaryBuilder { } } -impl From for BuildDictionaryError { - fn from(source: RusqliteError) -> Self { - BuildDictionaryError { - source: Box::new(source), - } - } -} - -impl From for BuildDictionaryError { - fn from(source: str::Utf8Error) -> Self { - BuildDictionaryError { - source: Box::new(source), - } - } -} - impl DictionaryBuilder for SqliteDictionaryBuilder { fn set_info(&mut self, info: DictionaryInfo) -> Result<(), BuildDictionaryError> { - let tx = self.dict.conn.transaction()?; + let err = || BuildDictionaryError::new("failed to set dictionary info"); + let tx = self.dict.conn.transaction().or_raise(err)?; { - let mut stmt = - tx.prepare("INSERT OR REPLACE INTO info_v1 (key, value) VALUES (?, ?)")?; - stmt.execute(["name", &info.name])?; - stmt.execute(["copyright", &info.copyright])?; - stmt.execute(["license", &info.license])?; - stmt.execute(["version", &info.version])?; - stmt.execute(["software", &info.software])?; + let mut stmt = tx + .prepare("INSERT OR REPLACE INTO info_v1 (key, value) VALUES (?, ?)") + .or_raise(err)?; + stmt.execute(["name", &info.name]).or_raise(err)?; + stmt.execute(["copyright", &info.copyright]).or_raise(err)?; + stmt.execute(["license", &info.license]).or_raise(err)?; + stmt.execute(["version", &info.version]).or_raise(err)?; + stmt.execute(["software", &info.software]).or_raise(err)?; } - tx.commit()?; + tx.commit().or_raise(err)?; Ok(()) } @@ -598,6 +584,7 @@ impl DictionaryBuilder for SqliteDictionaryBuilder { syllables: &[Syllable], phrase: Phrase, ) -> Result<(), BuildDictionaryError> { + let err = || BuildDictionaryError::new("failed to insert phrase"); let sort_id = if syllables.len() == 1 { self.sort_id += 1; self.sort_id @@ -605,29 +592,37 @@ impl DictionaryBuilder for SqliteDictionaryBuilder { 0 }; let syllables_bytes = syllables.to_bytes(); - let mut stmt = self.dict.conn.prepare_cached( - "INSERT OR REPLACE INTO dictionary_v1 ( + let mut stmt = self + .dict + .conn + .prepare_cached( + "INSERT OR REPLACE INTO dictionary_v1 ( syllables, phrase, freq, sort_id ) VALUES (?, ?, ?, ?)", - )?; + ) + .or_raise(err)?; stmt.execute(params![ syllables_bytes, phrase.as_str(), phrase.freq(), sort_id - ])?; + ]) + .or_raise(err)?; Ok(()) } fn build(&mut self, path: &Path) -> Result<(), BuildDictionaryError> { - let path = path.to_str().ok_or(BuildDictionaryError { - source: "cannot convert file path to utf8".into(), - })?; - self.dict.conn.execute("VACUUM INTO ?", [path])?; + let path = path + .to_str() + .or_raise(|| BuildDictionaryError::new("cannot convert file path to utf8"))?; + self.dict + .conn + .execute("VACUUM INTO ?", [path]) + .or_raise(|| BuildDictionaryError::new("failed to finalize dictionary"))?; Ok(()) } } diff --git a/src/dictionary/trie.rs b/src/dictionary/trie.rs index 5ad2d7ae..21a3a039 100644 --- a/src/dictionary/trie.rs +++ b/src/dictionary/trie.rs @@ -21,7 +21,7 @@ use super::{ BuildDictionaryError, Dictionary, DictionaryBuilder, DictionaryInfo, Entries, LookupStrategy, Phrase, }; -use crate::{dictionary::DictionaryUsage, zhuyin::Syllable}; +use crate::{dictionary::DictionaryUsage, exn::ResultExt, zhuyin::Syllable}; const DICT_FORMAT_VERSION: u8 = 0; @@ -1179,16 +1179,17 @@ impl DictionaryBuilder for TrieBuilder { } fn build(&mut self, path: &Path) -> Result<(), BuildDictionaryError> { + let err = || BuildDictionaryError::new("failed to finalize dictionary"); let mut tmpname = path.to_path_buf(); let pseudo_random = rand(); tmpname.set_file_name(format!("chewing-{pseudo_random}.dat")); - let database = File::create(&tmpname)?; + let database = File::create(&tmpname).or_raise(err)?; let mut writer = BufWriter::new(&database); - self.write(&mut writer)?; - writer.flush()?; - database.sync_data()?; + self.write(&mut writer).or_raise(err)?; + writer.flush().or_raise(err)?; + database.sync_data().or_raise(err)?; debug!("rename from {} to {}", tmpname.display(), path.display()); - fs::rename(&tmpname, path)?; + fs::rename(&tmpname, path).or_raise(err)?; Ok(()) } } diff --git a/src/editor/mod.rs b/src/editor/mod.rs index ba38b65c..42d81770 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -26,6 +26,7 @@ use crate::{ AssetLoader, Dictionary, DictionaryUsage, Layered, LookupStrategy, Trie, UpdateDictionaryError, UserDictionaryManager, }, + exn::{Exn, ResultExt}, input::{KeyState, KeyboardEvent, keysym::*}, zhuyin::Syllable, }; @@ -152,25 +153,6 @@ pub struct Editor { state: Box, } -/// All different errors that may happen when changing editor state. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum EditorError { - /// Requested invalid state change. - InvalidState, - /// Requested invalid input. - InvalidInput, - /// Requested state change was not possible. - Impossible, -} - -impl Display for EditorError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Editor cannot perform requested action.") - } -} - -impl Error for EditorError {} - #[derive(Debug)] pub(crate) struct SharedState { com: CompositionEditor, @@ -363,15 +345,19 @@ impl Editor { &mut self, syllables: &[Syllable], phrase: &str, - ) -> Result<(), UpdateDictionaryError> { - self.shared.learn_phrase(syllables, phrase) + ) -> Result<(), EditorError> { + self.shared + .learn_phrase(syllables, phrase) + .or_raise(|| EditorError::new(EditorErrorKind::InvalidState)) } pub fn unlearn_phrase( &mut self, syllables: &[Syllable], phrase: &str, - ) -> Result<(), UpdateDictionaryError> { - self.shared.unlearn_phrase(syllables, phrase) + ) -> Result<(), EditorError> { + self.shared + .unlearn_phrase(syllables, phrase) + .or_raise(|| EditorError::new(EditorErrorKind::InvalidState)) } /// All candidates after current page pub fn paginated_candidates(&self) -> Result, EditorError> { @@ -383,7 +369,7 @@ impl Editor { .skip(selecting.page_no * self.shared.options.candidates_per_page) .collect()) } else { - Err(EditorError::InvalidState) + Err(EditorError::new(EditorErrorKind::InvalidState)) } } pub fn all_candidates(&self) -> Result, EditorError> { @@ -391,7 +377,7 @@ impl Editor { if let Some(selecting) = any.downcast_ref::() { Ok(selecting.candidates(&self.shared, &self.shared.dict)) } else { - Err(EditorError::InvalidState) + Err(EditorError::new(EditorErrorKind::InvalidState)) } } pub fn current_page_no(&self) -> Result { @@ -399,7 +385,7 @@ impl Editor { if let Some(selecting) = any.downcast_ref::() { Ok(selecting.page_no) } else { - Err(EditorError::InvalidState) + Err(EditorError::new(EditorErrorKind::InvalidState)) } } pub fn total_page(&self) -> Result { @@ -407,14 +393,14 @@ impl Editor { if let Some(selecting) = any.downcast_ref::() { Ok(selecting.total_page(&self.shared, &self.shared.dict)) } else { - Err(EditorError::InvalidState) + Err(EditorError::new(EditorErrorKind::InvalidState)) } } pub fn select(&mut self, n: usize) -> Result<(), EditorError> { let any = self.state.as_mut() as &mut dyn Any; let selecting = match any.downcast_mut::() { Some(selecting) => selecting, - None => return Err(EditorError::InvalidState), + None => return Err(EditorError::new(EditorErrorKind::InvalidState)), }; match selecting.select(&mut self.shared, n) { Transition::ToState(to_state) => { @@ -427,7 +413,7 @@ impl Editor { self.shared.try_auto_commit(); } if self.shared.last_key_behavior == EditorKeyBehavior::Bell { - Err(EditorError::InvalidState) + Err(EditorError::new(EditorErrorKind::InvalidState)) } else { Ok(()) } @@ -438,7 +424,7 @@ impl Editor { self.state = Box::new(Entering); Ok(()) } else { - Err(EditorError::InvalidState) + Err(EditorError::new(EditorErrorKind::InvalidState)) } } pub fn cancel_entering_syllable(&mut self) { @@ -479,7 +465,7 @@ impl Editor { } pub fn commit(&mut self) -> Result<(), EditorError> { if self.shared.com.is_empty() { - return Err(EditorError::InvalidState); + return Err(EditorError::new(EditorErrorKind::InvalidState)); } self.shared.commit(); Ok(()) @@ -513,10 +499,10 @@ impl Editor { if let Some(s) = any.downcast_mut::() { match &mut s.sel { Selector::Phrase(s) => s.jump_to_next_selection_point(&self.shared.dict), - _ => Err(EditorError::InvalidState), + _ => Err(EditorError::new(EditorErrorKind::InvalidState)), } } else { - Err(EditorError::InvalidState) + Err(EditorError::new(EditorErrorKind::InvalidState)) } } pub fn jump_to_prev_selection_point(&mut self) -> Result<(), EditorError> { @@ -524,10 +510,10 @@ impl Editor { if let Some(s) = any.downcast_mut::() { match &mut s.sel { Selector::Phrase(s) => s.jump_to_prev_selection_point(&self.shared.dict), - _ => Err(EditorError::InvalidState), + _ => Err(EditorError::new(EditorErrorKind::InvalidState)), } } else { - Err(EditorError::InvalidState) + Err(EditorError::new(EditorErrorKind::InvalidState)) } } pub fn jump_to_first_selection_point(&mut self) -> Result<(), EditorError> { @@ -538,10 +524,10 @@ impl Editor { s.jump_to_first_selection_point(&self.shared.dict); Ok(()) } - _ => Err(EditorError::InvalidState), + _ => Err(EditorError::new(EditorErrorKind::InvalidState)), } } else { - Err(EditorError::InvalidState) + Err(EditorError::new(EditorErrorKind::InvalidState)) } } pub fn jump_to_last_selection_point(&mut self) -> Result<(), EditorError> { @@ -552,10 +538,10 @@ impl Editor { s.jump_to_last_selection_point(&self.shared.dict); Ok(()) } - _ => Err(EditorError::InvalidState), + _ => Err(EditorError::new(EditorErrorKind::InvalidState)), } } else { - Err(EditorError::InvalidState) + Err(EditorError::new(EditorErrorKind::InvalidState)) } } pub fn start_selecting(&mut self) -> Result<(), EditorError> { @@ -578,7 +564,7 @@ impl Editor { if self.is_selecting() { Ok(()) } else { - Err(EditorError::InvalidState) + Err(EditorError::new(EditorErrorKind::InvalidState)) } } pub fn notification(&self) -> &str { @@ -625,7 +611,7 @@ impl SharedState { &mut self, start: usize, end: usize, - ) -> Result<(), UpdateDictionaryError> { + ) -> Result<(), EditorError> { let result = self.learn_phrase_in_range_quiet(start, end); match &result { Ok(phrase) => { @@ -635,6 +621,7 @@ impl SharedState { Err(msg) => { msg.clone_into(&mut self.notice_buffer); Err(UpdateDictionaryError::new("failed to learn new phrase")) + .or_raise(|| EditorError::new(EditorErrorKind::InvalidState)) } } } @@ -678,11 +665,7 @@ impl SharedState { } result.map(|_| phrase) } - fn learn_phrase( - &mut self, - syllables: &[Syllable], - phrase: &str, - ) -> Result<(), UpdateDictionaryError> { + fn learn_phrase(&mut self, syllables: &[Syllable], phrase: &str) -> Result<(), EditorError> { if syllables.len() != phrase.chars().count() { warn!( "syllables({:?})[{}] and phrase({})[{}] has different length", @@ -693,11 +676,14 @@ impl SharedState { ); return Err(UpdateDictionaryError::new( "failed to learn phrase: syllables and phrase has different length", - )); + )) + .or_raise(|| EditorError::new(EditorErrorKind::InvalidState)); } let phrases = self.dict.lookup(syllables, LookupStrategy::Standard); if phrases.is_empty() { - self.dict.add_phrase(syllables, (phrase, 10).into())?; + self.dict + .add_phrase(syllables, (phrase, 10).into()) + .or_raise(|| EditorError::new(EditorErrorKind::InvalidState))?; return Ok(()); } let phrase = phrases @@ -714,12 +700,11 @@ impl SharedState { self.dirty_level += 1; Ok(()) } - fn unlearn_phrase( - &mut self, - syllables: &[Syllable], - phrase: &str, - ) -> Result<(), UpdateDictionaryError> { - let _ = self.dict.remove_phrase(syllables, phrase); + fn unlearn_phrase(&mut self, syllables: &[Syllable], phrase: &str) -> Result<(), EditorError> { + let _ = self + .dict + .remove_phrase(syllables, phrase) + .or_raise(|| EditorError::new(EditorErrorKind::InvalidState))?; self.dirty_level += 1; Ok(()) } @@ -1632,6 +1617,37 @@ impl State for Highlighting { } } +/// All different errors that may happen when changing editor state. +#[derive(Debug)] +pub enum EditorErrorKind { + /// Requested invalid state change. + InvalidState, + /// Requested invalid input. + InvalidInput, + /// Requested state change was not possible. + Impossible, +} + +#[derive(Debug)] +pub struct EditorError { + kind: EditorErrorKind, + source: Option>, +} + +impl EditorError { + fn new(kind: EditorErrorKind) -> EditorError { + EditorError { kind, source: None } + } +} + +impl Display for EditorError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Editor cannot perform requested action: {:?}", self.kind) + } +} + +impl_exn!(EditorError); + #[cfg(test)] mod tests { use super::collect_new_phrases; diff --git a/src/editor/selection/phrase.rs b/src/editor/selection/phrase.rs index 20d5cffd..bc55f837 100644 --- a/src/editor/selection/phrase.rs +++ b/src/editor/selection/phrase.rs @@ -3,7 +3,7 @@ use std::cmp::{Reverse, min}; use crate::{ conversion::{Composition, Gap, Interval}, dictionary::{Dictionary, Layered, LookupStrategy}, - editor::{EditorError, SharedState}, + editor::{EditorError, EditorErrorKind, SharedState}, zhuyin::Syllable, }; @@ -140,7 +140,7 @@ impl PhraseSelector { self.end = end; Ok(()) } else { - Err(EditorError::Impossible) + Err(EditorError::new(EditorErrorKind::Impossible)) } } pub(crate) fn jump_to_prev_selection_point( @@ -152,7 +152,7 @@ impl PhraseSelector { self.end = end; Ok(()) } else { - Err(EditorError::Impossible) + Err(EditorError::new(EditorErrorKind::Impossible)) } } pub(crate) fn jump_to_first_selection_point(&mut self, dict: &D) { diff --git a/src/exn.rs b/src/exn.rs new file mode 100644 index 00000000..08f1bcb8 --- /dev/null +++ b/src/exn.rs @@ -0,0 +1,123 @@ +//! [exn](https://crates.io/crates/exn) inspired error handling extensions + +use std::{convert::Infallible, error::Error}; + +pub(crate) trait Exn: Error { + fn with_source(self, err: impl Error + Send + Sync + 'static) -> Self; +} + +pub(crate) trait ResultExt { + type Success; + type Error: Error + Send + Sync + 'static; + + fn or_raise(self, err: F) -> Result + where + A: Exn, + F: FnOnce() -> A; +} + +impl ResultExt for Result +where + E: Error + Send + Sync + 'static, +{ + type Success = T; + type Error = E; + + fn or_raise(self, err: F) -> Result + where + A: Exn, + F: FnOnce() -> A, + { + match self { + Ok(t) => Ok(t), + Err(error) => Err(err().with_source(error)), + } + } +} + +impl ResultExt for Option { + type Success = T; + type Error = Infallible; + + fn or_raise(self, err: F) -> Result + where + A: Exn, + F: FnOnce() -> A, + { + match self { + Some(t) => Ok(t), + None => Err(err()), + } + } +} + +macro_rules! impl_exn { + ($error_type:ty) => { + impl Error for $error_type { + fn source(&self) -> Option<&(dyn Error + 'static)> { + self.source + .as_ref() + .map(|s| s.as_ref() as &(dyn Error + 'static)) + } + } + + impl Exn for $error_type { + fn with_source(mut self, err: impl Error + Send + Sync + 'static) -> Self { + self.source = Some(Box::new(err)); + self + } + } + }; +} + +#[cfg(test)] +mod tests { + use std::fmt::Display; + + use super::*; + + #[derive(Debug, Default)] + struct TestError { + source: Option>, + } + impl Display for TestError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "failed successfully") + } + } + impl_exn!(TestError); + + #[test] + fn walk_source() { + let source = std::io::Error::from(std::io::ErrorKind::Unsupported); + let test_error = TestError::default().with_source(source); + + assert!(test_error.source().is_some(), "can get source error back"); + } + + #[test] + fn exn_from_result() { + let error = || TestError::default(); + let source: std::io::Result<()> = + Err(std::io::Error::from(std::io::ErrorKind::Unsupported)); + let result_error = source.or_raise(error); + + assert!( + result_error.is_err(), + "can convert Source Result to Result with Exn" + ); + assert!( + result_error.unwrap_err().source().is_some(), + "can get source error back" + ); + } + + #[test] + fn exn_from_option() { + let error = || TestError::default(); + let opt: Option<()> = None; + let result_error = opt.or_raise(error); + + assert!(result_error.is_err(), "can convert None to Result::Err"); + } +} diff --git a/src/lib.rs b/src/lib.rs index 1b0bf4a2..e4e49709 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -75,6 +75,9 @@ //! Other required files `swkb.dat` and `symbols.dat` can be copied directly to //! the dictionary folder. +#[macro_use] +mod exn; + pub mod conversion; pub mod dictionary; pub mod editor; diff --git a/src/path.rs b/src/path.rs index 2099883a..25fbd68c 100644 --- a/src/path.rs +++ b/src/path.rs @@ -4,7 +4,7 @@ use std::{ env, ffi::OsStr, fs, - io::ErrorKind, + io::{self, ErrorKind}, path::{Path, PathBuf}, }; @@ -59,7 +59,7 @@ pub fn search_path_from_env_var() -> String { chewing_path } -pub(crate) fn find_path_by_files(search_path: &str, files: &[&str]) -> Option { +pub(crate) fn find_path_by_files(search_path: &str, files: &[&str]) -> Result { for path in search_path.split(SEARCH_PATH_SEP) { let prefix = Path::new(path).to_path_buf(); info!("Search files {:?} in {}", files, prefix.display()); @@ -73,10 +73,10 @@ pub(crate) fn find_path_by_files(search_path: &str, files: &[&str]) -> Option Vec { diff --git a/src/zhuyin/bopomofo.rs b/src/zhuyin/bopomofo.rs index 051eea5f..fa6c961f 100644 --- a/src/zhuyin/bopomofo.rs +++ b/src/zhuyin/bopomofo.rs @@ -208,74 +208,6 @@ impl Bopomofo { } } -/// Enum to store the various types of errors that can cause parsing a bopomofo -/// symbol to fail. -/// -/// # Example -/// -/// ``` -/// # use std::str::FromStr; -/// # use chewing::zhuyin::Bopomofo; -/// if let Err(e) = Bopomofo::from_str("a12") { -/// println!("Failed conversion to bopomofo: {e}"); -/// } -/// ``` -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -#[non_exhaustive] -pub enum BopomofoErrorKind { - /// Value being parsed is empty. - Empty, - /// Contains an invalid symbol. - InvalidSymbol, -} - -/// An error which can be returned when parsing an bopomofo symbol. -/// -/// # Potential causes -/// -/// Among other causes, `ParseBopomofoError` can be thrown because of leading or trailing whitespace -/// in the string e.g., when it is obtained from the standard input. -/// Using the [`str::trim()`] method ensures that no whitespace remains before parsing. -/// -/// # Example -/// -/// ``` -/// # use std::str::FromStr; -/// # use chewing::zhuyin::Bopomofo; -/// if let Err(e) = Bopomofo::from_str("a12") { -/// println!("Failed conversion to bopomofo: {e}"); -/// } -/// ``` -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct ParseBopomofoError { - kind: BopomofoErrorKind, -} - -impl ParseBopomofoError { - fn empty() -> ParseBopomofoError { - Self { - kind: BopomofoErrorKind::Empty, - } - } - fn invalid_symbol() -> ParseBopomofoError { - Self { - kind: BopomofoErrorKind::InvalidSymbol, - } - } - /// Outputs the detailed cause of parsing an bopomofo failing. - pub fn kind(&self) -> &BopomofoErrorKind { - &self.kind - } -} - -impl Display for ParseBopomofoError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Parse bopomofo error: {:?}", self.kind) - } -} - -impl Error for ParseBopomofoError {} - impl Display for Bopomofo { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_char((*self).into()) @@ -286,14 +218,14 @@ impl FromStr for Bopomofo { type Err = ParseBopomofoError; fn from_str(s: &str) -> Result { - if s.is_empty() { + let mut chars = s.chars(); + let Some(bopomofo) = chars.next().map(|ch| ch.try_into()) else { return Err(ParseBopomofoError::empty()); + }; + if let Some(ch) = chars.next() { + return Err(ParseBopomofoError::invalid_symbol(ch)); } - if s.chars().count() != 1 { - return Err(ParseBopomofoError::invalid_symbol()); - } - - s.chars().next().unwrap().try_into() + bopomofo } } @@ -393,11 +325,79 @@ impl TryFrom for Bopomofo { 'ˊ' => Ok(TONE2), 'ˇ' => Ok(TONE3), 'ˋ' => Ok(TONE4), - _ => Err(ParseBopomofoError::invalid_symbol()), + _ => Err(ParseBopomofoError::invalid_symbol(c)), + } + } +} + +/// Enum to store the various types of errors that can cause parsing a bopomofo +/// symbol to fail. +/// +/// # Example +/// +/// ``` +/// # use std::str::FromStr; +/// # use chewing::zhuyin::Bopomofo; +/// if let Err(e) = Bopomofo::from_str("a12") { +/// println!("Failed conversion to bopomofo: {e}"); +/// } +/// ``` +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum BopomofoErrorKind { + /// Value being parsed is empty. + Empty, + /// Contains an invalid symbol. + InvalidSymbol(char), +} + +/// An error which can be returned when parsing an bopomofo symbol. +/// +/// # Potential causes +/// +/// Among other causes, `ParseBopomofoError` can be thrown because of leading or trailing whitespace +/// in the string e.g., when it is obtained from the standard input. +/// Using the [`str::trim()`] method ensures that no whitespace remains before parsing. +/// +/// # Example +/// +/// ``` +/// # use std::str::FromStr; +/// # use chewing::zhuyin::Bopomofo; +/// if let Err(e) = Bopomofo::from_str("a12") { +/// println!("Failed conversion to bopomofo: {e}"); +/// } +/// ``` +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ParseBopomofoError { + kind: BopomofoErrorKind, +} + +impl ParseBopomofoError { + fn empty() -> ParseBopomofoError { + Self { + kind: BopomofoErrorKind::Empty, + } + } + fn invalid_symbol(ch: char) -> ParseBopomofoError { + Self { + kind: BopomofoErrorKind::InvalidSymbol(ch), } } + /// Outputs the detailed cause of parsing an bopomofo failing. + pub fn kind(&self) -> &BopomofoErrorKind { + &self.kind + } } +impl Display for ParseBopomofoError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Parse bopomofo error: {:?}", self.kind) + } +} + +impl Error for ParseBopomofoError {} + #[cfg(test)] mod tests { use super::Bopomofo; @@ -420,12 +420,12 @@ mod tests { #[test] fn parse_invalid() { assert_eq!( - Err(ParseBopomofoError::invalid_symbol()), + Err(ParseBopomofoError::invalid_symbol('b')), "abc".parse::() ); assert_eq!( - &BopomofoErrorKind::InvalidSymbol, - ParseBopomofoError::invalid_symbol().kind() + &BopomofoErrorKind::InvalidSymbol('c'), + ParseBopomofoError::invalid_symbol('c').kind() ); } diff --git a/src/zhuyin/syllable.rs b/src/zhuyin/syllable.rs index 1e08ecd9..a803ab9f 100644 --- a/src/zhuyin/syllable.rs +++ b/src/zhuyin/syllable.rs @@ -6,7 +6,8 @@ use std::{ str::FromStr, }; -use super::{Bopomofo, BopomofoKind, ParseBopomofoError}; +use super::{Bopomofo, BopomofoKind}; +use crate::exn::{Exn, ResultExt}; /// The consonants and vowels that are taken together to make a single sound. /// @@ -230,7 +231,7 @@ impl TryFrom for Syllable { fn try_from(value: u16) -> Result { // TODO check invalid value Ok(Syllable { - value: NonZeroU16::try_from(value).map_err(|_| DecodeSyllableError)?, + value: NonZeroU16::try_from(value).or_raise(|| DecodeSyllableError::new())?, }) } } @@ -239,10 +240,11 @@ impl FromStr for Syllable { type Err = ParseSyllableError; fn from_str(s: &str) -> Result { + let error = || ParseSyllableError::new(); let mut builder = Syllable::builder(); for c in s.chars() { - let bopomofo = Bopomofo::try_from(c)?; - builder = builder.insert(bopomofo)?; + let bopomofo = Bopomofo::try_from(c).or_raise(error)?; + builder = builder.insert(bopomofo).or_raise(error)?; } Ok(builder.build()) } @@ -352,8 +354,16 @@ impl SyllableBuilder { } /// Errors during decoding a syllable from a u16. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct DecodeSyllableError; +#[derive(Debug)] +pub struct DecodeSyllableError { + source: Option>, +} + +impl DecodeSyllableError { + fn new() -> DecodeSyllableError { + DecodeSyllableError { source: None } + } +} impl Display for DecodeSyllableError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -361,7 +371,7 @@ impl Display for DecodeSyllableError { } } -impl Error for DecodeSyllableError {} +impl_exn!(DecodeSyllableError); /// Errors when parsing a str to a syllable. #[derive(Clone, Debug, PartialEq, Eq)] @@ -421,14 +431,14 @@ impl Display for BuildSyllableError { impl Error for BuildSyllableError {} /// Errors when parsing a str to a syllable. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Debug)] pub struct ParseSyllableError { - kind: SyllableErrorKind, + source: Option>, } impl ParseSyllableError { - pub fn kind(&self) -> &SyllableErrorKind { - &self.kind + fn new() -> ParseSyllableError { + ParseSyllableError { source: None } } } @@ -438,21 +448,7 @@ impl Display for ParseSyllableError { } } -impl Error for ParseSyllableError {} - -impl From for ParseSyllableError { - fn from(_: ParseBopomofoError) -> Self { - ParseSyllableError { - kind: SyllableErrorKind::InvalidBopomofo, - } - } -} - -impl From for ParseSyllableError { - fn from(value: BuildSyllableError) -> Self { - ParseSyllableError { kind: value.kind } - } -} +impl_exn!(ParseSyllableError); /// Builds a syllable from bopomofos. /// diff --git a/tools/src/info.rs b/tools/src/info.rs index 9d8a0525..c90657a0 100644 --- a/tools/src/info.rs +++ b/tools/src/info.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{Context, Result}; use chewing::{ dictionary::{Dictionary, SingleDictionaryLoader, UserDictionaryManager}, path::{find_files_by_ext, search_path_from_env_var}, @@ -7,6 +7,7 @@ use chewing::{ use crate::flags; pub(crate) fn run(args: flags::Info) -> Result<()> { + let error = || "failed to inspect file"; if args.system { // FIXME: use find_files_by_ext and generic loader let loader = SingleDictionaryLoader::new(); @@ -24,7 +25,7 @@ pub(crate) fn run(args: flags::Info) -> Result<()> { } } if args.user { - let dict = UserDictionaryManager::new().init()?; + let dict = UserDictionaryManager::new().init().with_context(error)?; if args.json { print_json_info(&[dict], "user"); } else { @@ -32,7 +33,9 @@ pub(crate) fn run(args: flags::Info) -> Result<()> { } } if let Some(path) = args.path { - let dict = SingleDictionaryLoader::new().guess_format_and_load(&path)?; + let dict = SingleDictionaryLoader::new() + .guess_format_and_load(&path) + .with_context(error)?; if args.json { print_json_info(&[dict], "input"); } else { diff --git a/tools/src/init_database.rs b/tools/src/init_database.rs index 92b758f1..50597503 100644 --- a/tools/src/init_database.rs +++ b/tools/src/init_database.rs @@ -1,7 +1,5 @@ use std::{ any::Any, - error::Error, - fmt::Display, fs::{self, File}, io::{BufRead, BufReader}, path::Path, @@ -9,7 +7,7 @@ use std::{ #[cfg(not(feature = "sqlite"))] use anyhow::bail; -use anyhow::{Context, Result, anyhow}; +use anyhow::{Context, Result}; #[cfg(feature = "sqlite")] use chewing::dictionary::SqliteDictionaryBuilder; use chewing::{ @@ -19,53 +17,12 @@ use chewing::{ use crate::flags; -#[derive(Debug)] -struct ParseError { - line_num: usize, - line: String, - source: anyhow::Error, -} - -fn parse_error(line_num: usize, line: &str) -> ParseError { - ParseError { - line_num, - line: line.to_string(), - source: anyhow::anyhow!("Invalid format. Use the --csv flag to enable CSV parsing."), - } -} - -impl Display for ParseError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "Parsing failed at line {}: {}", - self.line_num + 1, - self.line - ) - } -} - -impl Error for ParseError { - fn source(&self) -> Option<&(dyn Error + 'static)> { - Some(self.source.as_ref()) - } -} - -trait IntoParseError { - fn parse_error(self, line_num: usize, line: &str) -> std::result::Result; -} - -impl IntoParseError for Result { - fn parse_error(self, line_num: usize, line: &str) -> std::result::Result { - self.map_err(|source| ParseError { - line_num, - line: line.to_string(), - source, - }) - } -} - pub(crate) fn run(args: flags::InitDatabase) -> Result<()> { + let error = "Failed to build dictionary file."; + let parse_error = |line_num, line: &str, msg| { + anyhow::Error::msg(format!("{line_num:>5} | {line}\n{msg} at line {line_num}")) + }; + let mut builder: Box = match args.db_type { flags::DbType::Sqlite => { #[cfg(feature = "sqlite")] @@ -85,14 +42,14 @@ pub(crate) fn run(args: flags::InitDatabase) -> Result<()> { let mut usage = args.usage; let software = format!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")); - let tsi = File::open(args.tsi_src)?; + let tsi = File::open(args.tsi_src).context(error)?; let reader = BufReader::new(tsi); let delimiter = if args.csv { ',' } else { ' ' }; let mut read_front_matter = true; let mut errors = vec![]; for (line_num, line) in reader.lines().enumerate() { - let line = line?; + let line = line.context(error)?; let line = line.trim(); if line.is_empty() { continue; @@ -103,7 +60,7 @@ pub(crate) fn run(args: flags::InitDatabase) -> Result<()> { } else if read_front_matter { let Some((key, value)) = line.trim_start_matches('#').trim().split_once(delimiter) else { - errors.push(parse_error(line_num, "invalid metadata").into()); + errors.push(parse_error(line_num, line, "Invalid metadata")); continue; }; let value = value.trim_end_matches(delimiter).to_string(); @@ -119,17 +76,17 @@ pub(crate) fn run(args: flags::InitDatabase) -> Result<()> { } else if line.starts_with('#') { continue; } - match parse_line(line_num, delimiter, &line, args.fix) { + match parse_line(delimiter, &line, args.fix) { Ok((syllables, phrase, freq)) => { if syllables.len() != phrase.chars().count() { - errors.push( - anyhow!("word count doesn't match").context(parse_error(line_num, line)), - ); + errors.push(parse_error(line_num, line, "Word count doesn't match")); continue; } - builder.insert(&syllables, (phrase, freq).into())?; + builder + .insert(&syllables, (phrase, freq).into()) + .context(error)?; } - Err(error) => errors.push(error), + Err(error) => errors.push(error.context(parse_error(line_num, line, "Parse error"))), }; } if !errors.is_empty() { @@ -137,7 +94,9 @@ pub(crate) fn run(args: flags::InitDatabase) -> Result<()> { eprintln!("{:#}", err); } if !args.fix { - eprintln!("Hint: Use --fix to automatically fix common errors"); + eprintln!(); + eprintln!("Hint: Use --csv flag to enable CSV parsing."); + eprintln!("Hint: Use --fix to automatically fix common errors."); } if !args.skip_invalid { std::process::exit(1) @@ -180,27 +139,21 @@ pub(crate) fn run(args: flags::InitDatabase) -> Result<()> { Ok(()) } -fn parse_line( - line_num: usize, - delimiter: char, - line: &str, - fix: bool, -) -> Result<(Vec, &str, u32)> { +fn parse_line(delimiter: char, line: &str, fix: bool) -> Result<(Vec, &str, u32)> { let phrase = line .split(delimiter) .find(|s| !s.is_empty()) - .ok_or(parse_error(line_num, line))? + .context("failed to parse phrase")? .trim_matches('"'); let freq: u32 = line .split(delimiter) .filter(|s| !s.is_empty()) .nth(1) - .ok_or(parse_error(line_num, line))? + .context("failed to parse frequency")? .trim_matches('"') .parse() - .context("Unable to parse frequency") - .parse_error(line_num, line)?; + .context("failed to parse frequency")?; let mut syllables = vec![]; @@ -225,13 +178,8 @@ fn parse_line( c }; syllable_builder = syllable_builder - .insert( - Bopomofo::try_from(c) - .context("parsing bopomofo") - .parse_error(line_num, line)?, - ) - .with_context(|| format!("Parsing syllables {}", syllable_str)) - .parse_error(line_num, line)?; + .insert(Bopomofo::try_from(c)?) + .with_context(|| format!("failed to parse syllables {}", syllable_str))?; } syllables.push(syllable_builder.build()); } @@ -257,7 +205,7 @@ mod tests { #[test] fn parse_ssv() { let line = "鑰匙 668 ㄧㄠˋ ㄔˊ # not official"; - if let Ok((syllables, phrase, freq)) = parse_line(0, ' ', &line, false) { + if let Ok((syllables, phrase, freq)) = parse_line(' ', &line, false) { assert_eq!(syllables, vec![syl![I, AU, TONE4], syl![CH, TONE2]]); assert_eq!("鑰匙", phrase); assert_eq!(668, freq); @@ -269,7 +217,7 @@ mod tests { #[test] fn parse_ssv_multiple_whitespace() { let line = "鑰匙 668 ㄧㄠˋ ㄔˊ # not official"; - if let Ok((syllables, phrase, freq)) = parse_line(0, ' ', &line, false) { + if let Ok((syllables, phrase, freq)) = parse_line(' ', &line, false) { assert_eq!(syllables, vec![syl![I, AU, TONE4], syl![CH, TONE2]]); assert_eq!("鑰匙", phrase); assert_eq!(668, freq); @@ -281,7 +229,7 @@ mod tests { #[test] fn parse_ssv_syllable_errors() { let line = "地永天長 50 ㄉ一ˋ ㄩㄥˇ ㄊ一ㄢ ㄔ丫ˊ"; - if let Ok((syllables, phrase, freq)) = parse_line(0, ' ', &line, true) { + if let Ok((syllables, phrase, freq)) = parse_line(' ', &line, true) { assert_eq!( syllables, vec![ @@ -301,7 +249,7 @@ mod tests { #[test] fn parse_csv() { let line = "鑰匙,668,ㄧㄠˋ ㄔˊ # not official"; - if let Ok((syllables, phrase, freq)) = parse_line(0, ',', &line, false) { + if let Ok((syllables, phrase, freq)) = parse_line(',', &line, false) { assert_eq!(syllables, vec![syl![I, AU, TONE4], syl![CH, TONE2]]); assert_eq!("鑰匙", phrase); assert_eq!(668, freq); @@ -313,7 +261,7 @@ mod tests { #[test] fn parse_csv_quoted() { let line = "\"鑰匙\",668,\"ㄧㄠˋ ㄔˊ # not official\""; - if let Ok((syllables, phrase, freq)) = parse_line(0, ',', &line, false) { + if let Ok((syllables, phrase, freq)) = parse_line(',', &line, false) { assert_eq!(syllables, vec![syl![I, AU, TONE4], syl![CH, TONE2]]); assert_eq!("鑰匙", phrase); assert_eq!(668, freq);