1use crate::{DrivenError, Result, UnifiedRule};
28use std::collections::HashMap;
29use std::path::{Path, PathBuf};
30
31#[derive(Debug)]
35pub struct GeneratorBridge {
36 template_paths: Vec<PathBuf>,
38 template_cache: HashMap<String, TemplateInfo>,
40 initialized: bool,
42}
43
44#[derive(Debug, Clone)]
46pub struct TemplateInfo {
47 pub id: String,
49 pub name: String,
51 pub description: String,
53 pub category: TemplateCategory,
55 pub parameters: Vec<ParameterInfo>,
57 pub output_pattern: String,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum TemplateCategory {
64 Rule,
66 Spec,
68 Scaffold,
70 Component,
72 Other,
74}
75
76#[derive(Debug, Clone)]
78pub struct ParameterInfo {
79 pub name: String,
81 pub description: String,
83 pub required: bool,
85 pub default: Option<String>,
87}
88
89#[derive(Debug, Clone)]
91pub struct GeneratedFile {
92 pub path: PathBuf,
94 pub content: String,
96 pub overwrite: bool,
98}
99
100#[derive(Debug, Clone, Default)]
102pub struct GenerateParams {
103 params: HashMap<String, ParamValue>,
105}
106
107#[derive(Debug, Clone)]
109pub enum ParamValue {
110 String(String),
112 Integer(i64),
114 Boolean(bool),
116 Array(Vec<String>),
118}
119
120impl GenerateParams {
121 pub fn new() -> Self {
123 Self::default()
124 }
125
126 pub fn set_string(mut self, key: &str, value: impl Into<String>) -> Self {
128 self.params
129 .insert(key.to_string(), ParamValue::String(value.into()));
130 self
131 }
132
133 pub fn set_int(mut self, key: &str, value: i64) -> Self {
135 self.params
136 .insert(key.to_string(), ParamValue::Integer(value));
137 self
138 }
139
140 pub fn set_bool(mut self, key: &str, value: bool) -> Self {
142 self.params
143 .insert(key.to_string(), ParamValue::Boolean(value));
144 self
145 }
146
147 pub fn set_array(mut self, key: &str, value: Vec<String>) -> Self {
149 self.params
150 .insert(key.to_string(), ParamValue::Array(value));
151 self
152 }
153
154 pub fn get(&self, key: &str) -> Option<&ParamValue> {
156 self.params.get(key)
157 }
158
159 pub fn get_string(&self, key: &str) -> Option<&str> {
161 match self.params.get(key) {
162 Some(ParamValue::String(s)) => Some(s),
163 _ => None,
164 }
165 }
166
167 pub fn get_int(&self, key: &str) -> Option<i64> {
169 match self.params.get(key) {
170 Some(ParamValue::Integer(i)) => Some(*i),
171 _ => None,
172 }
173 }
174
175 pub fn get_bool(&self, key: &str) -> Option<bool> {
177 match self.params.get(key) {
178 Some(ParamValue::Boolean(b)) => Some(*b),
179 _ => None,
180 }
181 }
182
183 pub fn to_string_map(&self) -> HashMap<String, String> {
185 self.params
186 .iter()
187 .map(|(k, v)| {
188 let value = match v {
189 ParamValue::String(s) => s.clone(),
190 ParamValue::Integer(i) => i.to_string(),
191 ParamValue::Boolean(b) => b.to_string(),
192 ParamValue::Array(a) => a.join(", "),
193 };
194 (k.clone(), value)
195 })
196 .collect()
197 }
198}
199
200impl GeneratorBridge {
201 pub fn new() -> Result<Self> {
203 let template_paths = vec![
204 PathBuf::from(".driven/templates"),
205 PathBuf::from(".dx/templates"),
206 ];
207
208 Ok(Self {
209 template_paths,
210 template_cache: HashMap::new(),
211 initialized: false,
212 })
213 }
214
215 pub fn with_paths(paths: Vec<PathBuf>) -> Result<Self> {
217 Ok(Self {
218 template_paths: paths,
219 template_cache: HashMap::new(),
220 initialized: false,
221 })
222 }
223
224 pub fn add_path(&mut self, path: impl AsRef<Path>) {
226 self.template_paths.push(path.as_ref().to_path_buf());
227 }
228
229 pub fn initialize(&mut self) -> Result<()> {
231 self.template_cache.clear();
232
233 let paths: Vec<PathBuf> = self.template_paths.clone();
235
236 for path in &paths {
237 if path.exists() {
238 self.scan_templates(path)?;
239 }
240 }
241
242 self.initialized = true;
243 Ok(())
244 }
245
246 fn scan_templates(&mut self, dir: &Path) -> Result<()> {
248 if !dir.is_dir() {
249 return Ok(());
250 }
251
252 for entry in std::fs::read_dir(dir).map_err(DrivenError::Io)? {
253 let entry = entry.map_err(DrivenError::Io)?;
254 let path = entry.path();
255
256 if path.is_file() {
257 if let Some(ext) = path.extension() {
258 if ext == "dxt" || ext == "hbs" || ext == "template" {
259 if let Ok(info) = self.parse_template_info(&path) {
260 self.template_cache.insert(info.id.clone(), info);
261 }
262 }
263 }
264 } else if path.is_dir() {
265 self.scan_templates(&path)?;
267 }
268 }
269
270 Ok(())
271 }
272
273 fn parse_template_info(&self, path: &Path) -> Result<TemplateInfo> {
275 let id = path
276 .file_stem()
277 .and_then(|s| s.to_str())
278 .ok_or_else(|| DrivenError::Template("Invalid template filename".to_string()))?
279 .to_string();
280
281 Ok(TemplateInfo {
284 id: id.clone(),
285 name: id.replace(['-', '_'], " "),
286 description: format!("Template from {}", path.display()),
287 category: self.infer_category(&id),
288 parameters: Vec::new(),
289 output_pattern: String::new(),
290 })
291 }
292
293 fn infer_category(&self, id: &str) -> TemplateCategory {
295 if id.contains("rule") || id.contains("cursor") || id.contains("copilot") {
296 TemplateCategory::Rule
297 } else if id.contains("spec") || id.contains("requirement") || id.contains("design") {
298 TemplateCategory::Spec
299 } else if id.contains("scaffold") || id.contains("project") {
300 TemplateCategory::Scaffold
301 } else if id.contains("component") || id.contains("model") {
302 TemplateCategory::Component
303 } else {
304 TemplateCategory::Other
305 }
306 }
307
308 pub fn list_templates(&self) -> Vec<&TemplateInfo> {
310 self.template_cache.values().collect()
311 }
312
313 pub fn list_templates_by_category(&self, category: TemplateCategory) -> Vec<&TemplateInfo> {
315 self.template_cache
316 .values()
317 .filter(|t| t.category == category)
318 .collect()
319 }
320
321 pub fn get_template(&self, id: &str) -> Option<&TemplateInfo> {
323 self.template_cache.get(id)
324 }
325
326 pub fn generate_rules(
331 &self,
332 template_id: &str,
333 params: &GenerateParams,
334 ) -> Result<Vec<UnifiedRule>> {
335 let _template = self
336 .get_template(template_id)
337 .ok_or_else(|| DrivenError::TemplateNotFound(template_id.to_string()))?;
338
339 let _params_map = params.to_string_map();
346
347 Ok(Vec::new())
348 }
349
350 pub fn generate_spec_scaffold(
354 &self,
355 spec_id: &str,
356 params: &GenerateParams,
357 ) -> Result<Vec<GeneratedFile>> {
358 let mut files = Vec::new();
359
360 let spec_name = params.get_string("name").unwrap_or(spec_id);
362
363 files.push(GeneratedFile {
365 path: PathBuf::from(format!(".driven/specs/{}/requirements.md", spec_id)),
366 content: self.generate_requirements_template(spec_name, params),
367 overwrite: false,
368 });
369
370 files.push(GeneratedFile {
372 path: PathBuf::from(format!(".driven/specs/{}/design.md", spec_id)),
373 content: self.generate_design_template(spec_name, params),
374 overwrite: false,
375 });
376
377 files.push(GeneratedFile {
379 path: PathBuf::from(format!(".driven/specs/{}/tasks.md", spec_id)),
380 content: self.generate_tasks_template(spec_name, params),
381 overwrite: false,
382 });
383
384 Ok(files)
385 }
386
387 fn generate_requirements_template(&self, name: &str, _params: &GenerateParams) -> String {
389 format!(
390 r#"# Requirements Document
391
392## Introduction
393
394{name}
395
396## Glossary
397
398- **System**: [Definition]
399
400## Requirements
401
402### Requirement 1
403
404**User Story:** As a [role], I want [feature], so that [benefit]
405
406#### Acceptance Criteria
407
4081. WHEN [event], THE [System] SHALL [response]
409"#,
410 name = name
411 )
412 }
413
414 fn generate_design_template(&self, name: &str, _params: &GenerateParams) -> String {
416 format!(
417 r#"# Design Document: {name}
418
419## Overview
420
421[Summary of the design]
422
423## Architecture
424
425[Architecture description]
426
427## Components and Interfaces
428
429[Component descriptions]
430
431## Data Models
432
433[Data model definitions]
434
435## Correctness Properties
436
437[Properties to validate]
438
439## Error Handling
440
441[Error handling strategy]
442
443## Testing Strategy
444
445[Testing approach]
446"#,
447 name = name
448 )
449 }
450
451 fn generate_tasks_template(&self, name: &str, _params: &GenerateParams) -> String {
453 format!(
454 r#"# Implementation Plan: {name}
455
456## Overview
457
458[Implementation approach]
459
460## Tasks
461
462- [ ] 1. Set up project structure
463 - [ ] 1.1 Create directory structure
464 - [ ] 1.2 Define core interfaces
465 - _Requirements: 1.1_
466
467- [ ] 2. Implement core functionality
468 - [ ] 2.1 Implement main logic
469 - _Requirements: 1.2_
470
471- [ ] 3. Checkpoint - Ensure all tests pass
472 - Ensure all tests pass, ask the user if questions arise.
473
474## Notes
475
476- Tasks marked with `*` are optional
477- Each task references specific requirements for traceability
478"#,
479 name = name
480 )
481 }
482
483 pub fn write_files(&self, files: &[GeneratedFile]) -> Result<Vec<PathBuf>> {
485 let mut written = Vec::new();
486
487 for file in files {
488 if let Some(parent) = file.path.parent() {
490 std::fs::create_dir_all(parent).map_err(DrivenError::Io)?;
491 }
492
493 if file.path.exists() && !file.overwrite {
495 continue;
496 }
497
498 std::fs::write(&file.path, &file.content).map_err(DrivenError::Io)?;
500
501 written.push(file.path.clone());
502 }
503
504 Ok(written)
505 }
506}
507
508impl Default for GeneratorBridge {
509 fn default() -> Self {
510 Self::new().unwrap_or_else(|_| Self {
511 template_paths: Vec::new(),
512 template_cache: HashMap::new(),
513 initialized: false,
514 })
515 }
516}
517
518#[derive(Debug, Default)]
522pub struct DrivenTemplateProvider {
523 templates: HashMap<String, DrivenTemplate>,
525}
526
527#[derive(Debug, Clone)]
529pub struct DrivenTemplate {
530 pub id: String,
532 pub content: String,
534 pub template_type: DrivenTemplateType,
536}
537
538#[derive(Debug, Clone, Copy, PartialEq, Eq)]
540pub enum DrivenTemplateType {
541 Rule,
543 Spec,
545 Hook,
547 Steering,
549}
550
551impl DrivenTemplateProvider {
552 pub fn new() -> Self {
554 Self::default()
555 }
556
557 pub fn register(&mut self, template: DrivenTemplate) {
559 self.templates.insert(template.id.clone(), template);
560 }
561
562 pub fn get(&self, id: &str) -> Option<&DrivenTemplate> {
564 self.templates.get(id)
565 }
566
567 pub fn list(&self) -> Vec<&DrivenTemplate> {
569 self.templates.values().collect()
570 }
571
572 pub fn register_builtins(&mut self) {
574 self.register(DrivenTemplate {
576 id: "driven-rule-basic".to_string(),
577 content: include_str!("templates/rule-basic.md").to_string(),
578 template_type: DrivenTemplateType::Rule,
579 });
580
581 self.register(DrivenTemplate {
583 id: "driven-spec-requirements".to_string(),
584 content: include_str!("templates/spec-requirements.md").to_string(),
585 template_type: DrivenTemplateType::Spec,
586 });
587
588 self.register(DrivenTemplate {
590 id: "driven-hook-file-save".to_string(),
591 content: include_str!("templates/hook-file-save.toml").to_string(),
592 template_type: DrivenTemplateType::Hook,
593 });
594
595 self.register(DrivenTemplate {
597 id: "driven-steering-always".to_string(),
598 content: include_str!("templates/steering-always.md").to_string(),
599 template_type: DrivenTemplateType::Steering,
600 });
601 }
602}
603
604#[cfg(test)]
605mod tests {
606 use super::*;
607
608 #[test]
609 fn test_generate_params() {
610 let params = GenerateParams::new()
611 .set_string("name", "TestFeature")
612 .set_int("version", 1)
613 .set_bool("enabled", true);
614
615 assert_eq!(params.get_string("name"), Some("TestFeature"));
616 assert_eq!(params.get_int("version"), Some(1));
617 assert_eq!(params.get_bool("enabled"), Some(true));
618 }
619
620 #[test]
621 fn test_params_to_string_map() {
622 let params = GenerateParams::new()
623 .set_string("name", "Test")
624 .set_int("count", 42)
625 .set_bool("flag", true);
626
627 let map = params.to_string_map();
628 assert_eq!(map.get("name"), Some(&"Test".to_string()));
629 assert_eq!(map.get("count"), Some(&"42".to_string()));
630 assert_eq!(map.get("flag"), Some(&"true".to_string()));
631 }
632
633 #[test]
634 fn test_generator_bridge_creation() {
635 let bridge = GeneratorBridge::new();
636 assert!(bridge.is_ok());
637 }
638
639 #[test]
640 fn test_infer_category() {
641 let bridge = GeneratorBridge::new().unwrap();
642
643 assert_eq!(
644 bridge.infer_category("cursor-rules"),
645 TemplateCategory::Rule
646 );
647 assert_eq!(
648 bridge.infer_category("spec-requirements"),
649 TemplateCategory::Spec
650 );
651 assert_eq!(
652 bridge.infer_category("project-scaffold"),
653 TemplateCategory::Scaffold
654 );
655 assert_eq!(
656 bridge.infer_category("react-component"),
657 TemplateCategory::Component
658 );
659 assert_eq!(
660 bridge.infer_category("other-template"),
661 TemplateCategory::Other
662 );
663 }
664
665 #[test]
666 fn test_spec_scaffold_generation() {
667 let bridge = GeneratorBridge::new().unwrap();
668 let params = GenerateParams::new().set_string("name", "Test Feature");
669
670 let files = bridge.generate_spec_scaffold("001", ¶ms).unwrap();
671
672 assert_eq!(files.len(), 3);
673 assert!(
674 files
675 .iter()
676 .any(|f| f.path.to_string_lossy().contains("requirements.md"))
677 );
678 assert!(
679 files
680 .iter()
681 .any(|f| f.path.to_string_lossy().contains("design.md"))
682 );
683 assert!(
684 files
685 .iter()
686 .any(|f| f.path.to_string_lossy().contains("tasks.md"))
687 );
688 }
689
690 #[test]
691 fn test_driven_template_provider() {
692 let mut provider = DrivenTemplateProvider::new();
693
694 provider.register(DrivenTemplate {
695 id: "test-template".to_string(),
696 content: "Test content".to_string(),
697 template_type: DrivenTemplateType::Rule,
698 });
699
700 assert!(provider.get("test-template").is_some());
701 assert_eq!(provider.list().len(), 1);
702 }
703}