diff options
Diffstat (limited to 'src/client.rs')
| -rw-r--r-- | src/client.rs | 85 |
1 files changed, 85 insertions, 0 deletions
diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..37205bf --- /dev/null +++ b/src/client.rs @@ -0,0 +1,85 @@ +//! HTTP client. +//! +//! dle.rae.es answers 403 to short or missing user agents, so requests to it +//! identify as a desktop browser. + +use std::time::Duration; + +use anyhow::{Context, Result}; +use ureq::Agent; + +/// Browser user agent, for dle.rae.es, which rejects clients that do not +/// identify as one. +pub const BROWSER_UA: &str = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 \ + (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"; + +/// Honest identification for APIs that want to know who is calling, as the +/// Wikimedia policy asks. +pub const CLIENT_UA: &str = concat!( + "drae-cli/", + env!("CARGO_PKG_VERSION"), + " (terminal dictionary)" +); + +pub struct Http { + agent: Agent, +} + +impl Http { + pub fn new(timeout_secs: u64) -> Self { + let config = Agent::config_builder() + .timeout_global(Some(Duration::from_secs(timeout_secs))) + // A 404 is an answer we want to read, not a network error. + .http_status_as_error(false) + .build(); + Http { + agent: config.new_agent(), + } + } + + /// Returns the status code and the body. + pub fn get(&self, url: &str, user_agent: &str) -> Result<(u16, String)> { + let mut res = self + .agent + .get(url) + .header("User-Agent", user_agent) + .header("Accept-Language", "es-ES,es;q=0.9") + .header("Accept", "text/html,application/json;q=0.9,*/*;q=0.8") + .call() + .with_context(|| format!("no se pudo conectar con {url}"))?; + let status = res.status().as_u16(); + let body = res + .body_mut() + .read_to_string() + .with_context(|| format!("respuesta ilegible de {url}"))?; + Ok((status, body)) + } +} + +/// Encodes a word for a URL path. The RAE needs `%C3%B1` for ñ; other safe +/// characters pass through unchanged. +pub fn percent_encode(s: &str) -> String { + let mut out = String::with_capacity(s.len()); + for b in s.as_bytes() { + match b { + b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => { + out.push(*b as char) + } + _ => out.push_str(&format!("%{b:02X}")), + } + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn encodes_spanish_characters() { + assert_eq!(percent_encode("año"), "a%C3%B1o"); + assert_eq!(percent_encode("café"), "caf%C3%A9"); + assert_eq!(percent_encode("perro"), "perro"); + assert_eq!(percent_encode("dar de sí"), "dar%20de%20s%C3%AD"); + } +} |