init projectmycelium

This commit is contained in:
mik-tf
2025-09-01 21:37:01 -04:00
commit b41efb0e99
319 changed files with 128160 additions and 0 deletions

View File

@@ -0,0 +1,693 @@
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<String>,
data: Option<Value>,
errors: Vec<String>,
metadata: HashMap<String, Value>,
content_type: Option<String>,
html_body: Option<String>,
raw_json: Option<Value>,
}
/// Standard API response structure
#[derive(Debug, Serialize, Deserialize)]
pub struct ApiResponse {
pub success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub errors: Vec<String>,
#[serde(skip_serializing_if = "HashMap::is_empty")]
pub metadata: HashMap<String, Value>,
}
/// 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<S: Into<String>>(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<T: Serialize>(data: Vec<T>, 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<S: Into<String>>(mut self, msg: S) -> Self {
self.message = Some(msg.into());
self
}
pub fn data<T: Serialize>(mut self, data: T) -> Self {
self.data = Some(json!(data));
self
}
pub fn add_error<S: Into<String>>(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<S: Into<String>>(mut self, body: S) -> Self {
self.data = Some(json!(body.into()));
self
}
pub fn errors<I, S>(mut self, errors: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
for error in errors {
self = self.add_error(error);
}
self
}
pub fn add_metadata<K, V>(mut self, key: K, value: V) -> Self
where
K: Into<String>,
V: Serialize,
{
self.metadata.insert(key.into(), json!(value));
self
}
pub fn metadata<I, K, V>(mut self, metadata: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: Into<String>,
V: Serialize,
{
for (key, value) in metadata {
self = self.add_metadata(key, value);
}
self
}
/// Sets JSON data directly for the response
pub fn json<T: Serialize>(mut self, data: T) -> Self {
self.data = Some(json!(data));
self
}
/// Sets HTML content for the response
pub fn html<S: Into<String>>(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<S: Into<String>>(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<C, M>(mut self, code: C, message: M, details: Value) -> Self
where
C: Into<String>,
M: Into<String>,
{
// 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<HttpResponse> {
// 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<T: Serialize>(user: T) -> Self {
Self::success()
.status(201)
.message("User created successfully")
.data(user)
}
/// Creates a user updated success response
pub fn user_updated<T: Serialize>(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<I, S>(errors: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
Self::error()
.status(422)
.message("Validation failed")
.errors(errors)
}
/// Creates a resource not found response
pub fn resource_not_found<S: Into<String>>(resource: S) -> Self {
Self::not_found()
.message(format!("{} not found", resource.into()))
}
/// Creates a duplicate resource response
pub fn duplicate_resource<S: Into<String>>(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"));
}
}