Skip to main content

robinpath_modules/modules/
xml_mod.rs

1use robinpath::{RobinPath, Value};
2
3pub fn register(rp: &mut RobinPath) {
4    rp.register_builtin("xml.parse", |args, _| {
5        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
6        Ok(parse_xml(&s))
7    });
8
9    rp.register_builtin("xml.stringify", |args, _| {
10        let val = args.first().cloned().unwrap_or(Value::Null);
11        let indent = args.get(1).map(|v| v.to_number() as usize).unwrap_or(2);
12        Ok(Value::String(stringify_xml(&val, 0, indent)))
13    });
14
15    rp.register_builtin("xml.isValid", |args, _| {
16        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
17        Ok(Value::Bool(is_valid_xml(&s)))
18    });
19
20    rp.register_builtin("xml.query", |args, _| {
21        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
22        let path = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
23        let parsed = parse_xml(&s);
24        Ok(get_by_path(&parsed, &path))
25    });
26
27    rp.register_builtin("xml.toJSON", |args, _| {
28        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
29        let parsed = parse_xml(&s);
30        let json_val: serde_json::Value = parsed.into();
31        Ok(Value::String(serde_json::to_string_pretty(&json_val).unwrap_or_default()))
32    });
33
34    rp.register_builtin("xml.fromJSON", |args, _| {
35        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
36        match serde_json::from_str::<serde_json::Value>(&s) {
37            Ok(v) => {
38                let val = Value::from(v);
39                Ok(Value::String(stringify_xml(&val, 0, 2)))
40            }
41            Err(e) => Err(format!("xml.fromJSON error: {}", e)),
42        }
43    });
44
45    rp.register_builtin("xml.parseFile", |args, _| {
46        let path = args.first().map(|v| v.to_display_string()).unwrap_or_default();
47        match std::fs::read_to_string(&path) {
48            Ok(content) => Ok(parse_xml(&content)),
49            Err(e) => Err(format!("xml.parseFile error: {}", e)),
50        }
51    });
52
53    rp.register_builtin("xml.writeFile", |args, _| {
54        let path = args.first().map(|v| v.to_display_string()).unwrap_or_default();
55        let val = args.get(1).cloned().unwrap_or(Value::Null);
56        let xml_str = stringify_xml(&val, 0, 2);
57        match std::fs::write(&path, &xml_str) {
58            Ok(()) => Ok(Value::Bool(true)),
59            Err(e) => Err(format!("xml.writeFile error: {}", e)),
60        }
61    });
62
63    rp.register_builtin("xml.count", |args, _| {
64        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
65        let tag = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
66        let open_tag = format!("<{}", tag);
67        let count = s.matches(&open_tag).count();
68        Ok(Value::Number(count as f64))
69    });
70
71    rp.register_builtin("xml.getAttribute", |args, _| {
72        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
73        let _element = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
74        let attr = args.get(2).map(|v| v.to_display_string()).unwrap_or_default();
75        let pattern = format!("{}=\"", attr);
76        if let Some(start) = s.find(&pattern) {
77            let rest = &s[start + pattern.len()..];
78            if let Some(end) = rest.find('"') {
79                return Ok(Value::String(rest[..end].to_string()));
80            }
81        }
82        Ok(Value::Null)
83    });
84}
85
86fn parse_xml(xml: &str) -> Value {
87    use quick_xml::Reader;
88
89    let trimmed = xml.trim();
90    if trimmed.is_empty() {
91        return Value::Null;
92    }
93
94    let mut reader = Reader::from_str(trimmed);
95
96    // Parse into a tree structure
97    match parse_element_from_reader(&mut reader) {
98        Some(val) => val,
99        None => Value::Null,
100    }
101}
102
103fn parse_element_from_reader(reader: &mut quick_xml::Reader<&[u8]>) -> Option<Value> {
104    use quick_xml::events::Event;
105
106    let mut root = indexmap::IndexMap::new();
107
108    loop {
109        match reader.read_event() {
110            Ok(Event::Start(ref e)) => {
111                let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
112                let mut attrs = indexmap::IndexMap::new();
113
114                // Collect attributes
115                for attr_result in e.attributes() {
116                    if let Ok(attr) = attr_result {
117                        let key = format!("@{}", String::from_utf8_lossy(attr.key.as_ref()));
118                        let val = String::from_utf8_lossy(&attr.value).to_string();
119                        attrs.insert(key, Value::String(val));
120                    }
121                }
122
123                // Parse children
124                let children = parse_children_from_reader(reader, &tag_name);
125
126                if attrs.is_empty() {
127                    // If existing key, convert to array
128                    if root.contains_key(&tag_name) {
129                        let existing = root.swap_remove(&tag_name).unwrap();
130                        let arr = match existing {
131                            Value::Array(mut a) => { a.push(children); a }
132                            other => vec![other, children],
133                        };
134                        root.insert(tag_name, Value::Array(arr));
135                    } else {
136                        root.insert(tag_name, children);
137                    }
138                } else {
139                    // Merge attrs with children
140                    if let Value::Object(child_map) = children {
141                        for (k, v) in child_map {
142                            attrs.insert(k, v);
143                        }
144                    } else if children != Value::String(String::new()) {
145                        attrs.insert("#text".to_string(), children);
146                    }
147                    if root.contains_key(&tag_name) {
148                        let existing = root.swap_remove(&tag_name).unwrap();
149                        let arr = match existing {
150                            Value::Array(mut a) => { a.push(Value::Object(attrs)); a }
151                            other => vec![other, Value::Object(attrs)],
152                        };
153                        root.insert(tag_name, Value::Array(arr));
154                    } else {
155                        root.insert(tag_name, Value::Object(attrs));
156                    }
157                }
158            }
159            Ok(Event::Empty(ref e)) => {
160                let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
161                let mut attrs = indexmap::IndexMap::new();
162                for attr_result in e.attributes() {
163                    if let Ok(attr) = attr_result {
164                        let key = format!("@{}", String::from_utf8_lossy(attr.key.as_ref()));
165                        let val = String::from_utf8_lossy(&attr.value).to_string();
166                        attrs.insert(key, Value::String(val));
167                    }
168                }
169                let val = if attrs.is_empty() {
170                    Value::String(String::new())
171                } else {
172                    Value::Object(attrs)
173                };
174                root.insert(tag_name, val);
175            }
176            Ok(Event::Text(ref e)) => {
177                let text = e.unescape().map(|s| s.to_string()).unwrap_or_default();
178                let trimmed = text.trim();
179                if !trimmed.is_empty() {
180                    return Some(Value::String(trimmed.to_string()));
181                }
182            }
183            Ok(Event::CData(ref e)) => {
184                let text = String::from_utf8_lossy(e.as_ref()).to_string();
185                if !text.trim().is_empty() {
186                    return Some(Value::String(text));
187                }
188            }
189            Ok(Event::End(_)) | Ok(Event::Eof) => break,
190            Ok(Event::Decl(_)) | Ok(Event::PI(_)) | Ok(Event::Comment(_)) => continue,
191            Ok(Event::DocType(_)) => continue,
192            Err(_) => break,
193        }
194    }
195
196    if root.is_empty() {
197        None
198    } else {
199        Some(Value::Object(root))
200    }
201}
202
203fn parse_children_from_reader(reader: &mut quick_xml::Reader<&[u8]>, parent_tag: &str) -> Value {
204    use quick_xml::events::Event;
205
206    let mut children = indexmap::IndexMap::new();
207    let mut text_parts = Vec::new();
208
209    loop {
210        match reader.read_event() {
211            Ok(Event::Start(ref e)) => {
212                let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
213                let mut attrs = indexmap::IndexMap::new();
214
215                for attr_result in e.attributes() {
216                    if let Ok(attr) = attr_result {
217                        let key = format!("@{}", String::from_utf8_lossy(attr.key.as_ref()));
218                        let val = String::from_utf8_lossy(&attr.value).to_string();
219                        attrs.insert(key, Value::String(val));
220                    }
221                }
222
223                let inner = parse_children_from_reader(reader, &tag_name);
224
225                let child_val = if attrs.is_empty() {
226                    inner
227                } else {
228                    if let Value::Object(child_map) = inner {
229                        for (k, v) in child_map {
230                            attrs.insert(k, v);
231                        }
232                    } else if inner != Value::String(String::new()) {
233                        attrs.insert("#text".to_string(), inner);
234                    }
235                    Value::Object(attrs)
236                };
237
238                if children.contains_key(&tag_name) {
239                    let existing = children.swap_remove(&tag_name).unwrap();
240                    let arr = match existing {
241                        Value::Array(mut a) => { a.push(child_val); a }
242                        other => vec![other, child_val],
243                    };
244                    children.insert(tag_name, Value::Array(arr));
245                } else {
246                    children.insert(tag_name, child_val);
247                }
248            }
249            Ok(Event::Empty(ref e)) => {
250                let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
251                let mut attrs = indexmap::IndexMap::new();
252                for attr_result in e.attributes() {
253                    if let Ok(attr) = attr_result {
254                        let key = format!("@{}", String::from_utf8_lossy(attr.key.as_ref()));
255                        let val = String::from_utf8_lossy(&attr.value).to_string();
256                        attrs.insert(key, Value::String(val));
257                    }
258                }
259                let val = if attrs.is_empty() {
260                    Value::String(String::new())
261                } else {
262                    Value::Object(attrs)
263                };
264                children.insert(tag_name, val);
265            }
266            Ok(Event::Text(ref e)) => {
267                let text = e.unescape().map(|s| s.to_string()).unwrap_or_default();
268                let trimmed = text.trim();
269                if !trimmed.is_empty() {
270                    text_parts.push(trimmed.to_string());
271                }
272            }
273            Ok(Event::CData(ref e)) => {
274                let text = String::from_utf8_lossy(e.as_ref()).to_string();
275                if !text.trim().is_empty() {
276                    text_parts.push(text);
277                }
278            }
279            Ok(Event::End(ref e)) => {
280                let name = String::from_utf8_lossy(e.name().as_ref()).to_string();
281                if name == parent_tag {
282                    break;
283                }
284            }
285            Ok(Event::Eof) => break,
286            Ok(Event::Decl(_)) | Ok(Event::PI(_)) | Ok(Event::Comment(_)) | Ok(Event::DocType(_)) => continue,
287            Err(_) => break,
288        }
289    }
290
291    if !children.is_empty() {
292        if !text_parts.is_empty() {
293            children.insert("#text".to_string(), Value::String(text_parts.join(" ")));
294        }
295        Value::Object(children)
296    } else if !text_parts.is_empty() {
297        Value::String(text_parts.join(" "))
298    } else {
299        Value::String(String::new())
300    }
301}
302
303fn is_valid_xml(s: &str) -> bool {
304    use quick_xml::events::Event;
305    use quick_xml::Reader;
306
307    let trimmed = s.trim();
308    if trimmed.is_empty() || !trimmed.contains('<') {
309        return false;
310    }
311
312    let mut reader = Reader::from_str(trimmed);
313    let mut depth = 0i32;
314    let mut had_element = false;
315
316    loop {
317        match reader.read_event() {
318            Ok(Event::Start(_)) => { depth += 1; had_element = true; }
319            Ok(Event::End(_)) => {
320                depth -= 1;
321                if depth < 0 { return false; }
322            }
323            Ok(Event::Empty(_)) => { had_element = true; }
324            Ok(Event::Eof) => break,
325            Ok(_) => continue,
326            Err(_) => return false,
327        }
328    }
329
330    had_element && depth == 0
331}
332
333fn stringify_xml(val: &Value, depth: usize, indent: usize) -> String {
334    let pad = " ".repeat(depth * indent);
335    match val {
336        Value::Object(obj) => {
337            let mut lines = Vec::new();
338            for (key, value) in obj {
339                if key.starts_with('@') || key == "#text" {
340                    continue; // skip attributes and text in this pass
341                }
342                match value {
343                    Value::Object(inner) => {
344                        // Check for attributes
345                        let mut attr_str = String::new();
346                        let mut has_children = false;
347                        let mut text_val = None;
348                        for (ik, iv) in inner {
349                            if ik.starts_with('@') {
350                                attr_str.push_str(&format!(" {}=\"{}\"", &ik[1..], iv.to_display_string()));
351                            } else if ik == "#text" {
352                                text_val = Some(iv.to_display_string());
353                            } else {
354                                has_children = true;
355                            }
356                        }
357                        if has_children {
358                            lines.push(format!("{}<{}{}>", pad, key, attr_str));
359                            lines.push(stringify_xml(value, depth + 1, indent));
360                            lines.push(format!("{}</{}>", pad, key));
361                        } else if let Some(text) = text_val {
362                            lines.push(format!("{}<{}{}>{}</{}>", pad, key, attr_str, text, key));
363                        } else {
364                            lines.push(format!("{}<{}{}></{}>", pad, key, attr_str, key));
365                        }
366                    }
367                    Value::Array(arr) => {
368                        for item in arr {
369                            match item {
370                                Value::Object(_) => {
371                                    lines.push(format!("{}<{}>", pad, key));
372                                    lines.push(stringify_xml(item, depth + 1, indent));
373                                    lines.push(format!("{}</{}>", pad, key));
374                                }
375                                _ => {
376                                    lines.push(format!("{}<{}>{}</{}>", pad, key, item.to_display_string(), key));
377                                }
378                            }
379                        }
380                    }
381                    _ => {
382                        lines.push(format!("{}<{}>{}</{}>", pad, key, value.to_display_string(), key));
383                    }
384                }
385            }
386            lines.join("\n")
387        }
388        _ => format!("{}{}", pad, val.to_display_string()),
389    }
390}
391
392fn get_by_path(val: &Value, path: &str) -> Value {
393    let parts: Vec<&str> = path.split('.').collect();
394    let mut current = val.clone();
395    for part in parts {
396        if let Value::Object(obj) = &current {
397            current = obj.get(part).cloned().unwrap_or(Value::Null);
398        } else {
399            return Value::Null;
400        }
401    }
402    current
403}