Skip to main content

serdes_ai_output/
error.rs

1//! Error types for output parsing and validation.
2
3use thiserror::Error;
4
5/// Error during output parsing.
6#[derive(Debug, Error)]
7pub enum OutputParseError {
8    /// Failed to parse JSON.
9    #[error("Failed to parse JSON: {0}")]
10    JsonParse(#[from] serde_json::Error),
11
12    /// Missing required field in the output.
13    #[error("Missing required field: {0}")]
14    MissingField(String),
15
16    /// Invalid value for a field.
17    #[error("Invalid value for field '{field}': {message}")]
18    InvalidField {
19        /// The field name.
20        field: String,
21        /// The error message.
22        message: String,
23    },
24
25    /// Unexpected tool call.
26    #[error("Unexpected tool call: expected '{expected}', got '{actual}'")]
27    UnexpectedTool {
28        /// Expected tool name.
29        expected: String,
30        /// Actual tool name received.
31        actual: String,
32    },
33
34    /// Output is not valid JSON.
35    #[error("Output is not valid JSON")]
36    NotJson,
37
38    /// No JSON found in the output.
39    #[error("No JSON object or array found in output")]
40    NoJsonFound,
41
42    /// Pattern mismatch.
43    #[error("Output does not match required pattern: {pattern}")]
44    PatternMismatch {
45        /// The pattern that was expected.
46        pattern: String,
47    },
48
49    /// Length constraint violation.
50    #[error("Output length {actual} is {direction} allowed {limit}")]
51    LengthViolation {
52        /// Actual length.
53        actual: usize,
54        /// The limit.
55        limit: usize,
56        /// Direction ("below" or "above").
57        direction: &'static str,
58    },
59
60    /// Regex compilation error.
61    #[error("Invalid regex pattern: {0}")]
62    RegexError(#[from] regex::Error),
63
64    /// Custom parse error.
65    #[error("Parse error: {0}")]
66    Custom(String),
67}
68
69impl OutputParseError {
70    /// Create a custom parse error.
71    pub fn custom(msg: impl Into<String>) -> Self {
72        Self::Custom(msg.into())
73    }
74
75    /// Create a missing field error.
76    pub fn missing_field(field: impl Into<String>) -> Self {
77        Self::MissingField(field.into())
78    }
79
80    /// Create an invalid field error.
81    pub fn invalid_field(field: impl Into<String>, message: impl Into<String>) -> Self {
82        Self::InvalidField {
83            field: field.into(),
84            message: message.into(),
85        }
86    }
87
88    /// Create an unexpected tool error.
89    pub fn unexpected_tool(expected: impl Into<String>, actual: impl Into<String>) -> Self {
90        Self::UnexpectedTool {
91            expected: expected.into(),
92            actual: actual.into(),
93        }
94    }
95
96    /// Create a length violation error for being too short.
97    pub fn too_short(actual: usize, min: usize) -> Self {
98        Self::LengthViolation {
99            actual,
100            limit: min,
101            direction: "below",
102        }
103    }
104
105    /// Create a length violation error for being too long.
106    pub fn too_long(actual: usize, max: usize) -> Self {
107        Self::LengthViolation {
108            actual,
109            limit: max,
110            direction: "above",
111        }
112    }
113}
114
115/// Error during output validation.
116#[derive(Debug, Error)]
117pub enum OutputValidationError {
118    /// Validation failed.
119    #[error("Validation failed: {message}")]
120    Failed {
121        /// The error message.
122        message: String,
123        /// Whether the model should retry.
124        retry: bool,
125    },
126
127    /// Model should retry with a different response.
128    #[error("Model retry requested: {0}")]
129    ModelRetry(String),
130
131    /// Parse error during validation.
132    #[error(transparent)]
133    Parse(#[from] OutputParseError),
134}
135
136impl OutputValidationError {
137    /// Create a retry error with a message for the model.
138    pub fn retry(msg: impl Into<String>) -> Self {
139        Self::ModelRetry(msg.into())
140    }
141
142    /// Create a failed validation error (no retry).
143    pub fn failed(msg: impl Into<String>) -> Self {
144        Self::Failed {
145            message: msg.into(),
146            retry: false,
147        }
148    }
149
150    /// Create a failed validation error (with retry).
151    pub fn failed_retry(msg: impl Into<String>) -> Self {
152        Self::Failed {
153            message: msg.into(),
154            retry: true,
155        }
156    }
157
158    /// Whether the model should retry.
159    pub fn should_retry(&self) -> bool {
160        match self {
161            Self::Failed { retry, .. } => *retry,
162            Self::ModelRetry(_) => true,
163            Self::Parse(_) => false,
164        }
165    }
166
167    /// Get the retry message if applicable.
168    pub fn retry_message(&self) -> Option<&str> {
169        match self {
170            Self::ModelRetry(msg) => Some(msg),
171            Self::Failed {
172                message,
173                retry: true,
174            } => Some(message),
175            _ => None,
176        }
177    }
178}
179
180/// Result type for output parsing.
181pub type ParseResult<T> = Result<T, OutputParseError>;
182
183/// Result type for output validation.
184pub type ValidationResult<T> = Result<T, OutputValidationError>;
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189
190    #[test]
191    fn test_parse_error_custom() {
192        let err = OutputParseError::custom("Something went wrong");
193        assert!(err.to_string().contains("Something went wrong"));
194    }
195
196    #[test]
197    fn test_parse_error_missing_field() {
198        let err = OutputParseError::missing_field("name");
199        assert!(err.to_string().contains("name"));
200    }
201
202    #[test]
203    fn test_parse_error_invalid_field() {
204        let err = OutputParseError::invalid_field("age", "must be positive");
205        assert!(err.to_string().contains("age"));
206        assert!(err.to_string().contains("must be positive"));
207    }
208
209    #[test]
210    fn test_parse_error_unexpected_tool() {
211        let err = OutputParseError::unexpected_tool("final_result", "search");
212        assert!(err.to_string().contains("final_result"));
213        assert!(err.to_string().contains("search"));
214    }
215
216    #[test]
217    fn test_parse_error_too_short() {
218        let err = OutputParseError::too_short(5, 10);
219        assert!(err.to_string().contains("5"));
220        assert!(err.to_string().contains("10"));
221        assert!(err.to_string().contains("below"));
222    }
223
224    #[test]
225    fn test_validation_error_retry() {
226        let err = OutputValidationError::retry("Please provide a valid email");
227        assert!(err.should_retry());
228        assert_eq!(err.retry_message(), Some("Please provide a valid email"));
229    }
230
231    #[test]
232    fn test_validation_error_failed_no_retry() {
233        let err = OutputValidationError::failed("Invalid data");
234        assert!(!err.should_retry());
235        assert!(err.retry_message().is_none());
236    }
237
238    #[test]
239    fn test_validation_error_failed_retry() {
240        let err = OutputValidationError::failed_retry("Try again");
241        assert!(err.should_retry());
242        assert_eq!(err.retry_message(), Some("Try again"));
243    }
244}