Skip to main content

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}