fluent_test/backend/assertions/
sentence.rs

1use std::fmt::{self, Display, Formatter};
2
3/// Represents a complete sentence structure for an assertion
4#[derive(Debug, Clone)]
5pub struct AssertionSentence {
6    /// The subject of the assertion (usually the variable name)
7    pub subject: String,
8    /// The verb of the assertion (e.g., "be", "have", "contain")
9    pub verb: String,
10    /// The object of the assertion (e.g., "greater than 42", "of length 5", "'test'")
11    pub object: String,
12    /// Optional qualifiers for the assertion (e.g., "within tolerance", "when rounded")
13    pub qualifiers: Vec<String>,
14    /// Whether the assertion is negated (e.g., "not be", "does not have")
15    pub negated: bool,
16}
17
18impl AssertionSentence {
19    /// Create a new assertion sentence
20    pub fn new(verb: impl Into<String>, object: impl Into<String>) -> Self {
21        return Self { subject: "".to_string(), verb: verb.into(), object: object.into(), qualifiers: Vec::new(), negated: false };
22    }
23
24    /// Set whether the assertion is negated
25    pub fn with_negation(mut self, negated: bool) -> Self {
26        self.negated = negated;
27        return self;
28    }
29
30    /// Add a qualifier to the assertion
31    pub fn with_qualifier(mut self, qualifier: impl Into<String>) -> Self {
32        self.qualifiers.push(qualifier.into());
33        return self;
34    }
35
36    /// Format the sentence into a readable string (raw format, without subject)
37    pub fn format(&self) -> String {
38        let mut result = if self.negated { format!("not {} {}", self.verb, self.object) } else { format!("{} {}", self.verb, self.object) };
39
40        if !self.qualifiers.is_empty() {
41            result.push(' ');
42            result.push_str(&self.qualifiers.join(" "));
43        }
44
45        return result;
46    }
47
48    /// Format the sentence with grammatically correct 'not' placement (after the verb)
49    /// This is used for display purposes where improved grammar is desired
50    pub fn format_grammatical(&self) -> String {
51        let mut result = if self.negated {
52            // Place "not" after the verb for grammatical correctness
53            format!("{} not {}", self.verb, self.object)
54        } else {
55            format!("{} {}", self.verb, self.object)
56        };
57
58        if !self.qualifiers.is_empty() {
59            result.push(' ');
60            result.push_str(&self.qualifiers.join(" "));
61        }
62
63        return result;
64    }
65
66    /// Format the sentence with the correct verb conjugation based on the subject
67    pub fn format_with_conjugation(&self, subject: &str) -> String {
68        // Determine if the subject is plural
69        let is_plural = Self::is_plural_subject(subject);
70
71        // Convert the infinitive verb to the correct form based on plurality
72        let conjugated_verb = self.conjugate_verb(is_plural);
73
74        let mut result = if self.negated {
75            // Place "not" after the conjugated verb for grammatical correctness
76            format!("{} not {}", conjugated_verb, self.object)
77        } else {
78            format!("{} {}", conjugated_verb, self.object)
79        };
80
81        if !self.qualifiers.is_empty() {
82            result.push(' ');
83            result.push_str(&self.qualifiers.join(" "));
84        }
85
86        return result;
87    }
88
89    /// Determine if a subject name is likely plural
90    fn is_plural_subject(subject: &str) -> bool {
91        // Extract the base variable name from expressions like "var.method()" or "&var"
92        let base_name = Self::extract_base_name(subject);
93
94        // Common plural endings in English
95        let plural_endings = ["s", "es", "ies"];
96
97        // Check for common plural variable naming patterns
98        let is_plural_ending = plural_endings.iter().any(|ending| base_name.ends_with(ending));
99
100        // Also check for commonly used pluralized variable names in programming
101        let common_plurals = [
102            "items",
103            "elements",
104            "values",
105            "arrays",
106            "lists",
107            "maps",
108            "sets",
109            "objects",
110            "attributes",
111            "properties",
112            "entries",
113            "keys",
114            "numbers",
115            "strings",
116            "data",
117            "results",
118            "options",
119            "errors",
120            "children",
121        ];
122
123        let is_common_plural = common_plurals.contains(&base_name.to_lowercase().as_str());
124
125        // Return true if either condition is met
126        return is_plural_ending || is_common_plural;
127    }
128
129    /// Extract the base variable name from expressions
130    fn extract_base_name(expr: &str) -> String {
131        // Remove reference symbols
132        let without_ref = expr.trim_start_matches('&');
133
134        // Handle method calls like "var.method()" - extract "var"
135        if let Some(dot_pos) = without_ref.find('.') {
136            return without_ref[0..dot_pos].to_string();
137        }
138
139        // Handle array/slice indexing like "var[0]" - extract "var"
140        if let Some(bracket_pos) = without_ref.find('[') {
141            return without_ref[0..bracket_pos].to_string();
142        }
143
144        // No special case, return as is
145        return without_ref.to_string();
146    }
147
148    /// Conjugate the verb based on plurality
149    fn conjugate_verb(&self, is_plural: bool) -> String {
150        // Special case handling for common verbs
151        match self.verb.as_str() {
152            "be" => {
153                if is_plural {
154                    "are".to_string()
155                } else {
156                    "is".to_string()
157                }
158            }
159            "have" => {
160                if is_plural {
161                    "have".to_string()
162                } else {
163                    "has".to_string()
164                }
165            }
166            "contain" => {
167                if is_plural {
168                    "contain".to_string()
169                } else {
170                    "contains".to_string()
171                }
172            }
173            "start with" => {
174                if is_plural {
175                    "start with".to_string()
176                } else {
177                    "starts with".to_string()
178                }
179            }
180            "end with" => {
181                if is_plural {
182                    "end with".to_string()
183                } else {
184                    "ends with".to_string()
185                }
186            }
187            // For other verbs, add 's' in singular form
188            verb => {
189                if is_plural {
190                    verb.to_string()
191                } else {
192                    // Handle special cases for verbs ending in certain characters
193                    if verb.ends_with('s') || verb.ends_with('x') || verb.ends_with('z') || verb.ends_with("sh") || verb.ends_with("ch") {
194                        format!("{}es", verb)
195                    } else if verb.ends_with('y')
196                        && !verb.ends_with("ay")
197                        && !verb.ends_with("ey")
198                        && !verb.ends_with("oy")
199                        && !verb.ends_with("uy")
200                    {
201                        format!("{}ies", &verb[0..verb.len() - 1])
202                    } else {
203                        format!("{}s", verb)
204                    }
205                }
206            }
207        }
208    }
209}
210
211impl Display for AssertionSentence {
212    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
213        return write!(f, "{}", self.format());
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220
221    #[test]
222    fn test_assertion_sentence_new() {
223        let sentence = AssertionSentence::new("be", "positive");
224
225        assert_eq!(sentence.subject, "");
226        assert_eq!(sentence.verb, "be");
227        assert_eq!(sentence.object, "positive");
228        assert_eq!(sentence.qualifiers.len(), 0);
229        assert_eq!(sentence.negated, false);
230    }
231
232    #[test]
233    fn test_with_negation() {
234        let sentence = AssertionSentence::new("be", "positive").with_negation(true);
235
236        assert_eq!(sentence.negated, true);
237
238        // Test chaining and toggle
239        let toggled_sentence = sentence.with_negation(false);
240
241        assert_eq!(toggled_sentence.negated, false);
242    }
243
244    #[test]
245    fn test_with_qualifier() {
246        let sentence = AssertionSentence::new("be", "in range").with_qualifier("when rounded");
247
248        assert_eq!(sentence.qualifiers, vec!["when rounded"]);
249
250        // Test multiple qualifiers
251        let updated_sentence = sentence.with_qualifier("with tolerance");
252
253        assert_eq!(updated_sentence.qualifiers, vec!["when rounded", "with tolerance"]);
254    }
255
256    #[test]
257    fn test_format_basic() {
258        let sentence = AssertionSentence::new("be", "positive");
259
260        assert_eq!(sentence.format(), "be positive");
261    }
262
263    #[test]
264    fn test_format_with_negation() {
265        let sentence = AssertionSentence::new("be", "positive").with_negation(true);
266
267        assert_eq!(sentence.format(), "not be positive");
268    }
269
270    #[test]
271    fn test_format_with_qualifiers() {
272        let sentence = AssertionSentence::new("be", "in range").with_qualifier("when rounded").with_qualifier("with tolerance");
273
274        assert_eq!(sentence.format(), "be in range when rounded with tolerance");
275    }
276
277    #[test]
278    fn test_format_with_negation_and_qualifiers() {
279        let sentence = AssertionSentence::new("be", "in range").with_negation(true).with_qualifier("when rounded");
280
281        assert_eq!(sentence.format(), "not be in range when rounded");
282    }
283
284    #[test]
285    fn test_format_grammatical() {
286        let sentence = AssertionSentence::new("be", "positive");
287
288        assert_eq!(sentence.format_grammatical(), "be positive");
289
290        let negated = sentence.clone().with_negation(true);
291
292        // The "not" should be after the verb for grammatical correctness
293        assert_eq!(negated.format_grammatical(), "be not positive");
294    }
295
296    #[test]
297    fn test_format_grammatical_with_qualifiers() {
298        let sentence = AssertionSentence::new("be", "in range").with_negation(true).with_qualifier("when rounded");
299
300        assert_eq!(sentence.format_grammatical(), "be not in range when rounded");
301    }
302
303    #[test]
304    fn test_is_plural_subject() {
305        // Test singular subjects
306        assert_eq!(AssertionSentence::is_plural_subject("value"), false);
307        assert_eq!(AssertionSentence::is_plural_subject("number"), false);
308        assert_eq!(AssertionSentence::is_plural_subject("count"), false);
309        assert_eq!(AssertionSentence::is_plural_subject("item"), false);
310
311        // Test plural subjects
312        assert_eq!(AssertionSentence::is_plural_subject("values"), true);
313        assert_eq!(AssertionSentence::is_plural_subject("numbers"), true);
314        assert_eq!(AssertionSentence::is_plural_subject("items"), true);
315        assert_eq!(AssertionSentence::is_plural_subject("lists"), true);
316
317        // Test common plural variable names
318        assert_eq!(AssertionSentence::is_plural_subject("data"), true);
319        assert_eq!(AssertionSentence::is_plural_subject("children"), true);
320    }
321
322    #[test]
323    fn test_extract_base_name() {
324        // Test reference extraction
325        assert_eq!(AssertionSentence::extract_base_name("&value"), "value");
326
327        // Test method call extraction
328        assert_eq!(AssertionSentence::extract_base_name("values.len()"), "values");
329
330        // Test array indexing extraction
331        assert_eq!(AssertionSentence::extract_base_name("items[0]"), "items");
332
333        // Test combined cases
334        assert_eq!(AssertionSentence::extract_base_name("&items[0]"), "items");
335        assert_eq!(AssertionSentence::extract_base_name("&values.len()"), "values");
336    }
337
338    #[test]
339    fn test_conjugate_verb() {
340        // Create a test sentence
341        let sentence = AssertionSentence::new("", "");
342
343        // Test special case verbs
344        let special_verbs = [
345            ("be", "is", "are"),
346            ("have", "has", "have"),
347            ("contain", "contains", "contain"),
348            ("start with", "starts with", "start with"),
349            ("end with", "ends with", "end with"),
350        ];
351
352        for (base, singular, plural) in special_verbs.iter() {
353            let mut test_sentence = sentence.clone();
354            test_sentence.verb = base.to_string();
355
356            assert_eq!(test_sentence.conjugate_verb(false), *singular);
357            assert_eq!(test_sentence.conjugate_verb(true), *plural);
358        }
359
360        // Test regular verbs
361        let regular_verbs = [("match", "matches"), ("exceed", "exceeds"), ("include", "includes")];
362
363        for (base, singular) in regular_verbs.iter() {
364            let mut test_sentence = sentence.clone();
365            test_sentence.verb = base.to_string();
366
367            assert_eq!(test_sentence.conjugate_verb(false), *singular);
368            assert_eq!(test_sentence.conjugate_verb(true), *base);
369        }
370
371        // Test verbs with special spelling rules
372        let special_spelling = [
373            // Verbs ending in s, x, z, sh, ch get 'es'
374            ("pass", "passes"),
375            ("fix", "fixes"),
376            ("buzz", "buzzes"),
377            ("wash", "washes"),
378            ("match", "matches"),
379            // Verbs ending in y (not preceded by a vowel) get 'ies'
380            ("try", "tries"),
381            ("fly", "flies"),
382            ("comply", "complies"),
383            // Verbs ending in y preceded by a vowel just get 's'
384            ("play", "plays"),
385            ("enjoy", "enjoys"),
386        ];
387
388        for (base, singular) in special_spelling.iter() {
389            let mut test_sentence = sentence.clone();
390            test_sentence.verb = base.to_string();
391
392            assert_eq!(test_sentence.conjugate_verb(false), *singular);
393            assert_eq!(test_sentence.conjugate_verb(true), *base);
394        }
395    }
396
397    #[test]
398    fn test_format_with_conjugation() {
399        // Test singular subject conjugation
400        let sentence = AssertionSentence::new("be", "positive");
401        assert_eq!(sentence.format_with_conjugation("value"), "is positive");
402
403        // Test plural subject conjugation
404        assert_eq!(sentence.format_with_conjugation("values"), "are positive");
405
406        // Test with negation - Note: we need to clone since with_negation consumes self
407        let negated = sentence.clone().with_negation(true);
408        assert_eq!(negated.format_with_conjugation("value"), "is not positive");
409        assert_eq!(negated.format_with_conjugation("values"), "are not positive");
410
411        // Test with qualifiers - Note: we need to clone since with_qualifier consumes self
412        let qualified = sentence.clone().with_qualifier("always");
413        assert_eq!(qualified.format_with_conjugation("value"), "is positive always");
414
415        // Test different verbs
416        let contain_sentence = AssertionSentence::new("contain", "element");
417        assert_eq!(contain_sentence.format_with_conjugation("list"), "contains element");
418        assert_eq!(contain_sentence.format_with_conjugation("lists"), "contain element");
419    }
420
421    #[test]
422    fn test_display_trait() {
423        let sentence = AssertionSentence::new("be", "positive");
424        assert_eq!(format!("{}", sentence), "be positive");
425
426        let negated = sentence.clone().with_negation(true);
427        assert_eq!(format!("{}", negated), "not be positive");
428    }
429}