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}