Skip to main content

robinpath_modules/modules/
toml_mod.rs

1use robinpath::{RobinPath, Value};
2
3pub fn register(rp: &mut RobinPath) {
4    rp.register_builtin("toml.parse", |args, _| {
5        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
6        Ok(parse_toml(&s))
7    });
8
9    rp.register_builtin("toml.stringify", |args, _| {
10        let val = args.first().cloned().unwrap_or(Value::Null);
11        Ok(Value::String(stringify_toml(&val)))
12    });
13
14    rp.register_builtin("toml.parseFile", |args, _| {
15        let path = args.first().map(|v| v.to_display_string()).unwrap_or_default();
16        match std::fs::read_to_string(&path) {
17            Ok(content) => Ok(parse_toml(&content)),
18            Err(e) => Err(format!("toml.parseFile error: {}", e)),
19        }
20    });
21
22    rp.register_builtin("toml.writeFile", |args, _| {
23        let path = args.first().map(|v| v.to_display_string()).unwrap_or_default();
24        let val = args.get(1).cloned().unwrap_or(Value::Null);
25        let toml_str = stringify_toml(&val);
26        match std::fs::write(&path, &toml_str) {
27            Ok(()) => Ok(Value::Bool(true)),
28            Err(e) => Err(format!("toml.writeFile error: {}", e)),
29        }
30    });
31
32    rp.register_builtin("toml.get", |args, _| {
33        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
34        let path = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
35        let parsed = parse_toml(&s);
36        Ok(get_by_path(&parsed, &path))
37    });
38
39    rp.register_builtin("toml.isValid", |args, _| {
40        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
41        let parsed = parse_toml(&s);
42        Ok(Value::Bool(!matches!(parsed, Value::Null)))
43    });
44
45    rp.register_builtin("toml.toJSON", |args, _| {
46        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
47        let parsed = parse_toml(&s);
48        let json_val: serde_json::Value = parsed.into();
49        Ok(Value::String(serde_json::to_string_pretty(&json_val).unwrap_or_default()))
50    });
51
52    rp.register_builtin("toml.fromJSON", |args, _| {
53        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
54        match serde_json::from_str::<serde_json::Value>(&s) {
55            Ok(v) => Ok(Value::String(stringify_toml(&Value::from(v)))),
56            Err(e) => Err(format!("toml.fromJSON error: {}", e)),
57        }
58    });
59}
60
61fn parse_toml(s: &str) -> Value {
62    let mut result = indexmap::IndexMap::new();
63    let mut current_section = String::new();
64
65    for line in s.lines() {
66        let trimmed = line.trim();
67        if trimmed.is_empty() || trimmed.starts_with('#') {
68            continue;
69        }
70        // Table header [section] or [section.subsection]
71        if trimmed.starts_with('[') && trimmed.ends_with(']') && !trimmed.starts_with("[[") {
72            current_section = trimmed[1..trimmed.len() - 1].trim().to_string();
73            ensure_section(&mut result, &current_section);
74            continue;
75        }
76        // Array of tables [[section]]
77        if trimmed.starts_with("[[") && trimmed.ends_with("]]") {
78            current_section = trimmed[2..trimmed.len() - 2].trim().to_string();
79            // Array of tables handled as array
80            continue;
81        }
82        // Key-value pair
83        if let Some(eq_pos) = trimmed.find('=') {
84            let key = trimmed[..eq_pos].trim().to_string();
85            let value_str = trimmed[eq_pos + 1..].trim();
86            let value = parse_toml_value(value_str);
87
88            if current_section.is_empty() {
89                result.insert(key, value);
90            } else {
91                set_in_section(&mut result, &current_section, key, value);
92            }
93        }
94    }
95    if result.is_empty() {
96        Value::Null
97    } else {
98        Value::Object(result)
99    }
100}
101
102fn parse_toml_value(s: &str) -> Value {
103    let trimmed = s.trim();
104    // String
105    if (trimmed.starts_with('"') && trimmed.ends_with('"'))
106        || (trimmed.starts_with('\'') && trimmed.ends_with('\''))
107    {
108        return Value::String(trimmed[1..trimmed.len() - 1].to_string());
109    }
110    // Boolean
111    if trimmed == "true" { return Value::Bool(true); }
112    if trimmed == "false" { return Value::Bool(false); }
113    // Array
114    if trimmed.starts_with('[') && trimmed.ends_with(']') {
115        let inner = &trimmed[1..trimmed.len() - 1];
116        let items: Vec<Value> = inner.split(',')
117            .map(|item| parse_toml_value(item.trim()))
118            .filter(|v| !matches!(v, Value::String(s) if s.is_empty()))
119            .collect();
120        return Value::Array(items);
121    }
122    // Number
123    if let Ok(n) = trimmed.parse::<f64>() {
124        return Value::Number(n);
125    }
126    // Integer with underscores
127    let cleaned = trimmed.replace('_', "");
128    if let Ok(n) = cleaned.parse::<f64>() {
129        return Value::Number(n);
130    }
131    Value::String(trimmed.to_string())
132}
133
134fn ensure_section(result: &mut indexmap::IndexMap<String, Value>, section: &str) {
135    let parts: Vec<&str> = section.split('.').collect();
136    let mut current = result;
137    for part in parts {
138        if !current.contains_key(part) {
139            current.insert(part.to_string(), Value::Object(indexmap::IndexMap::new()));
140        }
141        if let Some(Value::Object(obj)) = current.get_mut(part) {
142            current = obj;
143        } else {
144            return;
145        }
146    }
147}
148
149fn set_in_section(result: &mut indexmap::IndexMap<String, Value>, section: &str, key: String, value: Value) {
150    let parts: Vec<&str> = section.split('.').collect();
151    let mut current = result;
152    for part in parts {
153        if let Some(Value::Object(obj)) = current.get_mut(part) {
154            current = obj;
155        } else {
156            return;
157        }
158    }
159    current.insert(key, value);
160}
161
162fn stringify_toml(val: &Value) -> String {
163    let mut lines = Vec::new();
164    if let Value::Object(obj) = val {
165        // First output top-level key-values
166        for (k, v) in obj {
167            if !matches!(v, Value::Object(_)) {
168                lines.push(format!("{} = {}", k, format_toml_value(v)));
169            }
170        }
171        if !lines.is_empty() {
172            lines.push(String::new());
173        }
174        // Then output sections
175        for (k, v) in obj {
176            if let Value::Object(inner) = v {
177                lines.push(format!("[{}]", k));
178                for (ik, iv) in inner {
179                    if let Value::Object(_) = iv {
180                        // Nested section
181                        lines.push(String::new());
182                        lines.push(format!("[{}.{}]", k, ik));
183                        stringify_toml_section(iv, &mut lines);
184                    } else {
185                        lines.push(format!("{} = {}", ik, format_toml_value(iv)));
186                    }
187                }
188                lines.push(String::new());
189            }
190        }
191    }
192    lines.join("\n").trim_end().to_string()
193}
194
195fn stringify_toml_section(val: &Value, lines: &mut Vec<String>) {
196    if let Value::Object(obj) = val {
197        for (k, v) in obj {
198            lines.push(format!("{} = {}", k, format_toml_value(v)));
199        }
200    }
201}
202
203fn format_toml_value(val: &Value) -> String {
204    match val {
205        Value::String(s) => format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"")),
206        Value::Number(n) => {
207            if *n == (*n as i64) as f64 { format!("{}", *n as i64) } else { format!("{}", n) }
208        }
209        Value::Bool(b) => if *b { "true" } else { "false" }.to_string(),
210        Value::Array(arr) => {
211            let items: Vec<String> = arr.iter().map(format_toml_value).collect();
212            format!("[{}]", items.join(", "))
213        }
214        Value::Null => "\"\"".to_string(),
215        _ => format!("\"{}\"", val.to_display_string()),
216    }
217}
218
219fn get_by_path(val: &Value, path: &str) -> Value {
220    let parts: Vec<&str> = path.split('.').collect();
221    let mut current = val.clone();
222    for part in parts {
223        if let Value::Object(obj) = &current {
224            current = obj.get(part).cloned().unwrap_or(Value::Null);
225        } else {
226            return Value::Null;
227        }
228    }
229    current
230}