1use super::ApiError;
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9use std::time::{SystemTime, UNIX_EPOCH};
10
11pub const API_VERSION: &str = "1.0";
16
17#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
46pub struct ApiResponse<T: Serialize> {
47 pub api_version: &'static str,
49
50 pub timestamp: u64,
52
53 #[serde(skip_serializing_if = "Option::is_none")]
55 pub request_id: Option<String>,
56
57 #[serde(skip_serializing_if = "Option::is_none")]
59 pub command: Option<String>,
60
61 pub success: bool,
63
64 #[serde(skip_serializing_if = "Option::is_none")]
66 pub data: Option<T>,
67
68 #[serde(skip_serializing_if = "Option::is_none")]
70 pub error: Option<ApiError>,
71}
72
73#[allow(dead_code)]
78#[derive(Debug, Clone, Serialize, Deserialize)]
79#[serde(transparent)]
80pub struct AnyJson(pub serde_json::Value);
81
82impl JsonSchema for AnyJson {
83 fn schema_name() -> String {
84 "AnyJson".to_string()
85 }
86
87 fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
88 schemars::schema::Schema::Bool(true)
90 }
91}
92
93impl<T: Serialize> ApiResponse<T> {
94 #[must_use]
101 pub fn ok(command: impl Into<String>, data: T) -> Self {
102 Self {
103 api_version: API_VERSION,
104 timestamp: current_timestamp(),
105 request_id: None,
106 command: Some(command.into()),
107 success: true,
108 data: Some(data),
109 error: None,
110 }
111 }
112
113 #[must_use]
117 pub fn ok_data(data: T) -> Self {
118 Self {
119 api_version: API_VERSION,
120 timestamp: current_timestamp(),
121 request_id: None,
122 command: None,
123 success: true,
124 data: Some(data),
125 error: None,
126 }
127 }
128
129 #[must_use]
131 pub fn with_request_id(mut self, id: impl Into<String>) -> Self {
132 self.request_id = Some(id.into());
133 self
134 }
135
136 #[must_use]
138 pub fn with_command(mut self, command: impl Into<String>) -> Self {
139 self.command = Some(command.into());
140 self
141 }
142}
143
144impl<T: Serialize> ApiResponse<T> {
145 #[must_use]
152 pub fn err(command: impl Into<String>, error: ApiError) -> Self {
153 Self {
154 api_version: API_VERSION,
155 timestamp: current_timestamp(),
156 request_id: None,
157 command: Some(command.into()),
158 success: false,
159 data: None,
160 error: Some(error),
161 }
162 }
163
164 #[must_use]
168 pub fn err_only(error: ApiError) -> Self {
169 Self {
170 api_version: API_VERSION,
171 timestamp: current_timestamp(),
172 request_id: None,
173 command: None,
174 success: false,
175 data: None,
176 error: Some(error),
177 }
178 }
179}
180
181impl ApiResponse<()> {
182 #[must_use]
184 pub fn ok_empty(command: impl Into<String>) -> Self {
185 Self {
186 api_version: API_VERSION,
187 timestamp: current_timestamp(),
188 request_id: None,
189 command: Some(command.into()),
190 success: true,
191 data: None,
192 error: None,
193 }
194 }
195}
196
197fn current_timestamp() -> u64 {
199 SystemTime::now()
200 .duration_since(UNIX_EPOCH)
201 .map(|d| d.as_secs())
202 .unwrap_or(0)
203}
204
205impl<T: Serialize, E: Into<ApiError>> From<Result<T, E>> for ApiResponse<T> {
210 fn from(result: Result<T, E>) -> Self {
211 match result {
212 Ok(data) => Self::ok_data(data),
213 Err(e) => Self::err_only(e.into()),
214 }
215 }
216}
217
218#[allow(dead_code)]
224pub struct ApiResponseBuilder<T: Serialize> {
225 command: Option<String>,
226 request_id: Option<String>,
227 data: Option<T>,
228 error: Option<ApiError>,
229}
230
231#[allow(dead_code)]
232impl<T: Serialize> ApiResponseBuilder<T> {
233 #[must_use]
235 pub fn new() -> Self {
236 Self {
237 command: None,
238 request_id: None,
239 data: None,
240 error: None,
241 }
242 }
243
244 #[must_use]
246 pub fn command(mut self, cmd: impl Into<String>) -> Self {
247 self.command = Some(cmd.into());
248 self
249 }
250
251 #[must_use]
253 pub fn request_id(mut self, id: impl Into<String>) -> Self {
254 self.request_id = Some(id.into());
255 self
256 }
257
258 #[must_use]
260 pub fn data(mut self, data: T) -> Self {
261 self.data = Some(data);
262 self.error = None;
263 self
264 }
265
266 #[must_use]
268 pub fn error(mut self, error: ApiError) -> Self {
269 self.error = Some(error);
270 self.data = None;
271 self
272 }
273
274 #[must_use]
276 pub fn build(self) -> ApiResponse<T> {
277 let success = self.data.is_some();
278 ApiResponse {
279 api_version: API_VERSION,
280 timestamp: current_timestamp(),
281 request_id: self.request_id,
282 command: self.command,
283 success,
284 data: self.data,
285 error: self.error,
286 }
287 }
288}
289
290impl<T: Serialize> Default for ApiResponseBuilder<T> {
291 fn default() -> Self {
292 Self::new()
293 }
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299 use crate::ErrorCode;
300
301 #[test]
302 fn test_ok_response() {
303 let response = ApiResponse::ok("test", "hello");
304 assert!(response.success);
305 assert_eq!(response.data, Some("hello"));
306 assert!(response.error.is_none());
307 assert_eq!(response.api_version, "1.0");
308 assert!(response.timestamp > 0);
309 }
310
311 #[test]
312 fn test_err_response() {
313 let error = ApiError::from_code(ErrorCode::ConfigNotFound);
314 let response: ApiResponse<()> = ApiResponse::err("config show", error);
315 assert!(!response.success);
316 assert!(response.data.is_none());
317 assert!(response.error.is_some());
318 assert_eq!(response.error.as_ref().unwrap().code, "RCH-E001");
319 }
320
321 #[test]
322 fn test_ok_empty() {
323 let response = ApiResponse::ok_empty("shutdown");
324 assert!(response.success);
325 assert!(response.data.is_none());
326 assert!(response.error.is_none());
327 }
328
329 #[test]
330 fn test_with_request_id() {
331 let response = ApiResponse::ok("test", "data").with_request_id("req-123");
332 assert_eq!(response.request_id, Some("req-123".to_string()));
333 }
334
335 #[test]
336 fn test_serialization_success() {
337 #[derive(Serialize, Deserialize, PartialEq, Debug)]
338 struct Data {
339 count: u32,
340 }
341
342 let response = ApiResponse::ok("count", Data { count: 42 });
343 let json = serde_json::to_string(&response).unwrap();
344
345 assert!(json.contains("\"api_version\":\"1.0\""));
346 assert!(json.contains("\"success\":true"));
347 assert!(json.contains("\"count\":42"));
348 assert!(!json.contains("\"error\""));
349 }
350
351 #[test]
352 fn test_serialization_error() {
353 let error = ApiError::from_code(ErrorCode::SshConnectionFailed)
354 .with_context("worker", "test-worker");
355 let response: ApiResponse<()> = ApiResponse::err("probe", error);
356 let json = serde_json::to_string(&response).unwrap();
357
358 assert!(json.contains("\"success\":false"));
359 assert!(json.contains("\"code\":\"RCH-E100\""));
360 assert!(json.contains("\"worker\":\"test-worker\""));
361 assert!(!json.contains("\"data\""));
362 }
363
364 #[test]
365 fn test_builder() {
366 let response: ApiResponse<String> = ApiResponseBuilder::new()
367 .command("workers list")
368 .request_id("req-456")
369 .data("test data".to_string())
370 .build();
371
372 assert!(response.success);
373 assert_eq!(response.command, Some("workers list".to_string()));
374 assert_eq!(response.request_id, Some("req-456".to_string()));
375 assert_eq!(response.data, Some("test data".to_string()));
376 }
377
378 #[test]
379 fn test_from_result_ok() {
380 let result: Result<String, ApiError> = Ok("success".to_string());
381 let response: ApiResponse<String> = result.into();
382 assert!(response.success);
383 assert_eq!(response.data, Some("success".to_string()));
384 }
385
386 #[test]
387 fn test_from_result_err() {
388 let result: Result<String, ApiError> = Err(ApiError::from_code(ErrorCode::ConfigNotFound));
389 let response: ApiResponse<String> = result.into();
390 assert!(!response.success);
391 assert!(response.error.is_some());
392 }
393}