Skip to main content

gatel_core/
plugin.rs

1//! Dynamic module/plugin system for gatel.
2//!
3//! External crates can implement the [`ModuleLoader`] trait to register
4//! custom middleware and handlers. Modules are loaded during configuration
5//! parsing and can participate in the request/response lifecycle.
6
7use std::collections::HashMap;
8use std::sync::Arc;
9
10use crate::ProxyError;
11
12/// Result type for module operations.
13pub type ModuleResult<T> = Result<T, ProxyError>;
14
15/// A module loader creates module instances from configuration.
16///
17/// Implement this trait to add custom functionality to gatel. Each loader
18/// is responsible for a named directive (e.g., "my-custom-middleware") and
19/// creates `Module` instances when that directive appears in the config.
20pub trait ModuleLoader: Send + Sync {
21    /// The directive name that triggers this module (e.g., "waf", "graphql").
22    fn name(&self) -> &str;
23
24    /// Validate the configuration for this module.
25    /// Called during config parsing before the server starts.
26    /// Return Ok(()) if the config is valid, or an error describing the issue.
27    fn validate_config(&self, config: &HashMap<String, String>) -> ModuleResult<()> {
28        let _ = config;
29        Ok(())
30    }
31
32    /// Create a middleware instance from the given configuration.
33    /// Return None if this module does not provide middleware.
34    fn create_middleware(
35        &self,
36        config: &HashMap<String, String>,
37    ) -> ModuleResult<Option<Arc<dyn salvo::Handler>>> {
38        let _ = config;
39        Ok(None)
40    }
41
42    /// Create a handler instance from the given configuration.
43    /// Return None if this module does not provide a terminal handler.
44    fn create_handler(
45        &self,
46        config: &HashMap<String, String>,
47    ) -> ModuleResult<Option<Arc<dyn salvo::Handler>>> {
48        let _ = config;
49        Ok(None)
50    }
51
52    /// Called once when the module is loaded (server startup).
53    fn on_load(&self) -> ModuleResult<()> {
54        Ok(())
55    }
56
57    /// Called when the server is shutting down.
58    fn on_unload(&self) -> ModuleResult<()> {
59        Ok(())
60    }
61
62    /// Called when configuration is reloaded (hot-reload).
63    fn on_reload(&self, config: &HashMap<String, String>) -> ModuleResult<()> {
64        let _ = config;
65        Ok(())
66    }
67}
68
69/// Registry of available module loaders.
70///
71/// Modules are registered at startup and looked up by name during
72/// configuration parsing. The registry is thread-safe and can be
73/// shared across the application.
74pub struct ModuleRegistry {
75    loaders: HashMap<String, Box<dyn ModuleLoader>>,
76}
77
78impl ModuleRegistry {
79    /// Create an empty registry.
80    pub fn new() -> Self {
81        Self {
82            loaders: HashMap::new(),
83        }
84    }
85
86    /// Register a module loader. If a loader with the same name already
87    /// exists, it is replaced.
88    pub fn register(&mut self, loader: Box<dyn ModuleLoader>) {
89        let name = loader.name().to_string();
90        tracing::info!(module = %name, "registered module loader");
91        self.loaders.insert(name, loader);
92    }
93
94    /// Look up a loader by directive name.
95    pub fn get(&self, name: &str) -> Option<&dyn ModuleLoader> {
96        self.loaders.get(name).map(|b| b.as_ref())
97    }
98
99    /// Iterate over all registered loaders.
100    pub fn iter(&self) -> impl Iterator<Item = (&str, &dyn ModuleLoader)> {
101        self.loaders.iter().map(|(k, v)| (k.as_str(), v.as_ref()))
102    }
103
104    /// Call `on_load` on all registered modules.
105    pub fn load_all(&self) -> ModuleResult<()> {
106        for (name, loader) in &self.loaders {
107            loader.on_load().map_err(|e| {
108                ProxyError::Internal(format!("module '{name}' on_load failed: {e}"))
109            })?;
110        }
111        Ok(())
112    }
113
114    /// Call `on_unload` on all registered modules.
115    pub fn unload_all(&self) {
116        for (name, loader) in &self.loaders {
117            if let Err(e) = loader.on_unload() {
118                tracing::warn!(module = %name, error = %e, "module on_unload failed");
119            }
120        }
121    }
122
123    /// Call `on_reload` on all registered modules.
124    pub fn reload_all(&self, configs: &HashMap<String, HashMap<String, String>>) {
125        for (name, loader) in &self.loaders {
126            if let Some(cfg) = configs.get(name.as_str())
127                && let Err(e) = loader.on_reload(cfg)
128            {
129                tracing::warn!(module = %name, error = %e, "module on_reload failed");
130            }
131        }
132    }
133
134    /// Number of registered modules.
135    pub fn len(&self) -> usize {
136        self.loaders.len()
137    }
138
139    /// Whether the registry is empty.
140    pub fn is_empty(&self) -> bool {
141        self.loaders.is_empty()
142    }
143}
144
145impl Default for ModuleRegistry {
146    fn default() -> Self {
147        Self::new()
148    }
149}