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