//! # Grammalecte Rust Client //! //! This crate is a Rust client to the Grammalecte server API. //! //! Grammalecte is an Open Source software that allows to do //! french spell-checking. //! //! ## Integrated server //! The optional feature `embedded-server` allows you to spin up an //! temporary web server that will act as Grammalecte backend, instead //! of targetting an existing instance: //! //! ```rust,ignore //! use grammalecte_client::GrammalecteClient; //! //! let msg = "Les ange sont inssuportables!"; //! let res = GrammalecteClient::start_server() //! .unwrap() //! .spell_check(msg) //! .await //! .unwrap(); //! println!("RESULT = {:#?}", res); //! ``` //! //! ## Suggestion //! You can also ask Grammalecte to give you valid alternatives words: //! ```rust,ignore //! use grammalecte_client::GrammalecteClient; //! //! let res = GrammalecteClient::start_server() //! .unwrap() //! .suggest("bonjou") //! .await //! .unwrap(); //! assert!(res.suggestions.contains(&"bonjour".to_string())); //! println!("RESULT = {:#?}", res); //! ``` #[cfg(feature = "embedded-server")] use crate::server::EmbeddedServer; use std::collections::HashMap; use std::error::Error; #[cfg(feature = "embedded-server")] pub mod server; /// Spell check options #[derive(Hash, Debug, Eq, PartialEq)] pub enum GramOpt { /// Signes typographiques SignesTypographiques, /// Apostrophes typographiques /// /// Correction des apostrophes droites. Automatisme possible dans le menu Outils > Options d’autocorrection > Options linguistiques > Guillemets simples > Remplacer (à cocher) ApostropheTypographique, /// Ecriture épicène /// /// Normalisation de l’écriture épicène avec points médians. EcritureEpicene, /// Espaces surnuméraires /// /// Signale les espaces inutiles entre les mots, en début et en fin de ligne. EspacesSurnumeraires, /// Tabulations surnuméraires /// /// Signale les tabulations inutiles en début et en fin de ligne. TabulationsSurnumeraires, /// Espaces insécables /// /// Vérifie les espaces insécables avec les ponctuations « ! ? : ; » (à désactiver si vous utilisez une police Graphite) EspacesInsecables, /// Majuscules /// /// Vérifie l’utilisation des majuscules et des minuscules (par exemple, « la raison d’État », « les Européens »). Majuscules, /// Majuscules pour ministères /// /// Majuscules pour les intitulés des ministères. MajusuculesMinisteres, /// Virgules /// /// Vérifie s’il manque une ponctuation finale au paragraphe (seulement pour les paragraphes constitués de plusieurs phrases). Virgules, /// Ponctuation finale [!] /// /// Vérifie s’il manque une ponctuation finale au paragraphe (seulement pour les paragraphes constitués de plusieurs phrases). PonctuationFinale, /// Traits d’union et soudures /// /// Cherche les traits d’union manquants ou inutiles. TraitsUnionEtSoudures, /// Nombres /// /// Espaces insécables avant unités de mesure Nombres, /// Espaces insécables avant unités de mesure EspaceInsecableAvantUniteDeMesure, /// Normes françaises NormesFrancaises, /// Signaler ligatures typographiques /// /// Ligatures de fi, fl, ff, ffi, ffl, ft, st. LigaturesTypographiques, /// Apostrophe manquante après lettres isolées [!] /// /// Apostrophe manquante après les lettres l d s n c j m t ç. Cette option sert surtout à repérer les défauts de numérisation des textes et est déconseillée pour les textes scientifiques. ApostropheManquanteApresLettreIsolee, /// Chimie /// /// Typographie des composés chimiques (H₂O, CO₂, etc.). Chimie, /// Erreurs de numérisation (OCR) /// /// Erreurs de reconnaissance optique des caractères. Beaucoup de faux positifs. ErreurNumerisation, /// Noms et adjectifs Gramm, /// Confusions et faux-amis /// /// Cherche des erreurs souvent dues à l’homonymie (par exemple, les confusions entre « faîte » et « faite »). ConfusionFauxAmis, /// Locutions /// /// Écriture des locutions usuelles. Locutions, /// Accords (genre et nombre) /// /// Accords des noms et des adjectifs. AccordsGenreEtNombre, /// Verbes Verbes, /// Conjugaisons /// /// Accord des verbes avec leur sujet. Conjugaisons, /// Infinitif /// /// Confusion entre l’infinitif et d’autres formes. Infinitif, /// Impératif /// /// Vérifie notamment la deuxième personne du singulier (par exemple, les erreurs : « vas… », « prend… », « manges… »). Imperatif, /// Interrogatif /// /// Vérifie les formes interrogatives et suggère de lier les pronoms personnels avec les verbes. Interrogatif, /// Participes passés, adjectifs ParticipePassesEtAdjectifs, /// Modes verbaux ModesVerbaux, /// Style Style, /// Populaire /// /// Souligne un langage courant considéré comme erroné, comme « malgré que ». Populaire, /// Pléonasmes /// /// Repère des redondances sémantiques, comme « au jour d’aujourd’hui », « monter en haut », etc. Pleonasmes, /// Élisions et euphonies /// /// Signale les élisions incorrectes et les tournures dysphoniques. ElisisonsEtEuphonies, /// Adverge de négation [!} /// /// Ne … pas, ne … jamais, etc. AdverbesNegation, /// Répétitions dans le paragraphe [!] /// /// Sont exclus les mots grammaticaux, ceux commençant par une majuscule, ainsi que “être” et “avoir”. RepetitionsDansParagraphe, /// Répétitions dans la phrase [!] /// /// Sont exclus les mots grammaticaux, ainsi que “être” et “avoir”. RepetitionDansPhrase, /// Divers Misc, /// Mots composés [!] MotsComposes, /// Validation des dates Date, /// Debugagge Debug, /// Affiche l’identifiant de la règle de contrôle dans les messages d’erreur. IdControlRule, } impl GramOpt { /// Get the technical ID of the Grammalecte option pub fn id(&self) -> &'static str { match self { GramOpt::SignesTypographiques => "typo", GramOpt::ApostropheTypographique => "apos", GramOpt::EcritureEpicene => "eepi", GramOpt::EspacesSurnumeraires => "esp", GramOpt::TabulationsSurnumeraires => "tab", GramOpt::EspacesInsecables => "nbsp", GramOpt::Majuscules => "maj", GramOpt::MajusuculesMinisteres => "minis", GramOpt::Virgules => "virg", GramOpt::PonctuationFinale => "poncfin", GramOpt::TraitsUnionEtSoudures => "tu", GramOpt::Nombres => "num", GramOpt::EspaceInsecableAvantUniteDeMesure => "unit", GramOpt::NormesFrancaises => "nf", GramOpt::LigaturesTypographiques => "liga", GramOpt::ApostropheManquanteApresLettreIsolee => "mapos", GramOpt::Chimie => "chim", GramOpt::ErreurNumerisation => "ocr", GramOpt::Gramm => "gramm", GramOpt::ConfusionFauxAmis => "conf", GramOpt::Locutions => "loc", GramOpt::AccordsGenreEtNombre => "gn", GramOpt::Verbes => "verbs", GramOpt::Conjugaisons => "conj", GramOpt::Infinitif => "infi", GramOpt::Imperatif => "imp", GramOpt::Interrogatif => "inte", GramOpt::ParticipePassesEtAdjectifs => "ppas", GramOpt::ModesVerbaux => "vmode", GramOpt::Style => "style", GramOpt::Populaire => "bs", GramOpt::Pleonasmes => "pleo", GramOpt::ElisisonsEtEuphonies => "eleu", GramOpt::AdverbesNegation => "neg", GramOpt::RepetitionsDansParagraphe => "redon1", GramOpt::RepetitionDansPhrase => "redon2", GramOpt::Misc => "misc", GramOpt::MotsComposes => "mc", GramOpt::Date => "date", GramOpt::Debug => "debug", GramOpt::IdControlRule => "idrule", } } } /// Check spelling result #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct CheckResult { pub program: String, pub version: String, pub lang: String, pub error: String, #[serde(rename = "data")] pub paragraphs: Vec, } /// Check spell result of a given paragraph #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Paragraph { #[serde(rename = "iParagraph")] pub num: usize, #[serde(rename = "lGrammarErrors")] pub grammars: Vec, #[serde(rename = "lSpellingErrors")] pub spelling: Vec, } /// Single grammar error #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct GrammarError { #[serde(rename = "nStart")] pub offset_start: usize, #[serde(rename = "nEnd")] pub offset_end: usize, #[serde(rename = "sLineId")] pub rule_line_id: String, #[serde(rename = "sRuleId")] pub rule_id: String, #[serde(rename = "sType")] pub rule_type: String, #[serde(rename = "aColor")] pub rule_underline_color: Option>, #[serde(rename = "sMessage")] pub message: String, #[serde(rename = "aSuggestions")] pub suggestions: Vec, #[serde(rename = "URL")] pub url: String, } /// Spelling error information #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct SpellingError { pub i: usize, #[serde(rename = "nStart")] pub offset_start: usize, #[serde(rename = "nEnd")] pub offset_end: usize, #[serde(rename = "sValue")] pub bad_word: String, #[serde(rename = "sType")] pub error_type: String, } /// Response to a suggestion request #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct SuggestResult { /// Suggestions returned by Grammalecte pub suggestions: Vec, } /// The Grammalecte client itself pub struct GrammalecteClient { base_url: String, #[cfg(feature = "embedded-server")] _server: Option, } impl Default for GrammalecteClient { fn default() -> Self { Self { base_url: "http://localhost:8080".to_string(), #[cfg(feature = "embedded-server")] _server: None, } } } impl GrammalecteClient { /// Construct a new Grammalecte client, with a custom server URL pub fn new(base_url: &str) -> Self { Self { base_url: base_url.to_string(), #[cfg(feature = "embedded-server")] _server: None, } } /// Construct a new Grammalecte client, spinning up an associated /// temporary web server. /// /// Python 3.7 or higher must is required at runtime #[cfg(feature = "embedded-server")] pub fn start_server() -> Result> { let server = EmbeddedServer::start()?; Ok(Self { base_url: server.base_url(), _server: Some(server), }) } /// Run spell check on text pub async fn spell_check(&self, text: &str) -> Result> { self.spell_check_with_options(text, &HashMap::new()).await } /// Run spell check with custom options pub async fn spell_check_with_options( &self, text: &str, options: &HashMap, ) -> Result> { let url = format!("{}/gc_text/fr", self.base_url); log::debug!("Will use URL {} for spell check", url); let options = options .iter() .map(|t| (t.0.id(), t.1)) .collect::>(); let options = serde_json::to_string(&options)?; let mut params = HashMap::new(); params.insert("text", text); params.insert("options", &options); let result = reqwest::Client::new() .post(url) .form(¶ms) .send() .await? .json::() .await?; Ok(result) } /// Ask for word suggestion 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() .post(&url) .form(¶ms) .send() .await? .json() .await?) } } #[cfg(test)] #[cfg(feature = "embedded-server")] mod test { use crate::{GramOpt, GrammalecteClient}; use std::collections::HashMap; #[tokio::test] async fn simple_correction() { let _ = env_logger::builder().is_test(true).try_init(); let msg = "Les ange sont inssuportables!"; let res = GrammalecteClient::start_server() .unwrap() .spell_check(msg) .await .unwrap(); println!("RESULT = {:#?}", res); } #[tokio::test] async fn customize_options() { let _ = env_logger::builder().is_test(true).try_init(); let msg = "Bonjour !"; let mut opts = HashMap::new(); opts.insert(GramOpt::EspacesInsecables, false); let res = GrammalecteClient::start_server() .unwrap() .spell_check_with_options(msg, &opts) .await .unwrap(); println!("RESULT = {:#?}", res); assert!(res.paragraphs.is_empty()); } #[tokio::test] async fn simple_suggestion() { let _ = env_logger::builder().is_test(true).try_init(); let res = GrammalecteClient::start_server() .unwrap() .suggest("bonjou") .await .unwrap(); assert!(res.suggestions.contains(&"bonjour".to_string())); println!("RESULT = {:#?}", res); } }