1#[cfg(feature = "multithreaded")]
4use crate::api::check;
5use crate::{
6 api::{
7 check::{Request, Response},
8 languages, words,
9 },
10 error::{Error, Result},
11};
12#[cfg(feature = "cli")]
13use clap::Args;
14#[cfg(feature = "multithreaded")]
15use lifetime::IntoStatic;
16use reqwest::{
17 header::{HeaderValue, ACCEPT},
18 Client,
19};
20use serde::{Deserialize, Serialize};
21use serde_json::Value;
22use std::{io, path::PathBuf, time::Instant};
23
24pub fn parse_port(v: &str) -> Result<String> {
41 if v.is_empty() || (v.len() == 4 && v.chars().all(char::is_numeric)) {
42 return Ok(v.to_string());
43 }
44 Err(Error::InvalidValue(
45 "The value should be a 4 characters long string with digits only".to_string(),
46 ))
47}
48
49#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
50#[serde(rename_all = "camelCase")]
51#[non_exhaustive]
52pub struct ConfigFile {
55 pub max_text_length: Option<isize>,
57 pub max_text_hard_length: Option<isize>,
60 pub secret_token_key: Option<isize>,
63 pub max_check_time_millis: Option<isize>,
65 pub max_errors_per_word_rate: Option<isize>,
68 pub max_spelling_suggestions: Option<isize>,
71 pub max_check_threads: Option<isize>,
73 pub cache_size: Option<isize>,
75 pub cache_ttl_seconds: Option<isize>,
78 pub request_limit: Option<isize>,
80 pub request_limit_in_bytes: Option<isize>,
83 pub timeout_request_limit: Option<isize>,
85 pub request_limit_period_in_seconds: Option<isize>,
88 pub language_model: Option<PathBuf>,
92 pub word2vec_model: Option<PathBuf>,
94 pub fasttext_model: Option<PathBuf>,
97 pub fasttext_binary: Option<PathBuf>,
100 pub max_work_queue_size: Option<isize>,
102 pub rules_file: Option<PathBuf>,
105 pub warm_up: Option<bool>,
108 pub blocked_referrers: Option<Vec<String>>,
111 pub premium_only: Option<bool>,
113 pub disable_rule_ids: Option<Vec<String>>,
116 pub pipeline_caching: Option<bool>,
119 pub max_pipeline_pool_size: Option<isize>,
121 pub pipeline_expire_time_in_seconds: Option<isize>,
123 pub pipeline_prewarming: Option<bool>,
126 pub spellcheck_only: Option<std::collections::HashMap<String, String>>,
139}
140
141impl ConfigFile {
142 pub fn write_to<T: io::Write>(&self, w: &mut T) -> io::Result<()> {
144 let json = serde_json::to_value(self.clone()).unwrap();
145 let m = json.as_object().unwrap();
146 for (key, value) in m.iter() {
147 match value {
148 Value::Bool(b) => writeln!(w, "{key}={b}")?,
149 Value::Number(n) => writeln!(w, "{key}={n}")?,
150 Value::String(s) => writeln!(w, "{key}=\"{s}\"")?,
151 Value::Array(a) => {
152 writeln!(
153 w,
154 "{}=\"{}\"",
155 key,
156 a.iter()
157 .map(std::string::ToString::to_string)
158 .collect::<Vec<String>>()
159 .join(",")
160 )?
161 },
162 Value::Object(o) => {
163 for (key, value) in o.iter() {
164 writeln!(w, "{key}=\"{value}\"")?
165 }
166 },
167 Value::Null => writeln!(w, "# {key}=")?,
168 }
169 }
170 Ok(())
171 }
172}
173
174impl Default for ConfigFile {
175 fn default() -> Self {
176 Self {
177 max_text_length: None,
178 max_text_hard_length: None,
179 secret_token_key: None,
180 max_check_time_millis: None,
181 max_errors_per_word_rate: None,
182 max_spelling_suggestions: None,
183 max_check_threads: None,
184 cache_size: Some(0),
185 cache_ttl_seconds: Some(300),
186 request_limit: None,
187 request_limit_in_bytes: None,
188 timeout_request_limit: None,
189 request_limit_period_in_seconds: None,
190 language_model: None,
191 word2vec_model: None,
192 fasttext_model: None,
193 fasttext_binary: None,
194 max_work_queue_size: None,
195 rules_file: None,
196 warm_up: None,
197 blocked_referrers: None,
198 premium_only: None,
199 disable_rule_ids: None,
200 pipeline_caching: None,
201 max_pipeline_pool_size: None,
202 pipeline_expire_time_in_seconds: None,
203 pipeline_prewarming: None,
204 spellcheck_only: None,
205 }
206 }
207}
208
209#[cfg_attr(feature = "cli", derive(Args))]
212#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
213#[non_exhaustive]
214pub struct ServerParameters {
215 #[cfg_attr(feature = "cli", clap(long))]
218 config: Option<PathBuf>,
219 #[cfg_attr(feature = "cli", clap(short = 'p', long, name = "PRT", default_value = "8081", value_parser = parse_port))]
221 port: String,
222 #[cfg_attr(feature = "cli", clap(long))]
225 public: bool,
226 #[cfg_attr(feature = "cli", clap(long, name = "ORIGIN"))]
230 #[allow(rustdoc::bare_urls)]
231 allow_origin: Option<String>,
232 #[cfg_attr(feature = "cli", clap(short = 'v', long))]
234 verbose: bool,
235 #[cfg_attr(feature = "cli", clap(long))]
239 #[serde(rename = "languageModel")]
240 language_model: Option<PathBuf>,
241 #[cfg_attr(feature = "cli", clap(long))]
243 #[serde(rename = "word2vecModel")]
244 word2vec_model: Option<PathBuf>,
245 #[cfg_attr(feature = "cli", clap(long))]
248 #[serde(rename = "premiumAlways")]
249 premium_always: bool,
250}
251
252impl Default for ServerParameters {
253 fn default() -> Self {
254 Self {
255 config: None,
256 port: "8081".to_string(),
257 public: false,
258 allow_origin: None,
259 verbose: false,
260 language_model: None,
261 word2vec_model: None,
262 premium_always: false,
263 }
264 }
265}
266
267#[cfg_attr(feature = "cli", derive(Args))]
275#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
276pub struct ServerCli {
277 #[cfg_attr(
279 feature = "cli",
280 clap(
281 long,
282 default_value = "https://api.languagetoolplus.com",
283 env = "LANGUAGETOOL_HOSTNAME",
284 )
285 )]
286 pub hostname: String,
287 #[cfg_attr(feature = "cli", clap(short = 'p', long, name = "PRT", default_value = "", value_parser = parse_port, env = "LANGUAGETOOL_PORT"))]
290 pub port: String,
291}
292
293impl Default for ServerCli {
294 fn default() -> Self {
295 Self {
296 hostname: "https://api.languagetoolplus.com".to_string(),
297 port: "".to_string(),
298 }
299 }
300}
301
302impl ServerCli {
303 pub fn from_env() -> Result<Self> {
309 let hostname = std::env::var("LANGUAGETOOL_HOSTNAME")?;
310 let port = std::env::var("LANGUAGETOOL_PORT")?;
311
312 Ok(Self { hostname, port })
313 }
314
315 #[must_use]
319 pub fn from_env_or_default() -> Self {
320 ServerCli::from_env().unwrap_or_default()
321 }
322}
323
324#[derive(Clone, Debug)]
326pub struct ServerClient {
327 pub api: String,
329 pub client: Client,
331 max_suggestions: isize,
332}
333
334impl From<ServerCli> for ServerClient {
335 #[inline]
336 fn from(cli: ServerCli) -> Self {
337 Self::new(cli.hostname.as_str(), cli.port.as_str())
338 }
339}
340
341impl ServerClient {
342 #[must_use]
348 pub fn new(hostname: &str, port: &str) -> Self {
349 let api = if port.is_empty() {
350 format!("{hostname}/v2")
351 } else {
352 format!("{hostname}:{port}/v2")
353 };
354 let client = Client::new();
355 Self {
356 api,
357 client,
358 max_suggestions: -1,
359 }
360 }
361
362 #[must_use]
365 pub fn with_max_suggestions(mut self, max_suggestions: isize) -> Self {
366 self.max_suggestions = max_suggestions;
367 self
368 }
369
370 #[must_use]
372 pub fn from_cli(cli: ServerCli) -> Self {
373 cli.into()
374 }
375
376 pub async fn check(&self, request: &Request<'_>) -> Result<Response> {
378 let resp = self
379 .client
380 .post(format!("{0}/check", self.api))
381 .header(ACCEPT, HeaderValue::from_static("application/json"))
382 .form(request)
383 .send()
384 .await
385 .map_err(Error::Reqwest)?;
386
387 match resp.error_for_status_ref() {
388 Ok(_) => {
389 resp.json::<Response>()
390 .await
391 .map_err(Into::into)
392 .map(|mut resp| {
393 if self.max_suggestions > 0 {
394 let max = self.max_suggestions as usize;
395 resp.matches.iter_mut().for_each(|m| {
396 let len = m.replacements.len();
397 if max < len {
398 m.replacements[max] =
399 format!("... ({} not shown)", len - max).into();
400 m.replacements.truncate(max + 1);
401 }
402 });
403 }
404 resp
405 })
406 },
407 Err(_) => Err(Error::InvalidRequest(resp.text().await?)),
408 }
409 }
410
411 #[cfg(feature = "multithreaded")]
418 pub async fn check_multiple_and_join<'source>(
419 &self,
420 requests: Vec<Request<'source>>,
421 ) -> Result<check::ResponseWithContext<'source>> {
422 use std::borrow::Cow;
423
424 if requests.is_empty() {
425 return Err(Error::InvalidRequest(
426 "no request; cannot join zero request".to_string(),
427 ));
428 }
429
430 let tasks = requests
431 .into_iter()
432 .map(|r| r.into_static())
433 .map(|request| {
434 let server_client = self.clone();
435
436 tokio::spawn(async move {
437 let response = server_client.check(&request).await?;
438 let text = request.text.ok_or_else(|| {
439 Error::InvalidRequest(
440 "missing text field; cannot join requests with data annotations"
441 .to_string(),
442 )
443 })?;
444 Result::<(Cow<'static, str>, Response)>::Ok((text, response))
445 })
446 });
447
448 let mut response_with_context: Option<check::ResponseWithContext> = None;
449
450 for task in tasks {
451 let (text, response) = task.await.unwrap()?;
452
453 response_with_context = Some(match response_with_context {
454 Some(resp) => resp.append(check::ResponseWithContext::new(text, response)),
455 None => check::ResponseWithContext::new(text, response),
456 })
457 }
458
459 Ok(response_with_context.unwrap())
460 }
461
462 #[cfg(feature = "multithreaded")]
470 pub async fn check_multiple_and_join_without_context(
471 &self,
472 requests: Vec<Request<'_>>,
473 ) -> Result<check::Response> {
474 let mut response: Option<check::Response> = None;
475
476 let tasks = requests
477 .into_iter()
478 .map(|r| r.into_static())
479 .map(|request| {
480 let server_client = self.clone();
481
482 tokio::spawn(async move {
483 let response = server_client.check(&request).await?;
484 Result::<Response>::Ok(response)
485 })
486 });
487
488 for task in tasks {
490 let resp = task.await.unwrap()?;
491
492 response = Some(match response {
493 Some(r) => r.append(resp),
494 None => resp,
495 })
496 }
497
498 Ok(response.unwrap())
499 }
500
501 #[cfg(feature = "annotate")]
504 pub async fn annotate_check(
505 &self,
506 request: &Request<'_>,
507 origin: Option<&str>,
508 color: bool,
509 ) -> Result<String> {
510 let text = request.get_text();
511 let resp = self.check(request).await?;
512
513 Ok(resp.annotate(text.as_ref(), origin, color))
514 }
515
516 pub async fn languages(&self) -> Result<languages::Response> {
518 let resp = self
519 .client
520 .get(format!("{}/languages", self.api))
521 .send()
522 .await
523 .map_err(Error::Reqwest)?;
524
525 match resp.error_for_status_ref() {
526 Ok(_) => resp.json::<languages::Response>().await.map_err(Into::into),
527 Err(_) => Err(Error::InvalidRequest(resp.text().await?)),
528 }
529 }
530
531 pub async fn words(&self, request: &words::Request) -> Result<words::Response> {
533 let resp = self
534 .client
535 .get(format!("{}/words", self.api))
536 .header(ACCEPT, HeaderValue::from_static("application/json"))
537 .query(request)
538 .send()
539 .await
540 .map_err(Error::Reqwest)?;
541
542 match resp.error_for_status_ref() {
543 Ok(_) => resp.json::<words::Response>().await.map_err(Error::Reqwest),
544 Err(_) => Err(Error::InvalidRequest(resp.text().await?)),
545 }
546 }
547
548 pub async fn words_add(&self, request: &words::add::Request) -> Result<words::add::Response> {
550 let resp = self
551 .client
552 .post(format!("{}/words/add", self.api))
553 .header(ACCEPT, HeaderValue::from_static("application/json"))
554 .form(request)
555 .send()
556 .await
557 .map_err(Error::Reqwest)?;
558
559 match resp.error_for_status_ref() {
560 Ok(_) => {
561 resp.json::<words::add::Response>()
562 .await
563 .map_err(Error::Reqwest)
564 },
565 Err(_) => Err(Error::InvalidRequest(resp.text().await?)),
566 }
567 }
568
569 pub async fn words_delete(
571 &self,
572 request: &words::delete::Request,
573 ) -> Result<words::delete::Response> {
574 let resp = self
575 .client
576 .post(format!("{}/words/delete", self.api))
577 .header(ACCEPT, HeaderValue::from_static("application/json"))
578 .form(request)
579 .send()
580 .await
581 .map_err(Error::Reqwest)?;
582
583 match resp.error_for_status_ref() {
584 Ok(_) => {
585 resp.json::<words::delete::Response>()
586 .await
587 .map_err(Error::Reqwest)
588 },
589 Err(_) => Err(Error::InvalidRequest(resp.text().await?)),
590 }
591 }
592
593 pub async fn ping(&self) -> Result<u128> {
596 let start = Instant::now();
597 self.client.get(&self.api).send().await?;
598 Ok((Instant::now() - start).as_millis())
599 }
600}
601
602impl Default for ServerClient {
603 fn default() -> Self {
604 Self::from_cli(ServerCli::default())
605 }
606}
607
608impl ServerClient {
609 pub fn from_env() -> Result<Self> {
613 Ok(Self::from_cli(ServerCli::from_env()?))
614 }
615
616 #[must_use]
620 pub fn from_env_or_default() -> Self {
621 Self::from_cli(ServerCli::from_env_or_default())
622 }
623}
624
625#[cfg(test)]
626mod tests {
627 use std::borrow::Cow;
628
629 use assert_matches::assert_matches;
630
631 use super::ServerClient;
632 use crate::{api::check::Request, error::Error};
633
634 fn get_testing_server_client() -> ServerClient {
635 ServerClient::new("http://localhost", "8010")
636 }
637
638 #[tokio::test]
639 async fn test_server_ping() {
640 let client = get_testing_server_client();
641 assert!(
642 client.ping().await.is_ok(),
643 "\n----------------------------------------------------------------------------------------------\n\
644 IMPORTANT: Please ensure that there is a local LanguageTool service running on port 8010.\n\
645 ----------------------------------------------------------------------------------------------\n"
646 );
647 }
648
649 #[tokio::test]
650 async fn test_server_check_text() {
651 let client = get_testing_server_client();
652
653 let req = Request::default().with_text("je suis une poupee");
654 assert!(client.check(&req).await.is_ok());
655
656 let req = Request::default().with_text("Repeat ".repeat(1500));
658 assert_matches!(client.check(&req).await, Err(Error::InvalidRequest(_)));
659 }
660
661 #[tokio::test]
662 async fn test_server_check_data() {
663 let client = get_testing_server_client();
664 let req = Request::default()
665 .with_data_str("{\"annotation\":[{\"text\": \"je suis une poupee\"}]}")
666 .unwrap();
667 assert!(client.check(&req).await.is_ok());
668
669 let req = Request::default()
671 .with_data_str(&format!(
672 "{{\"annotation\":[{{\"text\": \"{}\"}}]}}",
673 "repeat".repeat(5000)
674 ))
675 .unwrap();
676 assert_matches!(client.check(&req).await, Err(Error::InvalidRequest(_)));
677 }
678
679 #[tokio::test]
680 async fn test_server_check_multiple_and_join() {
681 const TEXT: &str = "I am a doll.\nBut what are you?";
682 let client = get_testing_server_client();
683
684 let requests = Request::default()
685 .with_language("en-US".into())
686 .with_text(TEXT)
687 .split(20, "\n");
688 let resp = client.check_multiple_and_join(requests).await.unwrap();
689
690 assert_eq!(resp.text, Cow::from(TEXT));
691 assert_eq!(resp.text_length, TEXT.len());
692 #[cfg(feature = "unstable")]
693 assert!(!resp.response.warnings.as_ref().unwrap().incomplete_results);
694 assert_eq!(resp.response.iter_matches().next(), None);
695 assert_eq!(resp.response.language.name, "English (US)");
696
697 let requests = vec![Request::default().with_language("en-US".into())];
699 assert!(client.check_multiple_and_join(requests).await.is_err());
700 let requests = vec![Request::default()
701 .with_language("en-US".into())
702 .with_data_str("{\"annotation\":[{\"text\": \"je suis une poupee\"}]}")
703 .unwrap()];
704 assert!(client.check_multiple_and_join(requests).await.is_err());
705 }
706
707 #[tokio::test]
708 async fn test_server_check_multiple_and_join_without_context() {
709 let client = get_testing_server_client();
710
711 let requests = vec![Request::default()
712 .with_language("en-US".into())
713 .with_data_str("{\"annotation\":[{\"text\": \"I am a doll\"}]}")
714 .unwrap()];
715 let resp = client
716 .check_multiple_and_join_without_context(requests)
717 .await
718 .unwrap();
719
720 #[cfg(feature = "unstable")]
721 assert!(!resp.warnings.as_ref().unwrap().incomplete_results);
722 assert_eq!(resp.iter_matches().next(), None);
723 assert_eq!(resp.language.name, "English (US)");
724
725 let requests = vec![Request::default()
726 .with_language("en-US".into())
727 .with_text("I am a doll.")];
728 let resp = client
729 .check_multiple_and_join_without_context(requests)
730 .await
731 .unwrap();
732 assert_eq!(resp.iter_matches().next(), None);
733
734 let requests = vec![Request::default().with_language("en-US".into())];
736 assert!(client.check_multiple_and_join(requests).await.is_err());
737 }
738
739 #[cfg(feature = "annotate")]
740 #[tokio::test]
741 async fn test_server_annotate() {
742 let client = get_testing_server_client();
743
744 let req = Request::default()
745 .with_text("Who are you?")
746 .with_language("en-US".into());
747 let annotated = client
748 .annotate_check(&req, Some("origin"), false)
749 .await
750 .unwrap();
751 assert_eq!(
752 annotated,
753 "No errors were found in provided text".to_string()
754 );
755
756 let req = Request::default()
757 .with_text("Who ar you?")
758 .with_language("en-US".into());
759 let annotated = client
760 .annotate_check(&req, Some("origin"), false)
761 .await
762 .unwrap();
763 assert!(
764 annotated.starts_with("error[MORFOLOGIK_RULE_EN_US]: Possible spelling mistake found.")
765 );
766 assert!(annotated.contains("^^ Possible spelling mistake"));
767 }
768
769 #[tokio::test]
770 async fn test_server_languages() {
771 let client = get_testing_server_client();
772 assert!(client.languages().await.is_ok());
773 }
774}