Skip to main content

voirs_cli/plugins/
api.rs

1//! Plugin API definitions and utilities for VoiRS CLI plugins.
2
3use super::{Plugin, PluginError, PluginResult, PluginType};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::sync::Arc;
7
8/// Plugin API version information
9pub const PLUGIN_API_VERSION: &str = "1.0.0";
10pub const MIN_SUPPORTED_VERSION: &str = "1.0.0";
11pub const MAX_SUPPORTED_VERSION: &str = "1.99.99";
12
13/// Plugin API interface that all plugins must implement
14pub trait PluginApi: Send + Sync {
15    /// Get the plugin API version this plugin was compiled against
16    fn api_version(&self) -> &str;
17
18    /// Initialize the plugin with host information
19    fn initialize_api(&mut self, host_info: &HostInfo) -> PluginResult<()>;
20
21    /// Get plugin capabilities as a structured format
22    fn get_plugin_info(&self) -> PluginInfo;
23
24    /// Handle API calls from the host
25    fn handle_api_call(&self, call: &ApiCall) -> PluginResult<ApiResponse>;
26
27    /// Notify plugin of host events
28    fn notify(&self, event: &PluginEvent) -> PluginResult<()>;
29
30    /// Check if plugin supports a specific feature
31    fn supports_feature(&self, feature: &str) -> bool;
32}
33
34/// Information about the host application
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct HostInfo {
37    pub name: String,
38    pub version: String,
39    pub api_version: String,
40    pub capabilities: Vec<String>,
41    pub configuration: HashMap<String, serde_json::Value>,
42}
43
44/// Detailed plugin information
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct PluginInfo {
47    pub name: String,
48    pub version: String,
49    pub description: String,
50    pub author: String,
51    pub plugin_type: PluginType,
52    pub api_version: String,
53    pub supported_features: Vec<String>,
54    pub required_permissions: Vec<super::Permission>,
55    pub metadata: HashMap<String, serde_json::Value>,
56}
57
58/// API call structure for plugin communication
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct ApiCall {
61    pub id: String,
62    pub method: String,
63    pub params: serde_json::Value,
64    pub context: Option<CallContext>,
65}
66
67/// Context information for API calls
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct CallContext {
70    pub user_id: Option<String>,
71    pub session_id: Option<String>,
72    pub trace_id: Option<String>,
73    pub metadata: HashMap<String, String>,
74}
75
76/// Response from plugin API calls
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct ApiResponse {
79    pub id: String,
80    pub success: bool,
81    pub result: Option<serde_json::Value>,
82    pub error: Option<String>,
83    pub metadata: HashMap<String, serde_json::Value>,
84}
85
86/// Events that can be sent to plugins
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub enum PluginEvent {
89    /// Plugin has been loaded
90    Loaded,
91    /// Plugin is being unloaded
92    Unloading,
93    /// Host is shutting down
94    HostShutdown,
95    /// Configuration has changed
96    ConfigChanged(serde_json::Value),
97    /// System resource state changed
98    ResourceStateChanged {
99        cpu_usage: f32,
100        memory_usage: f32,
101        available_memory: u64,
102    },
103    /// Audio processing state changed
104    AudioStateChanged {
105        sample_rate: u32,
106        buffer_size: u32,
107        channels: u32,
108    },
109    /// Custom event with arbitrary data
110    Custom {
111        event_type: String,
112        data: serde_json::Value,
113    },
114}
115
116/// Plugin API registry for managing multiple plugin APIs
117pub struct ApiRegistry {
118    apis: HashMap<String, Arc<dyn PluginApi>>,
119    host_info: HostInfo,
120}
121
122impl ApiRegistry {
123    /// Create a new API registry
124    pub fn new() -> Self {
125        Self {
126            apis: HashMap::new(),
127            host_info: HostInfo {
128                name: "VoiRS CLI".to_string(),
129                version: env!("CARGO_PKG_VERSION").to_string(),
130                api_version: PLUGIN_API_VERSION.to_string(),
131                capabilities: vec![
132                    "audio_synthesis".to_string(),
133                    "voice_management".to_string(),
134                    "batch_processing".to_string(),
135                    "real_time_synthesis".to_string(),
136                    "plugin_system".to_string(),
137                ],
138                configuration: HashMap::new(),
139            },
140        }
141    }
142
143    /// Register a plugin API implementation
144    pub fn register_api(&mut self, name: String, api: Arc<dyn PluginApi>) -> PluginResult<()> {
145        if self.apis.contains_key(&name) {
146            return Err(PluginError::LoadingFailed(format!(
147                "API {} already registered",
148                name
149            )));
150        }
151
152        // Validate API version compatibility
153        if !self.is_version_compatible(api.api_version()) {
154            return Err(PluginError::ApiVersionMismatch {
155                expected: PLUGIN_API_VERSION.to_string(),
156                actual: api.api_version().to_string(),
157            });
158        }
159
160        self.apis.insert(name, api);
161        Ok(())
162    }
163
164    /// Unregister a plugin API
165    pub fn unregister_api(&mut self, name: &str) -> PluginResult<()> {
166        self.apis
167            .remove(name)
168            .ok_or_else(|| PluginError::NotFound(name.to_string()))?;
169        Ok(())
170    }
171
172    /// Get a registered API by name
173    pub fn get_api(&self, name: &str) -> Option<Arc<dyn PluginApi>> {
174        self.apis.get(name).cloned()
175    }
176
177    /// List all registered APIs
178    pub fn list_apis(&self) -> Vec<String> {
179        self.apis.keys().cloned().collect()
180    }
181
182    /// Initialize all registered APIs
183    pub fn initialize_all(&self) -> PluginResult<()> {
184        for (name, api) in &self.apis {
185            let mut api_mut = api.clone();
186            // Note: This would need proper mutable access in a real implementation
187            // For now, we'll skip the mutable initialization
188            tracing::info!("Initialized API: {}", name);
189        }
190        Ok(())
191    }
192
193    /// Broadcast an event to all registered APIs
194    pub fn broadcast_event(&self, event: &PluginEvent) -> PluginResult<()> {
195        for (name, api) in &self.apis {
196            if let Err(e) = api.notify(event) {
197                tracing::error!("Failed to notify API {}: {}", name, e);
198            }
199        }
200        Ok(())
201    }
202
203    /// Call a method on a specific API
204    pub fn call_api(&self, api_name: &str, call: &ApiCall) -> PluginResult<ApiResponse> {
205        let api = self
206            .apis
207            .get(api_name)
208            .ok_or_else(|| PluginError::NotFound(api_name.to_string()))?;
209
210        api.handle_api_call(call)
211    }
212
213    /// Check version compatibility
214    fn is_version_compatible(&self, version: &str) -> bool {
215        // Simple semantic versioning check
216        let min_parts: Vec<u32> = MIN_SUPPORTED_VERSION
217            .split('.')
218            .filter_map(|s| s.parse().ok())
219            .collect();
220        let max_parts: Vec<u32> = MAX_SUPPORTED_VERSION
221            .split('.')
222            .filter_map(|s| s.parse().ok())
223            .collect();
224        let version_parts: Vec<u32> = version.split('.').filter_map(|s| s.parse().ok()).collect();
225
226        if version_parts.len() != 3 || min_parts.len() != 3 || max_parts.len() != 3 {
227            return false;
228        }
229
230        // Check if version is within supported range
231        version_parts >= min_parts && version_parts <= max_parts
232    }
233
234    /// Get host information
235    pub fn get_host_info(&self) -> &HostInfo {
236        &self.host_info
237    }
238
239    /// Update host configuration
240    pub fn update_host_config(&mut self, key: String, value: serde_json::Value) {
241        self.host_info.configuration.insert(key, value);
242    }
243}
244
245impl Default for ApiRegistry {
246    fn default() -> Self {
247        Self::new()
248    }
249}
250
251/// Utility functions for plugin API
252pub mod utils {
253    use super::*;
254
255    /// Create a standard API call
256    pub fn create_api_call(method: &str, params: serde_json::Value) -> ApiCall {
257        ApiCall {
258            id: generate_call_id(),
259            method: method.to_string(),
260            params,
261            context: None,
262        }
263    }
264
265    /// Create an API call with context
266    pub fn create_api_call_with_context(
267        method: &str,
268        params: serde_json::Value,
269        context: CallContext,
270    ) -> ApiCall {
271        ApiCall {
272            id: generate_call_id(),
273            method: method.to_string(),
274            params,
275            context: Some(context),
276        }
277    }
278
279    /// Create a successful API response
280    pub fn create_success_response(call_id: &str, result: serde_json::Value) -> ApiResponse {
281        ApiResponse {
282            id: call_id.to_string(),
283            success: true,
284            result: Some(result),
285            error: None,
286            metadata: HashMap::new(),
287        }
288    }
289
290    /// Create an error API response
291    pub fn create_error_response(call_id: &str, error: &str) -> ApiResponse {
292        ApiResponse {
293            id: call_id.to_string(),
294            success: false,
295            result: None,
296            error: Some(error.to_string()),
297            metadata: HashMap::new(),
298        }
299    }
300
301    /// Generate a unique call ID
302    fn generate_call_id() -> String {
303        use std::time::{SystemTime, UNIX_EPOCH};
304        let timestamp = SystemTime::now()
305            .duration_since(UNIX_EPOCH)
306            .unwrap()
307            .as_nanos();
308        format!("call_{}", timestamp)
309    }
310
311    /// Validate API method name
312    pub fn is_valid_method_name(method: &str) -> bool {
313        !method.is_empty()
314            && method
315                .chars()
316                .all(|c| c.is_alphanumeric() || c == '_' || c == '.')
317    }
318
319    /// Extract plugin type from API call context
320    pub fn extract_plugin_type(call: &ApiCall) -> Option<PluginType> {
321        call.context
322            .as_ref()
323            .and_then(|ctx| ctx.metadata.get("plugin_type"))
324            .and_then(|t| match t.as_str() {
325                "Effect" => Some(PluginType::Effect),
326                "Voice" => Some(PluginType::Voice),
327                "Processor" => Some(PluginType::Processor),
328                "Extension" => Some(PluginType::Extension),
329                _ => None,
330            })
331    }
332}
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337
338    #[test]
339    fn test_api_registry_creation() {
340        let registry = ApiRegistry::new();
341        assert_eq!(registry.list_apis().len(), 0);
342        assert_eq!(registry.get_host_info().name, "VoiRS CLI");
343        assert_eq!(registry.get_host_info().api_version, PLUGIN_API_VERSION);
344    }
345
346    #[test]
347    fn test_version_compatibility() {
348        let registry = ApiRegistry::new();
349        assert!(registry.is_version_compatible("1.0.0"));
350        assert!(registry.is_version_compatible("1.0.1"));
351        assert!(registry.is_version_compatible("1.1.0"));
352        assert!(!registry.is_version_compatible("2.0.0"));
353        assert!(!registry.is_version_compatible("0.9.0"));
354    }
355
356    #[test]
357    fn test_api_call_creation() {
358        let call = utils::create_api_call("test_method", serde_json::json!({"param": "value"}));
359        assert_eq!(call.method, "test_method");
360        assert_eq!(call.params["param"], "value");
361        assert!(call.id.starts_with("call_"));
362    }
363
364    #[test]
365    fn test_api_response_creation() {
366        let response =
367            utils::create_success_response("test_id", serde_json::json!({"result": "ok"}));
368        assert_eq!(response.id, "test_id");
369        assert!(response.success);
370        assert_eq!(response.result.unwrap()["result"], "ok");
371
372        let error_response = utils::create_error_response("test_id", "test error");
373        assert_eq!(error_response.id, "test_id");
374        assert!(!error_response.success);
375        assert_eq!(error_response.error.unwrap(), "test error");
376    }
377
378    #[test]
379    fn test_method_name_validation() {
380        assert!(utils::is_valid_method_name("valid_method"));
381        assert!(utils::is_valid_method_name("method.with.dots"));
382        assert!(utils::is_valid_method_name("method123"));
383        assert!(!utils::is_valid_method_name(""));
384        assert!(!utils::is_valid_method_name("invalid-method"));
385        assert!(!utils::is_valid_method_name("invalid method"));
386    }
387
388    #[test]
389    fn test_host_info_structure() {
390        let registry = ApiRegistry::new();
391        let host_info = registry.get_host_info();
392
393        assert!(!host_info.name.is_empty());
394        assert!(!host_info.version.is_empty());
395        assert!(!host_info.api_version.is_empty());
396        assert!(!host_info.capabilities.is_empty());
397        assert!(host_info
398            .capabilities
399            .contains(&"audio_synthesis".to_string()));
400    }
401
402    #[test]
403    fn test_plugin_event_serialization() {
404        let event = PluginEvent::ConfigChanged(serde_json::json!({"setting": "value"}));
405        let serialized = serde_json::to_string(&event).unwrap();
406        let deserialized: PluginEvent = serde_json::from_str(&serialized).unwrap();
407
408        match deserialized {
409            PluginEvent::ConfigChanged(config) => {
410                assert_eq!(config["setting"], "value");
411            }
412            _ => panic!("Unexpected event type"),
413        }
414    }
415}