aboutsummaryrefslogtreecommitdiffstats
path: root/src/client.rs
diff options
context:
space:
mode:
authorElvis Claros Castro <elvis@claros.ar>2026-09-26 19:00:45 -0300
committerElvis Claros Castro <elvis@claros.ar>2026-09-26 19:00:45 -0300
commita946ba81f7a6089713337644053b00b825824a98 (patch)
treedf7c98401b3a879a0283e940afa3fecc9453d9d4 /src/client.rs
downloaddrae-cli-main.tar.gz
drae-cli-main.zip
drae: the RAE Spanish dictionary in the terminalHEADmain
Diffstat (limited to 'src/client.rs')
-rw-r--r--src/client.rs85
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");
+ }
+}