mockforge_plugin_core/
response.rs

1//! Response generator plugin interface
2//!
3//! This module defines the ResponsePlugin trait and related types for implementing
4//! custom response generation logic in MockForge. Response plugins can generate
5//! complex, dynamic responses based on request context, external data sources,
6//! or custom business logic.
7
8use crate::{PluginCapabilities, PluginContext, PluginError, PluginResult, Result};
9use axum::http::{HeaderMap, Method, StatusCode};
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12use std::collections::HashMap;
13
14/// Response generator plugin trait
15///
16/// Implement this trait to create custom response generation logic.
17/// Response plugins are called during the mock response generation phase
18/// to create dynamic responses based on request context and custom logic.
19#[async_trait::async_trait]
20pub trait ResponsePlugin: Send + Sync {
21    /// Get plugin capabilities (permissions and limits)
22    fn capabilities(&self) -> PluginCapabilities;
23
24    /// Initialize the plugin with configuration
25    async fn initialize(&self, config: &ResponsePluginConfig) -> Result<()>;
26
27    /// Check if this plugin can handle the given request
28    ///
29    /// This method is called to determine if the plugin should be used
30    /// to generate a response for the current request.
31    ///
32    /// # Arguments
33    /// * `context` - Plugin execution context
34    /// * `request` - Request information
35    /// * `config` - Plugin configuration
36    ///
37    /// # Returns
38    /// True if this plugin can handle the request
39    async fn can_handle(
40        &self,
41        context: &PluginContext,
42        request: &ResponseRequest,
43        config: &ResponsePluginConfig,
44    ) -> Result<PluginResult<bool>>;
45
46    /// Generate a response for the given request
47    ///
48    /// This method is called when the plugin has indicated it can handle
49    /// the request. It should generate an appropriate response.
50    ///
51    /// # Arguments
52    /// * `context` - Plugin execution context
53    /// * `request` - Request information
54    /// * `config` - Plugin configuration
55    ///
56    /// # Returns
57    /// Generated response
58    async fn generate_response(
59        &self,
60        context: &PluginContext,
61        request: &ResponseRequest,
62        config: &ResponsePluginConfig,
63    ) -> Result<PluginResult<ResponseData>>;
64
65    /// Get plugin priority (lower numbers = higher priority)
66    fn priority(&self) -> i32;
67
68    /// Validate plugin configuration
69    fn validate_config(&self, config: &ResponsePluginConfig) -> Result<()>;
70
71    /// Get supported content types
72    fn supported_content_types(&self) -> Vec<String>;
73
74    /// Cleanup plugin resources
75    async fn cleanup(&self) -> Result<()>;
76}
77
78/// Response plugin configuration
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct ResponsePluginConfig {
81    /// Plugin-specific configuration
82    pub config: HashMap<String, serde_json::Value>,
83    /// Enable/disable the plugin
84    pub enabled: bool,
85    /// Plugin priority (lower numbers = higher priority)
86    pub priority: i32,
87    /// Content types this plugin handles
88    pub content_types: Vec<String>,
89    /// URL patterns this plugin matches
90    pub url_patterns: Vec<String>,
91    /// HTTP methods this plugin handles
92    pub methods: Vec<String>,
93    /// Custom settings
94    pub settings: HashMap<String, serde_json::Value>,
95}
96
97impl Default for ResponsePluginConfig {
98    fn default() -> Self {
99        Self {
100            config: HashMap::new(),
101            enabled: true,
102            priority: 100,
103            content_types: vec!["application/json".to_string()],
104            url_patterns: vec!["*".to_string()],
105            methods: vec!["GET".to_string(), "POST".to_string()],
106            settings: HashMap::new(),
107        }
108    }
109}
110
111/// Response request information
112#[derive(Debug, Clone)]
113pub struct ResponseRequest {
114    /// HTTP method
115    pub method: Method,
116    /// Request URI
117    pub uri: String,
118    /// Request path
119    pub path: String,
120    /// Query parameters
121    pub query_params: HashMap<String, String>,
122    /// Request headers
123    pub headers: HeaderMap,
124    /// Request body (if available)
125    pub body: Option<Vec<u8>>,
126    /// Path parameters (from route matching)
127    pub path_params: HashMap<String, String>,
128    /// Client IP address
129    pub client_ip: Option<String>,
130    /// User agent
131    pub user_agent: Option<String>,
132    /// Request timestamp
133    pub timestamp: chrono::DateTime<chrono::Utc>,
134    /// Authentication context (if available)
135    pub auth_context: Option<HashMap<String, Value>>,
136    /// Custom request context
137    pub custom: HashMap<String, Value>,
138}
139
140impl ResponseRequest {
141    /// Create from axum request components
142    pub fn from_axum(
143        method: Method,
144        uri: axum::http::Uri,
145        headers: HeaderMap,
146        body: Option<Vec<u8>>,
147        path_params: HashMap<String, String>,
148    ) -> Self {
149        let query_params = uri
150            .query()
151            .map(|q| url::form_urlencoded::parse(q.as_bytes()).into_owned().collect())
152            .unwrap_or_default();
153
154        let client_ip = headers
155            .get("x-forwarded-for")
156            .or_else(|| headers.get("x-real-ip"))
157            .and_then(|h| h.to_str().ok())
158            .map(|s| s.to_string());
159
160        let user_agent =
161            headers.get("user-agent").and_then(|h| h.to_str().ok()).map(|s| s.to_string());
162
163        Self {
164            method,
165            uri: uri.to_string(),
166            path: uri.path().to_string(),
167            query_params,
168            headers,
169            body,
170            path_params,
171            client_ip,
172            user_agent,
173            timestamp: chrono::Utc::now(),
174            auth_context: None,
175            custom: HashMap::new(),
176        }
177    }
178
179    /// Get header value
180    pub fn header(&self, name: &str) -> Option<&str> {
181        self.headers.get(name).and_then(|h| h.to_str().ok())
182    }
183
184    /// Get query parameter value
185    pub fn query_param(&self, name: &str) -> Option<&str> {
186        self.query_params.get(name).map(|s| s.as_str())
187    }
188
189    /// Get path parameter value
190    pub fn path_param(&self, name: &str) -> Option<&str> {
191        self.path_params.get(name).map(|s| s.as_str())
192    }
193
194    /// Get authentication context value
195    pub fn auth_value(&self, key: &str) -> Option<&Value> {
196        self.auth_context.as_ref()?.get(key)
197    }
198
199    /// Get custom context value
200    pub fn custom_value(&self, key: &str) -> Option<&Value> {
201        self.custom.get(key)
202    }
203
204    /// Check if request matches URL pattern
205    pub fn matches_url_pattern(&self, pattern: &str) -> bool {
206        if pattern == "*" {
207            return true;
208        }
209
210        // Simple glob matching (can be enhanced with proper glob library)
211        if pattern.contains('*') {
212            let regex_pattern = pattern.replace('.', r"\.").replace('*', ".*");
213            regex::Regex::new(&format!("^{}$", regex_pattern))
214                .map(|re| re.is_match(&self.path))
215                .unwrap_or(false)
216        } else {
217            self.path == pattern
218        }
219    }
220
221    /// Check if request method is supported
222    pub fn matches_method(&self, methods: &[String]) -> bool {
223        methods.iter().any(|m| m == "*" || m == &self.method.to_string())
224    }
225}
226
227/// Response data structure
228#[derive(Debug, Clone, Serialize, Deserialize)]
229pub struct ResponseData {
230    /// HTTP status code
231    pub status_code: u16,
232    /// Response headers
233    pub headers: HashMap<String, String>,
234    /// Response body
235    pub body: Vec<u8>,
236    /// Content type
237    pub content_type: String,
238    /// Response metadata
239    pub metadata: HashMap<String, Value>,
240    /// Cache control directives
241    pub cache_control: Option<String>,
242    /// Custom response data
243    pub custom: HashMap<String, Value>,
244}
245
246impl ResponseData {
247    /// Create a new response
248    pub fn new(status_code: u16, content_type: String, body: Vec<u8>) -> Self {
249        Self {
250            status_code,
251            headers: HashMap::new(),
252            body,
253            content_type,
254            metadata: HashMap::new(),
255            cache_control: None,
256            custom: HashMap::new(),
257        }
258    }
259
260    /// Create JSON response
261    pub fn json<T: Serialize>(status_code: u16, data: &T) -> Result<Self> {
262        let body = serde_json::to_vec(data)
263            .map_err(|e| PluginError::execution(format!("JSON serialization error: {}", e)))?;
264
265        Ok(Self::new(status_code, "application/json".to_string(), body))
266    }
267
268    /// Create text response
269    pub fn text<S: Into<String>>(status_code: u16, text: S) -> Self {
270        Self::new(status_code, "text/plain".to_string(), text.into().into_bytes())
271    }
272
273    /// Create HTML response
274    pub fn html<S: Into<String>>(status_code: u16, html: S) -> Self {
275        Self::new(status_code, "text/html".to_string(), html.into().into_bytes())
276    }
277
278    /// Create XML response
279    pub fn xml<S: Into<String>>(status_code: u16, xml: S) -> Self {
280        Self::new(status_code, "application/xml".to_string(), xml.into().into_bytes())
281    }
282
283    /// Add header
284    pub fn with_header<S: Into<String>>(mut self, key: S, value: S) -> Self {
285        self.headers.insert(key.into(), value.into());
286        self
287    }
288
289    /// Add multiple headers
290    pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
291        self.headers.extend(headers);
292        self
293    }
294
295    /// Add metadata
296    pub fn with_metadata<S: Into<String>>(mut self, key: S, value: Value) -> Self {
297        self.metadata.insert(key.into(), value);
298        self
299    }
300
301    /// Set cache control
302    pub fn with_cache_control<S: Into<String>>(mut self, cache_control: S) -> Self {
303        self.cache_control = Some(cache_control.into());
304        self
305    }
306
307    /// Add custom data
308    pub fn with_custom<S: Into<String>>(mut self, key: S, value: Value) -> Self {
309        self.custom.insert(key.into(), value);
310        self
311    }
312
313    /// Convert to axum response
314    pub fn to_axum_response(self) -> Result<axum::response::Response> {
315        use axum::http::HeaderValue;
316        use axum::response::Response;
317
318        let mut response = Response::new(axum::body::Body::from(self.body));
319        *response.status_mut() = StatusCode::from_u16(self.status_code)
320            .map_err(|_| PluginError::execution("Invalid status code"))?;
321
322        // Add headers
323        for (key, value) in self.headers {
324            if let (Ok(header_name), Ok(header_value)) =
325                (key.parse::<axum::http::HeaderName>(), value.parse::<HeaderValue>())
326            {
327                response.headers_mut().insert(header_name, header_value);
328            }
329        }
330
331        // Set content type if not already set
332        if !response.headers().contains_key("content-type") {
333            if let Ok(header_value) = self.content_type.parse::<HeaderValue>() {
334                response.headers_mut().insert("content-type", header_value);
335            }
336        }
337
338        // Set cache control if specified
339        if let Some(cache_control) = self.cache_control {
340            if let Ok(header_value) = cache_control.parse::<HeaderValue>() {
341                response.headers_mut().insert("cache-control", header_value);
342            }
343        }
344
345        Ok(response)
346    }
347
348    /// Get body as string (if valid UTF-8)
349    pub fn body_as_string(&self) -> Option<String> {
350        String::from_utf8(self.body.clone()).ok()
351    }
352
353    /// Get body as JSON value
354    pub fn body_as_json(&self) -> Option<Value> {
355        serde_json::from_slice(&self.body).ok()
356    }
357}
358
359/// Response plugin registry entry
360pub struct ResponsePluginEntry {
361    /// Plugin ID
362    pub plugin_id: crate::PluginId,
363    /// Plugin instance
364    pub plugin: std::sync::Arc<dyn ResponsePlugin>,
365    /// Plugin configuration
366    pub config: ResponsePluginConfig,
367    /// Plugin capabilities
368    pub capabilities: PluginCapabilities,
369}
370
371impl ResponsePluginEntry {
372    /// Create new plugin entry
373    pub fn new(
374        plugin_id: crate::PluginId,
375        plugin: std::sync::Arc<dyn ResponsePlugin>,
376        config: ResponsePluginConfig,
377    ) -> Self {
378        let capabilities = plugin.capabilities();
379        Self {
380            plugin_id,
381            plugin,
382            config,
383            capabilities,
384        }
385    }
386
387    /// Check if plugin is enabled
388    pub fn is_enabled(&self) -> bool {
389        self.config.enabled
390    }
391
392    /// Get plugin priority
393    pub fn priority(&self) -> i32 {
394        self.config.priority
395    }
396
397    /// Check if plugin can handle the request
398    pub fn can_handle_request(&self, request: &ResponseRequest) -> bool {
399        self.is_enabled()
400            && request.matches_method(&self.config.methods)
401            && self
402                .config
403                .url_patterns
404                .iter()
405                .any(|pattern| request.matches_url_pattern(pattern))
406    }
407}
408
409/// Response modifier plugin trait
410///
411/// Implement this trait to modify responses after they have been generated.
412/// This allows plugins to transform, enhance, or filter responses before
413/// they are sent to the client.
414///
415/// Use cases include:
416/// - Adding custom headers or metadata
417/// - Compressing response bodies
418/// - Encrypting sensitive data
419/// - Filtering or redacting content
420/// - Adding CORS headers
421/// - Response validation
422#[async_trait::async_trait]
423pub trait ResponseModifierPlugin: Send + Sync {
424    /// Get plugin capabilities (permissions and limits)
425    fn capabilities(&self) -> PluginCapabilities;
426
427    /// Initialize the plugin with configuration
428    async fn initialize(&self, config: &ResponseModifierConfig) -> Result<()>;
429
430    /// Check if this plugin should modify the given response
431    ///
432    /// # Arguments
433    /// * `context` - Plugin execution context
434    /// * `request` - Original request information
435    /// * `response` - Current response data
436    /// * `config` - Plugin configuration
437    ///
438    /// # Returns
439    /// True if this plugin should modify the response
440    async fn should_modify(
441        &self,
442        context: &PluginContext,
443        request: &ResponseRequest,
444        response: &ResponseData,
445        config: &ResponseModifierConfig,
446    ) -> Result<PluginResult<bool>>;
447
448    /// Modify the response
449    ///
450    /// This method is called when the plugin has indicated it should modify
451    /// the response. It receives the current response and returns a modified version.
452    ///
453    /// # Arguments
454    /// * `context` - Plugin execution context
455    /// * `request` - Original request information
456    /// * `response` - Current response data to modify
457    /// * `config` - Plugin configuration
458    ///
459    /// # Returns
460    /// Modified response data
461    async fn modify_response(
462        &self,
463        context: &PluginContext,
464        request: &ResponseRequest,
465        response: ResponseData,
466        config: &ResponseModifierConfig,
467    ) -> Result<PluginResult<ResponseData>>;
468
469    /// Get plugin priority (lower numbers = higher priority, executed first)
470    fn priority(&self) -> i32;
471
472    /// Validate plugin configuration
473    fn validate_config(&self, config: &ResponseModifierConfig) -> Result<()>;
474
475    /// Cleanup plugin resources
476    async fn cleanup(&self) -> Result<()>;
477}
478
479/// Response modifier plugin configuration
480#[derive(Debug, Clone, Serialize, Deserialize)]
481pub struct ResponseModifierConfig {
482    /// Plugin-specific configuration
483    pub config: HashMap<String, serde_json::Value>,
484    /// Enable/disable the plugin
485    pub enabled: bool,
486    /// Plugin priority (lower numbers = higher priority, executed first)
487    pub priority: i32,
488    /// Content types this plugin modifies
489    pub content_types: Vec<String>,
490    /// URL patterns this plugin matches
491    pub url_patterns: Vec<String>,
492    /// HTTP methods this plugin handles
493    pub methods: Vec<String>,
494    /// Status codes this plugin modifies (empty = all)
495    pub status_codes: Vec<u16>,
496    /// Custom settings
497    pub settings: HashMap<String, serde_json::Value>,
498}
499
500impl Default for ResponseModifierConfig {
501    fn default() -> Self {
502        Self {
503            config: HashMap::new(),
504            enabled: true,
505            priority: 100,
506            content_types: vec!["application/json".to_string()],
507            url_patterns: vec!["*".to_string()],
508            methods: vec!["GET".to_string(), "POST".to_string()],
509            status_codes: vec![], // Empty means all status codes
510            settings: HashMap::new(),
511        }
512    }
513}
514
515/// Helper trait for creating response plugins
516pub trait ResponsePluginFactory: Send + Sync {
517    /// Create a new response plugin instance
518    fn create_plugin(&self) -> Result<Box<dyn ResponsePlugin>>;
519}
520
521/// Helper trait for creating response modifier plugins
522pub trait ResponseModifierPluginFactory: Send + Sync {
523    /// Create a new response modifier plugin instance
524    fn create_plugin(&self) -> Result<Box<dyn ResponseModifierPlugin>>;
525}
526
527/// Built-in response helpers
528pub mod helpers {
529    use super::*;
530
531    /// Create a standard error response
532    pub fn error_response(status_code: u16, message: &str) -> ResponseData {
533        let error_data = serde_json::json!({
534            "error": {
535                "message": message,
536                "timestamp": chrono::Utc::now().to_rfc3339(),
537                "status_code": status_code
538            }
539        });
540
541        ResponseData::json(status_code, &error_data)
542            .unwrap_or_else(|_| ResponseData::text(status_code, format!("Error: {}", message)))
543    }
544
545    /// Create a success response with data
546    pub fn success_response<T: Serialize>(data: &T) -> Result<ResponseData> {
547        ResponseData::json(200, data)
548    }
549
550    /// Create a redirect response
551    pub fn redirect_response(location: &str, permanent: bool) -> ResponseData {
552        let status_code = if permanent { 301 } else { 302 };
553        ResponseData::new(
554            status_code,
555            "text/plain".to_string(),
556            format!("Redirecting to: {}", location).into_bytes(),
557        )
558        .with_header("location", location)
559    }
560
561    /// Create a not found response
562    pub fn not_found_response(message: Option<&str>) -> ResponseData {
563        let message = message.unwrap_or("Resource not found");
564        error_response(404, message)
565    }
566
567    /// Create an unauthorized response
568    pub fn unauthorized_response(message: Option<&str>) -> ResponseData {
569        let message = message.unwrap_or("Unauthorized");
570        error_response(401, message)
571    }
572
573    /// Create a forbidden response
574    pub fn forbidden_response(message: Option<&str>) -> ResponseData {
575        let message = message.unwrap_or("Forbidden");
576        error_response(403, message)
577    }
578}
579
580#[cfg(test)]
581mod tests {
582
583    #[test]
584    fn test_module_compiles() {
585        // Basic compilation test
586    }
587}