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 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 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"); });
179
180 let result = rx.recv_timeout(std::time::Duration::from_secs(2));
182
183 match result {
185 Ok(_) => panic!("Thread finished execution within timeout"),
186 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
187 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 if f1.get_ref().metadata()?.len() != f2.get_ref().metadata()?.len() {
501 return Ok(false);
502 }
503 let mut buf1 = [0; 4096]; 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}