use actix_web::{HttpResponse, Result as ActixResult}; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use std::collections::HashMap; /// Centralized response builder for consistent API responses /// /// This builder consolidates all scattered HttpResponse construction throughout /// the Project Mycelium codebase into a single source of truth, following /// the established builder pattern architecture. /// /// Usage: /// ```rust,ignore /// // Success responses /// ResponseBuilder::success().data(user).build() /// ResponseBuilder::success().message("User created").build() /// /// // Error responses /// ResponseBuilder::error().message("Invalid input").status(400).build() /// ResponseBuilder::not_found().message("User not found").build() /// /// // Paginated responses /// ResponseBuilder::paginated(users, 1, 100, 10).build() /// ``` #[derive(Debug, Clone)] pub struct ResponseBuilder { success: bool, status_code: u16, message: Option, data: Option, errors: Vec, metadata: HashMap, content_type: Option, html_body: Option, raw_json: Option, } /// Standard API response structure #[derive(Debug, Serialize, Deserialize)] pub struct ApiResponse { pub success: bool, #[serde(skip_serializing_if = "Option::is_none")] pub message: Option, #[serde(skip_serializing_if = "Option::is_none")] pub data: Option, #[serde(skip_serializing_if = "Vec::is_empty")] pub errors: Vec, #[serde(skip_serializing_if = "HashMap::is_empty")] pub metadata: HashMap, } /// Pagination metadata structure #[derive(Debug, Serialize, Deserialize)] pub struct PaginationMeta { pub page: u32, pub per_page: u32, pub total: u32, pub total_pages: u32, pub has_next: bool, pub has_prev: bool, } impl ResponseBuilder { /// Creates a new response builder with default values pub fn new() -> Self { Self { success: true, status_code: 200, message: None, data: None, errors: Vec::new(), metadata: HashMap::new(), content_type: None, html_body: None, raw_json: None, } } /// Creates a success response builder pub fn success() -> Self { Self::new() } /// Creates an OK response builder pub fn ok() -> Self { Self { success: true, status_code: 200, message: None, data: None, errors: Vec::new(), metadata: HashMap::new(), content_type: None, html_body: None, raw_json: None, } } /// Creates an error response builder pub fn error() -> Self { Self { success: false, status_code: 400, message: None, data: None, errors: Vec::new(), metadata: HashMap::new(), content_type: None, html_body: None, raw_json: None, } } /// Creates a bad request response builder pub fn bad_request() -> Self { Self { success: false, status_code: 400, message: Some("Bad request".to_string()), data: None, errors: Vec::new(), metadata: HashMap::new(), content_type: None, html_body: None, raw_json: None, } } /// Creates a not found response builder pub fn not_found() -> Self { Self { success: false, status_code: 404, message: Some("Resource not found".to_string()), data: None, errors: Vec::new(), metadata: HashMap::new(), content_type: None, html_body: None, raw_json: None, } } /// Creates an unauthorized response builder pub fn unauthorized() -> Self { Self { success: false, status_code: 401, message: Some("Unauthorized access".to_string()), data: None, errors: Vec::new(), metadata: HashMap::new(), content_type: None, html_body: None, raw_json: None, } } /// Creates a forbidden response builder pub fn forbidden() -> Self { Self { success: false, status_code: 403, message: Some("Forbidden".to_string()), data: None, errors: Vec::new(), metadata: HashMap::new(), content_type: None, html_body: None, raw_json: None, } } /// Creates a payment required response builder pub fn payment_required() -> Self { Self { success: false, status_code: 402, message: Some("Payment Required".to_string()), data: None, errors: Vec::new(), metadata: HashMap::new(), content_type: None, html_body: None, raw_json: None, } } /// Creates an internal server error response builder pub fn internal_error() -> Self { Self { success: false, status_code: 500, message: Some("Internal server error".to_string()), data: None, errors: Vec::new(), metadata: HashMap::new(), content_type: None, html_body: None, raw_json: None, } } /// Creates a redirect response builder pub fn redirect>(location: S) -> Self { let mut metadata = HashMap::new(); metadata.insert("redirect_location".to_string(), json!(location.into())); Self { success: true, status_code: 302, message: None, data: None, errors: Vec::new(), metadata, content_type: None, html_body: None, raw_json: None, } } /// Adds a cookie to the response builder pub fn cookie(mut self, cookie: actix_web::cookie::Cookie) -> Self { self.metadata.insert("response_cookie".to_string(), json!({ "name": cookie.name(), "value": cookie.value(), "path": cookie.path(), "http_only": cookie.http_only(), "secure": cookie.secure(), "max_age": cookie.max_age().map(|d| d.whole_seconds()) })); self } /// Creates a paginated response builder pub fn paginated(data: Vec, page: u32, per_page: u32, total: u32) -> Self { let total_pages = (total + per_page - 1) / per_page; let has_next = page < total_pages; let has_prev = page > 1; let pagination_meta = PaginationMeta { page, per_page, total, total_pages, has_next, has_prev, }; let mut metadata = HashMap::new(); metadata.insert("pagination".to_string(), json!(pagination_meta)); Self { success: true, status_code: 200, message: None, data: Some(json!(data)), errors: Vec::new(), metadata, content_type: None, html_body: None, raw_json: None, } } // Builder methods for fluent interface pub fn status(mut self, code: u16) -> Self { self.status_code = code; self } pub fn message>(mut self, msg: S) -> Self { self.message = Some(msg.into()); self } pub fn data(mut self, data: T) -> Self { self.data = Some(json!(data)); self } pub fn add_error>(mut self, error: S) -> Self { self.errors.push(error.into()); if self.success { self.success = false; if self.status_code == 200 { self.status_code = 400; } } self } /// Sets a plain text body for the response pub fn body>(mut self, body: S) -> Self { self.data = Some(json!(body.into())); self } pub fn errors(mut self, errors: I) -> Self where I: IntoIterator, S: Into, { for error in errors { self = self.add_error(error); } self } pub fn add_metadata(mut self, key: K, value: V) -> Self where K: Into, V: Serialize, { self.metadata.insert(key.into(), json!(value)); self } pub fn metadata(mut self, metadata: I) -> Self where I: IntoIterator, K: Into, V: Serialize, { for (key, value) in metadata { self = self.add_metadata(key, value); } self } /// Sets JSON data directly for the response pub fn json(mut self, data: T) -> Self { self.data = Some(json!(data)); self } /// Sets HTML content for the response pub fn html>(mut self, html: S) -> Self { self.html_body = Some(html.into()); self.content_type = Some("text/html".to_string()); self } /// Sets a custom content type for the response pub fn content_type>(mut self, content_type: S) -> Self { self.content_type = Some(content_type.into()); self } /// Sets a canonical error envelope JSON body /// Shape: { "error": { "code": string, "message": string, "details": object } } /// Does not override status; use with helpers like `bad_request()`, `not_found()`, `payment_required()`. pub fn error_envelope(mut self, code: C, message: M, details: Value) -> Self where C: Into, M: Into, { // Mark as error semantics self.success = false; // Clear regular data body; we will send raw_json envelope instead self.data = None; // Set canonical error envelope self.raw_json = Some(json!({ "error": { "code": code.into(), "message": message.into(), "details": details } })); // If no status set yet, default to 400 if self.status_code == 200 { self.status_code = 400; } self } /// Builds the final HTTP response pub fn build(self) -> ActixResult { // Handle redirect responses specially if self.status_code == 302 { if let Some(location) = self.metadata.get("redirect_location") { if let Some(location_str) = location.as_str() { // Check if we have a cookie to add if let Some(cookie_data) = self.metadata.get("response_cookie") { if let Some(cookie_obj) = cookie_data.as_object() { if let (Some(name), Some(value)) = ( cookie_obj.get("name").and_then(|v| v.as_str()), cookie_obj.get("value").and_then(|v| v.as_str()) ) { let mut cookie_builder = actix_web::cookie::Cookie::build(name, value); if let Some(path) = cookie_obj.get("path").and_then(|v| v.as_str()) { cookie_builder = cookie_builder.path(path); } if let Some(http_only) = cookie_obj.get("http_only").and_then(|v| v.as_bool()) { if http_only { cookie_builder = cookie_builder.http_only(true); } } if let Some(secure) = cookie_obj.get("secure").and_then(|v| v.as_bool()) { if secure { cookie_builder = cookie_builder.secure(true); } } if let Some(max_age_secs) = cookie_obj.get("max_age").and_then(|v| v.as_i64()) { cookie_builder = cookie_builder.max_age(actix_web::cookie::time::Duration::seconds(max_age_secs)); } let cookie = cookie_builder.finish(); return Ok(HttpResponse::Found() .append_header(("Location", location_str)) .cookie(cookie) .finish()); } } } // Simple redirect without cookie return Ok(HttpResponse::Found() .append_header(("Location", location_str)) .finish()); } } } // Handle HTML responses if let Some(html_content) = self.html_body { let mut http_response = match self.status_code { 200 => HttpResponse::Ok(), 201 => HttpResponse::Created(), 400 => HttpResponse::BadRequest(), 401 => HttpResponse::Unauthorized(), 403 => HttpResponse::Forbidden(), 404 => HttpResponse::NotFound(), 500 => HttpResponse::InternalServerError(), _ => HttpResponse::build(actix_web::http::StatusCode::from_u16(self.status_code).unwrap_or(actix_web::http::StatusCode::INTERNAL_SERVER_ERROR)), }; let content_type = self.content_type.unwrap_or_else(|| "text/html".to_string()); return Ok(http_response.content_type(content_type).body(html_content)); } // Handle raw JSON envelope when provided (e.g., canonical error contract) if let Some(raw) = self.raw_json { let mut http_response = match self.status_code { 200 => HttpResponse::Ok(), 201 => HttpResponse::Created(), 400 => HttpResponse::BadRequest(), 401 => HttpResponse::Unauthorized(), 402 => HttpResponse::build(actix_web::http::StatusCode::PAYMENT_REQUIRED), 403 => HttpResponse::Forbidden(), 404 => HttpResponse::NotFound(), 500 => HttpResponse::InternalServerError(), _ => HttpResponse::build(actix_web::http::StatusCode::from_u16(self.status_code).unwrap_or(actix_web::http::StatusCode::INTERNAL_SERVER_ERROR)), }; return Ok(http_response.json(raw)); } // Handle regular JSON responses let response = ApiResponse { success: self.success, message: self.message, data: self.data, errors: self.errors, metadata: self.metadata, }; let mut http_response = match self.status_code { 200 => HttpResponse::Ok(), 201 => HttpResponse::Created(), 400 => HttpResponse::BadRequest(), 401 => HttpResponse::Unauthorized(), 403 => HttpResponse::Forbidden(), 404 => HttpResponse::NotFound(), 500 => HttpResponse::InternalServerError(), _ => HttpResponse::build(actix_web::http::StatusCode::from_u16(self.status_code).unwrap_or(actix_web::http::StatusCode::INTERNAL_SERVER_ERROR)), }; Ok(http_response.json(response)) } /// Builds a JSON response without HTTP wrapper (for testing) pub fn build_json(self) -> ApiResponse { ApiResponse { success: self.success, message: self.message, data: self.data, errors: self.errors, metadata: self.metadata, } } } impl Default for ResponseBuilder { fn default() -> Self { Self::new() } } /// Convenience macros for common response patterns #[macro_export] macro_rules! success_response { ($data:expr) => { $crate::utils::response_builder::ResponseBuilder::success() .data($data) .build() }; ($data:expr, $message:expr) => { $crate::utils::response_builder::ResponseBuilder::success() .data($data) .message($message) .build() }; } #[macro_export] macro_rules! error_response { ($message:expr) => { $crate::utils::response_builder::ResponseBuilder::error() .message($message) .build() }; ($message:expr, $status:expr) => { $crate::utils::response_builder::ResponseBuilder::error() .message($message) .status($status) .build() }; } #[macro_export] macro_rules! validation_error_response { ($errors:expr) => { $crate::utils::response_builder::ResponseBuilder::error() .message("Validation failed") .errors($errors) .status(422) .build() }; } /// Template methods for common response patterns impl ResponseBuilder { /// Creates a user creation success response pub fn user_created(user: T) -> Self { Self::success() .status(201) .message("User created successfully") .data(user) } /// Creates a user updated success response pub fn user_updated(user: T) -> Self { Self::success() .message("User updated successfully") .data(user) } /// Creates a user deleted success response pub fn user_deleted() -> Self { Self::success() .message("User deleted successfully") } /// Creates an authentication failed response pub fn auth_failed() -> Self { Self::unauthorized() .message("Authentication failed") } /// Creates a validation failed response pub fn validation_failed(errors: I) -> Self where I: IntoIterator, S: Into, { Self::error() .status(422) .message("Validation failed") .errors(errors) } /// Creates a resource not found response pub fn resource_not_found>(resource: S) -> Self { Self::not_found() .message(format!("{} not found", resource.into())) } /// Creates a duplicate resource response pub fn duplicate_resource>(resource: S) -> Self { Self::error() .status(409) .message(format!("{} already exists", resource.into())) } /// Creates a rate limit exceeded response pub fn rate_limit_exceeded() -> Self { Self::error() .status(429) .message("Rate limit exceeded") .add_metadata("retry_after", 60) } /// Creates a maintenance mode response pub fn maintenance_mode() -> Self { Self::error() .status(503) .message("Service temporarily unavailable for maintenance") } } #[cfg(test)] mod tests { use super::*; use serde_json::json; #[test] fn test_success_response() { let response = ResponseBuilder::success() .data(json!({"id": 1, "name": "Test"})) .message("Success") .build_json(); assert!(response.success); assert_eq!(response.message, Some("Success".to_string())); assert!(response.data.is_some()); assert!(response.errors.is_empty()); } #[test] fn test_error_response() { let response = ResponseBuilder::error() .message("Something went wrong") .add_error("Field is required") .build_json(); assert!(!response.success); assert_eq!(response.message, Some("Something went wrong".to_string())); assert_eq!(response.errors.len(), 1); assert_eq!(response.errors[0], "Field is required"); } #[test] fn test_paginated_response() { let data = vec![1, 2, 3, 4, 5]; let response = ResponseBuilder::paginated(data, 1, 5, 20).build_json(); assert!(response.success); assert!(response.metadata.contains_key("pagination")); let pagination = response.metadata.get("pagination").unwrap(); assert_eq!(pagination["page"], 1); assert_eq!(pagination["per_page"], 5); assert_eq!(pagination["total"], 20); assert_eq!(pagination["total_pages"], 4); assert_eq!(pagination["has_next"], true); assert_eq!(pagination["has_prev"], false); } #[test] fn test_template_methods() { let user = json!({"id": 1, "name": "John"}); let response = ResponseBuilder::user_created(user).build_json(); assert!(response.success); assert_eq!(response.message, Some("User created successfully".to_string())); assert!(response.data.is_some()); } #[test] fn test_validation_failed() { let errors = vec!["Name is required", "Email is invalid"]; let response = ResponseBuilder::validation_failed(errors).build_json(); assert!(!response.success); assert_eq!(response.message, Some("Validation failed".to_string())); assert_eq!(response.errors.len(), 2); } #[test] fn test_fluent_interface() { let response = ResponseBuilder::new() .status(201) .message("Created") .data(json!({"id": 1})) .add_metadata("version", "1.0") .build_json(); assert!(response.success); assert_eq!(response.message, Some("Created".to_string())); assert!(response.data.is_some()); assert!(response.metadata.contains_key("version")); } }