json_test/assertions/base.rs
1use crate::JsonTest;
2use jsonpath_rust::JsonPath;
3use serde_json::{Map, Value};
4use std::str::FromStr;
5
6/// Provides assertions for JSON values accessed via JSONPath expressions.
7///
8/// This struct is created by `JsonTest::assert_path()` and enables a fluent API
9/// for testing JSON values. All assertion methods follow a builder pattern,
10/// returning `&mut Self` for chaining.
11///
12/// # Examples
13///
14/// ```rust
15/// use json_test::{JsonTest, PropertyAssertions};
16/// use serde_json::json;
17///
18/// let data = json!({
19/// "user": {
20/// "name": "John",
21/// "age": 30
22/// }
23/// });
24///
25/// let mut test = JsonTest::new(&data);
26/// test.assert_path("$.user")
27/// .exists()
28/// .has_property("name")
29/// .has_property_value("age", json!(30));
30/// ```
31#[derive(Debug)]
32pub struct JsonPathAssertion<'a> {
33 pub(crate) path_str: String,
34 pub(crate) current_values: Vec<Value>,
35 pub(crate) test: Option<&'a mut JsonTest<'a>>,
36}
37
38impl<'a> JsonPathAssertion<'a> {
39 pub(crate) fn new_with_test(test: &'a mut JsonTest<'a>, json: &'a Value, path: &str) -> Self {
40 let parsed_path = JsonPath::<Value>::from_str(path)
41 .unwrap_or_else(|e| panic!("Invalid JSONPath expression: {}", e));
42
43 let result = parsed_path.find(json);
44 let current_values = match result {
45 Value::Array(values) => {
46 if !path.contains('[') && values.len() == 1 {
47 vec![values[0].clone()]
48 } else {
49 values
50 }
51 }
52 Value::Null => vec![],
53 other => vec![other],
54 };
55
56 Self {
57 path_str: path.to_string(),
58 current_values,
59 test: Some(test),
60 }
61 }
62
63 #[cfg(test)]
64 pub fn new_for_test(json: &'a Value, path: &str) -> Self {
65 let parsed_path = JsonPath::<Value>::from_str(path)
66 .unwrap_or_else(|e| panic!("Invalid JSONPath expression: {}", e));
67
68 let result = parsed_path.find(json);
69 let current_values = match result {
70 Value::Array(values) => {
71 if !path.contains('[') && values.len() == 1 {
72 vec![values[0].clone()]
73 } else {
74 values
75 }
76 }
77 Value::Null => vec![],
78 other => vec![other],
79 };
80
81 Self {
82 path_str: path.to_string(),
83 current_values,
84 test: None,
85 }
86 }
87
88 /// Asserts that the path exists and has at least one value.
89 ///
90 /// # Examples
91 ///
92 /// ```rust
93 /// # use json_test::JsonTest;
94 /// # use serde_json::json;
95 /// # let data = json!({"user": {"name": "John"}});
96 /// # let mut test = JsonTest::new(&data);
97 /// test.assert_path("$.user.name")
98 /// .exists();
99 /// ```
100 ///
101 /// # Panics
102 ///
103 /// Panics if the path does not exist in the JSON structure.
104 pub fn exists(&'a mut self) -> &'a mut Self {
105 if self.current_values.is_empty() {
106 panic!("Path {} does not exist", self.path_str);
107 }
108 self
109 }
110
111 /// Asserts that the path does not exist or has no values.
112 ///
113 /// # Examples
114 ///
115 /// ```rust
116 /// # use json_test::JsonTest;
117 /// # use serde_json::json;
118 /// # let data = json!({"user": {"name": "John"}});
119 /// # let mut test = JsonTest::new(&data);
120 /// test.assert_path("$.user.email")
121 /// .does_not_exist();
122 /// ```
123 ///
124 /// # Panics
125 ///
126 /// Panics if the path exists in the JSON structure.
127 pub fn does_not_exist(&'a mut self) -> &'a mut Self {
128 if !self.current_values.is_empty() {
129 panic!("Path {} exists but should not. Found values: {:?}",
130 self.path_str, self.current_values);
131 }
132 self
133 }
134
135 /// Asserts that the value at the current path equals the expected value.
136 ///
137 /// # Examples
138 ///
139 /// ```rust
140 /// # use json_test::JsonTest;
141 /// # use serde_json::json;
142 /// # let data = json!({"user": {"name": "John"}});
143 /// # let mut test = JsonTest::new(&data);
144 /// test.assert_path("$.user.name")
145 /// .equals(json!("John"));
146 /// ```
147 ///
148 /// # Panics
149 ///
150 /// - Panics if no value exists at the path
151 /// - Panics if the value doesn't match the expected value
152 pub fn equals(&'a mut self, expected: Value) -> &'a mut Self {
153 match self.current_values.get(0) {
154 Some(actual) if actual == &expected => self,
155 Some(actual) => panic!(
156 "Value mismatch at {}\nExpected: {}\nActual: {}",
157 self.path_str, expected, actual
158 ),
159 None => panic!("No value found at {}", self.path_str),
160 }
161 }
162
163 /// Asserts that the value at the current path is a string.
164 ///
165 /// # Examples
166 ///
167 /// ```rust
168 /// # use json_test::JsonTest;
169 /// # use serde_json::json;
170 /// # let data = json!({"message": "Hello"});
171 /// # let mut test = JsonTest::new(&data);
172 /// test.assert_path("$.message")
173 /// .is_string();
174 /// ```
175 ///
176 /// # Panics
177 ///
178 /// - Panics if no value exists at the path
179 /// - Panics if the value is not a string
180 pub fn is_string(&'a mut self) -> &'a mut Self {
181 match self.current_values.get(0) {
182 Some(Value::String(_)) => self,
183 Some(v) => panic!("Expected string at {}, got {:?}", self.path_str, v),
184 None => panic!("No value found at {}", self.path_str),
185 }
186 }
187
188 /// Asserts that the string value contains the given substring.
189 ///
190 /// # Examples
191 ///
192 /// ```rust
193 /// # use json_test::JsonTest;
194 /// # use serde_json::json;
195 /// # let data = json!({"email": "test@example.com"});
196 /// # let mut test = JsonTest::new(&data);
197 /// test.assert_path("$.email")
198 /// .contains_string("@example");
199 /// ```
200 ///
201 /// # Panics
202 ///
203 /// - Panics if no value exists at the path
204 /// - Panics if the value is not a string
205 /// - Panics if the string does not contain the substring
206 pub fn contains_string(&'a mut self, substring: &str) -> &'a mut Self {
207 match self.current_values.get(0) {
208 Some(Value::String(s)) if s.contains(substring) => self,
209 Some(Value::String(s)) => panic!(
210 "String at {} does not contain '{}'\nActual: {}",
211 self.path_str, substring, s
212 ),
213 Some(v) => panic!("Expected string at {}, got {:?}", self.path_str, v),
214 None => panic!("No value found at {}", self.path_str),
215 }
216 }
217
218 /// Asserts that the string value starts with the given prefix.
219 ///
220 /// # Examples
221 ///
222 /// ```rust
223 /// # use json_test::JsonTest;
224 /// # use serde_json::json;
225 /// # let data = json!({"id": "user_123"});
226 /// # let mut test = JsonTest::new(&data);
227 /// test.assert_path("$.id")
228 /// .starts_with("user_");
229 /// ```
230 ///
231 /// # Panics
232 ///
233 /// - Panics if no value exists at the path
234 /// - Panics if the value is not a string
235 /// - Panics if the string does not start with the prefix
236 pub fn starts_with(&'a mut self, prefix: &str) -> &'a mut Self {
237 match self.current_values.get(0) {
238 Some(Value::String(s)) if s.starts_with(prefix) => self,
239 Some(Value::String(s)) => panic!(
240 "String at {} does not start with '{}'\nActual: {}",
241 self.path_str, prefix, s
242 ),
243 Some(v) => panic!("Expected string at {}, got {:?}", self.path_str, v),
244 None => panic!("No value found at {}", self.path_str),
245 }
246 }
247
248 /// Asserts that the string value ends with the given suffix.
249 ///
250 /// # Examples
251 ///
252 /// ```rust
253 /// # use json_test::JsonTest;
254 /// # use serde_json::json;
255 /// # let data = json!({"file": "document.pdf"});
256 /// # let mut test = JsonTest::new(&data);
257 /// test.assert_path("$.file")
258 /// .ends_with(".pdf");
259 /// ```
260 ///
261 /// # Panics
262 ///
263 /// - Panics if no value exists at the path
264 /// - Panics if the value is not a string
265 /// - Panics if the string does not end with the suffix
266 pub fn ends_with(&'a mut self, suffix: &str) -> &'a mut Self {
267 match self.current_values.get(0) {
268 Some(Value::String(s)) if s.ends_with(suffix) => self,
269 Some(Value::String(s)) => panic!(
270 "String at {} does not end with '{}'\nActual: {}",
271 self.path_str, suffix, s
272 ),
273 Some(v) => panic!("Expected string at {}, got {:?}", self.path_str, v),
274 None => panic!("No value found at {}", self.path_str),
275 }
276 }
277
278 /// Asserts that the string value matches the given regular expression pattern.
279 ///
280 /// # Examples
281 ///
282 /// ```rust
283 /// # use json_test::JsonTest;
284 /// # use serde_json::json;
285 /// # let data = json!({"email": "test@example.com"});
286 /// # let mut test = JsonTest::new(&data);
287 /// test.assert_path("$.email")
288 /// .matches_pattern(r"^[^@]+@[^@]+\.[^@]+$");
289 /// ```
290 ///
291 /// # Panics
292 ///
293 /// - Panics if no value exists at the path
294 /// - Panics if the value is not a string
295 /// - Panics if the pattern is invalid
296 /// - Panics if the string does not match the pattern
297
298 pub fn matches_pattern(&'a mut self, pattern: &str) -> &'a mut Self {
299 let regex = regex::Regex::new(pattern)
300 .unwrap_or_else(|e| panic!("Invalid regex pattern: {}", e));
301
302 match self.current_values.get(0) {
303 Some(Value::String(s)) if regex.is_match(s) => self,
304 Some(Value::String(s)) => panic!(
305 "String at {} does not match pattern '{}'\nActual: {}",
306 self.path_str, pattern, s
307 ),
308 Some(v) => panic!("Expected string at {}, got {:?}", self.path_str, v),
309 None => panic!("No value found at {}", self.path_str),
310 }
311 }
312
313 /// Asserts that the value at the current path is a number.
314 ///
315 /// # Examples
316 ///
317 /// ```rust
318 /// # use json_test::JsonTest;
319 /// # use serde_json::json;
320 /// # let data = json!({"count": 42});
321 /// # let mut test = JsonTest::new(&data);
322 /// test.assert_path("$.count")
323 /// .is_number();
324 /// ```
325 ///
326 /// # Panics
327 ///
328 /// - Panics if no value exists at the path
329 /// - Panics if the value is not a number
330 pub fn is_number(&'a mut self) -> &'a mut Self {
331 match self.current_values.get(0) {
332 Some(Value::Number(_)) => self,
333 Some(v) => panic!("Expected number at {}, got {:?}", self.path_str, v),
334 None => panic!("No value found at {}", self.path_str),
335 }
336 }
337
338 /// Asserts that the numeric value is greater than the given value.
339 ///
340 /// # Examples
341 ///
342 /// ```rust
343 /// # use json_test::JsonTest;
344 /// # use serde_json::json;
345 /// # let data = json!({"age": 21});
346 /// # let mut test = JsonTest::new(&data);
347 /// test.assert_path("$.age")
348 /// .is_greater_than(18);
349 /// ```
350 ///
351 /// # Panics
352 ///
353 /// - Panics if no value exists at the path
354 /// - Panics if the value is not a number
355 /// - Panics if the value is not greater than the given value
356 pub fn is_greater_than(&'a mut self, value: i64) -> &'a mut Self {
357 match self.current_values.get(0) {
358 Some(Value::Number(n)) if n.as_i64().map_or(false, |x| x > value) => self,
359 Some(Value::Number(n)) => panic!(
360 "Number at {} is not greater than {}\nActual: {}",
361 self.path_str, value, n
362 ),
363 Some(v) => panic!("Expected number at {}, got {:?}", self.path_str, v),
364 None => panic!("No value found at {}", self.path_str),
365 }
366 }
367
368 /// Asserts that the numeric value is less than the given value.
369 ///
370 /// # Examples
371 ///
372 /// ```rust
373 /// # use json_test::JsonTest;
374 /// # use serde_json::json;
375 /// # let data = json!({"temperature": 36});
376 /// # let mut test = JsonTest::new(&data);
377 /// test.assert_path("$.temperature")
378 /// .is_less_than(40);
379 /// ```
380 ///
381 /// # Panics
382 ///
383 /// - Panics if no value exists at the path
384 /// - Panics if the value is not a number
385 /// - Panics if the value is not less than the given value
386 pub fn is_less_than(&'a mut self, value: i64) -> &'a mut Self {
387 match self.current_values.get(0) {
388 Some(Value::Number(n)) if n.as_i64().map_or(false, |x| x < value) => self,
389 Some(Value::Number(n)) => panic!(
390 "Number at {} is not less than {}\nActual: {}",
391 self.path_str, value, n
392 ),
393 Some(v) => panic!("Expected number at {}, got {:?}", self.path_str, v),
394 None => panic!("No value found at {}", self.path_str),
395 }
396 }
397
398 /// Asserts that the numeric value is between the given minimum and maximum values (inclusive).
399 ///
400 /// # Examples
401 ///
402 /// ```rust
403 /// # use json_test::JsonTest;
404 /// # use serde_json::json;
405 /// # let data = json!({"score": 85});
406 /// # let mut test = JsonTest::new(&data);
407 /// test.assert_path("$.score")
408 /// .is_between(0, 100);
409 /// ```
410 ///
411 /// # Panics
412 ///
413 /// - Panics if no value exists at the path
414 /// - Panics if the value is not a number
415 /// - Panics if the value is not between min and max (inclusive)
416 pub fn is_between(&'a mut self, min: i64, max: i64) -> &'a mut Self {
417 match self.current_values.get(0) {
418 Some(Value::Number(n)) if n.as_i64().map_or(false, |x| x >= min && x <= max) => self,
419 Some(Value::Number(n)) => panic!(
420 "Number at {} is not between {} and {}\nActual: {}",
421 self.path_str, min, max, n
422 ),
423 Some(v) => panic!("Expected number at {}, got {:?}", self.path_str, v),
424 None => panic!("No value found at {}", self.path_str),
425 }
426 }
427
428 /// Asserts that the value at the current path is an array.
429 ///
430 /// # Examples
431 ///
432 /// ```rust
433 /// # use json_test::JsonTest;
434 /// # use serde_json::json;
435 /// # let data = json!({"tags": ["rust", "testing"]});
436 /// # let mut test = JsonTest::new(&data);
437 /// test.assert_path("$.tags")
438 /// .is_array();
439 /// ```
440 ///
441 /// # Panics
442 ///
443 /// - Panics if no value exists at the path
444 /// - Panics if the value is not an array
445 pub fn is_array(&'a mut self) -> &'a mut Self {
446 match self.current_values.get(0) {
447 Some(Value::Array(_)) => self,
448 Some(v) => panic!("Expected array at {}, got {:?}", self.path_str, v),
449 None => panic!("No value found at {}", self.path_str),
450 }
451 }
452
453 /// Asserts that the array has the expected length.
454 ///
455 /// # Examples
456 ///
457 /// ```rust
458 /// # use json_test::JsonTest;
459 /// # use serde_json::json;
460 /// # let data = json!({"tags": ["rust", "testing"]});
461 /// # let mut test = JsonTest::new(&data);
462 /// test.assert_path("$.tags")
463 /// .is_array()
464 /// .has_length(2);
465 /// ```
466 ///
467 /// # Panics
468 ///
469 /// - Panics if no value exists at the path
470 /// - Panics if the value is not an array
471 /// - Panics if the array length doesn't match the expected length
472 pub fn has_length(&'a mut self, expected: usize) -> &'a mut Self {
473 match self.current_values.get(0) {
474 Some(Value::Array(arr)) if arr.len() == expected => self,
475 Some(Value::Array(arr)) => panic!(
476 "Array at {} has wrong length\nExpected: {}\nActual: {}",
477 self.path_str, expected, arr.len()
478 ),
479 Some(v) => panic!("Expected array at {}, got {:?}", self.path_str, v),
480 None => panic!("No value found at {}", self.path_str),
481 }
482 }
483
484 /// Asserts that the array contains the expected value.
485 ///
486 /// # Examples
487 ///
488 /// ```rust
489 /// # use json_test::JsonTest;
490 /// # use serde_json::json;
491 /// # let data = json!({"roles": ["user", "admin"]});
492 /// # let mut test = JsonTest::new(&data);
493 /// test.assert_path("$.roles")
494 /// .is_array()
495 /// .contains(&json!("admin"));
496 /// ```
497 ///
498 /// # Panics
499 ///
500 /// - Panics if no value exists at the path
501 /// - Panics if the value is not an array
502 /// - Panics if the array does not contain the expected value
503 pub fn contains(&'a mut self, expected: &Value) -> &'a mut Self {
504 match self.current_values.get(0) {
505 Some(Value::Array(arr)) if arr.contains(expected) => self,
506 Some(Value::Array(arr)) => panic!(
507 "Array at {} does not contain expected value\nExpected: {}\nArray: {:?}",
508 self.path_str, expected, arr
509 ),
510 Some(v) => panic!("Expected array at {}, got {:?}", self.path_str, v),
511 None => panic!("No value found at {}", self.path_str),
512 }
513 }
514
515 /// Asserts that the value matches a custom predicate.
516 ///
517 /// This method allows for complex value validation using custom logic.
518 ///
519 /// # Examples
520 ///
521 /// ```rust
522 /// # use json_test::JsonTest;
523 /// # use serde_json::json;
524 /// # let data = json!({"timestamp": "2024-01-01T12:00:00Z"});
525 /// # let mut test = JsonTest::new(&data);
526 /// test.assert_path("$.timestamp")
527 /// .matches(|value| {
528 /// value.as_str()
529 /// .map(|s| s.contains("T") && s.ends_with("Z"))
530 /// .unwrap_or(false)
531 /// });
532 /// ```
533 ///
534 /// # Panics
535 ///
536 /// - Panics if no value exists at the path
537 /// - Panics if the value doesn't satisfy the predicate
538 pub fn matches<F>(&'a mut self, predicate: F) -> &'a mut Self
539 where
540 F: FnOnce(&Value) -> bool,
541 {
542 match self.current_values.get(0) {
543 Some(value) if predicate(value) => self,
544 Some(value) => panic!(
545 "Value at {} does not match predicate\nActual value: {}",
546 self.path_str, value
547 ),
548 None => panic!("No value found at {}", self.path_str),
549 }
550 }
551
552 /// Asserts that the value is an object and returns it for further testing.
553 ///
554 /// This method is primarily used internally by property assertions.
555 ///
556 /// # Examples
557 ///
558 /// ```rust
559 /// # use json_test::JsonTest;
560 /// # use serde_json::json;
561 /// # let data = json!({"user": {"name": "John", "age": 30}});
562 /// # let mut test = JsonTest::new(&data);
563 /// let obj = test.assert_path("$.user")
564 /// .assert_object();
565 /// assert!(obj.contains_key("name"));
566 /// ```
567 ///
568 /// # Panics
569 ///
570 /// - Panics if no value exists at the path
571 /// - Panics if the value is not an object
572 pub fn assert_object(&self) -> Map<String, Value> {
573 match &self.current_values[..] {
574 [Value::Object(obj)] => obj.clone(),
575 _ => panic!(
576 "Expected object at {}, got: {:?}",
577 self.path_str, self.current_values
578 ),
579 }
580 }
581
582 /// Creates a new assertion for a different path while maintaining the test context.
583 ///
584 /// This method enables chaining assertions across different paths.
585 ///
586 /// # Examples
587 ///
588 /// ```rust
589 /// # use json_test::{JsonTest, PropertyAssertions};
590 /// # use serde_json::json;
591 /// # let data = json!({
592 /// # "user": {"name": "John"},
593 /// # "settings": {"theme": "dark"}
594 /// # });
595 /// # let mut test = JsonTest::new(&data);
596 /// test.assert_path("$.user")
597 /// .has_property("name")
598 /// .assert_path("$.settings")
599 /// .has_property("theme");
600 /// ```
601 ///
602 /// # Panics
603 ///
604 /// - Panics if called on an assertion without test context
605 pub fn assert_path(&'a mut self, path: &str) -> JsonPathAssertion<'a> {
606 match &mut self.test {
607 Some(test) => test.assert_path(path),
608 None => panic!("Cannot chain assertions without JsonTest context"),
609 }
610 }
611}