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