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}