sklears_core/
plugin_marketplace_impl.rs

1//! Concrete Plugin Marketplace Implementation
2//!
3//! This module provides a fully functional plugin marketplace for discovering,
4//! installing, and managing sklears plugins from multiple sources.
5
6use 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/// Concrete plugin marketplace implementation
15///
16/// Provides a complete marketplace experience with plugin discovery, installation,
17/// ratings, reviews, and automatic updates.
18#[derive(Debug)]
19pub struct ConcretePluginMarketplace {
20    /// Discovery service for finding plugins
21    pub discovery: PluginDiscoveryService,
22    /// Installation manager
23    pub installer: PluginInstaller,
24    /// Rating and review system
25    pub ratings: RatingSystem,
26    /// Update manager for automatic updates
27    pub updater: UpdateManager,
28    /// Marketplace configuration
29    pub config: MarketplaceConfig,
30}
31
32/// Configuration for the marketplace
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct MarketplaceConfig {
35    /// Local plugin directory
36    pub plugin_dir: PathBuf,
37    /// Enable automatic updates
38    pub auto_update: bool,
39    /// Check for updates interval (in seconds)
40    pub update_check_interval: u64,
41    /// Enable community ratings
42    pub enable_ratings: bool,
43    /// Require verified publishers
44    pub require_verified: bool,
45    /// Maximum concurrent downloads
46    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, // 24 hours
55            enable_ratings: true,
56            require_verified: false,
57            max_concurrent_downloads: 3,
58        }
59    }
60}
61
62/// Plugin installation manager
63#[derive(Debug)]
64pub struct PluginInstaller {
65    /// Installation directory
66    pub install_dir: PathBuf,
67    /// Installed plugins registry
68    pub installed: HashMap<String, InstalledPlugin>,
69    /// Download cache
70    pub download_cache: PathBuf,
71}
72
73/// Information about an installed plugin
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct InstalledPlugin {
76    /// Plugin identifier
77    pub id: String,
78    /// Plugin name
79    pub name: String,
80    /// Installed version
81    pub version: String,
82    /// Installation date
83    pub installed_at: SystemTime,
84    /// Installation path
85    pub install_path: PathBuf,
86    /// Plugin manifest
87    pub manifest: PluginManifest,
88}
89
90/// Rating and review system for plugins
91#[derive(Debug, Clone)]
92pub struct RatingSystem {
93    /// Plugin ratings
94    pub ratings: HashMap<String, PluginRating>,
95    /// User reviews
96    pub reviews: HashMap<String, Vec<UserReview>>,
97}
98
99/// Rating information for a plugin
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct PluginRating {
102    /// Plugin identifier
103    pub plugin_id: String,
104    /// Average rating (1.0 to 5.0)
105    pub average_rating: f64,
106    /// Total number of ratings
107    pub total_ratings: usize,
108    /// Rating distribution
109    pub rating_distribution: [usize; 5], // 1-star to 5-star counts
110    /// Total downloads
111    pub total_downloads: usize,
112}
113
114/// User review for a plugin
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct UserReview {
117    /// Review identifier
118    pub id: String,
119    /// Plugin identifier
120    pub plugin_id: String,
121    /// User identifier
122    pub user_id: String,
123    /// Rating (1-5)
124    pub rating: u8,
125    /// Review title
126    pub title: String,
127    /// Review content
128    pub content: String,
129    /// Helpful votes
130    pub helpful_votes: usize,
131    /// Posted timestamp
132    pub posted_at: SystemTime,
133    /// Verified purchase
134    pub verified_install: bool,
135}
136
137/// Automatic update manager
138#[derive(Debug)]
139pub struct UpdateManager {
140    /// Last update check time
141    pub last_check: Option<SystemTime>,
142    /// Available updates
143    pub available_updates: Vec<PluginUpdate>,
144    /// Update configuration
145    pub config: UpdateConfig,
146}
147
148/// Configuration for automatic updates
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct UpdateConfig {
151    /// Enable automatic updates
152    pub auto_update: bool,
153    /// Enable pre-release updates
154    pub include_prerelease: bool,
155    /// Update strategy
156    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/// Update strategy
170#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
171pub enum UpdateStrategy {
172    /// Manual updates only
173    Manual,
174    /// Notify when updates available
175    Notify,
176    /// Automatically download updates
177    AutoDownload,
178    /// Automatically install updates
179    AutoInstall,
180}
181
182/// Information about an available update
183#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct PluginUpdate {
185    /// Plugin identifier
186    pub plugin_id: String,
187    /// Current version
188    pub current_version: String,
189    /// New version
190    pub new_version: String,
191    /// Update description
192    pub description: String,
193    /// Update size in bytes
194    pub size_bytes: usize,
195    /// Is breaking change
196    pub breaking_change: bool,
197    /// Release notes
198    pub release_notes: String,
199}
200
201impl ConcretePluginMarketplace {
202    /// Create a new marketplace instance
203    pub fn new() -> Self {
204        let config = MarketplaceConfig::default();
205        Self::with_config(config)
206    }
207
208    /// Create a marketplace with custom configuration
209    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    /// Search for plugins in the marketplace
233    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(), // Use plugin_id as name for now
251                description: result.description,
252                version: "1.0.0".to_string(),  // Placeholder version
253                author: "Unknown".to_string(), // Placeholder author
254                rating: rating.map(|r| r.average_rating),
255                downloads: result.download_count as usize,
256                verified: false, // Placeholder
257                tags: vec![],    // Placeholder
258            });
259        }
260
261        Ok(plugins)
262    }
263
264    /// Get featured plugins
265    pub async fn get_featured(&self) -> Result<Vec<MarketplacePlugin>> {
266        // Get plugins with high ratings and many downloads
267        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        // Convert to MarketplacePlugin
286        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(), // Would fetch from metadata
292                    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    /// Install a plugin from the marketplace
307    pub async fn install(
308        &mut self,
309        plugin_id: &str,
310        version: Option<&str>,
311    ) -> Result<InstalledPlugin> {
312        // Check if already installed
313        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        // Download plugin
321        let download_path = self.download_plugin(plugin_id, version).await?;
322
323        // Load and verify manifest exists
324        let manifest = self.load_manifest(&download_path)?;
325
326        // TODO: Full validation would require loading the plugin first
327        // For now, we just verify the manifest can be loaded
328
329        // Install plugin
330        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    /// Uninstall a plugin
352    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        // Remove installation directory
358        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    /// Check for plugin updates
368    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            // Check if newer version is available
374            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, // Placeholder
382                        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    /// Submit a rating for a plugin
393    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        // Update rating
418        plugin_rating.rating_distribution[(rating - 1) as usize] += 1;
419        plugin_rating.total_ratings += 1;
420
421        // Recalculate average
422        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        // Add review if provided
431        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    /// Get reviews for a plugin
443    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    /// Get installed plugins
452    pub fn list_installed(&self) -> Vec<&InstalledPlugin> {
453        self.installer.installed.values().collect()
454    }
455
456    /// Download a plugin (simulated)
457    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        // Create download cache if it doesn't exist
464        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        // Simulate download
469        Ok(download_path)
470    }
471
472    /// Load plugin manifest (simulated)
473    fn load_manifest(&self, _path: &Path) -> Result<PluginManifest> {
474        // In a real implementation, this would parse the manifest file
475        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    /// Get latest version of a plugin (simulated)
505    async fn get_latest_version(&self, _plugin_id: &str) -> Result<Option<String>> {
506        // In a real implementation, this would query the repository
507        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/// Plugin information for marketplace display
518#[derive(Debug, Clone, Serialize, Deserialize)]
519pub struct MarketplacePlugin {
520    /// Plugin identifier
521    pub id: String,
522    /// Plugin name
523    pub name: String,
524    /// Description
525    pub description: String,
526    /// Version
527    pub version: String,
528    /// Author
529    pub author: String,
530    /// Average rating
531    pub rating: Option<f64>,
532    /// Total downloads
533    pub downloads: usize,
534    /// Verified publisher
535    pub verified: bool,
536    /// Tags
537    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); // Two 5-star ratings
630        assert_eq!(rating.rating_distribution[3], 1); // One 4-star rating
631        assert_eq!(rating.rating_distribution[2], 1); // One 3-star rating
632    }
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}