Skip to main content

robinpath_modules/modules/
yaml_mod.rs

1use robinpath::{RobinPath, Value};
2
3pub fn register(rp: &mut RobinPath) {
4    rp.register_builtin("yaml.parse", |args, _| {
5        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
6        Ok(parse_yaml(&s))
7    });
8
9    rp.register_builtin("yaml.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_yaml(&val, 0, indent)))
13    });
14
15    rp.register_builtin("yaml.parseFile", |args, _| {
16        let path = args.first().map(|v| v.to_display_string()).unwrap_or_default();
17        match std::fs::read_to_string(&path) {
18            Ok(content) => Ok(parse_yaml(&content)),
19            Err(e) => Err(format!("yaml.parseFile error: {}", e)),
20        }
21    });
22
23    rp.register_builtin("yaml.writeFile", |args, _| {
24        let path = args.first().map(|v| v.to_display_string()).unwrap_or_default();
25        let val = args.get(1).cloned().unwrap_or(Value::Null);
26        let indent = args.get(2).map(|v| v.to_number() as usize).unwrap_or(2);
27        let yaml_str = stringify_yaml(&val, 0, indent);
28        match std::fs::write(&path, &yaml_str) {
29            Ok(()) => Ok(Value::Bool(true)),
30            Err(e) => Err(format!("yaml.writeFile error: {}", e)),
31        }
32    });
33
34    rp.register_builtin("yaml.isValid", |args, _| {
35        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
36        // Basic validation: try parsing
37        let parsed = parse_yaml(&s);
38        Ok(Value::Bool(!matches!(parsed, Value::Null) || s.trim() == "null" || s.trim() == "~"))
39    });
40
41    rp.register_builtin("yaml.get", |args, _| {
42        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
43        let path = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
44        let parsed = parse_yaml(&s);
45        Ok(get_by_path(&parsed, &path))
46    });
47
48    rp.register_builtin("yaml.toJSON", |args, _| {
49        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
50        let parsed = parse_yaml(&s);
51        let json_val: serde_json::Value = parsed.into();
52        Ok(Value::String(serde_json::to_string_pretty(&json_val).unwrap_or_default()))
53    });
54
55    rp.register_builtin("yaml.fromJSON", |args, _| {
56        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
57        match serde_json::from_str::<serde_json::Value>(&s) {
58            Ok(v) => {
59                let val = Value::from(v);
60                Ok(Value::String(stringify_yaml(&val, 0, 2)))
61            }
62            Err(e) => Err(format!("yaml.fromJSON error: {}", e)),
63        }
64    });
65
66    rp.register_builtin("yaml.parseAll", |args, _| {
67        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
68        let docs: Vec<Value> = s.split("\n---")
69            .map(|doc| parse_yaml(doc.trim_start_matches("---").trim()))
70            .collect();
71        Ok(Value::Array(docs))
72    });
73}
74
75fn parse_yaml(s: &str) -> Value {
76    let trimmed = s.trim();
77    if trimmed.is_empty() {
78        return Value::Null;
79    }
80    // Try JSON first (YAML is superset of JSON)
81    if (trimmed.starts_with('{') && trimmed.ends_with('}'))
82        || (trimmed.starts_with('[') && trimmed.ends_with(']'))
83    {
84        if let Ok(v) = serde_json::from_str::<serde_json::Value>(trimmed) {
85            return Value::from(v);
86        }
87    }
88    // Parse YAML line by line
89    parse_yaml_lines(&trimmed.lines().collect::<Vec<_>>(), 0).0
90}
91
92fn parse_yaml_lines(lines: &[&str], base_indent: usize) -> (Value, usize) {
93    let mut obj = indexmap::IndexMap::new();
94    let mut i = 0;
95    let mut is_array = false;
96    let mut arr = Vec::new();
97
98    while i < lines.len() {
99        let line = lines[i];
100        let stripped = line.trim();
101        if stripped.is_empty() || stripped.starts_with('#') {
102            i += 1;
103            continue;
104        }
105
106        let indent = line.len() - line.trim_start().len();
107        if indent < base_indent {
108            break;
109        }
110
111        if stripped.starts_with("- ") {
112            is_array = true;
113            let item_str = stripped[2..].trim();
114            if item_str.contains(": ") {
115                // Array of objects - inline
116                let mut item_obj = indexmap::IndexMap::new();
117                let (k, v) = split_kv(item_str);
118                item_obj.insert(k, parse_yaml_value(v));
119                // Check for continuation lines at deeper indent
120                let item_indent = indent + 2;
121                i += 1;
122                while i < lines.len() {
123                    let next = lines[i];
124                    let next_stripped = next.trim();
125                    let next_indent = next.len() - next.trim_start().len();
126                    if next_stripped.is_empty() || next_stripped.starts_with('#') {
127                        i += 1;
128                        continue;
129                    }
130                    if next_indent >= item_indent && !next_stripped.starts_with("- ") {
131                        let (nk, nv) = split_kv(next_stripped);
132                        item_obj.insert(nk, parse_yaml_value(nv));
133                        i += 1;
134                    } else {
135                        break;
136                    }
137                }
138                arr.push(Value::Object(item_obj));
139            } else {
140                arr.push(parse_yaml_value(item_str));
141                i += 1;
142            }
143            continue;
144        }
145
146        if stripped.contains(": ") || stripped.ends_with(':') {
147            let (key, value_str) = split_kv(stripped);
148            if value_str.is_empty() {
149                // Nested object or array
150                i += 1;
151                let child_indent = if i < lines.len() {
152                    lines[i].len() - lines[i].trim_start().len()
153                } else {
154                    indent + 2
155                };
156                let (child_val, consumed) = parse_yaml_lines(&lines[i..], child_indent);
157                obj.insert(key, child_val);
158                i += consumed;
159            } else {
160                obj.insert(key, parse_yaml_value(value_str));
161                i += 1;
162            }
163        } else {
164            i += 1;
165        }
166    }
167
168    if is_array {
169        (Value::Array(arr), i)
170    } else if obj.is_empty() {
171        (Value::Null, i)
172    } else {
173        (Value::Object(obj), i)
174    }
175}
176
177fn split_kv(s: &str) -> (String, &str) {
178    if let Some(pos) = s.find(": ") {
179        (s[..pos].trim().to_string(), s[pos + 2..].trim())
180    } else if s.ends_with(':') {
181        (s[..s.len() - 1].trim().to_string(), "")
182    } else {
183        (s.to_string(), "")
184    }
185}
186
187fn parse_yaml_value(s: &str) -> Value {
188    let trimmed = s.trim();
189    match trimmed {
190        "true" | "yes" | "on" => Value::Bool(true),
191        "false" | "no" | "off" => Value::Bool(false),
192        "null" | "~" | "" => Value::Null,
193        _ => {
194            // Try number
195            if let Ok(n) = trimmed.parse::<f64>() {
196                Value::Number(n)
197            } else {
198                // Remove surrounding quotes
199                let unquoted = trimmed.trim_matches('"').trim_matches('\'');
200                Value::String(unquoted.to_string())
201            }
202        }
203    }
204}
205
206fn stringify_yaml(val: &Value, depth: usize, indent: usize) -> String {
207    let pad = " ".repeat(depth * indent);
208    match val {
209        Value::Object(obj) => {
210            let mut lines = Vec::new();
211            for (k, v) in obj {
212                match v {
213                    Value::Object(_) => {
214                        lines.push(format!("{}{}:", pad, k));
215                        lines.push(stringify_yaml(v, depth + 1, indent));
216                    }
217                    Value::Array(arr) => {
218                        lines.push(format!("{}{}:", pad, k));
219                        let item_pad = " ".repeat((depth + 1) * indent);
220                        for item in arr {
221                            match item {
222                                Value::Object(_) | Value::Array(_) => {
223                                    lines.push(format!("{}- ", item_pad));
224                                    lines.push(stringify_yaml(item, depth + 2, indent));
225                                }
226                                _ => lines.push(format!("{}- {}", item_pad, format_yaml_value(item))),
227                            }
228                        }
229                    }
230                    _ => lines.push(format!("{}{}: {}", pad, k, format_yaml_value(v))),
231                }
232            }
233            lines.join("\n")
234        }
235        Value::Array(arr) => {
236            let mut lines = Vec::new();
237            for item in arr {
238                lines.push(format!("{}- {}", pad, format_yaml_value(item)));
239            }
240            lines.join("\n")
241        }
242        _ => format!("{}{}", pad, format_yaml_value(val)),
243    }
244}
245
246fn format_yaml_value(val: &Value) -> String {
247    match val {
248        Value::String(s) => {
249            if s.contains(':') || s.contains('#') || s.contains('\n') || s.starts_with(' ') {
250                format!("\"{}\"", s.replace('"', "\\\""))
251            } else {
252                s.clone()
253            }
254        }
255        Value::Number(n) => {
256            if *n == (*n as i64) as f64 {
257                format!("{}", *n as i64)
258            } else {
259                format!("{}", n)
260            }
261        }
262        Value::Bool(b) => if *b { "true" } else { "false" }.to_string(),
263        Value::Null => "null".to_string(),
264        _ => val.to_display_string(),
265    }
266}
267
268fn get_by_path(val: &Value, path: &str) -> Value {
269    let parts: Vec<&str> = path.split('.').collect();
270    let mut current = val.clone();
271    for part in parts {
272        if let Value::Object(obj) = &current {
273            current = obj.get(part).cloned().unwrap_or(Value::Null);
274        } else {
275            return Value::Null;
276        }
277    }
278    current
279}