1extern crate serde_json;
5
6use std::borrow::Cow;
7
8use serde_json::Value;
9
10#[must_use]
16pub fn find_value_by_dotted_path_ref<'a>(path: &str, state: &'a Value) -> Option<Cow<'a, Value>> {
17 let mut current_value: &Value = state;
18
19 for part in path.split('.') {
20 match current_value {
21 Value::Object(map) => {
22 current_value = map.get(part)?;
23 }
24 Value::Array(arr) if part == "length" => {
25 return Some(Cow::Owned(Value::Number(serde_json::Number::from(
26 arr.len(),
27 ))));
28 }
29 Value::String(s) if part == "length" => {
30 return Some(Cow::Owned(Value::Number(serde_json::Number::from(s.len()))));
31 }
32 _ => return None,
33 }
34 }
35
36 Some(Cow::Borrowed(current_value))
37}
38
39#[must_use]
44pub fn find_value_by_dotted_path(path: &str, state: &Value) -> Option<Value> {
45 find_value_by_dotted_path_ref(path, state).map(Cow::into_owned)
46}
47
48#[cfg(test)]
49mod tests {
50 use super::*;
51 use webui_test_utils::test_json;
52
53 #[test]
54 fn test_find_value_by_dotted_path() {
55 let data = test_json!({
57 "name": {
58 "first": "John",
59 "last": "Doe"
60 },
61 "favorite": {
62 "categories": {
63 "movies": ["The Matrix", "The Godfather"],
64 "music": ["Jazz", "Blues"]
65 }
66 },
67 "age": 30
68 });
69
70 let value = find_value_by_dotted_path("name.first", &data);
72 assert_eq!(
73 value,
74 Some(Value::String("John".to_string())),
75 "Failed to get string."
76 );
77
78 let value = find_value_by_dotted_path("age", &data);
80 assert_eq!(
81 value,
82 Some(Value::Number(serde_json::Number::from(30))),
83 "Failed to get number."
84 );
85
86 let value = find_value_by_dotted_path("favorite.categories.music", &data);
88 assert_eq!(
89 value,
90 Some(Value::Array(vec![
91 Value::String("Jazz".to_string()),
92 Value::String("Blues".to_string())
93 ])),
94 "Failed to get array."
95 );
96
97 let value = find_value_by_dotted_path("name.middle", &data);
99 assert_eq!(value, None, "Failed to handle non-existent path.");
100
101 let value = find_value_by_dotted_path("age.years", &data);
103 assert_eq!(
104 value, None,
105 "Failed to handle path that leads to a non-object value."
106 );
107
108 let value = find_value_by_dotted_path("favorite.categories.music.length", &data);
110 assert_eq!(
111 value,
112 Some(Value::Number(serde_json::Number::from(2))),
113 "Failed to get length of array."
114 );
115 }
116
117 #[test]
118 fn test_nested_nested_path() {
119 let data = test_json!({
120 "user": {
121 "profile": {
122 "nameObj": {
123 "first": "John"
124 }
125 }
126 }
127 });
128 let value = find_value_by_dotted_path("user.profile.nameObj.first", &data);
129 assert_eq!(value, Some(Value::String("John".to_string())));
130 }
131
132 #[test]
133 fn test_empty_string_path() {
134 let data = test_json!({ "key": "value" });
135 let value = find_value_by_dotted_path("", &data);
136 assert_eq!(value, None);
137 }
138
139 #[test]
140 fn test_path_with_spaces() {
141 let data = test_json!({
142 "user": {
143 "profile": {
144 "name": "Alice"
145 }
146 }
147 });
148 let value = find_value_by_dotted_path("user.profile.non existent", &data);
149 assert_eq!(value, None);
150 }
151
152 #[test]
153 fn test_numeric_string_token() {
154 let data = test_json!({ "numeric_string": "123" });
155 let value = find_value_by_dotted_path("numeric_string", &data);
156 assert_eq!(value, Some(Value::String("123".to_string())));
157 }
158
159 #[test]
160 fn test_length_on_empty_list() {
161 let data = test_json!({ "empty_list": [] });
162 let value = find_value_by_dotted_path("empty_list.length", &data);
163 assert_eq!(value, Some(Value::Number(serde_json::Number::from(0))));
164 }
165
166 #[test]
167 fn test_length_on_nonempty_list() {
168 let data = test_json!({ "items": [1, 2, 3] });
169 let value = find_value_by_dotted_path("items.length", &data);
170 assert_eq!(value, Some(Value::Number(serde_json::Number::from(3))));
171 }
172
173 #[test]
174 fn test_length_named_property() {
175 let data = test_json!({
176 "nestedlength": {
177 "length": "some value"
178 }
179 });
180 let value = find_value_by_dotted_path("nestedlength.length", &data);
181 assert_eq!(value, Some(Value::String("some value".to_string())));
182 }
183
184 #[test]
185 fn test_length_on_nested_list() {
186 let data = test_json!({
187 "nested": {
188 "data": ["a", "b"]
189 }
190 });
191 let value = find_value_by_dotted_path("nested.data.length", &data);
192 assert_eq!(value, Some(Value::Number(serde_json::Number::from(2))));
193 }
194
195 #[test]
196 fn test_nonexistent_path_with_length() {
197 let data = test_json!({ "existing": "value" });
198 let value = find_value_by_dotted_path("non_existent.length", &data);
199 assert_eq!(value, None);
200 }
201
202 #[test]
203 fn test_length_on_string() {
204 let data = test_json!({ "str_val": "hello" });
205 let value = find_value_by_dotted_path("str_val.length", &data);
206 assert_eq!(value, Some(Value::Number(serde_json::Number::from(5))));
207 }
208
209 #[test]
210 fn test_length_on_string_matches_nodejs() {
211 let data = test_json!({ "string_value": "test" });
213 let value = find_value_by_dotted_path("string_value.length", &data);
214 assert_eq!(value, Some(Value::Number(serde_json::Number::from(4))));
215 }
216
217 #[test]
218 fn test_array_index_not_resolved() {
219 let data = test_json!({
220 "foo": {
221 "bar": [10, 20, 30]
222 }
223 });
224 let value = find_value_by_dotted_path("foo.bar.0", &data);
225 assert_eq!(value, None);
226 }
227
228 #[test]
229 fn test_negative_array_index_not_resolved() {
230 let data = test_json!({
231 "foo": {
232 "bar": [10, 20, 30]
233 }
234 });
235 let value = find_value_by_dotted_path("foo.bar.-1", &data);
236 assert_eq!(value, None);
237 }
238
239 #[test]
240 fn test_numeric_property_on_object() {
241 let data = test_json!({
242 "foo": {
243 "barObj": {
244 "0": "not an array"
245 }
246 }
247 });
248 let value = find_value_by_dotted_path("foo.barObj.0", &data);
249 assert_eq!(value, Some(Value::String("not an array".to_string())));
250 }
251
252 #[test]
253 fn test_property_value_zero() {
254 let data = test_json!({
255 "foo": {
256 "zero": 0
257 }
258 });
259 let value = find_value_by_dotted_path("foo.zero", &data);
260 assert_eq!(value, Some(Value::Number(serde_json::Number::from(0))));
261 }
262
263 #[test]
264 fn test_property_value_false() {
265 let data = test_json!({
266 "foo": {
267 "isFalse": false
268 }
269 });
270 let value = find_value_by_dotted_path("foo.isFalse", &data);
271 assert_eq!(value, Some(Value::Bool(false)));
272 }
273
274 #[test]
275 fn test_property_value_empty_string() {
276 let data = test_json!({
277 "foo": {
278 "empty": ""
279 }
280 });
281 let value = find_value_by_dotted_path("foo.empty", &data);
282 assert_eq!(value, Some(Value::String("".to_string())));
283 }
284
285 #[test]
286 fn test_deeply_nested_property() {
287 let data = test_json!({
288 "a": {
289 "b": {
290 "c": {
291 "d": {
292 "e": {
293 "f": 42
294 }
295 }
296 }
297 }
298 }
299 });
300 let value = find_value_by_dotted_path("a.b.c.d.e.f", &data);
301 assert_eq!(value, Some(Value::Number(serde_json::Number::from(42))));
302 }
303
304 #[test]
305 fn test_property_with_special_char() {
306 let data = test_json!({
307 "foo": {
308 "$bar": 123
309 }
310 });
311 let value = find_value_by_dotted_path("foo.$bar", &data);
312 assert_eq!(value, Some(Value::Number(serde_json::Number::from(123))));
313 }
314
315 #[test]
316 fn test_property_named_tostring() {
317 let data = test_json!({
318 "foo": {
319 "toString": "stringify"
320 }
321 });
322 let value = find_value_by_dotted_path("foo.toString", &data);
323 assert_eq!(value, Some(Value::String("stringify".to_string())));
324 }
325
326 #[test]
327 fn test_intermediate_property_not_found() {
328 let data = test_json!({
329 "foo": {
330 "bar": "baz"
331 }
332 });
333 let value = find_value_by_dotted_path("foo.undefinedMid.baz", &data);
334 assert_eq!(value, None);
335 }
336
337 #[test]
338 fn test_string_in_deeply_nested_path() {
339 let data = test_json!({
340 "first": {
341 "second": {
342 "third": {
343 "data": "deep_string"
344 }
345 }
346 }
347 });
348 let value = find_value_by_dotted_path("first.second.third.data", &data);
349 assert_eq!(value, Some(Value::String("deep_string".to_string())));
350 }
351
352 #[test]
353 fn test_double_value_directly() {
354 let data = test_json!({ "double_value": 1.23 });
355 let value = find_value_by_dotted_path("double_value", &data);
356 assert_eq!(
357 value,
358 Some(Value::Number(serde_json::Number::from_f64(1.23).unwrap()))
359 );
360 }
361
362 #[test]
363 fn test_ref_lookup_borrows_existing_value() {
364 let data = test_json!({
365 "user": {
366 "name": "Alice"
367 }
368 });
369 let value = find_value_by_dotted_path_ref("user.name", &data).expect("value");
370 let expected = data
371 .get("user")
372 .and_then(|user| user.get("name"))
373 .expect("expected value");
374
375 assert!(matches!(value, std::borrow::Cow::Borrowed(_)));
376 assert!(std::ptr::eq(value.as_ref(), expected));
377 }
378
379 #[test]
380 fn test_ref_lookup_owns_synthetic_length() {
381 let data = test_json!({ "items": [1, 2, 3] });
382 let value = find_value_by_dotted_path_ref("items.length", &data).expect("value");
383
384 assert!(matches!(value, std::borrow::Cow::Owned(_)));
385 assert_eq!(value.as_ref(), &Value::Number(serde_json::Number::from(3)));
386 }
387}