1use super::cookie::Cookie;
2use bytes::Bytes;
3use http_body_util::Full;
4
5pub struct HttpResponse {
7 status: u16,
8 body: String,
9 headers: Vec<(String, String)>,
10}
11
12pub 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 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 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 pub fn status(mut self, status: u16) -> Self {
44 self.status = status;
45 self
46 }
47
48 pub fn status_code(&self) -> u16 {
50 self.status
51 }
52
53 pub fn body(&self) -> &str {
55 &self.body
56 }
57
58 pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
60 self.headers.push((name.into(), value.into()));
61 self
62 }
63
64 pub fn cookie(self, cookie: Cookie) -> Self {
76 let header_value = cookie.to_header_value();
77 self.header("Set-Cookie", header_value)
78 }
79
80 pub fn ok(self) -> Response {
82 Ok(self)
83 }
84
85 pub fn into_hyper(self) -> hyper::Response<Full<Bytes>> {
87 let mut builder = hyper::Response::builder().status(self.status);
88
89 for (name, value) in self.headers {
90 builder = builder.header(name, value);
91 }
92
93 builder.body(Full::new(Bytes::from(self.body))).unwrap()
94 }
95}
96
97impl Default for HttpResponse {
98 fn default() -> Self {
99 Self::new()
100 }
101}
102
103pub trait ResponseExt {
105 fn status(self, code: u16) -> Self;
106 fn header(self, name: impl Into<String>, value: impl Into<String>) -> Self;
107}
108
109impl ResponseExt for Response {
110 fn status(self, code: u16) -> Self {
111 self.map(|r| r.status(code))
112 }
113
114 fn header(self, name: impl Into<String>, value: impl Into<String>) -> Self {
115 self.map(|r| r.header(name, value))
116 }
117}
118
119pub struct Redirect {
121 location: String,
122 query_params: Vec<(String, String)>,
123 status: u16,
124}
125
126impl Redirect {
127 pub fn to(path: impl Into<String>) -> Self {
129 Self {
130 location: path.into(),
131 query_params: Vec::new(),
132 status: 302,
133 }
134 }
135
136 pub fn route(name: &str) -> RedirectRouteBuilder {
138 RedirectRouteBuilder {
139 name: name.to_string(),
140 params: std::collections::HashMap::new(),
141 query_params: Vec::new(),
142 status: 302,
143 }
144 }
145
146 pub fn query(mut self, key: &str, value: impl Into<String>) -> Self {
148 self.query_params.push((key.to_string(), value.into()));
149 self
150 }
151
152 pub fn permanent(mut self) -> Self {
154 self.status = 301;
155 self
156 }
157
158 fn build_url(&self) -> String {
159 if self.query_params.is_empty() {
160 self.location.clone()
161 } else {
162 let query = self
163 .query_params
164 .iter()
165 .map(|(k, v)| format!("{k}={v}"))
166 .collect::<Vec<_>>()
167 .join("&");
168 format!("{}?{}", self.location, query)
169 }
170 }
171}
172
173impl From<Redirect> for Response {
175 fn from(redirect: Redirect) -> Response {
176 Ok(HttpResponse::new()
177 .status(redirect.status)
178 .header("Location", redirect.build_url()))
179 }
180}
181
182pub struct RedirectRouteBuilder {
184 name: String,
185 params: std::collections::HashMap<String, String>,
186 query_params: Vec<(String, String)>,
187 status: u16,
188}
189
190impl RedirectRouteBuilder {
191 pub fn with(mut self, key: &str, value: impl Into<String>) -> Self {
193 self.params.insert(key.to_string(), value.into());
194 self
195 }
196
197 pub fn query(mut self, key: &str, value: impl Into<String>) -> Self {
199 self.query_params.push((key.to_string(), value.into()));
200 self
201 }
202
203 pub fn permanent(mut self) -> Self {
205 self.status = 301;
206 self
207 }
208
209 fn build_url(&self) -> Option<String> {
210 use crate::routing::route_with_params;
211
212 let mut url = route_with_params(&self.name, &self.params)?;
213 if !self.query_params.is_empty() {
214 let query = self
215 .query_params
216 .iter()
217 .map(|(k, v)| format!("{k}={v}"))
218 .collect::<Vec<_>>()
219 .join("&");
220 url = format!("{url}?{query}");
221 }
222 Some(url)
223 }
224}
225
226impl From<RedirectRouteBuilder> for Response {
228 fn from(redirect: RedirectRouteBuilder) -> Response {
229 let url = redirect.build_url().ok_or_else(|| {
230 HttpResponse::text(format!("Route '{}' not found", redirect.name)).status(500)
231 })?;
232 Ok(HttpResponse::new()
233 .status(redirect.status)
234 .header("Location", url))
235 }
236}
237
238impl From<crate::error::FrameworkError> for HttpResponse {
246 fn from(err: crate::error::FrameworkError) -> HttpResponse {
247 let status = err.status_code();
248 let hint = err.hint();
249 let mut body = match &err {
250 crate::error::FrameworkError::ParamError { param_name } => {
251 serde_json::json!({
252 "message": format!("Missing required parameter: {}", param_name)
253 })
254 }
255 crate::error::FrameworkError::ValidationError { field, message } => {
256 serde_json::json!({
257 "message": "Validation failed",
258 "field": field,
259 "error": message
260 })
261 }
262 crate::error::FrameworkError::Validation(errors) => {
263 errors.to_json()
265 }
266 crate::error::FrameworkError::Unauthorized => {
267 serde_json::json!({
268 "message": "This action is unauthorized."
269 })
270 }
271 _ => {
272 serde_json::json!({
273 "message": err.to_string()
274 })
275 }
276 };
277 if let Some(hint_text) = hint {
278 if let Some(obj) = body.as_object_mut() {
279 obj.insert("hint".to_string(), serde_json::Value::String(hint_text));
280 }
281 }
282 HttpResponse::json(body).status(status)
283 }
284}
285
286impl From<crate::error::AppError> for HttpResponse {
290 fn from(err: crate::error::AppError) -> HttpResponse {
291 let framework_err: crate::error::FrameworkError = err.into();
293 framework_err.into()
294 }
295}
296
297pub struct InertiaRedirect<'a> {
315 request: &'a crate::http::Request,
316 location: String,
317 query_params: Vec<(String, String)>,
318}
319
320impl<'a> InertiaRedirect<'a> {
321 pub fn to(request: &'a crate::http::Request, path: impl Into<String>) -> Self {
323 Self {
324 request,
325 location: path.into(),
326 query_params: Vec::new(),
327 }
328 }
329
330 pub fn query(mut self, key: &str, value: impl Into<String>) -> Self {
332 self.query_params.push((key.to_string(), value.into()));
333 self
334 }
335
336 fn build_url(&self) -> String {
337 if self.query_params.is_empty() {
338 self.location.clone()
339 } else {
340 let query = self
341 .query_params
342 .iter()
343 .map(|(k, v)| format!("{k}={v}"))
344 .collect::<Vec<_>>()
345 .join("&");
346 format!("{}?{}", self.location, query)
347 }
348 }
349
350 fn is_post_like_method(&self) -> bool {
351 matches!(
352 self.request.method().as_str(),
353 "POST" | "PUT" | "PATCH" | "DELETE"
354 )
355 }
356}
357
358impl From<InertiaRedirect<'_>> for Response {
359 fn from(redirect: InertiaRedirect<'_>) -> Response {
360 let url = redirect.build_url();
361 let is_inertia = redirect.request.is_inertia();
362 let is_post_like = redirect.is_post_like_method();
363
364 if is_inertia {
365 let status = if is_post_like { 303 } else { 302 };
367 Ok(HttpResponse::new()
368 .status(status)
369 .header("X-Inertia", "true")
370 .header("Location", url))
371 } else {
372 Ok(HttpResponse::new().status(302).header("Location", url))
374 }
375 }
376}