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 {
46 let before = &self.expression[..self.offset.min(self.expression.len())];
47 match before.rfind('\n') {
48 Some(pos) => self.offset - pos - 1,
49 None => self.offset,
50 }
51 }
52}
53
54impl fmt::Display for JmespathError {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 let col = self.column();
57 write!(
58 f,
59 "{}\n{}\n{}",
60 self.reason,
61 self.expression,
62 " ".repeat(col)
63 )?;
64 write!(f, "^")
65 }
66}
67
68impl std::error::Error for JmespathError {}
69
70#[derive(Debug, Clone, PartialEq)]
72pub enum ErrorReason {
73 Parse(String),
75 Runtime(RuntimeError),
77}
78
79impl fmt::Display for ErrorReason {
80 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81 match self {
82 ErrorReason::Parse(msg) => write!(f, "Parse error: {msg}"),
83 ErrorReason::Runtime(err) => write!(f, "Runtime error: {err}"),
84 }
85 }
86}
87
88#[derive(Debug, Clone, PartialEq, thiserror::Error)]
90pub enum RuntimeError {
91 #[error("Invalid slice: step cannot be 0")]
93 InvalidSlice,
94 #[error("Too many arguments: expected {expected}, got {actual}")]
96 TooManyArguments { expected: usize, actual: usize },
97 #[error("Not enough arguments: expected {expected}, got {actual}")]
99 NotEnoughArguments { expected: usize, actual: usize },
100 #[error("Unknown function: {0}")]
102 UnknownFunction(String),
103 #[error("Invalid type at position {position}: expected {expected}, got {actual}")]
105 InvalidType {
106 expected: String,
107 actual: String,
108 position: usize,
109 },
110 #[error(
112 "Invalid return type at position {position}, invocation {invocation}: expected {expected}, got {actual}"
113 )]
114 InvalidReturnType {
115 expected: String,
116 actual: String,
117 position: usize,
118 invocation: usize,
119 },
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn line_single_line_expression() {
128 let err = JmespathError::new("foo.bar", 4, ErrorReason::Parse("test".into()));
129 assert_eq!(err.line(), 1);
130 }
131
132 #[test]
133 fn column_single_line_expression() {
134 let err = JmespathError::new("foo.bar", 4, ErrorReason::Parse("test".into()));
135 assert_eq!(err.column(), 4);
136 }
137
138 #[test]
139 fn line_multi_line_expression() {
140 let err = JmespathError::new("foo\nbar\nbaz", 8, ErrorReason::Parse("test".into()));
141 assert_eq!(err.line(), 3);
142 }
143
144 #[test]
145 fn column_multi_line_expression() {
146 let err = JmespathError::new("foo\nbar\nbaz", 8, ErrorReason::Parse("test".into()));
147 assert_eq!(err.column(), 0);
148 }
149
150 #[test]
151 fn column_mid_second_line() {
152 let err = JmespathError::new("foo\nbar.baz", 6, ErrorReason::Parse("test".into()));
153 assert_eq!(err.line(), 2);
154 assert_eq!(err.column(), 2);
155 }
156
157 #[test]
158 fn offset_beyond_expression_length() {
159 let err = JmespathError::new("foo", 100, ErrorReason::Parse("test".into()));
160 assert_eq!(err.line(), 1);
162 assert_eq!(err.column(), 100);
164 }
165
166 #[test]
167 fn display_format_parse_error() {
168 let err = JmespathError::new("foo.bar", 4, ErrorReason::Parse("bad token".into()));
169 let display = format!("{err}");
170 assert!(display.contains("Parse error: bad token"));
171 assert!(display.contains("foo.bar"));
172 assert!(display.contains("^"));
173 }
174
175 #[test]
176 fn display_format_runtime_error() {
177 let err = JmespathError::new(
178 "foo()",
179 0,
180 ErrorReason::Runtime(RuntimeError::UnknownFunction("foo".into())),
181 );
182 let display = format!("{err}");
183 assert!(display.contains("Runtime error"));
184 assert!(display.contains("Unknown function: foo"));
185 }
186
187 #[test]
188 fn runtime_error_invalid_slice_display() {
189 let err = RuntimeError::InvalidSlice;
190 assert_eq!(format!("{err}"), "Invalid slice: step cannot be 0");
191 }
192
193 #[test]
194 fn runtime_error_too_many_args_display() {
195 let err = RuntimeError::TooManyArguments {
196 expected: 2,
197 actual: 5,
198 };
199 assert_eq!(format!("{err}"), "Too many arguments: expected 2, got 5");
200 }
201
202 #[test]
203 fn runtime_error_not_enough_args_display() {
204 let err = RuntimeError::NotEnoughArguments {
205 expected: 3,
206 actual: 1,
207 };
208 assert_eq!(format!("{err}"), "Not enough arguments: expected 3, got 1");
209 }
210
211 #[test]
212 fn runtime_error_invalid_type_display() {
213 let err = RuntimeError::InvalidType {
214 expected: "string".into(),
215 actual: "number".into(),
216 position: 0,
217 };
218 let display = format!("{err}");
219 assert!(display.contains("expected string"));
220 assert!(display.contains("got number"));
221 }
222
223 #[test]
224 fn runtime_error_invalid_return_type_display() {
225 let err = RuntimeError::InvalidReturnType {
226 expected: "number".into(),
227 actual: "string".into(),
228 position: 1,
229 invocation: 2,
230 };
231 let display = format!("{err}");
232 assert!(display.contains("expected number"));
233 assert!(display.contains("got string"));
234 assert!(display.contains("position 1"));
235 assert!(display.contains("invocation 2"));
236 }
237
238 #[test]
239 fn error_reason_parse_display() {
240 let reason = ErrorReason::Parse("unexpected token".into());
241 assert_eq!(format!("{reason}"), "Parse error: unexpected token");
242 }
243
244 #[test]
245 fn error_reason_runtime_display() {
246 let reason = ErrorReason::Runtime(RuntimeError::InvalidSlice);
247 assert!(format!("{reason}").contains("Invalid slice"));
248 }
249
250 #[test]
251 fn from_ctx_uses_context_offset() {
252 let runtime = crate::Runtime::new();
253 let mut ctx = crate::Context::new("test_expr", &runtime);
254 ctx.offset = 5;
255 let err = JmespathError::from_ctx(&ctx, ErrorReason::Parse("test".into()));
256 assert_eq!(err.offset, 5);
257 assert_eq!(err.expression, "test_expr");
258 }
259
260 #[test]
261 fn jmespath_error_implements_std_error() {
262 let err = JmespathError::new("foo", 0, ErrorReason::Parse("test".into()));
263 let _: &dyn std::error::Error = &err;
264 }
265
266 #[test]
267 fn jmespath_error_clone_and_eq() {
268 let err = JmespathError::new("foo", 0, ErrorReason::Parse("test".into()));
269 let cloned = err.clone();
270 assert_eq!(err, cloned);
271 }
272}