use crate::{PluginCapabilities, PluginContext, PluginError, PluginResult, Result};
use axum::http::{HeaderMap, Method, StatusCode};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
#[async_trait::async_trait]
pub trait ResponsePlugin: Send + Sync {
fn capabilities(&self) -> PluginCapabilities;
async fn initialize(&self, config: &ResponsePluginConfig) -> Result<()>;
async fn can_handle(
&self,
context: &PluginContext,
request: &ResponseRequest,
config: &ResponsePluginConfig,
) -> Result<PluginResult<bool>>;
async fn generate_response(
&self,
context: &PluginContext,
request: &ResponseRequest,
config: &ResponsePluginConfig,
) -> Result<PluginResult<ResponseData>>;
fn priority(&self) -> i32;
fn validate_config(&self, config: &ResponsePluginConfig) -> Result<()>;
fn supported_content_types(&self) -> Vec<String>;
async fn cleanup(&self) -> Result<()>;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResponsePluginConfig {
pub config: HashMap<String, Value>,
pub enabled: bool,
pub priority: i32,
pub content_types: Vec<String>,
pub url_patterns: Vec<String>,
pub methods: Vec<String>,
pub settings: HashMap<String, Value>,
}
impl Default for ResponsePluginConfig {
fn default() -> Self {
Self {
config: HashMap::new(),
enabled: true,
priority: 100,
content_types: vec!["application/json".to_string()],
url_patterns: vec!["*".to_string()],
methods: vec!["GET".to_string(), "POST".to_string()],
settings: HashMap::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct ResponseRequest {
pub method: Method,
pub uri: String,
pub path: String,
pub query_params: HashMap<String, String>,
pub headers: HeaderMap,
pub body: Option<Vec<u8>>,
pub path_params: HashMap<String, String>,
pub client_ip: Option<String>,
pub user_agent: Option<String>,
pub timestamp: chrono::DateTime<chrono::Utc>,
pub auth_context: Option<HashMap<String, Value>>,
pub custom: HashMap<String, Value>,
}
impl ResponseRequest {
pub fn from_axum(
method: Method,
uri: axum::http::Uri,
headers: HeaderMap,
body: Option<Vec<u8>>,
path_params: HashMap<String, String>,
) -> Self {
let query_params = uri
.query()
.map(|q| url::form_urlencoded::parse(q.as_bytes()).into_owned().collect())
.unwrap_or_default();
let client_ip = headers
.get("x-forwarded-for")
.or_else(|| headers.get("x-real-ip"))
.and_then(|h| h.to_str().ok())
.map(|s| s.to_string());
let user_agent =
headers.get("user-agent").and_then(|h| h.to_str().ok()).map(|s| s.to_string());
Self {
method,
uri: uri.to_string(),
path: uri.path().to_string(),
query_params,
headers,
body,
path_params,
client_ip,
user_agent,
timestamp: chrono::Utc::now(),
auth_context: None,
custom: HashMap::new(),
}
}
pub fn header(&self, name: &str) -> Option<&str> {
self.headers.get(name).and_then(|h| h.to_str().ok())
}
pub fn query_param(&self, name: &str) -> Option<&str> {
self.query_params.get(name).map(|s| s.as_str())
}
pub fn path_param(&self, name: &str) -> Option<&str> {
self.path_params.get(name).map(|s| s.as_str())
}
pub fn auth_value(&self, key: &str) -> Option<&Value> {
self.auth_context.as_ref()?.get(key)
}
pub fn custom_value(&self, key: &str) -> Option<&Value> {
self.custom.get(key)
}
pub fn matches_url_pattern(&self, pattern: &str) -> bool {
if pattern == "*" {
return true;
}
if pattern.contains('*') {
let regex_pattern = pattern.replace('.', r"\.").replace('*', ".*");
regex::Regex::new(&format!("^{}$", regex_pattern))
.map(|re| re.is_match(&self.path))
.unwrap_or(false)
} else {
self.path == pattern
}
}
pub fn matches_method(&self, methods: &[String]) -> bool {
methods.iter().any(|m| m == "*" || m == &self.method.to_string())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResponseData {
pub status_code: u16,
pub headers: HashMap<String, String>,
pub body: Vec<u8>,
pub content_type: String,
pub metadata: HashMap<String, Value>,
pub cache_control: Option<String>,
pub custom: HashMap<String, Value>,
}
impl ResponseData {
pub fn new(status_code: u16, content_type: String, body: Vec<u8>) -> Self {
Self {
status_code,
headers: HashMap::new(),
body,
content_type,
metadata: HashMap::new(),
cache_control: None,
custom: HashMap::new(),
}
}
pub fn json<T: Serialize>(status_code: u16, data: &T) -> Result<Self> {
let body = serde_json::to_vec(data)
.map_err(|e| PluginError::execution(format!("JSON serialization error: {}", e)))?;
Ok(Self::new(status_code, "application/json".to_string(), body))
}
pub fn text<S: Into<String>>(status_code: u16, text: S) -> Self {
Self::new(status_code, "text/plain".to_string(), text.into().into_bytes())
}
pub fn html<S: Into<String>>(status_code: u16, html: S) -> Self {
Self::new(status_code, "text/html".to_string(), html.into().into_bytes())
}
pub fn xml<S: Into<String>>(status_code: u16, xml: S) -> Self {
Self::new(status_code, "application/xml".to_string(), xml.into().into_bytes())
}
pub fn with_header<S: Into<String>>(mut self, key: S, value: S) -> Self {
self.headers.insert(key.into(), value.into());
self
}
pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
self.headers.extend(headers);
self
}
pub fn with_metadata<S: Into<String>>(mut self, key: S, value: Value) -> Self {
self.metadata.insert(key.into(), value);
self
}
pub fn with_cache_control<S: Into<String>>(mut self, cache_control: S) -> Self {
self.cache_control = Some(cache_control.into());
self
}
pub fn with_custom<S: Into<String>>(mut self, key: S, value: Value) -> Self {
self.custom.insert(key.into(), value);
self
}
pub fn to_axum_response(self) -> Result<axum::response::Response> {
use axum::http::HeaderValue;
use axum::response::Response;
let mut response = Response::new(axum::body::Body::from(self.body));
*response.status_mut() = StatusCode::from_u16(self.status_code)
.map_err(|_| PluginError::execution("Invalid status code"))?;
for (key, value) in self.headers {
if let (Ok(header_name), Ok(header_value)) =
(key.parse::<axum::http::HeaderName>(), value.parse::<HeaderValue>())
{
response.headers_mut().insert(header_name, header_value);
}
}
if !response.headers().contains_key("content-type") {
if let Ok(header_value) = self.content_type.parse::<HeaderValue>() {
response.headers_mut().insert("content-type", header_value);
}
}
if let Some(cache_control) = self.cache_control {
if let Ok(header_value) = cache_control.parse::<HeaderValue>() {
response.headers_mut().insert("cache-control", header_value);
}
}
Ok(response)
}
pub fn body_as_string(&self) -> Option<String> {
String::from_utf8(self.body.clone()).ok()
}
pub fn body_as_json(&self) -> Option<Value> {
serde_json::from_slice(&self.body).ok()
}
}
pub struct ResponsePluginEntry {
pub plugin_id: crate::PluginId,
pub plugin: std::sync::Arc<dyn ResponsePlugin>,
pub config: ResponsePluginConfig,
pub capabilities: PluginCapabilities,
}
impl ResponsePluginEntry {
pub fn new(
plugin_id: crate::PluginId,
plugin: std::sync::Arc<dyn ResponsePlugin>,
config: ResponsePluginConfig,
) -> Self {
let capabilities = plugin.capabilities();
Self {
plugin_id,
plugin,
config,
capabilities,
}
}
pub fn is_enabled(&self) -> bool {
self.config.enabled
}
pub fn priority(&self) -> i32 {
self.config.priority
}
pub fn can_handle_request(&self, request: &ResponseRequest) -> bool {
self.is_enabled()
&& request.matches_method(&self.config.methods)
&& self
.config
.url_patterns
.iter()
.any(|pattern| request.matches_url_pattern(pattern))
}
}
#[async_trait::async_trait]
pub trait ResponseModifierPlugin: Send + Sync {
fn capabilities(&self) -> PluginCapabilities;
async fn initialize(&self, config: &ResponseModifierConfig) -> Result<()>;
async fn should_modify(
&self,
context: &PluginContext,
request: &ResponseRequest,
response: &ResponseData,
config: &ResponseModifierConfig,
) -> Result<PluginResult<bool>>;
async fn modify_response(
&self,
context: &PluginContext,
request: &ResponseRequest,
response: ResponseData,
config: &ResponseModifierConfig,
) -> Result<PluginResult<ResponseData>>;
fn priority(&self) -> i32;
fn validate_config(&self, config: &ResponseModifierConfig) -> Result<()>;
async fn cleanup(&self) -> Result<()>;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResponseModifierConfig {
pub config: HashMap<String, Value>,
pub enabled: bool,
pub priority: i32,
pub content_types: Vec<String>,
pub url_patterns: Vec<String>,
pub methods: Vec<String>,
pub status_codes: Vec<u16>,
pub settings: HashMap<String, Value>,
}
impl Default for ResponseModifierConfig {
fn default() -> Self {
Self {
config: HashMap::new(),
enabled: true,
priority: 100,
content_types: vec!["application/json".to_string()],
url_patterns: vec!["*".to_string()],
methods: vec!["GET".to_string(), "POST".to_string()],
status_codes: vec![], settings: HashMap::new(),
}
}
}
pub trait ResponsePluginFactory: Send + Sync {
fn create_plugin(&self) -> Result<Box<dyn ResponsePlugin>>;
}
pub trait ResponseModifierPluginFactory: Send + Sync {
fn create_plugin(&self) -> Result<Box<dyn ResponseModifierPlugin>>;
}
pub mod helpers {
use super::*;
pub fn error_response(status_code: u16, message: &str) -> ResponseData {
let error_data = serde_json::json!({
"error": {
"message": message,
"timestamp": chrono::Utc::now().to_rfc3339(),
"status_code": status_code
}
});
ResponseData::json(status_code, &error_data)
.unwrap_or_else(|_| ResponseData::text(status_code, format!("Error: {}", message)))
}
pub fn success_response<T: Serialize>(data: &T) -> Result<ResponseData> {
ResponseData::json(200, data)
}
pub fn redirect_response(location: &str, permanent: bool) -> ResponseData {
let status_code = if permanent { 301 } else { 302 };
ResponseData::new(
status_code,
"text/plain".to_string(),
format!("Redirecting to: {}", location).into_bytes(),
)
.with_header("location", location)
}
pub fn not_found_response(message: Option<&str>) -> ResponseData {
let message = message.unwrap_or("Resource not found");
error_response(404, message)
}
pub fn unauthorized_response(message: Option<&str>) -> ResponseData {
let message = message.unwrap_or("Unauthorized");
error_response(401, message)
}
pub fn forbidden_response(message: Option<&str>) -> ResponseData {
let message = message.unwrap_or("Forbidden");
error_response(403, message)
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_module_compiles() {
}
}