Skip to main content

robinpath_modules/modules/
config_mod.rs

1use robinpath::{RobinPath, Value};
2use std::collections::HashMap;
3use std::sync::{Arc, Mutex};
4
5struct ConfigStore {
6    data: indexmap::IndexMap<String, Value>,
7    frozen: bool,
8}
9
10pub fn register(rp: &mut RobinPath) {
11    let state = Arc::new(Mutex::new(HashMap::<String, ConfigStore>::new()));
12
13    // config.create name defaults? → bool
14    let s = state.clone();
15    rp.register_builtin("config.create", move |args, _| {
16        let name = args.first().map(|v| v.to_display_string()).unwrap_or_else(|| "default".to_string());
17        let defaults = args.get(1).cloned().unwrap_or(Value::Null);
18        let data = if let Value::Object(obj) = defaults { obj } else { indexmap::IndexMap::new() };
19        s.lock().unwrap().insert(name, ConfigStore { data, frozen: false });
20        Ok(Value::Bool(true))
21    });
22
23    // config.load filePath name? → bool
24    let s = state.clone();
25    rp.register_builtin("config.load", move |args, _| {
26        let path = args.first().map(|v| v.to_display_string()).unwrap_or_default();
27        let name = args.get(1).map(|v| v.to_display_string()).unwrap_or_else(|| "default".to_string());
28        let content = std::fs::read_to_string(&path)
29            .map_err(|e| format!("config.load error: {}", e))?;
30        let data = if path.ends_with(".json") {
31            match serde_json::from_str::<serde_json::Value>(&content) {
32                Ok(v) => {
33                    if let Value::Object(obj) = Value::from(v) { obj }
34                    else { return Err("JSON must be an object".to_string()); }
35                }
36                Err(e) => return Err(format!("JSON parse error: {}", e)),
37            }
38        } else {
39            // .env format
40            let mut obj = indexmap::IndexMap::new();
41            for line in content.lines() {
42                let trimmed = line.trim();
43                if trimmed.is_empty() || trimmed.starts_with('#') { continue; }
44                if let Some(eq) = trimmed.find('=') {
45                    let key = trimmed[..eq].trim().to_string();
46                    let val = trimmed[eq + 1..].trim().trim_matches('"').trim_matches('\'').to_string();
47                    obj.insert(key, Value::String(val));
48                }
49            }
50            obj
51        };
52        let mut configs = s.lock().unwrap();
53        let store = configs.entry(name).or_insert_with(|| ConfigStore {
54            data: indexmap::IndexMap::new(), frozen: false,
55        });
56        if store.frozen { return Err("Config is frozen".to_string()); }
57        for (k, v) in data { store.data.insert(k, v); }
58        Ok(Value::Bool(true))
59    });
60
61    // config.loadEnv prefix name? → bool
62    let s = state.clone();
63    rp.register_builtin("config.loadEnv", move |args, _| {
64        let prefix = args.first().map(|v| v.to_display_string()).unwrap_or_default();
65        let name = args.get(1).map(|v| v.to_display_string()).unwrap_or_else(|| "default".to_string());
66        let mut configs = s.lock().unwrap();
67        let store = configs.entry(name).or_insert_with(|| ConfigStore {
68            data: indexmap::IndexMap::new(), frozen: false,
69        });
70        if store.frozen { return Err("Config is frozen".to_string()); }
71        for (key, value) in std::env::vars() {
72            if prefix.is_empty() || key.starts_with(&prefix) {
73                let config_key = if prefix.is_empty() { key } else {
74                    key[prefix.len()..].trim_start_matches('_').to_lowercase()
75                };
76                store.data.insert(config_key, Value::String(value));
77            }
78        }
79        Ok(Value::Bool(true))
80    });
81
82    // config.get path default? name? → value
83    let s = state.clone();
84    rp.register_builtin("config.get", move |args, _| {
85        let path = args.first().map(|v| v.to_display_string()).unwrap_or_default();
86        let default = args.get(1).cloned().unwrap_or(Value::Null);
87        let name = args.get(2).map(|v| v.to_display_string()).unwrap_or_else(|| "default".to_string());
88        let configs = s.lock().unwrap();
89        if let Some(store) = configs.get(&name) {
90            let val = get_by_path(&Value::Object(store.data.clone()), &path);
91            if matches!(val, Value::Null) { Ok(default) } else { Ok(val) }
92        } else {
93            Ok(default)
94        }
95    });
96
97    // config.set path value name? → bool
98    let s = state.clone();
99    rp.register_builtin("config.set", move |args, _| {
100        let path = args.first().map(|v| v.to_display_string()).unwrap_or_default();
101        let value = args.get(1).cloned().unwrap_or(Value::Null);
102        let name = args.get(2).map(|v| v.to_display_string()).unwrap_or_else(|| "default".to_string());
103        let mut configs = s.lock().unwrap();
104        let store = configs.entry(name).or_insert_with(|| ConfigStore {
105            data: indexmap::IndexMap::new(), frozen: false,
106        });
107        if store.frozen { return Err("Config is frozen".to_string()); }
108        set_by_path(&mut store.data, &path, value);
109        Ok(Value::Bool(true))
110    });
111
112    // config.getAll name? → object
113    let s = state.clone();
114    rp.register_builtin("config.getAll", move |args, _| {
115        let name = args.first().map(|v| v.to_display_string()).unwrap_or_else(|| "default".to_string());
116        let configs = s.lock().unwrap();
117        if let Some(store) = configs.get(&name) {
118            Ok(Value::Object(store.data.clone()))
119        } else {
120            Ok(Value::Object(indexmap::IndexMap::new()))
121        }
122    });
123
124    // config.merge data name? → bool
125    let s = state.clone();
126    rp.register_builtin("config.merge", move |args, _| {
127        let data = args.first().cloned().unwrap_or(Value::Null);
128        let name = args.get(1).map(|v| v.to_display_string()).unwrap_or_else(|| "default".to_string());
129        if let Value::Object(incoming) = data {
130            let mut configs = s.lock().unwrap();
131            let store = configs.entry(name).or_insert_with(|| ConfigStore {
132                data: indexmap::IndexMap::new(), frozen: false,
133            });
134            if store.frozen { return Err("Config is frozen".to_string()); }
135            for (k, v) in incoming { store.data.insert(k, v); }
136            Ok(Value::Bool(true))
137        } else {
138            Err("Data must be an object".to_string())
139        }
140    });
141
142    // config.has path name? → bool
143    let s = state.clone();
144    rp.register_builtin("config.has", move |args, _| {
145        let path = args.first().map(|v| v.to_display_string()).unwrap_or_default();
146        let name = args.get(1).map(|v| v.to_display_string()).unwrap_or_else(|| "default".to_string());
147        let configs = s.lock().unwrap();
148        if let Some(store) = configs.get(&name) {
149            let val = get_by_path(&Value::Object(store.data.clone()), &path);
150            Ok(Value::Bool(!matches!(val, Value::Null)))
151        } else {
152            Ok(Value::Bool(false))
153        }
154    });
155
156    // config.remove path name? → bool
157    let s = state.clone();
158    rp.register_builtin("config.remove", move |args, _| {
159        let path = args.first().map(|v| v.to_display_string()).unwrap_or_default();
160        let name = args.get(1).map(|v| v.to_display_string()).unwrap_or_else(|| "default".to_string());
161        let mut configs = s.lock().unwrap();
162        if let Some(store) = configs.get_mut(&name) {
163            if store.frozen { return Err("Config is frozen".to_string()); }
164            store.data.shift_remove(&path);
165            Ok(Value::Bool(true))
166        } else {
167            Ok(Value::Bool(false))
168        }
169    });
170
171    // config.clear name? → bool
172    let s = state.clone();
173    rp.register_builtin("config.clear", move |args, _| {
174        let name = args.first().map(|v| v.to_display_string()).unwrap_or_else(|| "default".to_string());
175        let mut configs = s.lock().unwrap();
176        if let Some(store) = configs.get_mut(&name) {
177            if store.frozen { return Err("Config is frozen".to_string()); }
178            store.data.clear();
179            Ok(Value::Bool(true))
180        } else {
181            Ok(Value::Bool(false))
182        }
183    });
184
185    // config.list → array of config names
186    let s = state.clone();
187    rp.register_builtin("config.list", move |_args, _| {
188        let configs = s.lock().unwrap();
189        let names: Vec<Value> = configs.keys().map(|k| Value::String(k.clone())).collect();
190        Ok(Value::Array(names))
191    });
192
193    // config.validate required name? → {valid, missing}
194    let s = state.clone();
195    rp.register_builtin("config.validate", move |args, _| {
196        let required = args.first().cloned().unwrap_or(Value::Null);
197        let name = args.get(1).map(|v| v.to_display_string()).unwrap_or_else(|| "default".to_string());
198        let configs = s.lock().unwrap();
199        let store = configs.get(&name);
200        let mut missing = Vec::new();
201        if let Value::Array(keys) = required {
202            for key in &keys {
203                let k = key.to_display_string();
204                let found = store.map(|s| {
205                    let val = get_by_path(&Value::Object(s.data.clone()), &k);
206                    !matches!(val, Value::Null)
207                }).unwrap_or(false);
208                if !found { missing.push(Value::String(k)); }
209            }
210        }
211        let mut obj = indexmap::IndexMap::new();
212        obj.insert("valid".to_string(), Value::Bool(missing.is_empty()));
213        obj.insert("missing".to_string(), Value::Array(missing));
214        Ok(Value::Object(obj))
215    });
216
217    // config.freeze name? → bool
218    let s = state.clone();
219    rp.register_builtin("config.freeze", move |args, _| {
220        let name = args.first().map(|v| v.to_display_string()).unwrap_or_else(|| "default".to_string());
221        let mut configs = s.lock().unwrap();
222        if let Some(store) = configs.get_mut(&name) {
223            store.frozen = true;
224            Ok(Value::Bool(true))
225        } else {
226            Ok(Value::Bool(false))
227        }
228    });
229
230    // config.toEnv name? prefix? → env format string
231    let s = state.clone();
232    rp.register_builtin("config.toEnv", move |args, _| {
233        let name = args.first().map(|v| v.to_display_string()).unwrap_or_else(|| "default".to_string());
234        let prefix = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
235        let configs = s.lock().unwrap();
236        if let Some(store) = configs.get(&name) {
237            let lines: Vec<String> = store.data.iter().map(|(k, v)| {
238                let env_key = if prefix.is_empty() {
239                    k.to_uppercase()
240                } else {
241                    format!("{}_{}", prefix.to_uppercase(), k.to_uppercase())
242                };
243                format!("{}={}", env_key, v.to_display_string())
244            }).collect();
245            Ok(Value::String(lines.join("\n")))
246        } else {
247            Ok(Value::String(String::new()))
248        }
249    });
250}
251
252fn get_by_path(val: &Value, path: &str) -> Value {
253    let mut current = val.clone();
254    for part in path.split('.') {
255        if let Value::Object(obj) = &current {
256            current = obj.get(part).cloned().unwrap_or(Value::Null);
257        } else {
258            return Value::Null;
259        }
260    }
261    current
262}
263
264fn set_by_path(obj: &mut indexmap::IndexMap<String, Value>, path: &str, value: Value) {
265    let parts: Vec<&str> = path.split('.').collect();
266    if parts.len() == 1 {
267        obj.insert(path.to_string(), value);
268        return;
269    }
270    let key = parts[0].to_string();
271    let rest = parts[1..].join(".");
272    if !obj.contains_key(&key) {
273        obj.insert(key.clone(), Value::Object(indexmap::IndexMap::new()));
274    }
275    if let Some(Value::Object(inner)) = obj.get_mut(&key) {
276        set_by_path(inner, &rest, value);
277    }
278}