ri/hooks/mod.rs
1//! Copyright © 2025-2026 Wenze Wei. All Rights Reserved.
2//!
3//! This file is part of Ri.
4//! The Ri project belongs to the Dunimd Team.
5//!
6//! Licensed under the Apache License, Version 2.0 (the "License");
7//! You may not use this file except in compliance with the License.
8//! You may obtain a copy of the License at
9//!
10//! http://www.apache.org/licenses/LICENSE-2.0
11//!
12//! Unless required by applicable law or agreed to in writing, software
13//! distributed under the License is distributed on an "AS IS" BASIS,
14//! WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15//! See the License for the specific language governing permissions and
16//! limitations under the License.
17
18#![allow(non_snake_case)]
19
20//! # Hooks System
21//!
22//! This module provides an event bus system for Ri, enabling communication between components
23//! during various lifecycle events. It supports both synchronous and asynchronous module lifecycle
24//! phases, and allows for custom event handlers to be registered.
25//!
26//! ## Key Components
27//!
28//! - **RiHookKind**: Enum defining the different types of hooks
29//! - **RiModulePhase**: Enum defining the different module lifecycle phases
30//! - **RiHookEvent**: Struct representing a hook event
31//! - **RiHookBus**: Event bus for registering and emitting hooks
32//!
33//! ## Design Principles
34//!
35//! 1. **Event-Driven Architecture**: Uses an event bus pattern for loose coupling between components
36//! 2. **Lifecycle Support**: Covers all stages of module lifecycle, both synchronous and asynchronous
37//! 3. **Type Safety**: Uses enums for hook kinds and phases to ensure type safety
38//! 4. **Flexibility**: Allows registering multiple handlers for the same hook
39//! 5. **Contextual Information**: Events carry contextual information about the module and phase
40//! 6. **Error Propagation**: Hook handlers can return errors that propagate up the call stack
41//!
42//! ## Usage
43//!
44//! ```rust
45//! use ri::prelude::*;
46//!
47//! fn example() -> RiResult<()> {
48//! // Create a hook bus
49//! let mut hook_bus = RiHookBus::new();
50//!
51//! // Register a hook handler
52//! hook_bus.register(RiHookKind::Startup, "example.startup".to_string(), |ctx, event| {
53//! // Handle startup event
54//! Ok(())
55//! });
56//!
57//! // Create a service context (usually provided by the runtime)
58//! let ctx = RiServiceContext::new();
59//!
60//! // Emit a hook event
61//! hook_bus.emit(&RiHookKind::Startup, &ctx)?;
62//!
63//! Ok(())
64//! }
65
66use std::collections::HashMap as FxHashMap;
67
68use crate::core::{RiResult, RiServiceContext};
69
70// Type aliases for complex types
71/// Type alias for a hook handler function
72pub type RiHookHandler = Box<dyn Fn(&RiServiceContext, &RiHookEvent) -> RiResult<()> + Send + Sync>;
73
74/// Type alias for a hook handler entry (ID + handler)
75pub type RiHookHandlerEntry = (RiHookId, RiHookHandler);
76
77/// Type alias for a collection of hook handlers grouped by hook kind
78pub type RiHookHandlersMap = FxHashMap<RiHookKind, Vec<RiHookHandlerEntry>>;
79
80/// Hook kind definition.
81///
82/// This enum defines the different types of hooks that can be emitted during the application lifecycle.
83#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
84#[derive(Eq, Hash, PartialEq, Clone, Copy, Debug)]
85pub enum RiHookKind {
86 /// Emitted when the application starts up
87 Startup,
88 /// Emitted when the application shuts down
89 Shutdown,
90 /// Emitted before modules are initialized
91 BeforeModulesInit,
92 /// Emitted after modules are initialized
93 AfterModulesInit,
94 /// Emitted before modules are started
95 BeforeModulesStart,
96 /// Emitted after modules are started
97 AfterModulesStart,
98 /// Emitted before modules are shut down
99 BeforeModulesShutdown,
100 /// Emitted after modules are shut down
101 AfterModulesShutdown,
102 /// Emitted when configuration is reloaded
103 ConfigReload,
104}
105
106/// Module lifecycle phase definition.
107///
108/// This enum defines the different phases a module can go through during its lifecycle,
109/// including both synchronous and asynchronous phases.
110#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
111#[derive(Eq, Hash, PartialEq, Clone, Copy, Debug)]
112pub enum RiModulePhase {
113 /// Synchronous initialization phase
114 Init,
115 /// Synchronous phase before starting
116 BeforeStart,
117 /// Synchronous start phase
118 Start,
119 /// Synchronous phase after starting
120 AfterStart,
121 /// Synchronous phase before shutting down
122 BeforeShutdown,
123 /// Synchronous shutdown phase
124 Shutdown,
125 /// Synchronous phase after shutting down
126 AfterShutdown,
127 /// Asynchronous initialization phase
128 AsyncInit,
129 /// Asynchronous phase before starting
130 AsyncBeforeStart,
131 /// Asynchronous start phase
132 AsyncStart,
133 /// Asynchronous phase after starting
134 AsyncAfterStart,
135 /// Asynchronous phase before shutting down
136 AsyncBeforeShutdown,
137 /// Asynchronous shutdown phase
138 AsyncShutdown,
139 /// Asynchronous phase after shutting down
140 AsyncAfterShutdown,
141}
142
143impl RiModulePhase {
144 /// Returns the string representation of the module phase.
145 ///
146 /// # Returns
147 ///
148 /// A static string representing the module phase (e.g., "init", "start", "async_shutdown")
149 pub fn as_str(&self) -> &'static str {
150 match self {
151 RiModulePhase::Init => "init",
152 RiModulePhase::BeforeStart => "before_start",
153 RiModulePhase::Start => "start",
154 RiModulePhase::AfterStart => "after_start",
155 RiModulePhase::BeforeShutdown => "before_shutdown",
156 RiModulePhase::Shutdown => "shutdown",
157 RiModulePhase::AfterShutdown => "after_shutdown",
158 RiModulePhase::AsyncInit => "async_init",
159 RiModulePhase::AsyncBeforeStart => "async_before_start",
160 RiModulePhase::AsyncStart => "async_start",
161 RiModulePhase::AsyncAfterStart => "async_after_start",
162 RiModulePhase::AsyncBeforeShutdown => "async_before_shutdown",
163 RiModulePhase::AsyncShutdown => "async_shutdown",
164 RiModulePhase::AsyncAfterShutdown => "async_after_shutdown",
165 }
166 }
167}
168
169/// Hook event structure.
170///
171/// This struct represents an event that is emitted when a hook is triggered. It contains
172/// information about the hook kind, the module (if applicable), and the module phase (if applicable).
173#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
174#[derive(Clone, Debug)]
175pub struct RiHookEvent {
176 /// The kind of hook that was triggered
177 pub kind: RiHookKind,
178 /// The name of the module associated with the event (if any)
179 pub module: Option<String>,
180 /// The module phase associated with the event (if any)
181 pub phase: Option<RiModulePhase>,
182}
183
184impl RiHookEvent {
185 /// Creates a new hook event.
186 pub fn new(kind: RiHookKind, module: Option<String>, phase: Option<RiModulePhase>) -> Self {
187 Self { kind, module, phase }
188 }
189
190 /// Creates a config reload event.
191 pub fn config_reload(_path: String, _timestamp: chrono::DateTime<chrono::Utc>) -> Self {
192 Self {
193 kind: RiHookKind::ConfigReload,
194 module: Some("config_manager".to_string()),
195 phase: None,
196 }
197 }
198}
199
200/// Type alias for hook IDs.
201///
202/// Hook IDs are used to identify hook handlers and can be used for debugging and logging purposes.
203pub type RiHookId = String;
204
205/// Hook bus for registering and emitting hooks.
206///
207/// This struct manages the registration of hook handlers and the emission of hook events.
208/// It allows multiple handlers to be registered for the same hook kind.
209#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
210pub struct RiHookBus {
211 /// Internal storage for hook handlers, organized by hook kind
212 handlers: RiHookHandlersMap,
213}
214
215impl Default for RiHookBus {
216 fn default() -> Self {
217 Self::new()
218 }
219}
220
221impl RiHookBus {
222 /// Creates a new hook bus instance.
223 ///
224 /// Returns a new `RiHookBus` instance with no registered handlers.
225 pub fn new() -> Self {
226 RiHookBus { handlers: FxHashMap::default() }
227 }
228
229 /// Registers a hook handler for a specific hook kind.
230 ///
231 /// # Parameters
232 ///
233 /// - `kind`: The hook kind to register the handler for
234 /// - `id`: A unique ID for the hook handler (1-128 alphanumeric chars, dash, underscore)
235 /// - `handler`: The handler function to execute when the hook is emitted
236 ///
237 /// # Security
238 ///
239 /// Hook IDs are validated to prevent injection attacks. Only alphanumeric characters,
240 /// dashes, and underscores are allowed.
241 ///
242 /// The handler function takes a `RiServiceContext` and a `RiHookEvent` and returns a `RiResult<()>`.
243 pub fn register<F>(&mut self, kind: RiHookKind, id: RiHookId, handler: F)
244 where
245 F: Fn(&RiServiceContext, &RiHookEvent) -> RiResult<()> + Send + Sync + 'static,
246 {
247 // Security: Validate hook ID
248 if let Err(e) = Self::validate_hook_id(&id) {
249 log::warn!("[Ri.Hooks] Invalid hook ID '{}': {}", id, e);
250 return;
251 }
252
253 self.handlers.entry(kind).or_default().push((id, Box::new(handler)));
254 }
255
256 /// Validates a hook ID to prevent injection attacks.
257 ///
258 /// # Security
259 ///
260 /// Hook IDs must:
261 /// - Be 1-128 characters long
262 /// - Contain only alphanumeric characters, dashes, and underscores
263 /// - Not start with a digit or dash
264 fn validate_hook_id(id: &str) -> RiResult<()> {
265 if id.is_empty() || id.len() > 128 {
266 return Err(crate::core::RiError::Other(
267 "Hook ID must be 1-128 characters".to_string()
268 ));
269 }
270
271 let chars: Vec<char> = id.chars().collect();
272
273 // First character must be a letter or underscore
274 if !chars[0].is_ascii_alphabetic() && chars[0] != '_' {
275 return Err(crate::core::RiError::Other(
276 "Hook ID must start with a letter or underscore".to_string()
277 ));
278 }
279
280 for c in &chars {
281 if !c.is_ascii_alphanumeric() && *c != '-' && *c != '_' {
282 return Err(crate::core::RiError::Other(
283 "Hook ID can only contain alphanumeric characters, dashes, and underscores".to_string()
284 ));
285 }
286 }
287
288 Ok(())
289 }
290
291 /// Emits a hook event of the specified kind.
292 ///
293 /// # Parameters
294 ///
295 /// - `kind`: The hook kind to emit
296 /// - `ctx`: The service context to pass to the hook handlers
297 ///
298 /// # Returns
299 ///
300 /// A `RiResult<()>` indicating success or failure
301 pub fn emit(&self, kind: &RiHookKind, ctx: &RiServiceContext) -> RiResult<()> {
302 self.emit_with(kind, ctx, None, None)
303 }
304
305 /// Emits a hook event with additional contextual information.
306 ///
307 /// # Parameters
308 ///
309 /// - `kind`: The hook kind to emit
310 /// - `ctx`: The service context to pass to the hook handlers
311 /// - `module`: The name of the module associated with the event (if any)
312 /// - `phase`: The module phase associated with the event (if any)
313 ///
314 /// # Returns
315 ///
316 /// A `RiResult<()>` indicating success or failure
317 pub fn emit_with(
318 &self,
319 kind: &RiHookKind,
320 ctx: &RiServiceContext,
321 module: Option<&str>,
322 phase: Option<RiModulePhase>,
323 ) -> RiResult<()> {
324 let event = RiHookEvent {
325 kind: *kind,
326 module: module.map(|s| s.to_string()),
327 phase,
328 };
329 if let Some(list) = self.handlers.get(kind) {
330 for (_id, handler) in list {
331 handler(ctx, &event)?;
332 }
333 }
334 Ok(())
335 }
336
337 /// Emits a hook event without a service context.
338 ///
339 /// This is a simplified version of emit that creates a default context.
340 ///
341 /// # Parameters
342 ///
343 /// - `kind`: The hook kind to emit
344 /// - `module`: The name of the module associated with the event (if any)
345 /// - `phase`: The module phase associated with the event (if any)
346 ///
347 /// # Returns
348 ///
349 /// A `RiResult<()>` indicating success or failure
350 pub fn emit_simple(
351 &self,
352 kind: &RiHookKind,
353 module: Option<&str>,
354 phase: Option<RiModulePhase>,
355 ) -> RiResult<()> {
356 let event = RiHookEvent {
357 kind: *kind,
358 module: module.map(|s| s.to_string()),
359 phase,
360 };
361 if let Some(list) = self.handlers.get(kind) {
362 for (_id, handler) in list {
363 if let Ok(ctx) = RiServiceContext::new_default() {
364 handler(&ctx, &event)?;
365 }
366 }
367 }
368 Ok(())
369 }
370
371 /// Checks if there are any handlers registered for a hook kind.
372 ///
373 /// # Parameters
374 ///
375 /// - `kind`: The hook kind to check
376 ///
377 /// # Returns
378 ///
379 /// `true` if there are handlers registered, `false` otherwise
380 pub fn has_handlers(&self, kind: &RiHookKind) -> bool {
381 self.handlers.get(kind).map_or(false, |list| !list.is_empty())
382 }
383}
384
385#[cfg(feature = "pyo3")]
386#[pyo3::prelude::pymethods]
387impl RiHookBus {
388 #[new]
389 fn py_new() -> Self {
390 Self::new()
391 }
392}