Skip to main content

ferrin_spec/error/
data.rs

1//! Response data, parsing and validation errors.
2
3use super::BoxError;
4use crate::json::JsonValue;
5
6/// The provider returned an empty body where one was required.
7#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
8#[error("{message}")]
9pub struct EmptyResponseBodyError {
10    /// Explanation.
11    pub message: String,
12}
13
14impl EmptyResponseBodyError {
15    /// Creates an error with the default message.
16    #[must_use]
17    pub fn new() -> Self {
18        Self {
19            message: "empty response body".to_owned(),
20        }
21    }
22
23    /// Creates an error with a custom message.
24    #[must_use]
25    pub fn with_message(message: impl Into<String>) -> Self {
26        Self {
27            message: message.into(),
28        }
29    }
30}
31
32impl Default for EmptyResponseBodyError {
33    fn default() -> Self {
34        Self::new()
35    }
36}
37
38/// The provider produced no content.
39#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
40#[error("{message}")]
41pub struct NoContentGeneratedError {
42    /// Explanation.
43    pub message: String,
44}
45
46impl NoContentGeneratedError {
47    /// Creates an error with the default message.
48    #[must_use]
49    pub fn new() -> Self {
50        Self {
51            message: "no content generated".to_owned(),
52        }
53    }
54
55    /// Creates an error with a custom message.
56    #[must_use]
57    pub fn with_message(message: impl Into<String>) -> Self {
58        Self {
59            message: message.into(),
60        }
61    }
62}
63
64impl Default for NoContentGeneratedError {
65    fn default() -> Self {
66        Self::new()
67    }
68}
69
70/// The response has an unexpected shape.
71#[derive(Debug, thiserror::Error)]
72#[error("{message}")]
73pub struct InvalidResponseDataError {
74    /// Explanation.
75    pub message: String,
76    /// The offending data.
77    pub data: JsonValue,
78}
79
80impl InvalidResponseDataError {
81    /// Creates an error with a message and the offending data.
82    #[must_use]
83    pub fn new(message: impl Into<String>, data: JsonValue) -> Self {
84        Self {
85            message: message.into(),
86            data,
87        }
88    }
89
90    /// Creates an error whose message is derived from the data.
91    #[must_use]
92    pub fn from_data(data: JsonValue) -> Self {
93        let rendered = data.to_string();
94        let message = format!(
95            "invalid response data: {}",
96            super::truncate_for_display(&rendered, 512)
97        );
98        Self { message, data }
99    }
100}
101
102/// Text could not be parsed as JSON.
103#[derive(Debug, thiserror::Error)]
104#[error("json parsing failed: {cause}")]
105pub struct JsonParseError {
106    /// The text that failed to parse.
107    pub text: String,
108    /// The parser error.
109    #[source]
110    pub cause: BoxError,
111}
112
113impl JsonParseError {
114    /// Creates an error for `text` caused by `cause`.
115    #[must_use]
116    pub fn new(
117        text: impl Into<String>,
118        cause: impl std::error::Error + Send + Sync + 'static,
119    ) -> Self {
120        Self {
121            text: text.into(),
122            cause: Box::new(cause),
123        }
124    }
125}
126
127/// Where a validated value came from, for error messages.
128#[derive(Debug, Clone, Default, PartialEq, Eq)]
129pub struct TypeValidationContext {
130    /// Field or path being validated.
131    pub field: Option<String>,
132    /// Kind of entity (for example `tool input`).
133    pub entity_name: Option<String>,
134    /// Identifier of the entity (for example a tool call id).
135    pub entity_id: Option<String>,
136}
137
138/// A value failed schema or type validation.
139#[derive(Debug, thiserror::Error)]
140pub struct TypeValidationError {
141    /// The offending value.
142    pub value: JsonValue,
143    /// Where the value came from (boxed to keep the error small).
144    pub context: Option<Box<TypeValidationContext>>,
145    /// The validation error.
146    #[source]
147    pub cause: BoxError,
148}
149
150impl TypeValidationError {
151    /// Creates an error for `value` caused by `cause`.
152    #[must_use]
153    pub fn new(value: JsonValue, cause: impl std::error::Error + Send + Sync + 'static) -> Self {
154        Self {
155            value,
156            context: None,
157            cause: Box::new(cause),
158        }
159    }
160
161    /// Sets the validation context.
162    #[must_use]
163    pub fn with_context(mut self, context: TypeValidationContext) -> Self {
164        self.context = Some(Box::new(context));
165        self
166    }
167
168    /// Wraps `cause` unless it already is a [`TypeValidationError`] for the
169    /// same value and context, in which case it is returned unchanged.
170    #[must_use]
171    pub fn wrap(value: JsonValue, cause: BoxError, context: Option<TypeValidationContext>) -> Self {
172        let context = context.map(Box::new);
173        match cause.downcast::<TypeValidationError>() {
174            Ok(existing) if existing.value == value && existing.context == context => *existing,
175            Ok(existing) => Self {
176                value,
177                context,
178                cause: existing,
179            },
180            Err(cause) => Self {
181                value,
182                context,
183                cause,
184            },
185        }
186    }
187}
188
189impl std::fmt::Display for TypeValidationError {
190    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
191        f.write_str("type validation failed")?;
192        if let Some(context) = &self.context {
193            if let Some(field) = &context.field {
194                write!(f, " for {field}")?;
195            }
196            let mut parts = Vec::new();
197            if let Some(name) = &context.entity_name {
198                parts.push(name.clone());
199            }
200            if let Some(id) = &context.entity_id {
201                parts.push(format!("id: \"{id}\""));
202            }
203            if !parts.is_empty() {
204                write!(f, " ({})", parts.join(", "))?;
205            }
206        }
207        write!(f, ": {}", self.cause)
208    }
209}