Compare commits
No commits in common. "master" and "master" have entirely different histories.
1555
Cargo.lock
generated
1555
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
11
Cargo.toml
11
Cargo.toml
@ -1,12 +1,12 @@
|
||||
[package]
|
||||
name = "grammalecte_client"
|
||||
version = "0.1.5"
|
||||
version = "0.1.4"
|
||||
edition = "2021"
|
||||
authors = ["Pierre Hubert <pierre.git@communiquons.org>"]
|
||||
description = "Grammalecte HTTP client"
|
||||
license = "MIT"
|
||||
repository = "https://gitea.communiquons.org/pierre/GrammalecteClient"
|
||||
keywords = ["grammalecte", "spell-check", "spellcheck"]
|
||||
keywords = ["grammalecte", "spell-check"]
|
||||
readme = "README.md"
|
||||
categories = ["text-processing"]
|
||||
|
||||
@ -14,18 +14,17 @@ categories = ["text-processing"]
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1.0.96"
|
||||
reqwest = { version = "0.12.4", features = ["json"] }
|
||||
reqwest = { version = "0.11.18", features = ["json"] }
|
||||
serde = { version = "1.0.163", features = ["derive"] }
|
||||
log = "0.4.17"
|
||||
zip = { version = "2.1.3", optional = true }
|
||||
zip = { version = "0.6.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"]
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger = "0.11.3"
|
||||
env_logger = "0.10.0"
|
||||
tokio = { version = "1.28.1", features = ["full"] }
|
||||
|
@ -1,9 +0,0 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"packageRules": [
|
||||
{
|
||||
"matchUpdateTypes": ["major", "minor", "patch"],
|
||||
"automerge": true
|
||||
}
|
||||
]
|
||||
}
|
Binary file not shown.
49
src/lib.rs
49
src/lib.rs
@ -36,11 +36,10 @@
|
||||
//! 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;
|
||||
@ -303,28 +302,6 @@ 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,
|
||||
@ -358,7 +335,7 @@ impl GrammalecteClient {
|
||||
///
|
||||
/// Python 3.7 or higher must is required at runtime
|
||||
#[cfg(feature = "embedded-server")]
|
||||
pub fn start_server() -> Result<Self, Error> {
|
||||
pub fn start_server() -> Result<Self, Box<dyn Error>> {
|
||||
let server = EmbeddedServer::start()?;
|
||||
Ok(Self {
|
||||
base_url: server.base_url(),
|
||||
@ -367,7 +344,7 @@ impl GrammalecteClient {
|
||||
}
|
||||
|
||||
/// Run spell check on text
|
||||
pub async fn spell_check(&self, text: &str) -> Result<CheckResult, Error> {
|
||||
pub async fn spell_check(&self, text: &str) -> Result<CheckResult, Box<dyn Error>> {
|
||||
self.spell_check_with_options(text, &HashMap::new()).await
|
||||
}
|
||||
|
||||
@ -376,7 +353,7 @@ impl GrammalecteClient {
|
||||
&self,
|
||||
text: &str,
|
||||
options: &HashMap<GramOpt, bool>,
|
||||
) -> Result<CheckResult, Error> {
|
||||
) -> Result<CheckResult, Box<dyn Error>> {
|
||||
let url = format!("{}/gc_text/fr", self.base_url);
|
||||
log::debug!("Will use URL {} for spell check", url);
|
||||
|
||||
@ -384,7 +361,7 @@ impl GrammalecteClient {
|
||||
.iter()
|
||||
.map(|t| (t.0.id(), t.1))
|
||||
.collect::<HashMap<_, _>>();
|
||||
let options = serde_json::to_string(&options).map_err(Error::OptionJsonSerialization)?;
|
||||
let options = serde_json::to_string(&options)?;
|
||||
|
||||
let mut params = HashMap::new();
|
||||
params.insert("text", text);
|
||||
@ -394,32 +371,28 @@ impl GrammalecteClient {
|
||||
.post(url)
|
||||
.form(¶ms)
|
||||
.send()
|
||||
.await
|
||||
.map_err(Error::RequestSendCheckWithOptions)?
|
||||
.await?
|
||||
.json::<CheckResult>()
|
||||
.await
|
||||
.map_err(Error::CheckResultDeserialize)?;
|
||||
.await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Ask for word suggestion
|
||||
pub async fn suggest(&self, token: &str) -> Result<SuggestResult, Error> {
|
||||
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);
|
||||
|
||||
reqwest::Client::new()
|
||||
Ok(reqwest::Client::new()
|
||||
.post(&url)
|
||||
.form(¶ms)
|
||||
.send()
|
||||
.await
|
||||
.map_err(Error::RequestSendSuggest)?
|
||||
.await?
|
||||
.json()
|
||||
.await
|
||||
.map_err(Error::SuggestDeserialize)
|
||||
.await?)
|
||||
}
|
||||
}
|
||||
|
||||
|
119
src/server.rs
119
src/server.rs
@ -1,48 +1,9 @@
|
||||
use crate::server::utils::{get_free_port, wait_for_server};
|
||||
use crate::server::utils::{get_free_port, wait_for_port};
|
||||
use mktemp::Temp;
|
||||
use std::io::{self, Cursor, Read};
|
||||
use std::process::{Child, ExitStatus, Stdio};
|
||||
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("Server exit with `{status}`")]
|
||||
ServerExitWithStatus { status: ExitStatus },
|
||||
|
||||
#[error("Server exit with `{status}` :\n{msg}")]
|
||||
ServerExitWithError { status: ExitStatus, msg: String },
|
||||
|
||||
#[error("Error append during check grammalecte-server status")]
|
||||
ServerCheckStatus(#[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),
|
||||
}
|
||||
use std::error::Error;
|
||||
use std::io::{Cursor, Read};
|
||||
use std::process::{Child, Stdio};
|
||||
use zip::ZipArchive;
|
||||
|
||||
pub struct EmbeddedServer {
|
||||
_srv_dir: Temp,
|
||||
@ -52,31 +13,29 @@ pub struct EmbeddedServer {
|
||||
|
||||
impl EmbeddedServer {
|
||||
/// Start embedded Grammalecte server on a random free port
|
||||
pub fn start() -> Result<Self, Error> {
|
||||
Self::start_listen_on_port(get_free_port())
|
||||
pub fn start() -> Result<Self, Box<dyn 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, Error> {
|
||||
pub fn start_listen_on_port(port: u16) -> Result<Self, Box<dyn Error>> {
|
||||
log::info!("Will start server");
|
||||
// First, unpack server
|
||||
let dest = mktemp::Temp::new_dir().map_err(Error::CreateTempDir)?;
|
||||
let dest = mktemp::Temp::new_dir()?;
|
||||
let cursor = Cursor::new(include_bytes!("GrammalecteDist.zip"));
|
||||
let mut zip = ZipArchive::new(cursor).map_err(Error::ZipArchiveLoading)?;
|
||||
let mut zip = ZipArchive::new(cursor)?;
|
||||
for i in 0..zip.len() {
|
||||
let mut file = zip.by_index(i).map_err(Error::ZipFileIndex)?;
|
||||
let mut file = zip.by_index(i)?;
|
||||
if file.is_dir() {
|
||||
log::debug!("Create directory: {}", file.name());
|
||||
std::fs::create_dir_all(dest.join(file.name()))
|
||||
.map_err(Error::CreateDirectoryForZipFile)?;
|
||||
std::fs::create_dir_all(dest.join(file.name()))?;
|
||||
} else {
|
||||
log::debug!("Decompress file: {}", file.name());
|
||||
|
||||
let mut buff = Vec::with_capacity(file.size() as usize);
|
||||
file.read_to_end(&mut buff)
|
||||
.map_err(Error::ZipFileReadToEnd)?;
|
||||
file.read_to_end(&mut buff)?;
|
||||
|
||||
std::fs::write(dest.join(file.name()), buff).map_err(Error::WriteFile)?;
|
||||
std::fs::write(dest.join(file.name()), buff)?;
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,16 +48,15 @@ impl EmbeddedServer {
|
||||
log::info!("Will execute file {}", server_file);
|
||||
|
||||
// Start server
|
||||
let mut child = std::process::Command::new("/usr/bin/python3")
|
||||
let child = std::process::Command::new("/usr/bin/python3")
|
||||
.arg(server_file)
|
||||
.arg("-p")
|
||||
.arg(port.to_string())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(Error::StartServerProcess)?;
|
||||
.stderr(Stdio::null())
|
||||
.spawn()?;
|
||||
|
||||
wait_for_server(&mut child, port)?;
|
||||
wait_for_port(port)?;
|
||||
|
||||
Ok(Self {
|
||||
_srv_dir: dest,
|
||||
@ -120,14 +78,11 @@ impl Drop for EmbeddedServer {
|
||||
}
|
||||
|
||||
mod utils {
|
||||
use super::Error;
|
||||
use std::fmt::Write;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::process::Child;
|
||||
use std::io::ErrorKind;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Get a free port
|
||||
pub fn get_free_port() -> u16 {
|
||||
pub fn get_free_port() -> std::io::Result<u16> {
|
||||
let mut port = 0;
|
||||
|
||||
while !(2000..=64000).contains(&port) {
|
||||
@ -138,42 +93,20 @@ mod utils {
|
||||
port += 1;
|
||||
}
|
||||
|
||||
port
|
||||
Ok(port)
|
||||
}
|
||||
|
||||
pub fn wait_for_server(child: &mut Child, port: u16) -> Result<(), Error> {
|
||||
pub fn wait_for_port(port: u16) -> std::io::Result<()> {
|
||||
for _ in 0..50 {
|
||||
check_server(child)?;
|
||||
if port_scanner::scan_port(port) {
|
||||
return Ok(());
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
|
||||
Err(Error::WaitPortOpen { port })
|
||||
}
|
||||
|
||||
fn check_server(child: &mut Child) -> Result<(), Error> {
|
||||
match child.try_wait().map_err(Error::ServerCheckStatus)? {
|
||||
None => Ok(()), // Continue
|
||||
Some(status) => {
|
||||
if let Some(err) = child.stderr.take() {
|
||||
let mut msg = format!("grammalecte-server exit with `{status}`");
|
||||
writeln!(&mut msg, " :").unwrap();
|
||||
let err = BufReader::new(err);
|
||||
err.lines().for_each(|line| match line {
|
||||
Ok(line) => {
|
||||
writeln!(&mut msg, "\t{}", line).unwrap();
|
||||
}
|
||||
Err(err) => {
|
||||
writeln!(&mut msg, "__{err:?}").unwrap();
|
||||
}
|
||||
});
|
||||
Err(Error::ServerExitWithError { status, msg })
|
||||
} else {
|
||||
Err(Error::ServerExitWithStatus { status })
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(std::io::Error::new(
|
||||
ErrorKind::Other,
|
||||
format!("Port {} did not open in time!", port),
|
||||
))?
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user