1use crate::error::{HooksError, Result};
8use crate::types::{Action, CommandAction, Hook};
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct HookTemplate {
36 pub name: String,
38
39 pub description: Option<String>,
41
42 pub event: String,
44
45 pub action: Action,
47
48 pub parameters: Vec<TemplateParameter>,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct TemplateParameter {
57 pub name: String,
59
60 pub description: String,
62
63 pub required: bool,
65
66 pub default_value: Option<String>,
68}
69
70pub struct TemplateManager;
72
73impl TemplateManager {
74 pub fn get_builtin_templates() -> HashMap<String, HookTemplate> {
78 let mut templates = HashMap::new();
79
80 templates.insert("file_save".to_string(), Self::create_file_save_template());
82
83 templates.insert("git_hooks".to_string(), Self::create_git_hooks_template());
85
86 templates.insert(
88 "build_hooks".to_string(),
89 Self::create_build_hooks_template(),
90 );
91
92 templates
93 }
94
95 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 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 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 pub fn instantiate_template(
177 template: &HookTemplate,
178 hook_id: &str,
179 hook_name: &str,
180 parameters: &HashMap<String, String>,
181 ) -> Result<Hook> {
182 for param in &template.parameters {
184 if param.required
185 && !parameters.contains_key(¶m.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 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 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 other => Ok(other.clone()),
233 }
234 }
235
236 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 ¶ms,
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 ¶ms,
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, ¶ms);
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, ¶ms);
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, ¶ms);
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, ¶ms).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}