diff options
| author | Elvis Claros Castro <elvis@claros.ar> | 2026-09-26 19:00:45 -0300 |
|---|---|---|
| committer | Elvis Claros Castro <elvis@claros.ar> | 2026-09-26 19:00:45 -0300 |
| commit | a946ba81f7a6089713337644053b00b825824a98 (patch) | |
| tree | df7c98401b3a879a0283e940afa3fecc9453d9d4 /src/model.rs | |
| download | drae-cli-a946ba81f7a6089713337644053b00b825824a98.tar.gz drae-cli-a946ba81f7a6089713337644053b00b825824a98.zip | |
Diffstat (limited to 'src/model.rs')
| -rw-r--r-- | src/model.rs | 150 |
1 files changed, 150 insertions, 0 deletions
diff --git a/src/model.rs b/src/model.rs new file mode 100644 index 0000000..c6255e0 --- /dev/null +++ b/src/model.rs @@ -0,0 +1,150 @@ +//! 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<String>, + pub senses: Vec<Sense>, + /// Set phrases and expressions: «ni hablar», «vino de coco»… + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub locutions: Vec<Locution>, + #[serde(skip_serializing_if = "Option::is_none")] + pub conjugation: Option<Conjugation>, + /// 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<String> { + dedup(self.senses.iter().flat_map(|s| s.synonyms.iter())) + } + + pub fn all_antonyms(&self) -> Vec<String> { + 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<Item = &'a String>) -> Vec<String> { + let mut out: Vec<String> = 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<String>, + /// 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<Abbr>, + /// The definition text, without the number or the marks. + pub text: String, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub examples: Vec<String>, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub synonyms: Vec<String>, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub antonyms: Vec<String>, +} + +/// 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<Sense>, +} + +/// 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<ConjGroup>, +} + +/// A verb mood: «Modo indicativo», «Formas no personales»… +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConjGroup { + pub title: String, + pub tenses: Vec<Tense>, +} + +/// 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<String>, +} + +#[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<Sense>) -> 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()); + } +} |