1use indexmap::IndexMap;
2
3use crate::{
4 constants::QUOTED_KEY_MARKER,
5 types::{
6 is_identifier_segment,
7 JsonValue as Value,
8 PathExpansionMode,
9 ToonError,
10 ToonResult,
11 },
12};
13
14pub fn should_expand_key(key: &str, mode: PathExpansionMode) -> Option<Vec<String>> {
15 match mode {
16 PathExpansionMode::Off => None,
17 PathExpansionMode::Safe => {
18 if key.starts_with(QUOTED_KEY_MARKER) {
20 return None;
21 }
22
23 if !key.contains('.') {
24 return None;
25 }
26
27 let segments: Vec<String> = key.split('.').map(String::from).collect();
28
29 if segments.len() < 2 {
30 return None;
31 }
32
33 if segments.iter().all(|s| is_identifier_segment(s)) {
35 Some(segments)
36 } else {
37 None
38 }
39 }
40 }
41}
42
43pub fn deep_merge_value(
44 target: &mut IndexMap<String, Value>,
45 segments: &[String],
46 value: Value,
47 strict: bool,
48) -> ToonResult<()> {
49 if segments.is_empty() {
50 return Ok(());
51 }
52
53 if segments.len() == 1 {
54 let key = &segments[0];
55
56 if let Some(existing) = target.get(key) {
58 if strict {
59 return Err(ToonError::DeserializationError(format!(
60 "Path expansion conflict: key '{key}' already exists with value: {existing:?}",
61 )));
62 }
63 }
64
65 target.insert(key.clone(), value);
66 return Ok(());
67 }
68
69 let first_key = &segments[0];
70 let remaining_segments = &segments[1..];
71
72 let nested_obj = if let Some(existing_value) = target.get_mut(first_key) {
74 match existing_value {
75 Value::Object(obj) => obj,
76 _ => {
77 if strict {
78 return Err(ToonError::DeserializationError(format!(
79 "Path expansion conflict: key '{first_key}' exists as non-object: \
80 {existing_value:?}",
81 )));
82 }
83 *existing_value = Value::Object(IndexMap::new());
85 match existing_value {
86 Value::Object(obj) => obj,
87 _ => unreachable!(),
88 }
89 }
90 }
91 } else {
92 target.insert(first_key.clone(), Value::Object(IndexMap::new()));
93 match target.get_mut(first_key).unwrap() {
94 Value::Object(obj) => obj,
95 _ => unreachable!(),
96 }
97 };
98
99 deep_merge_value(nested_obj, remaining_segments, value, strict)
101}
102
103pub fn expand_paths_in_object(
104 obj: IndexMap<String, Value>,
105 mode: PathExpansionMode,
106 strict: bool,
107) -> ToonResult<IndexMap<String, Value>> {
108 let mut result = IndexMap::new();
109
110 for (key, mut value) in obj {
111 if let Value::Object(nested_obj) = value {
113 value = Value::Object(expand_paths_in_object(nested_obj, mode, strict)?);
114 }
115
116 let clean_key = if key.starts_with(QUOTED_KEY_MARKER) {
118 key.strip_prefix(QUOTED_KEY_MARKER).unwrap().to_string()
119 } else {
120 key.clone()
121 };
122
123 if let Some(segments) = should_expand_key(&key, mode) {
124 deep_merge_value(&mut result, &segments, value, strict)?;
125 } else {
126 if let Some(existing) = result.get(&clean_key) {
128 if strict {
129 return Err(ToonError::DeserializationError(format!(
130 "Key '{clean_key}' conflicts with existing value: {existing:?}",
131 )));
132 }
133 }
134 result.insert(clean_key, value);
135 }
136 }
137
138 Ok(result)
139}
140
141pub fn expand_paths_recursive(
142 value: Value,
143 mode: PathExpansionMode,
144 strict: bool,
145) -> ToonResult<Value> {
146 match value {
147 Value::Object(obj) => {
148 let expanded = expand_paths_in_object(obj, mode, strict)?;
149 Ok(Value::Object(expanded))
150 }
151 Value::Array(arr) => {
152 let expanded: Result<Vec<_>, _> = arr
153 .into_iter()
154 .map(|v| expand_paths_recursive(v, mode, strict))
155 .collect();
156 Ok(Value::Array(expanded?))
157 }
158 _ => Ok(value),
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use serde_json::json;
165
166 use super::*;
167
168 #[test]
169 fn test_should_expand_key_off_mode() {
170 assert!(should_expand_key("a.b.c", PathExpansionMode::Off).is_none());
171 }
172
173 #[test]
174 fn test_should_expand_key_safe_mode() {
175 assert_eq!(
177 should_expand_key("a.b", PathExpansionMode::Safe),
178 Some(vec!["a".to_string(), "b".to_string()])
179 );
180 assert_eq!(
181 should_expand_key("a.b.c", PathExpansionMode::Safe),
182 Some(vec!["a".to_string(), "b".to_string(), "c".to_string()])
183 );
184
185 assert!(should_expand_key("simple", PathExpansionMode::Safe).is_none());
187
188 assert!(should_expand_key("a.bad-key", PathExpansionMode::Safe).is_none());
190 assert!(should_expand_key("123.key", PathExpansionMode::Safe).is_none());
191 }
192
193 #[test]
194 fn test_deep_merge_simple() {
195 let mut target = IndexMap::new();
196 deep_merge_value(
197 &mut target,
198 &["a".to_string(), "b".to_string()],
199 Value::from(json!(1)),
200 true,
201 )
202 .unwrap();
203
204 let expected = json!({"a": {"b": 1}});
205 assert_eq!(Value::Object(target), Value::from(expected));
206 }
207
208 #[test]
209 fn test_deep_merge_multiple_paths() {
210 let mut target = IndexMap::new();
211
212 deep_merge_value(
213 &mut target,
214 &["a".to_string(), "b".to_string()],
215 Value::from(json!(1)),
216 true,
217 )
218 .unwrap();
219
220 deep_merge_value(
221 &mut target,
222 &["a".to_string(), "c".to_string()],
223 Value::from(json!(2)),
224 true,
225 )
226 .unwrap();
227
228 let expected = json!({"a": {"b": 1, "c": 2}});
229 assert_eq!(Value::Object(target), Value::from(expected));
230 }
231
232 #[test]
233 fn test_deep_merge_conflict_strict() {
234 let mut target = IndexMap::new();
235 target.insert("a".to_string(), Value::from(json!({"b": 1})));
236
237 let result = deep_merge_value(
238 &mut target,
239 &["a".to_string(), "b".to_string()],
240 Value::from(json!(2)),
241 true,
242 );
243
244 assert!(result.is_err());
245 }
246
247 #[test]
248 fn test_deep_merge_conflict_non_strict() {
249 let mut target = IndexMap::new();
250 target.insert("a".to_string(), Value::from(json!({"b": 1})));
251
252 deep_merge_value(
253 &mut target,
254 &["a".to_string(), "b".to_string()],
255 Value::from(json!(2)),
256 false,
257 )
258 .unwrap();
259
260 let expected = json!({"a": {"b": 2}});
261 assert_eq!(Value::Object(target), Value::from(expected));
262 }
263
264 #[test]
265 fn test_expand_paths_in_object() {
266 let mut obj = IndexMap::new();
267 obj.insert("a.b.c".to_string(), Value::from(json!(1)));
268 obj.insert("simple".to_string(), Value::from(json!(2)));
269
270 let result = expand_paths_in_object(obj, PathExpansionMode::Safe, true).unwrap();
271
272 let expected = json!({"a": {"b": {"c": 1}}, "simple": 2});
273 assert_eq!(Value::Object(result), Value::from(expected));
274 }
275
276 #[test]
277 fn test_expand_paths_with_merge() {
278 let mut obj = IndexMap::new();
279 obj.insert("a.b".to_string(), Value::from(json!(1)));
280 obj.insert("a.c".to_string(), Value::from(json!(2)));
281
282 let result = expand_paths_in_object(obj, PathExpansionMode::Safe, true).unwrap();
283
284 let expected = json!({"a": {"b": 1, "c": 2}});
285 assert_eq!(Value::Object(result), Value::from(expected));
286 }
287}