capsula_registry/
lib.rs

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
39/// Type alias for a hook creator function
40type HookCreator<P> = fn(&Value, &Path) -> CapsulaResult<Box<dyn HookErased<P>>>;
41
42/// Hook registry that stores hook creators
43pub struct HookRegistry<P: PhaseMarker> {
44    creators: HashMap<&'static str, HookCreator<P>>,
45}
46
47impl<P: PhaseMarker> HookRegistry<P> {
48    /// Create a new empty registry
49    pub fn new() -> Self {
50        Self {
51            creators: HashMap::new(),
52        }
53    }
54
55    /// Register a hook type by providing its type parameter
56    ///
57    /// This method uses the Hook trait's associated constant ID and from_config method
58    /// to register the hook type in the registry.
59    ///
60    /// # Example
61    /// ```ignore
62    /// let mut registry = HookRegistry::<PreRun>::new();
63    /// registry.register::<CwdHook>()?;
64    /// ```
65    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        // Create a function pointer that calls H::from_config and boxes the result
75        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    /// Create a hook from type name and configuration
85    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            // Enhance error message with hook type information
103            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    /// Get list of registered hook types
115    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
129/// Builder for setting up a registry with standard hook types
130pub 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    /// Register a hook type in the registry
142    ///
143    /// # Example
144    /// ```ignore
145    /// RegistryBuilder::<PreRun>::new()
146    ///     .with_hook::<CwdHook>()?
147    ///     .build()
148    /// ```
149    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
171/// Create a standard registry with all built-in hook types for pre-run phase
172pub 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
191/// Create a standard registry with all built-in hook types for post-run phase
192pub 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}