use super::rejection::RejectionCode;
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum ApiResponse<T> {
Success {
body: T,
},
#[serde(rename = "error")]
Rejected {
#[serde(rename = "error_details")]
details: ApiRejectedDetails,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ApiRejectedDetails {
pub reason: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rejection_code: Option<RejectionCode>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error_code: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error_log_id: Option<String>,
#[serde(skip)]
pub request_id: Option<String>,
}
impl fmt::Display for ApiRejectedDetails {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Reason: {}", self.reason)?;
if let Some(code) = &self.rejection_code {
write!(f, "\nRejection Code: {}", code)?;
}
if let Some(code) = &self.error_code {
write!(f, "\nError Code: {}", code)?;
}
if let Some(id) = &self.error_log_id {
write!(f, "\nError Log ID: {}", id)?;
}
if let Some(id) = &self.request_id {
write!(f, "\nRequest ID: {}", id)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_deserialize_success_response() {
let json = r#"{"status": "success", "body": {"value": 42}}"#;
let resp: ApiResponse<serde_json::Value> = serde_json::from_str(json).unwrap();
match resp {
ApiResponse::Success { body } => assert_eq!(body["value"], 42),
ApiResponse::Rejected { .. } => panic!("expected success"),
}
}
#[test]
fn test_deserialize_error_response_with_rejection() {
let json = r#"{
"status": "error",
"error_details": {
"reason": "Insufficient balance",
"rejection_code": "INSUFFICIENT_BALANCE",
"error_log_id": "LCERR_abc123"
}
}"#;
let resp: ApiResponse<serde_json::Value> = serde_json::from_str(json).unwrap();
match resp {
ApiResponse::Rejected {
details: error_details,
} => {
assert_eq!(error_details.reason, "Insufficient balance");
assert_eq!(
error_details.rejection_code,
Some(RejectionCode::InsufficientBalance)
);
assert_eq!(error_details.error_log_id, Some("LCERR_abc123".to_string()));
assert_eq!(error_details.error_code, None);
}
ApiResponse::Success { .. } => panic!("expected error"),
}
}
#[test]
fn test_deserialize_error_response_with_error_code() {
let json = r#"{
"status": "error",
"error_details": {
"reason": "Market not found",
"error_code": "NOT_FOUND"
}
}"#;
let resp: ApiResponse<serde_json::Value> = serde_json::from_str(json).unwrap();
match resp {
ApiResponse::Rejected {
details: error_details,
} => {
assert_eq!(error_details.reason, "Market not found");
assert_eq!(error_details.error_code, Some("NOT_FOUND".to_string()));
assert_eq!(error_details.rejection_code, None);
}
ApiResponse::Success { .. } => panic!("expected error"),
}
}
#[test]
fn test_display_all_fields() {
let details = ApiRejectedDetails {
reason: "Not enough funds".to_string(),
rejection_code: Some(RejectionCode::InsufficientBalance),
error_code: None,
error_log_id: Some("LCERR_abc".to_string()),
request_id: Some("req-123".to_string()),
};
let text = format!("{}", details);
assert!(text.contains("Reason: Not enough funds"));
assert!(text.contains("Rejection Code: Insufficient Balance"));
assert!(text.contains("Error Log ID: LCERR_abc"));
assert!(text.contains("Request ID: req-123"));
}
#[test]
fn test_display_reason_only() {
let details = ApiRejectedDetails {
reason: "Something broke".to_string(),
rejection_code: None,
error_code: None,
error_log_id: None,
request_id: None,
};
assert_eq!(format!("{}", details), "Reason: Something broke");
}
}