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