Skip to main content

ferro_json_ui/
data.rs

1//! Data path resolution for JSON-UI.
2//!
3//! Resolves slash-separated paths against JSON values, enabling components
4//! to reference dynamic data from handler payloads. Path format: `/segment/segment/...`
5//! where segments are object keys or array indices (numeric strings).
6
7use serde_json::Value;
8
9/// Resolves a slash-separated path against a JSON value.
10///
11/// Path format: `/segment/segment/...` where each segment is an object key
12/// or array index (numeric string). Leading slash is required for non-empty
13/// paths. Empty path or `"/"` returns the root value. Returns `None` if any
14/// segment fails to resolve.
15pub(crate) fn resolve_path<'a>(data: &'a Value, path: &str) -> Option<&'a Value> {
16    if path.is_empty() || path == "/" {
17        return Some(data);
18    }
19
20    let trimmed = path.strip_prefix('/').unwrap_or(path);
21    let segments: Vec<&str> = trimmed.split('/').collect();
22
23    let mut current = data;
24    for segment in segments {
25        if segment.is_empty() {
26            continue;
27        }
28        match current {
29            Value::Object(map) => {
30                current = map.get(segment)?;
31            }
32            Value::Array(arr) => {
33                let index: usize = segment.parse().ok()?;
34                current = arr.get(index)?;
35            }
36            _ => return None,
37        }
38    }
39
40    Some(current)
41}
42
43/// Resolves a path and converts the result to a string representation.
44///
45/// For `String` values, returns the string directly. For numbers and booleans,
46/// uses `to_string()`. For `null`, returns `None`. For objects and arrays,
47/// returns their JSON serialization.
48pub(crate) fn resolve_path_string(data: &Value, path: &str) -> Option<String> {
49    let value = resolve_path(data, path)?;
50    match value {
51        Value::String(s) => Some(s.clone()),
52        Value::Number(n) => Some(n.to_string()),
53        Value::Bool(b) => Some(b.to_string()),
54        Value::Null => None,
55        Value::Array(_) | Value::Object(_) => serde_json::to_string(value).ok(),
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62    use serde_json::json;
63
64    #[test]
65    fn simple_key_resolution() {
66        let data = json!({"name": "Alice"});
67        assert_eq!(resolve_path(&data, "/name"), Some(&json!("Alice")));
68    }
69
70    #[test]
71    fn nested_key_resolution() {
72        let data = json!({"user": {"name": "Bob"}});
73        assert_eq!(resolve_path(&data, "/user/name"), Some(&json!("Bob")));
74    }
75
76    #[test]
77    fn array_index_resolution() {
78        let data = json!({"users": [{"name": "Carol"}]});
79        assert_eq!(resolve_path(&data, "/users/0/name"), Some(&json!("Carol")));
80    }
81
82    #[test]
83    fn missing_key_returns_none() {
84        let data = json!({"name": "Alice"});
85        assert_eq!(resolve_path(&data, "/missing"), None);
86    }
87
88    #[test]
89    fn empty_path_returns_root() {
90        let data = json!({"name": "Alice"});
91        assert_eq!(resolve_path(&data, ""), Some(&data));
92    }
93
94    #[test]
95    fn root_slash_returns_root() {
96        let data = json!({"name": "Alice"});
97        assert_eq!(resolve_path(&data, "/"), Some(&data));
98    }
99
100    #[test]
101    fn numeric_value_resolution() {
102        let data = json!({"count": 42});
103        let result = resolve_path(&data, "/count");
104        assert_eq!(result, Some(&json!(42)));
105        assert!(result.unwrap().is_number());
106    }
107
108    #[test]
109    fn boolean_resolution() {
110        let data = json!({"active": true});
111        let result = resolve_path(&data, "/active");
112        assert_eq!(result, Some(&json!(true)));
113        assert!(result.unwrap().is_boolean());
114    }
115
116    #[test]
117    fn null_value_resolve_path() {
118        let data = json!({"deleted_at": null});
119        let result = resolve_path(&data, "/deleted_at");
120        assert_eq!(result, Some(&Value::Null));
121    }
122
123    #[test]
124    fn null_value_resolve_path_string_returns_none() {
125        let data = json!({"deleted_at": null});
126        assert_eq!(resolve_path_string(&data, "/deleted_at"), None);
127    }
128
129    #[test]
130    fn deep_nesting() {
131        let data = json!({"a": {"b": {"c": {"d": "deep"}}}});
132        assert_eq!(resolve_path(&data, "/a/b/c/d"), Some(&json!("deep")));
133    }
134
135    #[test]
136    fn invalid_array_index_returns_none() {
137        let data = json!({"items": [1, 2, 3]});
138        assert_eq!(resolve_path(&data, "/items/5"), None);
139        assert_eq!(resolve_path(&data, "/items/abc"), None);
140    }
141
142    #[test]
143    fn resolve_path_string_for_string() {
144        let data = json!({"name": "Alice"});
145        assert_eq!(
146            resolve_path_string(&data, "/name"),
147            Some("Alice".to_string())
148        );
149    }
150
151    #[test]
152    fn resolve_path_string_for_number() {
153        let data = json!({"count": 42});
154        assert_eq!(resolve_path_string(&data, "/count"), Some("42".to_string()));
155    }
156
157    #[test]
158    fn resolve_path_string_for_boolean() {
159        let data = json!({"active": true});
160        assert_eq!(
161            resolve_path_string(&data, "/active"),
162            Some("true".to_string())
163        );
164    }
165
166    #[test]
167    fn resolve_path_string_for_object() {
168        let data = json!({"user": {"name": "Alice"}});
169        let result = resolve_path_string(&data, "/user");
170        assert_eq!(result, Some(r#"{"name":"Alice"}"#.to_string()));
171    }
172
173    #[test]
174    fn resolve_path_string_for_array() {
175        let data = json!({"items": [1, 2, 3]});
176        let result = resolve_path_string(&data, "/items");
177        assert_eq!(result, Some("[1,2,3]".to_string()));
178    }
179
180    #[test]
181    fn resolve_path_string_missing_returns_none() {
182        let data = json!({"name": "Alice"});
183        assert_eq!(resolve_path_string(&data, "/missing"), None);
184    }
185
186    #[test]
187    fn resolve_path_on_non_object_non_array() {
188        let data = json!("just a string");
189        assert_eq!(resolve_path(&data, "/anything"), None);
190    }
191
192    #[test]
193    fn nested_array_access() {
194        let data = json!({"matrix": [[1, 2], [3, 4]]});
195        assert_eq!(resolve_path(&data, "/matrix/1/0"), Some(&json!(3)));
196    }
197}