agora_agentkit/
field_path.rs1use std::fmt;
15
16use serde::de::DeserializeOwned;
17
18#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct FieldError {
21 path: Option<String>,
22 message: String,
23}
24
25impl FieldError {
26 pub fn new(e: serde_path_to_error::Error<serde_json::Error>) -> Self {
28 let path = e.path().to_string();
29 let message = message(e.inner());
30 Self {
31 path: (!matches!(path.as_str(), "" | "." | "?")
37 && !names_its_field(&message, &path))
38 .then_some(path),
39 message,
40 }
41 }
42
43 pub fn path(&self) -> Option<&str> {
46 self.path.as_deref()
47 }
48
49 pub fn message(&self) -> &str {
51 &self.message
52 }
53
54 pub fn after(&self, lead: &str) -> String {
58 match &self.path {
59 Some(path) => format!("{lead} at `{path}`: {}", self.message),
60 None => format!("{lead}: {}", self.message),
61 }
62 }
63}
64
65impl fmt::Display for FieldError {
67 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68 match &self.path {
69 Some(path) => write!(f, "at `{path}`: {}", self.message),
70 None => f.write_str(&self.message),
71 }
72 }
73}
74
75impl std::error::Error for FieldError {}
76
77fn names_its_field(message: &str, path: &str) -> bool {
79 let rest = message
80 .strip_prefix('`')
81 .and_then(|m| m.strip_prefix(path))
82 .and_then(|m| m.strip_prefix('`'))
83 .or_else(|| message.strip_prefix(path));
84 rest.is_some_and(|rest| rest.starts_with([' ', ':']))
85}
86
87pub fn message(e: &serde_json::Error) -> String {
91 let text = e.to_string();
92 if e.line() == 0 {
93 return text;
94 }
95 let position = format!(" at line {} column {}", e.line(), e.column());
96 match text.strip_suffix(&position) {
97 Some(message) => message.to_string(),
98 None => text,
99 }
100}
101
102pub fn from_value<T: DeserializeOwned>(
104 value: serde_json::Value,
105) -> Result<T, FieldError> {
106 serde_path_to_error::deserialize(value).map_err(FieldError::new)
107}
108
109pub fn from_str<T: DeserializeOwned>(text: &str) -> Result<T, FieldError> {
112 from_slice(text.as_bytes())
113}
114
115pub fn from_slice<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, FieldError> {
118 let mut de = serde_json::Deserializer::from_slice(bytes);
119 let value =
120 serde_path_to_error::deserialize(&mut de).map_err(FieldError::new)?;
121 de.end().map_err(|e| FieldError {
122 path: None,
123 message: message(&e),
124 })?;
125 Ok(value)
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[derive(Debug, serde::Deserialize)]
133 #[serde(deny_unknown_fields)]
134 #[allow(dead_code)]
135 struct Outer {
136 options: Options,
137 #[serde(default)]
138 items: Vec<Item>,
139 }
140
141 #[derive(Debug, serde::Deserialize)]
142 #[serde(deny_unknown_fields)]
143 #[allow(dead_code)]
144 struct Options {
145 mode: Mode,
146 }
147
148 #[derive(Debug, serde::Deserialize)]
149 #[serde(rename_all = "snake_case")]
150 enum Mode {
151 Full,
152 Summary,
153 }
154
155 #[derive(Debug, serde::Deserialize)]
156 #[allow(dead_code)]
157 struct Item {
158 id: u32,
159 }
160
161 #[test]
164 fn a_nested_error_names_its_path_and_no_position() {
165 let text = r#"{"options": {"mode": "bogus"}}"#;
166 let before = serde_json::from_str::<Outer>(text).unwrap_err();
167 assert_eq!(
168 before.to_string(),
169 "unknown variant `bogus`, expected `full` or `summary` at line 1 \
170 column 28"
171 );
172 let after = from_str::<Outer>(text).unwrap_err();
173 assert_eq!(
174 after.to_string(),
175 "at `options.mode`: unknown variant `bogus`, expected `full` or \
176 `summary`"
177 );
178 assert_eq!(after.path(), Some("options.mode"));
179 let value: serde_json::Value = serde_json::from_str(text).unwrap();
181 assert_eq!(from_value::<Outer>(value).unwrap_err(), after);
182 let err = from_str::<Outer>(
184 r#"{"options": {"mode": "full"}, "items": [{"id": 1}, {"id": "x"}]}"#,
185 )
186 .unwrap_err();
187 assert_eq!(
188 err.to_string(),
189 "at `items[1].id`: invalid type: string \"x\", expected u32"
190 );
191 }
192
193 #[test]
196 fn whole_value_errors_keep_the_message_alone() {
197 let err =
198 from_str::<Outer>(r#"{"options": {"mode": "full"}"#).unwrap_err();
199 assert_eq!(err.path(), None);
200 assert_eq!(err.message(), "EOF while parsing an object");
201 let err = from_str::<Outer>("7").unwrap_err();
202 assert_eq!(err.path(), None);
203 assert_eq!(
204 err.message(),
205 "invalid type: integer `7`, expected struct Outer"
206 );
207 assert_eq!(
208 err.after("Invalid JSON"),
209 format!("Invalid JSON: {}", err.message())
210 );
211 let err = from_str::<serde_json::Value>("{} x").unwrap_err();
212 assert_eq!(err.to_string(), "trailing characters");
213 }
214
215 #[derive(Debug, serde::Deserialize)]
216 #[allow(dead_code)]
217 struct Named {
218 reply_to: Checked,
219 }
220
221 #[derive(Debug)]
222 struct Checked;
223
224 impl<'de> serde::Deserialize<'de> for Checked {
225 fn deserialize<D: serde::Deserializer<'de>>(
226 _: D,
227 ) -> Result<Self, D::Error> {
228 Err(serde::de::Error::custom(
229 "`reply_to` must be a post or comment id",
230 ))
231 }
232 }
233
234 #[test]
238 fn a_message_naming_its_field_is_not_given_it_twice() {
239 let err = from_str::<Named>(r#"{"reply_to": "x"}"#).unwrap_err();
240 assert_eq!(err.path(), None);
241 assert_eq!(err.to_string(), "`reply_to` must be a post or comment id");
242 assert!(names_its_field(
243 "entry_type \"bogus\" is not one of: a",
244 "entry_type"
245 ));
246 assert!(!names_its_field("entry_types are listed", "entry_type"));
247 assert!(!names_its_field("expected `reply_to` here", "reply_to"));
248 }
249}