derive_wizard/
backend.rs

1use crate::interview::{Interview, Section};
2use std::collections::HashMap;
3
4#[cfg(feature = "requestty-backend")]
5pub mod requestty_backend;
6
7#[cfg(feature = "dialoguer-backend")]
8pub mod dialoguer_backend;
9
10#[cfg(feature = "egui-backend")]
11pub mod egui_backend;
12
13/// Represents the answers collected from an interview
14#[derive(Debug, Clone, Default)]
15pub struct Answers {
16    values: HashMap<String, AnswerValue>,
17}
18
19/// A single answer value
20#[derive(Debug, Clone)]
21pub enum AnswerValue {
22    String(String),
23    Int(i64),
24    Float(f64),
25    Bool(bool),
26    Nested(Box<Answers>),
27}
28
29impl Answers {
30    pub fn new() -> Self {
31        Self {
32            values: HashMap::new(),
33        }
34    }
35
36    pub fn insert(&mut self, key: String, value: AnswerValue) {
37        self.values.insert(key, value);
38    }
39
40    pub fn get(&self, key: &str) -> Option<&AnswerValue> {
41        self.values.get(key)
42    }
43
44    pub fn merge(&mut self, other: Answers) {
45        self.values.extend(other.values);
46    }
47
48    pub fn as_string(&self, key: &str) -> Result<String, AnswerError> {
49        match self.get(key) {
50            Some(AnswerValue::String(s)) => Ok(s.clone()),
51            Some(_) => Err(AnswerError::TypeMismatch {
52                key: key.to_string(),
53                expected: "String",
54            }),
55            None => Err(AnswerError::MissingKey(key.to_string())),
56        }
57    }
58
59    pub fn as_int(&self, key: &str) -> Result<i64, AnswerError> {
60        match self.get(key) {
61            Some(AnswerValue::Int(i)) => Ok(*i),
62            Some(_) => Err(AnswerError::TypeMismatch {
63                key: key.to_string(),
64                expected: "Int",
65            }),
66            None => Err(AnswerError::MissingKey(key.to_string())),
67        }
68    }
69
70    pub fn as_float(&self, key: &str) -> Result<f64, AnswerError> {
71        match self.get(key) {
72            Some(AnswerValue::Float(f)) => Ok(*f),
73            Some(_) => Err(AnswerError::TypeMismatch {
74                key: key.to_string(),
75                expected: "Float",
76            }),
77            None => Err(AnswerError::MissingKey(key.to_string())),
78        }
79    }
80
81    pub fn as_bool(&self, key: &str) -> Result<bool, AnswerError> {
82        match self.get(key) {
83            Some(AnswerValue::Bool(b)) => Ok(*b),
84            Some(_) => Err(AnswerError::TypeMismatch {
85                key: key.to_string(),
86                expected: "Bool",
87            }),
88            None => Err(AnswerError::MissingKey(key.to_string())),
89        }
90    }
91
92    pub fn as_nested(&self, key: &str) -> Result<&Answers, AnswerError> {
93        match self.get(key) {
94            Some(AnswerValue::Nested(nested)) => Ok(nested),
95            Some(_) => Err(AnswerError::TypeMismatch {
96                key: key.to_string(),
97                expected: "Nested",
98            }),
99            None => Err(AnswerError::MissingKey(key.to_string())),
100        }
101    }
102}
103
104#[derive(Debug, thiserror::Error)]
105pub enum AnswerError {
106    #[error("Missing key: {0}")]
107    MissingKey(String),
108
109    #[error("Type mismatch for key '{key}': expected {expected}")]
110    TypeMismatch { key: String, expected: &'static str },
111}
112
113#[derive(Debug, thiserror::Error)]
114pub enum BackendError {
115    #[error("Answer error: {0}")]
116    Answer(#[from] AnswerError),
117
118    #[error("Validation error: {0}")]
119    Validation(String),
120
121    #[error("Execution error: {0}")]
122    ExecutionError(String),
123
124    #[error("IO error: {0}")]
125    Io(#[from] std::io::Error),
126
127    #[error("Backend-specific error: {0}")]
128    Custom(String),
129}
130
131/// Trait for interview execution backends
132pub trait InterviewBackend {
133    /// Execute an interview and return the collected answers
134    fn execute(&self, interview: &Interview) -> Result<Answers, BackendError>;
135
136    /// Execute a single section (optional, has default implementation)
137    fn execute_section(&self, section: &Section) -> Result<Answers, BackendError> {
138        // Default implementation - subclasses can override
139        let _ = section;
140        Err(BackendError::Custom(
141            "execute_section not implemented".to_string(),
142        ))
143    }
144}
145
146/// Test backend that returns predefined answers
147#[derive(Debug, Default)]
148pub struct TestBackend {
149    answers: Answers,
150}
151
152impl TestBackend {
153    pub fn new() -> Self {
154        Self::default()
155    }
156
157    pub fn with_string(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
158        self.answers
159            .insert(key.into(), AnswerValue::String(value.into()));
160        self
161    }
162
163    pub fn with_int(mut self, key: impl Into<String>, value: i64) -> Self {
164        self.answers.insert(key.into(), AnswerValue::Int(value));
165        self
166    }
167
168    pub fn with_float(mut self, key: impl Into<String>, value: f64) -> Self {
169        self.answers.insert(key.into(), AnswerValue::Float(value));
170        self
171    }
172
173    pub fn with_bool(mut self, key: impl Into<String>, value: bool) -> Self {
174        self.answers.insert(key.into(), AnswerValue::Bool(value));
175        self
176    }
177}
178
179impl InterviewBackend for TestBackend {
180    fn execute(&self, _interview: &Interview) -> Result<Answers, BackendError> {
181        // Simply return the predefined answers
182        Ok(self.answers.clone())
183    }
184}