elicitor_types/
responses.rs

1use std::collections::HashMap;
2
3use crate::{ResponsePath, ResponseValue};
4
5/// Error type for response access operations.
6#[derive(Debug, thiserror::Error)]
7pub enum ResponseError {
8    #[error("Missing response for path: {0}")]
9    MissingPath(ResponsePath),
10
11    #[error("Type mismatch at path '{path}': expected {expected}, got {actual}")]
12    TypeMismatch {
13        path: ResponsePath,
14        expected: &'static str,
15        actual: &'static str,
16    },
17}
18
19/// Collected responses from a survey.
20///
21/// Uses `ResponsePath` as keys to support hierarchical field access.
22/// Response paths are flat (not nested) - a nested field like `address.street`
23/// is stored with the key `ResponsePath::from("address.street")`.
24#[derive(Debug, Clone, Default)]
25pub struct Responses {
26    values: HashMap<ResponsePath, ResponseValue>,
27}
28
29impl Responses {
30    /// Create a new empty responses collection.
31    pub fn new() -> Self {
32        Self {
33            values: HashMap::new(),
34        }
35    }
36
37    /// Insert a response value at the given path.
38    pub fn insert(&mut self, path: impl Into<ResponsePath>, value: impl Into<ResponseValue>) {
39        self.values.insert(path.into(), value.into());
40    }
41
42    /// Get a response value at the given path.
43    pub fn get(&self, path: &ResponsePath) -> Option<&ResponseValue> {
44        self.values.get(path)
45    }
46
47    /// Check if a response exists at the given path.
48    pub fn contains(&self, path: &ResponsePath) -> bool {
49        self.values.contains_key(path)
50    }
51
52    /// Remove a response at the given path.
53    pub fn remove(&mut self, path: &ResponsePath) -> Option<ResponseValue> {
54        self.values.remove(path)
55    }
56
57    /// Get an iterator over all path-value pairs.
58    pub fn iter(&self) -> impl Iterator<Item = (&ResponsePath, &ResponseValue)> {
59        self.values.iter()
60    }
61
62    /// Get the number of responses.
63    pub fn len(&self) -> usize {
64        self.values.len()
65    }
66
67    /// Check if there are no responses.
68    pub fn is_empty(&self) -> bool {
69        self.values.is_empty()
70    }
71
72    /// Merge another responses collection into this one.
73    pub fn extend(&mut self, other: Responses) {
74        self.values.extend(other.values);
75    }
76
77    /// Filter responses to only those with the given path prefix, removing the prefix from keys.
78    ///
79    /// This is used when reconstructing nested types - extract responses for a nested
80    /// struct and strip the prefix so the nested `from_responses` sees root-level paths.
81    ///
82    /// # Example
83    /// ```
84    /// use elicitor_types::{Responses, ResponsePath, ResponseValue};
85    ///
86    /// let mut responses = Responses::new();
87    /// responses.insert("address.street", "123 Main St");
88    /// responses.insert("address.city", "Springfield");
89    /// responses.insert("name", "Alice");
90    ///
91    /// let address_responses = responses.filter_prefix(&ResponsePath::new("address"));
92    /// assert!(address_responses.get(&ResponsePath::new("street")).is_some());
93    /// assert!(address_responses.get(&ResponsePath::new("city")).is_some());
94    /// assert!(address_responses.get(&ResponsePath::new("name")).is_none());
95    /// ```
96    pub fn filter_prefix(&self, prefix: &ResponsePath) -> Self {
97        let mut filtered = Responses::new();
98        for (path, value) in &self.values {
99            if let Some(stripped) = path.strip_path_prefix(prefix) {
100                filtered.values.insert(stripped, value.clone());
101            }
102        }
103        filtered
104    }
105
106    // === Convenience accessors ===
107
108    /// Get a string value at the given path.
109    pub fn get_string(&self, path: &ResponsePath) -> Result<&str, ResponseError> {
110        match self.get(path) {
111            Some(ResponseValue::String(s)) => Ok(s),
112            Some(other) => Err(ResponseError::TypeMismatch {
113                path: path.clone(),
114                expected: "String",
115                actual: other.type_name(),
116            }),
117            None => Err(ResponseError::MissingPath(path.clone())),
118        }
119    }
120
121    /// Get an integer value at the given path.
122    pub fn get_int(&self, path: &ResponsePath) -> Result<i64, ResponseError> {
123        match self.get(path) {
124            Some(ResponseValue::Int(i)) => Ok(*i),
125            Some(other) => Err(ResponseError::TypeMismatch {
126                path: path.clone(),
127                expected: "Int",
128                actual: other.type_name(),
129            }),
130            None => Err(ResponseError::MissingPath(path.clone())),
131        }
132    }
133
134    /// Get a float value at the given path.
135    pub fn get_float(&self, path: &ResponsePath) -> Result<f64, ResponseError> {
136        match self.get(path) {
137            Some(ResponseValue::Float(f)) => Ok(*f),
138            Some(other) => Err(ResponseError::TypeMismatch {
139                path: path.clone(),
140                expected: "Float",
141                actual: other.type_name(),
142            }),
143            None => Err(ResponseError::MissingPath(path.clone())),
144        }
145    }
146
147    /// Get a boolean value at the given path.
148    pub fn get_bool(&self, path: &ResponsePath) -> Result<bool, ResponseError> {
149        match self.get(path) {
150            Some(ResponseValue::Bool(b)) => Ok(*b),
151            Some(other) => Err(ResponseError::TypeMismatch {
152                path: path.clone(),
153                expected: "Bool",
154                actual: other.type_name(),
155            }),
156            None => Err(ResponseError::MissingPath(path.clone())),
157        }
158    }
159
160    /// Get a chosen variant index at the given path.
161    pub fn get_chosen_variant(&self, path: &ResponsePath) -> Result<usize, ResponseError> {
162        match self.get(path) {
163            Some(ResponseValue::ChosenVariant(idx)) => Ok(*idx),
164            Some(other) => Err(ResponseError::TypeMismatch {
165                path: path.clone(),
166                expected: "ChosenVariant",
167                actual: other.type_name(),
168            }),
169            None => Err(ResponseError::MissingPath(path.clone())),
170        }
171    }
172
173    /// Get chosen variant indices at the given path.
174    pub fn get_chosen_variants(&self, path: &ResponsePath) -> Result<&[usize], ResponseError> {
175        match self.get(path) {
176            Some(ResponseValue::ChosenVariants(indices)) => Ok(indices),
177            Some(other) => Err(ResponseError::TypeMismatch {
178                path: path.clone(),
179                expected: "ChosenVariants",
180                actual: other.type_name(),
181            }),
182            None => Err(ResponseError::MissingPath(path.clone())),
183        }
184    }
185
186    /// Get a string list at the given path.
187    pub fn get_string_list(&self, path: &ResponsePath) -> Result<&[String], ResponseError> {
188        match self.get(path) {
189            Some(ResponseValue::StringList(list)) => Ok(list),
190            Some(other) => Err(ResponseError::TypeMismatch {
191                path: path.clone(),
192                expected: "StringList",
193                actual: other.type_name(),
194            }),
195            None => Err(ResponseError::MissingPath(path.clone())),
196        }
197    }
198
199    /// Get an integer list at the given path.
200    pub fn get_int_list(&self, path: &ResponsePath) -> Result<&[i64], ResponseError> {
201        match self.get(path) {
202            Some(ResponseValue::IntList(list)) => Ok(list),
203            Some(other) => Err(ResponseError::TypeMismatch {
204                path: path.clone(),
205                expected: "IntList",
206                actual: other.type_name(),
207            }),
208            None => Err(ResponseError::MissingPath(path.clone())),
209        }
210    }
211
212    /// Get a float list at the given path.
213    pub fn get_float_list(&self, path: &ResponsePath) -> Result<&[f64], ResponseError> {
214        match self.get(path) {
215            Some(ResponseValue::FloatList(list)) => Ok(list),
216            Some(other) => Err(ResponseError::TypeMismatch {
217                path: path.clone(),
218                expected: "FloatList",
219                actual: other.type_name(),
220            }),
221            None => Err(ResponseError::MissingPath(path.clone())),
222        }
223    }
224
225    /// Check if a response at the given path has a non-empty value.
226    ///
227    /// This is used for `Option<T>` fields: returns `false` if the response
228    /// is missing OR if it's an empty string (user skipped the optional field).
229    pub fn has_value(&self, path: &ResponsePath) -> bool {
230        match self.get(path) {
231            Some(ResponseValue::String(s)) => !s.is_empty(),
232            Some(_) => true,
233            None => false,
234        }
235    }
236}
237
238impl IntoIterator for Responses {
239    type Item = (ResponsePath, ResponseValue);
240    type IntoIter = std::collections::hash_map::IntoIter<ResponsePath, ResponseValue>;
241
242    fn into_iter(self) -> Self::IntoIter {
243        self.values.into_iter()
244    }
245}
246
247impl<'a> IntoIterator for &'a Responses {
248    type Item = (&'a ResponsePath, &'a ResponseValue);
249    type IntoIter = std::collections::hash_map::Iter<'a, ResponsePath, ResponseValue>;
250
251    fn into_iter(self) -> Self::IntoIter {
252        self.values.iter()
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    #[test]
261    fn insert_and_get() {
262        let mut responses = Responses::new();
263        responses.insert("name", "Alice");
264        responses.insert("age", ResponseValue::Int(30));
265
266        assert_eq!(
267            responses.get_string(&ResponsePath::new("name")).unwrap(),
268            "Alice"
269        );
270        assert_eq!(responses.get_int(&ResponsePath::new("age")).unwrap(), 30);
271    }
272
273    #[test]
274    fn filter_prefix() {
275        let mut responses = Responses::new();
276        responses.insert("address.street", "123 Main St");
277        responses.insert("address.city", "Springfield");
278        responses.insert("name", "Alice");
279
280        let filtered = responses.filter_prefix(&ResponsePath::new("address"));
281        assert_eq!(filtered.len(), 2);
282        assert_eq!(
283            filtered.get_string(&ResponsePath::new("street")).unwrap(),
284            "123 Main St"
285        );
286        assert_eq!(
287            filtered.get_string(&ResponsePath::new("city")).unwrap(),
288            "Springfield"
289        );
290    }
291
292    #[test]
293    fn type_mismatch_error() {
294        let mut responses = Responses::new();
295        responses.insert("age", ResponseValue::Int(30));
296
297        let result = responses.get_string(&ResponsePath::new("age"));
298        assert!(matches!(result, Err(ResponseError::TypeMismatch { .. })));
299    }
300}