tailwind_rs_core/
plugin_system.rs

1//! Plugin system for extending Tailwind-RS functionality
2//!
3//! This module provides a plugin system that allows users to extend Tailwind-RS
4//! with custom utilities, components, and optimizations.
5
6use crate::css_generator::{CssGenerator, CssRule, CssProperty};
7use crate::error::{Result, TailwindError};
8use std::collections::HashMap;
9use std::sync::Arc;
10
11/// Plugin hook types
12#[derive(Debug, Clone, PartialEq, Eq, Hash)]
13pub enum PluginHook {
14    /// Hook called before CSS generation
15    BeforeGenerate,
16    /// Hook called after CSS generation
17    AfterGenerate,
18    /// Hook called when a class is added
19    OnClassAdd,
20    /// Hook called when a rule is created
21    OnRuleCreate,
22    /// Hook called during optimization
23    OnOptimize,
24}
25
26/// Plugin context containing current state
27#[derive(Debug, Clone)]
28pub struct PluginContext {
29    /// Current CSS generator
30    pub generator: Arc<CssGenerator>,
31    /// Plugin data storage
32    pub data: HashMap<String, serde_json::Value>,
33    /// Configuration
34    pub config: HashMap<String, serde_json::Value>,
35}
36
37/// Plugin trait that all plugins must implement
38pub trait Plugin: Send + Sync {
39    /// Get the plugin name
40    fn name(&self) -> &str;
41    
42    /// Get the plugin version
43    fn version(&self) -> &str;
44    
45    /// Get the plugin description
46    fn description(&self) -> &str;
47    
48    /// Initialize the plugin
49    fn initialize(&mut self, context: &mut PluginContext) -> Result<()>;
50    
51    /// Handle plugin hooks
52    fn handle_hook(&mut self, hook: PluginHook, context: &mut PluginContext) -> Result<()>;
53    
54    /// Get plugin configuration schema
55    fn get_config_schema(&self) -> Option<serde_json::Value>;
56    
57    /// Validate plugin configuration
58    fn validate_config(&self, config: &serde_json::Value) -> Result<()>;
59}
60
61/// Plugin registry for managing plugins
62pub struct PluginRegistry {
63    plugins: HashMap<String, Box<dyn Plugin>>,
64    hooks: HashMap<PluginHook, Vec<String>>,
65    context: PluginContext,
66}
67
68impl PluginRegistry {
69    /// Create a new plugin registry
70    pub fn new() -> Self {
71        Self {
72            plugins: HashMap::new(),
73            hooks: HashMap::new(),
74            context: PluginContext {
75                generator: Arc::new(CssGenerator::new()),
76                data: HashMap::new(),
77                config: HashMap::new(),
78            },
79        }
80    }
81
82    /// Register a plugin
83    pub fn register_plugin(&mut self, plugin: Box<dyn Plugin>) -> Result<()> {
84        let name = plugin.name().to_string();
85        
86        if self.plugins.contains_key(&name) {
87            return Err(TailwindError::build(format!("Plugin '{}' is already registered", name)));
88        }
89        
90        // Initialize the plugin
91        let mut plugin_box = plugin;
92        plugin_box.initialize(&mut self.context)?;
93        
94        // Register the plugin
95        self.plugins.insert(name.clone(), plugin_box);
96        
97        // Register default hooks
98        self.register_default_hooks(&name);
99        
100        Ok(())
101    }
102
103    /// Unregister a plugin
104    pub fn unregister_plugin(&mut self, name: &str) -> Result<()> {
105        if !self.plugins.contains_key(name) {
106            return Err(TailwindError::build(format!("Plugin '{}' is not registered", name)));
107        }
108        
109        // Remove from plugins
110        self.plugins.remove(name);
111        
112        // Remove from hooks
113        for hook_list in self.hooks.values_mut() {
114            hook_list.retain(|plugin_name| plugin_name != name);
115        }
116        
117        Ok(())
118    }
119
120    /// Get a plugin by name
121    pub fn get_plugin(&self, name: &str) -> Option<&dyn Plugin> {
122        self.plugins.get(name).map(|p| p.as_ref())
123    }
124
125    /// Get a mutable plugin by name
126    pub fn get_plugin_mut(&mut self, name: &str) -> Option<&mut (dyn Plugin + '_)> {
127        if let Some(plugin) = self.plugins.get_mut(name) {
128            Some(plugin.as_mut())
129        } else {
130            None
131        }
132    }
133
134    /// List all registered plugins
135    pub fn list_plugins(&self) -> Vec<String> {
136        self.plugins.keys().cloned().collect()
137    }
138
139    /// Execute a hook for all registered plugins
140    pub fn execute_hook(&mut self, hook: PluginHook) -> Result<()> {
141        if let Some(plugin_names) = self.hooks.get(&hook) {
142            for plugin_name in plugin_names {
143                if let Some(plugin) = self.plugins.get_mut(plugin_name) {
144                    plugin.handle_hook(hook.clone(), &mut self.context)?;
145                }
146            }
147        }
148        Ok(())
149    }
150
151    /// Set plugin configuration
152    pub fn set_plugin_config(&mut self, plugin_name: &str, config: serde_json::Value) -> Result<()> {
153        if let Some(plugin) = self.plugins.get(plugin_name) {
154            plugin.validate_config(&config)?;
155        }
156        
157        self.context.config.insert(plugin_name.to_string(), config);
158        Ok(())
159    }
160
161    /// Get plugin configuration
162    pub fn get_plugin_config(&self, plugin_name: &str) -> Option<&serde_json::Value> {
163        self.context.config.get(plugin_name)
164    }
165
166    /// Set plugin data
167    pub fn set_plugin_data(&mut self, key: String, value: serde_json::Value) {
168        self.context.data.insert(key, value);
169    }
170
171    /// Get plugin data
172    pub fn get_plugin_data(&self, key: &str) -> Option<&serde_json::Value> {
173        self.context.data.get(key)
174    }
175
176    /// Update the CSS generator
177    pub fn update_generator(&mut self, generator: CssGenerator) {
178        self.context.generator = Arc::new(generator);
179    }
180
181    /// Get the current CSS generator
182    pub fn get_generator(&self) -> Arc<CssGenerator> {
183        self.context.generator.clone()
184    }
185
186    /// Register default hooks for a plugin
187    fn register_default_hooks(&mut self, plugin_name: &str) {
188        let default_hooks = vec![
189            PluginHook::BeforeGenerate,
190            PluginHook::AfterGenerate,
191            PluginHook::OnClassAdd,
192            PluginHook::OnRuleCreate,
193            PluginHook::OnOptimize,
194        ];
195        
196        for hook in default_hooks {
197            self.hooks.entry(hook).or_default().push(plugin_name.to_string());
198        }
199    }
200}
201
202
203/// Example plugin: Custom utilities
204#[derive(Debug)]
205pub struct CustomUtilitiesPlugin {
206    name: String,
207    version: String,
208    description: String,
209    custom_utilities: HashMap<String, CssRule>,
210}
211
212impl CustomUtilitiesPlugin {
213    /// Create a new custom utilities plugin
214    pub fn new() -> Self {
215        Self {
216            name: "custom-utilities".to_string(),
217            version: "1.0.0".to_string(),
218            description: "Adds custom utility classes".to_string(),
219            custom_utilities: HashMap::new(),
220        }
221    }
222
223    /// Add a custom utility
224    pub fn add_utility(&mut self, class_name: String, rule: CssRule) {
225        self.custom_utilities.insert(class_name, rule);
226    }
227}
228
229impl Plugin for CustomUtilitiesPlugin {
230    fn name(&self) -> &str {
231        &self.name
232    }
233
234    fn version(&self) -> &str {
235        &self.version
236    }
237
238    fn description(&self) -> &str {
239        &self.description
240    }
241
242    fn initialize(&mut self, _context: &mut PluginContext) -> Result<()> {
243        // Add some default custom utilities
244        self.add_utility("custom-shadow".to_string(), CssRule {
245            selector: ".custom-shadow".to_string(),
246            properties: vec![
247                CssProperty {
248                    name: "box-shadow".to_string(),
249                    value: "0 4px 6px -1px rgba(0, 0, 0, 0.1)".to_string(),
250                    important: false,
251                },
252            ],
253            media_query: None,
254            specificity: 10,
255        });
256        
257        Ok(())
258    }
259
260    fn handle_hook(&mut self, hook: PluginHook, _context: &mut PluginContext) -> Result<()> {
261        match hook {
262            PluginHook::BeforeGenerate => {
263                // Add custom utilities to the generator
264                // Note: This is a simplified implementation
265                // In a real implementation, we would need to modify the generator
266                println!("Custom utilities plugin: Adding {} custom utilities", self.custom_utilities.len());
267            }
268            PluginHook::AfterGenerate => {
269                println!("Custom utilities plugin: CSS generation completed");
270            }
271            _ => {}
272        }
273        Ok(())
274    }
275
276    fn get_config_schema(&self) -> Option<serde_json::Value> {
277        Some(serde_json::json!({
278            "type": "object",
279            "properties": {
280                "utilities": {
281                    "type": "array",
282                    "items": {
283                        "type": "object",
284                        "properties": {
285                            "name": {"type": "string"},
286                            "properties": {"type": "object"}
287                        }
288                    }
289                }
290            }
291        }))
292    }
293
294    fn validate_config(&self, config: &serde_json::Value) -> Result<()> {
295        if !config.is_object() {
296            return Err(TailwindError::build("Plugin config must be an object".to_string()));
297        }
298        Ok(())
299    }
300}
301
302/// Example plugin: CSS minifier
303#[derive(Debug)]
304pub struct MinifierPlugin {
305    name: String,
306    version: String,
307    description: String,
308    minify: bool,
309}
310
311impl MinifierPlugin {
312    /// Create a new minifier plugin
313    pub fn new() -> Self {
314        Self {
315            name: "minifier".to_string(),
316            version: "1.0.0".to_string(),
317            description: "Minifies CSS output".to_string(),
318            minify: true,
319        }
320    }
321}
322
323impl Plugin for MinifierPlugin {
324    fn name(&self) -> &str {
325        &self.name
326    }
327
328    fn version(&self) -> &str {
329        &self.version
330    }
331
332    fn description(&self) -> &str {
333        &self.description
334    }
335
336    fn initialize(&mut self, _context: &mut PluginContext) -> Result<()> {
337        Ok(())
338    }
339
340    fn handle_hook(&mut self, hook: PluginHook, _context: &mut PluginContext) -> Result<()> {
341        match hook {
342            PluginHook::OnOptimize => {
343                if self.minify {
344                    println!("Minifier plugin: Applying minification");
345                }
346            }
347            _ => {}
348        }
349        Ok(())
350    }
351
352    fn get_config_schema(&self) -> Option<serde_json::Value> {
353        Some(serde_json::json!({
354            "type": "object",
355            "properties": {
356                "enabled": {"type": "boolean"}
357            }
358        }))
359    }
360
361    fn validate_config(&self, config: &serde_json::Value) -> Result<()> {
362        if let Some(enabled) = config.get("enabled") {
363            if !enabled.is_boolean() {
364                return Err(TailwindError::build("Minifier enabled must be a boolean".to_string()));
365            }
366        }
367        Ok(())
368    }
369}
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374
375    #[test]
376    fn test_plugin_registry_creation() {
377        let registry = PluginRegistry::new();
378        assert!(registry.list_plugins().is_empty());
379    }
380
381    #[test]
382    fn test_register_plugin() {
383        let mut registry = PluginRegistry::new();
384        let plugin = Box::new(CustomUtilitiesPlugin::new());
385        
386        registry.register_plugin(plugin).unwrap();
387        
388        assert_eq!(registry.list_plugins().len(), 1);
389        assert!(registry.list_plugins().contains(&"custom-utilities".to_string()));
390    }
391
392    #[test]
393    fn test_duplicate_plugin_registration() {
394        let mut registry = PluginRegistry::new();
395        let plugin1 = Box::new(CustomUtilitiesPlugin::new());
396        let plugin2 = Box::new(CustomUtilitiesPlugin::new());
397        
398        registry.register_plugin(plugin1).unwrap();
399        let result = registry.register_plugin(plugin2);
400        
401        assert!(result.is_err());
402    }
403
404    #[test]
405    fn test_unregister_plugin() {
406        let mut registry = PluginRegistry::new();
407        let plugin = Box::new(CustomUtilitiesPlugin::new());
408        
409        registry.register_plugin(plugin).unwrap();
410        assert_eq!(registry.list_plugins().len(), 1);
411        
412        registry.unregister_plugin("custom-utilities").unwrap();
413        assert!(registry.list_plugins().is_empty());
414    }
415
416    #[test]
417    fn test_plugin_config() {
418        let mut registry = PluginRegistry::new();
419        let plugin = Box::new(MinifierPlugin::new());
420        
421        registry.register_plugin(plugin).unwrap();
422        
423        let config = serde_json::json!({"enabled": true});
424        registry.set_plugin_config("minifier", config.clone()).unwrap();
425        
426        assert_eq!(registry.get_plugin_config("minifier"), Some(&config));
427    }
428
429    #[test]
430    fn test_plugin_data() {
431        let mut registry = PluginRegistry::new();
432        
433        let data = serde_json::json!({"key": "value"});
434        registry.set_plugin_data("test_key".to_string(), data.clone());
435        
436        assert_eq!(registry.get_plugin_data("test_key"), Some(&data));
437    }
438
439    #[test]
440    fn test_execute_hook() {
441        let mut registry = PluginRegistry::new();
442        let plugin = Box::new(MinifierPlugin::new());
443        
444        registry.register_plugin(plugin).unwrap();
445        
446        // This should not panic
447        registry.execute_hook(PluginHook::OnOptimize).unwrap();
448    }
449
450    #[test]
451    fn test_custom_utilities_plugin() {
452        let mut plugin = CustomUtilitiesPlugin::new();
453        let mut context = PluginContext {
454            generator: Arc::new(CssGenerator::new()),
455            data: HashMap::new(),
456            config: HashMap::new(),
457        };
458        
459        plugin.initialize(&mut context).unwrap();
460        assert_eq!(plugin.name(), "custom-utilities");
461        assert_eq!(plugin.version(), "1.0.0");
462    }
463
464    #[test]
465    fn test_minifier_plugin() {
466        let mut plugin = MinifierPlugin::new();
467        let mut context = PluginContext {
468            generator: Arc::new(CssGenerator::new()),
469            data: HashMap::new(),
470            config: HashMap::new(),
471        };
472        
473        plugin.initialize(&mut context).unwrap();
474        assert_eq!(plugin.name(), "minifier");
475        assert_eq!(plugin.version(), "1.0.0");
476    }
477}