1use std::fmt;
4
5#[derive(Debug, Clone, PartialEq)]
7pub struct JmespathError {
8 pub offset: usize,
10 pub expression: String,
12 pub reason: ErrorReason,
14}
15
16impl JmespathError {
17 pub fn new(expression: &str, offset: usize, reason: ErrorReason) -> Self {
19 Self {
20 offset,
21 expression: expression.to_owned(),
22 reason,
23 }
24 }
25
26 pub fn from_ctx(ctx: &crate::Context<'_>, reason: ErrorReason) -> Self {
28 Self {
29 offset: ctx.offset,
30 expression: ctx.expression.to_owned(),
31 reason,
32 }
33 }
34
35 pub fn line(&self) -> usize {
37 self.expression[..self.offset.min(self.expression.len())]
38 .chars()
39 .filter(|c| *c == '\n')
40 .count()
41 + 1
42 }
43
44 pub fn column(&self) -> usize {
50 let before = &self.expression[..self.offset.min(self.expression.len())];
51 match before.rfind('\n') {
52 Some(pos) => before[pos + 1..].chars().count(),
53 None => before.chars().count(),
54 }
55 }
56}
57
58impl fmt::Display for JmespathError {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 let col = self.column();
61 write!(
62 f,
63 "{}\n{}\n{}",
64 self.reason,
65 self.expression,
66 " ".repeat(col)
67 )?;
68 write!(f, "^")
69 }
70}
71
72impl std::error::Error for JmespathError {}
73
74#[derive(Debug, Clone, PartialEq, thiserror::Error)]
79#[non_exhaustive]
80pub enum ErrorReason {
81 #[error("Parse error: {0}")]
83 Parse(String),
84 #[error("Runtime error: {0}")]
86 Runtime(RuntimeError),
87}
88
89#[derive(Debug, Clone, PartialEq, thiserror::Error)]
94#[non_exhaustive]
95pub enum RuntimeError {
96 #[error("Invalid slice: step cannot be 0")]
98 InvalidSlice,
99 #[error("Too many arguments: expected {expected}, got {actual}")]
101 TooManyArguments { expected: usize, actual: usize },
102 #[error("Not enough arguments: expected {expected}, got {actual}")]
104 NotEnoughArguments { expected: usize, actual: usize },
105 #[error("Unknown function: {0}")]
107 UnknownFunction(String),
108 #[error("Invalid type at position {position}: expected {expected}, got {actual}")]
110 InvalidType {
111 expected: String,
112 actual: String,
113 position: usize,
114 },
115 #[error(
117 "Invalid return type at position {position}, invocation {invocation}: expected {expected}, got {actual}"
118 )]
119 InvalidReturnType {
120 expected: String,
121 actual: String,
122 position: usize,
123 invocation: usize,
124 },
125 #[error("Recursion limit exceeded: maximum expression nesting depth is {limit}")]
127 RecursionLimitExceeded {
128 limit: usize,
130 },
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136
137 #[test]
138 fn line_single_line_expression() {
139 let err = JmespathError::new("foo.bar", 4, ErrorReason::Parse("test".into()));
140 assert_eq!(err.line(), 1);
141 }
142
143 #[test]
144 fn column_single_line_expression() {
145 let err = JmespathError::new("foo.bar", 4, ErrorReason::Parse("test".into()));
146 assert_eq!(err.column(), 4);
147 }
148
149 #[test]
150 fn line_multi_line_expression() {
151 let err = JmespathError::new("foo\nbar\nbaz", 8, ErrorReason::Parse("test".into()));
152 assert_eq!(err.line(), 3);
153 }
154
155 #[test]
156 fn column_multi_line_expression() {
157 let err = JmespathError::new("foo\nbar\nbaz", 8, ErrorReason::Parse("test".into()));
158 assert_eq!(err.column(), 0);
159 }
160
161 #[test]
162 fn column_mid_second_line() {
163 let err = JmespathError::new("foo\nbar.baz", 6, ErrorReason::Parse("test".into()));
164 assert_eq!(err.line(), 2);
165 assert_eq!(err.column(), 2);
166 }
167
168 #[test]
169 fn offset_beyond_expression_length() {
170 let err = JmespathError::new("foo", 100, ErrorReason::Parse("test".into()));
171 assert_eq!(err.line(), 1);
173 assert_eq!(err.column(), 3);
175 }
176
177 #[test]
178 fn column_counts_characters_not_bytes() {
179 let err = JmespathError::new("ä.x", 3, ErrorReason::Parse("test".into()));
182 assert_eq!(err.column(), 2);
183 }
184
185 #[test]
186 fn display_format_parse_error() {
187 let err = JmespathError::new("foo.bar", 4, ErrorReason::Parse("bad token".into()));
188 let display = format!("{err}");
189 assert!(display.contains("Parse error: bad token"));
190 assert!(display.contains("foo.bar"));
191 assert!(display.contains("^"));
192 }
193
194 #[test]
195 fn display_format_runtime_error() {
196 let err = JmespathError::new(
197 "foo()",
198 0,
199 ErrorReason::Runtime(RuntimeError::UnknownFunction("foo".into())),
200 );
201 let display = format!("{err}");
202 assert!(display.contains("Runtime error"));
203 assert!(display.contains("Unknown function: foo"));
204 }
205
206 #[test]
207 fn runtime_error_invalid_slice_display() {
208 let err = RuntimeError::InvalidSlice;
209 assert_eq!(format!("{err}"), "Invalid slice: step cannot be 0");
210 }
211
212 #[test]
213 fn runtime_error_too_many_args_display() {
214 let err = RuntimeError::TooManyArguments {
215 expected: 2,
216 actual: 5,
217 };
218 assert_eq!(format!("{err}"), "Too many arguments: expected 2, got 5");
219 }
220
221 #[test]
222 fn runtime_error_not_enough_args_display() {
223 let err = RuntimeError::NotEnoughArguments {
224 expected: 3,
225 actual: 1,
226 };
227 assert_eq!(format!("{err}"), "Not enough arguments: expected 3, got 1");
228 }
229
230 #[test]
231 fn runtime_error_invalid_type_display() {
232 let err = RuntimeError::InvalidType {
233 expected: "string".into(),
234 actual: "number".into(),
235 position: 0,
236 };
237 let display = format!("{err}");
238 assert!(display.contains("expected string"));
239 assert!(display.contains("got number"));
240 }
241
242 #[test]
243 fn runtime_error_invalid_return_type_display() {
244 let err = RuntimeError::InvalidReturnType {
245 expected: "number".into(),
246 actual: "string".into(),
247 position: 1,
248 invocation: 2,
249 };
250 let display = format!("{err}");
251 assert!(display.contains("expected number"));
252 assert!(display.contains("got string"));
253 assert!(display.contains("position 1"));
254 assert!(display.contains("invocation 2"));
255 }
256
257 #[test]
258 fn error_reason_parse_display() {
259 let reason = ErrorReason::Parse("unexpected token".into());
260 assert_eq!(format!("{reason}"), "Parse error: unexpected token");
261 }
262
263 #[test]
264 fn error_reason_runtime_display() {
265 let reason = ErrorReason::Runtime(RuntimeError::InvalidSlice);
266 assert!(format!("{reason}").contains("Invalid slice"));
267 }
268
269 #[test]
270 fn from_ctx_uses_context_offset() {
271 let runtime = crate::Runtime::new();
272 let mut ctx = crate::Context::new("test_expr", &runtime);
273 ctx.offset = 5;
274 let err = JmespathError::from_ctx(&ctx, ErrorReason::Parse("test".into()));
275 assert_eq!(err.offset, 5);
276 assert_eq!(err.expression, "test_expr");
277 }
278
279 #[test]
280 fn jmespath_error_implements_std_error() {
281 let err = JmespathError::new("foo", 0, ErrorReason::Parse("test".into()));
282 let _: &dyn std::error::Error = &err;
283 }
284
285 #[test]
286 fn jmespath_error_clone_and_eq() {
287 let err = JmespathError::new("foo", 0, ErrorReason::Parse("test".into()));
288 let cloned = err.clone();
289 assert_eq!(err, cloned);
290 }
291}