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
143#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
167#[serde(rename_all = "camelCase")]
168pub struct SearchQuery {
169 pub query: Option<String>,
171
172 pub category: Option<PluginCategory>,
174
175 #[serde(default)]
177 pub language: Option<String>,
178
179 pub tags: Vec<String>,
181
182 pub sort: SortOrder,
184
185 pub page: usize,
187
188 #[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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct RegistryConfig {
231 pub url: String,
233
234 pub token: Option<String>,
236
237 pub cache_dir: Option<String>,
239
240 pub timeout: u64,
242
243 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}