1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
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");
}
}
|