kit_rs/http/
response.rs

1use super::cookie::Cookie;
2use bytes::Bytes;
3use http_body_util::Full;
4
5/// HTTP Response builder providing Laravel-like response creation
6pub struct HttpResponse {
7    status: u16,
8    body: String,
9    headers: Vec<(String, String)>,
10}
11
12/// Response type alias - allows using `?` operator for early returns
13pub type Response = Result<HttpResponse, HttpResponse>;
14
15impl HttpResponse {
16    pub fn new() -> Self {
17        Self {
18            status: 200,
19            body: String::new(),
20            headers: Vec::new(),
21        }
22    }
23
24    /// Create a response with a string body
25    pub fn text(body: impl Into<String>) -> Self {
26        Self {
27            status: 200,
28            body: body.into(),
29            headers: vec![("Content-Type".to_string(), "text/plain".to_string())],
30        }
31    }
32
33    /// Create a JSON response from a serde_json::Value
34    pub fn json(body: serde_json::Value) -> Self {
35        Self {
36            status: 200,
37            body: body.to_string(),
38            headers: vec![("Content-Type".to_string(), "application/json".to_string())],
39        }
40    }
41
42    /// Set the HTTP status code
43    pub fn status(mut self, status: u16) -> Self {
44        self.status = status;
45        self
46    }
47
48    /// Add a header to the response
49    pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
50        self.headers.push((name.into(), value.into()));
51        self
52    }
53
54    /// Add a Set-Cookie header to the response
55    ///
56    /// # Example
57    ///
58    /// ```rust,ignore
59    /// use kit::{Cookie, HttpResponse};
60    ///
61    /// let response = HttpResponse::text("OK")
62    ///     .cookie(Cookie::new("session", "abc123"))
63    ///     .cookie(Cookie::new("user_id", "42"));
64    /// ```
65    pub fn cookie(self, cookie: Cookie) -> Self {
66        self.header("Set-Cookie", cookie.to_header_value())
67    }
68
69    /// Wrap this response in Ok() for use as Response type
70    pub fn ok(self) -> Response {
71        Ok(self)
72    }
73
74    /// Convert to hyper response
75    pub fn into_hyper(self) -> hyper::Response<Full<Bytes>> {
76        let mut builder = hyper::Response::builder().status(self.status);
77
78        for (name, value) in self.headers {
79            builder = builder.header(name, value);
80        }
81
82        builder.body(Full::new(Bytes::from(self.body))).unwrap()
83    }
84}
85
86impl Default for HttpResponse {
87    fn default() -> Self {
88        Self::new()
89    }
90}
91
92/// Extension trait for Response to enable method chaining on macros
93pub trait ResponseExt {
94    fn status(self, code: u16) -> Self;
95    fn header(self, name: impl Into<String>, value: impl Into<String>) -> Self;
96}
97
98impl ResponseExt for Response {
99    fn status(self, code: u16) -> Self {
100        self.map(|r| r.status(code))
101    }
102
103    fn header(self, name: impl Into<String>, value: impl Into<String>) -> Self {
104        self.map(|r| r.header(name, value))
105    }
106}
107
108/// HTTP Redirect response builder
109pub struct Redirect {
110    location: String,
111    query_params: Vec<(String, String)>,
112    status: u16,
113}
114
115impl Redirect {
116    /// Create a redirect to a specific URL/path
117    pub fn to(path: impl Into<String>) -> Self {
118        Self {
119            location: path.into(),
120            query_params: Vec::new(),
121            status: 302,
122        }
123    }
124
125    /// Create a redirect to a named route
126    pub fn route(name: &str) -> RedirectRouteBuilder {
127        RedirectRouteBuilder {
128            name: name.to_string(),
129            params: std::collections::HashMap::new(),
130            query_params: Vec::new(),
131            status: 302,
132        }
133    }
134
135    /// Add a query parameter
136    pub fn query(mut self, key: &str, value: impl Into<String>) -> Self {
137        self.query_params.push((key.to_string(), value.into()));
138        self
139    }
140
141    /// Set status to 301 (Moved Permanently)
142    pub fn permanent(mut self) -> Self {
143        self.status = 301;
144        self
145    }
146
147    fn build_url(&self) -> String {
148        if self.query_params.is_empty() {
149            self.location.clone()
150        } else {
151            let query = self
152                .query_params
153                .iter()
154                .map(|(k, v)| format!("{}={}", k, v))
155                .collect::<Vec<_>>()
156                .join("&");
157            format!("{}?{}", self.location, query)
158        }
159    }
160}
161
162/// Auto-convert Redirect to Response
163impl From<Redirect> for Response {
164    fn from(redirect: Redirect) -> Response {
165        Ok(HttpResponse::new()
166            .status(redirect.status)
167            .header("Location", redirect.build_url()))
168    }
169}
170
171/// Builder for redirects to named routes with parameters
172pub struct RedirectRouteBuilder {
173    name: String,
174    params: std::collections::HashMap<String, String>,
175    query_params: Vec<(String, String)>,
176    status: u16,
177}
178
179impl RedirectRouteBuilder {
180    /// Add a route parameter value
181    pub fn with(mut self, key: &str, value: impl Into<String>) -> Self {
182        self.params.insert(key.to_string(), value.into());
183        self
184    }
185
186    /// Add a query parameter
187    pub fn query(mut self, key: &str, value: impl Into<String>) -> Self {
188        self.query_params.push((key.to_string(), value.into()));
189        self
190    }
191
192    /// Set status to 301 (Moved Permanently)
193    pub fn permanent(mut self) -> Self {
194        self.status = 301;
195        self
196    }
197
198    fn build_url(&self) -> Option<String> {
199        use crate::routing::route_with_params;
200
201        let mut url = route_with_params(&self.name, &self.params)?;
202        if !self.query_params.is_empty() {
203            let query = self
204                .query_params
205                .iter()
206                .map(|(k, v)| format!("{}={}", k, v))
207                .collect::<Vec<_>>()
208                .join("&");
209            url = format!("{}?{}", url, query);
210        }
211        Some(url)
212    }
213}
214
215/// Auto-convert RedirectRouteBuilder to Response
216impl From<RedirectRouteBuilder> for Response {
217    fn from(redirect: RedirectRouteBuilder) -> Response {
218        let url = redirect.build_url().ok_or_else(|| {
219            HttpResponse::text(format!("Route '{}' not found", redirect.name)).status(500)
220        })?;
221        Ok(HttpResponse::new()
222            .status(redirect.status)
223            .header("Location", url))
224    }
225}
226
227/// Auto-convert FrameworkError to HttpResponse
228///
229/// This enables using the `?` operator in controller handlers to propagate
230/// framework errors as appropriate HTTP responses.
231impl From<crate::error::FrameworkError> for HttpResponse {
232    fn from(err: crate::error::FrameworkError) -> HttpResponse {
233        let status = err.status_code();
234        let body = match &err {
235            crate::error::FrameworkError::ParamError { param_name } => {
236                serde_json::json!({
237                    "error": format!("Missing required parameter: {}", param_name)
238                })
239            }
240            crate::error::FrameworkError::ValidationError { field, message } => {
241                serde_json::json!({
242                    "error": "Validation failed",
243                    "field": field,
244                    "message": message
245                })
246            }
247            crate::error::FrameworkError::Validation(errors) => {
248                // Laravel/Inertia-compatible validation error format
249                errors.to_json()
250            }
251            crate::error::FrameworkError::Unauthorized => {
252                serde_json::json!({
253                    "message": "This action is unauthorized."
254                })
255            }
256            _ => {
257                serde_json::json!({
258                    "error": err.to_string()
259                })
260            }
261        };
262        HttpResponse::json(body).status(status)
263    }
264}
265
266/// Auto-convert AppError to HttpResponse
267///
268/// This enables using the `?` operator in controller handlers with AppError.
269impl From<crate::error::AppError> for HttpResponse {
270    fn from(err: crate::error::AppError) -> HttpResponse {
271        // Convert AppError -> FrameworkError -> HttpResponse
272        let framework_err: crate::error::FrameworkError = err.into();
273        framework_err.into()
274    }
275}