forge_runtime/gateway/
response.rs1use axum::http::StatusCode;
2use axum::response::{IntoResponse, Response};
3use axum::Json;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct RpcResponse {
9 pub success: bool,
11 #[serde(skip_serializing_if = "Option::is_none")]
13 pub data: Option<serde_json::Value>,
14 #[serde(skip_serializing_if = "Option::is_none")]
16 pub error: Option<RpcError>,
17 #[serde(skip_serializing_if = "Option::is_none")]
19 pub request_id: Option<String>,
20}
21
22impl RpcResponse {
23 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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct RpcError {
68 pub code: String,
70 pub message: String,
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub details: Option<serde_json::Value>,
75}
76
77impl RpcError {
78 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 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 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 pub fn not_found(message: impl Into<String>) -> Self {
116 Self::new("NOT_FOUND", message)
117 }
118
119 pub fn unauthorized(message: impl Into<String>) -> Self {
121 Self::new("UNAUTHORIZED", message)
122 }
123
124 pub fn forbidden(message: impl Into<String>) -> Self {
126 Self::new("FORBIDDEN", message)
127 }
128
129 pub fn validation(message: impl Into<String>) -> Self {
131 Self::new("VALIDATION_ERROR", message)
132 }
133
134 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}