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)]
58pub struct RegistryEntry {
59 pub name: String,
61
62 pub description: String,
64
65 pub version: String,
67
68 pub versions: Vec<VersionEntry>,
70
71 pub author: AuthorInfo,
73
74 pub tags: Vec<String>,
76
77 pub category: PluginCategory,
79
80 pub downloads: u64,
82
83 pub rating: f64,
85
86 pub reviews_count: u32,
88
89 pub repository: Option<String>,
91
92 pub homepage: Option<String>,
94
95 pub license: String,
97
98 pub created_at: String,
100
101 pub updated_at: String,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct VersionEntry {
108 pub version: String,
110
111 pub download_url: String,
113
114 pub checksum: String,
116
117 pub size: u64,
119
120 pub published_at: String,
122
123 pub yanked: bool,
125
126 pub min_mockforge_version: Option<String>,
128
129 pub dependencies: HashMap<String, String>,
131}
132
133#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct SearchQuery {
158 pub query: Option<String>,
160
161 pub category: Option<PluginCategory>,
163
164 pub tags: Vec<String>,
166
167 pub sort: SortOrder,
169
170 pub page: usize,
172
173 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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct RegistryConfig {
213 pub url: String,
215
216 pub token: Option<String>,
218
219 pub cache_dir: Option<String>,
221
222 pub timeout: u64,
224
225 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}