use crate::application::cli::error::{CliError, CliResult};
use crate::application::cli::formatters::OutputFormatter;
use std::collections::HashMap;
pub trait WizardStep: Send + Sync {
fn name(&self) -> &str;
fn execute(&self, context: &mut WizardContext) -> CliResult<StepResult>;
fn can_skip(&self, _context: &WizardContext) -> bool {
false
}
fn description(&self) -> Option<&str> {
None
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StepResult {
Continue,
SkipTo(String),
Back,
Complete,
Cancel,
}
#[derive(Debug, Clone)]
pub struct WizardContext {
data: HashMap<String, String>,
resumable: bool,
completed_steps: Vec<String>,
}
impl WizardContext {
pub fn new() -> Self {
Self {
data: HashMap::new(),
resumable: false,
completed_steps: Vec::new(),
}
}
pub fn enable_resume(&mut self) {
self.resumable = true;
}
pub fn is_resumable(&self) -> bool {
self.resumable
}
pub fn set(&mut self, key: impl Into<String>, value: impl Into<String>) {
self.data.insert(key.into(), value.into());
}
pub fn get(&self, key: &str) -> Option<&str> {
self.data.get(key).map(|s| s.as_str())
}
pub fn contains(&self, key: &str) -> bool {
self.data.contains_key(key)
}
pub fn remove(&mut self, key: &str) -> Option<String> {
self.data.remove(key)
}
pub fn mark_completed(&mut self, step_name: impl Into<String>) {
self.completed_steps.push(step_name.into());
}
pub fn is_completed(&self, step_name: &str) -> bool {
self.completed_steps.contains(&step_name.to_string())
}
pub fn completed_steps(&self) -> &[String] {
&self.completed_steps
}
pub fn clear(&mut self) {
self.data.clear();
self.completed_steps.clear();
}
}
impl Default for WizardContext {
fn default() -> Self {
Self::new()
}
}
pub struct Wizard {
steps: Vec<Box<dyn WizardStep>>,
context: WizardContext,
formatter: OutputFormatter,
}
impl Wizard {
pub fn new() -> Self {
Self {
steps: Vec::new(),
context: WizardContext::new(),
formatter: OutputFormatter::new(),
}
}
pub fn add_step(mut self, step: Box<dyn WizardStep>) -> Self {
self.steps.push(step);
self
}
pub fn with_resume(mut self) -> Self {
self.context.enable_resume();
self
}
pub fn with_formatter(mut self, formatter: OutputFormatter) -> Self {
self.formatter = formatter;
self
}
pub fn run(mut self) -> CliResult<WizardContext> {
if self.steps.is_empty() {
return Err(CliError::execution("Wizard has no steps"));
}
let mut current_index = 0;
let mut history: Vec<usize> = Vec::new();
while current_index < self.steps.len() {
let step = &self.steps[current_index];
if self.context.is_resumable() && self.context.is_completed(step.name()) {
self.formatter
.info(&format!("Skipping completed step: {}", step.name()));
current_index += 1;
continue;
}
if step.can_skip(&self.context) {
self.formatter
.info(&format!("Skipping step: {}", step.name()));
current_index += 1;
continue;
}
if let Some(desc) = step.description() {
self.formatter
.section(&format!("{}: {}", step.name(), desc));
} else {
self.formatter.section(step.name());
}
match step.execute(&mut self.context) {
Ok(StepResult::Continue) => {
self.context.mark_completed(step.name().to_string());
history.push(current_index);
current_index += 1;
}
Ok(StepResult::SkipTo(target)) => {
if let Some(pos) = self.steps.iter().position(|s| s.name() == target) {
self.context.mark_completed(step.name().to_string());
history.push(current_index);
current_index = pos;
} else {
return Err(CliError::execution(format!("Step '{}' not found", target)));
}
}
Ok(StepResult::Back) => {
if let Some(prev) = history.pop() {
current_index = prev;
} else {
self.formatter.warning("Already at first step");
}
}
Ok(StepResult::Complete) => {
self.context.mark_completed(step.name().to_string());
break;
}
Ok(StepResult::Cancel) => {
return Err(CliError::Cancelled);
}
Err(e) => {
return Err(e);
}
}
}
Ok(self.context)
}
pub fn context(&self) -> &WizardContext {
&self.context
}
pub fn context_mut(&mut self) -> &mut WizardContext {
&mut self.context
}
}
impl Default for Wizard {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
struct TestStep {
name: String,
result: StepResult,
}
impl TestStep {
fn new(name: &str, result: StepResult) -> Box<Self> {
Box::new(Self {
name: name.to_string(),
result,
})
}
}
impl WizardStep for TestStep {
fn name(&self) -> &str {
&self.name
}
fn execute(&self, context: &mut WizardContext) -> CliResult<StepResult> {
context.set(self.name.clone(), "executed");
Ok(self.result.clone())
}
}
#[test]
fn test_wizard_context() {
let mut context = WizardContext::new();
context.set("key", "value");
assert_eq!(context.get("key"), Some("value"));
assert!(context.contains("key"));
assert_eq!(context.remove("key"), Some("value".to_string()));
assert!(!context.contains("key"));
}
#[test]
fn test_wizard_completed_steps() {
let mut context = WizardContext::new();
context.mark_completed("step1");
context.mark_completed("step2");
assert!(context.is_completed("step1"));
assert!(context.is_completed("step2"));
assert!(!context.is_completed("step3"));
}
#[test]
fn test_wizard_execution() {
let wizard = Wizard::new()
.add_step(TestStep::new("step1", StepResult::Continue))
.add_step(TestStep::new("step2", StepResult::Continue))
.add_step(TestStep::new("step3", StepResult::Complete));
let context = wizard.run().unwrap();
assert!(context.is_completed("step1"));
assert!(context.is_completed("step2"));
assert!(context.is_completed("step3"));
}
#[test]
fn test_wizard_cancel() {
let wizard = Wizard::new()
.add_step(TestStep::new("step1", StepResult::Continue))
.add_step(TestStep::new("step2", StepResult::Cancel));
let result = wizard.run();
assert!(matches!(result, Err(CliError::Cancelled)));
}
}