1use crate::Error;
2use reqwest::header::{
3 HeaderMap,
4 HeaderValue,
5 ACCEPT,
6};
7use scraper::{
8 Html,
9 Selector,
10};
11use std::sync::LazyLock;
12
13static ACCEPT_VALUE: HeaderValue = HeaderValue::from_static("*/*");
14
15#[derive(Debug, Clone)]
17pub struct Client {
18 pub client: reqwest::Client,
20}
21
22impl Client {
23 pub fn new() -> Self {
25 let mut headers = HeaderMap::new();
26 headers.insert(ACCEPT, ACCEPT_VALUE.clone());
27
28 let client = reqwest::Client::builder()
29 .user_agent("yodaspeak-rs")
30 .default_headers(headers)
31 .build()
32 .expect("failed to build client");
33
34 Self { client }
35 }
36
37 pub async fn translate(&self, data: &str) -> Result<String, Error> {
39 let text = self
40 .client
41 .post("https://www.yodaspeak.co.uk/index.php")
42 .form(&[("YodaMe", data), ("go", "Convert to Yoda-Speak!")])
43 .send()
44 .await?
45 .error_for_status()?
46 .text()
47 .await?;
48
49 tokio::task::spawn_blocking(move || {
50 static SELECTOR: LazyLock<Selector> = LazyLock::new(|| {
51 Selector::parse("#result textarea").expect("failed to parse `SELECTOR`")
52 });
53 let html = Html::parse_document(&text);
54
55 let result = html
56 .select(&SELECTOR)
57 .next()
58 .and_then(|el| el.text().next())
59 .ok_or(Error::MissingResult)?;
60
61 Ok(result.to_string())
62 })
63 .await?
64 }
65}
66
67impl Default for Client {
68 fn default() -> Self {
69 Self::new()
70 }
71}