rch_common/api/mod.rs
1//! Unified API Types for Remote Compilation Helper
2//!
3//! This module provides a consistent API interface for both CLI and daemon,
4//! enabling agents and tools to parse responses uniformly.
5//!
6//! # Design Principles
7//!
8//! 1. **Unified Error Codes**: All errors use the `RCH-Exxx` format from [`crate::ErrorCode`].
9//! 2. **Consistent Envelope**: All responses wrapped in [`ApiResponse<T>`].
10//! 3. **Machine-Readable**: Designed for programmatic parsing by AI agents.
11//! 4. **Backward Compatible**: Supports legacy error codes via mapping.
12//!
13//! # Example
14//!
15//! ```rust
16//! use rch_common::api::{ApiResponse, ApiError};
17//! use rch_common::ErrorCode;
18//!
19//! // Success response
20//! let response = ApiResponse::ok("workers list", vec!["worker1", "worker2"]);
21//! println!("{}", serde_json::to_string_pretty(&response).unwrap());
22//!
23//! // Error response
24//! let response: ApiResponse<()> = ApiResponse::err(
25//! "workers probe",
26//! ApiError::from_code(ErrorCode::SshConnectionFailed)
27//! .with_message("Connection refused to worker-1")
28//! .with_context("worker_id", "worker-1"),
29//! );
30//! println!("{}", serde_json::to_string_pretty(&response).unwrap());
31//! ```
32//!
33//! # JSON Output Format
34//!
35//! Success:
36//! ```json
37//! {
38//! "api_version": "1.0",
39//! "timestamp": 1705936800,
40//! "success": true,
41//! "data": { ... }
42//! }
43//! ```
44//!
45//! Error:
46//! ```json
47//! {
48//! "api_version": "1.0",
49//! "timestamp": 1705936800,
50//! "success": false,
51//! "error": {
52//! "code": "RCH-E100",
53//! "category": "network",
54//! "message": "SSH connection to worker failed",
55//! "details": "Connection refused to worker-1",
56//! "remediation": ["Check worker is running", "Verify SSH key"],
57//! "context": { "worker_id": "worker-1" }
58//! }
59//! }
60//! ```
61
62mod error;
63mod response;
64pub mod schema;
65
66pub use error::{ApiError, ErrorContext, LegacyErrorCode};
67pub use response::{API_VERSION, ApiResponse};
68pub use schema::{
69 ErrorCatalog, ErrorCategoryEntry, ErrorCodeEntry, SchemaExportResult, export_schemas,
70 generate_api_error_schema, generate_api_response_schema, generate_error_catalog,
71};
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76 use crate::ErrorCode;
77
78 #[test]
79 fn test_success_response_serialization() {
80 let response = ApiResponse::ok("test command", "test data");
81 let json = serde_json::to_string(&response).unwrap();
82 assert!(json.contains("\"success\":true"));
83 assert!(json.contains("\"api_version\":\"1.0\""));
84 assert!(json.contains("\"data\":\"test data\""));
85 }
86
87 #[test]
88 fn test_error_response_serialization() {
89 let error = ApiError::from_code(ErrorCode::ConfigNotFound);
90 let response: ApiResponse<()> = ApiResponse::err("config show", error);
91 let json = serde_json::to_string(&response).unwrap();
92 assert!(json.contains("\"success\":false"));
93 assert!(json.contains("\"code\":\"RCH-E001\""));
94 assert!(json.contains("\"category\":\"config\""));
95 }
96
97 #[test]
98 fn test_legacy_code_mapping() {
99 let legacy = LegacyErrorCode::WorkerUnreachable;
100 let modern = legacy.to_error_code();
101 assert_eq!(modern, ErrorCode::SshConnectionFailed);
102 }
103
104 #[test]
105 fn test_error_with_context() {
106 let error = ApiError::from_code(ErrorCode::WorkerNoneAvailable)
107 .with_message("All workers are busy")
108 .with_context("requested_slots", "8")
109 .with_context("available_workers", "0");
110
111 let json = serde_json::to_string(&error).unwrap();
112 assert!(json.contains("\"requested_slots\":\"8\""));
113 assert!(json.contains("\"available_workers\":\"0\""));
114 }
115
116 #[test]
117 fn test_response_with_request_id() {
118 let response = ApiResponse::ok("status", "healthy").with_request_id("req-12345");
119 let json = serde_json::to_string(&response).unwrap();
120 assert!(json.contains("\"request_id\":\"req-12345\""));
121 }
122}