rustapi_core/
response.rs

1//! Response types for RustAPI
2
3use crate::error::{ApiError, ErrorResponse};
4use bytes::Bytes;
5use http::{header, HeaderMap, HeaderValue, StatusCode};
6use http_body_util::Full;
7use serde::Serialize;
8use rustapi_openapi::{Operation, ResponseModifier, ResponseSpec, MediaType, SchemaRef, Schema};
9use std::collections::HashMap;
10
11/// HTTP Response type
12pub type Response = http::Response<Full<Bytes>>;
13
14/// Trait for types that can be converted into an HTTP response
15pub trait IntoResponse {
16    /// Convert self into a Response
17    fn into_response(self) -> Response;
18}
19
20// Implement for Response itself
21impl IntoResponse for Response {
22    fn into_response(self) -> Response {
23        self
24    }
25}
26
27// Implement for () - returns 200 OK with empty body
28impl IntoResponse for () {
29    fn into_response(self) -> Response {
30        http::Response::builder()
31            .status(StatusCode::OK)
32            .body(Full::new(Bytes::new()))
33            .unwrap()
34    }
35}
36
37// Implement for &'static str
38impl IntoResponse for &'static str {
39    fn into_response(self) -> Response {
40        http::Response::builder()
41            .status(StatusCode::OK)
42            .header(header::CONTENT_TYPE, "text/plain; charset=utf-8")
43            .body(Full::new(Bytes::from(self)))
44            .unwrap()
45    }
46}
47
48// Implement for String
49impl IntoResponse for String {
50    fn into_response(self) -> Response {
51        http::Response::builder()
52            .status(StatusCode::OK)
53            .header(header::CONTENT_TYPE, "text/plain; charset=utf-8")
54            .body(Full::new(Bytes::from(self)))
55            .unwrap()
56    }
57}
58
59// Implement for StatusCode
60impl IntoResponse for StatusCode {
61    fn into_response(self) -> Response {
62        http::Response::builder()
63            .status(self)
64            .body(Full::new(Bytes::new()))
65            .unwrap()
66    }
67}
68
69// Implement for (StatusCode, impl IntoResponse)
70impl<R: IntoResponse> IntoResponse for (StatusCode, R) {
71    fn into_response(self) -> Response {
72        let mut response = self.1.into_response();
73        *response.status_mut() = self.0;
74        response
75    }
76}
77
78// Implement for (StatusCode, HeaderMap, impl IntoResponse)
79impl<R: IntoResponse> IntoResponse for (StatusCode, HeaderMap, R) {
80    fn into_response(self) -> Response {
81        let mut response = self.2.into_response();
82        *response.status_mut() = self.0;
83        response.headers_mut().extend(self.1);
84        response
85    }
86}
87
88// Implement for Result<T, E> where both implement IntoResponse
89impl<T: IntoResponse, E: IntoResponse> IntoResponse for Result<T, E> {
90    fn into_response(self) -> Response {
91        match self {
92            Ok(v) => v.into_response(),
93            Err(e) => e.into_response(),
94        }
95    }
96}
97
98// Implement for ApiError
99// Implement for ApiError
100impl IntoResponse for ApiError {
101    fn into_response(self) -> Response {
102        let status = self.status;
103        let error_response = ErrorResponse::from(self);
104        let body = serde_json::to_vec(&error_response).unwrap_or_else(|_| {
105            br#"{"error":{"type":"internal_error","message":"Failed to serialize error"}}"#.to_vec()
106        });
107
108        http::Response::builder()
109            .status(status)
110            .header(header::CONTENT_TYPE, "application/json")
111            .body(Full::new(Bytes::from(body)))
112            .unwrap()
113    }
114}
115
116impl ResponseModifier for ApiError {
117    fn update_response(op: &mut Operation) {
118        // We define common error responses here
119        // 400 Bad Request
120        op.responses.insert("400".to_string(), ResponseSpec {
121            description: "Bad Request".to_string(),
122            content: {
123                let mut map = HashMap::new();
124                map.insert("application/json".to_string(), MediaType {
125                    schema: SchemaRef::Ref { reference: "#/components/schemas/ErrorSchema".to_string() },
126                });
127                Some(map)
128            },
129            ..Default::default()
130        });
131        
132        // 500 Internal Server Error
133        op.responses.insert("500".to_string(), ResponseSpec {
134            description: "Internal Server Error".to_string(),
135            content: {
136                let mut map = HashMap::new();
137                map.insert("application/json".to_string(), MediaType {
138                    schema: SchemaRef::Ref { reference: "#/components/schemas/ErrorSchema".to_string() },
139                });
140                Some(map)
141            },
142            ..Default::default()
143        });
144    }
145}
146
147/// 201 Created response wrapper
148///
149/// Returns HTTP 201 with JSON body.
150///
151/// # Example
152///
153/// ```rust,ignore
154/// async fn create_user(body: UserIn) -> Result<Created<UserOut>> {
155///     let user = db.create(body).await?;
156///     Ok(Created(user))
157/// }
158/// ```
159#[derive(Debug, Clone)]
160pub struct Created<T>(pub T);
161
162impl<T: Serialize> IntoResponse for Created<T> {
163    fn into_response(self) -> Response {
164        match serde_json::to_vec(&self.0) {
165            Ok(body) => http::Response::builder()
166                .status(StatusCode::CREATED)
167                .header(header::CONTENT_TYPE, "application/json")
168                .body(Full::new(Bytes::from(body)))
169                .unwrap(),
170            Err(err) => ApiError::internal(format!("Failed to serialize response: {}", err))
171                .into_response(),
172        }
173    }
174}
175
176impl<T: for<'a> Schema<'a>> ResponseModifier for Created<T> {
177    fn update_response(op: &mut Operation) {
178        let (name, _) = T::schema();
179        
180        let schema_ref = SchemaRef::Ref {
181            reference: format!("#/components/schemas/{}", name),
182        };
183        
184        op.responses.insert("201".to_string(), ResponseSpec {
185            description: "Created".to_string(),
186            content: {
187                let mut map = HashMap::new();
188                map.insert("application/json".to_string(), MediaType {
189                    schema: schema_ref,
190                });
191                Some(map)
192            },
193            ..Default::default()
194        });
195    }
196}
197
198/// 204 No Content response
199///
200/// Returns HTTP 204 with empty body.
201///
202/// # Example
203///
204/// ```rust,ignore
205/// async fn delete_user(id: i64) -> Result<NoContent> {
206///     db.delete(id).await?;
207///     Ok(NoContent)
208/// }
209/// ```
210#[derive(Debug, Clone, Copy)]
211pub struct NoContent;
212
213impl IntoResponse for NoContent {
214    fn into_response(self) -> Response {
215        http::Response::builder()
216            .status(StatusCode::NO_CONTENT)
217            .body(Full::new(Bytes::new()))
218            .unwrap()
219    }
220}
221
222impl ResponseModifier for NoContent {
223    fn update_response(op: &mut Operation) {
224        op.responses.insert("204".to_string(), ResponseSpec {
225            description: "No Content".to_string(),
226            ..Default::default()
227        });
228    }
229}
230
231/// HTML response wrapper
232#[derive(Debug, Clone)]
233pub struct Html<T>(pub T);
234
235impl<T: Into<String>> IntoResponse for Html<T> {
236    fn into_response(self) -> Response {
237        http::Response::builder()
238            .status(StatusCode::OK)
239            .header(header::CONTENT_TYPE, "text/html; charset=utf-8")
240            .body(Full::new(Bytes::from(self.0.into())))
241            .unwrap()
242    }
243}
244
245impl<T> ResponseModifier for Html<T> {
246    fn update_response(op: &mut Operation) {
247        op.responses.insert("200".to_string(), ResponseSpec {
248            description: "HTML Content".to_string(),
249            content: {
250                let mut map = HashMap::new();
251                map.insert("text/html".to_string(), MediaType {
252                    schema: SchemaRef::Inline(serde_json::json!({ "type": "string" })),
253                });
254                Some(map)
255            },
256            ..Default::default()
257        });
258    }
259}
260
261/// Redirect response
262#[derive(Debug, Clone)]
263pub struct Redirect {
264    status: StatusCode,
265    location: HeaderValue,
266}
267
268impl Redirect {
269    /// Create a 302 Found redirect
270    pub fn to(uri: &str) -> Self {
271        Self {
272            status: StatusCode::FOUND,
273            location: HeaderValue::from_str(uri).expect("Invalid redirect URI"),
274        }
275    }
276
277    /// Create a 301 Permanent redirect
278    pub fn permanent(uri: &str) -> Self {
279        Self {
280            status: StatusCode::MOVED_PERMANENTLY,
281            location: HeaderValue::from_str(uri).expect("Invalid redirect URI"),
282        }
283    }
284
285    /// Create a 307 Temporary redirect
286    pub fn temporary(uri: &str) -> Self {
287        Self {
288            status: StatusCode::TEMPORARY_REDIRECT,
289            location: HeaderValue::from_str(uri).expect("Invalid redirect URI"),
290        }
291    }
292}
293
294impl IntoResponse for Redirect {
295    fn into_response(self) -> Response {
296        http::Response::builder()
297            .status(self.status)
298            .header(header::LOCATION, self.location)
299            .body(Full::new(Bytes::new()))
300            .unwrap()
301    }
302}
303
304impl ResponseModifier for Redirect {
305    fn update_response(op: &mut Operation) {
306        // Can be 301, 302, 307. We'll verify what we can generically say.
307        // Or we document "3xx"
308        op.responses.insert("3xx".to_string(), ResponseSpec {
309            description: "Redirection".to_string(),
310            ..Default::default()
311        });
312    }
313}