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 request interceptor that modifies a header
337    #[derive(Clone)]
338    struct HeaderModifyingRequestInterceptor {
339        header_name: &'static str,
340        header_value: String,
341    }
342
343    impl HeaderModifyingRequestInterceptor {
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 RequestInterceptor for HeaderModifyingRequestInterceptor {
353        fn intercept(&self, mut request: Request) -> Request {
354            // Store the value in extensions since we can't modify headers directly
355            // In a real implementation, we'd need mutable header access
356            request
357                .extensions_mut()
358                .insert(format!("{}:{}", self.header_name, self.header_value));
359            request
360        }
361
362        fn clone_box(&self) -> Box<dyn RequestInterceptor> {
363            Box::new(self.clone())
364        }
365    }
366
367    /// A response interceptor that modifies a header
368    #[derive(Clone)]
369    struct HeaderModifyingResponseInterceptor {
370        header_name: &'static str,
371        header_value: String,
372    }
373
374    impl HeaderModifyingResponseInterceptor {
375        fn new(header_name: &'static str, header_value: impl Into<String>) -> Self {
376            Self {
377                header_name,
378                header_value: header_value.into(),
379            }
380        }
381    }
382
383    impl ResponseInterceptor for HeaderModifyingResponseInterceptor {
384        fn intercept(&self, mut response: Response) -> Response {
385            if let Ok(value) = self.header_value.parse() {
386                response.headers_mut().insert(self.header_name, value);
387            }
388            response
389        }
390
391        fn clone_box(&self) -> Box<dyn ResponseInterceptor> {
392            Box::new(self.clone())
393        }
394    }
395
396    // **Feature: v1-features-roadmap, Property 7: Interceptor modification propagation**
397    //
398    // For any modification made by an interceptor, subsequent interceptors and handlers
399    // SHALL receive the modified request/response.
400    //
401    // **Validates: Requirements 2.4, 2.5**
402    proptest! {
403        #![proptest_config(ProptestConfig::with_cases(100))]
404
405        #[test]
406        fn prop_interceptor_modification_propagation(
407            num_interceptors in 1usize..5usize,
408            header_values in prop::collection::vec("[a-zA-Z0-9]{1,10}", 1..5usize),
409        ) {
410            let mut chain = InterceptorChain::new();
411
412            // Add response interceptors that each add a unique header
413            for (i, value) in header_values.iter().enumerate().take(num_interceptors) {
414                let header_name = Box::leak(format!("x-test-{}", i).into_boxed_str());
415                chain.add_response_interceptor(
416                    HeaderModifyingResponseInterceptor::new(header_name, value.clone())
417                );
418            }
419
420            // Execute response interceptors
421            let response = create_test_response(StatusCode::OK);
422            let modified_response = chain.intercept_response(response);
423
424            // Verify all headers were added (modifications propagated)
425            for (i, value) in header_values.iter().enumerate().take(num_interceptors) {
426                let header_name = format!("x-test-{}", i);
427                let header_value = modified_response.headers().get(&header_name);
428                prop_assert!(header_value.is_some(), "Header {} should be present", header_name);
429                prop_assert_eq!(
430                    header_value.unwrap().to_str().unwrap(),
431                    value,
432                    "Header {} should have value {}", header_name, value
433                );
434            }
435        }
436    }
437
438    #[test]
439    fn test_empty_chain() {
440        let chain = InterceptorChain::new();
441        assert!(chain.is_empty());
442        assert_eq!(chain.request_interceptor_count(), 0);
443        assert_eq!(chain.response_interceptor_count(), 0);
444
445        // Should pass through unchanged
446        let request = create_test_request(Method::GET, "/test");
447        let _ = chain.intercept_request(request);
448
449        let response = create_test_response(StatusCode::OK);
450        let result = chain.intercept_response(response);
451        assert_eq!(result.status(), StatusCode::OK);
452    }
453
454    #[test]
455    fn test_single_request_interceptor() {
456        let order = Arc::new(std::sync::Mutex::new(Vec::new()));
457        let mut chain = InterceptorChain::new();
458        chain.add_request_interceptor(TrackingRequestInterceptor::new(42, order.clone()));
459
460        assert!(!chain.is_empty());
461        assert_eq!(chain.request_interceptor_count(), 1);
462
463        let request = create_test_request(Method::GET, "/test");
464        let _ = chain.intercept_request(request);
465
466        let recorded = order.lock().unwrap();
467        assert_eq!(recorded.len(), 1);
468        assert_eq!(recorded[0], 42);
469    }
470
471    #[test]
472    fn test_single_response_interceptor() {
473        let order = Arc::new(std::sync::Mutex::new(Vec::new()));
474        let mut chain = InterceptorChain::new();
475        chain.add_response_interceptor(TrackingResponseInterceptor::new(42, order.clone()));
476
477        assert!(!chain.is_empty());
478        assert_eq!(chain.response_interceptor_count(), 1);
479
480        let response = create_test_response(StatusCode::OK);
481        let _ = chain.intercept_response(response);
482
483        let recorded = order.lock().unwrap();
484        assert_eq!(recorded.len(), 1);
485        assert_eq!(recorded[0], 42);
486    }
487
488    #[test]
489    fn test_response_header_modification() {
490        let mut chain = InterceptorChain::new();
491        chain.add_response_interceptor(HeaderModifyingResponseInterceptor::new(
492            "x-custom", "value1",
493        ));
494        chain.add_response_interceptor(HeaderModifyingResponseInterceptor::new(
495            "x-another",
496            "value2",
497        ));
498
499        let response = create_test_response(StatusCode::OK);
500        let modified = chain.intercept_response(response);
501
502        // Both headers should be present
503        assert_eq!(
504            modified
505                .headers()
506                .get("x-custom")
507                .unwrap()
508                .to_str()
509                .unwrap(),
510            "value1"
511        );
512        assert_eq!(
513            modified
514                .headers()
515                .get("x-another")
516                .unwrap()
517                .to_str()
518                .unwrap(),
519            "value2"
520        );
521    }
522
523    #[test]
524    fn test_chain_clone() {
525        let order = Arc::new(std::sync::Mutex::new(Vec::new()));
526        let mut chain = InterceptorChain::new();
527        chain.add_request_interceptor(TrackingRequestInterceptor::new(1, order.clone()));
528        chain.add_response_interceptor(TrackingResponseInterceptor::new(2, order.clone()));
529
530        // Clone the chain
531        let cloned = chain.clone();
532
533        assert_eq!(cloned.request_interceptor_count(), 1);
534        assert_eq!(cloned.response_interceptor_count(), 1);
535    }
536}