Skip to main content

robinpath_modules/modules/
config_mod.rs

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