1use crate::{Error, Result};
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::path::{Path, PathBuf};
14use tracing::{debug, info, warn};
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct TemplateMetadata {
19 pub id: String,
21 pub name: String,
23 pub description: Option<String>,
25 pub version: String,
27 pub author: Option<String>,
29 pub tags: Vec<String>,
31 pub category: Option<String>,
33 pub content: String,
35 pub example: Option<String>,
37 pub dependencies: Vec<String>,
39 pub created_at: Option<String>,
41 pub updated_at: Option<String>,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct TemplateVersion {
48 pub version: String,
50 pub content: String,
52 pub changelog: Option<String>,
54 pub prerelease: bool,
56 pub released_at: String,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct TemplateLibraryEntry {
63 pub id: String,
65 pub name: String,
67 pub description: Option<String>,
69 pub author: Option<String>,
71 pub tags: Vec<String>,
73 pub category: Option<String>,
75 pub versions: Vec<TemplateVersion>,
77 pub latest_version: String,
79 pub dependencies: Vec<String>,
81 pub example: Option<String>,
83 pub created_at: Option<String>,
85 pub updated_at: Option<String>,
87}
88
89pub struct TemplateLibrary {
91 storage_dir: PathBuf,
93 templates: HashMap<String, TemplateLibraryEntry>,
95}
96
97impl TemplateLibrary {
98 pub fn new(storage_dir: impl AsRef<Path>) -> Result<Self> {
100 let storage_dir = storage_dir.as_ref().to_path_buf();
101
102 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 library.load_templates()?;
118
119 Ok(library)
120 }
121
122 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 }
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 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 pub fn register_template(&mut self, metadata: TemplateMetadata) -> Result<()> {
172 let template_id = metadata.id.clone();
173
174 let entry = if let Some(existing) = self.templates.get_mut(&template_id) {
176 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 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 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 self.save_template(&entry)?;
222
223 self.templates.insert(template_id, entry);
225
226 Ok(())
227 }
228
229 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 pub fn get_template(&self, id: &str) -> Option<&TemplateLibraryEntry> {
248 self.templates.get(id)
249 }
250
251 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 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 self.get_template_version(id, &entry.latest_version).unwrap_or_default()
265 })
266 })
267 }
268
269 pub fn list_templates(&self) -> Vec<&TemplateLibraryEntry> {
271 self.templates.values().collect()
272 }
273
274 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 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 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 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 self.remove_template(id)?;
333 } else {
334 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 let template_clone = template.clone();
342 drop(template); self.save_template(&template_clone)?;
346 }
347 }
348 Ok(())
349 }
350
351 pub fn storage_dir(&self) -> &Path {
353 &self.storage_dir
354 }
355}
356
357pub struct TemplateMarketplace {
359 registry_url: String,
361 auth_token: Option<String>,
363}
364
365impl TemplateMarketplace {
366 pub fn new(registry_url: String, auth_token: Option<String>) -> Self {
368 Self {
369 registry_url,
370 auth_token,
371 }
372 }
373
374 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 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 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 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
497pub struct TemplateLibraryManager {
499 library: TemplateLibrary,
501 marketplace: Option<TemplateMarketplace>,
503}
504
505impl TemplateLibraryManager {
506 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 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 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 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 pub fn library(&self) -> &TemplateLibrary {
563 &self.library
564 }
565
566 pub fn library_mut(&mut self) -> &mut TemplateLibrary {
568 &mut self.library
569 }
570
571 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}