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}