Skip to main content

reinhardt_forms/
wizard.rs

1use crate::form::{Form, FormError};
2use std::collections::HashMap;
3
4/// Type alias for wizard session data
5type WizardSessionData = HashMap<String, HashMap<String, serde_json::Value>>;
6
7/// Type alias for wizard step condition function
8type WizardConditionFn = Box<dyn Fn(&WizardSessionData) -> bool + Send + Sync>;
9
10/// FormWizard manages multi-step forms
11pub struct FormWizard {
12	steps: Vec<WizardStep>,
13	current_step: usize,
14	session_data: WizardSessionData,
15}
16
17/// A single step in the wizard
18pub struct WizardStep {
19	pub name: String,
20	pub form: Form,
21	pub condition: Option<WizardConditionFn>,
22}
23
24impl WizardStep {
25	/// Create a new wizard step
26	///
27	/// # Examples
28	///
29	/// ```
30	/// use reinhardt_forms::{WizardStep, Form};
31	///
32	/// let form = Form::new();
33	/// let step = WizardStep::new("step1".to_string(), form);
34	/// assert_eq!(step.name, "step1");
35	/// ```
36	pub fn new(name: String, form: Form) -> Self {
37		Self {
38			name,
39			form,
40			condition: None,
41		}
42	}
43	/// Add a condition for when this step should be available
44	///
45	/// # Examples
46	///
47	/// ```
48	/// use reinhardt_forms::{WizardStep, Form};
49	/// use std::collections::HashMap;
50	/// use serde_json::json;
51	///
52	/// let form = Form::new();
53	/// let step = WizardStep::new("step2".to_string(), form)
54	///     .with_condition(|data| {
55	///         data.get("step1").map_or(false, |step1_data| {
56	///             step1_data.get("age").and_then(|v| v.as_i64()).map_or(false, |age| age >= 18)
57	///         })
58	///     });
59	/// ```
60	pub fn with_condition<F>(mut self, condition: F) -> Self
61	where
62		F: Fn(&WizardSessionData) -> bool + Send + Sync + 'static,
63	{
64		self.condition = Some(Box::new(condition));
65		self
66	}
67	pub fn is_available(&self, session_data: &WizardSessionData) -> bool {
68		if let Some(condition) = &self.condition {
69			condition(session_data)
70		} else {
71			true
72		}
73	}
74}
75
76impl FormWizard {
77	/// Create a new form wizard
78	///
79	/// # Examples
80	///
81	/// ```
82	/// use reinhardt_forms::FormWizard;
83	///
84	/// let wizard = FormWizard::new("wizard".to_string());
85	/// assert_eq!(wizard.current_step(), 0);
86	/// assert!(wizard.steps().is_empty());
87	/// ```
88	pub fn new(_prefix: String) -> Self {
89		Self {
90			steps: vec![],
91			current_step: 0,
92			session_data: HashMap::new(),
93		}
94	}
95
96	pub fn steps(&self) -> &Vec<WizardStep> {
97		&self.steps
98	}
99	/// Add a step to the wizard
100	///
101	/// # Examples
102	///
103	/// ```
104	/// use reinhardt_forms::{FormWizard, WizardStep, Form};
105	///
106	/// let mut wizard = FormWizard::new("wizard".to_string());
107	/// let form = Form::new();
108	/// let step = WizardStep::new("step1".to_string(), form);
109	/// wizard.add_step(step);
110	/// assert_eq!(wizard.steps().len(), 1);
111	/// ```
112	pub fn add_step(&mut self, step: WizardStep) {
113		self.steps.push(step);
114	}
115	pub fn current_step(&self) -> usize {
116		self.current_step
117	}
118	pub fn current_step_name(&self) -> Option<&str> {
119		self.steps.get(self.current_step).map(|s| s.name.as_str())
120	}
121	pub fn current_form(&self) -> Option<&Form> {
122		self.steps.get(self.current_step).map(|s| &s.form)
123	}
124	pub fn current_form_mut(&mut self) -> Option<&mut Form> {
125		self.steps.get_mut(self.current_step).map(|s| &mut s.form)
126	}
127	pub fn total_steps(&self) -> usize {
128		self.steps.len()
129	}
130	pub fn is_first_step(&self) -> bool {
131		self.current_step == 0
132	}
133	pub fn is_last_step(&self) -> bool {
134		self.current_step + 1 >= self.steps.len()
135	}
136	/// Move to the next available step
137	///
138	/// # Examples
139	///
140	/// ```
141	/// use reinhardt_forms::{FormWizard, WizardStep, Form};
142	///
143	/// let mut wizard = FormWizard::new("wizard".to_string());
144	/// let form1 = Form::new();
145	/// let form2 = Form::new();
146	/// wizard.add_step(WizardStep::new("step1".to_string(), form1));
147	/// wizard.add_step(WizardStep::new("step2".to_string(), form2));
148	///
149	/// let result = wizard.next_step();
150	/// assert!(result.is_ok());
151	/// assert_eq!(wizard.current_step(), 1);
152	/// ```
153	pub fn next_step(&mut self) -> Result<(), String> {
154		if self.is_last_step() {
155			return Err("Already at last step".to_string());
156		}
157
158		// Find next available step
159		for i in (self.current_step + 1)..self.steps.len() {
160			if self.steps[i].is_available(&self.session_data) {
161				self.current_step = i;
162				return Ok(());
163			}
164		}
165
166		Err("No available next step".to_string())
167	}
168	/// Move to the previous step
169	///
170	/// # Examples
171	///
172	/// ```
173	/// use reinhardt_forms::{FormWizard, WizardStep, Form};
174	///
175	/// let mut wizard = FormWizard::new("wizard".to_string());
176	/// let form1 = Form::new();
177	/// let form2 = Form::new();
178	/// wizard.add_step(WizardStep::new("step1".to_string(), form1));
179	/// wizard.add_step(WizardStep::new("step2".to_string(), form2));
180	/// wizard.next_step().unwrap(); // Move to step 2
181	///
182	/// let result = wizard.previous_step();
183	/// assert!(result.is_ok());
184	/// assert_eq!(wizard.current_step(), 0);
185	/// ```
186	pub fn previous_step(&mut self) -> Result<(), String> {
187		if self.is_first_step() {
188			return Err("Already at first step".to_string());
189		}
190
191		// Find previous available step
192		for i in (0..self.current_step).rev() {
193			if self.steps[i].is_available(&self.session_data) {
194				self.current_step = i;
195				return Ok(());
196			}
197		}
198
199		Err("No available previous step".to_string())
200	}
201	/// Go to a specific step by name.
202	///
203	/// Forward navigation (to a step after the current one) requires that all
204	/// previous steps have been completed (i.e., their data has been saved to
205	/// the session). This prevents attackers from skipping required validation
206	/// steps such as terms acceptance or payment details.
207	///
208	/// Backward navigation (to a step before the current one) is always allowed,
209	/// enabling users to review and edit previous answers.
210	///
211	/// # Examples
212	///
213	/// ```
214	/// use reinhardt_forms::{FormWizard, WizardStep, Form};
215	/// use std::collections::HashMap;
216	/// use serde_json::json;
217	///
218	/// let mut wizard = FormWizard::new("wizard".to_string());
219	/// let form1 = Form::new();
220	/// let form2 = Form::new();
221	/// let form3 = Form::new();
222	/// wizard.add_step(WizardStep::new("step1".to_string(), form1));
223	/// wizard.add_step(WizardStep::new("step2".to_string(), form2));
224	/// wizard.add_step(WizardStep::new("step3".to_string(), form3));
225	///
226	/// // Forward navigation without completing previous steps is rejected
227	/// assert!(wizard.goto_step("step3").is_err());
228	///
229	/// // Complete step1 and step2 first
230	/// let mut data = HashMap::new();
231	/// data.insert("field".to_string(), json!("value"));
232	/// wizard.save_step_data(data.clone()).unwrap();
233	/// wizard.next_step().unwrap();
234	/// wizard.save_step_data(data).unwrap();
235	///
236	/// // Now forward navigation to step3 succeeds
237	/// assert!(wizard.goto_step("step3").is_ok());
238	/// ```
239	pub fn goto_step(&mut self, name: &str) -> Result<(), String> {
240		// Find the target step index
241		let target_index = self
242			.steps
243			.iter()
244			.position(|step| step.name == name && step.is_available(&self.session_data))
245			.ok_or_else(|| format!("Step '{}' not found or not available", name))?;
246
247		// Backward navigation is always allowed
248		if target_index <= self.current_step {
249			self.current_step = target_index;
250			return Ok(());
251		}
252
253		// Forward navigation: verify all steps between current and target have
254		// been completed (data saved in session)
255		for i in self.current_step..target_index {
256			let step_name = &self.steps[i].name;
257			if !self.session_data.contains_key(step_name) {
258				return Err(format!(
259					"Cannot skip to step '{}': step '{}' has not been completed",
260					name, step_name
261				));
262			}
263		}
264
265		self.current_step = target_index;
266		Ok(())
267	}
268	/// Save data for the current step
269	///
270	/// # Examples
271	///
272	/// ```
273	/// use reinhardt_forms::{FormWizard, WizardStep, Form};
274	/// use std::collections::HashMap;
275	/// use serde_json::json;
276	///
277	/// let mut wizard = FormWizard::new("wizard".to_string());
278	/// let form = Form::new();
279	/// wizard.add_step(WizardStep::new("step1".to_string(), form));
280	///
281	/// let mut data = HashMap::new();
282	/// data.insert("name".to_string(), json!("John"));
283	///
284	/// let result = wizard.save_step_data(data);
285	/// assert!(result.is_ok());
286	/// ```
287	pub fn save_step_data(
288		&mut self,
289		data: HashMap<String, serde_json::Value>,
290	) -> Result<(), FormError> {
291		if let Some(step) = self.steps.get(self.current_step) {
292			self.session_data.insert(step.name.clone(), data);
293			Ok(())
294		} else {
295			Err(FormError::Validation("Invalid step".to_string()))
296		}
297	}
298	pub fn get_all_data(&self) -> &HashMap<String, HashMap<String, serde_json::Value>> {
299		&self.session_data
300	}
301	pub fn get_step_data(&self, step_name: &str) -> Option<&HashMap<String, serde_json::Value>> {
302		self.session_data.get(step_name)
303	}
304	pub fn clear_data(&mut self) {
305		self.session_data.clear();
306		self.current_step = 0;
307	}
308	/// Process current step and move to next if valid
309	///
310	/// # Examples
311	///
312	/// ```
313	/// use reinhardt_forms::{FormWizard, WizardStep, Form};
314	/// use std::collections::HashMap;
315	/// use serde_json::json;
316	///
317	/// let mut wizard = FormWizard::new("wizard".to_string());
318	/// let form = Form::new();
319	/// wizard.add_step(WizardStep::new("step1".to_string(), form));
320	///
321	/// let mut data = HashMap::new();
322	/// data.insert("field".to_string(), json!("value"));
323	/// ```
324	pub fn process_step(
325		&mut self,
326		data: HashMap<String, serde_json::Value>,
327	) -> Result<bool, FormError> {
328		if let Some(form) = self.current_form_mut() {
329			form.bind(data.clone());
330
331			if form.is_valid() {
332				self.save_step_data(data)?;
333
334				if !self.is_last_step() {
335					self.next_step().map_err(FormError::Validation)?;
336					Ok(false) // Not done yet
337				} else {
338					Ok(true) // Wizard complete
339				}
340			} else {
341				Err(FormError::Validation("Form validation failed".to_string()))
342			}
343		} else {
344			Err(FormError::Validation("Invalid step".to_string()))
345		}
346	}
347	pub fn progress_percentage(&self) -> f32 {
348		if self.steps.is_empty() {
349			return 0.0;
350		}
351		((self.current_step + 1) as f32 / self.steps.len() as f32) * 100.0
352	}
353}
354
355#[cfg(test)]
356mod tests {
357	use super::*;
358	use crate::fields::CharField;
359
360	#[test]
361	fn test_wizard_basic() {
362		let mut wizard = FormWizard::new("registration".to_string());
363
364		let mut form1 = Form::new();
365		form1.add_field(Box::new(CharField::new("username".to_string())));
366		wizard.add_step(WizardStep::new("account".to_string(), form1));
367
368		let mut form2 = Form::new();
369		form2.add_field(Box::new(CharField::new("email".to_string())));
370		wizard.add_step(WizardStep::new("contact".to_string(), form2));
371
372		assert_eq!(wizard.total_steps(), 2);
373		assert_eq!(wizard.current_step(), 0);
374		assert_eq!(wizard.current_step_name(), Some("account"));
375		assert!(wizard.is_first_step());
376		assert!(!wizard.is_last_step());
377	}
378
379	#[test]
380	fn test_wizard_navigation() {
381		let mut wizard = FormWizard::new("test".to_string());
382
383		for i in 1..=3 {
384			let mut form = Form::new();
385			form.add_field(Box::new(CharField::new(format!("field{}", i))));
386			wizard.add_step(WizardStep::new(format!("step{}", i), form));
387		}
388
389		assert_eq!(wizard.current_step(), 0);
390
391		wizard.next_step().unwrap();
392		assert_eq!(wizard.current_step(), 1);
393
394		wizard.next_step().unwrap();
395		assert_eq!(wizard.current_step(), 2);
396		assert!(wizard.is_last_step());
397
398		wizard.previous_step().unwrap();
399		assert_eq!(wizard.current_step(), 1);
400	}
401
402	#[test]
403	fn test_wizard_conditional_step() {
404		let mut wizard = FormWizard::new("test".to_string());
405
406		let mut form1 = Form::new();
407		form1.add_field(Box::new(CharField::new("type".to_string())));
408		wizard.add_step(WizardStep::new("type_selection".to_string(), form1));
409
410		let mut form2 = Form::new();
411		form2.add_field(Box::new(CharField::new("premium_field".to_string())));
412		let step2 = WizardStep::new("premium".to_string(), form2).with_condition(|data| {
413			data.get("type_selection")
414				.and_then(|d| d.get("type"))
415				.and_then(|v| v.as_str())
416				.map(|s| s == "premium")
417				.unwrap_or(false)
418		});
419		wizard.add_step(step2);
420
421		// Initially step 2 is not available
422		assert!(!wizard.steps[1].is_available(&wizard.session_data));
423
424		// Add data that makes step 2 available
425		let mut data = HashMap::new();
426		data.insert("type".to_string(), serde_json::json!("premium"));
427		wizard.save_step_data(data).unwrap();
428
429		assert!(wizard.steps[1].is_available(&wizard.session_data));
430	}
431
432	#[test]
433	fn test_wizard_progress() {
434		let mut wizard = FormWizard::new("test".to_string());
435
436		for i in 1..=4 {
437			let mut form = Form::new();
438			form.add_field(Box::new(CharField::new(format!("field{}", i))));
439			wizard.add_step(WizardStep::new(format!("step{}", i), form));
440		}
441
442		assert_eq!(wizard.progress_percentage(), 25.0); // Step 1/4
443
444		wizard.next_step().unwrap();
445		assert_eq!(wizard.progress_percentage(), 50.0); // Step 2/4
446
447		wizard.next_step().unwrap();
448		assert_eq!(wizard.progress_percentage(), 75.0); // Step 3/4
449
450		wizard.next_step().unwrap();
451		assert_eq!(wizard.progress_percentage(), 100.0); // Step 4/4
452	}
453
454	#[test]
455	fn test_wizard_goto_step_backward_always_allowed() {
456		let mut wizard = FormWizard::new("test".to_string());
457
458		for i in 1..=3 {
459			let mut form = Form::new();
460			form.add_field(Box::new(CharField::new(format!("field{}", i))));
461			wizard.add_step(WizardStep::new(format!("step{}", i), form));
462		}
463
464		// Complete steps to advance
465		let mut data = HashMap::new();
466		data.insert("field1".to_string(), serde_json::json!("value"));
467		wizard.save_step_data(data.clone()).unwrap();
468		wizard.next_step().unwrap();
469		data.clear();
470		data.insert("field2".to_string(), serde_json::json!("value"));
471		wizard.save_step_data(data).unwrap();
472		wizard.next_step().unwrap();
473		assert_eq!(wizard.current_step(), 2);
474
475		// Backward navigation is always allowed
476		wizard.goto_step("step1").unwrap();
477		assert_eq!(wizard.current_step(), 0);
478		assert_eq!(wizard.current_step_name(), Some("step1"));
479	}
480
481	#[test]
482	fn test_wizard_goto_step_forward_requires_completed_steps() {
483		let mut wizard = FormWizard::new("test".to_string());
484
485		for i in 1..=3 {
486			let mut form = Form::new();
487			form.add_field(Box::new(CharField::new(format!("field{}", i))));
488			wizard.add_step(WizardStep::new(format!("step{}", i), form));
489		}
490
491		// Forward navigation without completing prior steps should fail
492		let result = wizard.goto_step("step3");
493		assert!(result.is_err());
494		assert_eq!(wizard.current_step(), 0);
495	}
496
497	#[test]
498	fn test_wizard_goto_step_forward_after_completing_steps() {
499		let mut wizard = FormWizard::new("test".to_string());
500
501		for i in 1..=3 {
502			let mut form = Form::new();
503			form.add_field(Box::new(CharField::new(format!("field{}", i))));
504			wizard.add_step(WizardStep::new(format!("step{}", i), form));
505		}
506
507		// Complete step1
508		let mut data = HashMap::new();
509		data.insert("field1".to_string(), serde_json::json!("value1"));
510		wizard.save_step_data(data).unwrap();
511
512		// Move to step2 and complete it
513		wizard.next_step().unwrap();
514		let mut data2 = HashMap::new();
515		data2.insert("field2".to_string(), serde_json::json!("value2"));
516		wizard.save_step_data(data2).unwrap();
517
518		// Now forward navigation to step3 should succeed
519		wizard.goto_step("step3").unwrap();
520		assert_eq!(wizard.current_step(), 2);
521		assert_eq!(wizard.current_step_name(), Some("step3"));
522	}
523}