mockforge_core/
template_library.rs

1//! Template Library System
2//!
3//! Provides a system for managing, versioning, and sharing templates.
4//! Supports:
5//! - Shared template storage
6//! - Template versioning
7//! - Template marketplace/registry
8//! - Template discovery and installation
9
10use crate::{Error, Result};
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::path::{Path, PathBuf};
14use tracing::{debug, info, warn};
15
16/// Template metadata
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct TemplateMetadata {
19    /// Template ID (unique identifier)
20    pub id: String,
21    /// Template name
22    pub name: String,
23    /// Template description
24    pub description: Option<String>,
25    /// Template version (semver format)
26    pub version: String,
27    /// Template author
28    pub author: Option<String>,
29    /// Template tags for categorization
30    pub tags: Vec<String>,
31    /// Template category (e.g., "user", "payment", "auth")
32    pub category: Option<String>,
33    /// Template content (the actual template string)
34    pub content: String,
35    /// Example usage
36    pub example: Option<String>,
37    /// Dependencies (other template IDs this template depends on)
38    pub dependencies: Vec<String>,
39    /// Creation timestamp
40    pub created_at: Option<String>,
41    /// Last updated timestamp
42    pub updated_at: Option<String>,
43}
44
45/// Template version information
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct TemplateVersion {
48    /// Version string (semver)
49    pub version: String,
50    /// Template content for this version
51    pub content: String,
52    /// Changelog entry for this version
53    pub changelog: Option<String>,
54    /// Whether this is a pre-release version
55    pub prerelease: bool,
56    /// Release date
57    pub released_at: String,
58}
59
60/// Template library entry (can have multiple versions)
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct TemplateLibraryEntry {
63    /// Template ID
64    pub id: String,
65    /// Template name
66    pub name: String,
67    /// Template description
68    pub description: Option<String>,
69    /// Template author
70    pub author: Option<String>,
71    /// Template tags
72    pub tags: Vec<String>,
73    /// Template category
74    pub category: Option<String>,
75    /// Available versions
76    pub versions: Vec<TemplateVersion>,
77    /// Latest version
78    pub latest_version: String,
79    /// Dependencies
80    pub dependencies: Vec<String>,
81    /// Example usage
82    pub example: Option<String>,
83    /// Creation timestamp
84    pub created_at: Option<String>,
85    /// Last updated timestamp
86    pub updated_at: Option<String>,
87}
88
89/// Template library registry
90pub struct TemplateLibrary {
91    /// Local storage directory
92    storage_dir: PathBuf,
93    /// In-memory cache of templates
94    templates: HashMap<String, TemplateLibraryEntry>,
95}
96
97impl TemplateLibrary {
98    /// Create a new template library
99    pub fn new(storage_dir: impl AsRef<Path>) -> Result<Self> {
100        let storage_dir = storage_dir.as_ref().to_path_buf();
101
102        // Create storage directory if it doesn't exist
103        std::fs::create_dir_all(&storage_dir).map_err(|e| {
104            Error::generic(format!(
105                "Failed to create template library directory {}: {}",
106                storage_dir.display(),
107                e
108            ))
109        })?;
110
111        let mut library = Self {
112            storage_dir,
113            templates: HashMap::new(),
114        };
115
116        // Load existing templates
117        library.load_templates()?;
118
119        Ok(library)
120    }
121
122    /// Load templates from storage
123    fn load_templates(&mut self) -> Result<()> {
124        let templates_dir = self.storage_dir.join("templates");
125
126        if !templates_dir.exists() {
127            return Ok(());
128        }
129
130        for entry in std::fs::read_dir(&templates_dir)
131            .map_err(|e| Error::generic(format!("Failed to read templates directory: {}", e)))?
132        {
133            let entry = entry
134                .map_err(|e| Error::generic(format!("Failed to read directory entry: {}", e)))?;
135
136            let path = entry.path();
137            if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("json") {
138                match self.load_template_file(&path) {
139                    Ok(Some(template)) => {
140                        let id = template.id.clone();
141                        self.templates.insert(id, template);
142                    }
143                    Ok(None) => {
144                        // File doesn't contain a valid template, skip
145                    }
146                    Err(e) => {
147                        warn!("Failed to load template from {}: {}", path.display(), e);
148                    }
149                }
150            }
151        }
152
153        info!("Loaded {} template(s) from library", self.templates.len());
154        Ok(())
155    }
156
157    /// Load a template from a file
158    fn load_template_file(&self, path: &Path) -> Result<Option<TemplateLibraryEntry>> {
159        let content = std::fs::read_to_string(path).map_err(|e| {
160            Error::generic(format!("Failed to read template file {}: {}", path.display(), e))
161        })?;
162
163        let template: TemplateLibraryEntry = serde_json::from_str(&content).map_err(|e| {
164            Error::generic(format!("Failed to parse template file {}: {}", path.display(), e))
165        })?;
166
167        Ok(Some(template))
168    }
169
170    /// Register a template in the library
171    pub fn register_template(&mut self, metadata: TemplateMetadata) -> Result<()> {
172        let template_id = metadata.id.clone();
173
174        // Check if template already exists
175        let entry = if let Some(existing) = self.templates.get_mut(&template_id) {
176            // Add new version to existing template
177            let version = TemplateVersion {
178                version: metadata.version.clone(),
179                content: metadata.content.clone(),
180                changelog: None,
181                prerelease: false,
182                released_at: chrono::Utc::now().to_rfc3339(),
183            };
184
185            existing.versions.push(version);
186            existing.versions.sort_by(|a, b| {
187                // Simple version comparison (could use semver crate for better comparison)
188                b.version.cmp(&a.version)
189            });
190            existing.latest_version = metadata.version.clone();
191            existing.updated_at = Some(chrono::Utc::now().to_rfc3339());
192
193            existing.clone()
194        } else {
195            // Create new template entry
196            let version = TemplateVersion {
197                version: metadata.version.clone(),
198                content: metadata.content.clone(),
199                changelog: None,
200                prerelease: false,
201                released_at: chrono::Utc::now().to_rfc3339(),
202            };
203
204            TemplateLibraryEntry {
205                id: metadata.id.clone(),
206                name: metadata.name.clone(),
207                description: metadata.description.clone(),
208                author: metadata.author.clone(),
209                tags: metadata.tags.clone(),
210                category: metadata.category.clone(),
211                versions: vec![version],
212                latest_version: metadata.version.clone(),
213                dependencies: metadata.dependencies.clone(),
214                example: metadata.example.clone(),
215                created_at: Some(chrono::Utc::now().to_rfc3339()),
216                updated_at: Some(chrono::Utc::now().to_rfc3339()),
217            }
218        };
219
220        // Save to disk
221        self.save_template(&entry)?;
222
223        // Update in-memory cache
224        self.templates.insert(template_id, entry);
225
226        Ok(())
227    }
228
229    /// Save a template to disk
230    fn save_template(&self, template: &TemplateLibraryEntry) -> Result<()> {
231        let templates_dir = self.storage_dir.join("templates");
232        std::fs::create_dir_all(&templates_dir)
233            .map_err(|e| Error::generic(format!("Failed to create templates directory: {}", e)))?;
234
235        let file_path = templates_dir.join(format!("{}.json", template.id));
236        let json = serde_json::to_string_pretty(template)
237            .map_err(|e| Error::generic(format!("Failed to serialize template: {}", e)))?;
238
239        std::fs::write(&file_path, json)
240            .map_err(|e| Error::generic(format!("Failed to write template file: {}", e)))?;
241
242        debug!("Saved template {} to {}", template.id, file_path.display());
243        Ok(())
244    }
245
246    /// Get a template by ID
247    pub fn get_template(&self, id: &str) -> Option<&TemplateLibraryEntry> {
248        self.templates.get(id)
249    }
250
251    /// Get a specific version of a template
252    pub fn get_template_version(&self, id: &str, version: &str) -> Option<String> {
253        self.templates
254            .get(id)
255            .and_then(|entry| entry.versions.iter().find(|v| v.version == version))
256            .map(|v| v.content.clone())
257    }
258
259    /// Get the latest version of a template
260    pub fn get_latest_template(&self, id: &str) -> Option<String> {
261        self.templates.get(id).map(|entry| {
262            entry.versions.first().map(|v| v.content.clone()).unwrap_or_else(|| {
263                // Fallback to latest_version field
264                self.get_template_version(id, &entry.latest_version).unwrap_or_default()
265            })
266        })
267    }
268
269    /// List all templates
270    pub fn list_templates(&self) -> Vec<&TemplateLibraryEntry> {
271        self.templates.values().collect()
272    }
273
274    /// Search templates by query
275    pub fn search_templates(&self, query: &str) -> Vec<&TemplateLibraryEntry> {
276        let query_lower = query.to_lowercase();
277
278        self.templates
279            .values()
280            .filter(|template| {
281                template.name.to_lowercase().contains(&query_lower)
282                    || template
283                        .description
284                        .as_ref()
285                        .map(|d| d.to_lowercase().contains(&query_lower))
286                        .unwrap_or(false)
287                    || template.tags.iter().any(|tag| tag.to_lowercase().contains(&query_lower))
288                    || template
289                        .category
290                        .as_ref()
291                        .map(|c| c.to_lowercase().contains(&query_lower))
292                        .unwrap_or(false)
293            })
294            .collect()
295    }
296
297    /// Search templates by category
298    pub fn templates_by_category(&self, category: &str) -> Vec<&TemplateLibraryEntry> {
299        self.templates
300            .values()
301            .filter(|template| {
302                template
303                    .category
304                    .as_ref()
305                    .map(|c| c.eq_ignore_ascii_case(category))
306                    .unwrap_or(false)
307            })
308            .collect()
309    }
310
311    /// Remove a template
312    pub fn remove_template(&mut self, id: &str) -> Result<()> {
313        if self.templates.remove(id).is_some() {
314            let file_path = self.storage_dir.join("templates").join(format!("{}.json", id));
315            if file_path.exists() {
316                std::fs::remove_file(&file_path).map_err(|e| {
317                    Error::generic(format!("Failed to remove template file: {}", e))
318                })?;
319            }
320            info!("Removed template: {}", id);
321        }
322        Ok(())
323    }
324
325    /// Remove a specific version of a template
326    pub fn remove_template_version(&mut self, id: &str, version: &str) -> Result<()> {
327        if let Some(template) = self.templates.get_mut(id) {
328            template.versions.retain(|v| v.version != version);
329
330            if template.versions.is_empty() {
331                // Remove entire template if no versions left
332                self.remove_template(id)?;
333            } else {
334                // Update latest version
335                template.versions.sort_by(|a, b| b.version.cmp(&a.version));
336                template.latest_version =
337                    template.versions.first().map(|v| v.version.clone()).unwrap_or_default();
338                template.updated_at = Some(chrono::Utc::now().to_rfc3339());
339
340                // Clone template to avoid borrow checker issues
341                let template_clone = template.clone();
342                drop(template); // Explicitly drop mutable borrow
343
344                // Save updated template
345                self.save_template(&template_clone)?;
346            }
347        }
348        Ok(())
349    }
350
351    /// Get storage directory
352    pub fn storage_dir(&self) -> &Path {
353        &self.storage_dir
354    }
355}
356
357/// Template marketplace/registry (for remote templates)
358pub struct TemplateMarketplace {
359    /// Registry URL
360    registry_url: String,
361    /// Authentication token (optional)
362    auth_token: Option<String>,
363}
364
365impl TemplateMarketplace {
366    /// Create a new template marketplace client
367    pub fn new(registry_url: String, auth_token: Option<String>) -> Self {
368        Self {
369            registry_url,
370            auth_token,
371        }
372    }
373
374    /// Search for templates in the marketplace
375    pub async fn search(&self, query: &str) -> Result<Vec<TemplateLibraryEntry>> {
376        let encoded_query = urlencoding::encode(query);
377        let url = format!("{}/api/templates/search?q={}", self.registry_url, encoded_query);
378
379        let mut request = reqwest::Client::new().get(&url);
380        if let Some(ref token) = self.auth_token {
381            request = request.bearer_auth(token);
382        }
383
384        let response = request
385            .send()
386            .await
387            .map_err(|e| Error::generic(format!("Failed to search marketplace: {}", e)))?;
388
389        if !response.status().is_success() {
390            return Err(Error::generic(format!(
391                "Marketplace search failed with status: {}",
392                response.status()
393            )));
394        }
395
396        let templates: Vec<TemplateLibraryEntry> = response
397            .json()
398            .await
399            .map_err(|e| Error::generic(format!("Failed to parse marketplace response: {}", e)))?;
400
401        Ok(templates)
402    }
403
404    /// Get a template from the marketplace
405    pub async fn get_template(
406        &self,
407        id: &str,
408        version: Option<&str>,
409    ) -> Result<TemplateLibraryEntry> {
410        let url = if let Some(version) = version {
411            format!("{}/api/templates/{}/{}", self.registry_url, id, version)
412        } else {
413            format!("{}/api/templates/{}", self.registry_url, id)
414        };
415
416        let mut request = reqwest::Client::new().get(&url);
417        if let Some(ref token) = self.auth_token {
418            request = request.bearer_auth(token);
419        }
420
421        let response = request.send().await.map_err(|e| {
422            Error::generic(format!("Failed to fetch template from marketplace: {}", e))
423        })?;
424
425        if !response.status().is_success() {
426            return Err(Error::generic(format!("Failed to fetch template: {}", response.status())));
427        }
428
429        let template: TemplateLibraryEntry = response
430            .json()
431            .await
432            .map_err(|e| Error::generic(format!("Failed to parse template: {}", e)))?;
433
434        Ok(template)
435    }
436
437    /// List featured/popular templates
438    pub async fn list_featured(&self) -> Result<Vec<TemplateLibraryEntry>> {
439        let url = format!("{}/api/templates/featured", self.registry_url);
440
441        let mut request = reqwest::Client::new().get(&url);
442        if let Some(ref token) = self.auth_token {
443            request = request.bearer_auth(token);
444        }
445
446        let response = request
447            .send()
448            .await
449            .map_err(|e| Error::generic(format!("Failed to fetch featured templates: {}", e)))?;
450
451        if !response.status().is_success() {
452            return Err(Error::generic(format!(
453                "Failed to fetch featured templates: {}",
454                response.status()
455            )));
456        }
457
458        let templates: Vec<TemplateLibraryEntry> = response
459            .json()
460            .await
461            .map_err(|e| Error::generic(format!("Failed to parse featured templates: {}", e)))?;
462
463        Ok(templates)
464    }
465
466    /// List templates by category
467    pub async fn list_by_category(&self, category: &str) -> Result<Vec<TemplateLibraryEntry>> {
468        let encoded_category = urlencoding::encode(category);
469        let url = format!("{}/api/templates/category/{}", self.registry_url, encoded_category);
470
471        let mut request = reqwest::Client::new().get(&url);
472        if let Some(ref token) = self.auth_token {
473            request = request.bearer_auth(token);
474        }
475
476        let response = request
477            .send()
478            .await
479            .map_err(|e| Error::generic(format!("Failed to fetch templates by category: {}", e)))?;
480
481        if !response.status().is_success() {
482            return Err(Error::generic(format!(
483                "Failed to fetch templates by category: {}",
484                response.status()
485            )));
486        }
487
488        let templates: Vec<TemplateLibraryEntry> = response
489            .json()
490            .await
491            .map_err(|e| Error::generic(format!("Failed to parse templates: {}", e)))?;
492
493        Ok(templates)
494    }
495}
496
497/// Template library manager (combines local library and marketplace)
498pub struct TemplateLibraryManager {
499    /// Local template library
500    library: TemplateLibrary,
501    /// Marketplace client (optional)
502    marketplace: Option<TemplateMarketplace>,
503}
504
505impl TemplateLibraryManager {
506    /// Create a new template library manager
507    pub fn new(storage_dir: impl AsRef<Path>) -> Result<Self> {
508        let library = TemplateLibrary::new(storage_dir)?;
509        Ok(Self {
510            library,
511            marketplace: None,
512        })
513    }
514
515    /// Enable marketplace integration
516    pub fn with_marketplace(mut self, registry_url: String, auth_token: Option<String>) -> Self {
517        self.marketplace = Some(TemplateMarketplace::new(registry_url, auth_token));
518        self
519    }
520
521    /// Install a template from marketplace to local library
522    pub async fn install_from_marketplace(
523        &mut self,
524        id: &str,
525        version: Option<&str>,
526    ) -> Result<()> {
527        let marketplace = self
528            .marketplace
529            .as_ref()
530            .ok_or_else(|| Error::generic("Marketplace not configured".to_string()))?;
531
532        let template = marketplace.get_template(id, version).await?;
533
534        // Convert to metadata and register
535        let latest_version = template
536            .versions
537            .first()
538            .ok_or_else(|| Error::generic("Template has no versions".to_string()))?;
539
540        let metadata = TemplateMetadata {
541            id: template.id.clone(),
542            name: template.name.clone(),
543            description: template.description.clone(),
544            version: latest_version.version.clone(),
545            author: template.author.clone(),
546            tags: template.tags.clone(),
547            category: template.category.clone(),
548            content: latest_version.content.clone(),
549            example: template.example.clone(),
550            dependencies: template.dependencies.clone(),
551            created_at: template.created_at.clone(),
552            updated_at: template.updated_at.clone(),
553        };
554
555        self.library.register_template(metadata)?;
556        info!("Installed template {} from marketplace", id);
557
558        Ok(())
559    }
560
561    /// Get local library reference
562    pub fn library(&self) -> &TemplateLibrary {
563        &self.library
564    }
565
566    /// Get mutable local library reference
567    pub fn library_mut(&mut self) -> &mut TemplateLibrary {
568        &mut self.library
569    }
570
571    /// Get marketplace reference
572    pub fn marketplace(&self) -> Option<&TemplateMarketplace> {
573        self.marketplace.as_ref()
574    }
575}
576
577#[cfg(test)]
578mod tests {
579    use super::*;
580    use tempfile::TempDir;
581
582    #[test]
583    fn test_template_metadata() {
584        let metadata = TemplateMetadata {
585            id: "user-profile".to_string(),
586            name: "User Profile Template".to_string(),
587            description: Some("Template for user profile data".to_string()),
588            version: "1.0.0".to_string(),
589            author: Some("Test Author".to_string()),
590            tags: vec!["user".to_string(), "profile".to_string()],
591            category: Some("user".to_string()),
592            content: "{{faker.name}} - {{faker.email}}".to_string(),
593            example: Some("John Doe - john@example.com".to_string()),
594            dependencies: Vec::new(),
595            created_at: None,
596            updated_at: None,
597        };
598
599        assert_eq!(metadata.id, "user-profile");
600        assert_eq!(metadata.version, "1.0.0");
601    }
602
603    #[tokio::test]
604    async fn test_template_library() {
605        let temp_dir = TempDir::new().unwrap();
606        let library = TemplateLibrary::new(temp_dir.path()).unwrap();
607
608        let metadata = TemplateMetadata {
609            id: "test-template".to_string(),
610            name: "Test Template".to_string(),
611            description: None,
612            version: "1.0.0".to_string(),
613            author: None,
614            tags: Vec::new(),
615            category: None,
616            content: "{{uuid}}".to_string(),
617            example: None,
618            dependencies: Vec::new(),
619            created_at: None,
620            updated_at: None,
621        };
622
623        let mut library = library;
624        library.register_template(metadata).unwrap();
625
626        let template = library.get_template("test-template");
627        assert!(template.is_some());
628        assert_eq!(template.unwrap().name, "Test Template");
629    }
630}