diff --git a/Cargo.lock b/Cargo.lock index d3abe7a..5bb4690 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -512,6 +512,7 @@ dependencies = [ "reqwest", "serde", "serde_json", + "thiserror", "tokio", "zip", ] @@ -1495,6 +1496,26 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "thiserror" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "time" version = "0.3.36" diff --git a/Cargo.toml b/Cargo.toml index 551de68..7b7bfa2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ zip = { version = "2.1.3", optional = true } mktemp = { version = "0.5.0", optional = true } rand = { version = "0.8.5", optional = true } port_scanner = { version = "0.1.5", optional = true } +thiserror = "1.0.61" [features] embedded-server = ["zip", "mktemp", "rand", "port_scanner"] diff --git a/src/lib.rs b/src/lib.rs index e03a68d..f631c68 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,10 +36,11 @@ //! println!("RESULT = {:#?}", res); //! ``` +use thiserror::Error; + #[cfg(feature = "embedded-server")] use crate::server::EmbeddedServer; use std::collections::HashMap; -use std::error::Error; #[cfg(feature = "embedded-server")] pub mod server; @@ -302,6 +303,28 @@ pub struct SuggestResult { pub suggestions: Vec, } +#[derive(Debug, Error)] +pub enum Error { + #[cfg(feature = "embedded-server")] + #[error("Grammalecte-server failed to start")] + ServerStartFailed(#[from] server::Error), + + #[error("Failed to Serialize Option in Json")] + OptionJsonSerialization(#[source] serde_json::Error), + + #[error("Failed to send request `check with option`")] + RequestSendCheckWithOptions(#[source] reqwest::Error), + + #[error("Failed to send request `suggest`")] + RequestSendSuggest(#[source] reqwest::Error), + + #[error("Failed to Deserialize Check result")] + CheckResultDeserialize(#[source] reqwest::Error), + + #[error("Failed to Deserialize Suggest result")] + SuggestDeserialize(#[source] reqwest::Error), +} + /// The Grammalecte client itself pub struct GrammalecteClient { base_url: String, @@ -335,7 +358,7 @@ impl GrammalecteClient { /// /// Python 3.7 or higher must is required at runtime #[cfg(feature = "embedded-server")] - pub fn start_server() -> Result> { + pub fn start_server() -> Result { let server = EmbeddedServer::start()?; Ok(Self { base_url: server.base_url(), @@ -344,7 +367,7 @@ impl GrammalecteClient { } /// Run spell check on text - pub async fn spell_check(&self, text: &str) -> Result> { + pub async fn spell_check(&self, text: &str) -> Result { self.spell_check_with_options(text, &HashMap::new()).await } @@ -353,7 +376,7 @@ impl GrammalecteClient { &self, text: &str, options: &HashMap, - ) -> Result> { + ) -> Result { let url = format!("{}/gc_text/fr", self.base_url); log::debug!("Will use URL {} for spell check", url); @@ -361,7 +384,7 @@ impl GrammalecteClient { .iter() .map(|t| (t.0.id(), t.1)) .collect::>(); - let options = serde_json::to_string(&options)?; + let options = serde_json::to_string(&options).map_err(Error::OptionJsonSerialization)?; let mut params = HashMap::new(); params.insert("text", text); @@ -371,28 +394,32 @@ impl GrammalecteClient { .post(url) .form(¶ms) .send() - .await? + .await + .map_err(Error::RequestSendCheckWithOptions)? .json::() - .await?; + .await + .map_err(Error::CheckResultDeserialize)?; Ok(result) } /// Ask for word suggestion - pub async fn suggest(&self, token: &str) -> Result> { + pub async fn suggest(&self, token: &str) -> Result { let url = format!("{}/suggest/fr", self.base_url); log::debug!("Will use URL {} for word suggestion", url); let mut params = HashMap::new(); params.insert("token", token); - Ok(reqwest::Client::new() + reqwest::Client::new() .post(&url) .form(¶ms) .send() - .await? + .await + .map_err(Error::RequestSendSuggest)? .json() - .await?) + .await + .map_err(Error::SuggestDeserialize) } } diff --git a/src/server.rs b/src/server.rs index ea7f312..439dac7 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,10 +1,41 @@ use crate::server::utils::{get_free_port, wait_for_port}; use mktemp::Temp; -use std::error::Error; -use std::io::{Cursor, Read}; +use std::io::{self, Cursor, Read}; use std::process::{Child, Stdio}; +use thiserror::Error; +use zip::result::ZipError; use zip::ZipArchive; +#[derive(Debug, Error)] +pub enum Error { + #[error("Grammalecte-server failed to launch process")] + StartServerProcess(#[source] io::Error), + + #[error("Get an available port failed")] + GetFreePort(#[source] io::Error), + + #[error("Port {port} did not open in time!")] + WaitPortOpen { port: u16 }, + + #[error("Create temporary directory failed")] + CreateTempDir(#[source] io::Error), + + #[error("Zip archive loading failed")] + ZipArchiveLoading(#[source] ZipError), + + #[error("Access file by index failed")] + ZipFileIndex(#[source] ZipError), + + #[error("Create directory for files from zip")] + CreateDirectoryForZipFile(#[source] io::Error), + + #[error("Read file from zip archive")] + ZipFileReadToEnd(#[source] io::Error), + + #[error("Write file from archive on disk")] + WriteFile(#[source] io::Error), +} + pub struct EmbeddedServer { _srv_dir: Temp, port: u16, @@ -13,29 +44,31 @@ pub struct EmbeddedServer { impl EmbeddedServer { /// Start embedded Grammalecte server on a random free port - pub fn start() -> Result> { - Self::start_listen_on_port(get_free_port()?) + pub fn start() -> Result { + Self::start_listen_on_port(get_free_port()) } /// Start embedded Grammalecte server on a given port - pub fn start_listen_on_port(port: u16) -> Result> { + pub fn start_listen_on_port(port: u16) -> Result { log::info!("Will start server"); // First, unpack server - let dest = mktemp::Temp::new_dir()?; + let dest = mktemp::Temp::new_dir().map_err(Error::CreateTempDir)?; let cursor = Cursor::new(include_bytes!("GrammalecteDist.zip")); - let mut zip = ZipArchive::new(cursor)?; + let mut zip = ZipArchive::new(cursor).map_err(Error::ZipArchiveLoading)?; for i in 0..zip.len() { - let mut file = zip.by_index(i)?; + let mut file = zip.by_index(i).map_err(Error::ZipFileIndex)?; if file.is_dir() { log::debug!("Create directory: {}", file.name()); - std::fs::create_dir_all(dest.join(file.name()))?; + std::fs::create_dir_all(dest.join(file.name())) + .map_err(Error::CreateDirectoryForZipFile)?; } else { log::debug!("Decompress file: {}", file.name()); let mut buff = Vec::with_capacity(file.size() as usize); - file.read_to_end(&mut buff)?; + file.read_to_end(&mut buff) + .map_err(Error::ZipFileReadToEnd)?; - std::fs::write(dest.join(file.name()), buff)?; + std::fs::write(dest.join(file.name()), buff).map_err(Error::WriteFile)?; } } @@ -54,7 +87,8 @@ impl EmbeddedServer { .arg(port.to_string()) .stdout(Stdio::null()) .stderr(Stdio::null()) - .spawn()?; + .spawn() + .map_err(Error::StartServerProcess)?; wait_for_port(port)?; @@ -78,11 +112,12 @@ impl Drop for EmbeddedServer { } mod utils { - use std::io::ErrorKind; use std::time::Duration; + use super::Error; + /// Get a free port - pub fn get_free_port() -> std::io::Result { + pub fn get_free_port() -> u16 { let mut port = 0; while !(2000..=64000).contains(&port) { @@ -93,10 +128,10 @@ mod utils { port += 1; } - Ok(port) + port } - pub fn wait_for_port(port: u16) -> std::io::Result<()> { + pub fn wait_for_port(port: u16) -> Result<(), Error> { for _ in 0..50 { if port_scanner::scan_port(port) { return Ok(()); @@ -104,9 +139,6 @@ mod utils { std::thread::sleep(Duration::from_millis(100)); } - Err(std::io::Error::new( - ErrorKind::Other, - format!("Port {} did not open in time!", port), - ))? + Err(Error::WaitPortOpen { port }) } }