use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct ServiceResponse<T = serde_json::Value> {
pub(crate) success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) data: Option<T>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) error: Option<ServiceError>,
#[cfg(feature = "timestamp")]
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) timestamp: Option<i64>,
}
impl<T> ServiceResponse<T>
where
T: Serialize,
{
pub fn success(data: T) -> Self {
Self {
success: true,
data: Some(data),
error: None,
#[cfg(feature = "timestamp")]
timestamp: Some(chrono::Utc::now().timestamp()),
}
}
pub fn error(error: ServiceError) -> Self {
Self {
success: false,
data: None,
error: Some(error),
#[cfg(feature = "timestamp")]
timestamp: Some(chrono::Utc::now().timestamp()),
}
}
pub fn is_success(&self) -> bool {
self.success
}
pub fn data(&self) -> Option<&T> {
self.data.as_ref()
}
pub fn error_ref(&self) -> Option<&ServiceError> {
self.error.as_ref()
}
#[cfg(feature = "timestamp")]
pub fn timestamp(&self) -> Option<i64> {
self.timestamp
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServiceError {
pub(crate) code: String,
pub(crate) message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) details: Option<serde_json::Value>,
pub(crate) http_status: u16,
}
impl ServiceError {
pub fn new(code: impl Into<String>, message: impl Into<String>, http_status: u16) -> Self {
Self {
code: code.into(),
message: message.into(),
details: None,
http_status,
}
}
pub fn with_details(
code: impl Into<String>,
message: impl Into<String>,
details: serde_json::Value,
http_status: u16,
) -> Self {
Self {
code: code.into(),
message: message.into(),
details: Some(details),
http_status,
}
}
pub fn code(&self) -> &str {
&self.code
}
pub fn message(&self) -> &str {
&self.message
}
pub fn details(&self) -> Option<&serde_json::Value> {
self.details.as_ref()
}
pub fn http_status(&self) -> u16 {
self.http_status
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_service_response_success() {
let response = ServiceResponse::success("test data");
assert!(response.is_success());
assert_eq!(response.data(), Some(&"test data"));
assert!(response.error_ref().is_none());
}
#[test]
fn test_service_response_error_response() {
let error = ServiceError::new("TEST_ERROR", "Test error message", 400);
let response = ServiceResponse::<String>::error(error);
assert!(!response.is_success());
assert!(response.data.is_none());
assert!(response.error.is_some());
}
#[test]
fn test_service_response_generic() {
#[derive(Debug, Serialize, Deserialize)]
struct User {
name: String,
age: u32,
}
let user = User {
name: "Alice".to_string(),
age: 30,
};
let response = ServiceResponse::success(user);
assert!(response.is_success());
let data = response.data().unwrap();
assert_eq!(data.name, "Alice");
}
#[test]
fn test_service_error_new() {
let error = ServiceError::new("NOT_FOUND", "Resource not found", 404);
assert_eq!(error.code(), "NOT_FOUND");
assert_eq!(error.message(), "Resource not found");
assert_eq!(error.http_status(), 404);
assert!(error.details().is_none());
}
#[test]
fn test_service_error_with_details() {
let details = serde_json::json!({
"resource": "user",
"id": "123"
});
let error =
ServiceError::with_details("VALIDATION_ERROR", "Invalid input", details.clone(), 422);
assert_eq!(error.code(), "VALIDATION_ERROR");
assert_eq!(error.message(), "Invalid input");
assert_eq!(error.http_status(), 422);
assert_eq!(error.details(), Some(&details));
}
#[test]
fn test_service_error_accessors() {
let error =
ServiceError::with_details("TEST", "message", serde_json::json!({"key": "value"}), 500);
assert_eq!(error.code(), "TEST");
assert_eq!(error.message(), "message");
assert_eq!(error.http_status(), 500);
let details = error.details().unwrap();
assert_eq!(details["key"], "value");
}
#[test]
fn test_service_response_serialization() {
let response = ServiceResponse::success("data");
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("\"success\":true"));
assert!(json.contains("\"data\":\"data\""));
}
#[test]
fn test_service_error_serialization() {
let error = ServiceError::new("ERROR_CODE", "Error message", 500);
let json = serde_json::to_string(&error).unwrap();
assert!(json.contains("\"code\":\"ERROR_CODE\""));
assert!(json.contains("\"message\":\"Error message\""));
assert!(json.contains("\"http_status\":500"));
}
#[test]
fn test_service_response_no_timestamp() {
let response = ServiceResponse::success("data");
assert!(response.is_success());
assert!(response.data().is_some());
}
#[test]
fn test_service_response_deserialization() {
let json = r#"{"success":true,"data":"test"}"#;
let response: ServiceResponse<String> = serde_json::from_str(json).unwrap();
assert!(response.is_success());
assert_eq!(response.data(), Some(&"test".to_string()));
}
#[test]
fn test_service_error_deserialization() {
let json = r#"{"code":"ERR","message":"msg","http_status":400}"#;
let error: ServiceError = serde_json::from_str(json).unwrap();
assert_eq!(error.code(), "ERR");
assert_eq!(error.message(), "msg");
assert_eq!(error.http_status(), 400);
}
#[test]
fn test_service_response_error_details() {
let error = ServiceError::with_details(
"CODE",
"message",
serde_json::json!({"field": "value"}),
400,
);
let response = ServiceResponse::<String>::error(error);
assert!(!response.is_success());
assert!(response.data.is_none());
let err = response.error_ref().unwrap();
assert_eq!(err.code(), "CODE");
}
#[test]
fn test_service_error_new_has_no_details() {
let error = ServiceError::new("NOT_FOUND", "missing", 404);
assert!(error.details().is_none());
assert_eq!(error.details(), None);
}
#[test]
fn test_service_error_with_null_details() {
let error = ServiceError::with_details("ERR", "msg", serde_json::Value::Null, 500);
assert_eq!(error.details(), Some(&serde_json::Value::Null));
}
#[test]
fn test_service_response_success_has_no_error() {
let response = ServiceResponse::success("data");
assert!(response.error_ref().is_none());
}
#[test]
fn test_service_response_error_has_no_data() {
let error = ServiceError::new("ERR", "msg", 500);
let response = ServiceResponse::<String>::error(error);
assert!(response.data().is_none());
}
#[test]
fn test_service_error_http_status_various_codes() {
for status in [200u16, 400, 401, 403, 404, 422, 429, 500, 503] {
let error = ServiceError::new("CODE", "msg", status);
assert_eq!(error.http_status(), status);
}
}
#[test]
fn test_service_response_with_vec_serialization() {
let response = ServiceResponse::success(vec![1, 2, 3]);
let json = serde_json::to_string(&response).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["success"], true);
assert_eq!(parsed["data"], serde_json::json!([1, 2, 3]));
}
#[test]
fn test_service_error_serialization_omits_details_when_none() {
let error = ServiceError::new("CODE", "msg", 400);
let json = serde_json::to_string(&error).unwrap();
assert!(
!json.contains("details"),
"details should be omitted when None: {}",
json
);
}
#[test]
fn test_service_error_serialization_includes_details_when_some() {
let error =
ServiceError::with_details("CODE", "msg", serde_json::json!({"key": "value"}), 400);
let json = serde_json::to_string(&error).unwrap();
assert!(
json.contains("details"),
"details should be included when Some: {}",
json
);
}
#[test]
fn test_service_response_is_success_false_for_error() {
let error = ServiceError::new("ERR", "msg", 500);
let response = ServiceResponse::<String>::error(error);
assert!(!response.is_success());
}
#[test]
fn test_service_error_debug_format() {
let error = ServiceError::new("DEBUG_CODE", "debug message", 418);
let debug = format!("{:?}", error);
assert!(debug.contains("DEBUG_CODE"));
assert!(debug.contains("debug message"));
assert!(debug.contains("418"));
}
}