rustapi_core/
interceptor.rs

1//! Request/Response Interceptor System for RustAPI
2//!
3//! This module provides interceptors that can modify requests before handlers
4//! and responses after handlers, without the complexity of Tower layers.
5//!
6//! # Overview
7//!
8//! Interceptors provide a simpler alternative to middleware for common use cases:
9//! - Adding headers to all requests/responses
10//! - Logging and metrics
11//! - Request/response transformation
12//!
13//! # Execution Order
14//!
15//! Request interceptors execute in registration order (1 → 2 → 3 → Handler).
16//! Response interceptors execute in reverse order (Handler → 3 → 2 → 1).
17//!
18//! # Example
19//!
20//! ```rust,ignore
21//! use rustapi_core::{RustApi, interceptor::{RequestInterceptor, ResponseInterceptor}};
22//!
23//! struct AddRequestId;
24//!
25//! impl RequestInterceptor for AddRequestId {
26//!     fn intercept(&self, mut req: Request) -> Request {
27//!         req.extensions_mut().insert(uuid::Uuid::new_v4());
28//!         req
29//!     }
30//! }
31//!
32//! struct AddServerHeader;
33//!
34//! impl ResponseInterceptor for AddServerHeader {
35//!     fn intercept(&self, mut res: Response) -> Response {
36//!         res.headers_mut().insert("X-Server", "RustAPI".parse().unwrap());
37//!         res
38//!     }
39//! }
40//!
41//! RustApi::new()
42//!     .request_interceptor(AddRequestId)
43//!     .response_interceptor(AddServerHeader)
44//!     .route("/", get(handler))
45//!     .run("127.0.0.1:8080")
46//!     .await
47//! ```
48
49use crate::request::Request;
50use crate::response::Response;
51
52/// Trait for intercepting and modifying requests before they reach handlers.
53///
54/// Request interceptors are executed in the order they are registered.
55/// Each interceptor receives the request, can modify it, and returns the
56/// (potentially modified) request for the next interceptor or handler.
57///
58/// # Example
59///
60/// ```rust,ignore
61/// use rustapi_core::interceptor::RequestInterceptor;
62/// use rustapi_core::Request;
63///
64/// struct LoggingInterceptor;
65///
66/// impl RequestInterceptor for LoggingInterceptor {
67///     fn intercept(&self, req: Request) -> Request {
68///         println!("Request: {} {}", req.method(), req.path());
69///         req
70///     }
71/// }
72/// ```
73pub trait RequestInterceptor: Send + Sync + 'static {
74    /// Intercept and optionally modify the request.
75    ///
76    /// The returned request will be passed to the next interceptor or handler.
77    fn intercept(&self, request: Request) -> Request;
78
79    /// Clone this interceptor into a boxed trait object.
80    fn clone_box(&self) -> Box<dyn RequestInterceptor>;
81}
82
83impl Clone for Box<dyn RequestInterceptor> {
84    fn clone(&self) -> Self {
85        self.clone_box()
86    }
87}
88
89/// Trait for intercepting and modifying responses after handlers complete.
90///
91/// Response interceptors are executed in reverse registration order.
92/// Each interceptor receives the response, can modify it, and returns the
93/// (potentially modified) response for the previous interceptor or client.
94///
95/// # Example
96///
97/// ```rust,ignore
98/// use rustapi_core::interceptor::ResponseInterceptor;
99/// use rustapi_core::Response;
100///
101/// struct AddCorsHeaders;
102///
103/// impl ResponseInterceptor for AddCorsHeaders {
104///     fn intercept(&self, mut res: Response) -> Response {
105///         res.headers_mut().insert(
106///             "Access-Control-Allow-Origin",
107///             "*".parse().unwrap()
108///         );
109///         res
110///     }
111/// }
112/// ```
113pub trait ResponseInterceptor: Send + Sync + 'static {
114    /// Intercept and optionally modify the response.
115    ///
116    /// The returned response will be passed to the previous interceptor or client.
117    fn intercept(&self, response: Response) -> Response;
118
119    /// Clone this interceptor into a boxed trait object.
120    fn clone_box(&self) -> Box<dyn ResponseInterceptor>;
121}
122
123impl Clone for Box<dyn ResponseInterceptor> {
124    fn clone(&self) -> Self {
125        self.clone_box()
126    }
127}
128
129/// Chain of request and response interceptors.
130///
131/// Manages the execution of multiple interceptors in the correct order:
132/// - Request interceptors: executed in registration order (first registered = first executed)
133/// - Response interceptors: executed in reverse order (last registered = first executed)
134#[derive(Clone, Default)]
135pub struct InterceptorChain {
136    request_interceptors: Vec<Box<dyn RequestInterceptor>>,
137    response_interceptors: Vec<Box<dyn ResponseInterceptor>>,
138}
139
140impl InterceptorChain {
141    /// Create a new empty interceptor chain.
142    pub fn new() -> Self {
143        Self {
144            request_interceptors: Vec::new(),
145            response_interceptors: Vec::new(),
146        }
147    }
148
149    /// Add a request interceptor to the chain.
150    ///
151    /// Interceptors are executed in the order they are added.
152    pub fn add_request_interceptor<I: RequestInterceptor>(&mut self, interceptor: I) {
153        self.request_interceptors.push(Box::new(interceptor));
154    }
155
156    /// Add a response interceptor to the chain.
157    ///
158    /// Interceptors are executed in reverse order (last added = first executed after handler).
159    pub fn add_response_interceptor<I: ResponseInterceptor>(&mut self, interceptor: I) {
160        self.response_interceptors.push(Box::new(interceptor));
161    }
162
163    /// Get the number of request interceptors.
164    pub fn request_interceptor_count(&self) -> usize {
165        self.request_interceptors.len()
166    }
167
168    /// Get the number of response interceptors.
169    pub fn response_interceptor_count(&self) -> usize {
170        self.response_interceptors.len()
171    }
172
173    /// Check if the chain has any interceptors.
174    pub fn is_empty(&self) -> bool {
175        self.request_interceptors.is_empty() && self.response_interceptors.is_empty()
176    }
177
178    /// Execute all request interceptors on the given request.
179    ///
180    /// Interceptors are executed in registration order.
181    /// Each interceptor receives the output of the previous one.
182    pub fn intercept_request(&self, mut request: Request) -> Request {
183        for interceptor in &self.request_interceptors {
184            request = interceptor.intercept(request);
185        }
186        request
187    }
188
189    /// Execute all response interceptors on the given response.
190    ///
191    /// Interceptors are executed in reverse registration order.
192    /// Each interceptor receives the output of the previous one.
193    pub fn intercept_response(&self, mut response: Response) -> Response {
194        // Execute in reverse order (last registered = first to process response)
195        for interceptor in self.response_interceptors.iter().rev() {
196            response = interceptor.intercept(response);
197        }
198        response
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205    use crate::path_params::PathParams;
206    use bytes::Bytes;
207    use http::{Extensions, Method, StatusCode};
208    use http_body_util::Full;
209    use proptest::prelude::*;
210    use std::sync::Arc;
211
212    /// Create a test request with the given method and path
213    fn create_test_request(method: Method, path: &str) -> Request {
214        let uri: http::Uri = path.parse().unwrap();
215        let builder = http::Request::builder().method(method).uri(uri);
216
217        let req = builder.body(()).unwrap();
218        let (parts, _) = req.into_parts();
219
220        Request::new(
221            parts,
222            crate::request::BodyVariant::Buffered(Bytes::new()),
223            Arc::new(Extensions::new()),
224            PathParams::new(),
225        )
226    }
227
228    /// Create a test response with the given status
229    fn create_test_response(status: StatusCode) -> Response {
230        http::Response::builder()
231            .status(status)
232            .body(Full::new(Bytes::from("test")))
233            .unwrap()
234    }
235
236    /// A request interceptor that adds a header tracking its ID
237    #[derive(Clone)]
238    struct TrackingRequestInterceptor {
239        id: usize,
240        order: Arc<std::sync::Mutex<Vec<usize>>>,
241    }
242
243    impl TrackingRequestInterceptor {
244        fn new(id: usize, order: Arc<std::sync::Mutex<Vec<usize>>>) -> Self {
245            Self { id, order }
246        }
247    }
248
249    impl RequestInterceptor for TrackingRequestInterceptor {
250        fn intercept(&self, request: Request) -> Request {
251            self.order.lock().unwrap().push(self.id);
252            request
253        }
254
255        fn clone_box(&self) -> Box<dyn RequestInterceptor> {
256            Box::new(self.clone())
257        }
258    }
259
260    /// A response interceptor that adds a header tracking its ID
261    #[derive(Clone)]
262    struct TrackingResponseInterceptor {
263        id: usize,
264        order: Arc<std::sync::Mutex<Vec<usize>>>,
265    }
266
267    impl TrackingResponseInterceptor {
268        fn new(id: usize, order: Arc<std::sync::Mutex<Vec<usize>>>) -> Self {
269            Self { id, order }
270        }
271    }
272
273    impl ResponseInterceptor for TrackingResponseInterceptor {
274        fn intercept(&self, response: Response) -> Response {
275            self.order.lock().unwrap().push(self.id);
276            response
277        }
278
279        fn clone_box(&self) -> Box<dyn ResponseInterceptor> {
280            Box::new(self.clone())
281        }
282    }
283
284    // **Feature: v1-features-roadmap, Property 6: Interceptor execution order**
285    //
286    // For any set of N registered interceptors, request interceptors SHALL execute
287    // in registration order (1→N) and response interceptors SHALL execute in
288    // reverse order (N→1).
289    //
290    // **Validates: Requirements 2.1, 2.2, 2.3**
291    proptest! {
292        #![proptest_config(ProptestConfig::with_cases(100))]
293
294        #[test]
295        fn prop_interceptor_execution_order(num_interceptors in 1usize..10usize) {
296            let request_order = Arc::new(std::sync::Mutex::new(Vec::new()));
297            let response_order = Arc::new(std::sync::Mutex::new(Vec::new()));
298
299            let mut chain = InterceptorChain::new();
300
301            // Add interceptors in order 0, 1, 2, ..., n-1
302            for i in 0..num_interceptors {
303                chain.add_request_interceptor(
304                    TrackingRequestInterceptor::new(i, request_order.clone())
305                );
306                chain.add_response_interceptor(
307                    TrackingResponseInterceptor::new(i, response_order.clone())
308                );
309            }
310
311            // Execute request interceptors
312            let request = create_test_request(Method::GET, "/test");
313            let _ = chain.intercept_request(request);
314
315            // Execute response interceptors
316            let response = create_test_response(StatusCode::OK);
317            let _ = chain.intercept_response(response);
318
319            // Verify request interceptor order: should be 0, 1, 2, ..., n-1
320            let req_order = request_order.lock().unwrap();
321            prop_assert_eq!(req_order.len(), num_interceptors);
322            for (idx, &id) in req_order.iter().enumerate() {
323                prop_assert_eq!(id, idx, "Request interceptor order mismatch at index {}", idx);
324            }
325
326            // Verify response interceptor order: should be n-1, n-2, ..., 1, 0 (reverse)
327            let res_order = response_order.lock().unwrap();
328            prop_assert_eq!(res_order.len(), num_interceptors);
329            for (idx, &id) in res_order.iter().enumerate() {
330                let expected = num_interceptors - 1 - idx;
331                prop_assert_eq!(id, expected, "Response interceptor order mismatch at index {}", idx);
332            }
333        }
334    }
335
336    /// A response interceptor that modifies a header
337    #[derive(Clone)]
338    struct HeaderModifyingResponseInterceptor {
339        header_name: &'static str,
340        header_value: String,
341    }
342
343    impl HeaderModifyingResponseInterceptor {
344        fn new(header_name: &'static str, header_value: impl Into<String>) -> Self {
345            Self {
346                header_name,
347                header_value: header_value.into(),
348            }
349        }
350    }
351
352    impl ResponseInterceptor for HeaderModifyingResponseInterceptor {
353        fn intercept(&self, mut response: Response) -> Response {
354            if let Ok(value) = self.header_value.parse() {
355                response.headers_mut().insert(self.header_name, value);
356            }
357            response
358        }
359
360        fn clone_box(&self) -> Box<dyn ResponseInterceptor> {
361            Box::new(self.clone())
362        }
363    }
364
365    // **Feature: v1-features-roadmap, Property 7: Interceptor modification propagation**
366    //
367    // For any modification made by an interceptor, subsequent interceptors and handlers
368    // SHALL receive the modified request/response.
369    //
370    // **Validates: Requirements 2.4, 2.5**
371    proptest! {
372        #![proptest_config(ProptestConfig::with_cases(100))]
373
374        #[test]
375        fn prop_interceptor_modification_propagation(
376            num_interceptors in 1usize..5usize,
377            header_values in prop::collection::vec("[a-zA-Z0-9]{1,10}", 1..5usize),
378        ) {
379            let mut chain = InterceptorChain::new();
380
381            // Add response interceptors that each add a unique header
382            for (i, value) in header_values.iter().enumerate().take(num_interceptors) {
383                let header_name = Box::leak(format!("x-test-{}", i).into_boxed_str());
384                chain.add_response_interceptor(
385                    HeaderModifyingResponseInterceptor::new(header_name, value.clone())
386                );
387            }
388
389            // Execute response interceptors
390            let response = create_test_response(StatusCode::OK);
391            let modified_response = chain.intercept_response(response);
392
393            // Verify all headers were added (modifications propagated)
394            for (i, value) in header_values.iter().enumerate().take(num_interceptors) {
395                let header_name = format!("x-test-{}", i);
396                let header_value = modified_response.headers().get(&header_name);
397                prop_assert!(header_value.is_some(), "Header {} should be present", header_name);
398                prop_assert_eq!(
399                    header_value.unwrap().to_str().unwrap(),
400                    value,
401                    "Header {} should have value {}", header_name, value
402                );
403            }
404        }
405    }
406
407    #[test]
408    fn test_empty_chain() {
409        let chain = InterceptorChain::new();
410        assert!(chain.is_empty());
411        assert_eq!(chain.request_interceptor_count(), 0);
412        assert_eq!(chain.response_interceptor_count(), 0);
413
414        // Should pass through unchanged
415        let request = create_test_request(Method::GET, "/test");
416        let _ = chain.intercept_request(request);
417
418        let response = create_test_response(StatusCode::OK);
419        let result = chain.intercept_response(response);
420        assert_eq!(result.status(), StatusCode::OK);
421    }
422
423    #[test]
424    fn test_single_request_interceptor() {
425        let order = Arc::new(std::sync::Mutex::new(Vec::new()));
426        let mut chain = InterceptorChain::new();
427        chain.add_request_interceptor(TrackingRequestInterceptor::new(42, order.clone()));
428
429        assert!(!chain.is_empty());
430        assert_eq!(chain.request_interceptor_count(), 1);
431
432        let request = create_test_request(Method::GET, "/test");
433        let _ = chain.intercept_request(request);
434
435        let recorded = order.lock().unwrap();
436        assert_eq!(recorded.len(), 1);
437        assert_eq!(recorded[0], 42);
438    }
439
440    #[test]
441    fn test_single_response_interceptor() {
442        let order = Arc::new(std::sync::Mutex::new(Vec::new()));
443        let mut chain = InterceptorChain::new();
444        chain.add_response_interceptor(TrackingResponseInterceptor::new(42, order.clone()));
445
446        assert!(!chain.is_empty());
447        assert_eq!(chain.response_interceptor_count(), 1);
448
449        let response = create_test_response(StatusCode::OK);
450        let _ = chain.intercept_response(response);
451
452        let recorded = order.lock().unwrap();
453        assert_eq!(recorded.len(), 1);
454        assert_eq!(recorded[0], 42);
455    }
456
457    #[test]
458    fn test_response_header_modification() {
459        let mut chain = InterceptorChain::new();
460        chain.add_response_interceptor(HeaderModifyingResponseInterceptor::new(
461            "x-custom", "value1",
462        ));
463        chain.add_response_interceptor(HeaderModifyingResponseInterceptor::new(
464            "x-another",
465            "value2",
466        ));
467
468        let response = create_test_response(StatusCode::OK);
469        let modified = chain.intercept_response(response);
470
471        // Both headers should be present
472        assert_eq!(
473            modified
474                .headers()
475                .get("x-custom")
476                .unwrap()
477                .to_str()
478                .unwrap(),
479            "value1"
480        );
481        assert_eq!(
482            modified
483                .headers()
484                .get("x-another")
485                .unwrap()
486                .to_str()
487                .unwrap(),
488            "value2"
489        );
490    }
491
492    #[test]
493    fn test_chain_clone() {
494        let order = Arc::new(std::sync::Mutex::new(Vec::new()));
495        let mut chain = InterceptorChain::new();
496        chain.add_request_interceptor(TrackingRequestInterceptor::new(1, order.clone()));
497        chain.add_response_interceptor(TrackingResponseInterceptor::new(2, order.clone()));
498
499        // Clone the chain
500        let cloned = chain.clone();
501
502        assert_eq!(cloned.request_interceptor_count(), 1);
503        assert_eq!(cloned.response_interceptor_count(), 1);
504    }
505}