fluent_test/backend/assertions/
sentence.rs1use std::fmt::{self, Display, Formatter};
2
3#[derive(Debug, Clone)]
5pub struct AssertionSentence {
6 pub subject: String,
8 pub verb: String,
10 pub object: String,
12 pub qualifiers: Vec<String>,
14 pub negated: bool,
16}
17
18impl AssertionSentence {
19 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 pub fn with_negation(mut self, negated: bool) -> Self {
26 self.negated = negated;
27 return self;
28 }
29
30 pub fn with_qualifier(mut self, qualifier: impl Into<String>) -> Self {
32 self.qualifiers.push(qualifier.into());
33 return self;
34 }
35
36 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 pub fn format_grammatical(&self) -> String {
51 let mut result = if self.negated {
52 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 pub fn format_with_conjugation(&self, subject: &str) -> String {
68 let is_plural = Self::is_plural_subject(subject);
70
71 let conjugated_verb = self.conjugate_verb(is_plural);
73
74 let mut result = if self.negated {
75 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 fn is_plural_subject(subject: &str) -> bool {
91 let base_name = Self::extract_base_name(subject);
93
94 let plural_endings = ["s", "es", "ies"];
96
97 let is_plural_ending = plural_endings.iter().any(|ending| base_name.ends_with(ending));
99
100 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 is_plural_ending || is_common_plural;
127 }
128
129 fn extract_base_name(expr: &str) -> String {
131 let without_ref = expr.trim_start_matches('&');
133
134 if let Some(dot_pos) = without_ref.find('.') {
136 return without_ref[0..dot_pos].to_string();
137 }
138
139 if let Some(bracket_pos) = without_ref.find('[') {
141 return without_ref[0..bracket_pos].to_string();
142 }
143
144 return without_ref.to_string();
146 }
147
148 fn conjugate_verb(&self, is_plural: bool) -> String {
150 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 verb => {
189 if is_plural {
190 verb.to_string()
191 } else {
192 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 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 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 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 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 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 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 assert_eq!(AssertionSentence::extract_base_name("&value"), "value");
326
327 assert_eq!(AssertionSentence::extract_base_name("values.len()"), "values");
329
330 assert_eq!(AssertionSentence::extract_base_name("items[0]"), "items");
332
333 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 let sentence = AssertionSentence::new("", "");
342
343 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 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 let special_spelling = [
373 ("pass", "passes"),
375 ("fix", "fixes"),
376 ("buzz", "buzzes"),
377 ("wash", "washes"),
378 ("match", "matches"),
379 ("try", "tries"),
381 ("fly", "flies"),
382 ("comply", "complies"),
383 ("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 let sentence = AssertionSentence::new("be", "positive");
401 assert_eq!(sentence.format_with_conjugation("value"), "is positive");
402
403 assert_eq!(sentence.format_with_conjugation("values"), "are positive");
405
406 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 let qualified = sentence.clone().with_qualifier("always");
413 assert_eq!(qualified.format_with_conjugation("value"), "is positive always");
414
415 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}