treewide: fix various UI bugs; optimize crypto dependencies & format
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: If8fe8b38c1d9c4fecd40ff71f88d2ae06a6a6964
This commit is contained in:
parent
764aafa88d
commit
3ccddce7fd
178 changed files with 58342 additions and 54241 deletions
|
|
@ -1,12 +1,16 @@
|
|||
//! Pinakes Plugin API
|
||||
//!
|
||||
//! This crate defines the stable plugin interface for Pinakes.
|
||||
//! Plugins can extend Pinakes by implementing one or more of the provided traits.
|
||||
//! Plugins can extend Pinakes by implementing one or more of the provided
|
||||
//! traits.
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use thiserror::Error;
|
||||
|
||||
pub mod manifest;
|
||||
|
|
@ -26,350 +30,361 @@ pub type PluginResult<T> = Result<T, PluginError>;
|
|||
/// Errors that can occur in plugin operations
|
||||
#[derive(Debug, Error, Serialize, Deserialize)]
|
||||
pub enum PluginError {
|
||||
#[error("Plugin initialization failed: {0}")]
|
||||
InitializationFailed(String),
|
||||
#[error("Plugin initialization failed: {0}")]
|
||||
InitializationFailed(String),
|
||||
|
||||
#[error("Unsupported operation: {0}")]
|
||||
UnsupportedOperation(String),
|
||||
#[error("Unsupported operation: {0}")]
|
||||
UnsupportedOperation(String),
|
||||
|
||||
#[error("Invalid input: {0}")]
|
||||
InvalidInput(String),
|
||||
#[error("Invalid input: {0}")]
|
||||
InvalidInput(String),
|
||||
|
||||
#[error("IO error: {0}")]
|
||||
IoError(String),
|
||||
#[error("IO error: {0}")]
|
||||
IoError(String),
|
||||
|
||||
#[error("Metadata extraction failed: {0}")]
|
||||
MetadataExtractionFailed(String),
|
||||
#[error("Metadata extraction failed: {0}")]
|
||||
MetadataExtractionFailed(String),
|
||||
|
||||
#[error("Thumbnail generation failed: {0}")]
|
||||
ThumbnailGenerationFailed(String),
|
||||
#[error("Thumbnail generation failed: {0}")]
|
||||
ThumbnailGenerationFailed(String),
|
||||
|
||||
#[error("Search backend error: {0}")]
|
||||
SearchBackendError(String),
|
||||
#[error("Search backend error: {0}")]
|
||||
SearchBackendError(String),
|
||||
|
||||
#[error("Permission denied: {0}")]
|
||||
PermissionDenied(String),
|
||||
#[error("Permission denied: {0}")]
|
||||
PermissionDenied(String),
|
||||
|
||||
#[error("Resource limit exceeded: {0}")]
|
||||
ResourceLimitExceeded(String),
|
||||
#[error("Resource limit exceeded: {0}")]
|
||||
ResourceLimitExceeded(String),
|
||||
|
||||
#[error("Plugin error: {0}")]
|
||||
Other(String),
|
||||
#[error("Plugin error: {0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
/// Context provided to plugins during initialization
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PluginContext {
|
||||
/// Plugin's data directory for persistent storage
|
||||
pub data_dir: PathBuf,
|
||||
/// Plugin's data directory for persistent storage
|
||||
pub data_dir: PathBuf,
|
||||
|
||||
/// Plugin's cache directory for temporary data
|
||||
pub cache_dir: PathBuf,
|
||||
/// Plugin's cache directory for temporary data
|
||||
pub cache_dir: PathBuf,
|
||||
|
||||
/// Plugin configuration from manifest
|
||||
pub config: HashMap<String, serde_json::Value>,
|
||||
/// Plugin configuration from manifest
|
||||
pub config: HashMap<String, serde_json::Value>,
|
||||
|
||||
/// Capabilities granted to the plugin
|
||||
pub capabilities: Capabilities,
|
||||
/// Capabilities granted to the plugin
|
||||
pub capabilities: Capabilities,
|
||||
}
|
||||
|
||||
/// Capabilities that can be granted to plugins
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct Capabilities {
|
||||
/// Filesystem access permissions
|
||||
pub filesystem: FilesystemCapability,
|
||||
/// Filesystem access permissions
|
||||
pub filesystem: FilesystemCapability,
|
||||
|
||||
/// Network access permissions
|
||||
pub network: NetworkCapability,
|
||||
/// Network access permissions
|
||||
pub network: NetworkCapability,
|
||||
|
||||
/// Environment variable access
|
||||
pub environment: EnvironmentCapability,
|
||||
/// Environment variable access
|
||||
pub environment: EnvironmentCapability,
|
||||
|
||||
/// Maximum memory usage in bytes
|
||||
pub max_memory_bytes: Option<usize>,
|
||||
/// Maximum memory usage in bytes
|
||||
pub max_memory_bytes: Option<usize>,
|
||||
|
||||
/// Maximum CPU time in milliseconds
|
||||
pub max_cpu_time_ms: Option<u64>,
|
||||
/// Maximum CPU time in milliseconds
|
||||
pub max_cpu_time_ms: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct FilesystemCapability {
|
||||
/// Paths allowed for reading
|
||||
pub read: Vec<PathBuf>,
|
||||
/// Paths allowed for reading
|
||||
pub read: Vec<PathBuf>,
|
||||
|
||||
/// Paths allowed for writing
|
||||
pub write: Vec<PathBuf>,
|
||||
/// Paths allowed for writing
|
||||
pub write: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct NetworkCapability {
|
||||
/// Whether network access is allowed
|
||||
pub enabled: bool,
|
||||
/// Whether network access is allowed
|
||||
pub enabled: bool,
|
||||
|
||||
/// Allowed domains (if None, all domains allowed when enabled)
|
||||
pub allowed_domains: Option<Vec<String>>,
|
||||
/// Allowed domains (if None, all domains allowed when enabled)
|
||||
pub allowed_domains: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct EnvironmentCapability {
|
||||
/// Whether environment variable access is allowed
|
||||
pub enabled: bool,
|
||||
/// Whether environment variable access is allowed
|
||||
pub enabled: bool,
|
||||
|
||||
/// Specific environment variables allowed (if None, all allowed when enabled)
|
||||
pub allowed_vars: Option<Vec<String>>,
|
||||
/// Specific environment variables allowed (if None, all allowed when
|
||||
/// enabled)
|
||||
pub allowed_vars: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// Base trait that all plugins must implement
|
||||
#[async_trait]
|
||||
pub trait Plugin: Send + Sync {
|
||||
/// Get plugin metadata
|
||||
fn metadata(&self) -> &PluginMetadata;
|
||||
/// Get plugin metadata
|
||||
fn metadata(&self) -> &PluginMetadata;
|
||||
|
||||
/// Initialize the plugin with provided context
|
||||
async fn initialize(&mut self, context: PluginContext) -> PluginResult<()>;
|
||||
/// Initialize the plugin with provided context
|
||||
async fn initialize(&mut self, context: PluginContext) -> PluginResult<()>;
|
||||
|
||||
/// Shutdown the plugin gracefully
|
||||
async fn shutdown(&mut self) -> PluginResult<()>;
|
||||
/// Shutdown the plugin gracefully
|
||||
async fn shutdown(&mut self) -> PluginResult<()>;
|
||||
|
||||
/// Get plugin health status
|
||||
async fn health_check(&self) -> PluginResult<HealthStatus>;
|
||||
/// Get plugin health status
|
||||
async fn health_check(&self) -> PluginResult<HealthStatus>;
|
||||
}
|
||||
|
||||
/// Plugin metadata
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PluginMetadata {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub author: String,
|
||||
pub description: String,
|
||||
pub api_version: String,
|
||||
pub capabilities_required: Capabilities,
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub author: String,
|
||||
pub description: String,
|
||||
pub api_version: String,
|
||||
pub capabilities_required: Capabilities,
|
||||
}
|
||||
|
||||
/// Health status of a plugin
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HealthStatus {
|
||||
pub healthy: bool,
|
||||
pub message: Option<String>,
|
||||
pub metrics: HashMap<String, f64>,
|
||||
pub healthy: bool,
|
||||
pub message: Option<String>,
|
||||
pub metrics: HashMap<String, f64>,
|
||||
}
|
||||
|
||||
/// Trait for plugins that provide custom media type support
|
||||
#[async_trait]
|
||||
pub trait MediaTypeProvider: Plugin {
|
||||
/// Get the list of media types this plugin supports
|
||||
fn supported_media_types(&self) -> Vec<MediaTypeDefinition>;
|
||||
/// Get the list of media types this plugin supports
|
||||
fn supported_media_types(&self) -> Vec<MediaTypeDefinition>;
|
||||
|
||||
/// Check if this plugin can handle the given file
|
||||
async fn can_handle(&self, path: &Path, mime_type: Option<&str>) -> PluginResult<bool>;
|
||||
/// Check if this plugin can handle the given file
|
||||
async fn can_handle(
|
||||
&self,
|
||||
path: &Path,
|
||||
mime_type: Option<&str>,
|
||||
) -> PluginResult<bool>;
|
||||
}
|
||||
|
||||
/// Definition of a custom media type
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MediaTypeDefinition {
|
||||
/// Unique identifier for this media type
|
||||
pub id: String,
|
||||
/// Unique identifier for this media type
|
||||
pub id: String,
|
||||
|
||||
/// Display name
|
||||
pub name: String,
|
||||
/// Display name
|
||||
pub name: String,
|
||||
|
||||
/// Category (e.g., "video", "audio", "document", "image")
|
||||
pub category: String,
|
||||
/// Category (e.g., "video", "audio", "document", "image")
|
||||
pub category: String,
|
||||
|
||||
/// File extensions associated with this type
|
||||
pub extensions: Vec<String>,
|
||||
/// File extensions associated with this type
|
||||
pub extensions: Vec<String>,
|
||||
|
||||
/// MIME types associated with this type
|
||||
pub mime_types: Vec<String>,
|
||||
/// MIME types associated with this type
|
||||
pub mime_types: Vec<String>,
|
||||
|
||||
/// Icon name or path
|
||||
pub icon: Option<String>,
|
||||
/// Icon name or path
|
||||
pub icon: Option<String>,
|
||||
}
|
||||
|
||||
/// Trait for plugins that extract metadata from files
|
||||
#[async_trait]
|
||||
pub trait MetadataExtractor: Plugin {
|
||||
/// Extract metadata from a file
|
||||
async fn extract_metadata(&self, path: &Path) -> PluginResult<ExtractedMetadata>;
|
||||
/// Extract metadata from a file
|
||||
async fn extract_metadata(
|
||||
&self,
|
||||
path: &Path,
|
||||
) -> PluginResult<ExtractedMetadata>;
|
||||
|
||||
/// Get the media types this extractor supports
|
||||
fn supported_types(&self) -> Vec<String>;
|
||||
/// Get the media types this extractor supports
|
||||
fn supported_types(&self) -> Vec<String>;
|
||||
}
|
||||
|
||||
/// Metadata extracted from a file
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ExtractedMetadata {
|
||||
pub title: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub author: Option<String>,
|
||||
pub created_at: Option<String>,
|
||||
pub duration_secs: Option<f64>,
|
||||
pub width: Option<u32>,
|
||||
pub height: Option<u32>,
|
||||
pub file_size_bytes: Option<u64>,
|
||||
pub codec: Option<String>,
|
||||
pub bitrate_kbps: Option<u32>,
|
||||
pub title: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub author: Option<String>,
|
||||
pub created_at: Option<String>,
|
||||
pub duration_secs: Option<f64>,
|
||||
pub width: Option<u32>,
|
||||
pub height: Option<u32>,
|
||||
pub file_size_bytes: Option<u64>,
|
||||
pub codec: Option<String>,
|
||||
pub bitrate_kbps: Option<u32>,
|
||||
|
||||
/// Custom metadata fields specific to this file type
|
||||
pub custom_fields: HashMap<String, serde_json::Value>,
|
||||
/// Custom metadata fields specific to this file type
|
||||
pub custom_fields: HashMap<String, serde_json::Value>,
|
||||
|
||||
/// Tags extracted from the file
|
||||
pub tags: Vec<String>,
|
||||
/// Tags extracted from the file
|
||||
pub tags: Vec<String>,
|
||||
}
|
||||
|
||||
/// Trait for plugins that generate thumbnails
|
||||
#[async_trait]
|
||||
pub trait ThumbnailGenerator: Plugin {
|
||||
/// Generate a thumbnail for the given file
|
||||
async fn generate_thumbnail(
|
||||
&self,
|
||||
path: &Path,
|
||||
output_path: &Path,
|
||||
options: ThumbnailOptions,
|
||||
) -> PluginResult<ThumbnailInfo>;
|
||||
/// Generate a thumbnail for the given file
|
||||
async fn generate_thumbnail(
|
||||
&self,
|
||||
path: &Path,
|
||||
output_path: &Path,
|
||||
options: ThumbnailOptions,
|
||||
) -> PluginResult<ThumbnailInfo>;
|
||||
|
||||
/// Get the media types this generator supports
|
||||
fn supported_types(&self) -> Vec<String>;
|
||||
/// Get the media types this generator supports
|
||||
fn supported_types(&self) -> Vec<String>;
|
||||
}
|
||||
|
||||
/// Options for thumbnail generation
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ThumbnailOptions {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub quality: u8,
|
||||
pub format: ThumbnailFormat,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub quality: u8,
|
||||
pub format: ThumbnailFormat,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum ThumbnailFormat {
|
||||
Jpeg,
|
||||
Png,
|
||||
WebP,
|
||||
Jpeg,
|
||||
Png,
|
||||
WebP,
|
||||
}
|
||||
|
||||
/// Information about a generated thumbnail
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ThumbnailInfo {
|
||||
pub path: PathBuf,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub file_size_bytes: u64,
|
||||
pub path: PathBuf,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub file_size_bytes: u64,
|
||||
}
|
||||
|
||||
/// Trait for plugins that provide custom search backends
|
||||
#[async_trait]
|
||||
pub trait SearchBackend: Plugin {
|
||||
/// Index a media item for search
|
||||
async fn index_item(&self, item: &SearchIndexItem) -> PluginResult<()>;
|
||||
/// Index a media item for search
|
||||
async fn index_item(&self, item: &SearchIndexItem) -> PluginResult<()>;
|
||||
|
||||
/// Remove an item from the search index
|
||||
async fn remove_item(&self, item_id: &str) -> PluginResult<()>;
|
||||
/// Remove an item from the search index
|
||||
async fn remove_item(&self, item_id: &str) -> PluginResult<()>;
|
||||
|
||||
/// Perform a search query
|
||||
async fn search(&self, query: &SearchQuery) -> PluginResult<Vec<SearchResult>>;
|
||||
/// Perform a search query
|
||||
async fn search(
|
||||
&self,
|
||||
query: &SearchQuery,
|
||||
) -> PluginResult<Vec<SearchResult>>;
|
||||
|
||||
/// Get search statistics
|
||||
async fn get_stats(&self) -> PluginResult<SearchStats>;
|
||||
/// Get search statistics
|
||||
async fn get_stats(&self) -> PluginResult<SearchStats>;
|
||||
}
|
||||
|
||||
/// Item to be indexed for search
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SearchIndexItem {
|
||||
pub id: String,
|
||||
pub title: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub content: Option<String>,
|
||||
pub tags: Vec<String>,
|
||||
pub media_type: String,
|
||||
pub metadata: HashMap<String, serde_json::Value>,
|
||||
pub id: String,
|
||||
pub title: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub content: Option<String>,
|
||||
pub tags: Vec<String>,
|
||||
pub media_type: String,
|
||||
pub metadata: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
/// Search query
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SearchQuery {
|
||||
pub query_text: String,
|
||||
pub filters: HashMap<String, serde_json::Value>,
|
||||
pub limit: usize,
|
||||
pub offset: usize,
|
||||
pub query_text: String,
|
||||
pub filters: HashMap<String, serde_json::Value>,
|
||||
pub limit: usize,
|
||||
pub offset: usize,
|
||||
}
|
||||
|
||||
/// Search result
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SearchResult {
|
||||
pub id: String,
|
||||
pub score: f64,
|
||||
pub highlights: Vec<String>,
|
||||
pub id: String,
|
||||
pub score: f64,
|
||||
pub highlights: Vec<String>,
|
||||
}
|
||||
|
||||
/// Search statistics
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SearchStats {
|
||||
pub total_indexed: usize,
|
||||
pub index_size_bytes: u64,
|
||||
pub last_update: Option<String>,
|
||||
pub total_indexed: usize,
|
||||
pub index_size_bytes: u64,
|
||||
pub last_update: Option<String>,
|
||||
}
|
||||
|
||||
/// Trait for plugins that handle events
|
||||
#[async_trait]
|
||||
pub trait EventHandler: Plugin {
|
||||
/// Handle an event
|
||||
async fn handle_event(&self, event: &Event) -> PluginResult<()>;
|
||||
/// Handle an event
|
||||
async fn handle_event(&self, event: &Event) -> PluginResult<()>;
|
||||
|
||||
/// Get the event types this handler is interested in
|
||||
fn interested_events(&self) -> Vec<EventType>;
|
||||
/// Get the event types this handler is interested in
|
||||
fn interested_events(&self) -> Vec<EventType>;
|
||||
}
|
||||
|
||||
/// Event type
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
pub enum EventType {
|
||||
MediaImported,
|
||||
MediaUpdated,
|
||||
MediaDeleted,
|
||||
MediaTagged,
|
||||
MediaUntagged,
|
||||
CollectionCreated,
|
||||
CollectionUpdated,
|
||||
CollectionDeleted,
|
||||
ScanStarted,
|
||||
ScanCompleted,
|
||||
Custom(String),
|
||||
MediaImported,
|
||||
MediaUpdated,
|
||||
MediaDeleted,
|
||||
MediaTagged,
|
||||
MediaUntagged,
|
||||
CollectionCreated,
|
||||
CollectionUpdated,
|
||||
CollectionDeleted,
|
||||
ScanStarted,
|
||||
ScanCompleted,
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
/// Event data
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Event {
|
||||
pub event_type: EventType,
|
||||
pub timestamp: String,
|
||||
pub data: HashMap<String, serde_json::Value>,
|
||||
pub event_type: EventType,
|
||||
pub timestamp: String,
|
||||
pub data: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
/// Trait for plugins that provide UI themes
|
||||
#[async_trait]
|
||||
pub trait ThemeProvider: Plugin {
|
||||
/// Get available themes from this provider
|
||||
fn get_themes(&self) -> Vec<ThemeDefinition>;
|
||||
/// Get available themes from this provider
|
||||
fn get_themes(&self) -> Vec<ThemeDefinition>;
|
||||
|
||||
/// Load a specific theme by ID
|
||||
async fn load_theme(&self, theme_id: &str) -> PluginResult<Theme>;
|
||||
/// Load a specific theme by ID
|
||||
async fn load_theme(&self, theme_id: &str) -> PluginResult<Theme>;
|
||||
}
|
||||
|
||||
/// Theme definition
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ThemeDefinition {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub author: String,
|
||||
pub preview_url: Option<String>,
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub author: String,
|
||||
pub preview_url: Option<String>,
|
||||
}
|
||||
|
||||
/// Theme data
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Theme {
|
||||
pub id: String,
|
||||
pub colors: HashMap<String, String>,
|
||||
pub fonts: HashMap<String, String>,
|
||||
pub custom_css: Option<String>,
|
||||
pub id: String,
|
||||
pub colors: HashMap<String, String>,
|
||||
pub fonts: HashMap<String, String>,
|
||||
pub custom_css: Option<String>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,213 +1,218 @@
|
|||
//! Plugin manifest parsing and validation
|
||||
|
||||
use std::{collections::HashMap, path::Path};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{Capabilities, EnvironmentCapability, FilesystemCapability, NetworkCapability};
|
||||
use crate::{
|
||||
Capabilities,
|
||||
EnvironmentCapability,
|
||||
FilesystemCapability,
|
||||
NetworkCapability,
|
||||
};
|
||||
|
||||
/// Plugin manifest file format (TOML)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PluginManifest {
|
||||
pub plugin: PluginInfo,
|
||||
pub plugin: PluginInfo,
|
||||
|
||||
#[serde(default)]
|
||||
pub capabilities: ManifestCapabilities,
|
||||
#[serde(default)]
|
||||
pub capabilities: ManifestCapabilities,
|
||||
|
||||
#[serde(default)]
|
||||
pub config: HashMap<String, toml::Value>,
|
||||
#[serde(default)]
|
||||
pub config: HashMap<String, toml::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PluginInfo {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub api_version: String,
|
||||
pub author: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub homepage: Option<String>,
|
||||
pub license: Option<String>,
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub api_version: String,
|
||||
pub author: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub homepage: Option<String>,
|
||||
pub license: Option<String>,
|
||||
|
||||
/// Plugin kind(s) - e.g., ["media_type", "metadata_extractor"]
|
||||
pub kind: Vec<String>,
|
||||
/// Plugin kind(s) - e.g., ["media_type", "metadata_extractor"]
|
||||
pub kind: Vec<String>,
|
||||
|
||||
/// Binary configuration
|
||||
pub binary: PluginBinary,
|
||||
/// Binary configuration
|
||||
pub binary: PluginBinary,
|
||||
|
||||
/// Dependencies on other plugins
|
||||
#[serde(default)]
|
||||
pub dependencies: Vec<String>,
|
||||
/// Dependencies on other plugins
|
||||
#[serde(default)]
|
||||
pub dependencies: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PluginBinary {
|
||||
/// Path to WASM binary
|
||||
pub wasm: String,
|
||||
/// Path to WASM binary
|
||||
pub wasm: String,
|
||||
|
||||
/// Optional entrypoint function name (default: "_start")
|
||||
pub entrypoint: Option<String>,
|
||||
/// Optional entrypoint function name (default: "_start")
|
||||
pub entrypoint: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ManifestCapabilities {
|
||||
#[serde(default)]
|
||||
pub filesystem: ManifestFilesystemCapability,
|
||||
#[serde(default)]
|
||||
pub filesystem: ManifestFilesystemCapability,
|
||||
|
||||
#[serde(default)]
|
||||
pub network: bool,
|
||||
#[serde(default)]
|
||||
pub network: bool,
|
||||
|
||||
#[serde(default)]
|
||||
pub environment: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub environment: Option<Vec<String>>,
|
||||
|
||||
#[serde(default)]
|
||||
pub max_memory_mb: Option<usize>,
|
||||
#[serde(default)]
|
||||
pub max_memory_mb: Option<usize>,
|
||||
|
||||
#[serde(default)]
|
||||
pub max_cpu_time_secs: Option<u64>,
|
||||
#[serde(default)]
|
||||
pub max_cpu_time_secs: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ManifestFilesystemCapability {
|
||||
#[serde(default)]
|
||||
pub read: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub read: Vec<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub write: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub write: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ManifestError {
|
||||
#[error("Failed to read manifest file: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
#[error("Failed to read manifest file: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
#[error("Failed to parse manifest: {0}")]
|
||||
ParseError(#[from] toml::de::Error),
|
||||
#[error("Failed to parse manifest: {0}")]
|
||||
ParseError(#[from] toml::de::Error),
|
||||
|
||||
#[error("Invalid manifest: {0}")]
|
||||
ValidationError(String),
|
||||
#[error("Invalid manifest: {0}")]
|
||||
ValidationError(String),
|
||||
}
|
||||
|
||||
impl PluginManifest {
|
||||
/// Load and parse a plugin manifest from a TOML file
|
||||
pub fn from_file(path: &Path) -> Result<Self, ManifestError> {
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
let manifest: Self = toml::from_str(&content)?;
|
||||
manifest.validate()?;
|
||||
Ok(manifest)
|
||||
/// Load and parse a plugin manifest from a TOML file
|
||||
pub fn from_file(path: &Path) -> Result<Self, ManifestError> {
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
let manifest: Self = toml::from_str(&content)?;
|
||||
manifest.validate()?;
|
||||
Ok(manifest)
|
||||
}
|
||||
|
||||
/// Parse a manifest from TOML string
|
||||
pub fn parse_str(content: &str) -> Result<Self, ManifestError> {
|
||||
let manifest: Self = toml::from_str(content)?;
|
||||
manifest.validate()?;
|
||||
Ok(manifest)
|
||||
}
|
||||
|
||||
/// Validate the manifest
|
||||
pub fn validate(&self) -> Result<(), ManifestError> {
|
||||
// Check API version format
|
||||
if self.plugin.api_version.is_empty() {
|
||||
return Err(ManifestError::ValidationError(
|
||||
"api_version cannot be empty".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
/// Parse a manifest from TOML string
|
||||
pub fn parse_str(content: &str) -> Result<Self, ManifestError> {
|
||||
let manifest: Self = toml::from_str(content)?;
|
||||
manifest.validate()?;
|
||||
Ok(manifest)
|
||||
// Check version format (basic semver check)
|
||||
if !self.plugin.version.contains('.') {
|
||||
return Err(ManifestError::ValidationError(
|
||||
"version must be in semver format (e.g., 1.0.0)".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
/// Validate the manifest
|
||||
pub fn validate(&self) -> Result<(), ManifestError> {
|
||||
// Check API version format
|
||||
if self.plugin.api_version.is_empty() {
|
||||
return Err(ManifestError::ValidationError(
|
||||
"api_version cannot be empty".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Check version format (basic semver check)
|
||||
if !self.plugin.version.contains('.') {
|
||||
return Err(ManifestError::ValidationError(
|
||||
"version must be in semver format (e.g., 1.0.0)".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Check that at least one kind is specified
|
||||
if self.plugin.kind.is_empty() {
|
||||
return Err(ManifestError::ValidationError(
|
||||
"at least one plugin kind must be specified".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Check plugin kinds are valid
|
||||
let valid_kinds = [
|
||||
"media_type",
|
||||
"metadata_extractor",
|
||||
"thumbnail_generator",
|
||||
"search_backend",
|
||||
"event_handler",
|
||||
"theme_provider",
|
||||
];
|
||||
|
||||
for kind in &self.plugin.kind {
|
||||
if !valid_kinds.contains(&kind.as_str()) {
|
||||
return Err(ManifestError::ValidationError(format!(
|
||||
"Invalid plugin kind: {}. Must be one of: {}",
|
||||
kind,
|
||||
valid_kinds.join(", ")
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
// Check WASM binary path is not empty
|
||||
if self.plugin.binary.wasm.is_empty() {
|
||||
return Err(ManifestError::ValidationError(
|
||||
"WASM binary path cannot be empty".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
// Check that at least one kind is specified
|
||||
if self.plugin.kind.is_empty() {
|
||||
return Err(ManifestError::ValidationError(
|
||||
"at least one plugin kind must be specified".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
/// Convert manifest capabilities to API capabilities
|
||||
pub fn to_capabilities(&self) -> Capabilities {
|
||||
Capabilities {
|
||||
filesystem: FilesystemCapability {
|
||||
read: self
|
||||
.capabilities
|
||||
.filesystem
|
||||
.read
|
||||
.iter()
|
||||
.map(|s| s.into())
|
||||
.collect(),
|
||||
write: self
|
||||
.capabilities
|
||||
.filesystem
|
||||
.write
|
||||
.iter()
|
||||
.map(|s| s.into())
|
||||
.collect(),
|
||||
},
|
||||
network: NetworkCapability {
|
||||
enabled: self.capabilities.network,
|
||||
allowed_domains: None,
|
||||
},
|
||||
environment: EnvironmentCapability {
|
||||
enabled: self.capabilities.environment.is_some(),
|
||||
allowed_vars: self.capabilities.environment.clone(),
|
||||
},
|
||||
max_memory_bytes: self
|
||||
.capabilities
|
||||
.max_memory_mb
|
||||
.map(|mb| mb.saturating_mul(1024).saturating_mul(1024)),
|
||||
max_cpu_time_ms: self
|
||||
.capabilities
|
||||
.max_cpu_time_secs
|
||||
.map(|secs| secs.saturating_mul(1000)),
|
||||
}
|
||||
// Check plugin kinds are valid
|
||||
let valid_kinds = [
|
||||
"media_type",
|
||||
"metadata_extractor",
|
||||
"thumbnail_generator",
|
||||
"search_backend",
|
||||
"event_handler",
|
||||
"theme_provider",
|
||||
];
|
||||
|
||||
for kind in &self.plugin.kind {
|
||||
if !valid_kinds.contains(&kind.as_str()) {
|
||||
return Err(ManifestError::ValidationError(format!(
|
||||
"Invalid plugin kind: {}. Must be one of: {}",
|
||||
kind,
|
||||
valid_kinds.join(", ")
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
/// Get plugin ID (derived from name and version)
|
||||
pub fn plugin_id(&self) -> String {
|
||||
format!("{}@{}", self.plugin.name, self.plugin.version)
|
||||
// Check WASM binary path is not empty
|
||||
if self.plugin.binary.wasm.is_empty() {
|
||||
return Err(ManifestError::ValidationError(
|
||||
"WASM binary path cannot be empty".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Convert manifest capabilities to API capabilities
|
||||
pub fn to_capabilities(&self) -> Capabilities {
|
||||
Capabilities {
|
||||
filesystem: FilesystemCapability {
|
||||
read: self
|
||||
.capabilities
|
||||
.filesystem
|
||||
.read
|
||||
.iter()
|
||||
.map(|s| s.into())
|
||||
.collect(),
|
||||
write: self
|
||||
.capabilities
|
||||
.filesystem
|
||||
.write
|
||||
.iter()
|
||||
.map(|s| s.into())
|
||||
.collect(),
|
||||
},
|
||||
network: NetworkCapability {
|
||||
enabled: self.capabilities.network,
|
||||
allowed_domains: None,
|
||||
},
|
||||
environment: EnvironmentCapability {
|
||||
enabled: self.capabilities.environment.is_some(),
|
||||
allowed_vars: self.capabilities.environment.clone(),
|
||||
},
|
||||
max_memory_bytes: self
|
||||
.capabilities
|
||||
.max_memory_mb
|
||||
.map(|mb| mb.saturating_mul(1024).saturating_mul(1024)),
|
||||
max_cpu_time_ms: self
|
||||
.capabilities
|
||||
.max_cpu_time_secs
|
||||
.map(|secs| secs.saturating_mul(1000)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get plugin ID (derived from name and version)
|
||||
pub fn plugin_id(&self) -> String {
|
||||
format!("{}@{}", self.plugin.name, self.plugin.version)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_valid_manifest() {
|
||||
let toml = r#"
|
||||
#[test]
|
||||
fn test_parse_valid_manifest() {
|
||||
let toml = r#"
|
||||
[plugin]
|
||||
name = "heif-support"
|
||||
version = "1.0.0"
|
||||
|
|
@ -223,15 +228,15 @@ wasm = "plugin.wasm"
|
|||
read = ["/tmp/pinakes-thumbnails"]
|
||||
"#;
|
||||
|
||||
let manifest = PluginManifest::parse_str(toml).unwrap();
|
||||
assert_eq!(manifest.plugin.name, "heif-support");
|
||||
assert_eq!(manifest.plugin.version, "1.0.0");
|
||||
assert_eq!(manifest.plugin.kind.len(), 2);
|
||||
}
|
||||
let manifest = PluginManifest::parse_str(toml).unwrap();
|
||||
assert_eq!(manifest.plugin.name, "heif-support");
|
||||
assert_eq!(manifest.plugin.version, "1.0.0");
|
||||
assert_eq!(manifest.plugin.kind.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_api_version() {
|
||||
let toml = r#"
|
||||
#[test]
|
||||
fn test_invalid_api_version() {
|
||||
let toml = r#"
|
||||
[plugin]
|
||||
name = "test"
|
||||
version = "1.0.0"
|
||||
|
|
@ -242,12 +247,12 @@ kind = ["media_type"]
|
|||
wasm = "plugin.wasm"
|
||||
"#;
|
||||
|
||||
assert!(PluginManifest::parse_str(toml).is_err());
|
||||
}
|
||||
assert!(PluginManifest::parse_str(toml).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_kind() {
|
||||
let toml = r#"
|
||||
#[test]
|
||||
fn test_invalid_kind() {
|
||||
let toml = r#"
|
||||
[plugin]
|
||||
name = "test"
|
||||
version = "1.0.0"
|
||||
|
|
@ -258,6 +263,6 @@ kind = ["invalid_kind"]
|
|||
wasm = "plugin.wasm"
|
||||
"#;
|
||||
|
||||
assert!(PluginManifest::parse_str(toml).is_err());
|
||||
}
|
||||
assert!(PluginManifest::parse_str(toml).is_err());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,156 +1,157 @@
|
|||
//! Shared types used across the plugin API
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Plugin identifier
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct PluginId(String);
|
||||
|
||||
impl PluginId {
|
||||
pub fn new(id: impl Into<String>) -> Self {
|
||||
Self(id.into())
|
||||
}
|
||||
pub fn new(id: impl Into<String>) -> Self {
|
||||
Self(id.into())
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PluginId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for PluginId {
|
||||
fn from(s: String) -> Self {
|
||||
Self(s)
|
||||
}
|
||||
fn from(s: String) -> Self {
|
||||
Self(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for PluginId {
|
||||
fn from(s: &str) -> Self {
|
||||
Self(s.to_string())
|
||||
}
|
||||
fn from(s: &str) -> Self {
|
||||
Self(s.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Plugin lifecycle state
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PluginState {
|
||||
/// Plugin is being loaded
|
||||
Loading,
|
||||
/// Plugin is being loaded
|
||||
Loading,
|
||||
|
||||
/// Plugin is initialized and ready
|
||||
Ready,
|
||||
/// Plugin is initialized and ready
|
||||
Ready,
|
||||
|
||||
/// Plugin is running
|
||||
Running,
|
||||
/// Plugin is running
|
||||
Running,
|
||||
|
||||
/// Plugin encountered an error
|
||||
Error,
|
||||
/// Plugin encountered an error
|
||||
Error,
|
||||
|
||||
/// Plugin is being shut down
|
||||
ShuttingDown,
|
||||
/// Plugin is being shut down
|
||||
ShuttingDown,
|
||||
|
||||
/// Plugin is stopped
|
||||
Stopped,
|
||||
/// Plugin is stopped
|
||||
Stopped,
|
||||
}
|
||||
|
||||
impl fmt::Display for PluginState {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Loading => write!(f, "loading"),
|
||||
Self::Ready => write!(f, "ready"),
|
||||
Self::Running => write!(f, "running"),
|
||||
Self::Error => write!(f, "error"),
|
||||
Self::ShuttingDown => write!(f, "shutting_down"),
|
||||
Self::Stopped => write!(f, "stopped"),
|
||||
}
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Loading => write!(f, "loading"),
|
||||
Self::Ready => write!(f, "ready"),
|
||||
Self::Running => write!(f, "running"),
|
||||
Self::Error => write!(f, "error"),
|
||||
Self::ShuttingDown => write!(f, "shutting_down"),
|
||||
Self::Stopped => write!(f, "stopped"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Plugin installation status
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PluginStatus {
|
||||
pub id: PluginId,
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub state: PluginState,
|
||||
pub enabled: bool,
|
||||
pub error_message: Option<String>,
|
||||
pub id: PluginId,
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub state: PluginState,
|
||||
pub enabled: bool,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
/// Version information
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Version {
|
||||
pub major: u32,
|
||||
pub minor: u32,
|
||||
pub patch: u32,
|
||||
pub major: u32,
|
||||
pub minor: u32,
|
||||
pub patch: u32,
|
||||
}
|
||||
|
||||
impl Version {
|
||||
pub fn new(major: u32, minor: u32, patch: u32) -> Self {
|
||||
Self {
|
||||
major,
|
||||
minor,
|
||||
patch,
|
||||
}
|
||||
pub fn new(major: u32, minor: u32, patch: u32) -> Self {
|
||||
Self {
|
||||
major,
|
||||
minor,
|
||||
patch,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse version from string (e.g., "1.2.3")
|
||||
pub fn parse(s: &str) -> Option<Self> {
|
||||
let parts: Vec<&str> = s.split('.').collect();
|
||||
if parts.len() != 3 {
|
||||
return None;
|
||||
}
|
||||
|
||||
/// Parse version from string (e.g., "1.2.3")
|
||||
pub fn parse(s: &str) -> Option<Self> {
|
||||
let parts: Vec<&str> = s.split('.').collect();
|
||||
if parts.len() != 3 {
|
||||
return None;
|
||||
}
|
||||
Some(Self {
|
||||
major: parts[0].parse().ok()?,
|
||||
minor: parts[1].parse().ok()?,
|
||||
patch: parts[2].parse().ok()?,
|
||||
})
|
||||
}
|
||||
|
||||
Some(Self {
|
||||
major: parts[0].parse().ok()?,
|
||||
minor: parts[1].parse().ok()?,
|
||||
patch: parts[2].parse().ok()?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Check if this version is compatible with another version
|
||||
/// Compatible if major version matches and minor version is >= required
|
||||
pub fn is_compatible_with(&self, required: &Version) -> bool {
|
||||
self.major == required.major && self.minor >= required.minor
|
||||
}
|
||||
/// Check if this version is compatible with another version
|
||||
/// Compatible if major version matches and minor version is >= required
|
||||
pub fn is_compatible_with(&self, required: &Version) -> bool {
|
||||
self.major == required.major && self.minor >= required.minor
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Version {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
|
||||
}
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_version_parse() {
|
||||
let v = Version::parse("1.2.3").unwrap();
|
||||
assert_eq!(v.major, 1);
|
||||
assert_eq!(v.minor, 2);
|
||||
assert_eq!(v.patch, 3);
|
||||
}
|
||||
#[test]
|
||||
fn test_version_parse() {
|
||||
let v = Version::parse("1.2.3").unwrap();
|
||||
assert_eq!(v.major, 1);
|
||||
assert_eq!(v.minor, 2);
|
||||
assert_eq!(v.patch, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_version_compatibility() {
|
||||
let v1 = Version::new(1, 2, 0);
|
||||
let v2 = Version::new(1, 1, 0);
|
||||
let v3 = Version::new(2, 0, 0);
|
||||
#[test]
|
||||
fn test_version_compatibility() {
|
||||
let v1 = Version::new(1, 2, 0);
|
||||
let v2 = Version::new(1, 1, 0);
|
||||
let v3 = Version::new(2, 0, 0);
|
||||
|
||||
assert!(v1.is_compatible_with(&v2)); // 1.2 >= 1.1
|
||||
assert!(!v2.is_compatible_with(&v1)); // 1.1 < 1.2
|
||||
assert!(!v1.is_compatible_with(&v3)); // Different major version
|
||||
}
|
||||
assert!(v1.is_compatible_with(&v2)); // 1.2 >= 1.1
|
||||
assert!(!v2.is_compatible_with(&v1)); // 1.1 < 1.2
|
||||
assert!(!v1.is_compatible_with(&v3)); // Different major version
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_version_display() {
|
||||
let v = Version::new(1, 2, 3);
|
||||
assert_eq!(v.to_string(), "1.2.3");
|
||||
}
|
||||
#[test]
|
||||
fn test_version_display() {
|
||||
let v = Version::new(1, 2, 3);
|
||||
assert_eq!(v.to_string(), "1.2.3");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,186 +1,196 @@
|
|||
//! WASM bridge types and helpers for plugin communication
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Memory allocation info for passing data between host and plugin
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WasmMemoryAlloc {
|
||||
/// Pointer to allocated memory
|
||||
pub ptr: u32,
|
||||
/// Pointer to allocated memory
|
||||
pub ptr: u32,
|
||||
|
||||
/// Size of allocation in bytes
|
||||
pub len: u32,
|
||||
/// Size of allocation in bytes
|
||||
pub len: u32,
|
||||
}
|
||||
|
||||
/// Request from host to plugin
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HostRequest {
|
||||
/// Request ID for matching with response
|
||||
pub request_id: String,
|
||||
/// Request ID for matching with response
|
||||
pub request_id: String,
|
||||
|
||||
/// Method name being called
|
||||
pub method: String,
|
||||
/// Method name being called
|
||||
pub method: String,
|
||||
|
||||
/// Serialized parameters
|
||||
pub params: Vec<u8>,
|
||||
/// Serialized parameters
|
||||
pub params: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Response from plugin to host
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PluginResponse {
|
||||
/// Request ID this response corresponds to
|
||||
pub request_id: String,
|
||||
/// Request ID this response corresponds to
|
||||
pub request_id: String,
|
||||
|
||||
/// Success or error
|
||||
pub result: WasmResult<Vec<u8>>,
|
||||
/// Success or error
|
||||
pub result: WasmResult<Vec<u8>>,
|
||||
}
|
||||
|
||||
/// Result type for WASM operations
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum WasmResult<T> {
|
||||
Ok(T),
|
||||
Err(String),
|
||||
Ok(T),
|
||||
Err(String),
|
||||
}
|
||||
|
||||
impl<T> From<Result<T, String>> for WasmResult<T> {
|
||||
fn from(r: Result<T, String>) -> Self {
|
||||
match r {
|
||||
Ok(v) => WasmResult::Ok(v),
|
||||
Err(e) => WasmResult::Err(e),
|
||||
}
|
||||
fn from(r: Result<T, String>) -> Self {
|
||||
match r {
|
||||
Ok(v) => WasmResult::Ok(v),
|
||||
Err(e) => WasmResult::Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Host functions available to plugins
|
||||
pub mod host_functions {
|
||||
/// Log a message from plugin
|
||||
pub const LOG: &str = "host_log";
|
||||
/// Log a message from plugin
|
||||
pub const LOG: &str = "host_log";
|
||||
|
||||
/// Read a file (if permitted)
|
||||
pub const READ_FILE: &str = "host_read_file";
|
||||
/// Read a file (if permitted)
|
||||
pub const READ_FILE: &str = "host_read_file";
|
||||
|
||||
/// Write a file (if permitted)
|
||||
pub const WRITE_FILE: &str = "host_write_file";
|
||||
/// Write a file (if permitted)
|
||||
pub const WRITE_FILE: &str = "host_write_file";
|
||||
|
||||
/// Make an HTTP request (if permitted)
|
||||
pub const HTTP_REQUEST: &str = "host_http_request";
|
||||
/// Make an HTTP request (if permitted)
|
||||
pub const HTTP_REQUEST: &str = "host_http_request";
|
||||
|
||||
/// Get configuration value
|
||||
pub const GET_CONFIG: &str = "host_get_config";
|
||||
/// Get configuration value
|
||||
pub const GET_CONFIG: &str = "host_get_config";
|
||||
|
||||
/// Emit an event
|
||||
pub const EMIT_EVENT: &str = "host_emit_event";
|
||||
/// Emit an event
|
||||
pub const EMIT_EVENT: &str = "host_emit_event";
|
||||
}
|
||||
|
||||
/// Log level for plugin logging
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
pub enum LogLevel {
|
||||
Trace,
|
||||
Debug,
|
||||
Info,
|
||||
Warn,
|
||||
Error,
|
||||
Trace,
|
||||
Debug,
|
||||
Info,
|
||||
Warn,
|
||||
Error,
|
||||
}
|
||||
|
||||
/// Log message from plugin
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LogMessage {
|
||||
pub level: LogLevel,
|
||||
pub target: String,
|
||||
pub message: String,
|
||||
pub fields: HashMap<String, String>,
|
||||
pub level: LogLevel,
|
||||
pub target: String,
|
||||
pub message: String,
|
||||
pub fields: HashMap<String, String>,
|
||||
}
|
||||
|
||||
/// HTTP request parameters
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HttpRequest {
|
||||
pub method: String,
|
||||
pub url: String,
|
||||
pub headers: HashMap<String, String>,
|
||||
pub body: Option<Vec<u8>>,
|
||||
pub method: String,
|
||||
pub url: String,
|
||||
pub headers: HashMap<String, String>,
|
||||
pub body: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
/// HTTP response
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HttpResponse {
|
||||
pub status: u16,
|
||||
pub headers: HashMap<String, String>,
|
||||
pub body: Vec<u8>,
|
||||
pub status: u16,
|
||||
pub headers: HashMap<String, String>,
|
||||
pub body: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Helper functions for serializing/deserializing data across WASM boundary
|
||||
pub mod helpers {
|
||||
use super::*;
|
||||
use super::*;
|
||||
|
||||
/// Serialize a value to bytes for passing to WASM
|
||||
pub fn serialize<T: Serialize>(value: &T) -> Result<Vec<u8>, String> {
|
||||
serde_json::to_vec(value).map_err(|e| format!("Serialization error: {}", e))
|
||||
}
|
||||
/// Serialize a value to bytes for passing to WASM
|
||||
pub fn serialize<T: Serialize>(value: &T) -> Result<Vec<u8>, String> {
|
||||
serde_json::to_vec(value).map_err(|e| format!("Serialization error: {}", e))
|
||||
}
|
||||
|
||||
/// Deserialize bytes from WASM to a value
|
||||
pub fn deserialize<T: for<'de> Deserialize<'de>>(bytes: &[u8]) -> Result<T, String> {
|
||||
serde_json::from_slice(bytes).map_err(|e| format!("Deserialization error: {}", e))
|
||||
}
|
||||
/// Deserialize bytes from WASM to a value
|
||||
pub fn deserialize<T: for<'de> Deserialize<'de>>(
|
||||
bytes: &[u8],
|
||||
) -> Result<T, String> {
|
||||
serde_json::from_slice(bytes)
|
||||
.map_err(|e| format!("Deserialization error: {}", e))
|
||||
}
|
||||
|
||||
/// Create a success response
|
||||
pub fn ok_response<T: Serialize>(request_id: String, value: &T) -> Result<Vec<u8>, String> {
|
||||
let result = WasmResult::Ok(serialize(value)?);
|
||||
let response = PluginResponse { request_id, result };
|
||||
serialize(&response)
|
||||
}
|
||||
/// Create a success response
|
||||
pub fn ok_response<T: Serialize>(
|
||||
request_id: String,
|
||||
value: &T,
|
||||
) -> Result<Vec<u8>, String> {
|
||||
let result = WasmResult::Ok(serialize(value)?);
|
||||
let response = PluginResponse { request_id, result };
|
||||
serialize(&response)
|
||||
}
|
||||
|
||||
/// Create an error response
|
||||
pub fn error_response(request_id: String, error: String) -> Result<Vec<u8>, String> {
|
||||
let result = WasmResult::<Vec<u8>>::Err(error);
|
||||
let response = PluginResponse { request_id, result };
|
||||
serialize(&response)
|
||||
}
|
||||
/// Create an error response
|
||||
pub fn error_response(
|
||||
request_id: String,
|
||||
error: String,
|
||||
) -> Result<Vec<u8>, String> {
|
||||
let result = WasmResult::<Vec<u8>>::Err(error);
|
||||
let response = PluginResponse { request_id, result };
|
||||
serialize(&response)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::helpers::*;
|
||||
use super::*;
|
||||
use super::{helpers::*, *};
|
||||
|
||||
#[test]
|
||||
fn test_serialize_deserialize() {
|
||||
let data = vec![1u8, 2, 3, 4];
|
||||
let bytes = serialize(&data).unwrap();
|
||||
let recovered: Vec<u8> = deserialize(&bytes).unwrap();
|
||||
assert_eq!(data, recovered);
|
||||
#[test]
|
||||
fn test_serialize_deserialize() {
|
||||
let data = vec![1u8, 2, 3, 4];
|
||||
let bytes = serialize(&data).unwrap();
|
||||
let recovered: Vec<u8> = deserialize(&bytes).unwrap();
|
||||
assert_eq!(data, recovered);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ok_response() {
|
||||
let request_id = "test-123".to_string();
|
||||
let value = "success";
|
||||
let response_bytes = ok_response(request_id.clone(), &value).unwrap();
|
||||
|
||||
let response: PluginResponse = deserialize(&response_bytes).unwrap();
|
||||
assert_eq!(response.request_id, request_id);
|
||||
|
||||
match response.result {
|
||||
WasmResult::Ok(data) => {
|
||||
let recovered: String = deserialize(&data).unwrap();
|
||||
assert_eq!(recovered, value);
|
||||
},
|
||||
WasmResult::Err(_) => panic!("Expected Ok result"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ok_response() {
|
||||
let request_id = "test-123".to_string();
|
||||
let value = "success";
|
||||
let response_bytes = ok_response(request_id.clone(), &value).unwrap();
|
||||
#[test]
|
||||
fn test_error_response() {
|
||||
let request_id = "test-456".to_string();
|
||||
let error_msg = "Something went wrong";
|
||||
let response_bytes =
|
||||
error_response(request_id.clone(), error_msg.to_string()).unwrap();
|
||||
|
||||
let response: PluginResponse = deserialize(&response_bytes).unwrap();
|
||||
assert_eq!(response.request_id, request_id);
|
||||
let response: PluginResponse = deserialize(&response_bytes).unwrap();
|
||||
assert_eq!(response.request_id, request_id);
|
||||
|
||||
match response.result {
|
||||
WasmResult::Ok(data) => {
|
||||
let recovered: String = deserialize(&data).unwrap();
|
||||
assert_eq!(recovered, value);
|
||||
}
|
||||
WasmResult::Err(_) => panic!("Expected Ok result"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_response() {
|
||||
let request_id = "test-456".to_string();
|
||||
let error_msg = "Something went wrong";
|
||||
let response_bytes = error_response(request_id.clone(), error_msg.to_string()).unwrap();
|
||||
|
||||
let response: PluginResponse = deserialize(&response_bytes).unwrap();
|
||||
assert_eq!(response.request_id, request_id);
|
||||
|
||||
match response.result {
|
||||
WasmResult::Err(msg) => assert_eq!(msg, error_msg),
|
||||
WasmResult::Ok(_) => panic!("Expected Err result"),
|
||||
}
|
||||
match response.result {
|
||||
WasmResult::Err(msg) => assert_eq!(msg, error_msg),
|
||||
WasmResult::Ok(_) => panic!("Expected Err result"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,67 +1,69 @@
|
|||
use pinakes_plugin_api::PluginManifest;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use pinakes_plugin_api::PluginManifest;
|
||||
|
||||
#[test]
|
||||
fn test_markdown_metadata_manifest() {
|
||||
let manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("examples/plugins/markdown-metadata/plugin.toml");
|
||||
let manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("examples/plugins/markdown-metadata/plugin.toml");
|
||||
|
||||
let manifest = PluginManifest::from_file(&manifest_path)
|
||||
.expect("Failed to parse markdown-metadata plugin.toml");
|
||||
let manifest = PluginManifest::from_file(&manifest_path)
|
||||
.expect("Failed to parse markdown-metadata plugin.toml");
|
||||
|
||||
assert_eq!(manifest.plugin.name, "markdown-metadata");
|
||||
assert_eq!(manifest.plugin.version, "1.0.0");
|
||||
assert_eq!(manifest.plugin.api_version, "1.0");
|
||||
assert_eq!(manifest.plugin.kind, vec!["metadata_extractor"]);
|
||||
assert_eq!(manifest.plugin.binary.wasm, "markdown_metadata.wasm");
|
||||
assert_eq!(manifest.plugin.name, "markdown-metadata");
|
||||
assert_eq!(manifest.plugin.version, "1.0.0");
|
||||
assert_eq!(manifest.plugin.api_version, "1.0");
|
||||
assert_eq!(manifest.plugin.kind, vec!["metadata_extractor"]);
|
||||
assert_eq!(manifest.plugin.binary.wasm, "markdown_metadata.wasm");
|
||||
|
||||
// Validate capabilities
|
||||
let caps = manifest.to_capabilities();
|
||||
assert_eq!(caps.filesystem.read.len(), 0);
|
||||
assert_eq!(caps.filesystem.write.len(), 0);
|
||||
assert!(!caps.network.enabled);
|
||||
// Validate capabilities
|
||||
let caps = manifest.to_capabilities();
|
||||
assert_eq!(caps.filesystem.read.len(), 0);
|
||||
assert_eq!(caps.filesystem.write.len(), 0);
|
||||
assert!(!caps.network.enabled);
|
||||
|
||||
// Validate config
|
||||
assert!(manifest.config.contains_key("extract_tags"));
|
||||
assert!(manifest.config.contains_key("parse_yaml"));
|
||||
assert!(manifest.config.contains_key("max_file_size"));
|
||||
// Validate config
|
||||
assert!(manifest.config.contains_key("extract_tags"));
|
||||
assert!(manifest.config.contains_key("parse_yaml"));
|
||||
assert!(manifest.config.contains_key("max_file_size"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_heif_support_manifest() {
|
||||
let manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("examples/plugins/heif-support/plugin.toml");
|
||||
let manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("examples/plugins/heif-support/plugin.toml");
|
||||
|
||||
let manifest = PluginManifest::from_file(&manifest_path)
|
||||
.expect("Failed to parse heif-support plugin.toml");
|
||||
let manifest = PluginManifest::from_file(&manifest_path)
|
||||
.expect("Failed to parse heif-support plugin.toml");
|
||||
|
||||
assert_eq!(manifest.plugin.name, "heif-support");
|
||||
assert_eq!(manifest.plugin.version, "1.0.0");
|
||||
assert_eq!(manifest.plugin.api_version, "1.0");
|
||||
assert_eq!(
|
||||
manifest.plugin.kind,
|
||||
vec!["media_type", "metadata_extractor", "thumbnail_generator"]
|
||||
);
|
||||
assert_eq!(manifest.plugin.binary.wasm, "heif_support.wasm");
|
||||
assert_eq!(manifest.plugin.name, "heif-support");
|
||||
assert_eq!(manifest.plugin.version, "1.0.0");
|
||||
assert_eq!(manifest.plugin.api_version, "1.0");
|
||||
assert_eq!(manifest.plugin.kind, vec![
|
||||
"media_type",
|
||||
"metadata_extractor",
|
||||
"thumbnail_generator"
|
||||
]);
|
||||
assert_eq!(manifest.plugin.binary.wasm, "heif_support.wasm");
|
||||
|
||||
// Validate capabilities
|
||||
let caps = manifest.to_capabilities();
|
||||
assert_eq!(caps.filesystem.read.len(), 1);
|
||||
assert_eq!(caps.filesystem.write.len(), 1);
|
||||
assert!(!caps.network.enabled);
|
||||
assert_eq!(caps.max_memory_bytes, Some(256 * 1024 * 1024)); // 256MB
|
||||
assert_eq!(caps.max_cpu_time_ms, Some(30 * 1000)); // 30 seconds
|
||||
// Validate capabilities
|
||||
let caps = manifest.to_capabilities();
|
||||
assert_eq!(caps.filesystem.read.len(), 1);
|
||||
assert_eq!(caps.filesystem.write.len(), 1);
|
||||
assert!(!caps.network.enabled);
|
||||
assert_eq!(caps.max_memory_bytes, Some(256 * 1024 * 1024)); // 256MB
|
||||
assert_eq!(caps.max_cpu_time_ms, Some(30 * 1000)); // 30 seconds
|
||||
|
||||
// Validate config
|
||||
assert!(manifest.config.contains_key("extract_exif"));
|
||||
assert!(manifest.config.contains_key("generate_thumbnails"));
|
||||
assert!(manifest.config.contains_key("thumbnail_quality"));
|
||||
// Validate config
|
||||
assert!(manifest.config.contains_key("extract_exif"));
|
||||
assert!(manifest.config.contains_key("generate_thumbnails"));
|
||||
assert!(manifest.config.contains_key("thumbnail_quality"));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue