Skip to main content

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