kit_rs/http/
response.rs

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