Skip to main content

capsula_core/
hook.rs

1use crate::error::{CapsulaError, CapsulaResult};
2use crate::run::PreparedRun;
3use serde::{Deserialize, Serialize};
4use std::marker::PhantomData;
5use std::path::PathBuf;
6
7#[derive(Debug, Clone, Default)]
8pub struct PreRun;
9#[derive(Debug, Clone, Default)]
10pub struct PostRun;
11
12pub trait PhaseMarker {
13    /// Short name used in artifact directory names (e.g., "pre", "post").
14    fn phase_name() -> &'static str;
15}
16impl PhaseMarker for PreRun {
17    fn phase_name() -> &'static str {
18        "pre"
19    }
20}
21impl PhaseMarker for PostRun {
22    fn phase_name() -> &'static str {
23        "post"
24    }
25}
26
27#[derive(Debug, Clone)]
28pub struct RuntimeParams<P: PhaseMarker> {
29    phase_marker: PhantomData<P>,
30    /// Per-hook artifact directory, created by the orchestrator when the hook
31    /// requests one via [`Hook::needs_artifact_dir`].
32    pub artifact_dir: Option<PathBuf>,
33}
34
35impl<P: PhaseMarker> RuntimeParams<P> {
36    /// Create `RuntimeParams` with no artifact directory.
37    #[must_use]
38    pub const fn new() -> Self {
39        Self {
40            phase_marker: PhantomData,
41            artifact_dir: None,
42        }
43    }
44
45    /// Create `RuntimeParams` with an artifact directory set.
46    #[must_use]
47    pub const fn with_artifact_dir(artifact_dir: PathBuf) -> Self {
48        Self {
49            phase_marker: PhantomData,
50            artifact_dir: Some(artifact_dir),
51        }
52    }
53}
54
55impl<P: PhaseMarker> Default for RuntimeParams<P> {
56    fn default() -> Self {
57        Self::new()
58    }
59}
60
61pub trait Hook<P: PhaseMarker>: Send + Sync {
62    /// The unique identifier for this hook type (e.g., "capture-cwd", "notify-slack")
63    const ID: &'static str;
64
65    type Output: super::captured::Captured + 'static;
66    type Config: Serialize + for<'de> Deserialize<'de>;
67
68    /// Create a hook instance from JSON configuration
69    fn from_config(
70        config: &serde_json::Value,
71        project_root: &std::path::Path,
72    ) -> CapsulaResult<Self>
73    where
74        Self: Sized;
75
76    fn config(&self) -> &Self::Config;
77    fn run(&self, metadata: &PreparedRun, params: &RuntimeParams<P>)
78    -> CapsulaResult<Self::Output>;
79
80    /// Whether this hook needs a dedicated artifact directory.
81    ///
82    /// When `true`, the orchestrator creates a directory under the run directory
83    /// (e.g., `pre-0-capture-file/`) and passes it via [`RuntimeParams::artifact_dir`].
84    fn needs_artifact_dir(&self) -> bool {
85        false
86    }
87}
88
89/// Engine-facing trait (object-safe, heterogenous)
90pub trait HookErased<P: PhaseMarker>: Send + Sync {
91    fn id(&self) -> String;
92    fn config_as_json(&self) -> Result<serde_json::Value, serde_json::Error>;
93    fn run(
94        &self,
95        metadata: &PreparedRun,
96        params: &RuntimeParams<P>,
97    ) -> Result<Box<dyn super::captured::Captured>, CapsulaError>;
98    fn needs_artifact_dir(&self) -> bool;
99}
100
101impl<T, P> HookErased<P> for T
102where
103    T: Hook<P> + Send + Sync + 'static,
104    P: PhaseMarker,
105{
106    fn id(&self) -> String {
107        T::ID.to_string()
108    }
109
110    fn config_as_json(&self) -> Result<serde_json::Value, serde_json::Error> {
111        serde_json::to_value(<T as Hook<P>>::config(self))
112    }
113
114    fn run(
115        &self,
116        metadata: &PreparedRun,
117        params: &RuntimeParams<P>,
118    ) -> Result<Box<dyn super::captured::Captured>, CapsulaError> {
119        let out = <T as Hook<P>>::run(self, metadata, params)?;
120        Ok(Box::new(out))
121    }
122
123    fn needs_artifact_dir(&self) -> bool {
124        <T as Hook<P>>::needs_artifact_dir(self)
125    }
126}