json_test/assertions/
property_assertions.rs

1use serde_json::Value;
2use crate::assertions::property_matcher::PropertyMatcher;
3
4/// Trait providing property testing capabilities for JSON objects.
5pub trait PropertyAssertions<'a> {
6    /// Asserts that the object has the specified property.
7    ///
8    /// # Examples
9    ///
10    /// ```rust
11    /// # use json_test::{JsonTest, PropertyAssertions};
12    /// # use serde_json::json;
13    /// # let data = json!({"user": {"name": "John"}});
14    /// # let mut test = JsonTest::new(&data);
15    /// test.assert_path("$.user")
16    ///     .has_property("name");
17    /// ```
18    ///
19    /// # Panics
20    ///
21    /// - Panics if the value is not an object
22    /// - Panics if the property doesn't exist
23    fn has_property(&'a mut self, name: &str) -> &'a mut Self;
24
25    /// Asserts that the object has all the specified properties.
26    ///
27    /// # Examples
28    ///
29    /// ```rust
30    /// # use json_test::{JsonTest, PropertyAssertions};
31    /// # use serde_json::json;
32    /// # let data = json!({"user": {"name": "John", "age": 30}});
33    /// # let mut test = JsonTest::new(&data);
34    /// test.assert_path("$.user")
35    ///     .has_properties(["name", "age"]);
36    /// ```
37    ///
38    /// # Panics
39    ///
40    /// - Panics if the value is not an object
41    /// - Panics if any of the properties don't exist
42    fn has_properties<I, S>(&'a mut self, names: I) -> &'a mut Self
43    where
44        I: IntoIterator<Item = S>,
45        S: AsRef<str>;
46
47    /// Asserts that the object has exactly the expected number of properties.
48    ///
49    /// # Examples
50    ///
51    /// ```rust
52    /// # use json_test::{JsonTest, PropertyAssertions};
53    /// # use serde_json::json;
54    /// # let data = json!({"user": {"name": "John", "age": 30}});
55    /// # let mut test = JsonTest::new(&data);
56    /// test.assert_path("$.user")
57    ///     .has_property_count(2);
58    /// ```
59    ///
60    /// # Panics
61    ///
62    /// - Panics if the value is not an object
63    /// - Panics if the number of properties doesn't match the expected countfn has_property_count(&'a mut self, expected: usize) -> &'a mut Self;
64    fn has_property_count(&'a mut self, expected: usize) -> &'a mut Self;
65
66    /// Asserts that the object has the expected number of properties matching a predicate.
67    ///
68    /// # Examples
69    ///
70    /// ```rust
71    /// # use json_test::{JsonTest, PropertyAssertions};
72    /// # use serde_json::json;
73    /// # let data = json!({"user": {"meta_created": "2024-01-01", "meta_updated": "2024-01-02", "name": "John"}});
74    /// # let mut test = JsonTest::new(&data);
75    /// test.assert_path("$.user")
76    ///     .has_property_count_matching(|key| key.starts_with("meta_"), 2);
77    /// ```
78    ///
79    /// # Panics
80    ///
81    /// - Panics if the value is not an object
82    /// - Panics if the number of matching properties doesn't equal the expected count
83
84    fn has_property_count_matching<F>(&'a mut self, predicate: F, expected: usize) -> &'a mut Self
85    where
86        F: Fn(&str) -> bool;
87
88    /// Asserts that a property has the expected value.
89    ///
90    /// # Examples
91    ///
92    /// ```rust
93    /// # use json_test::{JsonTest, PropertyAssertions};
94    /// # use serde_json::json;
95    /// # let data = json!({"user": {"name": "John", "age": 30}});
96    /// # let mut test = JsonTest::new(&data);
97    /// test.assert_path("$.user")
98    ///     .has_property_value("name", json!("John"));
99    /// ```
100    ///
101    /// # Panics
102    ///
103    /// - Panics if the value is not an object
104    /// - Panics if the property doesn't exist
105    /// - Panics if the property value doesn't match the expected value
106    fn has_property_value(&'a mut self, name: &str, expected: Value) -> &'a mut Self;
107
108    /// Asserts that a property's value satisfies a predicate.
109    ///
110    /// # Examples
111    ///
112    /// ```rust
113    /// # use json_test::{JsonTest, PropertyAssertions};
114    /// # use serde_json::json;
115    /// # let data = json!({"user": {"age": 30}});
116    /// # let mut test = JsonTest::new(&data);
117    /// test.assert_path("$.user")
118    ///     .has_property_matching("age", |v| v.as_u64().unwrap_or(0) > 18);
119    /// ```
120    ///
121    /// # Panics
122    ///
123    /// - Panics if the value is not an object
124    /// - Panics if the property doesn't exist
125    /// - Panics if the property value doesn't satisfy the predicate
126
127    fn has_property_matching<F>(&'a mut self, name: &str, predicate: F) -> &'a mut Self
128    where
129        F: Fn(&Value) -> bool;
130
131    /// Creates a PropertyMatcher for testing properties that match a predicate.
132    ///
133    /// # Examples
134    ///
135    /// ```rust
136    /// # use json_test::{JsonTest, PropertyAssertions};
137    /// # use serde_json::json;
138    /// # let data = json!({"user": {"meta_created": "2024-01-01", "meta_updated": "2024-01-02"}});
139    /// # let mut test = JsonTest::new(&data);
140    /// test.assert_path("$.user")
141    ///     .properties_matching(|key| key.starts_with("meta_"))
142    ///     .count(2)
143    ///     .and()
144    ///     .has_property_count(2);
145    /// ```
146    fn properties_matching<F>(&'a mut self, predicate: F) -> PropertyMatcher<'a>
147    where
148        F: Fn(&str) -> bool;
149}
150
151impl<'a> PropertyAssertions<'a> for super::base::JsonPathAssertion<'a> {
152    fn has_property(&'a mut self, name: &str) -> &'a mut Self {
153        let obj = self.assert_object();
154
155        if !obj.contains_key(name) {
156            let available = obj.keys()
157                .map(|s| s.as_str())
158                .collect::<Vec<_>>()
159                .join(", ");
160
161            panic!("Property '{}' not found at {}\nAvailable properties: {}",
162                   name, self.path_str, available);
163        }
164        self
165    }
166
167    fn has_properties<I, S>(&'_ mut self, names: I) -> &'_ mut Self
168    where
169        I: IntoIterator<Item = S>,
170        S: AsRef<str>,
171    {
172        let obj = self.assert_object();
173        let missing: Vec<String> = names.into_iter()
174            .filter(|name| !obj.contains_key(name.as_ref()))
175            .map(|name| name.as_ref().to_string())
176            .collect();
177
178        if !missing.is_empty() {
179            let available = obj.keys()
180                .map(|s| s.as_str())
181                .collect::<Vec<_>>()
182                .join(", ");
183
184            panic!("Missing properties at {}: {}\nAvailable properties: {}",
185                   self.path_str, missing.join(", "), available);
186        }
187        self
188    }
189
190    fn has_property_count(&'_ mut self, expected: usize) -> &'_ mut Self {
191        let obj = self.assert_object();
192        let actual = obj.len();
193
194        if actual != expected {
195            let properties = obj.keys()
196                .map(|s| s.as_str())
197                .collect::<Vec<_>>()
198                .join(", ");
199
200            panic!(
201                "Incorrect number of properties at {}\nExpected: {}\nActual: {}\nProperties: {}",
202                self.path_str, expected, actual, properties
203            );
204        }
205        self
206    }
207
208    fn has_property_count_matching<F>(&'_ mut self, predicate: F, expected: usize) -> &'_ mut Self
209    where
210        F: Fn(&str) -> bool,
211    {
212        let obj = self.assert_object();
213        let matching: Vec<&str> = obj.keys()
214            .filter(|k| predicate(k))
215            .map(|s| s.as_str())
216            .collect();
217
218        if matching.len() != expected {
219            panic!(
220                "Incorrect number of matching properties at {}\nExpected: {}\nActual: {}\nMatching properties: {}",
221                self.path_str, expected, matching.len(), matching.join(", ")
222            );
223        }
224        self
225    }
226
227    fn has_property_value(&'_ mut self, name: &str, expected: Value) -> &'_ mut Self {
228        let obj = self.assert_object();
229
230        match obj.get(name) {
231            Some(actual) if actual == &expected => self,
232            Some(actual) => {
233                panic!(
234                    "Property '{}' value mismatch at {}\nExpected: {}\nActual: {}",
235                    name, self.path_str, expected, actual
236                );
237            },
238            None => {
239                let available = obj.keys()
240                    .map(|s| s.as_str())
241                    .collect::<Vec<_>>()
242                    .join(", ");
243
244                panic!(
245                    "Property '{}' not found at {}\nAvailable properties: {}",
246                    name, self.path_str, available
247                );
248            }
249        }
250    }
251
252    fn has_property_matching<F>(&'_ mut self, name: &str, predicate: F) -> &'_ mut Self
253    where
254        F: Fn(&Value) -> bool,
255    {
256        let obj = self.assert_object();
257
258        match obj.get(name) {
259            Some(value) if predicate(value) => self,
260            Some(value) => {
261                panic!(
262                    "Property '{}' at {} does not match condition\nValue: {}",
263                    name, self.path_str, value
264                );
265            },
266            None => {
267                let available = obj.keys()
268                    .map(|s| s.as_str())
269                    .collect::<Vec<_>>()
270                    .join(", ");
271
272                panic!(
273                    "Property '{}' not found at {}\nAvailable properties: {}",
274                    name, self.path_str, available
275                );
276            }
277        }
278    }
279
280    fn properties_matching<F>(&'a mut self, predicate: F) -> PropertyMatcher<'a>
281    where
282        F: Fn(&str) -> bool,
283    {
284        let obj = self.assert_object();
285        let pairs: Vec<(String, Value)> = obj.iter()
286            .filter(|(k, _)| predicate(k))
287            .map(|(k, v)| (k.to_string(), v.clone()))
288            .collect();
289
290        PropertyMatcher::new(pairs, self)
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297    use crate::assertions::base::JsonPathAssertion;
298    use serde_json::json;
299
300    #[test]
301    fn test_property_assertions() {
302        let json = json!({
303            "user": {
304                "name": "John",
305                "age": 30,
306                "metadata": {
307                    "created_at": "2024-01-01",
308                    "updated_at": "2024-01-02"
309                }
310            }
311        });
312
313        let mut assertion = JsonPathAssertion::new_for_test(&json, "$.user");
314
315        assertion
316            .has_property("name")
317            .has_properties(vec!["name", "age"])
318            .has_property_count(3)
319            .has_property_value("name", json!("John"))
320            .has_property_matching("age", |v| v.as_u64().unwrap_or(0) > 20)
321            .has_property_count_matching(|k| k.ends_with("at"), 0);
322    }
323
324    #[test]
325    #[should_panic(expected = "Property 'email' not found")]
326    fn test_missing_property() {
327        let json = json!({"user": {"name": "John"}});
328        let mut assertion = JsonPathAssertion::new_for_test(&json, "$.user");
329        assertion.has_property("email");
330    }
331
332    #[test]
333    #[should_panic(expected = "Incorrect number of properties")]
334    fn test_property_count() {
335        let json = json!({"user": {"name": "John", "age": 30}});
336        let mut assertion = JsonPathAssertion::new_for_test(&json, "$.user");
337        assertion.has_property_count(1);
338    }
339
340    #[test]
341    #[should_panic(expected = "Property 'age' value mismatch")]
342    fn test_property_value_mismatch() {
343        let json = json!({"user": {"name": "John", "age": 30}});
344        let mut assertion = JsonPathAssertion::new_for_test(&json, "$.user");
345        assertion.has_property_value("age", json!(25));
346    }
347}