use crate::{DrivenError, Result, UnifiedRule};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
#[derive(Debug)]
pub struct GeneratorBridge {
template_paths: Vec<PathBuf>,
template_cache: HashMap<String, TemplateInfo>,
initialized: bool,
}
#[derive(Debug, Clone)]
pub struct TemplateInfo {
pub id: String,
pub name: String,
pub description: String,
pub category: TemplateCategory,
pub parameters: Vec<ParameterInfo>,
pub output_pattern: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TemplateCategory {
Rule,
Spec,
Scaffold,
Component,
Other,
}
#[derive(Debug, Clone)]
pub struct ParameterInfo {
pub name: String,
pub description: String,
pub required: bool,
pub default: Option<String>,
}
#[derive(Debug, Clone)]
pub struct GeneratedFile {
pub path: PathBuf,
pub content: String,
pub overwrite: bool,
}
#[derive(Debug, Clone, Default)]
pub struct GenerateParams {
params: HashMap<String, ParamValue>,
}
#[derive(Debug, Clone)]
pub enum ParamValue {
String(String),
Integer(i64),
Boolean(bool),
Array(Vec<String>),
}
impl GenerateParams {
pub fn new() -> Self {
Self::default()
}
pub fn set_string(mut self, key: &str, value: impl Into<String>) -> Self {
self.params
.insert(key.to_string(), ParamValue::String(value.into()));
self
}
pub fn set_int(mut self, key: &str, value: i64) -> Self {
self.params
.insert(key.to_string(), ParamValue::Integer(value));
self
}
pub fn set_bool(mut self, key: &str, value: bool) -> Self {
self.params
.insert(key.to_string(), ParamValue::Boolean(value));
self
}
pub fn set_array(mut self, key: &str, value: Vec<String>) -> Self {
self.params
.insert(key.to_string(), ParamValue::Array(value));
self
}
pub fn get(&self, key: &str) -> Option<&ParamValue> {
self.params.get(key)
}
pub fn get_string(&self, key: &str) -> Option<&str> {
match self.params.get(key) {
Some(ParamValue::String(s)) => Some(s),
_ => None,
}
}
pub fn get_int(&self, key: &str) -> Option<i64> {
match self.params.get(key) {
Some(ParamValue::Integer(i)) => Some(*i),
_ => None,
}
}
pub fn get_bool(&self, key: &str) -> Option<bool> {
match self.params.get(key) {
Some(ParamValue::Boolean(b)) => Some(*b),
_ => None,
}
}
pub fn to_string_map(&self) -> HashMap<String, String> {
self.params
.iter()
.map(|(k, v)| {
let value = match v {
ParamValue::String(s) => s.clone(),
ParamValue::Integer(i) => i.to_string(),
ParamValue::Boolean(b) => b.to_string(),
ParamValue::Array(a) => a.join(", "),
};
(k.clone(), value)
})
.collect()
}
}
impl GeneratorBridge {
pub fn new() -> Result<Self> {
let template_paths = vec![
PathBuf::from(".driven/templates"),
PathBuf::from(".dx/templates"),
];
Ok(Self {
template_paths,
template_cache: HashMap::new(),
initialized: false,
})
}
pub fn with_paths(paths: Vec<PathBuf>) -> Result<Self> {
Ok(Self {
template_paths: paths,
template_cache: HashMap::new(),
initialized: false,
})
}
pub fn add_path(&mut self, path: impl AsRef<Path>) {
self.template_paths.push(path.as_ref().to_path_buf());
}
pub fn initialize(&mut self) -> Result<()> {
self.template_cache.clear();
let paths: Vec<PathBuf> = self.template_paths.clone();
for path in &paths {
if path.exists() {
self.scan_templates(path)?;
}
}
self.initialized = true;
Ok(())
}
fn scan_templates(&mut self, dir: &Path) -> Result<()> {
if !dir.is_dir() {
return Ok(());
}
for entry in std::fs::read_dir(dir).map_err(DrivenError::Io)? {
let entry = entry.map_err(DrivenError::Io)?;
let path = entry.path();
if path.is_file() {
if let Some(ext) = path.extension() {
if ext == "dxt" || ext == "hbs" || ext == "template" {
if let Ok(info) = self.parse_template_info(&path) {
self.template_cache.insert(info.id.clone(), info);
}
}
}
} else if path.is_dir() {
self.scan_templates(&path)?;
}
}
Ok(())
}
fn parse_template_info(&self, path: &Path) -> Result<TemplateInfo> {
let id = path
.file_stem()
.and_then(|s| s.to_str())
.ok_or_else(|| DrivenError::Template("Invalid template filename".to_string()))?
.to_string();
Ok(TemplateInfo {
id: id.clone(),
name: id.replace(['-', '_'], " "),
description: format!("Template from {}", path.display()),
category: self.infer_category(&id),
parameters: Vec::new(),
output_pattern: String::new(),
})
}
fn infer_category(&self, id: &str) -> TemplateCategory {
if id.contains("rule") || id.contains("cursor") || id.contains("copilot") {
TemplateCategory::Rule
} else if id.contains("spec") || id.contains("requirement") || id.contains("design") {
TemplateCategory::Spec
} else if id.contains("scaffold") || id.contains("project") {
TemplateCategory::Scaffold
} else if id.contains("component") || id.contains("model") {
TemplateCategory::Component
} else {
TemplateCategory::Other
}
}
pub fn list_templates(&self) -> Vec<&TemplateInfo> {
self.template_cache.values().collect()
}
pub fn list_templates_by_category(&self, category: TemplateCategory) -> Vec<&TemplateInfo> {
self.template_cache
.values()
.filter(|t| t.category == category)
.collect()
}
pub fn get_template(&self, id: &str) -> Option<&TemplateInfo> {
self.template_cache.get(id)
}
pub fn generate_rules(
&self,
template_id: &str,
params: &GenerateParams,
) -> Result<Vec<UnifiedRule>> {
let _template = self
.get_template(template_id)
.ok_or_else(|| DrivenError::TemplateNotFound(template_id.to_string()))?;
let _params_map = params.to_string_map();
Ok(Vec::new())
}
pub fn generate_spec_scaffold(
&self,
spec_id: &str,
params: &GenerateParams,
) -> Result<Vec<GeneratedFile>> {
let mut files = Vec::new();
let spec_name = params.get_string("name").unwrap_or(spec_id);
files.push(GeneratedFile {
path: PathBuf::from(format!(".driven/specs/{}/requirements.md", spec_id)),
content: self.generate_requirements_template(spec_name, params),
overwrite: false,
});
files.push(GeneratedFile {
path: PathBuf::from(format!(".driven/specs/{}/design.md", spec_id)),
content: self.generate_design_template(spec_name, params),
overwrite: false,
});
files.push(GeneratedFile {
path: PathBuf::from(format!(".driven/specs/{}/tasks.md", spec_id)),
content: self.generate_tasks_template(spec_name, params),
overwrite: false,
});
Ok(files)
}
fn generate_requirements_template(&self, name: &str, _params: &GenerateParams) -> String {
format!(
r#"# Requirements Document
## Introduction
{name}
## Glossary
- **System**: [Definition]
## Requirements
### Requirement 1
**User Story:** As a [role], I want [feature], so that [benefit]
#### Acceptance Criteria
1. WHEN [event], THE [System] SHALL [response]
"#,
name = name
)
}
fn generate_design_template(&self, name: &str, _params: &GenerateParams) -> String {
format!(
r#"# Design Document: {name}
## Overview
[Summary of the design]
## Architecture
[Architecture description]
## Components and Interfaces
[Component descriptions]
## Data Models
[Data model definitions]
## Correctness Properties
[Properties to validate]
## Error Handling
[Error handling strategy]
## Testing Strategy
[Testing approach]
"#,
name = name
)
}
fn generate_tasks_template(&self, name: &str, _params: &GenerateParams) -> String {
format!(
r#"# Implementation Plan: {name}
## Overview
[Implementation approach]
## Tasks
- [ ] 1. Set up project structure
- [ ] 1.1 Create directory structure
- [ ] 1.2 Define core interfaces
- _Requirements: 1.1_
- [ ] 2. Implement core functionality
- [ ] 2.1 Implement main logic
- _Requirements: 1.2_
- [ ] 3. Checkpoint - Ensure all tests pass
- Ensure all tests pass, ask the user if questions arise.
## Notes
- Tasks marked with `*` are optional
- Each task references specific requirements for traceability
"#,
name = name
)
}
pub fn write_files(&self, files: &[GeneratedFile]) -> Result<Vec<PathBuf>> {
let mut written = Vec::new();
for file in files {
if let Some(parent) = file.path.parent() {
std::fs::create_dir_all(parent).map_err(DrivenError::Io)?;
}
if file.path.exists() && !file.overwrite {
continue;
}
std::fs::write(&file.path, &file.content).map_err(DrivenError::Io)?;
written.push(file.path.clone());
}
Ok(written)
}
}
impl Default for GeneratorBridge {
fn default() -> Self {
Self::new().unwrap_or_else(|_| Self {
template_paths: Vec::new(),
template_cache: HashMap::new(),
initialized: false,
})
}
}
#[derive(Debug, Default)]
pub struct DrivenTemplateProvider {
templates: HashMap<String, DrivenTemplate>,
}
#[derive(Debug, Clone)]
pub struct DrivenTemplate {
pub id: String,
pub content: String,
pub template_type: DrivenTemplateType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DrivenTemplateType {
Rule,
Spec,
Hook,
Steering,
}
impl DrivenTemplateProvider {
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, template: DrivenTemplate) {
self.templates.insert(template.id.clone(), template);
}
pub fn get(&self, id: &str) -> Option<&DrivenTemplate> {
self.templates.get(id)
}
pub fn list(&self) -> Vec<&DrivenTemplate> {
self.templates.values().collect()
}
pub fn register_builtins(&mut self) {
self.register(DrivenTemplate {
id: "driven-rule-basic".to_string(),
content: include_str!("templates/rule-basic.md").to_string(),
template_type: DrivenTemplateType::Rule,
});
self.register(DrivenTemplate {
id: "driven-spec-requirements".to_string(),
content: include_str!("templates/spec-requirements.md").to_string(),
template_type: DrivenTemplateType::Spec,
});
self.register(DrivenTemplate {
id: "driven-hook-file-save".to_string(),
content: include_str!("templates/hook-file-save.toml").to_string(),
template_type: DrivenTemplateType::Hook,
});
self.register(DrivenTemplate {
id: "driven-steering-always".to_string(),
content: include_str!("templates/steering-always.md").to_string(),
template_type: DrivenTemplateType::Steering,
});
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_params() {
let params = GenerateParams::new()
.set_string("name", "TestFeature")
.set_int("version", 1)
.set_bool("enabled", true);
assert_eq!(params.get_string("name"), Some("TestFeature"));
assert_eq!(params.get_int("version"), Some(1));
assert_eq!(params.get_bool("enabled"), Some(true));
}
#[test]
fn test_params_to_string_map() {
let params = GenerateParams::new()
.set_string("name", "Test")
.set_int("count", 42)
.set_bool("flag", true);
let map = params.to_string_map();
assert_eq!(map.get("name"), Some(&"Test".to_string()));
assert_eq!(map.get("count"), Some(&"42".to_string()));
assert_eq!(map.get("flag"), Some(&"true".to_string()));
}
#[test]
fn test_generator_bridge_creation() {
let bridge = GeneratorBridge::new();
assert!(bridge.is_ok());
}
#[test]
fn test_infer_category() {
let bridge = GeneratorBridge::new().unwrap();
assert_eq!(
bridge.infer_category("cursor-rules"),
TemplateCategory::Rule
);
assert_eq!(
bridge.infer_category("spec-requirements"),
TemplateCategory::Spec
);
assert_eq!(
bridge.infer_category("project-scaffold"),
TemplateCategory::Scaffold
);
assert_eq!(
bridge.infer_category("react-component"),
TemplateCategory::Component
);
assert_eq!(
bridge.infer_category("other-template"),
TemplateCategory::Other
);
}
#[test]
fn test_spec_scaffold_generation() {
let bridge = GeneratorBridge::new().unwrap();
let params = GenerateParams::new().set_string("name", "Test Feature");
let files = bridge.generate_spec_scaffold("001", ¶ms).unwrap();
assert_eq!(files.len(), 3);
assert!(
files
.iter()
.any(|f| f.path.to_string_lossy().contains("requirements.md"))
);
assert!(
files
.iter()
.any(|f| f.path.to_string_lossy().contains("design.md"))
);
assert!(
files
.iter()
.any(|f| f.path.to_string_lossy().contains("tasks.md"))
);
}
#[test]
fn test_driven_template_provider() {
let mut provider = DrivenTemplateProvider::new();
provider.register(DrivenTemplate {
id: "test-template".to_string(),
content: "Test content".to_string(),
template_type: DrivenTemplateType::Rule,
});
assert!(provider.get("test-template").is_some());
assert_eq!(provider.list().len(), 1);
}
}