1use 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#[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 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 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 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#[macro_export]
80macro_rules! export_plugin {
81 ($plugin_type:ty) => {
82 pub fn create_plugin() -> Box<dyn $crate::plugin::Plugin> {
84 Box::new(<$plugin_type>::new())
85 }
86
87 pub fn plugin_info() -> $crate::plugin::PluginInfo {
89 <$plugin_type>::new().info()
90 }
91 };
92}
93
94#[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 #[derive(Debug)]
110 pub struct QorzenPlugin {
111 context: Option<$crate::plugin::PluginContext>,
112 }
113
114 impl QorzenPlugin {
115 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#[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 #[derive(Debug)]
194 pub struct PluginSearchProvider {
195 id: String,
196 }
197
198 impl PluginSearchProvider {
199 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 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#[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 pub fn $id(props: serde_json::Value) -> $crate::error::Result<dioxus::prelude::VNode> {
287 $render_fn(props)
288 }
289
290 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#[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#[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#[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 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 pub fn description(mut self, description: &str) -> Self {
483 self.info.description = description.to_string();
484 self
485 }
486
487 pub fn author(mut self, author: &str) -> Self {
489 self.info.author = author.to_string();
490 self
491 }
492
493 pub fn license(mut self, license: &str) -> Self {
495 self.info.license = license.to_string();
496 self
497 }
498
499 pub fn homepage(mut self, homepage: &str) -> Self {
501 self.info.homepage = Some(homepage.to_string());
502 self
503 }
504
505 pub fn repository(mut self, repository: &str) -> Self {
507 self.info.repository = Some(repository.to_string());
508 self
509 }
510
511 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 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 pub fn ui_component(mut self, component: UIComponent) -> Self {
531 self.ui_components.push(component);
532 self
533 }
534
535 pub fn menu_item(mut self, item: MenuItem) -> Self {
537 self.menu_items.push(item);
538 self
539 }
540
541 pub fn api_route(mut self, route: ApiRoute) -> Self {
543 self.api_routes.push(route);
544 self
545 }
546
547 pub fn event_handler(mut self, handler: EventHandler) -> Self {
549 self.event_handlers.push(handler);
550 self
551 }
552
553 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#[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#[allow(dead_code)]
579pub trait PluginHelper {
580 fn context(&self) -> &PluginContext;
582
583 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 async fn get_config(&self, key: &str) -> Result<Option<Value>> {
598 self.context().api_client.get_config(key).await
599 }
600
601 async fn set_config(&self, key: &str, value: Value) -> Result<()> {
603 self.context().api_client.set_config(key, value).await
604 }
605
606 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 async fn get_user(&self) -> Result<Option<crate::auth::User>> {
616 self.context().api_client.get_current_user().await
617 }
618
619 async fn emit_event(&self, event_type: &str, data: Value) -> Result<()> {
621 let plugin_context = self.context();
622
623 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 plugin_context.event_bus.publish(event).await
641 }
642
643 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 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
676pub struct PluginTemplate;
678
679#[allow(dead_code)]
680impl PluginTemplate {
681 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 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 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 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 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) .build();
927
928 assert_eq!(metadata.info.supported_platforms.len(), 3); 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}