Skip to main content

ai_agents_core/
dot_path.rs

1//! Dot-path navigation utility for serde_json::Value.
2
3use std::collections::HashMap;
4
5use serde_json::Value;
6
7use crate::{AgentError, Result};
8
9/// Get a nested value from a Value using dot-notation (e.g. "a.b.c").
10/// Returns None if any segment along the path is missing.
11pub fn get_dot_path<'a>(value: &'a Value, path: &str) -> Option<&'a Value> {
12    if path.is_empty() {
13        return None;
14    }
15    let parts: Vec<&str> = path.split('.').collect();
16
17    let mut current = value;
18    for part in &parts {
19        match current {
20            Value::Object(map) => {
21                current = map.get(*part)?;
22            }
23            _ => return None,
24        }
25    }
26    Some(current)
27}
28
29/// Get a nested value from a HashMap root using dot-notation.
30/// The first path segment is the map key; remaining segments traverse nested objects.
31pub fn get_dot_path_from_map(map: &HashMap<String, Value>, path: &str) -> Option<Value> {
32    if path.is_empty() {
33        return None;
34    }
35    let parts: Vec<&str> = path.split('.').collect();
36
37    let mut current: Option<&Value> = map.get(parts[0]);
38
39    for part in &parts[1..] {
40        match current {
41            Some(Value::Object(obj)) => {
42                current = obj.get(*part);
43            }
44            _ => return None,
45        }
46    }
47
48    current.cloned()
49}
50
51/// Set a nested value in a Value using dot-notation.
52/// Creates intermediate objects along the path if they do not exist.
53pub fn set_dot_path(mut root: Value, path: &str, new_value: Value) -> Result<Value> {
54    if path.is_empty() {
55        return Err(AgentError::Config("Empty dot-path".into()));
56    }
57    let parts: Vec<&str> = path.split('.').collect();
58
59    let mut current = &mut root;
60    for (i, part) in parts.iter().enumerate() {
61        if i == parts.len() - 1 {
62            match current {
63                Value::Object(map) => {
64                    map.insert((*part).to_string(), new_value);
65                    return Ok(root);
66                }
67                _ => {
68                    return Err(AgentError::Config(format!(
69                        "Cannot set field '{}': parent is not an object",
70                        path
71                    )));
72                }
73            }
74        }
75
76        if !current.is_object() {
77            return Err(AgentError::Config(format!(
78                "Cannot traverse '{}': segment '{}' is not an object",
79                path, part
80            )));
81        }
82
83        let map = current.as_object_mut().unwrap();
84        if !map.contains_key(*part) {
85            map.insert((*part).to_string(), Value::Object(serde_json::Map::new()));
86        }
87        current = map.get_mut(*part).unwrap();
88    }
89
90    Err(AgentError::Config(format!(
91        "Failed to set dot-path '{}'",
92        path
93    )))
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use serde_json::json;
100
101    // get_dot_path tests
102
103    #[test]
104    fn test_get_dot_path_simple() {
105        let val = json!({"a": {"b": {"c": 42}}});
106        assert_eq!(get_dot_path(&val, "a.b.c"), Some(&json!(42)));
107    }
108
109    #[test]
110    fn test_get_dot_path_top_level() {
111        let val = json!({"name": "Alice"});
112        assert_eq!(get_dot_path(&val, "name"), Some(&json!("Alice")));
113    }
114
115    #[test]
116    fn test_get_dot_path_missing() {
117        let val = json!({"a": {"b": 1}});
118        assert_eq!(get_dot_path(&val, "a.c"), None);
119        assert_eq!(get_dot_path(&val, "x.y.z"), None);
120    }
121
122    #[test]
123    fn test_get_dot_path_non_object() {
124        let val = json!({"a": 42});
125        assert_eq!(get_dot_path(&val, "a.b"), None);
126    }
127
128    // set_dot_path tests
129
130    #[test]
131    fn test_set_dot_path_simple() {
132        let val = json!({"a": {"b": 1}});
133        let result = set_dot_path(val, "a.b", json!(99)).unwrap();
134        assert_eq!(result, json!({"a": {"b": 99}}));
135    }
136
137    #[test]
138    fn test_set_dot_path_top_level() {
139        let val = json!({"name": "old"});
140        let result = set_dot_path(val, "name", json!("new")).unwrap();
141        assert_eq!(result, json!({"name": "new"}));
142    }
143
144    #[test]
145    fn test_set_dot_path_creates_intermediate() {
146        let val = json!({});
147        let result = set_dot_path(val, "a.b.c", json!(true)).unwrap();
148        assert_eq!(result, json!({"a": {"b": {"c": true}}}));
149    }
150
151    #[test]
152    fn test_set_dot_path_preserves_siblings() {
153        let val = json!({"a": {"b": 1, "c": 2}});
154        let result = set_dot_path(val, "a.b", json!(99)).unwrap();
155        assert_eq!(result, json!({"a": {"b": 99, "c": 2}}));
156    }
157
158    #[test]
159    fn test_set_dot_path_array_value() {
160        let val = json!({"traits": {"personality": ["shy"]}});
161        let result = set_dot_path(val, "traits.personality", json!(["bold", "brave"])).unwrap();
162        assert_eq!(
163            result,
164            json!({"traits": {"personality": ["bold", "brave"]}})
165        );
166    }
167
168    #[test]
169    fn test_roundtrip_get_set() {
170        let val = json!({"identity": {"name": "Alice", "role": "Guard"}});
171        let name = get_dot_path(&val, "identity.name").cloned().unwrap();
172        assert_eq!(name, json!("Alice"));
173
174        let updated = set_dot_path(val, "identity.name", json!("Bob")).unwrap();
175        assert_eq!(get_dot_path(&updated, "identity.name"), Some(&json!("Bob")));
176        assert_eq!(
177            get_dot_path(&updated, "identity.role"),
178            Some(&json!("Guard"))
179        );
180    }
181
182    // get_dot_path_from_map tests
183
184    #[test]
185    fn test_get_from_map_single_segment() {
186        let mut map = HashMap::new();
187        map.insert("name".to_string(), json!("Alice"));
188        assert_eq!(get_dot_path_from_map(&map, "name"), Some(json!("Alice")));
189    }
190
191    #[test]
192    fn test_get_from_map_multi_segment() {
193        let mut map = HashMap::new();
194        map.insert("user".to_string(), json!({"profile": {"age": 30}}));
195        assert_eq!(
196            get_dot_path_from_map(&map, "user.profile.age"),
197            Some(json!(30))
198        );
199    }
200
201    #[test]
202    fn test_get_from_map_missing_root() {
203        let map: HashMap<String, Value> = HashMap::new();
204        assert_eq!(get_dot_path_from_map(&map, "missing"), None);
205    }
206
207    #[test]
208    fn test_get_from_map_missing_nested() {
209        let mut map = HashMap::new();
210        map.insert("user".to_string(), json!({"name": "Alice"}));
211        assert_eq!(get_dot_path_from_map(&map, "user.email"), None);
212    }
213
214    #[test]
215    fn test_get_from_map_non_object_intermediate() {
216        let mut map = HashMap::new();
217        map.insert("count".to_string(), json!(42));
218        assert_eq!(get_dot_path_from_map(&map, "count.value"), None);
219    }
220
221    #[test]
222    fn test_set_dot_path_empty_path_error() {
223        let val = json!({});
224        assert!(set_dot_path(val, "", json!(1)).is_err());
225    }
226
227    #[test]
228    fn test_set_dot_path_non_object_parent_error() {
229        let val = json!({"a": 42});
230        assert!(set_dot_path(val, "a.b", json!(1)).is_err());
231    }
232}