robinpath_modules/modules/
config_mod.rs1use 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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) = ¤t {
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}