Skip to main content

mockforge_plugin_registry/
lib.rs

1//! MockForge Plugin Registry
2//!
3//! Central registry for discovering, publishing, and installing plugins.
4
5pub mod api;
6pub mod config;
7pub mod dependencies;
8pub mod hot_reload;
9pub mod index;
10pub mod manifest;
11pub mod reviews;
12pub mod runtime;
13pub mod security;
14pub mod storage;
15
16use serde::{Deserialize, Serialize};
17use std::collections::HashMap;
18use thiserror::Error;
19
20/// Registry errors
21#[derive(Error, Debug)]
22pub enum RegistryError {
23    #[error("Plugin not found: {0}")]
24    PluginNotFound(String),
25
26    #[error("Invalid version: {0}")]
27    InvalidVersion(String),
28
29    #[error("Plugin already exists: {0}")]
30    PluginExists(String),
31
32    #[error("Authentication required")]
33    AuthRequired,
34
35    #[error("Permission denied")]
36    PermissionDenied,
37
38    #[error("Invalid manifest: {0}")]
39    InvalidManifest(String),
40
41    #[error("Storage error: {0}")]
42    Storage(String),
43
44    #[error("Network error: {0}")]
45    Network(String),
46
47    #[error(transparent)]
48    Io(#[from] std::io::Error),
49
50    #[error(transparent)]
51    Serde(#[from] serde_json::Error),
52}
53
54pub type Result<T> = std::result::Result<T, RegistryError>;
55
56/// Plugin registry entry
57#[derive(Debug, Clone, Serialize, Deserialize)]
58#[serde(rename_all = "camelCase")]
59pub struct RegistryEntry {
60    /// Plugin name
61    pub name: String,
62
63    /// Plugin description
64    pub description: String,
65
66    /// Current version
67    pub version: String,
68
69    /// All available versions
70    pub versions: Vec<VersionEntry>,
71
72    /// Author information
73    pub author: AuthorInfo,
74
75    /// Plugin tags
76    pub tags: Vec<String>,
77
78    /// Plugin category
79    pub category: PluginCategory,
80
81    /// Download count
82    pub downloads: u64,
83
84    /// Rating (0.0 - 5.0)
85    pub rating: f64,
86
87    /// Total reviews
88    pub reviews_count: u32,
89
90    /// Security score (0-100). Heuristic: verified+maintained plugins score higher.
91    #[serde(default)]
92    pub security_score: u8,
93
94    /// Source language of the plugin (rust/python/javascript/typescript/go/other).
95    #[serde(default)]
96    pub language: String,
97
98    /// Repository URL
99    pub repository: Option<String>,
100
101    /// Homepage URL
102    pub homepage: Option<String>,
103
104    /// License
105    pub license: String,
106
107    /// Created timestamp
108    pub created_at: String,
109
110    /// Updated timestamp
111    pub updated_at: String,
112}
113
114/// Version-specific entry
115#[derive(Debug, Clone, Serialize, Deserialize)]
116#[serde(rename_all = "camelCase")]
117pub struct VersionEntry {
118    /// Version string (semver)
119    pub version: String,
120
121    /// Download URL
122    pub download_url: String,
123
124    /// SHA-256 checksum
125    pub checksum: String,
126
127    /// File size in bytes
128    pub size: u64,
129
130    /// Published timestamp
131    pub published_at: String,
132
133    /// Yanked (removed from index)
134    pub yanked: bool,
135
136    /// Minimum MockForge version required
137    pub min_mockforge_version: Option<String>,
138
139    /// Dependencies
140    pub dependencies: HashMap<String, String>,
141}
142
143/// Author information
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct AuthorInfo {
146    pub name: String,
147    pub email: Option<String>,
148    pub url: Option<String>,
149}
150
151/// Plugin category
152#[derive(Debug, Clone, Serialize, Deserialize)]
153#[serde(rename_all = "lowercase")]
154pub enum PluginCategory {
155    Auth,
156    Template,
157    Response,
158    DataSource,
159    Middleware,
160    Testing,
161    Observability,
162    Other,
163}
164
165/// Search query
166#[derive(Debug, Clone, Serialize, Deserialize)]
167#[serde(rename_all = "camelCase")]
168pub struct SearchQuery {
169    /// Search terms
170    pub query: Option<String>,
171
172    /// Filter by category
173    pub category: Option<PluginCategory>,
174
175    /// Filter by source language (rust/python/javascript/typescript/go/other).
176    #[serde(default)]
177    pub language: Option<String>,
178
179    /// Filter by tags
180    pub tags: Vec<String>,
181
182    /// Sort order
183    pub sort: SortOrder,
184
185    /// Page number (0-indexed)
186    pub page: usize,
187
188    /// Results per page
189    #[serde(alias = "per_page")]
190    pub per_page: usize,
191}
192
193impl Default for SearchQuery {
194    fn default() -> Self {
195        Self {
196            query: None,
197            category: None,
198            language: None,
199            tags: vec![],
200            sort: SortOrder::Relevance,
201            page: 0,
202            per_page: 20,
203        }
204    }
205}
206
207/// Sort order
208#[derive(Debug, Clone, Serialize, Deserialize)]
209#[serde(rename_all = "lowercase")]
210pub enum SortOrder {
211    Relevance,
212    Downloads,
213    Rating,
214    Recent,
215    Name,
216}
217
218/// Search results
219#[derive(Debug, Clone, Serialize, Deserialize)]
220#[serde(rename_all = "camelCase")]
221pub struct SearchResults {
222    pub plugins: Vec<RegistryEntry>,
223    pub total: usize,
224    pub page: usize,
225    pub per_page: usize,
226}
227
228/// Registry configuration
229#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct RegistryConfig {
231    /// Registry URL
232    pub url: String,
233
234    /// API token (optional)
235    pub token: Option<String>,
236
237    /// Cache directory
238    pub cache_dir: Option<String>,
239
240    /// Timeout in seconds
241    pub timeout: u64,
242
243    /// Alternative registries
244    pub alternative_registries: Vec<String>,
245}
246
247impl Default for RegistryConfig {
248    fn default() -> Self {
249        Self {
250            url: "https://registry.mockforge.dev".to_string(),
251            token: None,
252            cache_dir: None,
253            timeout: 30,
254            alternative_registries: vec![],
255        }
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262
263    fn create_test_author() -> AuthorInfo {
264        AuthorInfo {
265            name: "Test Author".to_string(),
266            email: Some("test@example.com".to_string()),
267            url: Some("https://example.com".to_string()),
268        }
269    }
270
271    fn create_test_version_entry() -> VersionEntry {
272        VersionEntry {
273            version: "1.0.0".to_string(),
274            download_url: "https://example.com/plugin-1.0.0.tar.gz".to_string(),
275            checksum: "abc123def456".to_string(),
276            size: 12345,
277            published_at: "2025-01-01T00:00:00Z".to_string(),
278            yanked: false,
279            min_mockforge_version: Some("0.3.0".to_string()),
280            dependencies: HashMap::new(),
281        }
282    }
283
284    fn create_test_registry_entry() -> RegistryEntry {
285        RegistryEntry {
286            name: "test-plugin".to_string(),
287            description: "Test plugin".to_string(),
288            version: "1.0.0".to_string(),
289            versions: vec![create_test_version_entry()],
290            author: create_test_author(),
291            tags: vec!["test".to_string()],
292            category: PluginCategory::Auth,
293            downloads: 100,
294            rating: 4.5,
295            reviews_count: 10,
296            security_score: 0,
297            language: "rust".to_string(),
298            repository: Some("https://github.com/test/plugin".to_string()),
299            homepage: Some("https://plugin.example.com".to_string()),
300            license: "MIT".to_string(),
301            created_at: "2025-01-01T00:00:00Z".to_string(),
302            updated_at: "2025-01-01T00:00:00Z".to_string(),
303        }
304    }
305
306    // RegistryError tests
307    #[test]
308    fn test_registry_error_plugin_not_found() {
309        let error = RegistryError::PluginNotFound("my-plugin".to_string());
310        let display = error.to_string();
311        assert!(display.contains("Plugin not found"));
312        assert!(display.contains("my-plugin"));
313    }
314
315    #[test]
316    fn test_registry_error_invalid_version() {
317        let error = RegistryError::InvalidVersion("bad version".to_string());
318        let display = error.to_string();
319        assert!(display.contains("Invalid version"));
320    }
321
322    #[test]
323    fn test_registry_error_plugin_exists() {
324        let error = RegistryError::PluginExists("existing-plugin".to_string());
325        let display = error.to_string();
326        assert!(display.contains("Plugin already exists"));
327    }
328
329    #[test]
330    fn test_registry_error_auth_required() {
331        let error = RegistryError::AuthRequired;
332        let display = error.to_string();
333        assert!(display.contains("Authentication required"));
334    }
335
336    #[test]
337    fn test_registry_error_permission_denied() {
338        let error = RegistryError::PermissionDenied;
339        let display = error.to_string();
340        assert!(display.contains("Permission denied"));
341    }
342
343    #[test]
344    fn test_registry_error_invalid_manifest() {
345        let error = RegistryError::InvalidManifest("missing field".to_string());
346        let display = error.to_string();
347        assert!(display.contains("Invalid manifest"));
348    }
349
350    #[test]
351    fn test_registry_error_storage() {
352        let error = RegistryError::Storage("disk full".to_string());
353        let display = error.to_string();
354        assert!(display.contains("Storage error"));
355    }
356
357    #[test]
358    fn test_registry_error_network() {
359        let error = RegistryError::Network("connection refused".to_string());
360        let display = error.to_string();
361        assert!(display.contains("Network error"));
362    }
363
364    #[test]
365    fn test_registry_error_from_io() {
366        let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
367        let error: RegistryError = io_error.into();
368        assert!(matches!(error, RegistryError::Io(_)));
369    }
370
371    #[test]
372    fn test_registry_error_debug() {
373        let error = RegistryError::AuthRequired;
374        let debug = format!("{:?}", error);
375        assert!(debug.contains("AuthRequired"));
376    }
377
378    // AuthorInfo tests
379    #[test]
380    fn test_author_info_clone() {
381        let author = create_test_author();
382        let cloned = author.clone();
383        assert_eq!(author.name, cloned.name);
384        assert_eq!(author.email, cloned.email);
385        assert_eq!(author.url, cloned.url);
386    }
387
388    #[test]
389    fn test_author_info_debug() {
390        let author = create_test_author();
391        let debug = format!("{:?}", author);
392        assert!(debug.contains("AuthorInfo"));
393        assert!(debug.contains("Test Author"));
394    }
395
396    #[test]
397    fn test_author_info_serialize() {
398        let author = create_test_author();
399        let json = serde_json::to_string(&author).unwrap();
400        assert!(json.contains("\"name\":\"Test Author\""));
401        assert!(json.contains("\"email\":\"test@example.com\""));
402    }
403
404    #[test]
405    fn test_author_info_deserialize() {
406        let json = r#"{"name":"Author","email":null,"url":null}"#;
407        let author: AuthorInfo = serde_json::from_str(json).unwrap();
408        assert_eq!(author.name, "Author");
409        assert!(author.email.is_none());
410    }
411
412    // PluginCategory tests
413    #[test]
414    fn test_plugin_category_serialize_all() {
415        assert_eq!(serde_json::to_string(&PluginCategory::Auth).unwrap(), "\"auth\"");
416        assert_eq!(serde_json::to_string(&PluginCategory::Template).unwrap(), "\"template\"");
417        assert_eq!(serde_json::to_string(&PluginCategory::Response).unwrap(), "\"response\"");
418        assert_eq!(serde_json::to_string(&PluginCategory::DataSource).unwrap(), "\"datasource\"");
419        assert_eq!(serde_json::to_string(&PluginCategory::Middleware).unwrap(), "\"middleware\"");
420        assert_eq!(serde_json::to_string(&PluginCategory::Testing).unwrap(), "\"testing\"");
421        assert_eq!(
422            serde_json::to_string(&PluginCategory::Observability).unwrap(),
423            "\"observability\""
424        );
425        assert_eq!(serde_json::to_string(&PluginCategory::Other).unwrap(), "\"other\"");
426    }
427
428    #[test]
429    fn test_plugin_category_deserialize() {
430        let category: PluginCategory = serde_json::from_str("\"middleware\"").unwrap();
431        assert!(matches!(category, PluginCategory::Middleware));
432    }
433
434    #[test]
435    fn test_plugin_category_clone() {
436        let category = PluginCategory::Testing;
437        let cloned = category.clone();
438        assert!(matches!(cloned, PluginCategory::Testing));
439    }
440
441    #[test]
442    fn test_plugin_category_debug() {
443        let category = PluginCategory::Observability;
444        let debug = format!("{:?}", category);
445        assert!(debug.contains("Observability"));
446    }
447
448    // SortOrder tests
449    #[test]
450    fn test_sort_order_serialize_all() {
451        assert_eq!(serde_json::to_string(&SortOrder::Relevance).unwrap(), "\"relevance\"");
452        assert_eq!(serde_json::to_string(&SortOrder::Downloads).unwrap(), "\"downloads\"");
453        assert_eq!(serde_json::to_string(&SortOrder::Rating).unwrap(), "\"rating\"");
454        assert_eq!(serde_json::to_string(&SortOrder::Recent).unwrap(), "\"recent\"");
455        assert_eq!(serde_json::to_string(&SortOrder::Name).unwrap(), "\"name\"");
456    }
457
458    #[test]
459    fn test_sort_order_deserialize() {
460        let sort: SortOrder = serde_json::from_str("\"downloads\"").unwrap();
461        assert!(matches!(sort, SortOrder::Downloads));
462    }
463
464    #[test]
465    fn test_sort_order_clone() {
466        let sort = SortOrder::Rating;
467        let cloned = sort.clone();
468        assert!(matches!(cloned, SortOrder::Rating));
469    }
470
471    #[test]
472    fn test_sort_order_debug() {
473        let sort = SortOrder::Recent;
474        let debug = format!("{:?}", sort);
475        assert!(debug.contains("Recent"));
476    }
477
478    // VersionEntry tests
479    #[test]
480    fn test_version_entry_clone() {
481        let entry = create_test_version_entry();
482        let cloned = entry.clone();
483        assert_eq!(entry.version, cloned.version);
484        assert_eq!(entry.checksum, cloned.checksum);
485    }
486
487    #[test]
488    fn test_version_entry_debug() {
489        let entry = create_test_version_entry();
490        let debug = format!("{:?}", entry);
491        assert!(debug.contains("VersionEntry"));
492        assert!(debug.contains("1.0.0"));
493    }
494
495    #[test]
496    fn test_version_entry_serialize() {
497        let entry = create_test_version_entry();
498        let json = serde_json::to_string(&entry).unwrap();
499        assert!(json.contains("\"version\":\"1.0.0\""));
500        assert!(json.contains("\"yanked\":false"));
501    }
502
503    #[test]
504    fn test_version_entry_with_dependencies() {
505        let mut entry = create_test_version_entry();
506        entry.dependencies.insert("other-plugin".to_string(), "^1.0".to_string());
507
508        let json = serde_json::to_string(&entry).unwrap();
509        assert!(json.contains("other-plugin"));
510    }
511
512    #[test]
513    fn test_version_entry_yanked() {
514        let mut entry = create_test_version_entry();
515        entry.yanked = true;
516
517        let json = serde_json::to_string(&entry).unwrap();
518        assert!(json.contains("\"yanked\":true"));
519    }
520
521    // RegistryEntry tests
522    #[test]
523    fn test_registry_entry_serialization() {
524        let entry = create_test_registry_entry();
525        let json = serde_json::to_string(&entry).unwrap();
526        let deserialized: RegistryEntry = serde_json::from_str(&json).unwrap();
527        assert_eq!(entry.name, deserialized.name);
528        assert_eq!(entry.version, deserialized.version);
529        assert_eq!(entry.downloads, deserialized.downloads);
530    }
531
532    #[test]
533    fn test_registry_entry_clone() {
534        let entry = create_test_registry_entry();
535        let cloned = entry.clone();
536        assert_eq!(entry.name, cloned.name);
537        assert_eq!(entry.rating, cloned.rating);
538    }
539
540    #[test]
541    fn test_registry_entry_debug() {
542        let entry = create_test_registry_entry();
543        let debug = format!("{:?}", entry);
544        assert!(debug.contains("RegistryEntry"));
545        assert!(debug.contains("test-plugin"));
546    }
547
548    #[test]
549    fn test_registry_entry_with_no_optional_fields() {
550        let entry = RegistryEntry {
551            name: "minimal".to_string(),
552            description: "Minimal plugin".to_string(),
553            version: "0.1.0".to_string(),
554            versions: vec![],
555            author: AuthorInfo {
556                name: "Author".to_string(),
557                email: None,
558                url: None,
559            },
560            tags: vec![],
561            category: PluginCategory::Other,
562            downloads: 0,
563            rating: 0.0,
564            reviews_count: 0,
565            security_score: 0,
566            language: "rust".to_string(),
567            repository: None,
568            homepage: None,
569            license: "MIT".to_string(),
570            created_at: "2025-01-01".to_string(),
571            updated_at: "2025-01-01".to_string(),
572        };
573
574        let json = serde_json::to_string(&entry).unwrap();
575        let deserialized: RegistryEntry = serde_json::from_str(&json).unwrap();
576        assert!(deserialized.repository.is_none());
577    }
578
579    // SearchQuery tests
580    #[test]
581    fn test_search_query_default() {
582        let query = SearchQuery::default();
583        assert_eq!(query.page, 0);
584        assert_eq!(query.per_page, 20);
585        assert!(query.query.is_none());
586        assert!(query.category.is_none());
587        assert!(query.tags.is_empty());
588        assert!(matches!(query.sort, SortOrder::Relevance));
589    }
590
591    #[test]
592    fn test_search_query_clone() {
593        let query = SearchQuery {
594            query: Some("auth".to_string()),
595            page: 5,
596            ..Default::default()
597        };
598
599        let cloned = query.clone();
600        assert_eq!(query.query, cloned.query);
601        assert_eq!(query.page, cloned.page);
602    }
603
604    #[test]
605    fn test_search_query_serialize() {
606        let query = SearchQuery {
607            query: Some("jwt".to_string()),
608            category: Some(PluginCategory::Auth),
609            language: None,
610            tags: vec!["security".to_string()],
611            sort: SortOrder::Downloads,
612            page: 1,
613            per_page: 50,
614        };
615
616        let json = serde_json::to_string(&query).unwrap();
617        assert!(json.contains("\"query\":\"jwt\""));
618        assert!(json.contains("\"category\":\"auth\""));
619    }
620
621    #[test]
622    fn test_search_query_debug() {
623        let query = SearchQuery::default();
624        let debug = format!("{:?}", query);
625        assert!(debug.contains("SearchQuery"));
626    }
627
628    // SearchResults tests
629    #[test]
630    fn test_search_results_serialize() {
631        let results = SearchResults {
632            plugins: vec![create_test_registry_entry()],
633            total: 1,
634            page: 0,
635            per_page: 20,
636        };
637
638        let json = serde_json::to_string(&results).unwrap();
639        assert!(json.contains("\"total\":1"));
640        assert!(json.contains("test-plugin"));
641    }
642
643    #[test]
644    fn test_search_results_clone() {
645        let results = SearchResults {
646            plugins: vec![],
647            total: 100,
648            page: 5,
649            per_page: 20,
650        };
651
652        let cloned = results.clone();
653        assert_eq!(results.total, cloned.total);
654        assert_eq!(results.page, cloned.page);
655    }
656
657    #[test]
658    fn test_search_results_debug() {
659        let results = SearchResults {
660            plugins: vec![],
661            total: 0,
662            page: 0,
663            per_page: 20,
664        };
665
666        let debug = format!("{:?}", results);
667        assert!(debug.contains("SearchResults"));
668    }
669
670    #[test]
671    fn test_search_results_empty() {
672        let results = SearchResults {
673            plugins: vec![],
674            total: 0,
675            page: 0,
676            per_page: 20,
677        };
678
679        let json = serde_json::to_string(&results).unwrap();
680        let deserialized: SearchResults = serde_json::from_str(&json).unwrap();
681        assert!(deserialized.plugins.is_empty());
682        assert_eq!(deserialized.total, 0);
683    }
684
685    // RegistryConfig tests
686    #[test]
687    fn test_registry_config_default() {
688        let config = RegistryConfig::default();
689        assert_eq!(config.url, "https://registry.mockforge.dev");
690        assert!(config.token.is_none());
691        assert!(config.cache_dir.is_none());
692        assert_eq!(config.timeout, 30);
693        assert!(config.alternative_registries.is_empty());
694    }
695
696    #[test]
697    fn test_registry_config_clone() {
698        let config = RegistryConfig {
699            token: Some("secret-token".to_string()),
700            ..Default::default()
701        };
702
703        let cloned = config.clone();
704        assert_eq!(config.url, cloned.url);
705        assert_eq!(config.token, cloned.token);
706    }
707
708    #[test]
709    fn test_registry_config_serialize() {
710        let config = RegistryConfig::default();
711        let json = serde_json::to_string(&config).unwrap();
712        assert!(json.contains("\"url\":\"https://registry.mockforge.dev\""));
713        assert!(json.contains("\"timeout\":30"));
714    }
715
716    #[test]
717    fn test_registry_config_deserialize() {
718        let json = r#"{
719            "url": "https://custom.registry.com",
720            "token": "my-token",
721            "cache_dir": "/tmp/cache",
722            "timeout": 60,
723            "alternative_registries": ["https://alt.registry.com"]
724        }"#;
725
726        let config: RegistryConfig = serde_json::from_str(json).unwrap();
727        assert_eq!(config.url, "https://custom.registry.com");
728        assert_eq!(config.token, Some("my-token".to_string()));
729        assert_eq!(config.cache_dir, Some("/tmp/cache".to_string()));
730        assert_eq!(config.timeout, 60);
731        assert_eq!(config.alternative_registries.len(), 1);
732    }
733
734    #[test]
735    fn test_registry_config_debug() {
736        let config = RegistryConfig::default();
737        let debug = format!("{:?}", config);
738        assert!(debug.contains("RegistryConfig"));
739    }
740
741    #[test]
742    fn test_registry_config_with_alternatives() {
743        let config = RegistryConfig {
744            alternative_registries: vec![
745                "https://mirror1.registry.com".to_string(),
746                "https://mirror2.registry.com".to_string(),
747            ],
748            ..Default::default()
749        };
750
751        let json = serde_json::to_string(&config).unwrap();
752        assert!(json.contains("mirror1"));
753        assert!(json.contains("mirror2"));
754    }
755}