mockforge_chaos/
template_marketplace.rs

1//! Template Marketplace for orchestration templates
2//!
3//! Provides a marketplace for sharing and discovering chaos orchestration templates
4//! with ratings, categories, and version management.
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10/// Orchestration template
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct OrchestrationTemplate {
13    pub id: String,
14    pub name: String,
15    pub description: String,
16    pub author: String,
17    pub author_email: String,
18    pub version: String,
19    pub category: TemplateCategory,
20    pub tags: Vec<String>,
21    pub content: serde_json::Value,
22    pub readme: String,
23    pub example_usage: Option<String>,
24    pub requirements: Vec<String>,
25    pub compatibility: CompatibilityInfo,
26    pub stats: TemplateStats,
27    pub created_at: DateTime<Utc>,
28    pub updated_at: DateTime<Utc>,
29    pub published: bool,
30}
31
32/// Template category
33#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
34#[serde(rename_all = "kebab-case")]
35pub enum TemplateCategory {
36    NetworkChaos,
37    ServiceFailure,
38    LoadTesting,
39    ResilienceTesting,
40    SecurityTesting,
41    DataCorruption,
42    MultiProtocol,
43    CustomScenario,
44}
45
46/// Compatibility information
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct CompatibilityInfo {
49    pub min_version: String,
50    pub max_version: Option<String>,
51    pub required_features: Vec<String>,
52    pub protocols: Vec<String>,
53}
54
55/// Template statistics
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct TemplateStats {
58    pub downloads: u64,
59    pub stars: u64,
60    pub forks: u64,
61    pub rating: f64,
62    pub rating_count: u64,
63}
64
65/// Template review/rating
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct TemplateReview {
68    pub id: String,
69    pub template_id: String,
70    pub user_id: String,
71    pub user_name: String,
72    pub rating: u8,
73    pub comment: String,
74    pub created_at: DateTime<Utc>,
75    pub helpful_count: u64,
76}
77
78/// Search filters
79#[derive(Debug, Clone, Serialize, Deserialize, Default)]
80pub struct TemplateSearchFilters {
81    pub category: Option<TemplateCategory>,
82    pub tags: Vec<String>,
83    pub min_rating: Option<f64>,
84    pub author: Option<String>,
85    pub query: Option<String>,
86    pub sort_by: TemplateSortBy,
87    pub limit: usize,
88    pub offset: usize,
89}
90
91/// Sort options
92#[derive(Debug, Clone, Serialize, Deserialize)]
93#[serde(rename_all = "snake_case")]
94pub enum TemplateSortBy {
95    Popular,
96    Newest,
97    TopRated,
98    MostDownloaded,
99    RecentlyUpdated,
100}
101
102impl Default for TemplateSortBy {
103    fn default() -> Self {
104        Self::Popular
105    }
106}
107
108/// Template marketplace
109pub struct TemplateMarketplace {
110    templates: HashMap<String, OrchestrationTemplate>,
111    reviews: HashMap<String, Vec<TemplateReview>>,
112}
113
114impl TemplateMarketplace {
115    /// Create a new marketplace
116    pub fn new() -> Self {
117        Self {
118            templates: HashMap::new(),
119            reviews: HashMap::new(),
120        }
121    }
122
123    /// Publish a template
124    pub fn publish_template(&mut self, template: OrchestrationTemplate) -> Result<(), String> {
125        if template.id.is_empty() {
126            return Err("Template ID cannot be empty".to_string());
127        }
128
129        if template.name.is_empty() {
130            return Err("Template name cannot be empty".to_string());
131        }
132
133        if template.version.is_empty() {
134            return Err("Template version cannot be empty".to_string());
135        }
136
137        self.templates.insert(template.id.clone(), template);
138        Ok(())
139    }
140
141    /// Get a template by ID
142    pub fn get_template(&self, template_id: &str) -> Option<&OrchestrationTemplate> {
143        self.templates.get(template_id)
144    }
145
146    /// Search templates
147    pub fn search_templates(&self, filters: TemplateSearchFilters) -> Vec<OrchestrationTemplate> {
148        let mut results: Vec<_> = self
149            .templates
150            .values()
151            .filter(|t| t.published)
152            .filter(|t| {
153                // Category filter
154                if let Some(ref category) = filters.category {
155                    if &t.category != category {
156                        return false;
157                    }
158                }
159
160                // Tags filter
161                if !filters.tags.is_empty() && !filters.tags.iter().any(|tag| t.tags.contains(tag))
162                {
163                    return false;
164                }
165
166                // Min rating filter
167                if let Some(min_rating) = filters.min_rating {
168                    if t.stats.rating < min_rating {
169                        return false;
170                    }
171                }
172
173                // Author filter
174                if let Some(ref author) = filters.author {
175                    if !t.author.to_lowercase().contains(&author.to_lowercase()) {
176                        return false;
177                    }
178                }
179
180                // Query filter (search in name and description)
181                if let Some(ref query) = filters.query {
182                    let query_lower = query.to_lowercase();
183                    if !t.name.to_lowercase().contains(&query_lower)
184                        && !t.description.to_lowercase().contains(&query_lower)
185                    {
186                        return false;
187                    }
188                }
189
190                true
191            })
192            .cloned()
193            .collect();
194
195        // Sort results
196        match filters.sort_by {
197            TemplateSortBy::Popular => {
198                results.sort_by(|a, b| {
199                    let score_a = a.stats.downloads + a.stats.stars * 2;
200                    let score_b = b.stats.downloads + b.stats.stars * 2;
201                    score_b.cmp(&score_a)
202                });
203            }
204            TemplateSortBy::Newest => {
205                results.sort_by(|a, b| b.created_at.cmp(&a.created_at));
206            }
207            TemplateSortBy::TopRated => {
208                results.sort_by(|a, b| {
209                    b.stats.rating.partial_cmp(&a.stats.rating).unwrap_or(std::cmp::Ordering::Equal)
210                });
211            }
212            TemplateSortBy::MostDownloaded => {
213                results.sort_by(|a, b| b.stats.downloads.cmp(&a.stats.downloads));
214            }
215            TemplateSortBy::RecentlyUpdated => {
216                results.sort_by(|a, b| b.updated_at.cmp(&a.updated_at));
217            }
218        }
219
220        // Apply pagination
221        results.into_iter().skip(filters.offset).take(filters.limit).collect()
222    }
223
224    /// Download a template
225    pub fn download_template(
226        &mut self,
227        template_id: &str,
228    ) -> Result<OrchestrationTemplate, String> {
229        if let Some(template) = self.templates.get_mut(template_id) {
230            template.stats.downloads += 1;
231            Ok(template.clone())
232        } else {
233            Err(format!("Template '{}' not found", template_id))
234        }
235    }
236
237    /// Star a template
238    pub fn star_template(&mut self, template_id: &str) -> Result<(), String> {
239        if let Some(template) = self.templates.get_mut(template_id) {
240            template.stats.stars += 1;
241            Ok(())
242        } else {
243            Err(format!("Template '{}' not found", template_id))
244        }
245    }
246
247    /// Unstar a template
248    pub fn unstar_template(&mut self, template_id: &str) -> Result<(), String> {
249        if let Some(template) = self.templates.get_mut(template_id) {
250            if template.stats.stars > 0 {
251                template.stats.stars -= 1;
252            }
253            Ok(())
254        } else {
255            Err(format!("Template '{}' not found", template_id))
256        }
257    }
258
259    /// Add a review
260    pub fn add_review(&mut self, review: TemplateReview) -> Result<(), String> {
261        // Validate rating
262        if review.rating > 5 {
263            return Err("Rating must be between 1 and 5".to_string());
264        }
265
266        // Check if template exists
267        if !self.templates.contains_key(&review.template_id) {
268            return Err(format!("Template '{}' not found", review.template_id));
269        }
270
271        // Add review
272        self.reviews.entry(review.template_id.clone()).or_default().push(review.clone());
273
274        // Update template rating
275        self.update_template_rating(&review.template_id)?;
276
277        Ok(())
278    }
279
280    /// Update template rating
281    fn update_template_rating(&mut self, template_id: &str) -> Result<(), String> {
282        if let Some(reviews) = self.reviews.get(template_id) {
283            if let Some(template) = self.templates.get_mut(template_id) {
284                let total: u64 = reviews.iter().map(|r| r.rating as u64).sum();
285                let count = reviews.len() as u64;
286
287                template.stats.rating = if count > 0 {
288                    total as f64 / count as f64
289                } else {
290                    0.0
291                };
292                template.stats.rating_count = count;
293            }
294        }
295
296        Ok(())
297    }
298
299    /// Get reviews for a template
300    pub fn get_reviews(&self, template_id: &str) -> Vec<TemplateReview> {
301        self.reviews.get(template_id).cloned().unwrap_or_default()
302    }
303
304    /// Get popular templates
305    pub fn get_popular_templates(&self, limit: usize) -> Vec<OrchestrationTemplate> {
306        let mut templates: Vec<_> =
307            self.templates.values().filter(|t| t.published).cloned().collect();
308
309        templates.sort_by(|a, b| {
310            let score_a = a.stats.downloads + a.stats.stars * 2;
311            let score_b = b.stats.downloads + b.stats.stars * 2;
312            score_b.cmp(&score_a)
313        });
314
315        templates.into_iter().take(limit).collect()
316    }
317
318    /// Get templates by category
319    pub fn get_templates_by_category(
320        &self,
321        category: TemplateCategory,
322    ) -> Vec<OrchestrationTemplate> {
323        self.templates
324            .values()
325            .filter(|t| t.published && t.category == category)
326            .cloned()
327            .collect()
328    }
329
330    /// Get user templates
331    pub fn get_user_templates(&self, author_email: &str) -> Vec<OrchestrationTemplate> {
332        self.templates
333            .values()
334            .filter(|t| t.author_email == author_email)
335            .cloned()
336            .collect()
337    }
338
339    /// Update template
340    pub fn update_template(
341        &mut self,
342        template_id: &str,
343        updates: OrchestrationTemplate,
344    ) -> Result<(), String> {
345        if let Some(template) = self.templates.get_mut(template_id) {
346            *template = updates;
347            template.updated_at = Utc::now();
348            Ok(())
349        } else {
350            Err(format!("Template '{}' not found", template_id))
351        }
352    }
353
354    /// Delete template
355    pub fn delete_template(&mut self, template_id: &str) -> Result<(), String> {
356        if self.templates.remove(template_id).is_some() {
357            self.reviews.remove(template_id);
358            Ok(())
359        } else {
360            Err(format!("Template '{}' not found", template_id))
361        }
362    }
363
364    /// Get template count
365    pub fn template_count(&self) -> usize {
366        self.templates.values().filter(|t| t.published).count()
367    }
368}
369
370impl Default for TemplateMarketplace {
371    fn default() -> Self {
372        Self::new()
373    }
374}
375
376#[cfg(test)]
377mod tests {
378    use super::*;
379
380    fn create_test_template() -> OrchestrationTemplate {
381        OrchestrationTemplate {
382            id: "test-template-1".to_string(),
383            name: "Test Template".to_string(),
384            description: "A test template".to_string(),
385            author: "Test Author".to_string(),
386            author_email: "test@example.com".to_string(),
387            version: "1.0.0".to_string(),
388            category: TemplateCategory::NetworkChaos,
389            tags: vec!["test".to_string(), "network".to_string()],
390            content: serde_json::json!({}),
391            readme: "# Test Template".to_string(),
392            example_usage: None,
393            requirements: vec![],
394            compatibility: CompatibilityInfo {
395                min_version: "0.1.0".to_string(),
396                max_version: None,
397                required_features: vec![],
398                protocols: vec!["http".to_string()],
399            },
400            stats: TemplateStats {
401                downloads: 0,
402                stars: 0,
403                forks: 0,
404                rating: 0.0,
405                rating_count: 0,
406            },
407            created_at: Utc::now(),
408            updated_at: Utc::now(),
409            published: true,
410        }
411    }
412
413    #[test]
414    fn test_publish_template() {
415        let mut marketplace = TemplateMarketplace::new();
416        let template = create_test_template();
417
418        marketplace.publish_template(template).unwrap();
419        assert_eq!(marketplace.template_count(), 1);
420    }
421
422    #[test]
423    fn test_download_template() {
424        let mut marketplace = TemplateMarketplace::new();
425        let template = create_test_template();
426        marketplace.publish_template(template).unwrap();
427
428        marketplace.download_template("test-template-1").unwrap();
429
430        let downloaded = marketplace.get_template("test-template-1").unwrap();
431        assert_eq!(downloaded.stats.downloads, 1);
432    }
433
434    #[test]
435    fn test_star_template() {
436        let mut marketplace = TemplateMarketplace::new();
437        let template = create_test_template();
438        marketplace.publish_template(template).unwrap();
439
440        marketplace.star_template("test-template-1").unwrap();
441
442        let starred = marketplace.get_template("test-template-1").unwrap();
443        assert_eq!(starred.stats.stars, 1);
444    }
445
446    #[test]
447    fn test_add_review() {
448        let mut marketplace = TemplateMarketplace::new();
449        let template = create_test_template();
450        marketplace.publish_template(template).unwrap();
451
452        let review = TemplateReview {
453            id: "review-1".to_string(),
454            template_id: "test-template-1".to_string(),
455            user_id: "user-1".to_string(),
456            user_name: "Test User".to_string(),
457            rating: 5,
458            comment: "Great template!".to_string(),
459            created_at: Utc::now(),
460            helpful_count: 0,
461        };
462
463        marketplace.add_review(review).unwrap();
464
465        let reviews = marketplace.get_reviews("test-template-1");
466        assert_eq!(reviews.len(), 1);
467
468        let template = marketplace.get_template("test-template-1").unwrap();
469        assert_eq!(template.stats.rating, 5.0);
470    }
471
472    #[test]
473    fn test_search_templates() {
474        let mut marketplace = TemplateMarketplace::new();
475        let template = create_test_template();
476        marketplace.publish_template(template).unwrap();
477
478        let filters = TemplateSearchFilters {
479            category: Some(TemplateCategory::NetworkChaos),
480            tags: vec![],
481            min_rating: None,
482            author: None,
483            query: None,
484            sort_by: TemplateSortBy::Newest,
485            limit: 10,
486            offset: 0,
487        };
488
489        let results = marketplace.search_templates(filters);
490        assert_eq!(results.len(), 1);
491    }
492}