ricecoder_generation/templates/
discovery.rs1use crate::models::{
6 Boilerplate, BoilerplateDiscoveryResult, BoilerplateMetadata, BoilerplateSource, Template,
7 TemplateMetadata,
8};
9use crate::templates::error::{BoilerplateError, TemplateError};
10use crate::templates::loader::TemplateLoader;
11use std::collections::HashMap;
12use std::fs;
13use std::path::{Path, PathBuf};
14
15pub struct TemplateDiscovery {
17 loader: TemplateLoader,
18}
19
20impl TemplateDiscovery {
21 pub fn new() -> Self {
23 Self {
24 loader: TemplateLoader::new(),
25 }
26 }
27
28 pub fn discover(&mut self, project_root: &Path) -> Result<DiscoveryResult, TemplateError> {
39 let mut search_paths = Vec::new();
40 let mut templates = Vec::new();
41
42 let global_dir = self.get_global_templates_dir();
44 if global_dir.exists() {
45 search_paths.push(global_dir.clone());
46 let global_templates = self.loader.load_global_templates()?;
47 templates.extend(global_templates);
48 }
49
50 let project_dir = project_root.join(".ricecoder").join("templates");
52 if project_dir.exists() {
53 search_paths.push(project_dir.clone());
54 let project_templates = self.loader.load_project_templates(project_root)?;
55
56 let mut template_map: std::collections::HashMap<String, Template> =
58 templates.into_iter().map(|t| (t.id.clone(), t)).collect();
59
60 for template in project_templates {
62 template_map.insert(template.id.clone(), template);
63 }
64
65 templates = template_map.into_values().collect();
66 }
67
68 Ok(DiscoveryResult {
69 templates,
70 search_paths,
71 })
72 }
73
74 pub fn discover_by_language(
83 &mut self,
84 project_root: &Path,
85 language: &str,
86 ) -> Result<Vec<Template>, TemplateError> {
87 let result = self.discover(project_root)?;
88 Ok(result
89 .templates
90 .into_iter()
91 .filter(|t| t.language == language)
92 .collect())
93 }
94
95 pub fn discover_by_name(
104 &mut self,
105 project_root: &Path,
106 pattern: &str,
107 ) -> Result<Vec<Template>, TemplateError> {
108 let result = self.discover(project_root)?;
109 let pattern_lower = pattern.to_lowercase();
110 Ok(result
111 .templates
112 .into_iter()
113 .filter(|t| t.name.to_lowercase().contains(&pattern_lower))
114 .collect())
115 }
116
117 pub fn parse_metadata(&self, template_path: &Path) -> Result<TemplateMetadata, TemplateError> {
127 let content = fs::read_to_string(template_path).map_err(TemplateError::IoError)?;
128
129 let mut metadata = TemplateMetadata {
131 description: None,
132 version: None,
133 author: None,
134 };
135
136 for line in content.lines().take(10) {
138 if line.contains("@description") {
139 if let Some(desc) = line.split("@description").nth(1) {
140 metadata.description = Some(desc.trim().to_string());
141 }
142 } else if line.contains("@version") {
143 if let Some(ver) = line.split("@version").nth(1) {
144 metadata.version = Some(ver.trim().to_string());
145 }
146 } else if line.contains("@author") {
147 if let Some(auth) = line.split("@author").nth(1) {
148 metadata.author = Some(auth.trim().to_string());
149 }
150 }
151 }
152
153 Ok(metadata)
154 }
155
156 pub fn validate_template(&self, template_path: &Path) -> Result<(), TemplateError> {
166 if !template_path.exists() {
167 return Err(TemplateError::NotFound(format!(
168 "Template not found: {}",
169 template_path.display()
170 )));
171 }
172
173 if !template_path.is_file() {
174 return Err(TemplateError::ValidationFailed(format!(
175 "Template is not a file: {}",
176 template_path.display()
177 )));
178 }
179
180 fs::read_to_string(template_path).map_err(TemplateError::IoError)?;
182
183 Ok(())
184 }
185
186 fn get_global_templates_dir(&self) -> PathBuf {
188 if let Ok(home) = std::env::var("HOME") {
189 PathBuf::from(home).join(".ricecoder").join("templates")
190 } else if let Ok(home) = std::env::var("USERPROFILE") {
191 PathBuf::from(home).join(".ricecoder").join("templates")
193 } else {
194 PathBuf::from(".ricecoder/templates")
195 }
196 }
197}
198
199impl Default for TemplateDiscovery {
200 fn default() -> Self {
201 Self::new()
202 }
203}
204
205#[derive(Debug, Clone)]
207pub struct DiscoveryResult {
208 pub templates: Vec<Template>,
210 pub search_paths: Vec<PathBuf>,
212}
213
214pub struct BoilerplateDiscovery;
219
220impl BoilerplateDiscovery {
221 pub fn discover(project_root: &Path) -> Result<BoilerplateDiscoveryResult, BoilerplateError> {
237 let mut search_paths = Vec::new();
238 let mut boilerplate_map: HashMap<String, BoilerplateMetadata> = HashMap::new();
239
240 let global_dir = Self::get_global_boilerplates_dir();
242 if global_dir.exists() {
243 search_paths.push(global_dir.clone());
244 let global_boilerplates = Self::scan_boilerplate_directory(&global_dir, true)?;
245 for bp in global_boilerplates {
246 boilerplate_map.insert(bp.id.clone(), bp);
247 }
248 }
249
250 let project_dir = project_root.join(".ricecoder").join("boilerplates");
252 if project_dir.exists() {
253 search_paths.push(project_dir.clone());
254 let project_boilerplates = Self::scan_boilerplate_directory(&project_dir, false)?;
255 for bp in project_boilerplates {
256 boilerplate_map.insert(bp.id.clone(), bp);
257 }
258 }
259
260 let boilerplates: Vec<BoilerplateMetadata> = boilerplate_map.into_values().collect();
261
262 Ok(BoilerplateDiscoveryResult {
263 boilerplates,
264 search_paths,
265 })
266 }
267
268 pub fn discover_by_language(
277 project_root: &Path,
278 language: &str,
279 ) -> Result<Vec<BoilerplateMetadata>, BoilerplateError> {
280 let result = Self::discover(project_root)?;
281 Ok(result
282 .boilerplates
283 .into_iter()
284 .filter(|bp| bp.language.to_lowercase() == language.to_lowercase())
285 .collect())
286 }
287
288 pub fn discover_by_name(
297 project_root: &Path,
298 pattern: &str,
299 ) -> Result<Vec<BoilerplateMetadata>, BoilerplateError> {
300 let result = Self::discover(project_root)?;
301 let pattern_lower = pattern.to_lowercase();
302 Ok(result
303 .boilerplates
304 .into_iter()
305 .filter(|bp| bp.name.to_lowercase().contains(&pattern_lower))
306 .collect())
307 }
308
309 pub fn validate_boilerplate(boilerplate_path: &Path) -> Result<(), BoilerplateError> {
319 if !boilerplate_path.exists() {
320 return Err(BoilerplateError::NotFound(format!(
321 "Boilerplate not found: {}",
322 boilerplate_path.display()
323 )));
324 }
325
326 if !boilerplate_path.is_dir() {
327 return Err(BoilerplateError::InvalidStructure(format!(
328 "Boilerplate is not a directory: {}",
329 boilerplate_path.display()
330 )));
331 }
332
333 let yaml_path = boilerplate_path.join("boilerplate.yaml");
335 let json_path = boilerplate_path.join("boilerplate.json");
336
337 if !yaml_path.exists() && !json_path.exists() {
338 return Err(BoilerplateError::InvalidStructure(format!(
339 "Boilerplate missing metadata file (boilerplate.yaml or boilerplate.json): {}",
340 boilerplate_path.display()
341 )));
342 }
343
344 Ok(())
345 }
346
347 pub fn parse_metadata(boilerplate_path: &Path) -> Result<Boilerplate, BoilerplateError> {
355 Self::validate_boilerplate(boilerplate_path)?;
356
357 let yaml_path = boilerplate_path.join("boilerplate.yaml");
359 let json_path = boilerplate_path.join("boilerplate.json");
360
361 if yaml_path.exists() {
362 let content = fs::read_to_string(&yaml_path).map_err(BoilerplateError::IoError)?;
363 serde_yaml::from_str(&content)
364 .map_err(|e| BoilerplateError::InvalidStructure(format!("Invalid YAML: {}", e)))
365 } else if json_path.exists() {
366 let content = fs::read_to_string(&json_path).map_err(BoilerplateError::IoError)?;
367 serde_json::from_str(&content)
368 .map_err(|e| BoilerplateError::InvalidStructure(format!("Invalid JSON: {}", e)))
369 } else {
370 Err(BoilerplateError::InvalidStructure(
371 "No boilerplate metadata file found".to_string(),
372 ))
373 }
374 }
375
376 fn scan_boilerplate_directory(
385 directory: &Path,
386 is_global: bool,
387 ) -> Result<Vec<BoilerplateMetadata>, BoilerplateError> {
388 let mut boilerplates = Vec::new();
389
390 if !directory.exists() {
391 return Ok(boilerplates);
392 }
393
394 for entry in fs::read_dir(directory).map_err(BoilerplateError::IoError)? {
395 let entry = entry.map_err(BoilerplateError::IoError)?;
396 let path = entry.path();
397
398 if path.is_dir() {
399 if let Ok(boilerplate) = Self::parse_metadata(&path) {
401 let source = if is_global {
402 BoilerplateSource::Global(path.clone())
403 } else {
404 BoilerplateSource::Project(path.clone())
405 };
406
407 let metadata = BoilerplateMetadata {
408 id: boilerplate.id,
409 name: boilerplate.name,
410 description: boilerplate.description,
411 language: boilerplate.language,
412 source,
413 };
414
415 boilerplates.push(metadata);
416 }
417 }
418 }
419
420 Ok(boilerplates)
421 }
422
423 fn get_global_boilerplates_dir() -> PathBuf {
425 if let Ok(home) = std::env::var("HOME") {
426 PathBuf::from(home).join(".ricecoder").join("boilerplates")
427 } else if let Ok(home) = std::env::var("USERPROFILE") {
428 PathBuf::from(home).join(".ricecoder").join("boilerplates")
430 } else {
431 PathBuf::from(".ricecoder/boilerplates")
432 }
433 }
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439 use std::fs;
440 use tempfile::TempDir;
441
442 #[test]
443 fn test_discover_templates() {
444 let temp_dir = TempDir::new().unwrap();
445 let templates_dir = temp_dir.path().join(".ricecoder").join("templates");
446 fs::create_dir_all(&templates_dir).unwrap();
447
448 fs::write(
449 templates_dir.join("struct.rs.tmpl"),
450 "pub struct {{Name}} {}",
451 )
452 .unwrap();
453 fs::write(templates_dir.join("impl.rs.tmpl"), "impl {{Name}} {}").unwrap();
454
455 let mut discovery = TemplateDiscovery::new();
456 let result = discovery.discover(temp_dir.path()).unwrap();
457
458 assert_eq!(result.templates.len(), 2);
459 assert!(result.search_paths.iter().any(|p| p.ends_with("templates")));
460 }
461
462 #[test]
463 fn test_discover_by_language() {
464 let temp_dir = TempDir::new().unwrap();
465 let templates_dir = temp_dir.path().join(".ricecoder").join("templates");
466 fs::create_dir_all(&templates_dir).unwrap();
467
468 fs::write(
469 templates_dir.join("struct.rs.tmpl"),
470 "pub struct {{Name}} {}",
471 )
472 .unwrap();
473 fs::write(
474 templates_dir.join("component.ts.tmpl"),
475 "export const {{Name}} = () => {}",
476 )
477 .unwrap();
478
479 let mut discovery = TemplateDiscovery::new();
480 let rust_templates = discovery
481 .discover_by_language(temp_dir.path(), "rs")
482 .unwrap();
483
484 assert_eq!(rust_templates.len(), 1);
485 assert_eq!(rust_templates[0].language, "rs");
486 }
487
488 #[test]
489 fn test_discover_by_name() {
490 let temp_dir = TempDir::new().unwrap();
491 let templates_dir = temp_dir.path().join(".ricecoder").join("templates");
492 fs::create_dir_all(&templates_dir).unwrap();
493
494 fs::write(
495 templates_dir.join("struct.rs.tmpl"),
496 "pub struct {{Name}} {}",
497 )
498 .unwrap();
499 fs::write(templates_dir.join("impl.rs.tmpl"), "impl {{Name}} {}").unwrap();
500
501 let mut discovery = TemplateDiscovery::new();
502 let struct_templates = discovery
503 .discover_by_name(temp_dir.path(), "struct")
504 .unwrap();
505
506 assert_eq!(struct_templates.len(), 1);
507 assert_eq!(struct_templates[0].id, "struct");
508 }
509
510 #[test]
511 fn test_validate_template() {
512 let temp_dir = TempDir::new().unwrap();
513 let template_path = temp_dir.path().join("test.rs.tmpl");
514 fs::write(&template_path, "pub struct {{Name}} {}").unwrap();
515
516 let discovery = TemplateDiscovery::new();
517 assert!(discovery.validate_template(&template_path).is_ok());
518 }
519
520 #[test]
521 fn test_validate_nonexistent_template() {
522 let discovery = TemplateDiscovery::new();
523 let result = discovery.validate_template(Path::new("/nonexistent/template.rs.tmpl"));
524
525 assert!(result.is_err());
526 }
527
528 #[test]
529 fn test_parse_metadata() {
530 let temp_dir = TempDir::new().unwrap();
531 let template_path = temp_dir.path().join("test.rs.tmpl");
532
533 let content = "// @description A test template\n// @version 1.0.0\n// @author Test Author\npub struct {{Name}} {}";
534 fs::write(&template_path, content).unwrap();
535
536 let discovery = TemplateDiscovery::new();
537 let metadata = discovery.parse_metadata(&template_path).unwrap();
538
539 assert!(metadata.description.is_some());
540 assert!(metadata.version.is_some());
541 assert!(metadata.author.is_some());
542 }
543
544 #[test]
547 fn test_validate_boilerplate_missing_metadata() {
548 let temp_dir = TempDir::new().unwrap();
549 let bp_dir = temp_dir.path().join("test-bp");
550 fs::create_dir_all(&bp_dir).unwrap();
551
552 let result = BoilerplateDiscovery::validate_boilerplate(&bp_dir);
553 assert!(result.is_err());
554 }
555
556 #[test]
557 fn test_validate_boilerplate_with_yaml() {
558 let temp_dir = TempDir::new().unwrap();
559 let bp_dir = temp_dir.path().join("test-bp");
560 fs::create_dir_all(&bp_dir).unwrap();
561 fs::write(bp_dir.join("boilerplate.yaml"), "id: test\nname: Test").unwrap();
562
563 let result = BoilerplateDiscovery::validate_boilerplate(&bp_dir);
564 assert!(result.is_ok());
565 }
566
567 #[test]
568 fn test_validate_boilerplate_with_json() {
569 let temp_dir = TempDir::new().unwrap();
570 let bp_dir = temp_dir.path().join("test-bp");
571 fs::create_dir_all(&bp_dir).unwrap();
572 fs::write(
573 bp_dir.join("boilerplate.json"),
574 r#"{"id":"test","name":"Test"}"#,
575 )
576 .unwrap();
577
578 let result = BoilerplateDiscovery::validate_boilerplate(&bp_dir);
579 assert!(result.is_ok());
580 }
581
582 #[test]
583 fn test_discover_boilerplate_precedence() {
584 let temp_dir = TempDir::new().unwrap();
585 let project_dir = temp_dir.path().join(".ricecoder").join("boilerplates");
586 fs::create_dir_all(&project_dir).unwrap();
587
588 let project_bp = project_dir.join("my-bp");
590 fs::create_dir_all(&project_bp).unwrap();
591 let project_metadata = r#"
592id: my-bp
593name: My Boilerplate
594description: Project version
595language: rust
596files: []
597dependencies: []
598scripts: []
599"#;
600 fs::write(project_bp.join("boilerplate.yaml"), project_metadata).unwrap();
601
602 let result = BoilerplateDiscovery::discover(temp_dir.path()).unwrap();
603
604 assert!(result.boilerplates.iter().any(|bp| bp.id == "my-bp"));
606
607 let bp = result
609 .boilerplates
610 .iter()
611 .find(|bp| bp.id == "my-bp")
612 .unwrap();
613 assert!(matches!(bp.source, BoilerplateSource::Project(_)));
614 }
615
616 #[test]
617 fn test_discover_boilerplate_by_language() {
618 let temp_dir = TempDir::new().unwrap();
619 let project_dir = temp_dir.path().join(".ricecoder").join("boilerplates");
620 fs::create_dir_all(&project_dir).unwrap();
621
622 let rust_bp = project_dir.join("rust-bp");
624 fs::create_dir_all(&rust_bp).unwrap();
625 let rust_metadata = r#"
626id: rust-bp
627name: Rust Boilerplate
628description: Rust project
629language: rust
630files: []
631dependencies: []
632scripts: []
633"#;
634 fs::write(rust_bp.join("boilerplate.yaml"), rust_metadata).unwrap();
635
636 let ts_bp = project_dir.join("ts-bp");
638 fs::create_dir_all(&ts_bp).unwrap();
639 let ts_metadata = r#"
640id: ts-bp
641name: TypeScript Boilerplate
642description: TypeScript project
643language: typescript
644files: []
645dependencies: []
646scripts: []
647"#;
648 fs::write(ts_bp.join("boilerplate.yaml"), ts_metadata).unwrap();
649
650 let result = BoilerplateDiscovery::discover_by_language(temp_dir.path(), "rust").unwrap();
651 assert_eq!(result.len(), 1);
652 assert_eq!(result[0].id, "rust-bp");
653 }
654
655 #[test]
656 fn test_discover_boilerplate_by_name() {
657 let temp_dir = TempDir::new().unwrap();
658 let project_dir = temp_dir.path().join(".ricecoder").join("boilerplates");
659 fs::create_dir_all(&project_dir).unwrap();
660
661 let bp = project_dir.join("my-awesome-bp");
663 fs::create_dir_all(&bp).unwrap();
664 let metadata = r#"
665id: my-awesome-bp
666name: My Awesome Boilerplate
667description: Test
668language: rust
669files: []
670dependencies: []
671scripts: []
672"#;
673 fs::write(bp.join("boilerplate.yaml"), metadata).unwrap();
674
675 let result = BoilerplateDiscovery::discover_by_name(temp_dir.path(), "awesome").unwrap();
676 assert_eq!(result.len(), 1);
677 assert_eq!(result[0].id, "my-awesome-bp");
678 }
679}