elif_http/
response.rs

1//! Response abstraction for building HTTP responses
2//! 
3//! Provides fluent response building with status codes, headers, and JSON serialization.
4
5use std::collections::HashMap;
6use axum::{
7    http::{HeaderMap, HeaderName, HeaderValue, StatusCode},
8    response::{Json, Response, IntoResponse},
9    body::{Body, Bytes},
10};
11use serde::Serialize;
12use crate::error::{HttpError, HttpResult};
13
14/// Framework-native status codes - use instead of axum::http::StatusCode
15pub use axum::http::StatusCode as ElifStatusCode;
16
17/// Framework-native header map - use instead of axum::http::HeaderMap  
18pub use axum::http::HeaderMap as ElifHeaderMap;
19
20/// Response builder for creating HTTP responses with fluent API
21#[derive(Debug)]
22pub struct ElifResponse {
23    status: StatusCode,
24    headers: HeaderMap,
25    body: ResponseBody,
26}
27
28/// Response body types
29#[derive(Debug)]
30pub enum ResponseBody {
31    Empty,
32    Text(String),
33    Bytes(Bytes),
34    Json(serde_json::Value),
35}
36
37impl ElifResponse {
38    /// Create new response with OK status
39    pub fn new() -> Self {
40        Self {
41            status: StatusCode::OK,
42            headers: HeaderMap::new(),
43            body: ResponseBody::Empty,
44        }
45    }
46
47    /// Create response with specific status code
48    pub fn with_status(status: StatusCode) -> Self {
49        Self {
50            status,
51            headers: HeaderMap::new(),
52            body: ResponseBody::Empty,
53        }
54    }
55
56    /// Set response status code
57    pub fn status(mut self, status: StatusCode) -> Self {
58        self.status = status;
59        self
60    }
61
62    /// Add header to response
63    pub fn header<K, V>(mut self, key: K, value: V) -> HttpResult<Self>
64    where
65        K: TryInto<HeaderName>,
66        K::Error: std::fmt::Display,
67        V: TryInto<HeaderValue>,
68        V::Error: std::fmt::Display,
69    {
70        let header_name = key.try_into()
71            .map_err(|e| HttpError::internal(format!("Invalid header name: {}", e)))?;
72        let header_value = value.try_into()
73            .map_err(|e| HttpError::internal(format!("Invalid header value: {}", e)))?;
74        
75        self.headers.insert(header_name, header_value);
76        Ok(self)
77    }
78
79    /// Set Content-Type header
80    pub fn content_type(self, content_type: &str) -> HttpResult<Self> {
81        self.header("content-type", content_type)
82    }
83
84    /// Set response body as text
85    pub fn text<S: Into<String>>(mut self, text: S) -> Self {
86        self.body = ResponseBody::Text(text.into());
87        self
88    }
89
90    /// Set response body as bytes
91    pub fn bytes(mut self, bytes: Bytes) -> Self {
92        self.body = ResponseBody::Bytes(bytes);
93        self
94    }
95
96    /// Set response body as JSON
97    pub fn json<T: Serialize>(mut self, data: &T) -> HttpResult<Self> {
98        let json_value = serde_json::to_value(data)
99            .map_err(|e| HttpError::internal(format!("JSON serialization failed: {}", e)))?;
100        self.body = ResponseBody::Json(json_value);
101        Ok(self)
102    }
103
104    /// Set response body as raw JSON value
105    pub fn json_value(mut self, value: serde_json::Value) -> Self {
106        self.body = ResponseBody::Json(value);
107        self
108    }
109
110    /// Build the response
111    pub fn build(mut self) -> HttpResult<Response<Body>> {
112        // Set default content type based on body type
113        if !self.headers.contains_key("content-type") {
114            match &self.body {
115                ResponseBody::Json(_) => {
116                    self = self.content_type("application/json")?;
117                }
118                ResponseBody::Text(_) => {
119                    self = self.content_type("text/plain; charset=utf-8")?;
120                }
121                _ => {}
122            }
123        }
124
125        let body = match self.body {
126            ResponseBody::Empty => Body::empty(),
127            ResponseBody::Text(text) => Body::from(text),
128            ResponseBody::Bytes(bytes) => Body::from(bytes),
129            ResponseBody::Json(value) => {
130                let json_string = serde_json::to_string(&value)
131                    .map_err(|e| HttpError::internal(format!("JSON serialization failed: {}", e)))?;
132                Body::from(json_string)
133            }
134        };
135
136        let mut response = Response::builder()
137            .status(self.status);
138        
139        // Add headers
140        for (key, value) in self.headers.iter() {
141            response = response.header(key, value);
142        }
143
144        response.body(body)
145            .map_err(|e| HttpError::internal(format!("Failed to build response: {}", e)))
146    }
147}
148
149impl Default for ElifResponse {
150    fn default() -> Self {
151        Self::new()
152    }
153}
154
155/// Convenience methods for common response types
156impl ElifResponse {
157    /// Create 200 OK response
158    pub fn ok() -> Self {
159        Self::with_status(StatusCode::OK)
160    }
161
162    /// Create 201 Created response
163    pub fn created() -> Self {
164        Self::with_status(StatusCode::CREATED)
165    }
166
167    /// Create 204 No Content response
168    pub fn no_content() -> Self {
169        Self::with_status(StatusCode::NO_CONTENT)
170    }
171
172    /// Create 400 Bad Request response
173    pub fn bad_request() -> Self {
174        Self::with_status(StatusCode::BAD_REQUEST)
175    }
176
177    /// Create 401 Unauthorized response
178    pub fn unauthorized() -> Self {
179        Self::with_status(StatusCode::UNAUTHORIZED)
180    }
181
182    /// Create 403 Forbidden response
183    pub fn forbidden() -> Self {
184        Self::with_status(StatusCode::FORBIDDEN)
185    }
186
187    /// Create 404 Not Found response
188    pub fn not_found() -> Self {
189        Self::with_status(StatusCode::NOT_FOUND)
190    }
191
192    /// Create 422 Unprocessable Entity response
193    pub fn unprocessable_entity() -> Self {
194        Self::with_status(StatusCode::UNPROCESSABLE_ENTITY)
195    }
196
197    /// Create 500 Internal Server Error response
198    pub fn internal_server_error() -> Self {
199        Self::with_status(StatusCode::INTERNAL_SERVER_ERROR)
200    }
201
202    /// Create JSON response with data
203    pub fn json_ok<T: Serialize>(data: &T) -> HttpResult<Response<Body>> {
204        Self::ok().json(data)?.build()
205    }
206
207    /// Create JSON error response
208    pub fn json_error(status: StatusCode, message: &str) -> HttpResult<Response<Body>> {
209        let error_data = serde_json::json!({
210            "error": {
211                "code": status.as_u16(),
212                "message": message
213            }
214        });
215        
216        Self::with_status(status)
217            .json_value(error_data)
218            .build()
219    }
220
221    /// Create validation error response
222    pub fn validation_error<T: Serialize>(errors: &T) -> HttpResult<Response<Body>> {
223        let error_data = serde_json::json!({
224            "error": {
225                "code": 422,
226                "message": "Validation failed",
227                "details": errors
228            }
229        });
230        
231        Self::unprocessable_entity()
232            .json_value(error_data)
233            .build()
234    }
235}
236
237/// Helper trait for converting types to ElifResponse
238pub trait IntoElifResponse {
239    fn into_elif_response(self) -> HttpResult<ElifResponse>;
240}
241
242impl IntoElifResponse for String {
243    fn into_elif_response(self) -> HttpResult<ElifResponse> {
244        Ok(ElifResponse::ok().text(self))
245    }
246}
247
248impl IntoElifResponse for &str {
249    fn into_elif_response(self) -> HttpResult<ElifResponse> {
250        Ok(ElifResponse::ok().text(self))
251    }
252}
253
254impl IntoElifResponse for StatusCode {
255    fn into_elif_response(self) -> HttpResult<ElifResponse> {
256        Ok(ElifResponse::with_status(self))
257    }
258}
259
260/// Convert ElifResponse to Axum Response
261impl IntoResponse for ElifResponse {
262    fn into_response(self) -> Response {
263        match self.build() {
264            Ok(response) => response,
265            Err(e) => {
266                // Fallback error response
267                (StatusCode::INTERNAL_SERVER_ERROR, format!("Response build failed: {}", e)).into_response()
268            }
269        }
270    }
271}
272
273/// Redirect response builders
274impl ElifResponse {
275    /// Create 301 Moved Permanently redirect
276    pub fn redirect_permanent(location: &str) -> HttpResult<Self> {
277        Ok(Self::with_status(StatusCode::MOVED_PERMANENTLY)
278            .header("location", location)?)
279    }
280
281    /// Create 302 Found (temporary) redirect
282    pub fn redirect_temporary(location: &str) -> HttpResult<Self> {
283        Ok(Self::with_status(StatusCode::FOUND)
284            .header("location", location)?)
285    }
286
287    /// Create 303 See Other redirect
288    pub fn redirect_see_other(location: &str) -> HttpResult<Self> {
289        Ok(Self::with_status(StatusCode::SEE_OTHER)
290            .header("location", location)?)
291    }
292}
293
294/// File download response builders
295impl ElifResponse {
296    /// Create file download response
297    pub fn download(filename: &str, content: Bytes) -> HttpResult<Self> {
298        let content_disposition = format!("attachment; filename=\"{}\"", filename);
299        
300        Ok(Self::ok()
301            .header("content-disposition", content_disposition)?
302            .header("content-type", "application/octet-stream")?
303            .bytes(content))
304    }
305
306    /// Create inline file response (display in browser)
307    pub fn file_inline(filename: &str, content_type: &str, content: Bytes) -> HttpResult<Self> {
308        let content_disposition = format!("inline; filename=\"{}\"", filename);
309        
310        Ok(Self::ok()
311            .header("content-disposition", content_disposition)?
312            .header("content-type", content_type)?
313            .bytes(content))
314    }
315}
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320    use serde_json::json;
321
322    #[test]
323    fn test_basic_response_building() {
324        let response = ElifResponse::ok()
325            .text("Hello, World!");
326
327        assert_eq!(response.status, StatusCode::OK);
328        match response.body {
329            ResponseBody::Text(text) => assert_eq!(text, "Hello, World!"),
330            _ => panic!("Expected text body"),
331        }
332    }
333
334    #[test]
335    fn test_json_response() {
336        let data = json!({
337            "name": "John Doe",
338            "age": 30
339        });
340
341        let response = ElifResponse::ok()
342            .json_value(data.clone());
343
344        match response.body {
345            ResponseBody::Json(value) => assert_eq!(value, data),
346            _ => panic!("Expected JSON body"),
347        }
348    }
349
350    #[test]
351    fn test_status_codes() {
352        assert_eq!(ElifResponse::created().status, StatusCode::CREATED);
353        assert_eq!(ElifResponse::not_found().status, StatusCode::NOT_FOUND);
354        assert_eq!(ElifResponse::internal_server_error().status, StatusCode::INTERNAL_SERVER_ERROR);
355    }
356
357    #[test]
358    fn test_headers() {
359        let response = ElifResponse::ok()
360            .header("x-custom-header", "test-value")
361            .unwrap();
362
363        assert!(response.headers.contains_key("x-custom-header"));
364        assert_eq!(
365            response.headers.get("x-custom-header").unwrap(),
366            &HeaderValue::from_static("test-value")
367        );
368    }
369
370    #[test]
371    fn test_redirect_responses() {
372        let redirect = ElifResponse::redirect_permanent("/new-location").unwrap();
373        assert_eq!(redirect.status, StatusCode::MOVED_PERMANENTLY);
374        assert!(redirect.headers.contains_key("location"));
375    }
376}