Skip to main content

prosaic_core/
agreement.rs

1//! Grammatical agreement features for multilingual rendering.
2//!
3//! Carries gender / number / case / definiteness / animacy / person
4//! metadata on entity-typed context values. The English grammar layer
5//! ignores these features entirely; non-English grammars (`-es`, `-de`,
6//! etc.) consult them to produce correctly-agreeing articles, adjectives,
7//! pronouns, and verb forms.
8
9/// Grammatical gender axis.
10///
11/// Defaults to [`Gender::Unknown`] so English callers pay no cognitive cost.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14pub enum Gender {
15    #[default]
16    Unknown,
17    Masc,
18    Fem,
19    Neut,
20    /// Dutch, Scandinavian 2-gender ("common" + "neuter") systems.
21    Common,
22}
23
24/// Grammatical number axis.
25///
26/// Defaults to [`Number::Unknown`] so English callers pay no cognitive cost.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
28#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
29pub enum Number {
30    #[default]
31    Unknown,
32    Singular,
33    Plural,
34    /// Arabic, Slovenian, Biblical Hebrew dual number.
35    Dual,
36}
37
38/// Grammatical case axis.
39///
40/// Defaults to [`Case::Unknown`]. Additional cases (instrumental, locative,
41/// ablative, etc.) are reserved for v2 when Finnish/Russian/etc. become targets.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
43#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
44pub enum Case {
45    #[default]
46    Unknown,
47    Nominative,
48    Accusative,
49    Dative,
50    Genitive,
51}
52
53/// Definiteness axis.
54///
55/// Defaults to [`Definiteness::Unknown`].
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
57#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
58pub enum Definiteness {
59    #[default]
60    Unknown,
61    Definite,
62    Indefinite,
63}
64
65/// Animacy axis (relevant to Russian/Polish case declension and similar).
66///
67/// Defaults to [`Animacy::Unknown`].
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
69#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
70pub enum Animacy {
71    #[default]
72    Unknown,
73    Animate,
74    Inanimate,
75}
76
77/// Grammatical person axis for agreement feature metadata.
78///
79/// Defaults to [`AgreementPerson::Third`], which is correct for most
80/// named entities (services, components, etc.).
81///
82/// Note: this is distinct from [`crate::language::Person`], which drives
83/// verb conjugation in the English grammar layer. This enum carries
84/// agreement metadata on [`crate::Value::Entity`] values.
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
86#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
87pub enum AgreementPerson {
88    First,
89    Second,
90    #[default]
91    Third,
92}
93
94/// A bundle of grammatical agreement features for a named entity.
95///
96/// All fields default to their "unknown" / most-neutral variant so that English
97/// callers can ignore the struct entirely while non-English grammars consult it
98/// for gender, number, case, definiteness, animacy, and person.
99///
100/// # Example
101///
102/// ```
103/// use prosaic_core::agreement::{AgreementFeatures, Gender, Number, Definiteness};
104///
105/// let f = AgreementFeatures::new()
106///     .with_gender(Gender::Fem)
107///     .with_number(Number::Singular)
108///     .with_definiteness(Definiteness::Definite);
109///
110/// assert_eq!(f.gender, Gender::Fem);
111/// assert_eq!(f.number, Number::Singular);
112/// assert_eq!(f.definiteness, Definiteness::Definite);
113/// ```
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
115#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
116pub struct AgreementFeatures {
117    pub gender: Gender,
118    pub number: Number,
119    pub case: Case,
120    pub definiteness: Definiteness,
121    pub animacy: Animacy,
122    pub person: AgreementPerson,
123}
124
125impl AgreementFeatures {
126    /// Create a new `AgreementFeatures` with all fields set to their defaults
127    /// (all Unknown / Third person). Equivalent to `Default::default()`.
128    pub fn new() -> Self {
129        Self::default()
130    }
131
132    /// Set the gender axis.
133    pub fn with_gender(mut self, g: Gender) -> Self {
134        self.gender = g;
135        self
136    }
137
138    /// Set the number axis.
139    pub fn with_number(mut self, n: Number) -> Self {
140        self.number = n;
141        self
142    }
143
144    /// Set the case axis.
145    pub fn with_case(mut self, c: Case) -> Self {
146        self.case = c;
147        self
148    }
149
150    /// Set the definiteness axis.
151    pub fn with_definiteness(mut self, d: Definiteness) -> Self {
152        self.definiteness = d;
153        self
154    }
155
156    /// Set the animacy axis.
157    pub fn with_animacy(mut self, a: Animacy) -> Self {
158        self.animacy = a;
159        self
160    }
161
162    /// Set the person axis.
163    pub fn with_person(mut self, p: AgreementPerson) -> Self {
164        self.person = p;
165        self
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn default_is_all_unknown() {
175        let f = AgreementFeatures::default();
176        assert_eq!(f.gender, Gender::Unknown);
177        assert_eq!(f.number, Number::Unknown);
178        assert_eq!(f.case, Case::Unknown);
179        assert_eq!(f.definiteness, Definiteness::Unknown);
180        assert_eq!(f.animacy, Animacy::Unknown);
181        assert_eq!(f.person, AgreementPerson::Third);
182    }
183
184    #[test]
185    fn builder_with_methods_set_fields() {
186        let f = AgreementFeatures::new()
187            .with_gender(Gender::Fem)
188            .with_number(Number::Singular)
189            .with_case(Case::Accusative)
190            .with_definiteness(Definiteness::Definite)
191            .with_animacy(Animacy::Animate)
192            .with_person(AgreementPerson::First);
193        assert_eq!(f.gender, Gender::Fem);
194        assert_eq!(f.number, Number::Singular);
195        assert_eq!(f.case, Case::Accusative);
196        assert_eq!(f.definiteness, Definiteness::Definite);
197        assert_eq!(f.animacy, Animacy::Animate);
198        assert_eq!(f.person, AgreementPerson::First);
199    }
200
201    #[test]
202    fn features_are_copy() {
203        fn takes_copy<T: Copy>(_: T) {}
204        takes_copy(AgreementFeatures::default());
205        takes_copy(Gender::Fem);
206        takes_copy(Number::Plural);
207        takes_copy(Case::Dative);
208        takes_copy(Definiteness::Indefinite);
209        takes_copy(Animacy::Animate);
210        takes_copy(AgreementPerson::First);
211    }
212
213    #[test]
214    fn all_gender_variants_are_distinct() {
215        assert_ne!(Gender::Masc, Gender::Fem);
216        assert_ne!(Gender::Fem, Gender::Neut);
217        assert_ne!(Gender::Neut, Gender::Common);
218        assert_ne!(Gender::Common, Gender::Unknown);
219    }
220
221    #[test]
222    fn all_number_variants_are_distinct() {
223        assert_ne!(Number::Singular, Number::Plural);
224        assert_ne!(Number::Plural, Number::Dual);
225        assert_ne!(Number::Dual, Number::Unknown);
226    }
227
228    #[test]
229    fn all_case_variants_are_distinct() {
230        assert_ne!(Case::Nominative, Case::Accusative);
231        assert_ne!(Case::Accusative, Case::Dative);
232        assert_ne!(Case::Dative, Case::Genitive);
233        assert_ne!(Case::Genitive, Case::Unknown);
234    }
235}