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::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 library.load_templates()?;
117
118 Ok(library)
119 }
120
121 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 }
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 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 pub fn register_template(&mut self, metadata: TemplateMetadata) -> Result<()> {
174 let template_id = metadata.id.clone();
175
176 let entry = if let Some(existing) = self.templates.get_mut(&template_id) {
178 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 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 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 self.save_template(&entry)?;
224
225 self.templates.insert(template_id, entry);
227
228 Ok(())
229 }
230
231 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 pub fn get_template(&self, id: &str) -> Option<&TemplateLibraryEntry> {
250 self.templates.get(id)
251 }
252
253 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 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 self.get_template_version(id, &entry.latest_version).unwrap_or_default()
267 })
268 })
269 }
270
271 pub fn list_templates(&self) -> Vec<&TemplateLibraryEntry> {
273 self.templates.values().collect()
274 }
275
276 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 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 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 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 self.remove_template(id)?;
334 } else {
335 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 let template_clone = template.clone();
343 let _ = template; self.save_template(&template_clone)?;
347 }
348 }
349 Ok(())
350 }
351
352 pub fn storage_dir(&self) -> &Path {
354 &self.storage_dir
355 }
356}
357
358pub struct TemplateMarketplace {
360 registry_url: String,
362 auth_token: Option<String>,
364}
365
366impl TemplateMarketplace {
367 pub fn new(registry_url: String, auth_token: Option<String>) -> Self {
369 Self {
370 registry_url,
371 auth_token,
372 }
373 }
374
375 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 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 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 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
500pub struct TemplateLibraryManager {
502 library: TemplateLibrary,
504 marketplace: Option<TemplateMarketplace>,
506}
507
508impl TemplateLibraryManager {
509 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 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 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 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 pub fn library(&self) -> &TemplateLibrary {
566 &self.library
567 }
568
569 pub fn library_mut(&mut self) -> &mut TemplateLibrary {
571 &mut self.library
572 }
573
574 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}