ricecoder_hooks/config/
templates.rs

1//! Hook templates and built-in patterns
2//!
3//! Provides built-in hook templates for common patterns like file save hooks,
4//! git hooks, and build hooks. Templates can be instantiated with parameters
5//! to create concrete hooks.
6
7use crate::error::{HooksError, Result};
8use crate::types::{Action, CommandAction, Hook};
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11
12/// Hook template for creating hooks with parameters
13///
14/// Templates define reusable hook patterns that can be instantiated with
15/// specific parameters. This allows users to create hooks without writing
16/// full hook configurations.
17///
18/// # Examples
19///
20/// ```ignore
21/// let template = HookTemplate {
22///     name: "Format on Save".to_string(),
23///     description: Some("Format code when file is saved".to_string()),
24///     event: "file_saved".to_string(),
25///     action: Action::Command(CommandAction {
26///         command: "prettier".to_string(),
27///         args: vec!["--write".to_string(), "{{file_path}}".to_string()],
28///         timeout_ms: Some(5000),
29///         capture_output: true,
30///     }),
31///     parameters: vec![],
32/// };
33/// ```
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct HookTemplate {
36    /// Template name
37    pub name: String,
38
39    /// Template description
40    pub description: Option<String>,
41
42    /// Event that triggers hooks from this template
43    pub event: String,
44
45    /// Action template (may contain parameter placeholders)
46    pub action: Action,
47
48    /// Template parameters
49    pub parameters: Vec<TemplateParameter>,
50}
51
52/// Template parameter definition
53///
54/// Defines a parameter that can be customized when instantiating a template.
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct TemplateParameter {
57    /// Parameter name
58    pub name: String,
59
60    /// Parameter description
61    pub description: String,
62
63    /// Whether the parameter is required
64    pub required: bool,
65
66    /// Default value if not provided
67    pub default_value: Option<String>,
68}
69
70/// Template manager for creating and managing hook templates
71pub struct TemplateManager;
72
73impl TemplateManager {
74    /// Get all built-in templates
75    ///
76    /// Returns a map of template names to template definitions.
77    pub fn get_builtin_templates() -> HashMap<String, HookTemplate> {
78        let mut templates = HashMap::new();
79
80        // File save template
81        templates.insert("file_save".to_string(), Self::create_file_save_template());
82
83        // Git hooks template
84        templates.insert("git_hooks".to_string(), Self::create_git_hooks_template());
85
86        // Build hooks template
87        templates.insert(
88            "build_hooks".to_string(),
89            Self::create_build_hooks_template(),
90        );
91
92        templates
93    }
94
95    /// Create file save template
96    ///
97    /// Template for running actions when files are saved.
98    fn create_file_save_template() -> HookTemplate {
99        HookTemplate {
100            name: "File Save Hook".to_string(),
101            description: Some("Run actions when files are saved".to_string()),
102            event: "file_saved".to_string(),
103            action: Action::Command(CommandAction {
104                command: "{{command}}".to_string(),
105                args: vec!["{{file_path}}".to_string()],
106                timeout_ms: Some(5000),
107                capture_output: true,
108            }),
109            parameters: vec![TemplateParameter {
110                name: "command".to_string(),
111                description: "Command to run on file save".to_string(),
112                required: true,
113                default_value: None,
114            }],
115        }
116    }
117
118    /// Create git hooks template
119    ///
120    /// Template for running actions on git events.
121    fn create_git_hooks_template() -> HookTemplate {
122        HookTemplate {
123            name: "Git Hooks".to_string(),
124            description: Some(
125                "Run actions on git events (pre-commit, post-commit, etc.)".to_string(),
126            ),
127            event: "git_event".to_string(),
128            action: Action::Command(CommandAction {
129                command: "{{command}}".to_string(),
130                args: vec![],
131                timeout_ms: Some(10000),
132                capture_output: true,
133            }),
134            parameters: vec![TemplateParameter {
135                name: "command".to_string(),
136                description: "Command to run on git event".to_string(),
137                required: true,
138                default_value: None,
139            }],
140        }
141    }
142
143    /// Create build hooks template
144    ///
145    /// Template for running actions on build events.
146    fn create_build_hooks_template() -> HookTemplate {
147        HookTemplate {
148            name: "Build Hooks".to_string(),
149            description: Some(
150                "Run actions on build events (pre-build, post-build, etc.)".to_string(),
151            ),
152            event: "build_event".to_string(),
153            action: Action::Command(CommandAction {
154                command: "{{command}}".to_string(),
155                args: vec![],
156                timeout_ms: Some(30000),
157                capture_output: true,
158            }),
159            parameters: vec![TemplateParameter {
160                name: "command".to_string(),
161                description: "Command to run on build event".to_string(),
162                required: true,
163                default_value: None,
164            }],
165        }
166    }
167
168    /// Instantiate a template with parameters
169    ///
170    /// Creates a concrete hook from a template by substituting parameters.
171    ///
172    /// # Errors
173    ///
174    /// Returns an error if required parameters are missing or if the template
175    /// cannot be instantiated.
176    pub fn instantiate_template(
177        template: &HookTemplate,
178        hook_id: &str,
179        hook_name: &str,
180        parameters: &HashMap<String, String>,
181    ) -> Result<Hook> {
182        // Validate required parameters
183        for param in &template.parameters {
184            if param.required
185                && !parameters.contains_key(&param.name)
186                && param.default_value.is_none()
187            {
188                return Err(HooksError::InvalidConfiguration(format!(
189                    "Required parameter '{}' not provided for template '{}'",
190                    param.name, template.name
191                )));
192            }
193        }
194
195        // Substitute parameters in action
196        let action = Self::substitute_action(&template.action, parameters)?;
197
198        Ok(Hook {
199            id: hook_id.to_string(),
200            name: hook_name.to_string(),
201            description: template.description.clone(),
202            event: template.event.clone(),
203            action,
204            enabled: true,
205            tags: vec!["template".to_string()],
206            metadata: serde_json::json!({
207                "template": template.name,
208            }),
209            condition: None,
210        })
211    }
212
213    /// Substitute parameters in an action
214    fn substitute_action(action: &Action, parameters: &HashMap<String, String>) -> Result<Action> {
215        match action {
216            Action::Command(cmd) => {
217                let command = Self::substitute_string(&cmd.command, parameters);
218                let args = cmd
219                    .args
220                    .iter()
221                    .map(|arg| Self::substitute_string(arg, parameters))
222                    .collect();
223
224                Ok(Action::Command(CommandAction {
225                    command,
226                    args,
227                    timeout_ms: cmd.timeout_ms,
228                    capture_output: cmd.capture_output,
229                }))
230            }
231            // For other action types, return as-is for now
232            other => Ok(other.clone()),
233        }
234    }
235
236    /// Substitute parameters in a string
237    ///
238    /// Replaces `{{parameter_name}}` with the corresponding parameter value.
239    fn substitute_string(template: &str, parameters: &HashMap<String, String>) -> String {
240        let mut result = template.to_string();
241
242        for (name, value) in parameters {
243            let placeholder = format!("{{{{{}}}}}", name);
244            result = result.replace(&placeholder, value);
245        }
246
247        result
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254
255    #[test]
256    fn test_get_builtin_templates() {
257        let templates = TemplateManager::get_builtin_templates();
258        assert!(templates.contains_key("file_save"));
259        assert!(templates.contains_key("git_hooks"));
260        assert!(templates.contains_key("build_hooks"));
261    }
262
263    #[test]
264    fn test_file_save_template() {
265        let templates = TemplateManager::get_builtin_templates();
266        let template = templates.get("file_save").expect("Should find template");
267        assert_eq!(template.name, "File Save Hook");
268        assert_eq!(template.event, "file_saved");
269        assert_eq!(template.parameters.len(), 1);
270        assert_eq!(template.parameters[0].name, "command");
271        assert!(template.parameters[0].required);
272    }
273
274    #[test]
275    fn test_git_hooks_template() {
276        let templates = TemplateManager::get_builtin_templates();
277        let template = templates.get("git_hooks").expect("Should find template");
278        assert_eq!(template.name, "Git Hooks");
279        assert_eq!(template.event, "git_event");
280    }
281
282    #[test]
283    fn test_build_hooks_template() {
284        let templates = TemplateManager::get_builtin_templates();
285        let template = templates.get("build_hooks").expect("Should find template");
286        assert_eq!(template.name, "Build Hooks");
287        assert_eq!(template.event, "build_event");
288    }
289
290    #[test]
291    fn test_instantiate_template_valid() {
292        let templates = TemplateManager::get_builtin_templates();
293        let template = templates.get("file_save").expect("Should find template");
294
295        let mut params = HashMap::new();
296        params.insert("command".to_string(), "prettier".to_string());
297
298        let hook = TemplateManager::instantiate_template(
299            template,
300            "format-on-save",
301            "Format on Save",
302            &params,
303        )
304        .expect("Should instantiate template");
305
306        assert_eq!(hook.id, "format-on-save");
307        assert_eq!(hook.name, "Format on Save");
308        assert_eq!(hook.event, "file_saved");
309        assert!(hook.enabled);
310    }
311
312    #[test]
313    fn test_instantiate_template_missing_required_parameter() {
314        let templates = TemplateManager::get_builtin_templates();
315        let template = templates.get("file_save").expect("Should find template");
316
317        let params = HashMap::new();
318
319        let result = TemplateManager::instantiate_template(
320            template,
321            "format-on-save",
322            "Format on Save",
323            &params,
324        );
325
326        assert!(result.is_err());
327    }
328
329    #[test]
330    fn test_substitute_string() {
331        let template = "prettier --write {{file_path}}";
332        let mut params = HashMap::new();
333        params.insert("file_path".to_string(), "/path/to/file.js".to_string());
334
335        let result = TemplateManager::substitute_string(template, &params);
336        assert_eq!(result, "prettier --write /path/to/file.js");
337    }
338
339    #[test]
340    fn test_substitute_string_multiple_parameters() {
341        let template = "{{command}} {{file_path}} {{format}}";
342        let mut params = HashMap::new();
343        params.insert("command".to_string(), "prettier".to_string());
344        params.insert("file_path".to_string(), "/path/to/file.js".to_string());
345        params.insert("format".to_string(), "json".to_string());
346
347        let result = TemplateManager::substitute_string(template, &params);
348        assert_eq!(result, "prettier /path/to/file.js json");
349    }
350
351    #[test]
352    fn test_substitute_string_no_parameters() {
353        let template = "echo hello";
354        let params = HashMap::new();
355
356        let result = TemplateManager::substitute_string(template, &params);
357        assert_eq!(result, "echo hello");
358    }
359
360    #[test]
361    fn test_substitute_action_command() {
362        let action = Action::Command(CommandAction {
363            command: "{{command}}".to_string(),
364            args: vec!["{{file_path}}".to_string()],
365            timeout_ms: Some(5000),
366            capture_output: true,
367        });
368
369        let mut params = HashMap::new();
370        params.insert("command".to_string(), "prettier".to_string());
371        params.insert("file_path".to_string(), "/path/to/file.js".to_string());
372
373        let result =
374            TemplateManager::substitute_action(&action, &params).expect("Should substitute");
375
376        match result {
377            Action::Command(cmd) => {
378                assert_eq!(cmd.command, "prettier");
379                assert_eq!(cmd.args[0], "/path/to/file.js");
380            }
381            _ => panic!("Expected command action"),
382        }
383    }
384}