pinakes-plugin-api: update manifest, types, and wasm interface
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: Ic574cc8d1d24967a8c997a3092037e526a6a6964
This commit is contained in:
parent
eb6c0a3577
commit
c8425a4c34
4 changed files with 53 additions and 17 deletions
|
|
@ -34,7 +34,7 @@ pub struct PluginInfo {
|
|||
pub homepage: Option<String>,
|
||||
pub license: Option<String>,
|
||||
|
||||
/// Plugin kind(s) - e.g., ["media_type", "metadata_extractor"]
|
||||
/// Plugin kind(s) - e.g., `media_type`, `metadata_extractor`
|
||||
pub kind: Vec<String>,
|
||||
|
||||
/// Binary configuration
|
||||
|
|
@ -95,6 +95,12 @@ pub enum ManifestError {
|
|||
|
||||
impl PluginManifest {
|
||||
/// Load and parse a plugin manifest from a TOML file
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`ManifestError::IoError`] if the file cannot be read,
|
||||
/// [`ManifestError::ParseError`] if the TOML is invalid, or
|
||||
/// [`ManifestError::ValidationError`] if the manifest fails validation.
|
||||
pub fn from_file(path: &Path) -> Result<Self, ManifestError> {
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
let manifest: Self = toml::from_str(&content)?;
|
||||
|
|
@ -103,6 +109,11 @@ impl PluginManifest {
|
|||
}
|
||||
|
||||
/// Parse a manifest from TOML string
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`ManifestError::ParseError`] if the TOML is invalid, or
|
||||
/// [`ManifestError::ValidationError`] if the manifest fails validation.
|
||||
pub fn parse_str(content: &str) -> Result<Self, ManifestError> {
|
||||
let manifest: Self = toml::from_str(content)?;
|
||||
manifest.validate()?;
|
||||
|
|
@ -110,6 +121,11 @@ impl PluginManifest {
|
|||
}
|
||||
|
||||
/// Validate the manifest
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`ManifestError::ValidationError`] if any required field is empty
|
||||
/// or otherwise invalid.
|
||||
pub fn validate(&self) -> Result<(), ManifestError> {
|
||||
// Check API version format
|
||||
if self.plugin.api_version.is_empty() {
|
||||
|
|
@ -163,6 +179,7 @@ impl PluginManifest {
|
|||
}
|
||||
|
||||
/// Convert manifest capabilities to API capabilities
|
||||
#[must_use]
|
||||
pub fn to_capabilities(&self) -> Capabilities {
|
||||
Capabilities {
|
||||
filesystem: FilesystemCapability {
|
||||
|
|
@ -171,14 +188,14 @@ impl PluginManifest {
|
|||
.filesystem
|
||||
.read
|
||||
.iter()
|
||||
.map(|s| s.into())
|
||||
.map(std::convert::Into::into)
|
||||
.collect(),
|
||||
write: self
|
||||
.capabilities
|
||||
.filesystem
|
||||
.write
|
||||
.iter()
|
||||
.map(|s| s.into())
|
||||
.map(std::convert::Into::into)
|
||||
.collect(),
|
||||
},
|
||||
network: NetworkCapability {
|
||||
|
|
@ -201,6 +218,7 @@ impl PluginManifest {
|
|||
}
|
||||
|
||||
/// Get plugin ID (derived from name and version)
|
||||
#[must_use]
|
||||
pub fn plugin_id(&self) -> String {
|
||||
format!("{}@{}", self.plugin.name, self.plugin.version)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ impl PluginId {
|
|||
Self(id.into())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
|
@ -91,7 +92,8 @@ pub struct Version {
|
|||
}
|
||||
|
||||
impl Version {
|
||||
pub fn new(major: u32, minor: u32, patch: u32) -> Self {
|
||||
#[must_use]
|
||||
pub const fn new(major: u32, minor: u32, patch: u32) -> Self {
|
||||
Self {
|
||||
major,
|
||||
minor,
|
||||
|
|
@ -100,6 +102,7 @@ impl Version {
|
|||
}
|
||||
|
||||
/// Parse version from string (e.g., "1.2.3")
|
||||
#[must_use]
|
||||
pub fn parse(s: &str) -> Option<Self> {
|
||||
let parts: Vec<&str> = s.split('.').collect();
|
||||
if parts.len() != 3 {
|
||||
|
|
@ -115,7 +118,8 @@ impl Version {
|
|||
|
||||
/// 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 {
|
||||
#[must_use]
|
||||
pub const fn is_compatible_with(&self, required: &Self) -> bool {
|
||||
self.major == required.major && self.minor >= required.minor
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,8 +47,8 @@ pub enum WasmResult<T> {
|
|||
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),
|
||||
Ok(v) => Self::Ok(v),
|
||||
Err(e) => Self::Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -112,22 +112,35 @@ pub struct HttpResponse {
|
|||
|
||||
/// Helper functions for serializing/deserializing data across WASM boundary
|
||||
pub mod helpers {
|
||||
use super::*;
|
||||
use super::{Deserialize, PluginResponse, Serialize, WasmResult};
|
||||
|
||||
/// Serialize a value to bytes for passing to WASM
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error string if the value cannot be serialized to JSON.
|
||||
pub fn serialize<T: Serialize>(value: &T) -> Result<Vec<u8>, String> {
|
||||
serde_json::to_vec(value).map_err(|e| format!("Serialization error: {}", e))
|
||||
serde_json::to_vec(value).map_err(|e| format!("Serialization error: {e}"))
|
||||
}
|
||||
|
||||
/// Deserialize bytes from WASM to a value
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error string if the bytes cannot be deserialized as `T`.
|
||||
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))
|
||||
.map_err(|e| format!("Deserialization error: {e}"))
|
||||
}
|
||||
|
||||
/// Create a success response
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error string if `value` or the response envelope cannot be
|
||||
/// serialized.
|
||||
pub fn ok_response<T: Serialize>(
|
||||
request_id: String,
|
||||
value: &T,
|
||||
|
|
@ -138,6 +151,10 @@ pub mod helpers {
|
|||
}
|
||||
|
||||
/// Create an error response
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error string if the response envelope cannot be serialized.
|
||||
pub fn error_response(
|
||||
request_id: String,
|
||||
error: String,
|
||||
|
|
|
|||
|
|
@ -106,10 +106,7 @@ async fn test_plugin_context_creation() {
|
|||
|
||||
assert_eq!(context.data_dir, PathBuf::from("/data/test-plugin"));
|
||||
assert_eq!(context.cache_dir, PathBuf::from("/cache/test-plugin"));
|
||||
assert_eq!(
|
||||
context.config.get("enabled").unwrap(),
|
||||
&serde_json::json!(true)
|
||||
);
|
||||
assert_eq!(&context.config["enabled"], &serde_json::json!(true));
|
||||
assert!(context.capabilities.network.enabled);
|
||||
assert_eq!(
|
||||
context.capabilities.max_memory_bytes,
|
||||
|
|
@ -178,7 +175,7 @@ async fn test_extracted_metadata_structure() {
|
|||
assert_eq!(metadata.width, Some(1920));
|
||||
assert_eq!(metadata.height, Some(1080));
|
||||
assert_eq!(metadata.tags.len(), 2);
|
||||
assert_eq!(metadata.custom_fields.get("color_space").unwrap(), "sRGB");
|
||||
assert_eq!(&metadata.custom_fields["color_space"], "sRGB");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
@ -199,7 +196,7 @@ async fn test_search_query_serialization() {
|
|||
assert_eq!(deserialized.query_text, "nature landscape");
|
||||
assert_eq!(deserialized.limit, 50);
|
||||
assert_eq!(deserialized.offset, 0);
|
||||
assert_eq!(deserialized.filters.get("type").unwrap(), "image");
|
||||
assert_eq!(&deserialized.filters["type"], "image");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
@ -439,7 +436,7 @@ async fn test_plugin_error_variants() {
|
|||
for error in errors {
|
||||
let serialized = serde_json::to_string(&error).unwrap();
|
||||
let deserialized: PluginError = serde_json::from_str(&serialized).unwrap();
|
||||
assert_eq!(format!("{}", error), format!("{}", deserialized));
|
||||
assert_eq!(format!("{error}"), format!("{}", deserialized));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue