Skip to main content

webui_state/
lib.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT license.
3
4extern crate serde_json;
5
6use std::borrow::Cow;
7
8use serde_json::Value;
9
10/// Finds a value in a JSON object by dotted path and returns a borrowed value when possible.
11///
12/// Most lookups borrow directly from `state`. Synthetic values such as array
13/// and string `.length` are returned as owned values because they do not exist
14/// in the source JSON tree.
15#[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/// Finds a value in a JSON object by dotted path and returns an owned value.
40///
41/// Prefer [`find_value_by_dotted_path_ref`] in render-time hot paths so state
42/// values can be borrowed without cloning.
43#[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        // Create a JSON object
56        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        // Test a successful path
71        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        // Test a path that leads to single string
79        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        // Test a path that leads to an array
87        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        // Test a non-existent path
98        let value = find_value_by_dotted_path("name.middle", &data);
99        assert_eq!(value, None, "Failed to handle non-existent path.");
100
101        // Test a path that leads to a non-object value
102        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        // Get length of array.
109        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        // NodeJS: 'string_value.length' on "test" → 4
212        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}