1use crate::error::{Result, SklearsError};
7use crate::plugin::discovery_marketplace::{PluginDiscoveryService, SearchQuery};
8use crate::plugin::validation::PluginManifest;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::path::{Path, PathBuf};
12use std::time::SystemTime;
13
14#[derive(Debug)]
19pub struct ConcretePluginMarketplace {
20 pub discovery: PluginDiscoveryService,
22 pub installer: PluginInstaller,
24 pub ratings: RatingSystem,
26 pub updater: UpdateManager,
28 pub config: MarketplaceConfig,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct MarketplaceConfig {
35 pub plugin_dir: PathBuf,
37 pub auto_update: bool,
39 pub update_check_interval: u64,
41 pub enable_ratings: bool,
43 pub require_verified: bool,
45 pub max_concurrent_downloads: usize,
47}
48
49impl Default for MarketplaceConfig {
50 fn default() -> Self {
51 Self {
52 plugin_dir: std::env::temp_dir().join("sklears_plugins"),
53 auto_update: false,
54 update_check_interval: 86400, enable_ratings: true,
56 require_verified: false,
57 max_concurrent_downloads: 3,
58 }
59 }
60}
61
62#[derive(Debug)]
64pub struct PluginInstaller {
65 pub install_dir: PathBuf,
67 pub installed: HashMap<String, InstalledPlugin>,
69 pub download_cache: PathBuf,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct InstalledPlugin {
76 pub id: String,
78 pub name: String,
80 pub version: String,
82 pub installed_at: SystemTime,
84 pub install_path: PathBuf,
86 pub manifest: PluginManifest,
88}
89
90#[derive(Debug, Clone)]
92pub struct RatingSystem {
93 pub ratings: HashMap<String, PluginRating>,
95 pub reviews: HashMap<String, Vec<UserReview>>,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct PluginRating {
102 pub plugin_id: String,
104 pub average_rating: f64,
106 pub total_ratings: usize,
108 pub rating_distribution: [usize; 5], pub total_downloads: usize,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct UserReview {
117 pub id: String,
119 pub plugin_id: String,
121 pub user_id: String,
123 pub rating: u8,
125 pub title: String,
127 pub content: String,
129 pub helpful_votes: usize,
131 pub posted_at: SystemTime,
133 pub verified_install: bool,
135}
136
137#[derive(Debug)]
139pub struct UpdateManager {
140 pub last_check: Option<SystemTime>,
142 pub available_updates: Vec<PluginUpdate>,
144 pub config: UpdateConfig,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct UpdateConfig {
151 pub auto_update: bool,
153 pub include_prerelease: bool,
155 pub strategy: UpdateStrategy,
157}
158
159impl Default for UpdateConfig {
160 fn default() -> Self {
161 Self {
162 auto_update: false,
163 include_prerelease: false,
164 strategy: UpdateStrategy::Manual,
165 }
166 }
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
171pub enum UpdateStrategy {
172 Manual,
174 Notify,
176 AutoDownload,
178 AutoInstall,
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct PluginUpdate {
185 pub plugin_id: String,
187 pub current_version: String,
189 pub new_version: String,
191 pub description: String,
193 pub size_bytes: usize,
195 pub breaking_change: bool,
197 pub release_notes: String,
199}
200
201impl ConcretePluginMarketplace {
202 pub fn new() -> Self {
204 let config = MarketplaceConfig::default();
205 Self::with_config(config)
206 }
207
208 pub fn with_config(config: MarketplaceConfig) -> Self {
210 let install_dir = config.plugin_dir.clone();
211
212 Self {
213 discovery: PluginDiscoveryService::new(),
214 installer: PluginInstaller {
215 install_dir: install_dir.clone(),
216 installed: HashMap::new(),
217 download_cache: install_dir.join("cache"),
218 },
219 ratings: RatingSystem {
220 ratings: HashMap::new(),
221 reviews: HashMap::new(),
222 },
223 updater: UpdateManager {
224 last_check: None,
225 available_updates: Vec::new(),
226 config: UpdateConfig::default(),
227 },
228 config,
229 }
230 }
231
232 pub async fn search(&self, query: &str) -> Result<Vec<MarketplacePlugin>> {
234 let search_query = SearchQuery {
235 text: query.to_string(),
236 category: None,
237 capabilities: vec![],
238 limit: Some(50),
239 min_rating: None,
240 };
241
242 let results = self.discovery.search(&search_query).await?;
243
244 let mut plugins = Vec::new();
245 for result in results {
246 let rating = self.ratings.ratings.get(&result.plugin_id);
247
248 plugins.push(MarketplacePlugin {
249 id: result.plugin_id.clone(),
250 name: result.plugin_id.clone(), description: result.description,
252 version: "1.0.0".to_string(), author: "Unknown".to_string(), rating: rating.map(|r| r.average_rating),
255 downloads: result.download_count as usize,
256 verified: false, tags: vec![], });
259 }
260
261 Ok(plugins)
262 }
263
264 pub async fn get_featured(&self) -> Result<Vec<MarketplacePlugin>> {
266 let mut featured: Vec<_> = self
268 .ratings
269 .ratings
270 .values()
271 .filter(|r| r.average_rating >= 4.5 && r.total_downloads >= 1000)
272 .map(|r| r.plugin_id.clone())
273 .collect();
274
275 featured.sort_by_key(|id| {
276 self.ratings
277 .ratings
278 .get(id)
279 .map(|r| r.total_downloads)
280 .unwrap_or(0)
281 });
282 featured.reverse();
283 featured.truncate(10);
284
285 let mut result = Vec::new();
287 for plugin_id in featured {
288 if let Some(rating) = self.ratings.ratings.get(&plugin_id) {
289 result.push(MarketplacePlugin {
290 id: plugin_id.clone(),
291 name: plugin_id.clone(), description: "Featured plugin".to_string(),
293 version: "1.0.0".to_string(),
294 author: "Unknown".to_string(),
295 rating: Some(rating.average_rating),
296 downloads: rating.total_downloads,
297 verified: false,
298 tags: vec![],
299 });
300 }
301 }
302
303 Ok(result)
304 }
305
306 pub async fn install(
308 &mut self,
309 plugin_id: &str,
310 version: Option<&str>,
311 ) -> Result<InstalledPlugin> {
312 if self.installer.installed.contains_key(plugin_id) {
314 return Err(SklearsError::InvalidOperation(format!(
315 "Plugin {} is already installed",
316 plugin_id
317 )));
318 }
319
320 let download_path = self.download_plugin(plugin_id, version).await?;
322
323 let manifest = self.load_manifest(&download_path)?;
325
326 let install_path = self.installer.install_dir.join(plugin_id);
331 std::fs::create_dir_all(&install_path).map_err(|e| {
332 SklearsError::InvalidOperation(format!("Failed to create install directory: {}", e))
333 })?;
334
335 let installed = InstalledPlugin {
336 id: plugin_id.to_string(),
337 name: manifest.metadata.name.clone(),
338 version: manifest.metadata.version.clone(),
339 installed_at: SystemTime::now(),
340 install_path: install_path.clone(),
341 manifest,
342 };
343
344 self.installer
345 .installed
346 .insert(plugin_id.to_string(), installed.clone());
347
348 Ok(installed)
349 }
350
351 pub fn uninstall(&mut self, plugin_id: &str) -> Result<()> {
353 let installed = self.installer.installed.remove(plugin_id).ok_or_else(|| {
354 SklearsError::InvalidOperation(format!("Plugin {} is not installed", plugin_id))
355 })?;
356
357 if installed.install_path.exists() {
359 std::fs::remove_dir_all(&installed.install_path).map_err(|e| {
360 SklearsError::InvalidOperation(format!("Failed to remove plugin files: {}", e))
361 })?;
362 }
363
364 Ok(())
365 }
366
367 pub async fn check_for_updates(&mut self) -> Result<Vec<PluginUpdate>> {
369 self.updater.last_check = Some(SystemTime::now());
370 self.updater.available_updates.clear();
371
372 for (plugin_id, installed) in &self.installer.installed {
373 if let Some(latest) = self.get_latest_version(plugin_id).await? {
375 if latest != installed.version {
376 self.updater.available_updates.push(PluginUpdate {
377 plugin_id: plugin_id.clone(),
378 current_version: installed.version.clone(),
379 new_version: latest.clone(),
380 description: format!("Update {} to version {}", plugin_id, latest),
381 size_bytes: 1024 * 1024, breaking_change: false,
383 release_notes: "Bug fixes and improvements".to_string(),
384 });
385 }
386 }
387 }
388
389 Ok(self.updater.available_updates.clone())
390 }
391
392 pub fn rate_plugin(
394 &mut self,
395 plugin_id: &str,
396 rating: u8,
397 review: Option<UserReview>,
398 ) -> Result<()> {
399 if !(1..=5).contains(&rating) {
400 return Err(SklearsError::InvalidOperation(
401 "Rating must be between 1 and 5".to_string(),
402 ));
403 }
404
405 let plugin_rating = self
406 .ratings
407 .ratings
408 .entry(plugin_id.to_string())
409 .or_insert_with(|| PluginRating {
410 plugin_id: plugin_id.to_string(),
411 average_rating: 0.0,
412 total_ratings: 0,
413 rating_distribution: [0; 5],
414 total_downloads: 0,
415 });
416
417 plugin_rating.rating_distribution[(rating - 1) as usize] += 1;
419 plugin_rating.total_ratings += 1;
420
421 let total: f64 = plugin_rating
423 .rating_distribution
424 .iter()
425 .enumerate()
426 .map(|(i, &count)| (i + 1) as f64 * count as f64)
427 .sum();
428 plugin_rating.average_rating = total / plugin_rating.total_ratings as f64;
429
430 if let Some(review) = review {
432 self.ratings
433 .reviews
434 .entry(plugin_id.to_string())
435 .or_default()
436 .push(review);
437 }
438
439 Ok(())
440 }
441
442 pub fn get_reviews(&self, plugin_id: &str) -> Vec<&UserReview> {
444 self.ratings
445 .reviews
446 .get(plugin_id)
447 .map(|reviews| reviews.iter().collect())
448 .unwrap_or_default()
449 }
450
451 pub fn list_installed(&self) -> Vec<&InstalledPlugin> {
453 self.installer.installed.values().collect()
454 }
455
456 async fn download_plugin(&self, plugin_id: &str, _version: Option<&str>) -> Result<PathBuf> {
458 let download_path = self
459 .installer
460 .download_cache
461 .join(format!("{}.tar.gz", plugin_id));
462
463 std::fs::create_dir_all(&self.installer.download_cache).map_err(|e| {
465 SklearsError::InvalidOperation(format!("Failed to create cache directory: {}", e))
466 })?;
467
468 Ok(download_path)
470 }
471
472 fn load_manifest(&self, _path: &Path) -> Result<PluginManifest> {
474 use crate::plugin::security::PublisherInfo;
476 use crate::plugin::types_config::PluginMetadata;
477 use crate::plugin::validation::MarketplaceInfo;
478 Ok(PluginManifest {
479 metadata: PluginMetadata::default(),
480 permissions: vec![],
481 api_usage: None,
482 contains_unsafe_code: false,
483 dependencies: vec![],
484 code_analysis: None,
485 signature: None,
486 content_hash: String::new(),
487 publisher: PublisherInfo {
488 name: "test".to_string(),
489 email: "test@test.com".to_string(),
490 website: None,
491 verified: false,
492 trust_score: 5,
493 },
494 marketplace: MarketplaceInfo {
495 url: "https://marketplace.example.com".to_string(),
496 downloads: 0,
497 rating: 0.0,
498 reviews: 0,
499 last_updated: chrono::Utc::now().to_rfc3339(),
500 },
501 })
502 }
503
504 async fn get_latest_version(&self, _plugin_id: &str) -> Result<Option<String>> {
506 Ok(Some("2.0.0".to_string()))
508 }
509}
510
511impl Default for ConcretePluginMarketplace {
512 fn default() -> Self {
513 Self::new()
514 }
515}
516
517#[derive(Debug, Clone, Serialize, Deserialize)]
519pub struct MarketplacePlugin {
520 pub id: String,
522 pub name: String,
524 pub description: String,
526 pub version: String,
528 pub author: String,
530 pub rating: Option<f64>,
532 pub downloads: usize,
534 pub verified: bool,
536 pub tags: Vec<String>,
538}
539
540#[cfg(test)]
541mod tests {
542 use super::*;
543
544 #[test]
545 fn test_marketplace_creation() {
546 let marketplace = ConcretePluginMarketplace::new();
547 assert!(marketplace.installer.installed.is_empty());
548 }
549
550 #[test]
551 fn test_rating_plugin() {
552 let mut marketplace = ConcretePluginMarketplace::new();
553
554 marketplace.rate_plugin("test_plugin", 5, None).unwrap();
555 marketplace.rate_plugin("test_plugin", 4, None).unwrap();
556 marketplace.rate_plugin("test_plugin", 5, None).unwrap();
557
558 let rating = marketplace.ratings.ratings.get("test_plugin").unwrap();
559 assert_eq!(rating.total_ratings, 3);
560 assert!((rating.average_rating - 4.67).abs() < 0.01);
561 }
562
563 #[test]
564 fn test_invalid_rating() {
565 let mut marketplace = ConcretePluginMarketplace::new();
566 let result = marketplace.rate_plugin("test_plugin", 0, None);
567 assert!(result.is_err());
568
569 let result = marketplace.rate_plugin("test_plugin", 6, None);
570 assert!(result.is_err());
571 }
572
573 #[test]
574 fn test_review_submission() {
575 let mut marketplace = ConcretePluginMarketplace::new();
576
577 let review = UserReview {
578 id: "review1".to_string(),
579 plugin_id: "test_plugin".to_string(),
580 user_id: "user1".to_string(),
581 rating: 5,
582 title: "Great plugin!".to_string(),
583 content: "Works perfectly".to_string(),
584 helpful_votes: 0,
585 posted_at: SystemTime::now(),
586 verified_install: true,
587 };
588
589 marketplace
590 .rate_plugin("test_plugin", 5, Some(review))
591 .unwrap();
592
593 let reviews = marketplace.get_reviews("test_plugin");
594 assert_eq!(reviews.len(), 1);
595 assert_eq!(reviews[0].title, "Great plugin!");
596 }
597
598 #[test]
599 fn test_list_installed() {
600 let marketplace = ConcretePluginMarketplace::new();
601 let installed = marketplace.list_installed();
602 assert_eq!(installed.len(), 0);
603 }
604
605 #[test]
606 fn test_update_strategy() {
607 let config = UpdateConfig::default();
608 assert_eq!(config.strategy, UpdateStrategy::Manual);
609 assert!(!config.auto_update);
610 }
611
612 #[test]
613 fn test_marketplace_config() {
614 let config = MarketplaceConfig::default();
615 assert!(!config.auto_update);
616 assert_eq!(config.max_concurrent_downloads, 3);
617 }
618
619 #[test]
620 fn test_rating_distribution() {
621 let mut marketplace = ConcretePluginMarketplace::new();
622
623 marketplace.rate_plugin("test", 5, None).unwrap();
624 marketplace.rate_plugin("test", 5, None).unwrap();
625 marketplace.rate_plugin("test", 4, None).unwrap();
626 marketplace.rate_plugin("test", 3, None).unwrap();
627
628 let rating = marketplace.ratings.ratings.get("test").unwrap();
629 assert_eq!(rating.rating_distribution[4], 2); assert_eq!(rating.rating_distribution[3], 1); assert_eq!(rating.rating_distribution[2], 1); }
633
634 #[test]
635 fn test_uninstall_nonexistent() {
636 let mut marketplace = ConcretePluginMarketplace::new();
637 let result = marketplace.uninstall("nonexistent");
638 assert!(result.is_err());
639 }
640
641 #[test]
642 fn test_installed_plugin_creation() {
643 use crate::plugin::security::PublisherInfo;
644 use crate::plugin::types_config::PluginMetadata;
645 use crate::plugin::validation::MarketplaceInfo;
646
647 let plugin = InstalledPlugin {
648 id: "test".to_string(),
649 name: "Test Plugin".to_string(),
650 version: "1.0.0".to_string(),
651 installed_at: SystemTime::now(),
652 install_path: PathBuf::from("/tmp/test"),
653 manifest: PluginManifest {
654 metadata: PluginMetadata::default(),
655 permissions: vec![],
656 api_usage: None,
657 contains_unsafe_code: false,
658 dependencies: vec![],
659 code_analysis: None,
660 signature: None,
661 content_hash: String::new(),
662 publisher: PublisherInfo {
663 name: "test".to_string(),
664 email: "test@test.com".to_string(),
665 website: Some("https://test.com".to_string()),
666 verified: true,
667 trust_score: 8,
668 },
669 marketplace: MarketplaceInfo {
670 url: "https://marketplace.example.com/test".to_string(),
671 downloads: 100,
672 rating: 4.5,
673 reviews: 10,
674 last_updated: chrono::Utc::now().to_rfc3339(),
675 },
676 },
677 };
678
679 assert_eq!(plugin.id, "test");
680 assert_eq!(plugin.version, "1.0.0");
681 }
682}