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]
|
[package]
|
||||||
name = "grammalecte_client"
|
name = "grammalecte_client"
|
||||||
version = "0.1.5"
|
version = "0.1.4"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Pierre Hubert <pierre.git@communiquons.org>"]
|
authors = ["Pierre Hubert <pierre.git@communiquons.org>"]
|
||||||
description = "Grammalecte HTTP client"
|
description = "Grammalecte HTTP client"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://gitea.communiquons.org/pierre/GrammalecteClient"
|
repository = "https://gitea.communiquons.org/pierre/GrammalecteClient"
|
||||||
keywords = ["grammalecte", "spell-check", "spellcheck"]
|
keywords = ["grammalecte", "spell-check"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
categories = ["text-processing"]
|
categories = ["text-processing"]
|
||||||
|
|
||||||
@ -14,18 +14,17 @@ categories = ["text-processing"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde_json = "1.0.96"
|
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"] }
|
serde = { version = "1.0.163", features = ["derive"] }
|
||||||
log = "0.4.17"
|
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 }
|
mktemp = { version = "0.5.0", optional = true }
|
||||||
rand = { version = "0.8.5", optional = true }
|
rand = { version = "0.8.5", optional = true }
|
||||||
port_scanner = {version = "0.1.5", optional = true}
|
port_scanner = {version = "0.1.5", optional = true}
|
||||||
thiserror = "1.0.61"
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
embedded-server = ["zip", "mktemp", "rand", "port_scanner"]
|
embedded-server = ["zip", "mktemp", "rand", "port_scanner"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
env_logger = "0.11.3"
|
env_logger = "0.10.0"
|
||||||
tokio = { version = "1.28.1", features = ["full"] }
|
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);
|
//! println!("RESULT = {:#?}", res);
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[cfg(feature = "embedded-server")]
|
#[cfg(feature = "embedded-server")]
|
||||||
use crate::server::EmbeddedServer;
|
use crate::server::EmbeddedServer;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::error::Error;
|
||||||
|
|
||||||
#[cfg(feature = "embedded-server")]
|
#[cfg(feature = "embedded-server")]
|
||||||
pub mod server;
|
pub mod server;
|
||||||
@ -303,28 +302,6 @@ pub struct SuggestResult {
|
|||||||
pub suggestions: Vec<String>,
|
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
|
/// The Grammalecte client itself
|
||||||
pub struct GrammalecteClient {
|
pub struct GrammalecteClient {
|
||||||
base_url: String,
|
base_url: String,
|
||||||
@ -358,7 +335,7 @@ impl GrammalecteClient {
|
|||||||
///
|
///
|
||||||
/// Python 3.7 or higher must is required at runtime
|
/// Python 3.7 or higher must is required at runtime
|
||||||
#[cfg(feature = "embedded-server")]
|
#[cfg(feature = "embedded-server")]
|
||||||
pub fn start_server() -> Result<Self, Error> {
|
pub fn start_server() -> Result<Self, Box<dyn Error>> {
|
||||||
let server = EmbeddedServer::start()?;
|
let server = EmbeddedServer::start()?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
base_url: server.base_url(),
|
base_url: server.base_url(),
|
||||||
@ -367,7 +344,7 @@ impl GrammalecteClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Run spell check on text
|
/// 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
|
self.spell_check_with_options(text, &HashMap::new()).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -376,7 +353,7 @@ impl GrammalecteClient {
|
|||||||
&self,
|
&self,
|
||||||
text: &str,
|
text: &str,
|
||||||
options: &HashMap<GramOpt, bool>,
|
options: &HashMap<GramOpt, bool>,
|
||||||
) -> Result<CheckResult, Error> {
|
) -> Result<CheckResult, Box<dyn Error>> {
|
||||||
let url = format!("{}/gc_text/fr", self.base_url);
|
let url = format!("{}/gc_text/fr", self.base_url);
|
||||||
log::debug!("Will use URL {} for spell check", url);
|
log::debug!("Will use URL {} for spell check", url);
|
||||||
|
|
||||||
@ -384,7 +361,7 @@ impl GrammalecteClient {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|t| (t.0.id(), t.1))
|
.map(|t| (t.0.id(), t.1))
|
||||||
.collect::<HashMap<_, _>>();
|
.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();
|
let mut params = HashMap::new();
|
||||||
params.insert("text", text);
|
params.insert("text", text);
|
||||||
@ -394,32 +371,28 @@ impl GrammalecteClient {
|
|||||||
.post(url)
|
.post(url)
|
||||||
.form(¶ms)
|
.form(¶ms)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await?
|
||||||
.map_err(Error::RequestSendCheckWithOptions)?
|
|
||||||
.json::<CheckResult>()
|
.json::<CheckResult>()
|
||||||
.await
|
.await?;
|
||||||
.map_err(Error::CheckResultDeserialize)?;
|
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ask for word suggestion
|
/// 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);
|
let url = format!("{}/suggest/fr", self.base_url);
|
||||||
log::debug!("Will use URL {} for word suggestion", url);
|
log::debug!("Will use URL {} for word suggestion", url);
|
||||||
|
|
||||||
let mut params = HashMap::new();
|
let mut params = HashMap::new();
|
||||||
params.insert("token", token);
|
params.insert("token", token);
|
||||||
|
|
||||||
reqwest::Client::new()
|
Ok(reqwest::Client::new()
|
||||||
.post(&url)
|
.post(&url)
|
||||||
.form(¶ms)
|
.form(¶ms)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await?
|
||||||
.map_err(Error::RequestSendSuggest)?
|
|
||||||
.json()
|
.json()
|
||||||
.await
|
.await?)
|
||||||
.map_err(Error::SuggestDeserialize)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 mktemp::Temp;
|
||||||
use std::io::{self, Cursor, Read};
|
use std::error::Error;
|
||||||
use std::process::{Child, ExitStatus, Stdio};
|
use std::io::{Cursor, Read};
|
||||||
use thiserror::Error;
|
use std::process::{Child, Stdio};
|
||||||
use zip::{result::ZipError, ZipArchive};
|
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("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),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct EmbeddedServer {
|
pub struct EmbeddedServer {
|
||||||
_srv_dir: Temp,
|
_srv_dir: Temp,
|
||||||
@ -52,31 +13,29 @@ pub struct EmbeddedServer {
|
|||||||
|
|
||||||
impl EmbeddedServer {
|
impl EmbeddedServer {
|
||||||
/// Start embedded Grammalecte server on a random free port
|
/// Start embedded Grammalecte server on a random free port
|
||||||
pub fn start() -> Result<Self, Error> {
|
pub fn start() -> Result<Self, Box<dyn Error>> {
|
||||||
Self::start_listen_on_port(get_free_port())
|
Self::start_listen_on_port(get_free_port()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start embedded Grammalecte server on a given 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");
|
log::info!("Will start server");
|
||||||
// First, unpack 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 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() {
|
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() {
|
if file.is_dir() {
|
||||||
log::debug!("Create directory: {}", file.name());
|
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 {
|
} else {
|
||||||
log::debug!("Decompress file: {}", file.name());
|
log::debug!("Decompress file: {}", file.name());
|
||||||
|
|
||||||
let mut buff = Vec::with_capacity(file.size() as usize);
|
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).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);
|
log::info!("Will execute file {}", server_file);
|
||||||
|
|
||||||
// Start server
|
// 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(server_file)
|
||||||
.arg("-p")
|
.arg("-p")
|
||||||
.arg(port.to_string())
|
.arg(port.to_string())
|
||||||
.stdout(Stdio::null())
|
.stdout(Stdio::null())
|
||||||
.stderr(Stdio::piped())
|
.stderr(Stdio::null())
|
||||||
.spawn()
|
.spawn()?;
|
||||||
.map_err(Error::StartServerProcess)?;
|
|
||||||
|
|
||||||
wait_for_server(&mut child, port)?;
|
wait_for_port(port)?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
_srv_dir: dest,
|
_srv_dir: dest,
|
||||||
@ -120,14 +78,11 @@ impl Drop for EmbeddedServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mod utils {
|
mod utils {
|
||||||
use super::Error;
|
use std::io::ErrorKind;
|
||||||
use std::fmt::Write;
|
|
||||||
use std::io::{BufRead, BufReader};
|
|
||||||
use std::process::Child;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
/// Get a free port
|
/// Get a free port
|
||||||
pub fn get_free_port() -> u16 {
|
pub fn get_free_port() -> std::io::Result<u16> {
|
||||||
let mut port = 0;
|
let mut port = 0;
|
||||||
|
|
||||||
while !(2000..=64000).contains(&port) {
|
while !(2000..=64000).contains(&port) {
|
||||||
@ -138,42 +93,20 @@ mod utils {
|
|||||||
port += 1;
|
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 {
|
for _ in 0..50 {
|
||||||
check_server(child)?;
|
|
||||||
if port_scanner::scan_port(port) {
|
if port_scanner::scan_port(port) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
std::thread::sleep(Duration::from_millis(100));
|
std::thread::sleep(Duration::from_millis(100));
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(Error::WaitPortOpen { port })
|
Err(std::io::Error::new(
|
||||||
}
|
ErrorKind::Other,
|
||||||
|
format!("Port {} did not open in time!", 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 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user