1use crate::errors::HttpResult;
37use crate::response::{ElifResponse, ElifStatusCode, ResponseBody};
38use axum::body::Bytes;
39use serde::Serialize;
40use tracing;
41
42#[derive(Debug)]
47pub struct ResponseBuilder {
48 status: Option<ElifStatusCode>,
49 headers: Vec<(String, String)>,
50 body: Option<ResponseBody>,
51}
52
53impl ResponseBuilder {
54 pub fn new() -> Self {
56 Self {
57 status: None,
58 headers: Vec::new(),
59 body: None,
60 }
61 }
62
63 pub fn ok(mut self) -> Self {
67 self.status = Some(ElifStatusCode::OK);
68 self
69 }
70
71 pub fn created(mut self) -> Self {
73 self.status = Some(ElifStatusCode::CREATED);
74 self
75 }
76
77 pub fn accepted(mut self) -> Self {
79 self.status = Some(ElifStatusCode::ACCEPTED);
80 self
81 }
82
83 pub fn no_content(mut self) -> Self {
85 self.status = Some(ElifStatusCode::NO_CONTENT);
86 self
87 }
88
89 pub fn bad_request(mut self) -> Self {
91 self.status = Some(ElifStatusCode::BAD_REQUEST);
92 self
93 }
94
95 pub fn unauthorized(mut self) -> Self {
97 self.status = Some(ElifStatusCode::UNAUTHORIZED);
98 self
99 }
100
101 pub fn forbidden(mut self) -> Self {
103 self.status = Some(ElifStatusCode::FORBIDDEN);
104 self
105 }
106
107 pub fn not_found(mut self) -> Self {
109 self.status = Some(ElifStatusCode::NOT_FOUND);
110 self
111 }
112
113 pub fn unprocessable_entity(mut self) -> Self {
115 self.status = Some(ElifStatusCode::UNPROCESSABLE_ENTITY);
116 self
117 }
118
119 pub fn internal_server_error(mut self) -> Self {
121 self.status = Some(ElifStatusCode::INTERNAL_SERVER_ERROR);
122 self
123 }
124
125 pub fn status(mut self, status: ElifStatusCode) -> Self {
127 self.status = Some(status);
128 self
129 }
130
131 pub fn json<T: Serialize>(mut self, data: T) -> Self {
135 match serde_json::to_value(&data) {
136 Ok(value) => {
137 self.body = Some(ResponseBody::Json(value));
138 self.headers
139 .push(("content-type".to_string(), "application/json".to_string()));
140 self
141 }
142 Err(err) => {
143 tracing::error!("JSON serialization failed: {}", err);
145 self.status = Some(ElifStatusCode::INTERNAL_SERVER_ERROR);
147 self.body = Some(ResponseBody::Text(format!(
148 "JSON serialization failed: {}",
149 err
150 )));
151 self
152 }
153 }
154 }
155
156 pub fn text<S: Into<String>>(mut self, text: S) -> Self {
158 self.body = Some(ResponseBody::Text(text.into()));
159 self.headers.push((
160 "content-type".to_string(),
161 "text/plain; charset=utf-8".to_string(),
162 ));
163 self
164 }
165
166 pub fn html<S: Into<String>>(mut self, html: S) -> Self {
168 self.body = Some(ResponseBody::Text(html.into()));
169 self.headers.push((
170 "content-type".to_string(),
171 "text/html; charset=utf-8".to_string(),
172 ));
173 self
174 }
175
176 pub fn bytes(mut self, bytes: Bytes) -> Self {
178 self.body = Some(ResponseBody::Bytes(bytes));
179 self
180 }
181
182 pub fn redirect<S: Into<String>>(mut self, location: S) -> Self {
186 self.headers.push(("location".to_string(), location.into()));
187 if self.status.is_none() {
188 self.status = Some(ElifStatusCode::FOUND);
189 }
190 self
191 }
192
193 pub fn permanent(mut self) -> Self {
195 self.status = Some(ElifStatusCode::MOVED_PERMANENTLY);
196 self
197 }
198
199 pub fn temporary(mut self) -> Self {
201 self.status = Some(ElifStatusCode::FOUND);
202 self
203 }
204
205 pub fn header<K, V>(mut self, key: K, value: V) -> Self
209 where
210 K: Into<String>,
211 V: Into<String>,
212 {
213 self.headers.push((key.into(), value.into()));
214 self
215 }
216
217 pub fn location<S: Into<String>>(mut self, url: S) -> Self {
219 self.headers.push(("location".to_string(), url.into()));
220 self
221 }
222
223 pub fn cache_control<S: Into<String>>(mut self, value: S) -> Self {
225 self.headers
226 .push(("cache-control".to_string(), value.into()));
227 self
228 }
229
230 pub fn content_type<S: Into<String>>(mut self, content_type: S) -> Self {
232 self.headers
233 .push(("content-type".to_string(), content_type.into()));
234 self
235 }
236
237 pub fn cookie<S: Into<String>>(mut self, cookie_value: S) -> Self {
239 self.headers
240 .push(("set-cookie".to_string(), cookie_value.into()));
241 self
242 }
243
244 pub fn error<S: Into<String>>(mut self, message: S) -> Self {
248 let error_data = serde_json::json!({
249 "error": {
250 "message": message.into(),
251 "timestamp": chrono::Utc::now().to_rfc3339()
252 }
253 });
254
255 self.body = Some(ResponseBody::Json(error_data));
256 self.headers
257 .push(("content-type".to_string(), "application/json".to_string()));
258 self
259 }
260
261 pub fn validation_error<T: Serialize>(mut self, errors: T) -> Self {
263 let error_data = serde_json::json!({
264 "error": {
265 "type": "validation",
266 "details": errors,
267 "timestamp": chrono::Utc::now().to_rfc3339()
268 }
269 });
270
271 self.body = Some(ResponseBody::Json(error_data));
272 self.headers
273 .push(("content-type".to_string(), "application/json".to_string()));
274 if self.status.is_none() {
275 self.status = Some(ElifStatusCode::UNPROCESSABLE_ENTITY);
276 }
277 self
278 }
279
280 pub fn not_found_with_message<S: Into<String>>(mut self, message: S) -> Self {
282 let error_data = serde_json::json!({
283 "error": {
284 "type": "not_found",
285 "message": message.into(),
286 "timestamp": chrono::Utc::now().to_rfc3339()
287 }
288 });
289
290 self.body = Some(ResponseBody::Json(error_data));
291 self.headers
292 .push(("content-type".to_string(), "application/json".to_string()));
293 self.status = Some(ElifStatusCode::NOT_FOUND);
294 self
295 }
296
297 pub fn cors(mut self, origin: &str) -> Self {
301 self.headers.push((
302 "access-control-allow-origin".to_string(),
303 origin.to_string(),
304 ));
305 self
306 }
307
308 pub fn cors_with_credentials(mut self, origin: &str) -> Self {
310 self.headers.push((
311 "access-control-allow-origin".to_string(),
312 origin.to_string(),
313 ));
314 self.headers.push((
315 "access-control-allow-credentials".to_string(),
316 "true".to_string(),
317 ));
318 self
319 }
320
321 pub fn with_security_headers(mut self) -> Self {
325 self.headers.extend([
326 ("x-content-type-options".to_string(), "nosniff".to_string()),
327 ("x-frame-options".to_string(), "DENY".to_string()),
328 ("x-xss-protection".to_string(), "1; mode=block".to_string()),
329 (
330 "referrer-policy".to_string(),
331 "strict-origin-when-cross-origin".to_string(),
332 ),
333 ]);
334 self
335 }
336
337 pub fn send(self) -> HttpResult<ElifResponse> {
344 Ok(self.build())
345 }
346
347 pub fn finish(self) -> HttpResult<ElifResponse> {
351 Ok(self.build())
352 }
353
354 pub fn build(self) -> ElifResponse {
356 let mut response = ElifResponse::new();
357
358 if let Some(status) = self.status {
360 response = response.status(status);
361 }
362
363 let body_sets_content_type = matches!(
365 self.body,
366 Some(ResponseBody::Json(_)) | Some(ResponseBody::Text(_))
367 );
368
369 if let Some(body) = self.body {
371 match body {
372 ResponseBody::Empty => {}
373 ResponseBody::Text(text) => {
374 response = response.text(text);
375 }
376 ResponseBody::Bytes(bytes) => {
377 response = response.bytes(bytes);
378 }
379 ResponseBody::Json(value) => {
380 response = response.json_value(value);
381 }
382 }
383 }
384
385 let has_explicit_content_type = self
387 .headers
388 .iter()
389 .any(|(k, _)| k.to_lowercase() == "content-type");
390
391 for (key, value) in self.headers {
392 if key.to_lowercase() == "content-type"
394 && body_sets_content_type
395 && !has_explicit_content_type
396 {
397 continue;
398 }
399
400 if let (Ok(name), Ok(val)) = (
401 crate::response::ElifHeaderName::from_str(&key),
402 crate::response::ElifHeaderValue::from_str(&value),
403 ) {
404 response.headers_mut().append(name, val);
406 } else {
407 return ElifResponse::internal_server_error();
408 }
409 }
410
411 response
412 }
413}
414
415impl Default for ResponseBuilder {
416 fn default() -> Self {
417 Self::new()
418 }
419}
420
421impl From<ResponseBuilder> for ElifResponse {
423 fn from(builder: ResponseBuilder) -> Self {
424 builder.build()
425 }
426}
427
428pub fn response() -> ResponseBuilder {
454 ResponseBuilder::new()
455}
456
457pub fn json_response<T: Serialize>(data: T) -> ResponseBuilder {
461 response().json(data)
462}
463
464pub fn text_response<S: Into<String>>(content: S) -> ResponseBuilder {
468 response().text(content)
469}
470
471pub fn redirect_response<S: Into<String>>(location: S) -> ResponseBuilder {
475 response().redirect(location)
476}
477
478#[cfg(test)]
479mod tests {
480 use super::*;
481 use serde_json::json;
482
483 #[test]
484 fn test_basic_response_builder() {
485 let resp: ElifResponse = response().text("Hello World").ok().into();
486 assert_eq!(resp.status_code(), ElifStatusCode::OK);
487 }
488
489 #[test]
490 fn test_json_response() {
491 let data = json!({"name": "Alice", "age": 30});
492 let resp: ElifResponse = response().json(data).into();
493 assert_eq!(resp.status_code(), ElifStatusCode::OK);
494 }
495
496 #[test]
497 fn test_status_helpers() {
498 let resp: ElifResponse = response().text("Created").created().into();
499 assert_eq!(resp.status_code(), ElifStatusCode::CREATED);
500
501 let resp: ElifResponse = response().text("Not Found").not_found().into();
502 assert_eq!(resp.status_code(), ElifStatusCode::NOT_FOUND);
503 }
504
505 #[test]
506 fn test_redirect_helpers() {
507 let resp: ElifResponse = response().redirect("/login").into();
508 assert_eq!(resp.status_code(), ElifStatusCode::FOUND);
509
510 let resp: ElifResponse = response().redirect("/users").permanent().into();
511 assert_eq!(resp.status_code(), ElifStatusCode::MOVED_PERMANENTLY);
512 }
513
514 #[test]
515 fn test_redirect_method_call_order_independence() {
516 let resp1: ElifResponse = response().redirect("/test").permanent().into();
520 assert_eq!(resp1.status_code(), ElifStatusCode::MOVED_PERMANENTLY);
521 assert!(resp1.has_header("location"));
522
523 let resp2: ElifResponse = response().permanent().redirect("/test").into();
525 assert_eq!(resp2.status_code(), ElifStatusCode::MOVED_PERMANENTLY);
526 assert!(resp2.has_header("location"));
527
528 assert_eq!(resp1.status_code(), resp2.status_code());
530 }
531
532 #[test]
533 fn test_temporary_method_call_order_independence() {
534 let resp1: ElifResponse = response().redirect("/test").temporary().into();
538 assert_eq!(resp1.status_code(), ElifStatusCode::FOUND);
539 assert!(resp1.has_header("location"));
540
541 let resp2: ElifResponse = response().temporary().redirect("/test").into();
543 assert_eq!(resp2.status_code(), ElifStatusCode::FOUND);
544 assert!(resp2.has_header("location"));
545
546 assert_eq!(resp1.status_code(), resp2.status_code());
548 }
549
550 #[test]
551 fn test_redirect_status_override_behavior() {
552 let resp: ElifResponse = response().redirect("/default").into();
556 assert_eq!(resp.status_code(), ElifStatusCode::FOUND);
557
558 let resp: ElifResponse = response().permanent().redirect("/perm").into();
560 assert_eq!(resp.status_code(), ElifStatusCode::MOVED_PERMANENTLY);
561
562 let resp: ElifResponse = response().temporary().redirect("/temp").into();
564 assert_eq!(resp.status_code(), ElifStatusCode::FOUND);
565
566 let resp: ElifResponse = response().redirect("/test").permanent().into();
568 assert_eq!(resp.status_code(), ElifStatusCode::MOVED_PERMANENTLY);
569
570 let resp: ElifResponse = response().redirect("/test").permanent().temporary().into();
572 assert_eq!(resp.status_code(), ElifStatusCode::FOUND);
573 }
574
575 #[test]
576 fn test_header_chaining() {
577 let resp: ElifResponse = response()
578 .text("Hello")
579 .header("x-custom", "value")
580 .cache_control("no-cache")
581 .into();
582
583 assert!(resp.has_header("x-custom"));
584 assert!(resp.has_header("cache-control"));
585 }
586
587 #[test]
588 fn test_complex_chaining() {
589 let user_data = json!({"id": 1, "name": "Alice"});
590 let resp: ElifResponse = response()
591 .json(user_data)
592 .created()
593 .location("/users/1")
594 .cache_control("no-cache")
595 .header("x-custom", "test")
596 .into();
597
598 assert_eq!(resp.status_code(), ElifStatusCode::CREATED);
599 assert!(resp.has_header("location"));
600 assert!(resp.has_header("cache-control"));
601 assert!(resp.has_header("x-custom"));
602 }
603
604 #[test]
605 fn test_error_responses() {
606 let resp: ElifResponse = response()
607 .error("Something went wrong")
608 .internal_server_error()
609 .into();
610 assert_eq!(resp.status_code(), ElifStatusCode::INTERNAL_SERVER_ERROR);
611
612 let validation_errors = json!({"email": ["Email is required"]});
613 let resp: ElifResponse = response().validation_error(validation_errors).into();
614 assert_eq!(resp.status_code(), ElifStatusCode::UNPROCESSABLE_ENTITY);
615 }
616
617 #[test]
618 fn test_global_helpers() {
619 let data = json!({"message": "Hello"});
620 let resp: ElifResponse = json_response(data).ok().into();
621 assert_eq!(resp.status_code(), ElifStatusCode::OK);
622
623 let resp: ElifResponse = text_response("Hello World").into();
624 assert_eq!(resp.status_code(), ElifStatusCode::OK);
625
626 let resp: ElifResponse = redirect_response("/home").into();
627 assert_eq!(resp.status_code(), ElifStatusCode::FOUND);
628 }
629
630 #[test]
631 fn test_cors_helpers() {
632 let resp: ElifResponse = response().json(json!({"data": "test"})).cors("*").into();
633
634 assert!(resp.has_header("access-control-allow-origin"));
635 }
636
637 #[test]
638 fn test_security_headers() {
639 let resp: ElifResponse = response()
640 .text("Secure content")
641 .with_security_headers()
642 .into();
643
644 assert!(resp.has_header("x-content-type-options"));
645 assert!(resp.has_header("x-frame-options"));
646 assert!(resp.has_header("x-xss-protection"));
647 assert!(resp.has_header("referrer-policy"));
648 }
649
650 #[test]
651 fn test_multi_value_headers() {
652 let resp: ElifResponse = response()
654 .text("Hello")
655 .header("set-cookie", "session=abc123; Path=/")
656 .header("set-cookie", "theme=dark; Path=/")
657 .header("set-cookie", "lang=en; Path=/")
658 .into();
659
660 assert!(resp.has_header("set-cookie"));
662
663 assert_eq!(resp.status_code(), ElifStatusCode::OK);
665 }
666
667 #[test]
668 fn test_cookie_helper_method() {
669 let resp: ElifResponse = response()
671 .json(json!({"user": "alice"}))
672 .cookie("session=12345; HttpOnly; Secure")
673 .cookie("csrf=token123; SameSite=Strict")
674 .cookie("theme=dark; Path=/")
675 .created()
676 .into();
677
678 assert!(resp.has_header("set-cookie"));
679 assert_eq!(resp.status_code(), ElifStatusCode::CREATED);
680 }
681
682 #[test]
683 fn test_terminal_methods() {
684 let result: HttpResult<ElifResponse> =
686 response().json(json!({"data": "test"})).created().send();
687
688 assert!(result.is_ok());
689 let resp = result.unwrap();
690 assert_eq!(resp.status_code(), ElifStatusCode::CREATED);
691
692 let result: HttpResult<ElifResponse> = response()
694 .text("Hello World")
695 .cache_control("no-cache")
696 .finish();
697
698 assert!(result.is_ok());
699 let resp = result.unwrap();
700 assert_eq!(resp.status_code(), ElifStatusCode::OK);
701 assert!(resp.has_header("cache-control"));
702 }
703
704 #[test]
705 fn test_laravel_style_chaining() {
706 let result: HttpResult<ElifResponse> = response()
708 .json(json!({"user_id": 123}))
709 .created()
710 .location("/users/123")
711 .cookie("session=abc123; HttpOnly")
712 .header("x-custom", "value")
713 .send();
714
715 assert!(result.is_ok());
716 let resp = result.unwrap();
717 assert_eq!(resp.status_code(), ElifStatusCode::CREATED);
718 assert!(resp.has_header("location"));
719 assert!(resp.has_header("set-cookie"));
720 assert!(resp.has_header("x-custom"));
721 }
722
723 #[test]
724 fn test_json_serialization_error_handling() {
725 use std::collections::HashMap;
726
727 let valid_data = HashMap::from([("key", "value")]);
732 let resp: ElifResponse = response().json(valid_data).into();
733
734 assert_eq!(resp.status_code(), ElifStatusCode::OK);
735
736 }
746
747 #[test]
748 fn test_header_append_vs_insert_behavior() {
749 let resp: ElifResponse = response()
751 .json(json!({"test": "data"}))
752 .header("x-custom", "value1")
753 .header("x-custom", "value2")
754 .header("x-custom", "value3")
755 .into();
756
757 assert!(resp.has_header("x-custom"));
758 assert_eq!(resp.status_code(), ElifStatusCode::OK);
759
760 }
763}