mockforge_core/
lifecycle.rs

1//! Lifecycle hooks for extensibility
2//!
3//! This module provides a comprehensive lifecycle hook system that allows
4//! extensions to hook into various lifecycle events in MockForge:
5//!
6//! - Request/Response lifecycle: before_request, after_response
7//! - Server lifecycle: on_startup, on_shutdown
8//! - Mock lifecycle: on_mock_created, on_mock_updated, on_mock_deleted
9//!
10//! # Examples
11//!
12//! ```rust
13//! use mockforge_core::lifecycle::{LifecycleHook, RequestContext, ResponseContext};
14//! use async_trait::async_trait;
15//!
16//! struct LoggingHook;
17//!
18//! #[async_trait]
19//! impl LifecycleHook for LoggingHook {
20//!     async fn before_request(&self, ctx: &RequestContext) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
21//!         println!("Request: {} {}", ctx.method, ctx.path);
22//!         Ok(())
23//!     }
24//!
25//!     async fn after_response(&self, ctx: &ResponseContext) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
26//!         println!("Response: {} in {}ms", ctx.status_code, ctx.response_time_ms);
27//!         Ok(())
28//!     }
29//! }
30//! ```
31
32use async_trait::async_trait;
33use serde::{Deserialize, Serialize};
34use std::collections::HashMap;
35use std::sync::Arc;
36use tokio::sync::RwLock;
37
38/// Request context for lifecycle hooks
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct RequestContext {
41    /// HTTP method
42    pub method: String,
43    /// Request path
44    pub path: String,
45    /// Request headers
46    pub headers: HashMap<String, String>,
47    /// Query parameters
48    pub query_params: HashMap<String, String>,
49    /// Request body (if available)
50    pub body: Option<Vec<u8>>,
51    /// Request ID for tracking
52    pub request_id: String,
53    /// Timestamp when request was received
54    pub timestamp: chrono::DateTime<chrono::Utc>,
55}
56
57/// Response context for lifecycle hooks
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct ResponseContext {
60    /// Request context
61    pub request: RequestContext,
62    /// HTTP status code
63    pub status_code: u16,
64    /// Response headers
65    pub headers: HashMap<String, String>,
66    /// Response body (if available)
67    pub body: Option<Vec<u8>>,
68    /// Response time in milliseconds
69    pub response_time_ms: u64,
70    /// Timestamp when response was sent
71    pub timestamp: chrono::DateTime<chrono::Utc>,
72}
73
74/// Mock lifecycle event
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub enum MockLifecycleEvent {
77    /// Mock was created
78    Created {
79        /// Mock ID
80        id: String,
81        /// Mock name
82        name: String,
83        /// Mock configuration (serialized)
84        config: serde_json::Value,
85    },
86    /// Mock was updated
87    Updated {
88        /// Mock ID
89        id: String,
90        /// Mock name
91        name: String,
92        /// Updated mock configuration (serialized)
93        config: serde_json::Value,
94    },
95    /// Mock was deleted
96    Deleted {
97        /// Mock ID
98        id: String,
99        /// Mock name
100        name: String,
101    },
102    /// Mock was enabled
103    Enabled {
104        /// Mock ID
105        id: String,
106    },
107    /// Mock was disabled
108    Disabled {
109        /// Mock ID
110        id: String,
111    },
112}
113
114/// Server lifecycle event
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub enum ServerLifecycleEvent {
117    /// Server is starting up
118    Startup {
119        /// Server configuration
120        config: serde_json::Value,
121    },
122    /// Server is shutting down
123    Shutdown {
124        /// Shutdown reason
125        reason: String,
126    },
127}
128
129/// Comprehensive lifecycle hook trait
130///
131/// Implement this trait to hook into various lifecycle events in MockForge.
132/// All methods have default no-op implementations, so you only need to
133/// implement the hooks you care about.
134#[async_trait]
135pub trait LifecycleHook: Send + Sync {
136    /// Called before a request is processed
137    ///
138    /// This hook is called after the request is received but before it's
139    /// matched against mocks or processed. You can use this to:
140    /// - Log requests
141    /// - Modify request headers
142    /// - Add request metadata
143    /// - Perform authentication checks
144    async fn before_request(
145        &self,
146        _ctx: &RequestContext,
147    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
148        Ok(())
149    }
150
151    /// Called after a response is generated
152    ///
153    /// This hook is called after the response is generated but before it's
154    /// sent to the client. You can use this to:
155    /// - Log responses
156    /// - Modify response headers
157    /// - Add response metadata
158    /// - Perform response validation
159    async fn after_response(
160        &self,
161        _ctx: &ResponseContext,
162    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
163        Ok(())
164    }
165
166    /// Called when a mock is created
167    async fn on_mock_created(
168        &self,
169        _event: &MockLifecycleEvent,
170    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
171        Ok(())
172    }
173
174    /// Called when a mock is updated
175    async fn on_mock_updated(
176        &self,
177        _event: &MockLifecycleEvent,
178    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
179        Ok(())
180    }
181
182    /// Called when a mock is deleted
183    async fn on_mock_deleted(
184        &self,
185        _event: &MockLifecycleEvent,
186    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
187        Ok(())
188    }
189
190    /// Called when a mock is enabled or disabled
191    async fn on_mock_state_changed(
192        &self,
193        _event: &MockLifecycleEvent,
194    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
195        Ok(())
196    }
197
198    /// Called when the server starts up
199    async fn on_startup(
200        &self,
201        _event: &ServerLifecycleEvent,
202    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
203        Ok(())
204    }
205
206    /// Called when the server shuts down
207    async fn on_shutdown(
208        &self,
209        _event: &ServerLifecycleEvent,
210    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
211        Ok(())
212    }
213}
214
215/// Lifecycle hook registry
216///
217/// Manages all registered lifecycle hooks and provides methods to invoke them.
218pub struct LifecycleHookRegistry {
219    hooks: Arc<RwLock<Vec<Arc<dyn LifecycleHook>>>>,
220}
221
222impl LifecycleHookRegistry {
223    /// Create a new lifecycle hook registry
224    pub fn new() -> Self {
225        Self {
226            hooks: Arc::new(RwLock::new(Vec::new())),
227        }
228    }
229
230    /// Register a lifecycle hook
231    pub async fn register_hook(&self, hook: Arc<dyn LifecycleHook>) {
232        let mut hooks = self.hooks.write().await;
233        hooks.push(hook);
234    }
235
236    /// Invoke all registered before_request hooks
237    pub async fn invoke_before_request(&self, ctx: &RequestContext) {
238        let hooks = self.hooks.read().await;
239        for hook in hooks.iter() {
240            if let Err(e) = hook.before_request(ctx).await {
241                tracing::error!("Error in before_request hook: {}", e);
242            }
243        }
244    }
245
246    /// Invoke all registered after_response hooks
247    pub async fn invoke_after_response(&self, ctx: &ResponseContext) {
248        let hooks = self.hooks.read().await;
249        for hook in hooks.iter() {
250            if let Err(e) = hook.after_response(ctx).await {
251                tracing::error!("Error in after_response hook: {}", e);
252            }
253        }
254    }
255
256    /// Invoke all registered on_mock_created hooks
257    pub async fn invoke_mock_created(&self, event: &MockLifecycleEvent) {
258        let hooks = self.hooks.read().await;
259        for hook in hooks.iter() {
260            if let Err(e) = hook.on_mock_created(event).await {
261                tracing::error!("Error in on_mock_created hook: {}", e);
262            }
263        }
264    }
265
266    /// Invoke all registered on_mock_updated hooks
267    pub async fn invoke_mock_updated(&self, event: &MockLifecycleEvent) {
268        let hooks = self.hooks.read().await;
269        for hook in hooks.iter() {
270            if let Err(e) = hook.on_mock_updated(event).await {
271                tracing::error!("Error in on_mock_updated hook: {}", e);
272            }
273        }
274    }
275
276    /// Invoke all registered on_mock_deleted hooks
277    pub async fn invoke_mock_deleted(&self, event: &MockLifecycleEvent) {
278        let hooks = self.hooks.read().await;
279        for hook in hooks.iter() {
280            if let Err(e) = hook.on_mock_deleted(event).await {
281                tracing::error!("Error in on_mock_deleted hook: {}", e);
282            }
283        }
284    }
285
286    /// Invoke all registered on_mock_state_changed hooks
287    pub async fn invoke_mock_state_changed(&self, event: &MockLifecycleEvent) {
288        let hooks = self.hooks.read().await;
289        for hook in hooks.iter() {
290            if let Err(e) = hook.on_mock_state_changed(event).await {
291                tracing::error!("Error in on_mock_state_changed hook: {}", e);
292            }
293        }
294    }
295
296    /// Invoke all registered on_startup hooks
297    pub async fn invoke_startup(&self, event: &ServerLifecycleEvent) {
298        let hooks = self.hooks.read().await;
299        for hook in hooks.iter() {
300            if let Err(e) = hook.on_startup(event).await {
301                tracing::error!("Error in on_startup hook: {}", e);
302            }
303        }
304    }
305
306    /// Invoke all registered on_shutdown hooks
307    pub async fn invoke_shutdown(&self, event: &ServerLifecycleEvent) {
308        let hooks = self.hooks.read().await;
309        for hook in hooks.iter() {
310            if let Err(e) = hook.on_shutdown(event).await {
311                tracing::error!("Error in on_shutdown hook: {}", e);
312            }
313        }
314    }
315}
316
317impl Default for LifecycleHookRegistry {
318    fn default() -> Self {
319        Self::new()
320    }
321}
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326
327    struct TestHook {
328        before_request_called: Arc<RwLock<bool>>,
329        after_response_called: Arc<RwLock<bool>>,
330    }
331
332    #[async_trait]
333    impl LifecycleHook for TestHook {
334        async fn before_request(
335            &self,
336            _ctx: &RequestContext,
337        ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
338            *self.before_request_called.write().await = true;
339            Ok(())
340        }
341
342        async fn after_response(
343            &self,
344            _ctx: &ResponseContext,
345        ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
346            *self.after_response_called.write().await = true;
347            Ok(())
348        }
349    }
350
351    #[tokio::test]
352    async fn test_lifecycle_hooks() {
353        let registry = LifecycleHookRegistry::new();
354        let before_called = Arc::new(RwLock::new(false));
355        let after_called = Arc::new(RwLock::new(false));
356
357        let hook = Arc::new(TestHook {
358            before_request_called: before_called.clone(),
359            after_response_called: after_called.clone(),
360        });
361
362        registry.register_hook(hook).await;
363
364        let request_ctx = RequestContext {
365            method: "GET".to_string(),
366            path: "/test".to_string(),
367            headers: HashMap::new(),
368            query_params: HashMap::new(),
369            body: None,
370            request_id: "test-1".to_string(),
371            timestamp: chrono::Utc::now(),
372        };
373
374        registry.invoke_before_request(&request_ctx).await;
375        assert!(*before_called.read().await);
376
377        let response_ctx = ResponseContext {
378            request: request_ctx,
379            status_code: 200,
380            headers: HashMap::new(),
381            body: None,
382            response_time_ms: 10,
383            timestamp: chrono::Utc::now(),
384        };
385
386        registry.invoke_after_response(&response_ctx).await;
387        assert!(*after_called.read().await);
388    }
389}