Skip to main content

yeti_types/
hooks.rs

1//! Resource execution hooks — pre/post/failure hooks for request dispatch.
2//!
3//! Hook types and configuration are defined here in yeti-types so they can be
4//! referenced by both yeti-sdk (which runs hooks) and yeti-router (which
5//! dispatches requests).
6
7use serde::{Deserialize, Serialize};
8
9/// The phase at which a hook fires relative to resource dispatch.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum HookEvent {
12    /// Fires before the resource handler. Can deny the request.
13    PreRequest,
14    /// Fires after a successful resource handler invocation.
15    PostRequest,
16    /// Fires after a failed resource handler invocation (4xx/5xx).
17    PostRequestFailure,
18}
19
20impl HookEvent {
21    /// Returns the environment variable value for `HOOK_EVENT`.
22    #[must_use]
23    pub const fn as_str(&self) -> &'static str {
24        match self {
25            Self::PreRequest => "pre_request",
26            Self::PostRequest => "post_request",
27            Self::PostRequestFailure => "post_request_failure",
28        }
29    }
30}
31
32/// Result of running a hook command.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct HookRunResult {
35    /// Whether the hook denied the request (exit code 2).
36    pub denied: bool,
37    /// Whether the hook itself failed (non-zero, non-2 exit code or timeout).
38    pub failed: bool,
39    /// Messages captured from the hook's stdout.
40    pub messages: Vec<String>,
41}
42
43impl HookRunResult {
44    /// Hook allowed the request (no messages).
45    #[must_use]
46    pub const fn allow() -> Self {
47        Self {
48            denied: false,
49            failed: false,
50            messages: Vec::new(),
51        }
52    }
53
54    /// Hook allowed the request with informational messages.
55    #[must_use]
56    pub const fn allow_with(messages: Vec<String>) -> Self {
57        Self {
58            denied: false,
59            failed: false,
60            messages,
61        }
62    }
63
64    /// Whether the hook denied the request.
65    #[must_use]
66    pub const fn is_denied(&self) -> bool {
67        self.denied
68    }
69
70    /// Whether the hook itself failed (script error, timeout, etc.).
71    #[must_use]
72    pub const fn is_failed(&self) -> bool {
73        self.failed
74    }
75}
76
77/// Hook configuration declared under `[package.metadata.app.hooks]` in
78/// the app's `Cargo.toml`.
79///
80/// # Example
81/// ```toml
82/// [package.metadata.app.hooks]
83/// pre_request = ["./scripts/rate_limit.sh"]
84/// post_request = ["./scripts/log_request.sh"]
85/// post_request_failure = ["./scripts/alert_on_failure.sh"]
86/// ```
87#[derive(Debug, Clone, Default, Deserialize, Serialize)]
88pub struct HookConfig {
89    /// Commands to run before the resource handler.
90    /// Exit 0 = allow, exit 2 = deny, other = hook failure.
91    #[serde(default)]
92    pub pre_request: Vec<String>,
93
94    /// Commands to run after a successful resource handler invocation.
95    /// Fire-and-forget; results do not affect the response.
96    #[serde(default)]
97    pub post_request: Vec<String>,
98
99    /// Commands to run after a failed resource handler invocation.
100    /// Fire-and-forget; results do not affect the response.
101    #[serde(default)]
102    pub post_request_failure: Vec<String>,
103}
104
105impl HookConfig {
106    /// Returns true if no hooks are configured in any phase.
107    #[must_use]
108    pub const fn is_empty(&self) -> bool {
109        self.pre_request.is_empty()
110            && self.post_request.is_empty()
111            && self.post_request_failure.is_empty()
112    }
113}