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 mut query = SearchQuery::default();
572        query.query = Some("auth".to_string());
573        query.page = 5;
574
575        let cloned = query.clone();
576        assert_eq!(query.query, cloned.query);
577        assert_eq!(query.page, cloned.page);
578    }
579
580    #[test]
581    fn test_search_query_serialize() {
582        let query = SearchQuery {
583            query: Some("jwt".to_string()),
584            category: Some(PluginCategory::Auth),
585            tags: vec!["security".to_string()],
586            sort: SortOrder::Downloads,
587            page: 1,
588            per_page: 50,
589        };
590
591        let json = serde_json::to_string(&query).unwrap();
592        assert!(json.contains("\"query\":\"jwt\""));
593        assert!(json.contains("\"category\":\"auth\""));
594    }
595
596    #[test]
597    fn test_search_query_debug() {
598        let query = SearchQuery::default();
599        let debug = format!("{:?}", query);
600        assert!(debug.contains("SearchQuery"));
601    }
602
603    // SearchResults tests
604    #[test]
605    fn test_search_results_serialize() {
606        let results = SearchResults {
607            plugins: vec![create_test_registry_entry()],
608            total: 1,
609            page: 0,
610            per_page: 20,
611        };
612
613        let json = serde_json::to_string(&results).unwrap();
614        assert!(json.contains("\"total\":1"));
615        assert!(json.contains("test-plugin"));
616    }
617
618    #[test]
619    fn test_search_results_clone() {
620        let results = SearchResults {
621            plugins: vec![],
622            total: 100,
623            page: 5,
624            per_page: 20,
625        };
626
627        let cloned = results.clone();
628        assert_eq!(results.total, cloned.total);
629        assert_eq!(results.page, cloned.page);
630    }
631
632    #[test]
633    fn test_search_results_debug() {
634        let results = SearchResults {
635            plugins: vec![],
636            total: 0,
637            page: 0,
638            per_page: 20,
639        };
640
641        let debug = format!("{:?}", results);
642        assert!(debug.contains("SearchResults"));
643    }
644
645    #[test]
646    fn test_search_results_empty() {
647        let results = SearchResults {
648            plugins: vec![],
649            total: 0,
650            page: 0,
651            per_page: 20,
652        };
653
654        let json = serde_json::to_string(&results).unwrap();
655        let deserialized: SearchResults = serde_json::from_str(&json).unwrap();
656        assert!(deserialized.plugins.is_empty());
657        assert_eq!(deserialized.total, 0);
658    }
659
660    // RegistryConfig tests
661    #[test]
662    fn test_registry_config_default() {
663        let config = RegistryConfig::default();
664        assert_eq!(config.url, "https://registry.mockforge.dev");
665        assert!(config.token.is_none());
666        assert!(config.cache_dir.is_none());
667        assert_eq!(config.timeout, 30);
668        assert!(config.alternative_registries.is_empty());
669    }
670
671    #[test]
672    fn test_registry_config_clone() {
673        let mut config = RegistryConfig::default();
674        config.token = Some("secret-token".to_string());
675
676        let cloned = config.clone();
677        assert_eq!(config.url, cloned.url);
678        assert_eq!(config.token, cloned.token);
679    }
680
681    #[test]
682    fn test_registry_config_serialize() {
683        let config = RegistryConfig::default();
684        let json = serde_json::to_string(&config).unwrap();
685        assert!(json.contains("\"url\":\"https://registry.mockforge.dev\""));
686        assert!(json.contains("\"timeout\":30"));
687    }
688
689    #[test]
690    fn test_registry_config_deserialize() {
691        let json = r#"{
692            "url": "https://custom.registry.com",
693            "token": "my-token",
694            "cache_dir": "/tmp/cache",
695            "timeout": 60,
696            "alternative_registries": ["https://alt.registry.com"]
697        }"#;
698
699        let config: RegistryConfig = serde_json::from_str(json).unwrap();
700        assert_eq!(config.url, "https://custom.registry.com");
701        assert_eq!(config.token, Some("my-token".to_string()));
702        assert_eq!(config.cache_dir, Some("/tmp/cache".to_string()));
703        assert_eq!(config.timeout, 60);
704        assert_eq!(config.alternative_registries.len(), 1);
705    }
706
707    #[test]
708    fn test_registry_config_debug() {
709        let config = RegistryConfig::default();
710        let debug = format!("{:?}", config);
711        assert!(debug.contains("RegistryConfig"));
712    }
713
714    #[test]
715    fn test_registry_config_with_alternatives() {
716        let mut config = RegistryConfig::default();
717        config.alternative_registries = vec![
718            "https://mirror1.registry.com".to_string(),
719            "https://mirror2.registry.com".to_string(),
720        ];
721
722        let json = serde_json::to_string(&config).unwrap();
723        assert!(json.contains("mirror1"));
724        assert!(json.contains("mirror2"));
725    }
726}