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