aboutsummaryrefslogtreecommitdiffstats
path: root/src/model.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/model.rs')
-rw-r--r--src/model.rs150
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());
+ }
+}