use super::ApiError;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};
pub const API_VERSION: &str = "1.0";
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ApiResponse<T: Serialize> {
pub api_version: &'static str,
pub timestamp: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub request_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub command: Option<String>,
pub success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<T>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<ApiError>,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(transparent)]
pub struct AnyJson(pub serde_json::Value);
impl JsonSchema for AnyJson {
fn schema_name() -> String {
"AnyJson".to_string()
}
fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
schemars::schema::Schema::Bool(true)
}
}
impl<T: Serialize> ApiResponse<T> {
#[must_use]
pub fn ok(command: impl Into<String>, data: T) -> Self {
Self {
api_version: API_VERSION,
timestamp: current_timestamp(),
request_id: None,
command: Some(command.into()),
success: true,
data: Some(data),
error: None,
}
}
#[must_use]
pub fn ok_data(data: T) -> Self {
Self {
api_version: API_VERSION,
timestamp: current_timestamp(),
request_id: None,
command: None,
success: true,
data: Some(data),
error: None,
}
}
#[must_use]
pub fn with_request_id(mut self, id: impl Into<String>) -> Self {
self.request_id = Some(id.into());
self
}
#[must_use]
pub fn with_command(mut self, command: impl Into<String>) -> Self {
self.command = Some(command.into());
self
}
}
impl<T: Serialize> ApiResponse<T> {
#[must_use]
pub fn err(command: impl Into<String>, error: ApiError) -> Self {
Self {
api_version: API_VERSION,
timestamp: current_timestamp(),
request_id: None,
command: Some(command.into()),
success: false,
data: None,
error: Some(error),
}
}
#[must_use]
pub fn err_only(error: ApiError) -> Self {
Self {
api_version: API_VERSION,
timestamp: current_timestamp(),
request_id: None,
command: None,
success: false,
data: None,
error: Some(error),
}
}
}
impl ApiResponse<()> {
#[must_use]
pub fn ok_empty(command: impl Into<String>) -> Self {
Self {
api_version: API_VERSION,
timestamp: current_timestamp(),
request_id: None,
command: Some(command.into()),
success: true,
data: None,
error: None,
}
}
}
fn current_timestamp() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
impl<T: Serialize, E: Into<ApiError>> From<Result<T, E>> for ApiResponse<T> {
fn from(result: Result<T, E>) -> Self {
match result {
Ok(data) => Self::ok_data(data),
Err(e) => Self::err_only(e.into()),
}
}
}
#[allow(dead_code)]
pub struct ApiResponseBuilder<T: Serialize> {
command: Option<String>,
request_id: Option<String>,
data: Option<T>,
error: Option<ApiError>,
}
#[allow(dead_code)]
impl<T: Serialize> ApiResponseBuilder<T> {
#[must_use]
pub fn new() -> Self {
Self {
command: None,
request_id: None,
data: None,
error: None,
}
}
#[must_use]
pub fn command(mut self, cmd: impl Into<String>) -> Self {
self.command = Some(cmd.into());
self
}
#[must_use]
pub fn request_id(mut self, id: impl Into<String>) -> Self {
self.request_id = Some(id.into());
self
}
#[must_use]
pub fn data(mut self, data: T) -> Self {
self.data = Some(data);
self.error = None;
self
}
#[must_use]
pub fn error(mut self, error: ApiError) -> Self {
self.error = Some(error);
self.data = None;
self
}
#[must_use]
pub fn build(self) -> ApiResponse<T> {
let success = self.data.is_some();
ApiResponse {
api_version: API_VERSION,
timestamp: current_timestamp(),
request_id: self.request_id,
command: self.command,
success,
data: self.data,
error: self.error,
}
}
}
impl<T: Serialize> Default for ApiResponseBuilder<T> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ErrorCode;
#[test]
fn test_ok_response() {
let response = ApiResponse::ok("test", "hello");
assert!(response.success);
assert_eq!(response.data, Some("hello"));
assert!(response.error.is_none());
assert_eq!(response.api_version, "1.0");
assert!(response.timestamp > 0);
}
#[test]
fn test_err_response() {
let error = ApiError::from_code(ErrorCode::ConfigNotFound);
let response: ApiResponse<()> = ApiResponse::err("config show", error);
assert!(!response.success);
assert!(response.data.is_none());
assert!(response.error.is_some());
assert_eq!(response.error.as_ref().unwrap().code, "RCH-E001");
}
#[test]
fn test_ok_empty() {
let response = ApiResponse::ok_empty("shutdown");
assert!(response.success);
assert!(response.data.is_none());
assert!(response.error.is_none());
}
#[test]
fn test_with_request_id() {
let response = ApiResponse::ok("test", "data").with_request_id("req-123");
assert_eq!(response.request_id, Some("req-123".to_string()));
}
#[test]
fn test_serialization_success() {
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct Data {
count: u32,
}
let response = ApiResponse::ok("count", Data { count: 42 });
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("\"api_version\":\"1.0\""));
assert!(json.contains("\"success\":true"));
assert!(json.contains("\"count\":42"));
assert!(!json.contains("\"error\""));
}
#[test]
fn test_serialization_error() {
let error = ApiError::from_code(ErrorCode::SshConnectionFailed)
.with_context("worker", "test-worker");
let response: ApiResponse<()> = ApiResponse::err("probe", error);
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("\"success\":false"));
assert!(json.contains("\"code\":\"RCH-E100\""));
assert!(json.contains("\"worker\":\"test-worker\""));
assert!(!json.contains("\"data\""));
}
#[test]
fn test_builder() {
let response: ApiResponse<String> = ApiResponseBuilder::new()
.command("workers list")
.request_id("req-456")
.data("test data".to_string())
.build();
assert!(response.success);
assert_eq!(response.command, Some("workers list".to_string()));
assert_eq!(response.request_id, Some("req-456".to_string()));
assert_eq!(response.data, Some("test data".to_string()));
}
#[test]
fn test_from_result_ok() {
let result: Result<String, ApiError> = Ok("success".to_string());
let response: ApiResponse<String> = result.into();
assert!(response.success);
assert_eq!(response.data, Some("success".to_string()));
}
#[test]
fn test_from_result_err() {
let result: Result<String, ApiError> = Err(ApiError::from_code(ErrorCode::ConfigNotFound));
let response: ApiResponse<String> = result.into();
assert!(!response.success);
assert!(response.error.is_some());
}
}