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