init projectmycelium
This commit is contained in:
693
src/utils/response_builder.rs
Normal file
693
src/utils/response_builder.rs
Normal 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"));
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user