//! The data model shared by both sources (DLE and rae-api) and both output //! formats (terminal and JSON). use serde::{Deserialize, Serialize}; /// A dictionary entry: everything known about a word. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Entry { /// Headword as the dictionary writes it (may carry a superscript). pub word: String, #[serde(skip_serializing_if = "Option::is_none")] pub etymology: Option, pub senses: Vec, /// Set phrases and expressions: «ni hablar», «vino de coco»… #[serde(default, skip_serializing_if = "Vec::is_empty")] pub locutions: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub conjugation: Option, /// Where it came from: `dle` or `rae-api`. pub source: String, } impl Entry { /// Synonyms of all senses, without repeats and in order of appearance. pub fn all_synonyms(&self) -> Vec { dedup(self.senses.iter().flat_map(|s| s.synonyms.iter())) } pub fn all_antonyms(&self) -> Vec { dedup(self.senses.iter().flat_map(|s| s.antonyms.iter())) } pub fn has_synonyms(&self) -> bool { self.senses.iter().any(|s| !s.synonyms.is_empty()) } pub fn has_antonyms(&self) -> bool { self.senses.iter().any(|s| !s.antonyms.is_empty()) } } fn dedup<'a>(it: impl Iterator) -> Vec { let mut out: Vec = Vec::new(); for w in it { if !out.iter().any(|e| e == w) { out.push(w.clone()); } } out } /// A numbered sense. #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct Sense { /// Sense number, without the dot: `"1"`. #[serde(skip_serializing_if = "Option::is_none")] pub number: Option, /// Grammar and usage marks (`f.`, `intr.`, `p. us.`) with their expanded /// meaning, which the DLE keeps in the `title` attribute. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub grammar: Vec, /// The definition text, without the number or the marks. pub text: String, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub examples: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub synonyms: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub antonyms: Vec, } /// A dictionary abbreviation together with its expanded form. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Abbr { /// As printed: `f.` pub short: String, /// As the dictionary explains it: `nombre femenino` pub long: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Locution { pub phrase: String, pub senses: Vec, } /// The full conjugation of a verb, grouped the way the DLE groups it. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Conjugation { /// «Conjugación de "hablar"» pub title: String, pub groups: Vec, } /// A verb mood: «Modo indicativo», «Formas no personales»… #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ConjGroup { pub title: String, pub tenses: Vec, } /// A verb tense with its forms, each one with its pronoun /// («yo hablo», «vos hablás»). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Tense { pub name: String, pub forms: Vec, } #[cfg(test)] mod tests { use super::*; fn sense(syn: &[&str], ant: &[&str]) -> Sense { Sense { synonyms: syn.iter().map(|s| s.to_string()).collect(), antonyms: ant.iter().map(|s| s.to_string()).collect(), ..Default::default() } } fn entry(senses: Vec) -> Entry { Entry { word: "casa".into(), etymology: None, senses, locutions: vec![], conjugation: None, source: "dle".into(), } } #[test] fn collects_synonyms_across_senses_without_repeating() { let e = entry(vec![ sense(&["vivienda", "hogar"], &[]), sense(&["hogar", "morada"], &[]), ]); assert_eq!(e.all_synonyms(), ["vivienda", "hogar", "morada"]); assert!(e.has_synonyms()); assert!(!e.has_antonyms()); } #[test] fn reports_no_synonyms_for_an_entry_without_them() { let e = entry(vec![sense(&[], &[])]); assert!(e.all_synonyms().is_empty()); assert!(!e.has_synonyms()); } }