forge_runtime/gateway/
response.rs

1use axum::http::StatusCode;
2use axum::response::{IntoResponse, Response};
3use axum::Json;
4use serde::{Deserialize, Serialize};
5
6/// RPC response for function calls.
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct RpcResponse {
9    /// Whether the call succeeded.
10    pub success: bool,
11    /// Result data (if successful).
12    #[serde(skip_serializing_if = "Option::is_none")]
13    pub data: Option<serde_json::Value>,
14    /// Error information (if failed).
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub error: Option<RpcError>,
17    /// Request ID for tracing.
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub request_id: Option<String>,
20}
21
22impl RpcResponse {
23    /// Create a successful response.
24    pub fn success(data: serde_json::Value) -> Self {
25        Self {
26            success: true,
27            data: Some(data),
28            error: None,
29            request_id: None,
30        }
31    }
32
33    /// Create an error response.
34    pub fn error(error: RpcError) -> Self {
35        Self {
36            success: false,
37            data: None,
38            error: Some(error),
39            request_id: None,
40        }
41    }
42
43    /// Add request ID to the response.
44    pub fn with_request_id(mut self, request_id: impl Into<String>) -> Self {
45        self.request_id = Some(request_id.into());
46        self
47    }
48}
49
50impl IntoResponse for RpcResponse {
51    fn into_response(self) -> Response {
52        let status = if self.success {
53            StatusCode::OK
54        } else {
55            self.error
56                .as_ref()
57                .map(|e| e.status_code())
58                .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
59        };
60
61        (status, Json(self)).into_response()
62    }
63}
64
65/// RPC error information.
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct RpcError {
68    /// Error code.
69    pub code: String,
70    /// Human-readable error message.
71    pub message: String,
72    /// Additional error details.
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub details: Option<serde_json::Value>,
75}
76
77impl RpcError {
78    /// Create a new error.
79    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
80        Self {
81            code: code.into(),
82            message: message.into(),
83            details: None,
84        }
85    }
86
87    /// Create an error with details.
88    pub fn with_details(
89        code: impl Into<String>,
90        message: impl Into<String>,
91        details: serde_json::Value,
92    ) -> Self {
93        Self {
94            code: code.into(),
95            message: message.into(),
96            details: Some(details),
97        }
98    }
99
100    /// Get HTTP status code for this error.
101    pub fn status_code(&self) -> StatusCode {
102        match self.code.as_str() {
103            "NOT_FOUND" => StatusCode::NOT_FOUND,
104            "UNAUTHORIZED" => StatusCode::UNAUTHORIZED,
105            "FORBIDDEN" => StatusCode::FORBIDDEN,
106            "VALIDATION_ERROR" => StatusCode::BAD_REQUEST,
107            "INVALID_ARGUMENT" => StatusCode::BAD_REQUEST,
108            "TIMEOUT" => StatusCode::GATEWAY_TIMEOUT,
109            "RATE_LIMITED" => StatusCode::TOO_MANY_REQUESTS,
110            _ => StatusCode::INTERNAL_SERVER_ERROR,
111        }
112    }
113
114    /// Create a not found error.
115    pub fn not_found(message: impl Into<String>) -> Self {
116        Self::new("NOT_FOUND", message)
117    }
118
119    /// Create an unauthorized error.
120    pub fn unauthorized(message: impl Into<String>) -> Self {
121        Self::new("UNAUTHORIZED", message)
122    }
123
124    /// Create a forbidden error.
125    pub fn forbidden(message: impl Into<String>) -> Self {
126        Self::new("FORBIDDEN", message)
127    }
128
129    /// Create a validation error.
130    pub fn validation(message: impl Into<String>) -> Self {
131        Self::new("VALIDATION_ERROR", message)
132    }
133
134    /// Create an internal error.
135    pub fn internal(message: impl Into<String>) -> Self {
136        Self::new("INTERNAL_ERROR", message)
137    }
138}
139
140impl From<forge_core::error::ForgeError> for RpcError {
141    fn from(err: forge_core::error::ForgeError) -> Self {
142        match err {
143            forge_core::error::ForgeError::NotFound(msg) => Self::not_found(msg),
144            forge_core::error::ForgeError::Unauthorized(msg) => Self::unauthorized(msg),
145            forge_core::error::ForgeError::Forbidden(msg) => Self::forbidden(msg),
146            forge_core::error::ForgeError::Validation(msg) => Self::validation(msg),
147            forge_core::error::ForgeError::InvalidArgument(msg) => {
148                Self::new("INVALID_ARGUMENT", msg)
149            }
150            forge_core::error::ForgeError::Timeout(msg) => Self::new("TIMEOUT", msg),
151            forge_core::error::ForgeError::Database(msg) => {
152                Self::internal(format!("Database error: {}", msg))
153            }
154            forge_core::error::ForgeError::Function(msg) => {
155                Self::internal(format!("Function error: {}", msg))
156            }
157            _ => Self::internal(err.to_string()),
158        }
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    #[test]
167    fn test_success_response() {
168        let resp = RpcResponse::success(serde_json::json!({"id": 1}));
169        assert!(resp.success);
170        assert!(resp.data.is_some());
171        assert!(resp.error.is_none());
172    }
173
174    #[test]
175    fn test_error_response() {
176        let resp = RpcResponse::error(RpcError::not_found("User not found"));
177        assert!(!resp.success);
178        assert!(resp.data.is_none());
179        assert!(resp.error.is_some());
180        assert_eq!(resp.error.as_ref().unwrap().code, "NOT_FOUND");
181    }
182
183    #[test]
184    fn test_error_status_codes() {
185        assert_eq!(RpcError::not_found("").status_code(), StatusCode::NOT_FOUND);
186        assert_eq!(
187            RpcError::unauthorized("").status_code(),
188            StatusCode::UNAUTHORIZED
189        );
190        assert_eq!(RpcError::forbidden("").status_code(), StatusCode::FORBIDDEN);
191        assert_eq!(
192            RpcError::validation("").status_code(),
193            StatusCode::BAD_REQUEST
194        );
195        assert_eq!(
196            RpcError::internal("").status_code(),
197            StatusCode::INTERNAL_SERVER_ERROR
198        );
199    }
200
201    #[test]
202    fn test_with_request_id() {
203        let resp = RpcResponse::success(serde_json::json!(null)).with_request_id("req-123");
204        assert_eq!(resp.request_id, Some("req-123".to_string()));
205    }
206}