1#[cfg(not(feature = "std"))]
2use alloc::format;
3#[cfg(not(feature = "std"))]
4use alloc::string::{String, ToString};
5#[cfg(not(feature = "std"))]
6use alloc::vec::Vec;
7
8use crate::engine::Engine;
9use crate::error::ProsaicError;
10use crate::language::{Conjunction, Person, Tense, VerbForm, Voice};
11
12#[derive(Debug, Clone)]
14pub struct Subject {
15 entity_type: Option<String>,
16 name: String,
17}
18
19pub fn subject(entity_type: &str, name: &str) -> Subject {
22 Subject {
23 entity_type: Some(entity_type.to_string()),
24 name: name.to_string(),
25 }
26}
27
28pub fn named(name: &str) -> Subject {
30 Subject {
31 entity_type: None,
32 name: name.to_string(),
33 }
34}
35
36#[derive(Debug, Clone)]
38pub struct Clause {
39 intro: String,
40 amount: Option<usize>,
41 noun: Option<String>,
42 items: Vec<String>,
43 truncate_at: Option<usize>,
44 conjunction: Conjunction,
45}
46
47impl Clause {
48 pub fn which(verb: &str) -> Self {
50 Self {
51 intro: format!("which {verb}"),
52 amount: None,
53 noun: None,
54 items: Vec::new(),
55 truncate_at: None,
56 conjunction: Conjunction::And,
57 }
58 }
59
60 pub fn with_intro(intro: &str) -> Self {
62 Self {
63 intro: intro.to_string(),
64 amount: None,
65 noun: None,
66 items: Vec::new(),
67 truncate_at: None,
68 conjunction: Conjunction::And,
69 }
70 }
71
72 pub fn amount(mut self, n: usize) -> Self {
74 self.amount = Some(n);
75 self
76 }
77
78 pub fn noun(mut self, noun: &str) -> Self {
80 self.noun = Some(noun.to_string());
81 self
82 }
83
84 pub fn list(mut self, items: &[&str]) -> Self {
86 self.items = items.iter().map(|s| s.to_string()).collect();
87 self
88 }
89
90 pub fn truncate(mut self, n: usize) -> Self {
92 self.truncate_at = Some(n);
93 self
94 }
95
96 pub fn conjunction(mut self, conjunction: Conjunction) -> Self {
98 self.conjunction = conjunction;
99 self
100 }
101
102 fn render(&self, engine: &Engine) -> Result<String, ProsaicError> {
103 let lang = engine.language();
104 let mut parts: Vec<String> = Vec::new();
105
106 if !self.intro.is_empty() {
107 parts.push(self.intro.clone());
108 }
109
110 if let Some(amount) = self.amount {
111 parts.push(amount.to_string());
112
113 if let Some(ref noun) = self.noun {
114 parts.push(lang.pluralize(noun, amount));
115 }
116 } else if let Some(ref noun) = self.noun {
117 parts.push(noun.clone());
118 }
119
120 if !self.items.is_empty() {
121 let display_items = self.truncated_items();
122 let refs: Vec<&str> = display_items.iter().map(|s| s.as_str()).collect();
123 let joined = lang.join_list(&refs, self.conjunction);
124 parts.push(format!("[{joined}]"));
125 }
126
127 Ok(parts.join(" "))
128 }
129
130 fn truncated_items(&self) -> Vec<String> {
131 match self.truncate_at {
132 Some(max) if self.items.len() > max => {
133 let remaining = self.items.len() - max;
134 let mut result: Vec<String> = self.items[..max].to_vec();
135 result.push(format!("{remaining} more"));
136 result
137 }
138 _ => self.items.clone(),
139 }
140 }
141}
142
143#[derive(Debug, Clone)]
150pub struct Sentence {
151 subject: Option<Subject>,
152 verb: Option<String>,
153 form: VerbForm,
154 voice: Voice,
155 person: Person,
156 preposition: Option<String>,
157 object: Option<String>,
158 clauses: Vec<Clause>,
159}
160
161impl Sentence {
162 pub fn new() -> Self {
163 Self {
164 subject: None,
165 verb: None,
166 form: VerbForm::SimplePast,
167 voice: Voice::Passive,
168 person: Person::Third,
169 preposition: None,
170 object: None,
171 clauses: Vec::new(),
172 }
173 }
174
175 pub fn subject(mut self, subject: Subject) -> Self {
177 self.subject = Some(subject);
178 self
179 }
180
181 pub fn verb(mut self, verb: &str, tense: Tense) -> Self {
185 self.verb = Some(verb.to_string());
186 self.form = VerbForm::from(tense);
187 self
188 }
189
190 pub fn form(mut self, form: VerbForm) -> Self {
193 self.form = form;
194 self
195 }
196
197 pub fn verb_word(mut self, verb: &str) -> Self {
200 self.verb = Some(verb.to_string());
201 self
202 }
203
204 pub fn voice(mut self, voice: Voice) -> Self {
209 self.voice = voice;
210 self
211 }
212
213 pub fn person(mut self, person: Person) -> Self {
216 self.person = person;
217 self
218 }
219
220 pub fn preposition(mut self, prep: &str) -> Self {
224 self.preposition = Some(prep.to_string());
225 self
226 }
227
228 pub fn object(mut self, object: &str) -> Self {
230 self.object = Some(object.to_string());
231 self
232 }
233
234 pub fn clause(mut self, clause: Clause) -> Self {
236 self.clauses.push(clause);
237 self
238 }
239
240 pub fn render(&self, engine: &Engine) -> Result<String, ProsaicError> {
242 let lang = engine.language();
243 let mut parts: Vec<String> = Vec::new();
244
245 if let Some(ref subject) = self.subject {
247 match &subject.entity_type {
248 Some(et) => parts.push(format!("The {} {}", et, subject.name)),
249 None => parts.push(subject.name.clone()),
250 }
251 }
252
253 if let Some(ref verb) = self.verb {
255 let phrase = lang.verb_phrase(verb, self.form, self.voice, self.person);
256 parts.push(phrase);
257 }
258
259 if let Some(ref object) = self.object {
261 match &self.preposition {
262 Some(prep) => parts.push(format!("{prep} {object}")),
263 None => {
264 if self.voice == Voice::Passive {
266 parts.push(format!("to {object}"));
267 } else {
268 parts.push(object.clone());
269 }
270 }
271 }
272 }
273
274 let mut sentence = parts.join(" ");
275
276 for clause in &self.clauses {
278 let rendered = clause.render(engine)?;
279 if !rendered.is_empty() {
280 sentence.push(' ');
281 sentence.push_str(&rendered);
282 }
283 }
284
285 Ok(sentence)
286 }
287}
288
289impl Default for Sentence {
290 fn default() -> Self {
291 Self::new()
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298 use crate::language::{Conjunction, Language, Person, Tense};
299
300 struct TestLang;
301
302 impl Language for TestLang {
303 fn pluralize(&self, word: &str, count: usize) -> String {
304 if count == 1 {
305 word.to_string()
306 } else {
307 format!("{word}s")
308 }
309 }
310 fn singularize(&self, word: &str) -> String {
311 word.strip_suffix('s').unwrap_or(word).to_string()
312 }
313 fn article(&self, word: &str) -> &str {
314 if word.starts_with(|c: char| "aeiou".contains(c.to_ascii_lowercase())) {
315 "an"
316 } else {
317 "a"
318 }
319 }
320 fn conjugate(&self, verb: &str, tense: Tense, _person: Person) -> String {
321 match (verb, tense) {
322 ("be", Tense::Past) => "was".to_string(),
323 ("be", Tense::Present) => "is".to_string(),
324 ("have", Tense::Present) => "has".to_string(),
325 (_, Tense::Past) => format!("{verb}ed"),
326 (_, Tense::Present) => verb.to_string(),
327 (_, Tense::Future) => format!("will {verb}"),
328 }
329 }
330 fn past_participle(&self, verb: &str) -> String {
331 format!("{verb}ed")
332 }
333 fn present_participle(&self, verb: &str) -> String {
334 format!("{verb}ing")
335 }
336 fn join_list(&self, items: &[&str], conjunction: Conjunction) -> String {
337 let conj = match conjunction {
338 Conjunction::And => "and",
339 Conjunction::Or => "or",
340 };
341 match items.len() {
342 0 => String::new(),
343 1 => items[0].to_string(),
344 2 => format!("{} {conj} {}", items[0], items[1]),
345 _ => {
346 let head = items[..items.len() - 1].join(", ");
347 format!("{head}, {conj} {}", items[items.len() - 1])
348 }
349 }
350 }
351 fn ordinal(&self, n: usize) -> String {
352 format!("{n}th")
353 }
354 fn number_to_words(&self, n: usize) -> String {
355 format!("<{n}>")
356 }
357 }
358
359 fn test_engine() -> Engine {
360 Engine::new(TestLang)
361 }
362
363 #[test]
364 fn passive_voice_past_tense() {
365 let engine = test_engine();
366 let s = Sentence::new()
367 .subject(subject("class", "Foo"))
368 .verb("rename", Tense::Past)
369 .object("Foobar")
370 .render(&engine)
371 .unwrap();
372
373 assert_eq!(s, "The class Foo was renameed to Foobar");
374 }
375
376 #[test]
377 fn active_voice_past_tense() {
378 let engine = test_engine();
379 let s = Sentence::new()
380 .subject(subject("class", "Foo"))
381 .verb("rename", Tense::Past)
382 .object("Foobar")
383 .voice(Voice::Active)
384 .render(&engine)
385 .unwrap();
386
387 assert_eq!(s, "The class Foo renameed Foobar");
388 }
389
390 #[test]
391 fn passive_voice_with_clause() {
392 let engine = test_engine();
393 let s = Sentence::new()
394 .subject(subject("class", "Foo"))
395 .verb("rename", Tense::Past)
396 .object("Foobar")
397 .clause(Clause::which("impacts").amount(6).noun("direct consumer"))
398 .render(&engine)
399 .unwrap();
400
401 assert_eq!(
402 s,
403 "The class Foo was renameed to Foobar which impacts 6 direct consumers"
404 );
405 }
406
407 #[test]
408 fn passive_voice_with_clause_and_list() {
409 let engine = test_engine();
410 let s = Sentence::new()
411 .subject(subject("class", "Foo"))
412 .verb("rename", Tense::Past)
413 .object("Foobar")
414 .clause(
415 Clause::which("impacts")
416 .amount(6)
417 .noun("direct consumer")
418 .list(&["Baz", "Qux", "Quux", "Corge", "Grault", "Garply"])
419 .truncate(3),
420 )
421 .render(&engine)
422 .unwrap();
423
424 assert_eq!(
425 s,
426 "The class Foo was renameed to Foobar which impacts 6 direct consumers \
427 [Baz, Qux, Quux, and 3 more]"
428 );
429 }
430
431 #[test]
432 fn passive_voice_no_object() {
433 let engine = test_engine();
434 let s = Sentence::new()
435 .subject(named("UserService"))
436 .verb("modify", Tense::Past)
437 .render(&engine)
438 .unwrap();
439
440 assert_eq!(s, "UserService was modifyed");
441 }
442
443 #[test]
444 fn active_voice_no_object() {
445 let engine = test_engine();
446 let s = Sentence::new()
447 .subject(named("UserService"))
448 .verb("modify", Tense::Past)
449 .voice(Voice::Active)
450 .render(&engine)
451 .unwrap();
452
453 assert_eq!(s, "UserService modifyed");
454 }
455
456 #[test]
457 fn custom_preposition() {
458 let engine = test_engine();
459 let s = Sentence::new()
460 .subject(subject("class", "Foo"))
461 .verb("convert", Tense::Past)
462 .preposition("into")
463 .object("Bar")
464 .render(&engine)
465 .unwrap();
466
467 assert_eq!(s, "The class Foo was converted into Bar");
468 }
469
470 #[test]
471 fn passive_present_tense() {
472 let engine = test_engine();
473 let s = Sentence::new()
474 .subject(subject("module", "Core"))
475 .verb("export", Tense::Present)
476 .clause(Clause::with_intro("").amount(5).noun("component"))
477 .render(&engine)
478 .unwrap();
479
480 assert_eq!(s, "The module Core is exported 5 components");
481 }
482
483 #[test]
484 fn passive_future_tense() {
485 let engine = test_engine();
486 let s = Sentence::new()
487 .subject(subject("interface", "Foo"))
488 .verb("deprecate", Tense::Future)
489 .render(&engine)
490 .unwrap();
491
492 assert_eq!(s, "The interface Foo will be deprecateed");
493 }
494
495 #[test]
496 fn clause_no_truncation_needed() {
497 let engine = test_engine();
498 let s = Sentence::new()
499 .subject(subject("method", "getData"))
500 .verb("delete", Tense::Past)
501 .clause(
502 Clause::which("impacts")
503 .amount(2)
504 .noun("caller")
505 .list(&["ComponentA", "ComponentB"]),
506 )
507 .render(&engine)
508 .unwrap();
509
510 assert_eq!(
511 s,
512 "The method getData was deleteed which impacts 2 callers [ComponentA and ComponentB]"
513 );
514 }
515}