1use crate::canon::Canon;
26use anyhow::Result;
27use serde_json::Value as JsonValue;
28use std::path::Path;
29
30pub trait Plugin: Send + Sync {
35 fn name(&self) -> &str;
37
38 fn on_load(&mut self, config: &JsonValue) -> Result<()>;
43
44 fn transform_canon(&self, canon: &mut Canon) -> Result<()>;
51
52 fn transform_output(&self, target: &str, value: &mut JsonValue) -> Result<()>;
60}
61
62pub struct PluginManager {
64 plugins: Vec<Box<dyn Plugin>>,
65 #[allow(dead_code)]
66 libraries: Vec<libloading::Library>,
67}
68
69impl PluginManager {
70 pub fn new() -> Self {
72 Self {
73 plugins: Vec::new(),
74 libraries: Vec::new(),
75 }
76 }
77
78 pub fn load_builtin(&mut self, name: &str, config: &JsonValue) -> Result<()> {
80 let mut plugin: Box<dyn Plugin> = match name {
81 "env-expander" => Box::new(EnvExpanderPlugin::new()),
82 _ => anyhow::bail!("unknown built-in plugin: {}", name),
83 };
84 plugin.on_load(config)?;
85 self.plugins.push(plugin);
86 Ok(())
87 }
88
89 pub unsafe fn load_dynamic(&mut self, path: &Path, config: &JsonValue) -> Result<()> {
94 let lib = unsafe { libloading::Library::new(path)? };
96 let create: libloading::Symbol<unsafe extern "C" fn() -> *mut dyn Plugin> =
97 unsafe { lib.get(b"create_plugin")? };
98 let raw = unsafe { create() };
100 let mut plugin = unsafe { Box::from_raw(raw) };
101 plugin.on_load(config)?;
102 self.plugins.push(plugin);
103 self.libraries.push(lib);
104 Ok(())
105 }
106
107 pub fn transform_canon(&self, canon: &mut Canon) -> Result<()> {
109 for plugin in &self.plugins {
110 plugin.transform_canon(canon)?;
111 }
112 Ok(())
113 }
114
115 pub fn transform_output(&self, target: &str, value: &mut JsonValue) -> Result<()> {
117 for plugin in &self.plugins {
118 plugin.transform_output(target, value)?;
119 }
120 Ok(())
121 }
122
123 pub fn count(&self) -> usize {
125 self.plugins.len()
126 }
127}
128
129impl Default for PluginManager {
130 fn default() -> Self {
131 Self::new()
132 }
133}
134
135pub struct EnvExpanderPlugin {
148 prefix: String,
149 suffix: String,
150}
151
152impl EnvExpanderPlugin {
153 pub fn new() -> Self {
155 Self {
156 prefix: "${".to_string(),
157 suffix: "}".to_string(),
158 }
159 }
160
161 fn expand_string(&self, s: &str) -> String {
162 let mut result = s.to_string();
163 let mut start = 0;
164
165 while let Some(prefix_pos) = result[start..].find(&self.prefix) {
166 let abs_prefix = start + prefix_pos;
167 let search_start = abs_prefix + self.prefix.len();
168
169 if let Some(suffix_pos) = result[search_start..].find(&self.suffix) {
170 let var_name = &result[search_start..search_start + suffix_pos];
171 if let Ok(value) = std::env::var(var_name) {
172 let full_pattern = format!("{}{}{}", self.prefix, var_name, self.suffix);
173 result = result.replace(&full_pattern, &value);
174 } else {
176 start = search_start + suffix_pos + self.suffix.len();
177 }
178 } else {
179 break;
180 }
181 }
182
183 result
184 }
185
186 fn expand_value(&self, value: &mut JsonValue) {
187 match value {
188 JsonValue::String(s) => {
189 *s = self.expand_string(s);
190 }
191 JsonValue::Array(arr) => {
192 for item in arr {
193 self.expand_value(item);
194 }
195 }
196 JsonValue::Object(obj) => {
197 for (_, v) in obj.iter_mut() {
198 self.expand_value(v);
199 }
200 }
201 _ => {}
202 }
203 }
204}
205
206impl Default for EnvExpanderPlugin {
207 fn default() -> Self {
208 Self::new()
209 }
210}
211
212impl Plugin for EnvExpanderPlugin {
213 fn name(&self) -> &str {
214 "env-expander"
215 }
216
217 fn on_load(&mut self, config: &JsonValue) -> Result<()> {
218 if let Some(prefix) = config.get("prefix").and_then(|v| v.as_str()) {
219 self.prefix = prefix.to_string();
220 }
221 if let Some(suffix) = config.get("suffix").and_then(|v| v.as_str()) {
222 self.suffix = suffix.to_string();
223 }
224 Ok(())
225 }
226
227 fn transform_canon(&self, canon: &mut Canon) -> Result<()> {
228 for server in canon.servers.values_mut() {
229 if let Some(cmd) = &mut server.command {
230 *cmd = self.expand_string(cmd);
231 }
232 if let Some(args) = &mut server.args {
233 for arg in args.iter_mut() {
234 *arg = self.expand_string(arg);
235 }
236 }
237 if let Some(url) = &mut server.url {
238 *url = self.expand_string(url);
239 }
240 if let Some(env) = &mut server.env {
241 for value in env.values_mut() {
242 *value = self.expand_string(value);
243 }
244 }
245 }
246 Ok(())
247 }
248
249 fn transform_output(&self, _target: &str, value: &mut JsonValue) -> Result<()> {
250 self.expand_value(value);
251 Ok(())
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258 use crate::canon::{Canon, CanonServer};
259 use std::collections::BTreeMap;
260
261 fn set_env(key: &str, value: &str) {
263 unsafe { std::env::set_var(key, value) };
265 }
266
267 fn remove_env(key: &str) {
268 unsafe { std::env::remove_var(key) };
270 }
271
272 #[test]
275 fn test_env_expander_new_default_delimiters() {
276 let plugin = EnvExpanderPlugin::new();
277 assert_eq!(plugin.prefix, "${");
278 assert_eq!(plugin.suffix, "}");
279 }
280
281 #[test]
282 fn test_env_expander_expand_string_simple() {
283 set_env("TEST_VAR_123", "hello");
284 let plugin = EnvExpanderPlugin::new();
285
286 let result = plugin.expand_string("prefix ${TEST_VAR_123} suffix");
287
288 assert_eq!(result, "prefix hello suffix");
289 remove_env("TEST_VAR_123");
290 }
291
292 #[test]
293 fn test_env_expander_expand_string_multiple() {
294 set_env("VAR_A", "first");
295 set_env("VAR_B", "second");
296 let plugin = EnvExpanderPlugin::new();
297
298 let result = plugin.expand_string("${VAR_A} and ${VAR_B}");
299
300 assert_eq!(result, "first and second");
301 remove_env("VAR_A");
302 remove_env("VAR_B");
303 }
304
305 #[test]
306 fn test_env_expander_expand_string_undefined_var() {
307 let plugin = EnvExpanderPlugin::new();
308 remove_env("UNDEFINED_VAR_XYZ");
309
310 let result = plugin.expand_string("value: ${UNDEFINED_VAR_XYZ}");
311
312 assert_eq!(result, "value: ${UNDEFINED_VAR_XYZ}");
314 }
315
316 #[test]
317 fn test_env_expander_expand_string_no_vars() {
318 let plugin = EnvExpanderPlugin::new();
319
320 let result = plugin.expand_string("no variables here");
321
322 assert_eq!(result, "no variables here");
323 }
324
325 #[test]
326 fn test_env_expander_expand_string_custom_delimiters() {
327 set_env("CUSTOM_VAR", "custom_value");
328 let mut plugin = EnvExpanderPlugin::new();
329 plugin.prefix = "{{".to_string();
330 plugin.suffix = "}}".to_string();
331
332 let result = plugin.expand_string("value: {{CUSTOM_VAR}}");
333
334 assert_eq!(result, "value: custom_value");
335 remove_env("CUSTOM_VAR");
336 }
337
338 #[test]
339 fn test_env_expander_on_load_custom_config() {
340 let mut plugin = EnvExpanderPlugin::new();
341 let config = serde_json::json!({
342 "prefix": "[[",
343 "suffix": "]]"
344 });
345
346 plugin.on_load(&config).unwrap();
347
348 assert_eq!(plugin.prefix, "[[");
349 assert_eq!(plugin.suffix, "]]");
350 }
351
352 #[test]
353 fn test_env_expander_on_load_partial_config() {
354 let mut plugin = EnvExpanderPlugin::new();
355 let config = serde_json::json!({
356 "prefix": "%%"
357 });
358
359 plugin.on_load(&config).unwrap();
360
361 assert_eq!(plugin.prefix, "%%");
362 assert_eq!(plugin.suffix, "}"); }
364
365 #[test]
366 fn test_env_expander_on_load_empty_config() {
367 let mut plugin = EnvExpanderPlugin::new();
368 let config = serde_json::json!({});
369
370 plugin.on_load(&config).unwrap();
371
372 assert_eq!(plugin.prefix, "${");
373 assert_eq!(plugin.suffix, "}");
374 }
375
376 #[test]
377 fn test_env_expander_transform_canon_command() {
378 set_env("CMD_PATH", "/usr/bin/custom");
379 let plugin = EnvExpanderPlugin::new();
380
381 let mut servers = BTreeMap::new();
382 servers.insert("test".to_string(), CanonServer {
383 kind: None,
384 command: Some("${CMD_PATH}".to_string()),
385 args: None,
386 env: None,
387 cwd: None,
388 url: None,
389 headers: None,
390 bearer_token_env_var: None,
391 enabled: None,
392 });
393 let mut canon = Canon { version: Some(1), servers, plugins: vec![] };
394
395 plugin.transform_canon(&mut canon).unwrap();
396
397 assert_eq!(canon.servers["test"].command, Some("/usr/bin/custom".to_string()));
398 remove_env("CMD_PATH");
399 }
400
401 #[test]
402 fn test_env_expander_transform_canon_args() {
403 set_env("ARG_VAL", "expanded_arg");
404 let plugin = EnvExpanderPlugin::new();
405
406 let mut servers = BTreeMap::new();
407 servers.insert("test".to_string(), CanonServer {
408 kind: None,
409 command: Some("cmd".to_string()),
410 args: Some(vec!["--value".to_string(), "${ARG_VAL}".to_string()]),
411 env: None,
412 cwd: None,
413 url: None,
414 headers: None,
415 bearer_token_env_var: None,
416 enabled: None,
417 });
418 let mut canon = Canon { version: Some(1), servers, plugins: vec![] };
419
420 plugin.transform_canon(&mut canon).unwrap();
421
422 let args = canon.servers["test"].args.as_ref().unwrap();
423 assert_eq!(args[1], "expanded_arg");
424 remove_env("ARG_VAL");
425 }
426
427 #[test]
428 fn test_env_expander_transform_canon_url() {
429 set_env("API_HOST", "api.example.com");
430 let plugin = EnvExpanderPlugin::new();
431
432 let mut servers = BTreeMap::new();
433 servers.insert("api".to_string(), CanonServer {
434 kind: Some("http".to_string()),
435 command: None,
436 args: None,
437 env: None,
438 cwd: None,
439 url: Some("https://${API_HOST}/v1".to_string()),
440 headers: None,
441 bearer_token_env_var: None,
442 enabled: None,
443 });
444 let mut canon = Canon { version: Some(1), servers, plugins: vec![] };
445
446 plugin.transform_canon(&mut canon).unwrap();
447
448 assert_eq!(canon.servers["api"].url, Some("https://api.example.com/v1".to_string()));
449 remove_env("API_HOST");
450 }
451
452 #[test]
453 fn test_env_expander_transform_output() {
454 set_env("OUT_VAL", "output_value");
455 let plugin = EnvExpanderPlugin::new();
456 let mut value = serde_json::json!({
457 "key": "${OUT_VAL}",
458 "nested": {
459 "inner": "${OUT_VAL}"
460 }
461 });
462
463 plugin.transform_output("test", &mut value).unwrap();
464
465 assert_eq!(value["key"], "output_value");
466 assert_eq!(value["nested"]["inner"], "output_value");
467 remove_env("OUT_VAL");
468 }
469
470 #[test]
471 fn test_env_expander_expand_value_array() {
472 set_env("ARR_VAL", "array_item");
473 let plugin = EnvExpanderPlugin::new();
474 let mut value = serde_json::json!(["static", "${ARR_VAL}", "other"]);
475
476 plugin.expand_value(&mut value);
477
478 assert_eq!(value[1], "array_item");
479 remove_env("ARR_VAL");
480 }
481
482 #[test]
483 fn test_env_expander_name() {
484 let plugin = EnvExpanderPlugin::new();
485 assert_eq!(plugin.name(), "env-expander");
486 }
487
488 #[test]
491 fn test_plugin_manager_new() {
492 let manager = PluginManager::new();
493 assert_eq!(manager.count(), 0);
494 }
495
496 #[test]
497 fn test_plugin_manager_default() {
498 let manager = PluginManager::default();
499 assert_eq!(manager.count(), 0);
500 }
501
502 #[test]
503 fn test_plugin_manager_load_builtin_env_expander() {
504 let mut manager = PluginManager::new();
505
506 manager.load_builtin("env-expander", &serde_json::json!({})).unwrap();
507
508 assert_eq!(manager.count(), 1);
509 }
510
511 #[test]
512 fn test_plugin_manager_load_builtin_unknown_error() {
513 let mut manager = PluginManager::new();
514
515 let result = manager.load_builtin("unknown-plugin", &serde_json::json!({}));
516
517 assert!(result.is_err());
518 }
519
520 #[test]
521 fn test_plugin_manager_transform_canon() {
522 set_env("MGR_TEST", "manager_value");
523 let mut manager = PluginManager::new();
524 manager.load_builtin("env-expander", &serde_json::json!({})).unwrap();
525
526 let mut servers = BTreeMap::new();
527 servers.insert("test".to_string(), CanonServer {
528 kind: None,
529 command: Some("${MGR_TEST}".to_string()),
530 args: None,
531 env: None,
532 cwd: None,
533 url: None,
534 headers: None,
535 bearer_token_env_var: None,
536 enabled: None,
537 });
538 let mut canon = Canon { version: Some(1), servers, plugins: vec![] };
539
540 manager.transform_canon(&mut canon).unwrap();
541
542 assert_eq!(canon.servers["test"].command, Some("manager_value".to_string()));
543 remove_env("MGR_TEST");
544 }
545
546 #[test]
547 fn test_plugin_manager_transform_output() {
548 set_env("OUT_MGR", "output_mgr");
549 let mut manager = PluginManager::new();
550 manager.load_builtin("env-expander", &serde_json::json!({})).unwrap();
551 let mut value = serde_json::json!({"key": "${OUT_MGR}"});
552
553 manager.transform_output("test", &mut value).unwrap();
554
555 assert_eq!(value["key"], "output_mgr");
556 remove_env("OUT_MGR");
557 }
558
559 #[test]
560 fn test_plugin_manager_multiple_plugins() {
561 let mut manager = PluginManager::new();
562 manager.load_builtin("env-expander", &serde_json::json!({})).unwrap();
563 assert_eq!(manager.count(), 1);
566 }
567}
568