GrammalecteClient/src/lib.rs

447 lines
14 KiB
Rust
Raw Permalink Normal View History

2022-12-19 11:30:33 +00:00
//! # 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:
//!
2023-05-23 12:41:19 +00:00
//! ```rust,ignore
2022-12-19 11:30:33 +00:00
//! 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:
2023-05-23 12:41:19 +00:00
//! ```rust,ignore
//! use grammalecte_client::GrammalecteClient;
//!
2022-12-19 11:30:33 +00:00
//! let res = GrammalecteClient::start_server()
//! .unwrap()
//! .suggest("bonjou")
//! .await
//! .unwrap();
//! assert!(res.suggestions.contains(&"bonjour".to_string()));
//! println!("RESULT = {:#?}", res);
//! ```
2022-12-19 11:20:44 +00:00
#[cfg(feature = "embedded-server")]
use crate::server::EmbeddedServer;
2022-12-18 16:49:19 +00:00
use std::collections::HashMap;
use std::error::Error;
2022-12-19 11:20:44 +00:00
#[cfg(feature = "embedded-server")]
2023-05-23 12:46:45 +00:00
pub mod server;
2022-12-19 11:20:44 +00:00
2022-12-19 10:19:22 +00:00
/// 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 dautocorrection > 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 lutilisation 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 sil manque une ponctuation finale au paragraphe (seulement pour les paragraphes constitués de plusieurs phrases).
Virgules,
/// Ponctuation finale [!]
///
/// Vérifie sil manque une ponctuation finale au paragraphe (seulement pour les paragraphes constitués de plusieurs phrases).
PonctuationFinale,
/// Traits dunion et soudures
///
/// Cherche les traits dunion 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 à lhomonymie (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 linfinitif et dautres 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 daujourdhui », « 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 lidentifiant de la règle de contrôle dans les messages derreur.
IdControlRule,
}
impl GramOpt {
2022-12-19 11:30:33 +00:00
/// Get the technical ID of the Grammalecte option
2022-12-19 10:19:22 +00:00
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",
}
}
}
2022-12-18 16:49:19 +00:00
/// 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<Paragraph>,
}
2022-12-19 11:30:33 +00:00
/// Check spell result of a given paragraph
2022-12-18 16:49:19 +00:00
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Paragraph {
#[serde(rename = "iParagraph")]
pub num: usize,
#[serde(rename = "lGrammarErrors")]
pub grammars: Vec<GrammarError>,
#[serde(rename = "lSpellingErrors")]
pub spelling: Vec<SpellingError>,
}
2022-12-19 11:30:33 +00:00
/// Single grammar error
2022-12-18 16:49:19 +00:00
#[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")]
2022-12-19 13:21:31 +00:00
pub rule_underline_color: Option<Vec<u8>>,
2022-12-18 16:49:19 +00:00
#[serde(rename = "sMessage")]
pub message: String,
#[serde(rename = "aSuggestions")]
pub suggestions: Vec<String>,
#[serde(rename = "URL")]
pub url: String,
}
2022-12-19 11:30:33 +00:00
/// Spelling error information
2022-12-18 16:49:19 +00:00
#[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,
}
2022-12-19 11:30:33 +00:00
/// Response to a suggestion request
2022-12-19 09:33:05 +00:00
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct SuggestResult {
2022-12-20 09:14:57 +00:00
/// Suggestions returned by Grammalecte
pub suggestions: Vec<String>,
2022-12-19 09:33:05 +00:00
}
2022-12-19 11:30:33 +00:00
/// The Grammalecte client itself
2022-12-18 16:49:19 +00:00
pub struct GrammalecteClient {
base_url: String,
2022-12-19 11:20:44 +00:00
#[cfg(feature = "embedded-server")]
_server: Option<EmbeddedServer>,
2022-12-18 16:49:19 +00:00
}
impl Default for GrammalecteClient {
fn default() -> Self {
Self {
base_url: "http://localhost:8080".to_string(),
2022-12-19 11:20:44 +00:00
#[cfg(feature = "embedded-server")]
_server: None,
2022-12-18 16:49:19 +00:00
}
}
}
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(),
2022-12-19 11:20:44 +00:00
#[cfg(feature = "embedded-server")]
_server: None,
2022-12-18 16:49:19 +00:00
}
}
2022-12-19 11:20:44 +00:00
/// 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<Self, Box<dyn Error>> {
let server = EmbeddedServer::start()?;
Ok(Self {
base_url: server.base_url(),
_server: Some(server),
})
}
2022-12-18 16:49:19 +00:00
/// Run spell check on text
pub async fn spell_check(&self, text: &str) -> Result<CheckResult, Box<dyn Error>> {
2022-12-19 13:21:31 +00:00
self.spell_check_with_options(text, &HashMap::new()).await
2022-12-19 10:19:22 +00:00
}
/// Run spell check with custom options
pub async fn spell_check_with_options(
&self,
text: &str,
2022-12-19 13:21:31 +00:00
options: &HashMap<GramOpt, bool>,
2022-12-19 10:19:22 +00:00
) -> Result<CheckResult, Box<dyn Error>> {
2022-12-18 16:49:19 +00:00
let url = format!("{}/gc_text/fr", self.base_url);
2022-12-19 09:33:05 +00:00
log::debug!("Will use URL {} for spell check", url);
2022-12-18 16:49:19 +00:00
2022-12-19 10:19:22 +00:00
let options = options
2023-05-23 12:41:19 +00:00
.iter()
2022-12-19 10:19:22 +00:00
.map(|t| (t.0.id(), t.1))
.collect::<HashMap<_, _>>();
let options = serde_json::to_string(&options)?;
2022-12-18 16:49:19 +00:00
let mut params = HashMap::new();
params.insert("text", text);
2022-12-19 10:19:22 +00:00
params.insert("options", &options);
2022-12-18 16:49:19 +00:00
let result = reqwest::Client::new()
.post(url)
.form(&params)
.send()
.await?
.json::<CheckResult>()
.await?;
Ok(result)
}
2022-12-19 09:33:05 +00:00
/// Ask for word suggestion
pub async fn suggest(&self, token: &str) -> Result<SuggestResult, Box<dyn 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()
.post(&url)
.form(&params)
.send()
.await?
.json()
.await?)
}
2022-12-18 16:49:19 +00:00
}
#[cfg(test)]
2022-12-19 11:20:44 +00:00
#[cfg(feature = "embedded-server")]
2022-12-18 16:49:19 +00:00
mod test {
2022-12-19 10:19:22 +00:00
use crate::{GramOpt, GrammalecteClient};
use std::collections::HashMap;
2022-12-18 16:49:19 +00:00
#[tokio::test]
async fn simple_correction() {
let _ = env_logger::builder().is_test(true).try_init();
let msg = "Les ange sont inssuportables!";
2022-12-19 11:20:44 +00:00
let res = GrammalecteClient::start_server()
.unwrap()
.spell_check(msg)
.await
.unwrap();
2022-12-18 16:49:19 +00:00
println!("RESULT = {:#?}", res);
}
2022-12-19 09:33:05 +00:00
2022-12-19 10:19:22 +00:00
#[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);
2022-12-19 11:20:44 +00:00
let res = GrammalecteClient::start_server()
.unwrap()
2022-12-19 13:21:31 +00:00
.spell_check_with_options(msg, &opts)
2022-12-19 10:19:22 +00:00
.await
.unwrap();
println!("RESULT = {:#?}", res);
assert!(res.paragraphs.is_empty());
}
2022-12-19 09:33:05 +00:00
#[tokio::test]
async fn simple_suggestion() {
let _ = env_logger::builder().is_test(true).try_init();
2022-12-19 11:20:44 +00:00
let res = GrammalecteClient::start_server()
.unwrap()
2022-12-19 09:33:05 +00:00
.suggest("bonjou")
.await
.unwrap();
assert!(res.suggestions.contains(&"bonjour".to_string()));
println!("RESULT = {:#?}", res);
}
2022-12-18 16:49:19 +00:00
}