qorzen_oxide/plugin/
sdk.rs

1// src/plugin/sdk.rs
2
3//! Plugin Development SDK
4//!
5//! This module provides convenient macros and utilities for plugin development.
6
7use super::{ApiRoute, EventHandler, MenuItem, PluginContext, PluginInfo, UIComponent};
8use crate::auth::Permission;
9use crate::error::Result;
10use crate::event::Event;
11use crate::types::Metadata;
12use chrono::{DateTime, Utc};
13use serde_json::Value;
14use std::any::Any;
15use std::collections::HashMap;
16
17/// A generic event type for plugin-emitted events
18#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
19pub struct PluginEvent {
20    pub event_type: String,
21    pub plugin_id: String,
22    pub source: String,
23    pub data: serde_json::Value,
24    pub timestamp: DateTime<Utc>,
25    pub metadata: Metadata,
26}
27
28#[allow(dead_code)]
29impl PluginEvent {
30    /// Create a new plugin event
31    pub fn new(
32        event_type: impl Into<String>,
33        plugin_id: impl Into<String>,
34        source: impl Into<String>,
35        data: serde_json::Value,
36    ) -> Self {
37        Self {
38            event_type: event_type.into(),
39            plugin_id: plugin_id.into(),
40            source: source.into(),
41            data,
42            timestamp: Utc::now(),
43            metadata: HashMap::new(),
44        }
45    }
46
47    /// Add metadata to the event
48    pub fn with_metadata(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
49        self.metadata.insert(key.into(), value);
50        self
51    }
52}
53
54impl Event for PluginEvent {
55    fn event_type(&self) -> &'static str {
56        // Since we need to return a &'static str, we'll need to use a leaked string
57        // In practice, this should be handled differently, but for the SDK this works
58        Box::leak(self.event_type.clone().into_boxed_str())
59    }
60
61    fn source(&self) -> &str {
62        &self.source
63    }
64
65    fn metadata(&self) -> &Metadata {
66        &self.metadata
67    }
68
69    fn as_any(&self) -> &dyn Any {
70        self
71    }
72
73    fn timestamp(&self) -> DateTime<Utc> {
74        self.timestamp
75    }
76}
77
78/// Export the plugin creation function for native plugins
79#[macro_export]
80macro_rules! export_plugin {
81    ($plugin_type:ty) => {
82        // For safe plugin loading, we register the plugin factory function
83        pub fn create_plugin() -> Box<dyn $crate::plugin::Plugin> {
84            Box::new(<$plugin_type>::new())
85        }
86
87        // Export plugin info for discovery
88        pub fn plugin_info() -> $crate::plugin::PluginInfo {
89            <$plugin_type>::new().info()
90        }
91    };
92}
93
94/// Create a plugin with basic metadata using a more ergonomic syntax
95#[macro_export]
96macro_rules! plugin {
97    (
98        id: $id:expr,
99        name: $name:expr,
100        version: $version:expr,
101        author: $author:expr,
102        description: $description:expr,
103        $(license: $license:expr,)?
104        $(permissions: [$($permission:expr),*],)?
105        $(dependencies: [$($dep:expr),*],)?
106        impl $impl_block:tt
107    ) => {
108        /// Auto-generated plugin struct
109        #[derive(Debug)]
110        pub struct QorzenPlugin {
111            context: Option<$crate::plugin::PluginContext>,
112        }
113
114        impl QorzenPlugin {
115            /// Create a new plugin instance
116            pub fn new() -> Self {
117                Self {
118                    context: None,
119                }
120            }
121        }
122
123        #[async_trait::async_trait]
124        impl $crate::plugin::Plugin for QorzenPlugin {
125            fn info(&self) -> $crate::plugin::PluginInfo {
126                $crate::plugin::PluginInfo {
127                    id: $id.to_string(),
128                    name: $name.to_string(),
129                    version: $version.to_string(),
130                    description: $description.to_string(),
131                    author: $author.to_string(),
132                    license: plugin!(@license $($license)?).to_string(),
133                    homepage: None,
134                    repository: None,
135                    minimum_core_version: "0.1.0".to_string(),
136                    supported_platforms: vec![$crate::plugin::Platform::All],
137                }
138            }
139
140            fn required_dependencies(&self) -> Vec<$crate::plugin::PluginDependency> {
141                vec![
142                    $($(
143                        $crate::plugin::PluginDependency {
144                            plugin_id: $dep.to_string(),
145                            version_requirement: "*".to_string(),
146                            optional: false,
147                        }
148                    ),*)?
149                ]
150            }
151
152            fn required_permissions(&self) -> Vec<$crate::auth::Permission> {
153                let mut permissions = Vec::new();
154                $($(
155                    let parts: Vec<&str> = $permission.split('.').collect();
156                    if parts.len() == 2 {
157                        permissions.push($crate::auth::Permission {
158                            resource: parts[0].to_string(),
159                            action: parts[1].to_string(),
160                            scope: $crate::auth::PermissionScope::Global,
161                        });
162                    }
163                )*)?
164                permissions
165            }
166
167            $impl_block
168        }
169
170        $crate::export_plugin!(QorzenPlugin);
171    };
172
173    (@license) => { "MIT" };
174    (@license $license:expr) => { $license };
175}
176
177/// Create a search provider for a plugin with convenient syntax
178#[macro_export]
179macro_rules! search_provider {
180    (
181        id: $id:expr,
182        name: $name:expr,
183        description: $desc:expr,
184        $(priority: $priority:expr,)?
185        $(result_types: [$($result_type:expr),*],)?
186        $(supports_facets: $facets:expr,)?
187        $(supports_suggestions: $suggestions:expr,)?
188        search: $search_fn:expr,
189        $(suggestions: $suggestions_fn:expr,)?
190        $(health_check: $health_fn:expr,)?
191    ) => {
192        /// Auto-generated search provider struct
193        #[derive(Debug)]
194        pub struct PluginSearchProvider {
195            id: String,
196        }
197
198        impl PluginSearchProvider {
199            /// Create a new search provider
200            pub fn new() -> Self {
201                Self {
202                    id: $id.to_string(),
203                }
204            }
205        }
206
207        #[async_trait::async_trait]
208        impl $crate::plugin::search::SearchProvider for PluginSearchProvider {
209            fn provider_id(&self) -> &str {
210                $id
211            }
212
213            fn provider_name(&self) -> &str {
214                $name
215            }
216
217            fn description(&self) -> &str {
218                $desc
219            }
220
221            fn priority(&self) -> i32 {
222                search_provider!(@priority $($priority)?)
223            }
224
225            fn supported_result_types(&self) -> Vec<String> {
226                vec![
227                    $($(String::from($result_type)),*)?
228                ]
229            }
230
231            fn supports_facets(&self) -> bool {
232                search_provider!(@supports_facets $($facets)?)
233            }
234
235            fn supports_suggestions(&self) -> bool {
236                search_provider!(@supports_suggestions $($suggestions)?)
237            }
238
239            async fn search(&self, query: &$crate::plugin::search::SearchQuery)
240                -> $crate::error::Result<Vec<$crate::plugin::search::SearchResult>> {
241                $search_fn(query).await
242            }
243
244            $(
245                async fn get_suggestions(&self, query: &$crate::plugin::search::SearchQuery)
246                    -> $crate::error::Result<Vec<$crate::plugin::search::SearchSuggestion>> {
247                    $suggestions_fn(query).await
248                }
249            )?
250
251            async fn health_check(&self) -> $crate::error::Result<$crate::plugin::search::ProviderHealth> {
252                $(
253                    return $health_fn().await;
254                )?
255
256                // Default health check implementation
257                Ok($crate::plugin::search::ProviderHealth {
258                    is_healthy: true,
259                    response_time_ms: Some(1),
260                    error_message: None,
261                    last_check: chrono::Utc::now(),
262                })
263            }
264        }
265    };
266
267    (@priority) => { 100 };
268    (@priority $priority:expr) => { $priority };
269    (@supports_facets) => { false };
270    (@supports_facets $facets:expr) => { $facets };
271    (@supports_suggestions) => { false };
272    (@supports_suggestions $suggestions:expr) => { $suggestions };
273}
274
275/// Create a UI component for a plugin with validation
276#[macro_export]
277macro_rules! ui_component {
278    (
279        id: $id:expr,
280        name: $name:expr,
281        component_type: $comp_type:expr,
282        $(permissions: [$($permission:expr),*],)?
283        render: $render_fn:expr
284    ) => {
285        /// Component render function
286        pub fn $id(props: serde_json::Value) -> $crate::error::Result<dioxus::prelude::VNode> {
287            $render_fn(props)
288        }
289
290        /// Get UI component metadata
291        pub fn get_ui_component() -> $crate::plugin::UIComponent {
292            $crate::plugin::UIComponent {
293                id: stringify!($id).to_string(),
294                name: $name.to_string(),
295                component_type: $comp_type,
296                props: serde_json::Value::Object(serde_json::Map::new()),
297                required_permissions: vec![
298                    $($(
299                        {
300                            let parts: Vec<&str> = $permission.split('.').collect();
301                            if parts.len() == 2 {
302                                $crate::auth::Permission {
303                                    resource: parts[0].to_string(),
304                                    action: parts[1].to_string(),
305                                    scope: $crate::auth::PermissionScope::Global,
306                                }
307                            } else {
308                                $crate::auth::Permission {
309                                    resource: "unknown".to_string(),
310                                    action: "unknown".to_string(),
311                                    scope: $crate::auth::PermissionScope::Global,
312                                }
313                            }
314                        }
315                    ),*)?
316                ],
317            }
318        }
319    };
320}
321
322/// Create a menu item for a plugin with hierarchical support
323#[macro_export]
324macro_rules! menu_item {
325    (
326        id: $id:expr,
327        label: $label:expr,
328        $(icon: $icon:expr,)?
329        $(route: $route:expr,)?
330        $(action: $action:expr,)?
331        $(order: $order:expr,)?
332        $(permissions: [$($permission:expr),*],)?
333        $(children: [$($child:expr),*],)?
334    ) => {
335        $crate::plugin::MenuItem {
336            id: $id.to_string(),
337            label: $label.to_string(),
338            icon: menu_item!(@icon $($icon)?),
339            route: menu_item!(@route $($route)?),
340            action: menu_item!(@action $($action)?),
341            required_permissions: vec![
342                $($(
343                    {
344                        let parts: Vec<&str> = $permission.split('.').collect();
345                        if parts.len() == 2 {
346                            $crate::auth::Permission {
347                                resource: parts[0].to_string(),
348                                action: parts[1].to_string(),
349                                scope: $crate::auth::PermissionScope::Global,
350                            }
351                        } else {
352                            $crate::auth::Permission {
353                                resource: "unknown".to_string(),
354                                action: "unknown".to_string(),
355                                scope: $crate::auth::PermissionScope::Global,
356                            }
357                        }
358                    }
359                ),*)?
360            ],
361            order: menu_item!(@order $($order)?),
362            children: vec![
363                $($($child),*)?
364            ],
365        }
366    };
367
368    (@icon) => { None };
369    (@icon $icon:expr) => { Some($icon.to_string()) };
370    (@route) => { None };
371    (@route $route:expr) => { Some($route.to_string()) };
372    (@action) => { None };
373    (@action $action:expr) => { Some($action.to_string()) };
374    (@order) => { 0 };
375    (@order $order:expr) => { $order };
376}
377
378/// Create an API route definition with documentation
379#[macro_export]
380macro_rules! api_route {
381    (
382        path: $path:expr,
383        method: $method:expr,
384        handler: $handler:expr,
385        $(permissions: [$($permission:expr),*],)?
386        $(rate_limit: {
387            requests_per_minute: $rpm:expr,
388            burst_limit: $burst:expr
389        },)?
390        documentation: {
391            summary: $summary:expr,
392            description: $description:expr,
393            $(parameters: [$($param:expr),*],)?
394            $(responses: [$($response:expr),*],)?
395        }
396    ) => {
397        $crate::plugin::ApiRoute {
398            path: $path.to_string(),
399            method: $method,
400            handler_id: stringify!($handler).to_string(),
401            required_permissions: vec![
402                $($(
403                    {
404                        let parts: Vec<&str> = $permission.split('.').collect();
405                        if parts.len() == 2 {
406                            $crate::auth::Permission {
407                                resource: parts[0].to_string(),
408                                action: parts[1].to_string(),
409                                scope: $crate::auth::PermissionScope::Global,
410                            }
411                        } else {
412                            $crate::auth::Permission {
413                                resource: "unknown".to_string(),
414                                action: "unknown".to_string(),
415                                scope: $crate::auth::PermissionScope::Global,
416                            }
417                        }
418                    }
419                ),*)?
420            ],
421            rate_limit: api_route!(@rate_limit $($rpm, $burst)?),
422            documentation: $crate::plugin::ApiDocumentation {
423                summary: $summary.to_string(),
424                description: $description.to_string(),
425                parameters: vec![
426                    $($($param),*)?
427                ],
428                responses: vec![
429                    $($($response),*)?
430                ],
431                examples: vec![],
432            },
433        }
434    };
435
436    (@rate_limit) => { None };
437    (@rate_limit $rpm:expr, $burst:expr) => {
438        Some($crate::plugin::RateLimit {
439            requests_per_minute: $rpm,
440            burst_limit: $burst,
441        })
442    };
443}
444
445/// Plugin development utilities and builder pattern
446#[allow(dead_code)]
447pub struct PluginBuilder {
448    info: PluginInfo,
449    permissions: Vec<Permission>,
450    ui_components: Vec<UIComponent>,
451    menu_items: Vec<MenuItem>,
452    api_routes: Vec<ApiRoute>,
453    event_handlers: Vec<EventHandler>,
454}
455
456#[allow(dead_code)]
457impl PluginBuilder {
458    /// Start building a new plugin
459    pub fn new(id: &str, name: &str, version: &str) -> Self {
460        Self {
461            info: PluginInfo {
462                id: id.to_string(),
463                name: name.to_string(),
464                version: version.to_string(),
465                description: String::new(),
466                author: String::new(),
467                license: "MIT".to_string(),
468                homepage: None,
469                repository: None,
470                minimum_core_version: "0.1.0".to_string(),
471                supported_platforms: vec![super::Platform::All],
472            },
473            permissions: Vec::new(),
474            ui_components: Vec::new(),
475            menu_items: Vec::new(),
476            api_routes: Vec::new(),
477            event_handlers: Vec::new(),
478        }
479    }
480
481    /// Set plugin description
482    pub fn description(mut self, description: &str) -> Self {
483        self.info.description = description.to_string();
484        self
485    }
486
487    /// Set plugin author
488    pub fn author(mut self, author: &str) -> Self {
489        self.info.author = author.to_string();
490        self
491    }
492
493    /// Set plugin license
494    pub fn license(mut self, license: &str) -> Self {
495        self.info.license = license.to_string();
496        self
497    }
498
499    /// Set plugin homepage
500    pub fn homepage(mut self, homepage: &str) -> Self {
501        self.info.homepage = Some(homepage.to_string());
502        self
503    }
504
505    /// Set plugin repository
506    pub fn repository(mut self, repository: &str) -> Self {
507        self.info.repository = Some(repository.to_string());
508        self
509    }
510
511    /// Add supported platform
512    pub fn platform(mut self, platform: super::Platform) -> Self {
513        if !self.info.supported_platforms.contains(&platform) {
514            self.info.supported_platforms.push(platform);
515        }
516        self
517    }
518
519    /// Add required permission
520    pub fn permission(mut self, resource: &str, action: &str) -> Self {
521        self.permissions.push(Permission {
522            resource: resource.to_string(),
523            action: action.to_string(),
524            scope: crate::auth::PermissionScope::Global,
525        });
526        self
527    }
528
529    /// Add UI component
530    pub fn ui_component(mut self, component: UIComponent) -> Self {
531        self.ui_components.push(component);
532        self
533    }
534
535    /// Add menu item
536    pub fn menu_item(mut self, item: MenuItem) -> Self {
537        self.menu_items.push(item);
538        self
539    }
540
541    /// Add API route
542    pub fn api_route(mut self, route: ApiRoute) -> Self {
543        self.api_routes.push(route);
544        self
545    }
546
547    /// Add event handler
548    pub fn event_handler(mut self, handler: EventHandler) -> Self {
549        self.event_handlers.push(handler);
550        self
551    }
552
553    /// Build the plugin metadata
554    pub fn build(self) -> PluginMetadata {
555        PluginMetadata {
556            info: self.info,
557            permissions: self.permissions,
558            ui_components: self.ui_components,
559            menu_items: self.menu_items,
560            api_routes: self.api_routes,
561            event_handlers: self.event_handlers,
562        }
563    }
564}
565
566/// Plugin metadata collection for development
567#[allow(dead_code)]
568pub struct PluginMetadata {
569    pub info: PluginInfo,
570    pub permissions: Vec<Permission>,
571    pub ui_components: Vec<UIComponent>,
572    pub menu_items: Vec<MenuItem>,
573    pub api_routes: Vec<ApiRoute>,
574    pub event_handlers: Vec<EventHandler>,
575}
576
577/// Helper traits for plugin development
578#[allow(dead_code)]
579pub trait PluginHelper {
580    /// Get plugin context
581    fn context(&self) -> &PluginContext;
582
583    /// Log a message with the plugin context
584    fn log(&self, level: &str, message: &str) {
585        let plugin_id = &self.context().plugin_id;
586        match level {
587            "trace" => tracing::trace!("[Plugin:{}] {}", plugin_id, message),
588            "debug" => tracing::debug!("[Plugin:{}] {}", plugin_id, message),
589            "info" => tracing::info!("[Plugin:{}] {}", plugin_id, message),
590            "warn" => tracing::warn!("[Plugin:{}] {}", plugin_id, message),
591            "error" => tracing::error!("[Plugin:{}] {}", plugin_id, message),
592            _ => tracing::info!("[Plugin:{}] {}", plugin_id, message),
593        }
594    }
595
596    /// Get configuration value
597    async fn get_config(&self, key: &str) -> Result<Option<Value>> {
598        self.context().api_client.get_config(key).await
599    }
600
601    /// Set configuration value
602    async fn set_config(&self, key: &str, value: Value) -> Result<()> {
603        self.context().api_client.set_config(key, value).await
604    }
605
606    /// Check if user has permission
607    async fn check_permission(&self, resource: &str, action: &str) -> Result<bool> {
608        self.context()
609            .api_client
610            .check_permission(resource, action)
611            .await
612    }
613
614    /// Get current user
615    async fn get_user(&self) -> Result<Option<crate::auth::User>> {
616        self.context().api_client.get_current_user().await
617    }
618
619    /// Emit an event using the proper event system
620    async fn emit_event(&self, event_type: &str, data: Value) -> Result<()> {
621        let plugin_context = self.context();
622
623        // Create a proper plugin event that implements the Event trait
624        let event = PluginEvent::new(
625            event_type,
626            &plugin_context.plugin_id,
627            format!("plugin.{}", plugin_context.plugin_id),
628            data,
629        )
630        .with_metadata(
631            "plugin_version".to_string(),
632            serde_json::Value::String(plugin_context.config.version.clone()),
633        )
634        .with_metadata(
635            "plugin_config_id".to_string(),
636            serde_json::Value::String(plugin_context.config.plugin_id.clone()),
637        );
638
639        // Use the event bus to publish the event
640        plugin_context.event_bus.publish(event).await
641    }
642
643    /// Emit an event with custom metadata
644    async fn emit_event_with_metadata(
645        &self,
646        event_type: &str,
647        data: Value,
648        metadata: HashMap<String, serde_json::Value>,
649    ) -> Result<()> {
650        let plugin_context = self.context();
651
652        let mut event = PluginEvent::new(
653            event_type,
654            &plugin_context.plugin_id,
655            format!("plugin.{}", plugin_context.plugin_id),
656            data,
657        )
658        .with_metadata(
659            "plugin_version".to_string(),
660            serde_json::Value::String(plugin_context.config.version.clone()),
661        )
662        .with_metadata(
663            "plugin_config_id".to_string(),
664            serde_json::Value::String(plugin_context.config.plugin_id.clone()),
665        );
666
667        // Add custom metadata
668        for (key, value) in metadata {
669            event = event.with_metadata(key, value);
670        }
671
672        plugin_context.event_bus.publish(event).await
673    }
674}
675
676/// Generate plugin template files for development
677pub struct PluginTemplate;
678
679#[allow(dead_code)]
680impl PluginTemplate {
681    /// Generate a basic plugin template
682    pub fn generate_basic(
683        plugin_id: &str,
684        plugin_name: &str,
685        author: &str,
686    ) -> HashMap<String, String> {
687        let mut files = HashMap::new();
688
689        // Cargo.toml
690        files.insert(
691            "Cargo.toml".to_string(),
692            format!(
693                r#"
694[package]
695name = "{}"
696version = "0.1.0"
697edition = "2021"
698
699[lib]
700crate-type = ["cdylib", "rlib"]
701
702[dependencies]
703qorzen-oxide = {{ path = "../../" }}
704async-trait = "0.1"
705serde = {{ version = "1.0", features = ["derive"] }}
706serde_json = "1.0"
707tokio = {{ version = "1.0", features = ["macros"] }}
708chrono = {{ version = "0.4", features = ["serde"] }}
709dioxus = "0.6"
710"#,
711                plugin_id
712            ),
713        );
714
715        // src/lib.rs
716        files.insert(
717            "src/lib.rs".to_string(),
718            format!(
719                r#"
720use qorzen_oxide::plugin::*;
721use qorzen_oxide::{{plugin, export_plugin}};
722
723plugin! {{
724    id: "{}",
725    name: "{}",
726    version: "0.1.0",
727    author: "{}",
728    description: "A sample plugin for the Qorzen framework",
729    permissions: ["data.read", "ui.render"],
730
731    impl {{
732        async fn initialize(&mut self, context: PluginContext) -> qorzen_oxide::error::Result<()> {{
733            self.context = Some(context);
734            tracing::info!("Plugin '{}' initialized successfully");
735            Ok(())
736        }}
737
738        async fn shutdown(&mut self) -> qorzen_oxide::error::Result<()> {{
739            tracing::info!("Plugin '{}' shutting down");
740            Ok(())
741        }}
742
743        fn ui_components(&self) -> Vec<UIComponent> {{
744            vec![]
745        }}
746
747        fn menu_items(&self) -> Vec<MenuItem> {{
748            vec![]
749        }}
750
751        fn settings_schema(&self) -> Option<qorzen_oxide::config::SettingsSchema> {{
752            None
753        }}
754
755        fn api_routes(&self) -> Vec<ApiRoute> {{
756            vec![]
757        }}
758
759        fn event_handlers(&self) -> Vec<EventHandler> {{
760            vec![]
761        }}
762
763        fn render_component(&self, component_id: &str, props: serde_json::Value)
764            -> qorzen_oxide::error::Result<dioxus::prelude::VNode> {{
765            Err(qorzen_oxide::error::Error::plugin(&self.info().id, "No components implemented"))
766        }}
767
768        async fn handle_api_request(&self, route_id: &str, request: ApiRequest)
769            -> qorzen_oxide::error::Result<ApiResponse> {{
770            Err(qorzen_oxide::error::Error::plugin(&self.info().id, "No API routes implemented"))
771        }}
772
773        async fn handle_event(&self, handler_id: &str, event: &dyn qorzen_oxide::event::Event)
774            -> qorzen_oxide::error::Result<()> {{
775            Ok(())
776        }}
777    }}
778}}
779"#,
780                plugin_id, plugin_name, author, plugin_name, plugin_name
781            ),
782        );
783
784        // plugin.toml
785        files.insert(
786            "plugin.toml".to_string(),
787            format!(
788                r#"
789[plugin]
790id = "{}"
791name = "{}"
792version = "0.1.0"
793description = "A sample plugin for the Qorzen framework"
794author = "{}"
795license = "MIT"
796minimum_core_version = "0.1.0"
797api_version = "0.1.0"
798
799[build]
800entry = "src/lib.rs"
801sources = ["src/**/*.rs"]
802features = ["default"]
803hot_reload = true
804
805[targets.web]
806platform = "web"
807arch = ["wasm32"]
808features = ["web"]
809
810[targets.desktop]
811platform = "desktop"
812arch = ["x86_64", "aarch64"]
813os = ["windows", "macos", "linux"]
814features = ["desktop"]
815
816permissions = [
817    "data.read",
818    "ui.render"
819]
820
821provides = [
822    "example.functionality"
823]
824
825requires = [
826    "core.events"
827]
828"#,
829                plugin_id, plugin_name, author
830            ),
831        );
832
833        // README.md
834        files.insert(
835            "README.md".to_string(),
836            format!(
837                r#"
838# {}
839
840{}
841
842## Building
843
844```bash
845cargo build --release
846```
847
848## Installation
849
850Register the plugin factory in your application and it will be available for loading.
851
852## Features
853
854- Basic plugin functionality
855- Safe loading without dynamic library risks
856- Configurable settings
857- Event handling
858
859## Configuration
860
861This plugin supports the following configuration options:
862
863- `enabled`: Enable/disable the plugin
864- `setting1`: Example setting
865
866## License
867
868MIT
869"#,
870                plugin_name, "A sample plugin demonstrating the Qorzen plugin system"
871            ),
872        );
873
874        files
875    }
876}
877
878#[cfg(test)]
879mod tests {
880    use super::*;
881
882    #[test]
883    fn test_plugin_builder() {
884        let metadata = PluginBuilder::new("test_plugin", "Test Plugin", "1.0.0")
885            .description("A test plugin")
886            .author("Test Author")
887            .homepage("https://example.com")
888            .repository("https://github.com/example/plugin")
889            .permission("data", "read")
890            .permission("ui", "render")
891            .build();
892
893        assert_eq!(metadata.info.id, "test_plugin");
894        assert_eq!(metadata.info.name, "Test Plugin");
895        assert_eq!(
896            metadata.info.homepage,
897            Some("https://example.com".to_string())
898        );
899        assert_eq!(metadata.permissions.len(), 2);
900    }
901
902    #[test]
903    fn test_plugin_template_generation() {
904        let files =
905            PluginTemplate::generate_basic("example_plugin", "Example Plugin", "Test Author");
906
907        assert!(files.contains_key("Cargo.toml"));
908        assert!(files.contains_key("src/lib.rs"));
909        assert!(files.contains_key("plugin.toml"));
910        assert!(files.contains_key("README.md"));
911
912        let cargo_toml = files.get("Cargo.toml").unwrap();
913        assert!(cargo_toml.contains("example_plugin"));
914
915        let lib_rs = files.get("src/lib.rs").unwrap();
916        assert!(lib_rs.contains("Example Plugin"));
917        assert!(lib_rs.contains("Test Author"));
918    }
919
920    #[test]
921    fn test_builder_platform_support() {
922        let metadata = PluginBuilder::new("test", "Test", "1.0.0")
923            .platform(super::super::Platform::Web)
924            .platform(super::super::Platform::Windows)
925            .platform(super::super::Platform::Web) // Duplicate should be ignored
926            .build();
927
928        assert_eq!(metadata.info.supported_platforms.len(), 3); // All, Web, Windows
929        assert!(metadata
930            .info
931            .supported_platforms
932            .contains(&super::super::Platform::Web));
933        assert!(metadata
934            .info
935            .supported_platforms
936            .contains(&super::super::Platform::Windows));
937    }
938
939    #[test]
940    fn test_plugin_event_creation() {
941        let event = PluginEvent::new(
942            "test.event",
943            "test_plugin",
944            "plugin.test_plugin",
945            serde_json::json!({"key": "value"}),
946        )
947        .with_metadata("extra", serde_json::json!("metadata"));
948
949        assert_eq!(event.plugin_id, "test_plugin");
950        assert_eq!(event.source, "plugin.test_plugin");
951        assert!(event.metadata.contains_key("extra"));
952    }
953}