1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10#[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#[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#[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#[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#[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#[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#[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
108pub struct TemplateMarketplace {
110 templates: HashMap<String, OrchestrationTemplate>,
111 reviews: HashMap<String, Vec<TemplateReview>>,
112}
113
114impl TemplateMarketplace {
115 pub fn new() -> Self {
117 Self {
118 templates: HashMap::new(),
119 reviews: HashMap::new(),
120 }
121 }
122
123 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 pub fn get_template(&self, template_id: &str) -> Option<&OrchestrationTemplate> {
143 self.templates.get(template_id)
144 }
145
146 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 if let Some(ref category) = filters.category {
155 if &t.category != category {
156 return false;
157 }
158 }
159
160 if !filters.tags.is_empty() && !filters.tags.iter().any(|tag| t.tags.contains(tag))
162 {
163 return false;
164 }
165
166 if let Some(min_rating) = filters.min_rating {
168 if t.stats.rating < min_rating {
169 return false;
170 }
171 }
172
173 if let Some(ref author) = filters.author {
175 if !t.author.to_lowercase().contains(&author.to_lowercase()) {
176 return false;
177 }
178 }
179
180 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 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 results.into_iter().skip(filters.offset).take(filters.limit).collect()
222 }
223
224 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 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 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 pub fn add_review(&mut self, review: TemplateReview) -> Result<(), String> {
261 if review.rating > 5 {
263 return Err("Rating must be between 1 and 5".to_string());
264 }
265
266 if !self.templates.contains_key(&review.template_id) {
268 return Err(format!("Template '{}' not found", review.template_id));
269 }
270
271 self.reviews.entry(review.template_id.clone()).or_default().push(review.clone());
273
274 self.update_template_rating(&review.template_id)?;
276
277 Ok(())
278 }
279
280 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 pub fn get_reviews(&self, template_id: &str) -> Vec<TemplateReview> {
301 self.reviews.get(template_id).cloned().unwrap_or_default()
302 }
303
304 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 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 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 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 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 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}