improve error management with thiserror use
- add thiserror dependency - use distinct Error enum in lib and server
This commit is contained in:
		
							
								
								
									
										21
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										21
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							@@ -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"
 | 
			
		||||
 
 | 
			
		||||
@@ -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"]
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										49
									
								
								src/lib.rs
									
									
									
									
									
								
							
							
						
						
									
										49
									
								
								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<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[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<Self, Box<dyn Error>> {
 | 
			
		||||
    pub fn start_server() -> Result<Self, Error> {
 | 
			
		||||
        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<CheckResult, Box<dyn Error>> {
 | 
			
		||||
    pub async fn spell_check(&self, text: &str) -> Result<CheckResult, Error> {
 | 
			
		||||
        self.spell_check_with_options(text, &HashMap::new()).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -353,7 +376,7 @@ impl GrammalecteClient {
 | 
			
		||||
        &self,
 | 
			
		||||
        text: &str,
 | 
			
		||||
        options: &HashMap<GramOpt, bool>,
 | 
			
		||||
    ) -> Result<CheckResult, Box<dyn Error>> {
 | 
			
		||||
    ) -> Result<CheckResult, Error> {
 | 
			
		||||
        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::<HashMap<_, _>>();
 | 
			
		||||
        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::<CheckResult>()
 | 
			
		||||
            .await?;
 | 
			
		||||
            .await
 | 
			
		||||
            .map_err(Error::CheckResultDeserialize)?;
 | 
			
		||||
 | 
			
		||||
        Ok(result)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Ask for word suggestion
 | 
			
		||||
    pub async fn suggest(&self, token: &str) -> Result<SuggestResult, Box<dyn Error>> {
 | 
			
		||||
    pub async fn suggest(&self, token: &str) -> Result<SuggestResult, Error> {
 | 
			
		||||
        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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,39 @@
 | 
			
		||||
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 zip::ZipArchive;
 | 
			
		||||
use thiserror::Error;
 | 
			
		||||
use zip::{result::ZipError, 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,
 | 
			
		||||
@@ -13,29 +43,31 @@ pub struct EmbeddedServer {
 | 
			
		||||
 | 
			
		||||
impl EmbeddedServer {
 | 
			
		||||
    /// Start embedded Grammalecte server on a random free port
 | 
			
		||||
    pub fn start() -> Result<Self, Box<dyn Error>> {
 | 
			
		||||
        Self::start_listen_on_port(get_free_port()?)
 | 
			
		||||
    pub fn start() -> Result<Self, Error> {
 | 
			
		||||
        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<Self, Box<dyn Error>> {
 | 
			
		||||
    pub fn start_listen_on_port(port: u16) -> Result<Self, Error> {
 | 
			
		||||
        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 +86,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 +111,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<u16> {
 | 
			
		||||
    pub fn get_free_port() -> u16 {
 | 
			
		||||
        let mut port = 0;
 | 
			
		||||
 | 
			
		||||
        while !(2000..=64000).contains(&port) {
 | 
			
		||||
@@ -93,10 +127,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 +138,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 })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user