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}