rustisan_core/
http.rs

1//! HTTP module for the Rustisan framework
2//!
3//! This module contains the HTTP abstractions for requests and responses.
4
5use std::fmt;
6use std::str::FromStr;
7use std::collections::HashMap;
8
9use axum::{
10    body::Bytes,
11    extract::Request as AxumRequest,
12    http::{HeaderMap, HeaderValue, Method, StatusCode, Uri, Version},
13    response::{IntoResponse, Response as AxumResponse},
14    Json,
15};
16use serde::{de::DeserializeOwned, Serialize};
17use serde_json::{json, Value};
18
19use crate::errors::{Result, RustisanError};
20
21/// Wrapper for HTTP requests
22pub struct Request {
23    /// The HTTP method
24    pub method: Method,
25    /// The request URI
26    pub uri: Uri,
27    /// The HTTP version
28    pub version: Version,
29    /// The request headers
30    pub headers: HeaderMap,
31    /// The request path parameters
32    pub path_params: HashMap<String, String>,
33    /// The request query parameters
34    pub query_params: HashMap<String, String>,
35    /// The request cookies
36    pub cookies: HashMap<String, String>,
37    /// The request body as bytes
38    body: Option<Bytes>,
39    /// The inner Axum request
40    inner: AxumRequest,
41}
42
43impl Request {
44    /// Creates a new request from an Axum request
45    pub async fn from_axum(mut req: AxumRequest) -> Self {
46        let method = req.method().clone();
47        let uri = req.uri().clone();
48        let version = req.version();
49        let headers = req.headers().clone();
50
51        // Extract cookies
52        let cookies = Self::extract_cookies(&headers);
53
54        // Extract query parameters
55        let query_params = Self::extract_query_params(uri.query());
56
57        // For now, just set body to None - in a real implementation we'd extract it properly
58        let body = None;
59
60        Self {
61            method,
62            uri,
63            version,
64            headers,
65            path_params: HashMap::new(),
66            query_params,
67            cookies,
68            body,
69            inner: req,
70        }
71    }
72
73    /// Extracts cookies from headers
74    fn extract_cookies(headers: &HeaderMap) -> HashMap<String, String> {
75        let mut cookies = HashMap::new();
76
77        if let Some(cookie_header) = headers.get("cookie") {
78            if let Ok(cookie_str) = cookie_header.to_str() {
79                for cookie in cookie_str.split(';') {
80                    let cookie = cookie.trim();
81                    if let Some(idx) = cookie.find('=') {
82                        let (key, value) = cookie.split_at(idx);
83                        cookies.insert(key.trim().to_owned(), value[1..].trim().to_owned());
84                    }
85                }
86            }
87        }
88
89        cookies
90    }
91
92    /// Extracts query parameters from a query string
93    fn extract_query_params(query: Option<&str>) -> HashMap<String, String> {
94        let mut params = HashMap::new();
95
96        if let Some(query_str) = query {
97            for param in query_str.split('&') {
98                if let Some(idx) = param.find('=') {
99                    let (key, value) = param.split_at(idx);
100                    params.insert(key.to_owned(), value[1..].to_owned());
101                } else {
102                    params.insert(param.to_owned(), String::new());
103                }
104            }
105        }
106
107        params
108    }
109
110    /// Gets a path parameter
111    pub fn param<T: FromStr>(&self, name: &str) -> Result<T> {
112        self.path_params
113            .get(name)
114            .ok_or_else(|| RustisanError::BadRequest(format!("Missing path parameter: {}", name)))
115            .and_then(|s| {
116                s.parse::<T>().map_err(|_| {
117                    RustisanError::BadRequest(format!("Invalid path parameter: {}", name))
118                })
119            })
120    }
121
122    /// Gets a query parameter
123    pub fn query<T: FromStr>(&self, name: &str) -> Result<T> {
124        self.query_params
125            .get(name)
126            .ok_or_else(|| {
127                RustisanError::BadRequest(format!("Missing query parameter: {}", name))
128            })
129            .and_then(|s| {
130                s.parse::<T>().map_err(|_| {
131                    RustisanError::BadRequest(format!("Invalid query parameter: {}", name))
132                })
133            })
134    }
135
136    /// Gets a query parameter with a default value
137    pub fn query_or<T: FromStr>(&self, name: &str, default: T) -> T {
138        self.query(name).unwrap_or(default)
139    }
140
141    /// Gets a cookie
142    pub fn cookie(&self, name: &str) -> Option<&String> {
143        self.cookies.get(name)
144    }
145
146    /// Gets a header
147    pub fn header(&self, name: &str) -> Option<&HeaderValue> {
148        self.headers.get(name)
149    }
150
151    /// Checks if the request has JSON content
152    pub fn is_json(&self) -> bool {
153        self.headers
154            .get("content-type")
155            .and_then(|v| v.to_str().ok())
156            .map(|s| s.starts_with("application/json"))
157            .unwrap_or(false)
158    }
159
160    /// Deserializes the request body as JSON
161    pub async fn json<T: DeserializeOwned>(&self) -> Result<T> {
162        if !self.is_json() {
163            return Err(RustisanError::BadRequest(
164                "Content-Type must be application/json".to_string(),
165            ));
166        }
167
168        match &self.body {
169            Some(bytes) => serde_json::from_slice(bytes)
170                .map_err(|e| RustisanError::BadRequest(format!("Invalid JSON: {}", e))),
171            None => Err(RustisanError::BadRequest("Empty request body".to_string())),
172        }
173    }
174
175    /// Gets the request body as bytes
176    pub fn body(&self) -> Option<&Bytes> {
177        self.body.as_ref()
178    }
179
180    /// Gets the request body as a string
181    pub fn body_string(&self) -> Result<String> {
182        match &self.body {
183            Some(bytes) => String::from_utf8(bytes.to_vec())
184                .map_err(|e| RustisanError::BadRequest(format!("Invalid UTF-8: {}", e))),
185            None => Err(RustisanError::BadRequest("Empty request body".to_string())),
186        }
187    }
188
189    /// Gets the inner Axum request
190    pub fn inner(&self) -> &AxumRequest {
191        &self.inner
192    }
193}
194
195/// HTTP response wrapper
196#[derive(Debug, Clone, Serialize)]
197pub struct Response {
198    /// The response status code
199    #[serde(skip)]
200    pub status: StatusCode,
201    /// The response data
202    #[serde(flatten)]
203    pub data: Value,
204    /// Optional success message
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub message: Option<String>,
207}
208
209impl Response {
210    /// Creates a new response
211    pub fn new(status: StatusCode, data: Value) -> Self {
212        Self {
213            status,
214            data,
215            message: None,
216        }
217    }
218
219    /// Creates a new response with a message
220    pub fn with_message(status: StatusCode, data: Value, message: String) -> Self {
221        Self {
222            status,
223            data,
224            message: Some(message),
225        }
226    }
227
228    /// Creates a successful response (200 OK)
229    pub fn ok<S: Into<String>>(body: S) -> Result<Self> {
230        Ok(Self {
231            status: StatusCode::OK,
232            data: json!({"data": body.into()}),
233            message: None,
234        })
235    }
236
237    /// Creates a JSON response (200 OK)
238    pub fn json(data: Value) -> Result<Self> {
239        Ok(Self {
240            status: StatusCode::OK,
241            data,
242            message: None,
243        })
244    }
245
246    /// Creates a successful response with message
247    pub fn success(data: Value, message: Option<String>) -> Result<Self> {
248        Ok(Self {
249            status: StatusCode::OK,
250            data,
251            message,
252        })
253    }
254
255    /// Creates a created response (201 Created)
256    pub fn created(data: Value) -> Result<Self> {
257        Ok(Self {
258            status: StatusCode::CREATED,
259            data,
260            message: None,
261        })
262    }
263
264    /// Creates a no content response (204 No Content)
265    pub fn no_content() -> Result<Self> {
266        Ok(Self {
267            status: StatusCode::NO_CONTENT,
268            data: json!({}),
269            message: None,
270        })
271    }
272
273    /// Creates a bad request response (400 Bad Request)
274    pub fn bad_request<S: Into<String>>(message: S) -> Result<Self> {
275        Ok(Self {
276            status: StatusCode::BAD_REQUEST,
277            data: json!({"error": message.into()}),
278            message: None,
279        })
280    }
281
282    /// Creates an unauthorized response (401 Unauthorized)
283    pub fn unauthorized<S: Into<String>>(message: S) -> Result<Self> {
284        Ok(Self {
285            status: StatusCode::UNAUTHORIZED,
286            data: json!({"error": message.into()}),
287            message: None,
288        })
289    }
290
291    /// Creates a forbidden response (403 Forbidden)
292    pub fn forbidden<S: Into<String>>(message: S) -> Result<Self> {
293        Ok(Self {
294            status: StatusCode::FORBIDDEN,
295            data: json!({"error": message.into()}),
296            message: None,
297        })
298    }
299
300    /// Creates a not found response (404 Not Found)
301    pub fn not_found<S: Into<String>>(message: S) -> Result<Self> {
302        Ok(Self {
303            status: StatusCode::NOT_FOUND,
304            data: json!({"error": message.into()}),
305            message: None,
306        })
307    }
308
309    /// Creates an internal server error response (500 Internal Server Error)
310    pub fn internal_error<S: Into<String>>(message: S) -> Result<Self> {
311        Ok(Self {
312            status: StatusCode::INTERNAL_SERVER_ERROR,
313            data: json!({"error": message.into()}),
314            message: None,
315        })
316    }
317
318    /// Adds a message to the response
319    pub fn with_message_str<S: Into<String>>(mut self, message: S) -> Self {
320        self.message = Some(message.into());
321        self
322    }
323}
324
325impl IntoResponse for Response {
326    fn into_response(self) -> AxumResponse {
327        (StatusCode::OK, Json(self)).into_response()
328    }
329}
330
331impl fmt::Debug for Request {
332    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
333        f.debug_struct("Request")
334            .field("method", &self.method)
335            .field("uri", &self.uri)
336            .field("version", &self.version)
337            .field("headers", &self.headers)
338            .field("path_params", &self.path_params)
339            .field("query_params", &self.query_params)
340            .field("cookies", &self.cookies)
341            .field("body", &self.body.as_ref().map(|_| "[bytes]"))
342            .finish()
343    }
344}
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349
350    #[test]
351    fn test_response_creation() {
352        let resp = Response::ok("Hello").unwrap();
353        assert_eq!(resp.status, StatusCode::OK);
354
355        let resp = Response::json(json!({"name": "John"})).unwrap();
356        assert_eq!(resp.status, StatusCode::OK);
357
358        let resp = Response::created(json!({"id": 1})).unwrap();
359        assert_eq!(resp.status, StatusCode::CREATED);
360    }
361
362    #[test]
363    fn test_error_responses() {
364        let resp = Response::not_found("User not found").unwrap();
365        assert_eq!(resp.status, StatusCode::NOT_FOUND);
366
367        let resp = Response::bad_request("Invalid input").unwrap();
368        assert_eq!(resp.status, StatusCode::BAD_REQUEST);
369    }
370
371    #[test]
372    fn test_response_with_message() {
373        let resp = Response::success(json!({"result": true}), Some("Operation successful".to_string())).unwrap();
374        assert_eq!(resp.status, StatusCode::OK);
375        assert_eq!(resp.message, Some("Operation successful".to_string()));
376    }
377}