Skip to main content

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}