tquest/
lib.rs

1mod ui;
2
3mod questionaire;
4
5mod controller;
6
7mod persistence;
8
9use controller::QuestionaireController;
10use anyhow::{anyhow, Result};
11use ui::{ProceedScreenResult, QuestionaireView};
12
13use std::{fs, path::Path};
14
15pub use persistence::{FileQuestionairePersistence, QuestionairePersistence};
16pub use questionaire::{Questionaire, QuestionaireBuilder, QuestionaireEntry, QuestionEntry, RepeatedQuestionEntry, 
17    SubBlock, EntryType, StringEntry, IntEntry, FloatEntry, BoolEntry, 
18    OptionEntry, BlockAnswer, QuestionAnswerInput, AnswerEntry, RepeatedQuestionAnswers, QuestionAnswer};
19pub use controller::QuestionaireResult;
20pub use ui::Ui;
21
22const PERSISTENCE_FILE_NAME: &str = "tquest.tmp";
23const TITLE: &str = "A short questionaire";
24
25pub struct QuestionaireRunner {
26    persistence_file: String,
27    imported_data: Option<Vec<QuestionAnswer>>,
28    title: String,
29    autofil: bool,
30    questionaire: Questionaire,
31}
32
33impl QuestionaireRunner {
34    pub fn builder() -> QuestionaireRunnerBuilder {
35        QuestionaireRunnerBuilder::default()
36    }
37
38    fn check_for_old_persistence_file(&self) -> bool {
39        let p = Path::new(&self.persistence_file);
40        p.is_file()
41    }
42    
43    fn remove_persistence_file(&self) {
44        let p = Path::new(&self.persistence_file);
45        if p.is_file() {
46            let _ = fs::remove_file(p);
47        }
48    }
49
50    fn handle_persistence_file(&self, persistence: &mut FileQuestionairePersistence, ui: &mut Ui) -> Result<bool> {
51        if self.check_for_old_persistence_file() {
52            let r = ui.show_proceed_screen("00", "Found persistence file, for a questionaire. Do you want to load it to proceed where you stopped last time?", None, 0, 0, None);
53            match r {
54                Ok(res) => {
55                    match res {
56                        ProceedScreenResult::Canceled => {
57                            return Err(anyhow!("Canceled by user"));
58                        },
59                        ProceedScreenResult::Proceeded(p) => {
60                            if p {
61                                let _ = persistence.load(Some(&self.persistence_file));
62                            }
63                        },
64                    }
65                },
66                Err(_) => {
67                    return Err(anyhow!("error while processing"));
68                },
69            };
70            Ok(true)
71        } else {
72            Ok(false)
73        }
74    }
75
76    pub fn run(&self) -> Result<QuestionaireResult> {    
77        let mut ui: Ui = Ui::new()?;
78        ui.print_title(&self.title);
79        let mut persistence = FileQuestionairePersistence::new(&self.persistence_file)?;
80    
81        let mut persistence_file_exists: bool = false;
82        if self.imported_data.is_some() {
83            persistence.import(self.imported_data.as_ref().unwrap());
84        } else {
85            persistence_file_exists = self.handle_persistence_file(&mut persistence, &mut ui)?;
86        }
87    
88        ui.fast_forward = self.autofil;
89        let mut c: QuestionaireController<Ui, FileQuestionairePersistence> = QuestionaireController::new(&self.questionaire, ui, persistence);
90        if persistence_file_exists {
91            self.remove_persistence_file();
92        }
93        c.run()
94    }
95}
96
97#[derive(Default)]
98pub struct QuestionaireRunnerBuilder {
99    persistence_file: Option<String>,
100    title: Option<String>,
101    autofil: bool,
102    imported_data: Option<Vec<QuestionAnswer>>,
103}
104
105impl QuestionaireRunnerBuilder {
106    pub fn persistence_file(&mut self, v: &str) -> &mut Self {
107        self.persistence_file = Some(v.to_string());
108        self
109    }
110    pub fn title(&mut self, v: &str) -> &mut Self {
111        self.title = Some(v.to_string());
112        self
113    }
114    pub fn imported_data(&mut self, v: Option<Vec<QuestionAnswer>>) -> &mut Self {
115        self.imported_data = v;
116        self
117    }
118    pub fn autofil(&mut self, v: bool) -> &mut Self {
119        self.autofil = v;
120        self
121    }
122    pub fn build(&self, questionaire: Questionaire) -> Result<QuestionaireRunner> {
123        let persistence_file = if let Some (pf) = self.persistence_file.as_ref() {
124            pf.to_string()
125        } else {
126            PERSISTENCE_FILE_NAME.to_string()
127        };
128        let title = if let Some (t) = self.title.as_ref() {
129            t.to_string()
130        } else {
131            TITLE.to_string()
132        };
133        let imported_data = if let Some(id) = self.imported_data.as_ref() {
134            Some(id.clone())
135        } else {
136            None
137        };
138        Ok(QuestionaireRunner {
139            persistence_file,
140            title,
141            autofil: self.autofil,
142            imported_data,
143            questionaire,
144        })
145    }
146
147}
148
149
150#[cfg(test)]
151mod tests {
152    use crate::*;
153
154    #[test]
155    #[ignore]
156    fn test_fast_forward() {
157        use test_helper::create_complex_questionaire;
158        // Thread start - here
159        let tmp_file = "tmp/test_persistence.tmp";
160        let source_file = "res/tquest.tmp";
161        let mut persistence = FileQuestionairePersistence::new(tmp_file).unwrap();
162        persistence.load(Some(source_file)).expect("error while loading 'res/tquest.tmp'");
163        let mut ui: Ui = Ui::new().expect("error while crating UI");
164        ui.fast_forward =  true;
165
166        let p = Path::new(tmp_file);
167        if p.is_file() {
168            let _ = fs::remove_file(p);
169        }
170
171        // Channel for communication
172        let (tx, rx) = std::sync::mpsc::channel::<()>();
173        std::thread::spawn(move || {
174            let questionaire = create_complex_questionaire();
175            let mut c: QuestionaireController<Ui, FileQuestionairePersistence> = QuestionaireController::new(&questionaire, ui, persistence);
176            let _ = c.run();
177            tx.send(()).expect("Error sending termination signal"); // Signal thread termination
178        });
179
180    // Wait for thread or timeout
181        let result = rx.recv_timeout(std::time::Duration::from_secs(2));
182
183        // Handle results
184        match result {
185            Ok(_) => panic!("Thread finished execution within timeout"),
186            Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
187                // TODO compare the input with the output file ...
188                if let Ok(v) = test_helper::is_same_file(tmp_file, source_file) {
189                    assert!(v);
190                } else {
191                    panic!("something went wrong");
192                }
193            },
194            Err(_) => panic!("Error receiving termination signal"),
195        }
196        
197    }
198
199
200}
201
202#[cfg(test)]
203mod test_helper {
204    use crate::{
205        questionaire::{Questionaire, SubBlock, StringEntry, QuestionEntry, 
206            EntryType, OptionEntry},
207        QuestionaireEntry,
208    };
209
210    use std::{io::Write, path::Path};
211    use std::fs::{File, remove_file};
212    use std::io::{BufReader, Read};
213
214
215
216    #[test]
217    fn test_json() {
218        let p = Path::new("tmp");
219        if ! p.is_dir() {
220            let _ = std::fs::create_dir(p);
221        }
222        let pf = Path::new("tmp/test.json");
223        if p.is_file() {
224            let _ = remove_file(pf);
225        }
226
227        assert!(!p.is_file());
228    
229        let q = create_complex_questionaire();
230        let json_string = serde_json::to_string(&q).unwrap();
231
232        let mut file = File::create("tmp/test.json").unwrap();
233        let _ = file.write_all(json_string.as_bytes());
234
235        assert!(pf.is_file());
236
237        let q2: Questionaire = serde_json::from_str(&json_string).unwrap();
238        assert_eq!(q, q2);
239    }
240    
241    pub fn create_small_questionaire() -> Questionaire {
242        Questionaire::builder()
243            .id("id00")
244            .start_text("In the following questionaire you will be asked about your family and things. Do you want to proceed?")
245            .end_text("All data are collected. Do you want to process them?")
246            .questions(vec![
247                    QuestionaireEntry::Question (
248                        QuestionEntry::builder()
249                        .id("id01")
250                        .query_text("What's your name?")
251                        .entry_type(EntryType::String(
252                            StringEntry::builder()
253                            .min_length(2)
254                            .max_length(100)
255                            .build()
256                        ))
257                        .build()
258                    ),
259                    QuestionaireEntry::Question (
260                        QuestionEntry::builder()
261                        .query_text("What's your date of birth?")
262                        .id("id01")
263                        .help_text("Provide the date of birth in YYYY-MM-DD format")
264                        .entry_type(EntryType::String(
265                            StringEntry::builder()
266                            .regexp("\\d\\d\\d\\d-\\d\\d-\\d\\d")
267                            .build()
268                        ))
269                        .build()
270                    )
271                ])
272            .build()
273    }
274
275    pub fn create_complex_questionaire() -> Questionaire {
276        fn get_brother_questions(id_pre: &str) -> Vec<QuestionaireEntry> {
277            vec![
278                QuestionaireEntry::Question (
279                    QuestionEntry::builder()
280                    .id(&format!("{}_01", id_pre))
281                    .query_text("What's his name?")
282                    .entry_type(EntryType::String(
283                        StringEntry::builder()
284                        .min_length(2)
285                        .max_length(50)
286                        .build()
287                    ))
288                    .build()
289                ),
290                QuestionaireEntry::Question (
291                    QuestionEntry::builder()
292                    .id(&format!("{}_02", id_pre))
293                    .query_text("What's his date of birth?")
294                    .help_text("Provide the date of birth in YYYY-MM-DD format")
295                    .entry_type(EntryType::String(
296                        StringEntry::builder()
297                        .regexp("\\d\\d\\d\\d-\\d\\d-\\d\\d")
298                        .build()
299                    ))
300                    .build()
301                ),
302            ]
303        }
304        
305        fn get_sister_questions(id_pre: &str) -> Vec<QuestionaireEntry> {
306            vec![
307                QuestionaireEntry::Question (
308                    QuestionEntry::builder()
309                    .id(&format!("{}_01", id_pre))
310                    .query_text("What's her name?")
311                    .entry_type(EntryType::String(
312                        StringEntry::builder()
313                        .min_length(2)
314                        .max_length(50)
315                        .build()
316                    ))
317                    .build()
318                ),
319                QuestionaireEntry::Question (
320                    QuestionEntry::builder()
321                    .id(&format!("{}_02", id_pre))
322                    .query_text("What's her date of birth?")
323                    .help_text("Provide the date of birth in YYYY-MM-DD format")
324                    .entry_type(EntryType::String(
325                        StringEntry::builder()
326                        .regexp("\\d\\d\\d\\d-\\d\\d-\\d\\d")
327                        .build()
328                    ))
329                    .build()
330                ),
331            ]
332        }
333        
334        fn get_sibling_entries(id_pre: &str) -> Vec<QuestionaireEntry> {
335            let id_block_1 = format!("{}_01", id_pre);
336            let id_block_2 = format!("{}_02", id_pre);
337            vec![
338                QuestionaireEntry::Block(
339                    SubBlock::builder()
340                    .id(&id_block_1)
341                    .start_text("Do you have a sister?")
342                    .end_text("Do you have another sister?")
343                    .entries(get_sister_questions(&id_block_1))
344                    .loop_over_entries(true)
345                    .build()
346                ),
347                QuestionaireEntry::Block(
348                    SubBlock::builder()
349                    .id(&id_block_2)
350                    .start_text("Do you have a brother?")
351                    .end_text("Do you have another brother?")
352                    .entries(get_brother_questions(&id_block_2))
353                    .loop_over_entries(true)
354                    .build()
355                )
356            ]
357        }
358        
359        fn get_job_end_entries(id_pre: &str) -> Vec<QuestionaireEntry> {
360            vec![
361                QuestionaireEntry::Question(
362                    QuestionEntry::builder()
363                    .id(&format!("{}_01", id_pre))
364                    .query_text("What was your end date there?")
365                    .help_text("Provide the year and optional month in 'YYYY-MM' or 'YYYY' format.")
366                    .entry_type(EntryType::String(
367                        StringEntry::builder()
368                        .min_length(2)
369                        .max_length(100)
370                        .build()
371                    ))
372                    .build()
373                ),
374                QuestionaireEntry::Question(
375                    QuestionEntry::builder()
376                    .id(&format!("{}_02", id_pre))
377                    .query_text("Why did you leave the job?")
378                    .help_text("Provide the main reason for leaving")
379                    .entry_type(EntryType::Option(
380                        OptionEntry::builder()
381                        .options(vec![
382                            "I left by my own".to_string(),
383                            "I was laid off".to_string(),
384                            "Other reason".to_string(),
385                        ])
386                        .build()
387                    ))
388                    .build()
389                )
390            ]
391        }
392        
393        fn get_job_entries(id_pre: &str) -> Vec<QuestionaireEntry> {
394            vec![
395                QuestionaireEntry::Question(
396                    QuestionEntry::builder()
397                    .id(&format!("{}_01", id_pre))
398                    .query_text("What was the name of the company you worked for?")
399                    .entry_type(EntryType::String(
400                        StringEntry::builder()
401                        .min_length(2)
402                        .max_length(200)
403                        .build()
404                    ))
405                    .build()
406                ),
407                QuestionaireEntry::Question(
408                    QuestionEntry::builder()
409                    .id(&format!("{}_02", id_pre))
410                    .query_text("What was your job title?")
411                    .entry_type(EntryType::String(
412                        StringEntry::builder()
413                        .min_length(2)
414                        .max_length(100)
415                        .build()
416                    ))
417                    .build()
418                ),
419                QuestionaireEntry::Question(
420                    QuestionEntry::builder()
421                    .id(&format!("{}_03", id_pre))
422                    .query_text("What was your start date there?")
423                    .help_text("Provide the year and optional month in 'YYYY-MM' or 'YYYY' format")
424                    .entry_type(EntryType::String(
425                        StringEntry::builder()
426                        .min_length(2)
427                        .max_length(100)
428                        .build()
429                    ))
430                    .build()
431                ),
432                QuestionaireEntry::Block(
433                    SubBlock::builder()
434                    .id(&format!("{}_04", id_pre))
435                    .start_text("Have you finished your job there?")
436                    .entries(get_job_end_entries(&format!("{}_04", id_pre)))
437                    .build()
438                )
439            ]
440        }
441        
442        Questionaire::builder()
443        .id("id00")
444        .title("Fun Questionaire")
445        .start_text("In the following questionaire you will be asked about your family and things. Do you want to proceed?")
446        .end_text("All data are collected. Do you want to process them?")
447        .questions(
448            vec![
449                QuestionaireEntry::Question (
450                    QuestionEntry::builder()
451                    .id("id01")
452                    .query_text("What's your name?")
453                    .entry_type(EntryType::String(
454                        StringEntry::builder()
455                        .min_length(2)
456                        .max_length(100)
457                        .build()
458                    ))
459                    .build()
460                ),
461                QuestionaireEntry::Question (
462                    QuestionEntry::builder()
463                    .id("id02")
464                    .query_text("What's your date of birth?")
465                    .help_text("Provide the date of birth in YYYY-MM-DD format")
466                    .entry_type(EntryType::String(
467                        StringEntry::builder()
468                        .regexp("\\d\\d\\d\\d-\\d\\d-\\d\\d")
469                        .build()
470                    ))
471                    .build()
472                ),
473                QuestionaireEntry::Block(
474                    SubBlock::builder()
475                    .id("id03")
476                    .start_text("Do you have brothers or sisters?")
477                    .end_text("Do you have more brothers and sisters?")
478                    .entries(get_sibling_entries("id03"))
479                    .loop_over_entries(true)
480                    .build()
481                ),
482                QuestionaireEntry::Block(
483                    SubBlock::builder()
484                    .id("id04")
485                    .start_text("Have you already worked in a job?")
486                    .end_text("Have you worked in another job?")
487                    .entries(get_job_entries("id04"))
488                    .loop_over_entries(true)
489                    .build()
490                )
491            ]
492        )
493        .build()
494    }
495
496    pub fn is_same_file(file1: &str, file2: &str) -> Result<bool, std::io::Error> {
497      let mut f1 = BufReader::new(File::open(file1)?);
498      let mut f2 = BufReader::new(File::open(file2)?);
499      // Check file sizes first
500      if f1.get_ref().metadata()?.len() != f2.get_ref().metadata()?.len() {
501        return Ok(false);
502      }
503      let mut buf1 = [0; 4096]; // Read in chunks of 4096 bytes
504      let mut buf2 = [0; 4096];
505      loop {
506        let n1 = f1.read(&mut buf1)?;
507        let n2 = f2.read(&mut buf2)?;
508        if n1 != n2 || &buf1[..n1] != &buf2[..n2] {
509          return Ok(false);
510        }
511        if n1 == 0 {
512          return Ok(true);
513        }
514      }
515    }
516}