Skip to main content

driven/emitter/
mod.rs

1//! Target Format Generators
2//!
3//! Emits unified rules to editor-specific formats.
4
5mod claude;
6mod copilot;
7mod cursor;
8mod generic;
9mod windsurf;
10
11pub use claude::ClaudeEmitter;
12pub use copilot::CopilotEmitter;
13pub use cursor::CursorEmitter;
14pub use generic::GenericEmitter;
15pub use windsurf::WindsurfEmitter;
16
17use crate::{DrivenError, Editor, Result, parser::UnifiedRule};
18use std::path::Path;
19
20/// Trait for emitting rules to a specific format
21pub trait RuleEmitter {
22    /// Emit rules to a file
23    fn emit_file(&self, rules: &[UnifiedRule], path: &Path) -> Result<()>;
24
25    /// Emit rules to a string
26    fn emit_string(&self, rules: &[UnifiedRule]) -> Result<String>;
27
28    /// Get the editor this emitter targets
29    fn editor(&self) -> Editor;
30}
31
32/// Universal emitter that auto-selects format
33pub struct Emitter {
34    inner: Box<dyn RuleEmitter + Send + Sync>,
35}
36
37impl std::fmt::Debug for Emitter {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        f.debug_struct("Emitter")
40            .field("editor", &self.inner.editor())
41            .finish()
42    }
43}
44
45impl Emitter {
46    /// Create an emitter for a specific editor
47    pub fn for_editor(editor: Editor) -> Self {
48        let inner: Box<dyn RuleEmitter + Send + Sync> = match editor {
49            Editor::Cursor => Box::new(CursorEmitter::new()),
50            Editor::Copilot => Box::new(CopilotEmitter::new()),
51            Editor::Windsurf => Box::new(WindsurfEmitter::new()),
52            Editor::Claude => Box::new(ClaudeEmitter::new()),
53            Editor::Aider => Box::new(GenericEmitter::new()),
54            Editor::Cline => Box::new(GenericEmitter::new()),
55        };
56        Self { inner }
57    }
58
59    /// Emit rules to a file
60    pub fn emit(&self, rules: &[UnifiedRule], path: impl AsRef<Path>) -> Result<()> {
61        self.inner.emit_file(rules, path.as_ref())
62    }
63
64    /// Emit rules to a string
65    pub fn emit_string(&self, rules: &[UnifiedRule]) -> Result<String> {
66        self.inner.emit_string(rules)
67    }
68
69    /// Get the editor this emitter targets
70    pub fn editor(&self) -> Editor {
71        self.inner.editor()
72    }
73}
74
75/// Helper to ensure parent directory exists
76pub(crate) fn ensure_parent_dir(path: &Path) -> Result<()> {
77    if let Some(parent) = path.parent() {
78        if !parent.exists() {
79            std::fs::create_dir_all(parent).map_err(|e| {
80                DrivenError::Io(std::io::Error::other(format!(
81                    "Failed to create directory {}: {}",
82                    parent.display(),
83                    e
84                )))
85            })?;
86        }
87    }
88    Ok(())
89}
90
91/// Format a section heading for markdown
92pub(crate) fn format_heading(level: usize, text: &str) -> String {
93    let hashes = "#".repeat(level.min(6));
94    format!("{} {}\n\n", hashes, text)
95}
96
97/// Format bullet points for markdown
98pub(crate) fn format_bullet_list(items: &[String]) -> String {
99    items.iter().map(|item| format!("- {}\n", item)).collect()
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn test_format_heading() {
108        assert_eq!(format_heading(1, "Test"), "# Test\n\n");
109        assert_eq!(format_heading(2, "Test"), "## Test\n\n");
110    }
111
112    #[test]
113    fn test_format_bullet_list() {
114        let items = vec!["Item 1".to_string(), "Item 2".to_string()];
115        let result = format_bullet_list(&items);
116        assert!(result.contains("- Item 1"));
117        assert!(result.contains("- Item 2"));
118    }
119}