use crate::error::{Result, SklearsError};
use crate::plugin::discovery_marketplace::{PluginDiscoveryService, SearchQuery};
use crate::plugin::validation::PluginManifest;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
#[derive(Debug)]
pub struct ConcretePluginMarketplace {
pub discovery: PluginDiscoveryService,
pub installer: PluginInstaller,
pub ratings: RatingSystem,
pub updater: UpdateManager,
pub config: MarketplaceConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MarketplaceConfig {
pub plugin_dir: PathBuf,
pub auto_update: bool,
pub update_check_interval: u64,
pub enable_ratings: bool,
pub require_verified: bool,
pub max_concurrent_downloads: usize,
}
impl Default for MarketplaceConfig {
fn default() -> Self {
Self {
plugin_dir: std::env::temp_dir().join("sklears_plugins"),
auto_update: false,
update_check_interval: 86400, enable_ratings: true,
require_verified: false,
max_concurrent_downloads: 3,
}
}
}
#[derive(Debug)]
pub struct PluginInstaller {
pub install_dir: PathBuf,
pub installed: HashMap<String, InstalledPlugin>,
pub download_cache: PathBuf,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstalledPlugin {
pub id: String,
pub name: String,
pub version: String,
pub installed_at: SystemTime,
pub install_path: PathBuf,
pub manifest: PluginManifest,
}
#[derive(Debug, Clone)]
pub struct RatingSystem {
pub ratings: HashMap<String, PluginRating>,
pub reviews: HashMap<String, Vec<UserReview>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginRating {
pub plugin_id: String,
pub average_rating: f64,
pub total_ratings: usize,
pub rating_distribution: [usize; 5], pub total_downloads: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserReview {
pub id: String,
pub plugin_id: String,
pub user_id: String,
pub rating: u8,
pub title: String,
pub content: String,
pub helpful_votes: usize,
pub posted_at: SystemTime,
pub verified_install: bool,
}
#[derive(Debug)]
pub struct UpdateManager {
pub last_check: Option<SystemTime>,
pub available_updates: Vec<PluginUpdate>,
pub config: UpdateConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateConfig {
pub auto_update: bool,
pub include_prerelease: bool,
pub strategy: UpdateStrategy,
}
impl Default for UpdateConfig {
fn default() -> Self {
Self {
auto_update: false,
include_prerelease: false,
strategy: UpdateStrategy::Manual,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum UpdateStrategy {
Manual,
Notify,
AutoDownload,
AutoInstall,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginUpdate {
pub plugin_id: String,
pub current_version: String,
pub new_version: String,
pub description: String,
pub size_bytes: usize,
pub breaking_change: bool,
pub release_notes: String,
}
impl ConcretePluginMarketplace {
pub fn new() -> Self {
let config = MarketplaceConfig::default();
Self::with_config(config)
}
pub fn with_config(config: MarketplaceConfig) -> Self {
let install_dir = config.plugin_dir.clone();
Self {
discovery: PluginDiscoveryService::new(),
installer: PluginInstaller {
install_dir: install_dir.clone(),
installed: HashMap::new(),
download_cache: install_dir.join("cache"),
},
ratings: RatingSystem {
ratings: HashMap::new(),
reviews: HashMap::new(),
},
updater: UpdateManager {
last_check: None,
available_updates: Vec::new(),
config: UpdateConfig::default(),
},
config,
}
}
pub async fn search(&self, query: &str) -> Result<Vec<MarketplacePlugin>> {
let search_query = SearchQuery {
text: query.to_string(),
category: None,
capabilities: vec![],
limit: Some(50),
min_rating: None,
};
let results = self.discovery.search(&search_query).await?;
let mut plugins = Vec::new();
for result in results {
let rating = self.ratings.ratings.get(&result.plugin_id);
plugins.push(MarketplacePlugin {
id: result.plugin_id.clone(),
name: result.plugin_id.clone(), description: result.description,
version: "1.0.0".to_string(), author: "Unknown".to_string(), rating: rating.map(|r| r.average_rating),
downloads: result.download_count as usize,
verified: false, tags: vec![], });
}
Ok(plugins)
}
pub async fn get_featured(&self) -> Result<Vec<MarketplacePlugin>> {
let mut featured: Vec<_> = self
.ratings
.ratings
.values()
.filter(|r| r.average_rating >= 4.5 && r.total_downloads >= 1000)
.map(|r| r.plugin_id.clone())
.collect();
featured.sort_by_key(|id| {
self.ratings
.ratings
.get(id)
.map(|r| r.total_downloads)
.unwrap_or(0)
});
featured.reverse();
featured.truncate(10);
let mut result = Vec::new();
for plugin_id in featured {
if let Some(rating) = self.ratings.ratings.get(&plugin_id) {
result.push(MarketplacePlugin {
id: plugin_id.clone(),
name: plugin_id.clone(), description: "Featured plugin".to_string(),
version: "1.0.0".to_string(),
author: "Unknown".to_string(),
rating: Some(rating.average_rating),
downloads: rating.total_downloads,
verified: false,
tags: vec![],
});
}
}
Ok(result)
}
pub async fn install(
&mut self,
plugin_id: &str,
version: Option<&str>,
) -> Result<InstalledPlugin> {
if self.installer.installed.contains_key(plugin_id) {
return Err(SklearsError::InvalidOperation(format!(
"Plugin {} is already installed",
plugin_id
)));
}
let download_path = self.download_plugin(plugin_id, version).await?;
let manifest = self.load_manifest(&download_path)?;
self.validate_manifest_metadata(&manifest)?;
let install_path = self.installer.install_dir.join(plugin_id);
std::fs::create_dir_all(&install_path).map_err(|e| {
SklearsError::InvalidOperation(format!("Failed to create install directory: {}", e))
})?;
let installed = InstalledPlugin {
id: plugin_id.to_string(),
name: manifest.metadata.name.clone(),
version: manifest.metadata.version.clone(),
installed_at: SystemTime::now(),
install_path: install_path.clone(),
manifest,
};
self.installer
.installed
.insert(plugin_id.to_string(), installed.clone());
Ok(installed)
}
pub fn uninstall(&mut self, plugin_id: &str) -> Result<()> {
let installed = self.installer.installed.remove(plugin_id).ok_or_else(|| {
SklearsError::InvalidOperation(format!("Plugin {} is not installed", plugin_id))
})?;
if installed.install_path.exists() {
std::fs::remove_dir_all(&installed.install_path).map_err(|e| {
SklearsError::InvalidOperation(format!("Failed to remove plugin files: {}", e))
})?;
}
Ok(())
}
pub async fn check_for_updates(&mut self) -> Result<Vec<PluginUpdate>> {
self.updater.last_check = Some(SystemTime::now());
self.updater.available_updates.clear();
for (plugin_id, installed) in &self.installer.installed {
if let Some(latest) = self.get_latest_version(plugin_id).await? {
if latest != installed.version {
self.updater.available_updates.push(PluginUpdate {
plugin_id: plugin_id.clone(),
current_version: installed.version.clone(),
new_version: latest.clone(),
description: format!("Update {} to version {}", plugin_id, latest),
size_bytes: 1024 * 1024, breaking_change: false,
release_notes: "Bug fixes and improvements".to_string(),
});
}
}
}
Ok(self.updater.available_updates.clone())
}
pub fn rate_plugin(
&mut self,
plugin_id: &str,
rating: u8,
review: Option<UserReview>,
) -> Result<()> {
if !(1..=5).contains(&rating) {
return Err(SklearsError::InvalidOperation(
"Rating must be between 1 and 5".to_string(),
));
}
let plugin_rating = self
.ratings
.ratings
.entry(plugin_id.to_string())
.or_insert_with(|| PluginRating {
plugin_id: plugin_id.to_string(),
average_rating: 0.0,
total_ratings: 0,
rating_distribution: [0; 5],
total_downloads: 0,
});
plugin_rating.rating_distribution[(rating - 1) as usize] += 1;
plugin_rating.total_ratings += 1;
let total: f64 = plugin_rating
.rating_distribution
.iter()
.enumerate()
.map(|(i, &count)| (i + 1) as f64 * count as f64)
.sum();
plugin_rating.average_rating = total / plugin_rating.total_ratings as f64;
if let Some(review) = review {
self.ratings
.reviews
.entry(plugin_id.to_string())
.or_default()
.push(review);
}
Ok(())
}
pub fn get_reviews(&self, plugin_id: &str) -> Vec<&UserReview> {
self.ratings
.reviews
.get(plugin_id)
.map(|reviews| reviews.iter().collect())
.unwrap_or_default()
}
pub fn list_installed(&self) -> Vec<&InstalledPlugin> {
self.installer.installed.values().collect()
}
async fn download_plugin(&self, plugin_id: &str, _version: Option<&str>) -> Result<PathBuf> {
let download_path = self
.installer
.download_cache
.join(format!("{}.tar.gz", plugin_id));
std::fs::create_dir_all(&self.installer.download_cache).map_err(|e| {
SklearsError::InvalidOperation(format!("Failed to create cache directory: {}", e))
})?;
Ok(download_path)
}
fn load_manifest(&self, _path: &Path) -> Result<PluginManifest> {
use crate::plugin::security::PublisherInfo;
use crate::plugin::types_config::PluginMetadata;
use crate::plugin::validation::MarketplaceInfo;
Ok(PluginManifest {
metadata: PluginMetadata::default(),
permissions: vec![],
api_usage: None,
contains_unsafe_code: false,
dependencies: vec![],
code_analysis: None,
signature: None,
content_hash: String::new(),
publisher: PublisherInfo {
name: "test".to_string(),
email: "test@test.com".to_string(),
website: None,
verified: false,
trust_score: 5,
},
marketplace: MarketplaceInfo {
url: "https://marketplace.example.com".to_string(),
downloads: 0,
rating: 0.0,
reviews: 0,
last_updated: chrono::Utc::now().to_rfc3339(),
},
})
}
fn validate_manifest_metadata(&self, manifest: &PluginManifest) -> Result<()> {
let meta = &manifest.metadata;
if meta.name.is_empty() {
return Err(SklearsError::InvalidOperation(
"Plugin manifest: 'name' field is required".to_string(),
));
}
if meta.version.is_empty() {
return Err(SklearsError::InvalidOperation(
"Plugin manifest: 'version' field is required".to_string(),
));
}
if meta.author.is_empty() {
return Err(SklearsError::InvalidOperation(
"Plugin manifest: 'author' field is required".to_string(),
));
}
let is_valid_semver = {
let parts: Vec<&str> = meta.version.split('.').collect();
(2..=3).contains(&parts.len()) && parts.iter().all(|p| p.parse::<u32>().is_ok())
};
if !is_valid_semver {
return Err(SklearsError::InvalidOperation(format!(
"Plugin manifest: 'version' \"{}\" is not a valid semver string (expected X.Y or X.Y.Z)",
meta.version
)));
}
const SKLEARS_SDK_VERSION: (u32, u32, u32) = (0, 1, 0);
if !meta.min_sdk_version.is_empty() {
let parse_version = |v: &str| -> Option<(u32, u32, u32)> {
let parts: Vec<&str> = v.split('.').collect();
if parts.len() < 2 {
return None;
}
let major = parts[0].parse::<u32>().ok()?;
let minor = parts[1].parse::<u32>().ok()?;
let patch = parts
.get(2)
.and_then(|p| p.parse::<u32>().ok())
.unwrap_or(0);
Some((major, minor, patch))
};
match parse_version(&meta.min_sdk_version) {
None => {
return Err(SklearsError::InvalidOperation(format!(
"Plugin manifest: 'min_sdk_version' \"{}\" is not a valid version string",
meta.min_sdk_version
)));
}
Some(min_sdk) if min_sdk > SKLEARS_SDK_VERSION => {
return Err(SklearsError::InvalidOperation(format!(
"Plugin requires SDK {}, but current SDK is {}.{}.{}",
meta.min_sdk_version,
SKLEARS_SDK_VERSION.0,
SKLEARS_SDK_VERSION.1,
SKLEARS_SDK_VERSION.2,
)));
}
Some(_) => { }
}
}
if manifest.content_hash.is_empty() {
log::warn!(
"Plugin '{}' has no content_hash in its manifest; integrity verification \
will not be possible without a full load",
meta.name
);
}
if self.config.require_verified && !manifest.publisher.verified {
return Err(SklearsError::InvalidOperation(format!(
"Plugin '{}' was published by an unverified publisher ('{}'); \
set require_verified = false to allow unverified plugins",
meta.name, manifest.publisher.name
)));
}
Ok(())
}
async fn get_latest_version(&self, _plugin_id: &str) -> Result<Option<String>> {
Ok(Some("2.0.0".to_string()))
}
}
impl Default for ConcretePluginMarketplace {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MarketplacePlugin {
pub id: String,
pub name: String,
pub description: String,
pub version: String,
pub author: String,
pub rating: Option<f64>,
pub downloads: usize,
pub verified: bool,
pub tags: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_marketplace_creation() {
let marketplace = ConcretePluginMarketplace::new();
assert!(marketplace.installer.installed.is_empty());
}
#[test]
fn test_rating_plugin() {
let mut marketplace = ConcretePluginMarketplace::new();
marketplace
.rate_plugin("test_plugin", 5, None)
.expect("rate_plugin should succeed");
marketplace
.rate_plugin("test_plugin", 4, None)
.expect("rate_plugin should succeed");
marketplace
.rate_plugin("test_plugin", 5, None)
.expect("rate_plugin should succeed");
let rating = marketplace
.ratings
.ratings
.get("test_plugin")
.expect("key should exist");
assert_eq!(rating.total_ratings, 3);
assert!((rating.average_rating - 4.67).abs() < 0.01);
}
#[test]
fn test_invalid_rating() {
let mut marketplace = ConcretePluginMarketplace::new();
let result = marketplace.rate_plugin("test_plugin", 0, None);
assert!(result.is_err());
let result = marketplace.rate_plugin("test_plugin", 6, None);
assert!(result.is_err());
}
#[test]
fn test_review_submission() {
let mut marketplace = ConcretePluginMarketplace::new();
let review = UserReview {
id: "review1".to_string(),
plugin_id: "test_plugin".to_string(),
user_id: "user1".to_string(),
rating: 5,
title: "Great plugin!".to_string(),
content: "Works perfectly".to_string(),
helpful_votes: 0,
posted_at: SystemTime::now(),
verified_install: true,
};
marketplace
.rate_plugin("test_plugin", 5, Some(review))
.expect("expected valid value");
let reviews = marketplace.get_reviews("test_plugin");
assert_eq!(reviews.len(), 1);
assert_eq!(reviews[0].title, "Great plugin!");
}
#[test]
fn test_list_installed() {
let marketplace = ConcretePluginMarketplace::new();
let installed = marketplace.list_installed();
assert_eq!(installed.len(), 0);
}
#[test]
fn test_update_strategy() {
let config = UpdateConfig::default();
assert_eq!(config.strategy, UpdateStrategy::Manual);
assert!(!config.auto_update);
}
#[test]
fn test_marketplace_config() {
let config = MarketplaceConfig::default();
assert!(!config.auto_update);
assert_eq!(config.max_concurrent_downloads, 3);
}
#[test]
fn test_rating_distribution() {
let mut marketplace = ConcretePluginMarketplace::new();
marketplace
.rate_plugin("test", 5, None)
.expect("rate_plugin should succeed");
marketplace
.rate_plugin("test", 5, None)
.expect("rate_plugin should succeed");
marketplace
.rate_plugin("test", 4, None)
.expect("rate_plugin should succeed");
marketplace
.rate_plugin("test", 3, None)
.expect("rate_plugin should succeed");
let rating = marketplace
.ratings
.ratings
.get("test")
.expect("key should exist");
assert_eq!(rating.rating_distribution[4], 2); assert_eq!(rating.rating_distribution[3], 1); assert_eq!(rating.rating_distribution[2], 1); }
#[test]
fn test_uninstall_nonexistent() {
let mut marketplace = ConcretePluginMarketplace::new();
let result = marketplace.uninstall("nonexistent");
assert!(result.is_err());
}
#[test]
fn test_installed_plugin_creation() {
use crate::plugin::security::PublisherInfo;
use crate::plugin::types_config::PluginMetadata;
use crate::plugin::validation::MarketplaceInfo;
let plugin = InstalledPlugin {
id: "test".to_string(),
name: "Test Plugin".to_string(),
version: "1.0.0".to_string(),
installed_at: SystemTime::now(),
install_path: std::env::temp_dir().join("test"),
manifest: PluginManifest {
metadata: PluginMetadata::default(),
permissions: vec![],
api_usage: None,
contains_unsafe_code: false,
dependencies: vec![],
code_analysis: None,
signature: None,
content_hash: String::new(),
publisher: PublisherInfo {
name: "test".to_string(),
email: "test@test.com".to_string(),
website: Some("https://test.com".to_string()),
verified: true,
trust_score: 8,
},
marketplace: MarketplaceInfo {
url: "https://marketplace.example.com/test".to_string(),
downloads: 100,
rating: 4.5,
reviews: 10,
last_updated: chrono::Utc::now().to_rfc3339(),
},
},
};
assert_eq!(plugin.id, "test");
assert_eq!(plugin.version, "1.0.0");
}
fn make_valid_manifest() -> PluginManifest {
use crate::plugin::security::PublisherInfo;
use crate::plugin::types_config::PluginMetadata;
use crate::plugin::validation::MarketplaceInfo;
PluginManifest {
metadata: PluginMetadata {
name: "my_plugin".to_string(),
version: "1.0.0".to_string(),
author: "Test Author".to_string(),
min_sdk_version: "0.1.0".to_string(),
..PluginMetadata::default()
},
permissions: vec![],
api_usage: None,
contains_unsafe_code: false,
dependencies: vec![],
code_analysis: None,
signature: None,
content_hash: "abc123".to_string(),
publisher: PublisherInfo {
name: "publisher".to_string(),
email: "pub@test.com".to_string(),
website: None,
verified: true,
trust_score: 9,
},
marketplace: MarketplaceInfo {
url: "https://marketplace.example.com/my_plugin".to_string(),
downloads: 0,
rating: 0.0,
reviews: 0,
last_updated: chrono::Utc::now().to_rfc3339(),
},
}
}
#[test]
fn test_manifest_validation_valid_plugin() {
let marketplace = ConcretePluginMarketplace::new();
let manifest = make_valid_manifest();
assert!(
marketplace.validate_manifest_metadata(&manifest).is_ok(),
"A fully valid manifest should pass validation"
);
}
#[test]
fn test_manifest_validation_empty_name_fails() {
let marketplace = ConcretePluginMarketplace::new();
let mut manifest = make_valid_manifest();
manifest.metadata.name = String::new();
let result = marketplace.validate_manifest_metadata(&manifest);
assert!(result.is_err());
assert!(result
.expect_err("result must be Err")
.to_string()
.contains("name"));
}
#[test]
fn test_manifest_validation_empty_author_fails() {
let marketplace = ConcretePluginMarketplace::new();
let mut manifest = make_valid_manifest();
manifest.metadata.author = String::new();
let result = marketplace.validate_manifest_metadata(&manifest);
assert!(result.is_err());
assert!(result
.expect_err("result must be Err")
.to_string()
.contains("author"));
}
#[test]
fn test_manifest_validation_invalid_semver_fails() {
let marketplace = ConcretePluginMarketplace::new();
let mut manifest = make_valid_manifest();
manifest.metadata.version = "not-a-version".to_string();
let result = marketplace.validate_manifest_metadata(&manifest);
assert!(result.is_err());
assert!(result
.expect_err("result must be Err")
.to_string()
.contains("semver"));
}
#[test]
fn test_manifest_validation_future_sdk_version_fails() {
let marketplace = ConcretePluginMarketplace::new();
let mut manifest = make_valid_manifest();
manifest.metadata.min_sdk_version = "99.0.0".to_string();
let result = marketplace.validate_manifest_metadata(&manifest);
assert!(result.is_err());
assert!(result
.expect_err("result must be Err")
.to_string()
.contains("SDK"));
}
#[test]
fn test_manifest_validation_require_verified_fails_for_unverified() {
use crate::plugin::security::PublisherInfo;
let config = MarketplaceConfig {
require_verified: true,
..Default::default()
};
let marketplace = ConcretePluginMarketplace::with_config(config);
let mut manifest = make_valid_manifest();
manifest.publisher = PublisherInfo {
name: "unknown_pub".to_string(),
email: "x@x.com".to_string(),
website: None,
verified: false,
trust_score: 3,
};
let result = marketplace.validate_manifest_metadata(&manifest);
assert!(result.is_err());
assert!(result
.expect_err("result must be Err")
.to_string()
.contains("unverified"));
}
#[test]
fn test_manifest_validation_require_verified_passes_for_verified() {
let config = MarketplaceConfig {
require_verified: true,
..Default::default()
};
let marketplace = ConcretePluginMarketplace::with_config(config);
let manifest = make_valid_manifest(); assert!(marketplace.validate_manifest_metadata(&manifest).is_ok());
}
}