orcs_hook/action.rs
1//! Hook action — return type from hook handlers.
2//!
3//! Determines what the runtime does after a hook executes.
4//! `Default` is intentionally NOT implemented to prevent
5//! accidental context destruction.
6
7use crate::HookContext;
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11/// What the hook wants the runtime to do after execution.
12///
13/// # Pre-hooks (can modify/abort)
14///
15/// - `Continue` — pass (possibly modified) context downstream
16/// - `Skip` — skip the operation, return the given value as result
17/// - `Abort` — abort the operation with an error
18///
19/// # Post-hooks (observe/replace)
20///
21/// - `Continue` — pass context to next post-hook
22/// - `Replace` — replace the result payload, continue chain
23///
24/// # Intentional omission
25///
26/// `Default` is NOT implemented. `Continue(HookContext::empty())` would
27/// destroy the original context silently. Handlers must return explicitly.
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub enum HookAction {
30 /// Continue with (possibly modified) context.
31 Continue(Box<HookContext>),
32
33 /// Skip the operation entirely (pre-hooks only).
34 /// The `Value` is returned as the operation's result.
35 Skip(Value),
36
37 /// Abort the operation with an error (pre-hooks only).
38 Abort {
39 /// Reason for aborting.
40 reason: String,
41 },
42
43 /// Replace the result with a different value (post-hooks only).
44 /// In a post-hook chain, the replaced value becomes the new payload
45 /// for subsequent hooks.
46 Replace(Value),
47}
48
49impl HookAction {
50 /// Returns `true` if this is a `Continue` variant.
51 #[must_use]
52 pub fn is_continue(&self) -> bool {
53 matches!(self, Self::Continue(_))
54 }
55
56 /// Returns `true` if this is a `Skip` variant.
57 #[must_use]
58 pub fn is_skip(&self) -> bool {
59 matches!(self, Self::Skip(_))
60 }
61
62 /// Returns `true` if this is an `Abort` variant.
63 #[must_use]
64 pub fn is_abort(&self) -> bool {
65 matches!(self, Self::Abort { .. })
66 }
67
68 /// Returns `true` if this is a `Replace` variant.
69 #[must_use]
70 pub fn is_replace(&self) -> bool {
71 matches!(self, Self::Replace(_))
72 }
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78 use crate::HookPoint;
79 use orcs_types::{ChannelId, ComponentId, Principal};
80 use serde_json::json;
81
82 fn dummy_ctx() -> HookContext {
83 HookContext::new(
84 HookPoint::RequestPreDispatch,
85 ComponentId::builtin("test"),
86 ChannelId::new(),
87 Principal::System,
88 0,
89 json!(null),
90 )
91 }
92
93 #[test]
94 fn continue_variant() {
95 let action = HookAction::Continue(Box::new(dummy_ctx()));
96 assert!(action.is_continue());
97 assert!(!action.is_skip());
98 assert!(!action.is_abort());
99 assert!(!action.is_replace());
100 }
101
102 #[test]
103 fn skip_variant() {
104 let action = HookAction::Skip(json!({"skipped": true}));
105 assert!(action.is_skip());
106 assert!(!action.is_continue());
107 }
108
109 #[test]
110 fn abort_variant() {
111 let action = HookAction::Abort {
112 reason: "policy".into(),
113 };
114 assert!(action.is_abort());
115 assert!(!action.is_continue());
116 }
117
118 #[test]
119 fn replace_variant() {
120 let action = HookAction::Replace(json!({"new": "value"}));
121 assert!(action.is_replace());
122 assert!(!action.is_continue());
123 }
124}