1use crate::response::{ElifResponse, ElifStatusCode, ResponseBody};
37use crate::errors::HttpResult;
38use serde::Serialize;
39use axum::body::Bytes;
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.push(("content-type".to_string(), "application/json".to_string()));
139 self
140 }
141 Err(err) => {
142 tracing::error!("JSON serialization failed: {}", err);
144 self.status = Some(ElifStatusCode::INTERNAL_SERVER_ERROR);
146 self.body = Some(ResponseBody::Text(format!("JSON serialization failed: {}", err)));
147 self
148 }
149 }
150 }
151
152 pub fn text<S: Into<String>>(mut self, text: S) -> Self {
154 self.body = Some(ResponseBody::Text(text.into()));
155 self.headers.push(("content-type".to_string(), "text/plain; charset=utf-8".to_string()));
156 self
157 }
158
159 pub fn html<S: Into<String>>(mut self, html: S) -> Self {
161 self.body = Some(ResponseBody::Text(html.into()));
162 self.headers.push(("content-type".to_string(), "text/html; charset=utf-8".to_string()));
163 self
164 }
165
166 pub fn bytes(mut self, bytes: Bytes) -> Self {
168 self.body = Some(ResponseBody::Bytes(bytes));
169 self
170 }
171
172 pub fn redirect<S: Into<String>>(mut self, location: S) -> Self {
176 self.headers.push(("location".to_string(), location.into()));
177 if self.status.is_none() {
178 self.status = Some(ElifStatusCode::FOUND);
179 }
180 self
181 }
182
183 pub fn permanent(mut self) -> Self {
185 self.status = Some(ElifStatusCode::MOVED_PERMANENTLY);
186 self
187 }
188
189 pub fn temporary(mut self) -> Self {
191 self.status = Some(ElifStatusCode::FOUND);
192 self
193 }
194
195 pub fn header<K, V>(mut self, key: K, value: V) -> Self
199 where
200 K: Into<String>,
201 V: Into<String>,
202 {
203 self.headers.push((key.into(), value.into()));
204 self
205 }
206
207 pub fn location<S: Into<String>>(mut self, url: S) -> Self {
209 self.headers.push(("location".to_string(), url.into()));
210 self
211 }
212
213 pub fn cache_control<S: Into<String>>(mut self, value: S) -> Self {
215 self.headers.push(("cache-control".to_string(), value.into()));
216 self
217 }
218
219 pub fn content_type<S: Into<String>>(mut self, content_type: S) -> Self {
221 self.headers.push(("content-type".to_string(), content_type.into()));
222 self
223 }
224
225 pub fn cookie<S: Into<String>>(mut self, cookie_value: S) -> Self {
227 self.headers.push(("set-cookie".to_string(), cookie_value.into()));
228 self
229 }
230
231 pub fn error<S: Into<String>>(mut self, message: S) -> Self {
235 let error_data = serde_json::json!({
236 "error": {
237 "message": message.into(),
238 "timestamp": chrono::Utc::now().to_rfc3339()
239 }
240 });
241
242 self.body = Some(ResponseBody::Json(error_data));
243 self.headers.push(("content-type".to_string(), "application/json".to_string()));
244 self
245 }
246
247 pub fn validation_error<T: Serialize>(mut self, errors: T) -> Self {
249 let error_data = serde_json::json!({
250 "error": {
251 "type": "validation",
252 "details": errors,
253 "timestamp": chrono::Utc::now().to_rfc3339()
254 }
255 });
256
257 self.body = Some(ResponseBody::Json(error_data));
258 self.headers.push(("content-type".to_string(), "application/json".to_string()));
259 if self.status.is_none() {
260 self.status = Some(ElifStatusCode::UNPROCESSABLE_ENTITY);
261 }
262 self
263 }
264
265 pub fn not_found_with_message<S: Into<String>>(mut self, message: S) -> Self {
267 let error_data = serde_json::json!({
268 "error": {
269 "type": "not_found",
270 "message": message.into(),
271 "timestamp": chrono::Utc::now().to_rfc3339()
272 }
273 });
274
275 self.body = Some(ResponseBody::Json(error_data));
276 self.headers.push(("content-type".to_string(), "application/json".to_string()));
277 self.status = Some(ElifStatusCode::NOT_FOUND);
278 self
279 }
280
281 pub fn cors(mut self, origin: &str) -> Self {
285 self.headers.push(("access-control-allow-origin".to_string(), origin.to_string()));
286 self
287 }
288
289 pub fn cors_with_credentials(mut self, origin: &str) -> Self {
291 self.headers.push(("access-control-allow-origin".to_string(), origin.to_string()));
292 self.headers.push(("access-control-allow-credentials".to_string(), "true".to_string()));
293 self
294 }
295
296 pub fn with_security_headers(mut self) -> Self {
300 self.headers.extend([
301 ("x-content-type-options".to_string(), "nosniff".to_string()),
302 ("x-frame-options".to_string(), "DENY".to_string()),
303 ("x-xss-protection".to_string(), "1; mode=block".to_string()),
304 ("referrer-policy".to_string(), "strict-origin-when-cross-origin".to_string()),
305 ]);
306 self
307 }
308
309 pub fn send(self) -> HttpResult<ElifResponse> {
316 Ok(self.build())
317 }
318
319 pub fn finish(self) -> HttpResult<ElifResponse> {
323 Ok(self.build())
324 }
325
326 pub fn build(self) -> ElifResponse {
328 let mut response = ElifResponse::new();
329
330 if let Some(status) = self.status {
332 response = response.status(status);
333 }
334
335 let body_sets_content_type = matches!(
337 self.body,
338 Some(ResponseBody::Json(_)) | Some(ResponseBody::Text(_))
339 );
340
341 if let Some(body) = self.body {
343 match body {
344 ResponseBody::Empty => {},
345 ResponseBody::Text(text) => {
346 response = response.text(text);
347 }
348 ResponseBody::Bytes(bytes) => {
349 response = response.bytes(bytes);
350 }
351 ResponseBody::Json(value) => {
352 response = response.json_value(value);
353 }
354 }
355 }
356
357 let has_explicit_content_type = self.headers.iter()
359 .any(|(k, _)| k.to_lowercase() == "content-type");
360
361 for (key, value) in self.headers {
362 if key.to_lowercase() == "content-type" &&
364 body_sets_content_type &&
365 !has_explicit_content_type {
366 continue;
367 }
368
369 if let (Ok(name), Ok(val)) = (
370 crate::response::ElifHeaderName::from_str(&key),
371 crate::response::ElifHeaderValue::from_str(&value)
372 ) {
373 response.headers_mut().append(name, val);
375 } else {
376 return ElifResponse::internal_server_error();
377 }
378 }
379
380 response
381 }
382}
383
384impl Default for ResponseBuilder {
385 fn default() -> Self {
386 Self::new()
387 }
388}
389
390impl From<ResponseBuilder> for ElifResponse {
392 fn from(builder: ResponseBuilder) -> Self {
393 builder.build()
394 }
395}
396
397pub fn response() -> ResponseBuilder {
423 ResponseBuilder::new()
424}
425
426pub fn json_response<T: Serialize>(data: T) -> ResponseBuilder {
430 response().json(data)
431}
432
433pub fn text_response<S: Into<String>>(content: S) -> ResponseBuilder {
437 response().text(content)
438}
439
440pub fn redirect_response<S: Into<String>>(location: S) -> ResponseBuilder {
444 response().redirect(location)
445}
446
447#[cfg(test)]
448mod tests {
449 use super::*;
450 use serde_json::json;
451
452 #[test]
453 fn test_basic_response_builder() {
454 let resp: ElifResponse = response().text("Hello World").ok().into();
455 assert_eq!(resp.status_code(), ElifStatusCode::OK);
456 }
457
458 #[test]
459 fn test_json_response() {
460 let data = json!({"name": "Alice", "age": 30});
461 let resp: ElifResponse = response().json(data).into();
462 assert_eq!(resp.status_code(), ElifStatusCode::OK);
463 }
464
465 #[test]
466 fn test_status_helpers() {
467 let resp: ElifResponse = response().text("Created").created().into();
468 assert_eq!(resp.status_code(), ElifStatusCode::CREATED);
469
470 let resp: ElifResponse = response().text("Not Found").not_found().into();
471 assert_eq!(resp.status_code(), ElifStatusCode::NOT_FOUND);
472 }
473
474 #[test]
475 fn test_redirect_helpers() {
476 let resp: ElifResponse = response().redirect("/login").into();
477 assert_eq!(resp.status_code(), ElifStatusCode::FOUND);
478
479 let resp: ElifResponse = response().redirect("/users").permanent().into();
480 assert_eq!(resp.status_code(), ElifStatusCode::MOVED_PERMANENTLY);
481 }
482
483 #[test]
484 fn test_redirect_method_call_order_independence() {
485 let resp1: ElifResponse = response().redirect("/test").permanent().into();
489 assert_eq!(resp1.status_code(), ElifStatusCode::MOVED_PERMANENTLY);
490 assert!(resp1.has_header("location"));
491
492 let resp2: ElifResponse = response().permanent().redirect("/test").into();
494 assert_eq!(resp2.status_code(), ElifStatusCode::MOVED_PERMANENTLY);
495 assert!(resp2.has_header("location"));
496
497 assert_eq!(resp1.status_code(), resp2.status_code());
499 }
500
501 #[test]
502 fn test_temporary_method_call_order_independence() {
503 let resp1: ElifResponse = response().redirect("/test").temporary().into();
507 assert_eq!(resp1.status_code(), ElifStatusCode::FOUND);
508 assert!(resp1.has_header("location"));
509
510 let resp2: ElifResponse = response().temporary().redirect("/test").into();
512 assert_eq!(resp2.status_code(), ElifStatusCode::FOUND);
513 assert!(resp2.has_header("location"));
514
515 assert_eq!(resp1.status_code(), resp2.status_code());
517 }
518
519 #[test]
520 fn test_redirect_status_override_behavior() {
521 let resp: ElifResponse = response().redirect("/default").into();
525 assert_eq!(resp.status_code(), ElifStatusCode::FOUND);
526
527 let resp: ElifResponse = response().permanent().redirect("/perm").into();
529 assert_eq!(resp.status_code(), ElifStatusCode::MOVED_PERMANENTLY);
530
531 let resp: ElifResponse = response().temporary().redirect("/temp").into();
533 assert_eq!(resp.status_code(), ElifStatusCode::FOUND);
534
535 let resp: ElifResponse = response().redirect("/test").permanent().into();
537 assert_eq!(resp.status_code(), ElifStatusCode::MOVED_PERMANENTLY);
538
539 let resp: ElifResponse = response().redirect("/test").permanent().temporary().into();
541 assert_eq!(resp.status_code(), ElifStatusCode::FOUND);
542 }
543
544 #[test]
545 fn test_header_chaining() {
546 let resp: ElifResponse = response()
547 .text("Hello")
548 .header("x-custom", "value")
549 .cache_control("no-cache")
550 .into();
551
552 assert!(resp.has_header("x-custom"));
553 assert!(resp.has_header("cache-control"));
554 }
555
556 #[test]
557 fn test_complex_chaining() {
558 let user_data = json!({"id": 1, "name": "Alice"});
559 let resp: ElifResponse = response()
560 .json(user_data)
561 .created()
562 .location("/users/1")
563 .cache_control("no-cache")
564 .header("x-custom", "test")
565 .into();
566
567 assert_eq!(resp.status_code(), ElifStatusCode::CREATED);
568 assert!(resp.has_header("location"));
569 assert!(resp.has_header("cache-control"));
570 assert!(resp.has_header("x-custom"));
571 }
572
573 #[test]
574 fn test_error_responses() {
575 let resp: ElifResponse = response().error("Something went wrong").internal_server_error().into();
576 assert_eq!(resp.status_code(), ElifStatusCode::INTERNAL_SERVER_ERROR);
577
578 let validation_errors = json!({"email": ["Email is required"]});
579 let resp: ElifResponse = response().validation_error(validation_errors).into();
580 assert_eq!(resp.status_code(), ElifStatusCode::UNPROCESSABLE_ENTITY);
581 }
582
583 #[test]
584 fn test_global_helpers() {
585 let data = json!({"message": "Hello"});
586 let resp: ElifResponse = json_response(data).ok().into();
587 assert_eq!(resp.status_code(), ElifStatusCode::OK);
588
589 let resp: ElifResponse = text_response("Hello World").into();
590 assert_eq!(resp.status_code(), ElifStatusCode::OK);
591
592 let resp: ElifResponse = redirect_response("/home").into();
593 assert_eq!(resp.status_code(), ElifStatusCode::FOUND);
594 }
595
596 #[test]
597 fn test_cors_helpers() {
598 let resp: ElifResponse = response()
599 .json(json!({"data": "test"}))
600 .cors("*")
601 .into();
602
603 assert!(resp.has_header("access-control-allow-origin"));
604 }
605
606 #[test]
607 fn test_security_headers() {
608 let resp: ElifResponse = response()
609 .text("Secure content")
610 .with_security_headers()
611 .into();
612
613 assert!(resp.has_header("x-content-type-options"));
614 assert!(resp.has_header("x-frame-options"));
615 assert!(resp.has_header("x-xss-protection"));
616 assert!(resp.has_header("referrer-policy"));
617 }
618
619 #[test]
620 fn test_multi_value_headers() {
621 let resp: ElifResponse = response()
623 .text("Hello")
624 .header("set-cookie", "session=abc123; Path=/")
625 .header("set-cookie", "theme=dark; Path=/")
626 .header("set-cookie", "lang=en; Path=/")
627 .into();
628
629 assert!(resp.has_header("set-cookie"));
631
632 assert_eq!(resp.status_code(), ElifStatusCode::OK);
634 }
635
636 #[test]
637 fn test_cookie_helper_method() {
638 let resp: ElifResponse = response()
640 .json(json!({"user": "alice"}))
641 .cookie("session=12345; HttpOnly; Secure")
642 .cookie("csrf=token123; SameSite=Strict")
643 .cookie("theme=dark; Path=/")
644 .created()
645 .into();
646
647 assert!(resp.has_header("set-cookie"));
648 assert_eq!(resp.status_code(), ElifStatusCode::CREATED);
649 }
650
651 #[test]
652 fn test_terminal_methods() {
653 let result: HttpResult<ElifResponse> = response()
655 .json(json!({"data": "test"}))
656 .created()
657 .send();
658
659 assert!(result.is_ok());
660 let resp = result.unwrap();
661 assert_eq!(resp.status_code(), ElifStatusCode::CREATED);
662
663 let result: HttpResult<ElifResponse> = response()
665 .text("Hello World")
666 .cache_control("no-cache")
667 .finish();
668
669 assert!(result.is_ok());
670 let resp = result.unwrap();
671 assert_eq!(resp.status_code(), ElifStatusCode::OK);
672 assert!(resp.has_header("cache-control"));
673 }
674
675 #[test]
676 fn test_laravel_style_chaining() {
677 let result: HttpResult<ElifResponse> = response()
679 .json(json!({"user_id": 123}))
680 .created()
681 .location("/users/123")
682 .cookie("session=abc123; HttpOnly")
683 .header("x-custom", "value")
684 .send();
685
686 assert!(result.is_ok());
687 let resp = result.unwrap();
688 assert_eq!(resp.status_code(), ElifStatusCode::CREATED);
689 assert!(resp.has_header("location"));
690 assert!(resp.has_header("set-cookie"));
691 assert!(resp.has_header("x-custom"));
692 }
693
694 #[test]
695 fn test_json_serialization_error_handling() {
696 use std::collections::HashMap;
697
698 let valid_data = HashMap::from([("key", "value")]);
703 let resp: ElifResponse = response()
704 .json(valid_data)
705 .into();
706
707 assert_eq!(resp.status_code(), ElifStatusCode::OK);
708
709 }
719
720 #[test]
721 fn test_header_append_vs_insert_behavior() {
722 let resp: ElifResponse = response()
724 .json(json!({"test": "data"}))
725 .header("x-custom", "value1")
726 .header("x-custom", "value2")
727 .header("x-custom", "value3")
728 .into();
729
730 assert!(resp.has_header("x-custom"));
731 assert_eq!(resp.status_code(), ElifStatusCode::OK);
732
733 }
736}