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