Skip to main content

elicitation/primitives/
errors.rs

1//! Error type generators for testing.
2//!
3//! Sometimes you need to test error handling without triggering actual failures.
4//! This module provides Generator implementations for common error types,
5//! allowing agents to create mock errors for testing.
6//!
7//! # Use Case: Testing Error Handlers
8//!
9//! ```rust,ignore
10//! use std::io;
11//! use elicitation::{IoErrorGenerationMode, IoErrorGenerator, Generator};
12//!
13//! // Create an error generator for testing
14//! let mode = IoErrorGenerationMode::NotFound("config.toml".to_string());
15//! let generator = IoErrorGenerator::new(mode);
16//!
17//! // Generate error for test
18//! let error = generator.generate();
19//!
20//! // Test your error handler
21//! fn handle_error(e: io::Error) -> String {
22//!     format!("Error: {}", e)
23//! }
24//! let result = handle_error(error);
25//! assert!(result.contains("config.toml"));
26//! ```
27
28use crate::{
29    ElicitClient, ElicitError, ElicitErrorKind, ElicitResult, Elicitation, Generator, Prompt,
30    Select, mcp,
31};
32use std::io;
33
34// ============================================================================
35// std::io::Error Generator
36// ============================================================================
37
38/// Generation mode for std::io::Error.
39///
40/// Allows creating IO errors for testing without actual IO failures.
41#[derive(Debug, Clone, PartialEq, Eq, Hash)]
42pub enum IoErrorGenerationMode {
43    /// File/directory not found error.
44    NotFound(String),
45    /// Permission denied error.
46    PermissionDenied(String),
47    /// Connection refused error.
48    ConnectionRefused(String),
49    /// Connection reset error.
50    ConnectionReset(String),
51    /// Broken pipe error.
52    BrokenPipe(String),
53    /// Already exists error.
54    AlreadyExists(String),
55    /// Invalid input error.
56    InvalidInput(String),
57    /// Timeout error.
58    TimedOut(String),
59    /// Unexpected EOF error.
60    UnexpectedEof(String),
61    /// Generic "other" error.
62    Other(String),
63}
64
65impl IoErrorGenerationMode {
66    /// Get the error kind for this mode.
67    pub fn error_kind(&self) -> io::ErrorKind {
68        match self {
69            IoErrorGenerationMode::NotFound(_) => io::ErrorKind::NotFound,
70            IoErrorGenerationMode::PermissionDenied(_) => io::ErrorKind::PermissionDenied,
71            IoErrorGenerationMode::ConnectionRefused(_) => io::ErrorKind::ConnectionRefused,
72            IoErrorGenerationMode::ConnectionReset(_) => io::ErrorKind::ConnectionReset,
73            IoErrorGenerationMode::BrokenPipe(_) => io::ErrorKind::BrokenPipe,
74            IoErrorGenerationMode::AlreadyExists(_) => io::ErrorKind::AlreadyExists,
75            IoErrorGenerationMode::InvalidInput(_) => io::ErrorKind::InvalidInput,
76            IoErrorGenerationMode::TimedOut(_) => io::ErrorKind::TimedOut,
77            IoErrorGenerationMode::UnexpectedEof(_) => io::ErrorKind::UnexpectedEof,
78            IoErrorGenerationMode::Other(_) => io::ErrorKind::Other,
79        }
80    }
81
82    /// Get the message for this mode.
83    pub fn message(&self) -> &str {
84        match self {
85            IoErrorGenerationMode::NotFound(msg)
86            | IoErrorGenerationMode::PermissionDenied(msg)
87            | IoErrorGenerationMode::ConnectionRefused(msg)
88            | IoErrorGenerationMode::ConnectionReset(msg)
89            | IoErrorGenerationMode::BrokenPipe(msg)
90            | IoErrorGenerationMode::AlreadyExists(msg)
91            | IoErrorGenerationMode::InvalidInput(msg)
92            | IoErrorGenerationMode::TimedOut(msg)
93            | IoErrorGenerationMode::UnexpectedEof(msg)
94            | IoErrorGenerationMode::Other(msg) => msg,
95        }
96    }
97}
98
99impl Select for IoErrorGenerationMode {
100    fn options() -> &'static [Self] {
101        // Can't return non-static Self with String fields
102        // Options will be constructed from labels
103        &[]
104    }
105
106    fn labels() -> &'static [&'static str] {
107        &[
108            "NotFound",
109            "PermissionDenied",
110            "ConnectionRefused",
111            "ConnectionReset",
112            "BrokenPipe",
113            "AlreadyExists",
114            "InvalidInput",
115            "TimedOut",
116            "UnexpectedEof",
117            "Other",
118        ]
119    }
120
121    fn from_label(label: &str) -> Option<Self> {
122        // Message will be elicited separately
123        match label {
124            "NotFound" => Some(IoErrorGenerationMode::NotFound(String::new())),
125            "PermissionDenied" => Some(IoErrorGenerationMode::PermissionDenied(String::new())),
126            "ConnectionRefused" => Some(IoErrorGenerationMode::ConnectionRefused(String::new())),
127            "ConnectionReset" => Some(IoErrorGenerationMode::ConnectionReset(String::new())),
128            "BrokenPipe" => Some(IoErrorGenerationMode::BrokenPipe(String::new())),
129            "AlreadyExists" => Some(IoErrorGenerationMode::AlreadyExists(String::new())),
130            "InvalidInput" => Some(IoErrorGenerationMode::InvalidInput(String::new())),
131            "TimedOut" => Some(IoErrorGenerationMode::TimedOut(String::new())),
132            "UnexpectedEof" => Some(IoErrorGenerationMode::UnexpectedEof(String::new())),
133            "Other" => Some(IoErrorGenerationMode::Other(String::new())),
134            _ => None,
135        }
136    }
137}
138
139crate::default_style!(IoErrorGenerationMode => IoErrorGenerationModeStyle);
140
141impl Prompt for IoErrorGenerationMode {
142    fn prompt() -> Option<&'static str> {
143        Some("Select the type of IO error to create for testing:")
144    }
145}
146
147impl Elicitation for IoErrorGenerationMode {
148    type Style = IoErrorGenerationModeStyle;
149
150    async fn elicit(client: &ElicitClient<'_>) -> ElicitResult<Self> {
151        let params = mcp::select_params(
152            Self::prompt().unwrap_or("Select an option:"),
153            Self::labels(),
154        );
155
156        let result = client
157            .peer()
158            .call_tool(rmcp::model::CallToolRequestParams {
159                meta: None,
160                name: mcp::tool_names::elicit_select().into(),
161                arguments: Some(params),
162                task: None,
163            })
164            .await?;
165
166        let value = mcp::extract_value(result)?;
167        let label = mcp::parse_string(value)?;
168
169        let selected = Self::from_label(&label).ok_or_else(|| {
170            ElicitError::new(ElicitErrorKind::ParseError(
171                "Invalid IO error kind".to_string(),
172            ))
173        })?;
174
175        // Elicit error message
176        let message = String::elicit(client).await?;
177
178        // Create mode with the message
179        let mode = match selected {
180            IoErrorGenerationMode::NotFound(_) => IoErrorGenerationMode::NotFound(message),
181            IoErrorGenerationMode::PermissionDenied(_) => {
182                IoErrorGenerationMode::PermissionDenied(message)
183            }
184            IoErrorGenerationMode::ConnectionRefused(_) => {
185                IoErrorGenerationMode::ConnectionRefused(message)
186            }
187            IoErrorGenerationMode::ConnectionReset(_) => {
188                IoErrorGenerationMode::ConnectionReset(message)
189            }
190            IoErrorGenerationMode::BrokenPipe(_) => IoErrorGenerationMode::BrokenPipe(message),
191            IoErrorGenerationMode::AlreadyExists(_) => {
192                IoErrorGenerationMode::AlreadyExists(message)
193            }
194            IoErrorGenerationMode::InvalidInput(_) => IoErrorGenerationMode::InvalidInput(message),
195            IoErrorGenerationMode::TimedOut(_) => IoErrorGenerationMode::TimedOut(message),
196            IoErrorGenerationMode::UnexpectedEof(_) => {
197                IoErrorGenerationMode::UnexpectedEof(message)
198            }
199            IoErrorGenerationMode::Other(_) => IoErrorGenerationMode::Other(message),
200        };
201
202        Ok(mode)
203    }
204}
205
206/// Generator for creating std::io::Error instances for testing.
207///
208/// Allows deterministic creation of IO errors without actual IO failures.
209#[derive(Debug, Clone)]
210pub struct IoErrorGenerator {
211    mode: IoErrorGenerationMode,
212}
213
214impl IoErrorGenerator {
215    /// Create a new IO error generator.
216    pub fn new(mode: IoErrorGenerationMode) -> Self {
217        Self { mode }
218    }
219
220    /// Get the generation mode.
221    pub fn mode(&self) -> &IoErrorGenerationMode {
222        &self.mode
223    }
224}
225
226impl Generator for IoErrorGenerator {
227    type Target = io::Error;
228
229    fn generate(&self) -> Self::Target {
230        io::Error::new(self.mode.error_kind(), self.mode.message())
231    }
232}
233
234// Elicitation for io::Error itself
235crate::default_style!(io::Error => IoErrorStyle);
236
237impl Prompt for io::Error {
238    fn prompt() -> Option<&'static str> {
239        Some("Create an IO error for testing:")
240    }
241}
242
243impl Elicitation for io::Error {
244    type Style = IoErrorStyle;
245
246    async fn elicit(client: &ElicitClient<'_>) -> ElicitResult<Self> {
247        tracing::debug!("Eliciting io::Error for testing");
248
249        // Elicit generation mode
250        let mode = IoErrorGenerationMode::elicit(client).await?;
251
252        // Create generator and generate error
253        let generator = IoErrorGenerator::new(mode);
254        Ok(generator.generate())
255    }
256}
257
258// ============================================================================
259// serde_json::Error Generator
260// ============================================================================
261
262#[cfg(feature = "serde_json")]
263mod json_error {
264    use super::*;
265
266    /// Generation mode for serde_json::Error.
267    ///
268    /// Creates real JSON parsing errors by attempting to parse invalid JSON.
269    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
270    pub enum JsonErrorGenerationMode {
271        /// Syntax error (invalid JSON).
272        SyntaxError,
273        /// EOF while parsing (incomplete JSON).
274        EofWhileParsing,
275        /// Invalid number.
276        InvalidNumber,
277        /// Invalid escape sequence.
278        InvalidEscape,
279        /// Invalid Unicode code point.
280        InvalidUnicode,
281    }
282
283    impl Select for JsonErrorGenerationMode {
284        fn options() -> &'static [Self] {
285            &[
286                JsonErrorGenerationMode::SyntaxError,
287                JsonErrorGenerationMode::EofWhileParsing,
288                JsonErrorGenerationMode::InvalidNumber,
289                JsonErrorGenerationMode::InvalidEscape,
290                JsonErrorGenerationMode::InvalidUnicode,
291            ]
292        }
293
294        fn labels() -> &'static [&'static str] {
295            &[
296                "Syntax Error",
297                "EOF While Parsing",
298                "Invalid Number",
299                "Invalid Escape",
300                "Invalid Unicode",
301            ]
302        }
303
304        fn from_label(label: &str) -> Option<Self> {
305            match label {
306                "Syntax Error" => Some(JsonErrorGenerationMode::SyntaxError),
307                "EOF While Parsing" => Some(JsonErrorGenerationMode::EofWhileParsing),
308                "Invalid Number" => Some(JsonErrorGenerationMode::InvalidNumber),
309                "Invalid Escape" => Some(JsonErrorGenerationMode::InvalidEscape),
310                "Invalid Unicode" => Some(JsonErrorGenerationMode::InvalidUnicode),
311                _ => None,
312            }
313        }
314    }
315
316    crate::default_style!(JsonErrorGenerationMode => JsonErrorGenerationModeStyle);
317
318    impl Prompt for JsonErrorGenerationMode {
319        fn prompt() -> Option<&'static str> {
320            Some("Select the type of JSON error to create for testing:")
321        }
322    }
323
324    impl Elicitation for JsonErrorGenerationMode {
325        type Style = JsonErrorGenerationModeStyle;
326
327        async fn elicit(client: &ElicitClient<'_>) -> ElicitResult<Self> {
328            let params = mcp::select_params(
329                Self::prompt().unwrap_or("Select an option:"),
330                Self::labels(),
331            );
332
333            let result = client
334                .peer()
335                .call_tool(rmcp::model::CallToolRequestParams {
336                    meta: None,
337                    name: mcp::tool_names::elicit_select().into(),
338                    arguments: Some(params),
339                    task: None,
340                })
341                .await?;
342
343            let value = mcp::extract_value(result)?;
344            let label = mcp::parse_string(value)?;
345
346            Self::from_label(&label).ok_or_else(|| {
347                ElicitError::new(ElicitErrorKind::ParseError(
348                    "Invalid JSON error kind".to_string(),
349                ))
350            })
351        }
352    }
353
354    /// Generator for creating serde_json::Error instances for testing.
355    ///
356    /// Creates real JSON errors by parsing intentionally invalid JSON.
357    #[derive(Debug, Clone, Copy)]
358    pub struct JsonErrorGenerator {
359        mode: JsonErrorGenerationMode,
360    }
361
362    impl JsonErrorGenerator {
363        /// Create a new JSON error generator.
364        pub fn new(mode: JsonErrorGenerationMode) -> Self {
365            Self { mode }
366        }
367
368        /// Get the generation mode.
369        pub fn mode(&self) -> JsonErrorGenerationMode {
370            self.mode
371        }
372    }
373
374    impl Generator for JsonErrorGenerator {
375        type Target = serde_json::Error;
376
377        fn generate(&self) -> Self::Target {
378            // Create real JSON errors by parsing invalid JSON
379            let invalid_json = match self.mode {
380                JsonErrorGenerationMode::SyntaxError => "{invalid}",
381                JsonErrorGenerationMode::EofWhileParsing => "{\"key\":",
382                JsonErrorGenerationMode::InvalidNumber => "{\"num\": 1e999999}",
383                JsonErrorGenerationMode::InvalidEscape => r#"{"str": "\x"}"#,
384                JsonErrorGenerationMode::InvalidUnicode => r#"{"str": "\uDEAD"}"#,
385            };
386
387            // Parse will fail, giving us a real serde_json::Error
388            serde_json::from_str::<serde_json::Value>(invalid_json)
389                .expect_err("Invalid JSON should always error")
390        }
391    }
392
393    // Elicitation for serde_json::Error itself
394    crate::default_style!(serde_json::Error => JsonErrorStyle);
395
396    impl Prompt for serde_json::Error {
397        fn prompt() -> Option<&'static str> {
398            Some("Create a JSON parsing error for testing:")
399        }
400    }
401
402    impl Elicitation for serde_json::Error {
403        type Style = JsonErrorStyle;
404
405        async fn elicit(client: &ElicitClient<'_>) -> ElicitResult<Self> {
406            tracing::debug!("Eliciting serde_json::Error for testing");
407
408            // Elicit generation mode
409            let mode = JsonErrorGenerationMode::elicit(client).await?;
410
411            // Create generator and generate error
412            let generator = JsonErrorGenerator::new(mode);
413            Ok(generator.generate())
414        }
415    }
416}
417
418#[cfg(feature = "serde_json")]
419pub use json_error::{JsonErrorGenerationMode, JsonErrorGenerator};
420
421#[cfg(test)]
422mod tests {
423    use super::*;
424
425    #[test]
426    fn test_io_error_generation() {
427        let mode = IoErrorGenerationMode::NotFound("config.toml".to_string());
428        let generator = IoErrorGenerator::new(mode);
429        let error = generator.generate();
430
431        assert_eq!(error.kind(), io::ErrorKind::NotFound);
432        assert!(error.to_string().contains("config.toml"));
433    }
434
435    #[test]
436    fn test_io_error_kinds() {
437        let modes = vec![
438            IoErrorGenerationMode::PermissionDenied("test".to_string()),
439            IoErrorGenerationMode::ConnectionRefused("test".to_string()),
440            IoErrorGenerationMode::BrokenPipe("test".to_string()),
441        ];
442
443        for mode in modes {
444            let generator = IoErrorGenerator::new(mode.clone());
445            let error = generator.generate();
446            assert_eq!(error.kind(), mode.error_kind());
447        }
448    }
449
450    #[cfg(feature = "serde_json")]
451    #[test]
452    fn test_json_error_generation() {
453        let mode = JsonErrorGenerationMode::SyntaxError;
454        let generator = JsonErrorGenerator::new(mode);
455        let error = generator.generate();
456
457        // Error should be a real serde_json::Error with non-empty message
458        assert!(!error.to_string().is_empty());
459    }
460
461    #[cfg(feature = "serde_json")]
462    #[test]
463    fn test_json_error_kinds() {
464        let modes = vec![
465            JsonErrorGenerationMode::SyntaxError,
466            JsonErrorGenerationMode::EofWhileParsing,
467            JsonErrorGenerationMode::InvalidNumber,
468        ];
469
470        for mode in modes {
471            let generator = JsonErrorGenerator::new(mode);
472            let error = generator.generate();
473            // All should produce real errors
474            assert!(!error.to_string().is_empty());
475        }
476    }
477}