mockforge_runtime_daemon/
detector.rs

1//! 404 detection middleware for the runtime daemon
2//!
3//! This module provides middleware that detects 404 responses and triggers
4//! automatic mock generation.
5
6use axum::{extract::Request, http::StatusCode, middleware::Next, response::Response};
7use std::sync::Arc;
8use tokio::sync::RwLock;
9use tracing::{debug, info, warn};
10
11use crate::auto_generator::AutoGenerator;
12use crate::config::RuntimeDaemonConfig;
13
14/// State for the 404 detector middleware
15#[derive(Clone)]
16pub struct NotFoundDetector {
17    /// Daemon configuration
18    config: Arc<RuntimeDaemonConfig>,
19    /// Auto-generator for creating mocks (wrapped in Arc for sharing)
20    generator: Arc<RwLock<Option<Arc<AutoGenerator>>>>,
21}
22
23impl NotFoundDetector {
24    /// Create a new 404 detector
25    pub fn new(config: RuntimeDaemonConfig) -> Self {
26        Self {
27            config: Arc::new(config),
28            generator: Arc::new(RwLock::new(None)),
29        }
30    }
31
32    /// Set the auto-generator
33    pub async fn set_generator(&self, generator: Arc<AutoGenerator>) {
34        let mut gen = self.generator.write().await;
35        *gen = Some(generator);
36    }
37
38    /// Check if a path should be excluded from auto-generation
39    fn should_exclude(&self, path: &str) -> bool {
40        self.config.exclude_patterns.iter().any(|pattern| {
41            if pattern.starts_with('/') {
42                path.starts_with(pattern)
43            } else {
44                path.contains(pattern)
45            }
46        })
47    }
48
49    /// Middleware function that detects 404s and triggers auto-generation
50    pub async fn detect_and_auto_create(self, request: Request, next: Next) -> Response {
51        // Extract request details before consuming the request
52        let method = request.method().clone();
53        let uri = request.uri().clone();
54        let path = uri.path().to_string();
55
56        // Execute the request and get the response
57        let response = next.run(request).await;
58
59        // Only process if daemon is enabled and auto-create is enabled
60        if !self.config.enabled || !self.config.auto_create_on_404 {
61            return response;
62        }
63
64        // Check if response is a 404
65        if response.status() != StatusCode::NOT_FOUND {
66            return response;
67        }
68
69        // Check if path should be excluded
70        if self.should_exclude(&path) {
71            debug!("Excluding path from auto-generation: {}", path);
72            return response;
73        }
74
75        info!("Detected 404 for {} {}, triggering auto-generation", method, path);
76
77        // Try to auto-generate a mock
78        let generator = self.generator.read().await;
79        if let Some(ref gen) = *generator {
80            // Clone what we need for async operation
81            let method_str = method.to_string();
82            let path_str = path.clone();
83            let gen_clone = Arc::clone(gen);
84
85            // Spawn async task to generate mock (don't block the response)
86            tokio::spawn(async move {
87                if let Err(e) = gen_clone.generate_mock_from_404(&method_str, &path_str).await {
88                    warn!("Failed to auto-generate mock for {} {}: {}", method_str, path_str, e);
89                } else {
90                    info!("Successfully auto-generated mock for {} {}", method_str, path_str);
91                }
92            });
93        } else {
94            debug!("Auto-generator not available, skipping mock creation");
95        }
96
97        response
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[tokio::test]
106    async fn test_should_exclude() {
107        let config = RuntimeDaemonConfig {
108            exclude_patterns: vec!["/health".to_string(), "/metrics".to_string()],
109            ..Default::default()
110        };
111        let detector = NotFoundDetector::new(config);
112
113        assert!(detector.should_exclude("/health"));
114        assert!(detector.should_exclude("/health/check"));
115        assert!(detector.should_exclude("/metrics"));
116        assert!(!detector.should_exclude("/api/users"));
117    }
118}