1use capsula_core::error::{CapsulaError, CapsulaResult};
2use capsula_core::hook::{HookErased, PhaseMarker, PostRun, PreRun};
3use serde_json::Value;
4use std::collections::HashMap;
5use std::path::Path;
6use thiserror::Error;
7
8#[derive(Error, Debug)]
9pub enum RegistryError {
10 #[error("Unknown hook type: '{0}'")]
11 HookTypeNotFound(String),
12
13 #[error("Hook id '{0}' is already registered")]
14 AlreadyRegistered(String),
15
16 #[error("Failed to create hook '{hook}': {message}")]
17 HookCreationFailed { hook: String, message: String },
18}
19
20impl From<RegistryError> for CapsulaError {
21 fn from(err: RegistryError) -> Self {
22 match err {
23 RegistryError::HookTypeNotFound(ty) => CapsulaError::Configuration {
24 message: format!(
25 "Unknown hook id '{}'. Check your configuration file for typos.",
26 ty
27 ),
28 },
29 RegistryError::AlreadyRegistered(ty) => CapsulaError::Configuration {
30 message: format!("Hook id '{}' is already registered", ty),
31 },
32 RegistryError::HookCreationFailed { hook, message } => CapsulaError::Configuration {
33 message: format!("Failed to create '{}' hook: {}", hook, message),
34 },
35 }
36 }
37}
38
39type HookCreator<P> = fn(&Value, &Path) -> CapsulaResult<Box<dyn HookErased<P>>>;
41
42pub struct HookRegistry<P: PhaseMarker> {
44 creators: HashMap<&'static str, HookCreator<P>>,
45}
46
47impl<P: PhaseMarker> HookRegistry<P> {
48 pub fn new() -> Self {
50 Self {
51 creators: HashMap::new(),
52 }
53 }
54
55 pub fn register<H>(&mut self) -> Result<(), RegistryError>
66 where
67 H: capsula_core::hook::Hook<P> + 'static,
68 {
69 let id = H::ID;
70 if self.creators.contains_key(id) {
71 return Err(RegistryError::AlreadyRegistered(id.to_string()));
72 }
73
74 let creator: HookCreator<P> = |config, project_root| {
76 let hook = H::from_config(config, project_root)?;
77 Ok(Box::new(hook))
78 };
79
80 self.creators.insert(id, creator);
81 Ok(())
82 }
83
84 pub fn create_hook(
86 &self,
87 hook_id: &str,
88 config: &Value,
89 project_root: &Path,
90 ) -> CapsulaResult<Box<dyn HookErased<P>>> {
91 let creator = self.creators.get(hook_id).ok_or_else(|| {
92 let available = self.registered_types().join(", ");
93 CapsulaError::Configuration {
94 message: format!(
95 "Unknown hook id '{}'. Available types: {}",
96 hook_id, available
97 ),
98 }
99 })?;
100
101 creator(config, project_root).map_err(|e| {
102 match e {
104 CapsulaError::HookFailed { .. } => e,
105 _ => CapsulaError::HookFailed {
106 hook: hook_id.to_string(),
107 message: e.to_string(),
108 source: Box::new(e),
109 },
110 }
111 })
112 }
113
114 pub fn registered_types(&self) -> Vec<&'static str> {
116 self.creators.keys().copied().collect()
117 }
118}
119
120impl<P> Default for HookRegistry<P>
121where
122 P: PhaseMarker,
123{
124 fn default() -> Self {
125 Self::new()
126 }
127}
128
129pub struct RegistryBuilder<P: PhaseMarker> {
131 registry: HookRegistry<P>,
132}
133
134impl<P: PhaseMarker> RegistryBuilder<P> {
135 pub fn new() -> Self {
136 Self {
137 registry: HookRegistry::new(),
138 }
139 }
140
141 pub fn with_hook<H>(mut self) -> Result<Self, RegistryError>
150 where
151 H: capsula_core::hook::Hook<P> + 'static,
152 {
153 self.registry.register::<H>()?;
154 Ok(self)
155 }
156
157 pub fn build(self) -> HookRegistry<P> {
158 self.registry
159 }
160}
161
162impl<P> Default for RegistryBuilder<P>
163where
164 P: PhaseMarker,
165{
166 fn default() -> Self {
167 Self::new()
168 }
169}
170
171pub fn standard_pre_run_hook_registry() -> HookRegistry<PreRun> {
173 RegistryBuilder::new()
174 .with_hook::<capsula_capture_cwd::CwdHook>()
175 .unwrap_or_else(|e| panic!("Failed to register capture-cwd hook: {}", e))
176 .with_hook::<capsula_capture_git_repo::GitHook>()
177 .unwrap_or_else(|e| panic!("Failed to register capture-git-repo hook: {}", e))
178 .with_hook::<capsula_capture_file::FileHook>()
179 .unwrap_or_else(|e| panic!("Failed to register capture-file hook: {}", e))
180 .with_hook::<capsula_capture_env::EnvVarHook>()
181 .unwrap_or_else(|e| panic!("Failed to register capture-env hook: {}", e))
182 .with_hook::<capsula_capture_command::CommandHook>()
183 .unwrap_or_else(|e| panic!("Failed to register capture-command hook: {}", e))
184 .with_hook::<capsula_capture_machine::MachineHook>()
185 .unwrap_or_else(|e| panic!("Failed to register Machine hook: {}", e))
186 .with_hook::<capsula_notify_slack::SlackNotifyHook>()
187 .unwrap_or_else(|e| panic!("Failed to register notify-slack hook: {}", e))
188 .build()
189}
190
191pub fn standard_post_run_hook_registry() -> HookRegistry<PostRun> {
193 RegistryBuilder::new()
194 .with_hook::<capsula_capture_cwd::CwdHook>()
195 .unwrap_or_else(|e| panic!("Failed to register capture-cwd hook: {}", e))
196 .with_hook::<capsula_capture_git_repo::GitHook>()
197 .unwrap_or_else(|e| panic!("Failed to register capture-git-repo hook: {}", e))
198 .with_hook::<capsula_capture_file::FileHook>()
199 .unwrap_or_else(|e| panic!("Failed to register capture-file hook: {}", e))
200 .with_hook::<capsula_capture_env::EnvVarHook>()
201 .unwrap_or_else(|e| panic!("Failed to register capture-env hook: {}", e))
202 .with_hook::<capsula_capture_command::CommandHook>()
203 .unwrap_or_else(|e| panic!("Failed to register capture-command hook: {}", e))
204 .with_hook::<capsula_capture_machine::MachineHook>()
205 .unwrap_or_else(|e| panic!("Failed to register Machine hook: {}", e))
206 .with_hook::<capsula_notify_slack::SlackNotifyHook>()
207 .unwrap_or_else(|e| panic!("Failed to register notify-slack hook: {}", e))
208 .build()
209}