use axum::{
http::{header, HeaderValue, StatusCode},
response::{IntoResponse, Response},
Json,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug)]
pub struct Created<T> {
data: T,
location: Option<String>,
}
impl<T> Created<T> {
pub fn new(data: T) -> Self {
Self {
data,
location: None,
}
}
pub fn with_location(mut self, location: impl Into<String>) -> Self {
self.location = Some(location.into());
self
}
}
impl<T: Serialize> IntoResponse for Created<T> {
fn into_response(self) -> Response {
let mut response = (StatusCode::CREATED, Json(&self.data)).into_response();
if let Some(location) = self.location {
if let Ok(header_value) = HeaderValue::from_str(&location) {
response
.headers_mut()
.insert(header::LOCATION, header_value);
}
}
response
}
}
#[derive(Debug, Clone, Copy)]
pub struct NoContent;
impl IntoResponse for NoContent {
fn into_response(self) -> Response {
StatusCode::NO_CONTENT.into_response()
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Conflict {
error: String,
#[serde(skip_serializing_if = "Option::is_none")]
code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
detail: Option<String>,
status: u16,
}
impl Conflict {
pub fn new(error: impl Into<String>) -> Self {
Self {
error: error.into(),
code: None,
detail: None,
status: StatusCode::CONFLICT.as_u16(),
}
}
pub fn with_code(mut self, code: impl Into<String>) -> Self {
self.code = Some(code.into());
self
}
pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
self.detail = Some(detail.into());
self
}
}
impl IntoResponse for Conflict {
fn into_response(self) -> Response {
(StatusCode::CONFLICT, Json(self)).into_response()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FieldError {
pub field: String,
pub code: String,
pub message: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ValidationError {
error: String,
code: String,
status: u16,
pub errors: HashMap<String, Vec<FieldError>>,
}
impl ValidationError {
pub fn new(error: impl Into<String>) -> Self {
Self {
error: error.into(),
code: "VALIDATION_ERROR".to_string(),
status: StatusCode::UNPROCESSABLE_ENTITY.as_u16(),
errors: HashMap::new(),
}
}
pub fn add_field_error(
&mut self,
field: impl Into<String>,
code: impl Into<String>,
message: impl Into<String>,
) {
let field = field.into();
let error = FieldError {
field: field.clone(),
code: code.into(),
message: message.into(),
};
self.errors.entry(field).or_default().push(error);
}
pub fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
pub fn error_count(&self) -> usize {
self.errors.values().map(|v| v.len()).sum()
}
}
impl IntoResponse for ValidationError {
fn into_response(self) -> Response {
(StatusCode::UNPROCESSABLE_ENTITY, Json(self)).into_response()
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Success<T> {
data: T,
#[serde(skip_serializing_if = "Option::is_none")]
message: Option<String>,
}
impl<T> Success<T> {
pub fn new(data: T) -> Self {
Self {
data,
message: None,
}
}
pub fn with_message(mut self, message: impl Into<String>) -> Self {
self.message = Some(message.into());
self
}
}
impl<T: Serialize> IntoResponse for Success<T> {
fn into_response(self) -> Response {
(StatusCode::OK, Json(self)).into_response()
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Accepted {
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
status_url: Option<String>,
status: u16,
}
impl Accepted {
pub fn new() -> Self {
Self {
message: "Request accepted for processing".to_string(),
status_url: None,
status: StatusCode::ACCEPTED.as_u16(),
}
}
pub fn with_message(mut self, message: impl Into<String>) -> Self {
self.message = message.into();
self
}
pub fn with_status_url(mut self, url: impl Into<String>) -> Self {
self.status_url = Some(url.into());
self
}
}
impl Default for Accepted {
fn default() -> Self {
Self::new()
}
}
impl IntoResponse for Accepted {
fn into_response(self) -> Response {
(StatusCode::ACCEPTED, Json(self)).into_response()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Serialize, Deserialize)]
struct TestData {
id: u64,
name: String,
}
#[test]
fn test_created_response() {
let data = TestData {
id: 1,
name: "Test".to_string(),
};
let response = Created::new(data).with_location("/test/1");
let response = response.into_response();
assert_eq!(response.status(), StatusCode::CREATED);
}
#[test]
fn test_no_content_response() {
let response = NoContent.into_response();
assert_eq!(response.status(), StatusCode::NO_CONTENT);
}
#[test]
fn test_conflict_response() {
let conflict = Conflict::new("Resource already exists")
.with_code("DUPLICATE")
.with_detail("A resource with this ID already exists");
assert_eq!(conflict.status, 409);
assert_eq!(conflict.code, Some("DUPLICATE".to_string()));
}
#[test]
fn test_validation_error() {
let mut error = ValidationError::new("Validation failed");
error.add_field_error("email", "REQUIRED", "Email is required");
error.add_field_error("email", "INVALID_FORMAT", "Invalid email format");
error.add_field_error("password", "TOO_SHORT", "Password too short");
assert!(error.has_errors());
assert_eq!(error.error_count(), 3);
assert_eq!(error.errors.get("email").unwrap().len(), 2);
assert_eq!(error.errors.get("password").unwrap().len(), 1);
}
#[test]
fn test_success_response() {
let data = TestData {
id: 1,
name: "Test".to_string(),
};
let success = Success::new(data).with_message("Operation successful");
assert_eq!(success.message, Some("Operation successful".to_string()));
}
#[test]
fn test_accepted_response() {
let accepted = Accepted::new()
.with_message("Processing started")
.with_status_url("/status/123");
assert_eq!(accepted.status, 202);
assert_eq!(accepted.status_url, Some("/status/123".to_string()));
}
}