1use std::collections::HashMap;
4use std::path::PathBuf;
5use std::sync::Arc;
6
7use async_trait::async_trait;
8use axum::Router;
9use serde::Serialize;
10
11use crate::{Event, FilterFactory, Source};
12
13pub const API_VERSION: u32 = 1;
15
16#[derive(Debug, Clone, Serialize)]
18#[serde(rename_all = "camelCase")]
19pub struct PluginManifest {
20 pub name: &'static str,
23 pub version: &'static str,
24 pub description: &'static str,
25 pub api_version: u32,
27}
28
29#[async_trait]
33pub trait EventListener: Send + Sync + 'static {
34 async fn on_event(&self, session_id: &str, event: &Event);
35}
36
37#[derive(Debug, Clone)]
39pub struct PluginContext {
40 pub settings: HashMap<String, String>,
43 pub data_dir: PathBuf,
46 pub node_version: &'static str,
48}
49
50impl PluginContext {
51 pub fn setting(&self, key: &str) -> Option<&str> {
52 self.settings.get(&key.to_ascii_lowercase()).map(String::as_str)
53 }
54
55 pub fn from_env(name: &str, data_root: &std::path::Path, node_version: &'static str) -> Self {
57 let prefix = format!("WAVYZON_PLUGIN_{}_", name.to_ascii_uppercase().replace('-', "_"));
58 let settings = std::env::vars()
59 .filter_map(|(k, v)| k.strip_prefix(&prefix).map(|s| (s.to_ascii_lowercase(), v)))
60 .collect();
61 Self {
62 settings,
63 data_dir: data_root.join("plugins").join(name),
64 node_version,
65 }
66 }
67}
68
69#[derive(Default)]
71pub struct Registry {
72 pub(crate) sources: Vec<Arc<dyn Source>>,
73 pub(crate) filters: Vec<Arc<dyn FilterFactory>>,
74 pub(crate) listeners: Vec<Arc<dyn EventListener>>,
75 pub(crate) routes: Vec<(String, Router)>,
76 current: Option<String>,
77}
78
79impl Registry {
80 pub fn new() -> Self {
81 Self::default()
82 }
83
84 pub fn source(&mut self, source: Arc<dyn Source>) -> &mut Self {
87 self.sources.push(source);
88 self
89 }
90
91 pub fn filter(&mut self, factory: Arc<dyn FilterFactory>) -> &mut Self {
93 self.filters.push(factory);
94 self
95 }
96
97 pub fn listener(&mut self, listener: Arc<dyn EventListener>) -> &mut Self {
99 self.listeners.push(listener);
100 self
101 }
102
103 pub fn routes(&mut self, router: Router) -> &mut Self {
106 let name = self.current.clone().unwrap_or_else(|| "unnamed".into());
107 self.routes.push((name, router));
108 self
109 }
110
111 #[doc(hidden)]
112 pub fn begin(&mut self, plugin: &str) {
113 self.current = Some(plugin.to_string());
114 }
115
116 pub fn sources(&self) -> &[Arc<dyn Source>] {
117 &self.sources
118 }
119 pub fn filters(&self) -> &[Arc<dyn FilterFactory>] {
120 &self.filters
121 }
122 pub fn listeners(&self) -> &[Arc<dyn EventListener>] {
123 &self.listeners
124 }
125 pub fn take_routes(&mut self) -> Vec<(String, Router)> {
127 std::mem::take(&mut self.routes)
128 }
129}
130
131#[async_trait]
151pub trait Plugin: Send + Sync + 'static {
152 fn manifest(&self) -> PluginManifest;
153
154 async fn register(&self, ctx: &PluginContext, registry: &mut Registry) -> Result<(), PluginError>;
157
158 async fn shutdown(&self) {}
160}
161
162#[derive(Debug, thiserror::Error)]
163pub enum PluginError {
164 #[error("plugin {plugin} targets API v{found}, this node speaks v{expected}")]
165 ApiVersion { plugin: String, found: u32, expected: u32 },
166 #[error("plugin configuration: {0}")]
167 Config(String),
168 #[error("{0}")]
169 Other(String),
170}