ai_agents_core/
dot_path.rs1use std::collections::HashMap;
4
5use serde_json::Value;
6
7use crate::{AgentError, Result};
8
9pub 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
29pub 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
51pub 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 #[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 #[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 #[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}