//! JSON RPC types for structured plugin function calls. //! //! Each extension point maps to well-known exported function names. //! Requests are serialized to JSON, passed to the plugin, and responses //! are deserialized from JSON written by the plugin via `host_set_result`. use std::{collections::HashMap, path::PathBuf}; use serde::{Deserialize, Serialize}; /// Request to check if a plugin can handle a file #[derive(Debug, Serialize)] pub struct CanHandleRequest { pub path: PathBuf, pub mime_type: Option, } /// Response from `can_handle` #[derive(Debug, Deserialize)] pub struct CanHandleResponse { pub can_handle: bool, } /// Media type definition returned by `supported_media_types` #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PluginMediaTypeDefinition { pub id: String, pub name: String, pub category: Option, pub extensions: Vec, pub mime_types: Vec, } /// Request to extract metadata from a file #[derive(Debug, Serialize)] pub struct ExtractMetadataRequest { pub path: PathBuf, } /// Metadata response from a plugin (all fields optional for partial results) #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct ExtractMetadataResponse { #[serde(default)] pub title: Option, #[serde(default)] pub artist: Option, #[serde(default)] pub album: Option, #[serde(default)] pub genre: Option, #[serde(default)] pub year: Option, #[serde(default)] pub duration_secs: Option, #[serde(default)] pub description: Option, #[serde(default)] pub extra: HashMap, } /// Request to generate a thumbnail #[derive(Debug, Serialize)] pub struct GenerateThumbnailRequest { pub source_path: PathBuf, pub output_path: PathBuf, pub max_width: u32, pub max_height: u32, pub format: String, } /// Response from thumbnail generation #[derive(Debug, Deserialize)] pub struct GenerateThumbnailResponse { pub path: PathBuf, pub width: u32, pub height: u32, pub format: String, } /// Event sent to event handler plugins #[derive(Debug, Serialize)] pub struct HandleEventRequest { pub event_type: String, pub payload: serde_json::Value, } /// Search request for search backend plugins #[derive(Debug, Serialize)] pub struct SearchRequest { pub query: String, pub limit: usize, pub offset: usize, } /// Search response #[derive(Debug, Clone, Deserialize)] pub struct SearchResponse { pub results: Vec, #[serde(default)] pub total_count: Option, } /// Individual search result #[derive(Debug, Clone, Deserialize)] pub struct SearchResultItem { pub id: String, pub score: f64, pub snippet: Option, } /// Request to index a media item in a search backend #[derive(Debug, Serialize)] pub struct IndexItemRequest { pub id: String, pub title: Option, pub artist: Option, pub album: Option, pub description: Option, pub tags: Vec, pub media_type: String, pub path: PathBuf, } /// Request to remove a media item from a search backend #[derive(Debug, Serialize)] pub struct RemoveItemRequest { pub id: String, } /// A theme definition returned by a theme provider plugin #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PluginThemeDefinition { pub id: String, pub name: String, pub description: Option, pub dark: bool, } /// Response from `load_theme` #[derive(Debug, Clone, Deserialize)] pub struct LoadThemeResponse { pub css: Option, pub colors: HashMap, } #[cfg(test)] mod tests { use super::*; #[test] fn test_extract_metadata_request_serialization() { let req = ExtractMetadataRequest { path: "/tmp/test.mp3".into(), }; let json = serde_json::to_string(&req).unwrap(); assert!(json.contains("/tmp/test.mp3")); } #[test] fn test_extract_metadata_response_partial() { let json = r#"{"title":"My Song","extra":{"bpm":"120"}}"#; let resp: ExtractMetadataResponse = serde_json::from_str(json).unwrap(); assert_eq!(resp.title.as_deref(), Some("My Song")); assert_eq!(resp.artist, None); assert_eq!(resp.extra.get("bpm").map(String::as_str), Some("120")); } #[test] fn test_extract_metadata_response_empty() { let json = "{}"; let resp: ExtractMetadataResponse = serde_json::from_str(json).unwrap(); assert_eq!(resp.title, None); assert!(resp.extra.is_empty()); } #[test] fn test_can_handle_response() { let json = r#"{"can_handle":true}"#; let resp: CanHandleResponse = serde_json::from_str(json).unwrap(); assert!(resp.can_handle); } #[test] fn test_can_handle_response_false() { let json = r#"{"can_handle":false}"#; let resp: CanHandleResponse = serde_json::from_str(json).unwrap(); assert!(!resp.can_handle); } #[test] fn test_plugin_media_type_definition_round_trip() { let def = PluginMediaTypeDefinition { id: "heif".to_string(), name: "HEIF Image".to_string(), category: Some("image".to_string()), extensions: vec!["heif".to_string(), "heic".to_string()], mime_types: vec!["image/heif".to_string()], }; let json = serde_json::to_string(&def).unwrap(); let parsed: PluginMediaTypeDefinition = serde_json::from_str(&json).unwrap(); assert_eq!(parsed.id, "heif"); assert_eq!(parsed.extensions.len(), 2); } #[test] fn test_search_response() { let json = r#"{"results":[{"id":"abc","score":0.95,"snippet":"match here"}]}"#; let resp: SearchResponse = serde_json::from_str(json).unwrap(); assert_eq!(resp.results.len(), 1); assert_eq!(resp.results[0].id, "abc"); } #[test] fn test_generate_thumbnail_request_serialization() { let req = GenerateThumbnailRequest { source_path: "/media/photo.heif".into(), output_path: "/tmp/thumb.jpg".into(), max_width: 256, max_height: 256, format: "jpeg".to_string(), }; let json = serde_json::to_string(&req).unwrap(); assert!(json.contains("photo.heif")); assert!(json.contains("256")); } #[test] fn test_handle_event_request_serialization() { let req = HandleEventRequest { event_type: "MediaImported".to_string(), payload: serde_json::json!({"id": "abc-123"}), }; let json = serde_json::to_string(&req).unwrap(); assert!(json.contains("MediaImported")); assert!(json.contains("abc-123")); } }