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
67            .body(Full::new(Bytes::from(self.body)))
68            .unwrap()
69    }
70}
71
72impl Default for HttpResponse {
73    fn default() -> Self {
74        Self::new()
75    }
76}
77
78/// Extension trait for Response to enable method chaining on macros
79pub trait ResponseExt {
80    fn status(self, code: u16) -> Self;
81    fn header(self, name: impl Into<String>, value: impl Into<String>) -> Self;
82}
83
84impl ResponseExt for Response {
85    fn status(self, code: u16) -> Self {
86        self.map(|r| r.status(code))
87    }
88
89    fn header(self, name: impl Into<String>, value: impl Into<String>) -> Self {
90        self.map(|r| r.header(name, value))
91    }
92}
93
94/// HTTP Redirect response builder
95pub struct Redirect {
96    location: String,
97    query_params: Vec<(String, String)>,
98    status: u16,
99}
100
101impl Redirect {
102    /// Create a redirect to a specific URL/path
103    pub fn to(path: impl Into<String>) -> Self {
104        Self {
105            location: path.into(),
106            query_params: Vec::new(),
107            status: 302,
108        }
109    }
110
111    /// Create a redirect to a named route
112    pub fn route(name: &str) -> RedirectRouteBuilder {
113        RedirectRouteBuilder {
114            name: name.to_string(),
115            params: std::collections::HashMap::new(),
116            query_params: Vec::new(),
117            status: 302,
118        }
119    }
120
121    /// Add a query parameter
122    pub fn query(mut self, key: &str, value: impl Into<String>) -> Self {
123        self.query_params.push((key.to_string(), value.into()));
124        self
125    }
126
127    /// Set status to 301 (Moved Permanently)
128    pub fn permanent(mut self) -> Self {
129        self.status = 301;
130        self
131    }
132
133    fn build_url(&self) -> String {
134        if self.query_params.is_empty() {
135            self.location.clone()
136        } else {
137            let query = self
138                .query_params
139                .iter()
140                .map(|(k, v)| format!("{}={}", k, v))
141                .collect::<Vec<_>>()
142                .join("&");
143            format!("{}?{}", self.location, query)
144        }
145    }
146}
147
148/// Auto-convert Redirect to Response
149impl From<Redirect> for Response {
150    fn from(redirect: Redirect) -> Response {
151        Ok(HttpResponse::new()
152            .status(redirect.status)
153            .header("Location", redirect.build_url()))
154    }
155}
156
157/// Builder for redirects to named routes with parameters
158pub struct RedirectRouteBuilder {
159    name: String,
160    params: std::collections::HashMap<String, String>,
161    query_params: Vec<(String, String)>,
162    status: u16,
163}
164
165impl RedirectRouteBuilder {
166    /// Add a route parameter value
167    pub fn with(mut self, key: &str, value: impl Into<String>) -> Self {
168        self.params.insert(key.to_string(), value.into());
169        self
170    }
171
172    /// Add a query parameter
173    pub fn query(mut self, key: &str, value: impl Into<String>) -> Self {
174        self.query_params.push((key.to_string(), value.into()));
175        self
176    }
177
178    /// Set status to 301 (Moved Permanently)
179    pub fn permanent(mut self) -> Self {
180        self.status = 301;
181        self
182    }
183
184    fn build_url(&self) -> Option<String> {
185        use crate::routing::route_with_params;
186
187        let mut url = route_with_params(&self.name, &self.params)?;
188        if !self.query_params.is_empty() {
189            let query = self
190                .query_params
191                .iter()
192                .map(|(k, v)| format!("{}={}", k, v))
193                .collect::<Vec<_>>()
194                .join("&");
195            url = format!("{}?{}", url, query);
196        }
197        Some(url)
198    }
199}
200
201/// Auto-convert RedirectRouteBuilder to Response
202impl From<RedirectRouteBuilder> for Response {
203    fn from(redirect: RedirectRouteBuilder) -> Response {
204        let url = redirect.build_url().ok_or_else(|| {
205            HttpResponse::text(format!("Route '{}' not found", redirect.name)).status(500)
206        })?;
207        Ok(HttpResponse::new()
208            .status(redirect.status)
209            .header("Location", url))
210    }
211}
212
213/// Auto-convert FrameworkError to HttpResponse
214///
215/// This enables using the `?` operator in controller handlers to propagate
216/// framework errors as appropriate HTTP responses.
217impl From<crate::error::FrameworkError> for HttpResponse {
218    fn from(err: crate::error::FrameworkError) -> HttpResponse {
219        let status = err.status_code();
220        let body = match &err {
221            crate::error::FrameworkError::ParamError { param_name } => {
222                serde_json::json!({
223                    "error": format!("Missing required parameter: {}", param_name)
224                })
225            }
226            crate::error::FrameworkError::ValidationError { field, message } => {
227                serde_json::json!({
228                    "error": "Validation failed",
229                    "field": field,
230                    "message": message
231                })
232            }
233            _ => {
234                serde_json::json!({
235                    "error": err.to_string()
236                })
237            }
238        };
239        HttpResponse::json(body).status(status)
240    }
241}