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