use std::collections::HashMap;
use std::io::Read;
use std::sync::OnceLock;
use std::time::Duration;
use crate::DefinitionSource;
static DEFS_GZ: &[u8] = include_bytes!("../assets/definitions-en.tsv.gz");
fn local_map() -> &'static HashMap<String, String> {
static MAP: OnceLock<HashMap<String, String>> = OnceLock::new();
MAP.get_or_init(|| {
let mut text = String::new();
if flate2::read::GzDecoder::new(DEFS_GZ)
.read_to_string(&mut text)
.is_err()
{
return HashMap::new();
}
text.lines()
.filter_map(|line| line.split_once('\t'))
.map(|(w, d)| (w.to_string(), d.to_string()))
.collect()
})
}
pub fn define_local(word: &str) -> Option<String> {
let key = word.trim().to_ascii_lowercase();
if key.is_empty() {
return None;
}
local_map().get(&key).cloned()
}
pub fn define(word: &str, source: DefinitionSource) -> Option<String> {
match source {
DefinitionSource::Local => define_local(word),
DefinitionSource::Off | DefinitionSource::Online => None,
}
}
pub fn define_online(word: &str) -> Option<String> {
let word = word.trim();
if word.is_empty() {
return None;
}
let url = format!(
"https://api.dictionaryapi.dev/api/v2/entries/en/{}",
urlencode(word)
);
let agent: ureq::Agent = ureq::AgentBuilder::new()
.timeout(Duration::from_secs(8))
.build();
let json: serde_json::Value = agent.get(&url).call().ok()?.into_json().ok()?;
let def = json
.get(0)?
.get("meanings")?
.as_array()?
.iter()
.find_map(|m| {
m.get("definitions")?
.as_array()?
.iter()
.find_map(|d| d.get("definition").and_then(serde_json::Value::as_str))
})?;
Some(def.trim().to_string())
}
fn urlencode(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for b in s.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 local_covers_common_words_and_misses_gracefully() {
assert!(define_local("difference").is_some());
assert!(define_local("acquiesce").is_some());
assert!(define_local("Veneer").is_some());
assert!(define_local("recombobulate").is_none());
assert!(define_local("").is_none());
assert!(define_local(" ").is_none());
}
#[test]
fn define_routes_by_source() {
assert!(define("acquiesce", DefinitionSource::Local).is_some());
assert!(define("acquiesce", DefinitionSource::Online).is_none());
assert!(define("acquiesce", DefinitionSource::Off).is_none());
}
}