1use candid::{CandidType, Deserialize};
11use serde_bytes::ByteBuf;
12
13#[derive(Clone, Debug, CandidType, Deserialize)]
18pub struct HttpRequest {
19 pub method: String,
21 pub url: String,
23 pub headers: Vec<(String, String)>,
25 pub body: ByteBuf,
27}
28
29impl HttpRequest {
30 pub fn path(&self) -> &str {
50 match self.url.find('?') {
51 None => &self.url[..],
52 Some(index) => &self.url[..index],
53 }
54 }
55
56 pub fn raw_query_param(&self, param: &str) -> Option<&str> {
78 const QUERY_SEPARATOR: &str = "?";
79 let query_string = self.url.split(QUERY_SEPARATOR).nth(1)?;
80 if query_string.is_empty() {
81 return None;
82 }
83 const PARAMETER_SEPARATOR: &str = "&";
84 for chunk in query_string.split(PARAMETER_SEPARATOR) {
85 const KEY_VALUE_SEPARATOR: &str = "=";
86 let mut split = chunk.splitn(2, KEY_VALUE_SEPARATOR);
87 let name = split.next()?;
88 if name == param {
89 return Some(split.next().unwrap_or_default());
90 }
91 }
92 None
93 }
94}
95
96#[derive(Clone, Debug, CandidType, Deserialize)]
118pub struct HttpResponse {
119 pub status_code: u16,
121 pub headers: Vec<(String, String)>,
123 pub body: ByteBuf,
125}
126
127pub struct HttpResponseBuilder(HttpResponse);
162
163impl HttpResponseBuilder {
164 pub fn ok() -> Self {
166 Self(HttpResponse {
167 status_code: 200,
168 headers: vec![],
169 body: ByteBuf::default(),
170 })
171 }
172
173 pub fn bad_request() -> Self {
175 Self(HttpResponse {
176 status_code: 400,
177 headers: vec![],
178 body: ByteBuf::from("bad request"),
179 })
180 }
181
182 pub fn not_found() -> Self {
184 Self(HttpResponse {
185 status_code: 404,
186 headers: vec![],
187 body: ByteBuf::from("not found"),
188 })
189 }
190
191 pub fn server_error(reason: impl ToString) -> Self {
196 Self(HttpResponse {
197 status_code: 500,
198 headers: vec![],
199 body: ByteBuf::from(reason.to_string()),
200 })
201 }
202
203 pub fn header(mut self, name: impl ToString, value: impl ToString) -> Self {
209 self.0.headers.push((name.to_string(), value.to_string()));
210 self
211 }
212
213 pub fn body(mut self, bytes: impl Into<Vec<u8>>) -> Self {
218 self.0.body = ByteBuf::from(bytes.into());
219 self
220 }
221
222 pub fn with_body_and_content_length(self, bytes: impl Into<Vec<u8>>) -> Self {
227 let bytes = bytes.into();
228 self.header("Content-Length", bytes.len()).body(bytes)
229 }
230
231 pub fn build(self) -> HttpResponse {
233 self.0
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 #[test]
242 fn path_returns_full_url_when_no_query_string() {
243 let http_request = HttpRequest {
244 method: "GET".to_string(),
245 url: "/path/to/resource".to_string(),
246 headers: vec![],
247 body: Default::default(),
248 };
249 assert_eq!(http_request.path(), "/path/to/resource");
250 }
251
252 #[test]
253 fn path_returns_path_without_query_string() {
254 let http_request = HttpRequest {
255 method: "GET".to_string(),
256 url: "/path/to/resource?query=1".to_string(),
257 headers: vec![],
258 body: Default::default(),
259 };
260 assert_eq!(http_request.path(), "/path/to/resource");
261 }
262
263 #[test]
264 fn path_handles_empty_url() {
265 let http_request = HttpRequest {
266 method: "GET".to_string(),
267 url: "".to_string(),
268 headers: vec![],
269 body: Default::default(),
270 };
271 assert_eq!(http_request.path(), "");
272 }
273
274 #[test]
275 fn raw_query_param_returns_none_for_empty_query_string() {
276 let http_request = HttpRequest {
277 method: "GET".to_string(),
278 url: "/endpoint?".to_string(),
279 headers: vec![],
280 body: Default::default(),
281 };
282 assert_eq!(http_request.raw_query_param("key"), None);
283 }
284
285 #[test]
286 fn raw_query_param_returns_none_for_missing_key() {
287 let http_request = HttpRequest {
288 method: "GET".to_string(),
289 url: "/endpoint?other=value".to_string(),
290 headers: vec![],
291 body: Default::default(),
292 };
293 assert_eq!(http_request.raw_query_param("key"), None);
294 }
295
296 #[test]
297 fn raw_query_param_returns_empty_value_for_key_without_value() {
298 let http_request = HttpRequest {
299 method: "GET".to_string(),
300 url: "/endpoint?key=".to_string(),
301 headers: vec![],
302 body: Default::default(),
303 };
304 assert_eq!(http_request.raw_query_param("key"), Some(""));
305 }
306
307 #[test]
308 fn raw_query_param_handles_multiple_keys_with_same_name() {
309 let http_request = HttpRequest {
310 method: "GET".to_string(),
311 url: "/endpoint?key=value1&key=value2".to_string(),
312 headers: vec![],
313 body: Default::default(),
314 };
315 assert_eq!(http_request.raw_query_param("key"), Some("value1"));
316 }
317
318 #[test]
319 fn raw_query_param_handles_url_without_query_separator() {
320 let http_request = HttpRequest {
321 method: "GET".to_string(),
322 url: "/endpoint".to_string(),
323 headers: vec![],
324 body: Default::default(),
325 };
326 assert_eq!(http_request.raw_query_param("key"), None);
327 }
328
329 #[test]
330 fn raw_query_param_returns_none_for_partial_match() {
331 let http_request = HttpRequest {
332 method: "GET".to_string(),
333 url: "/endpoint?key1=value1".to_string(),
334 headers: vec![],
335 body: Default::default(),
336 };
337 assert_eq!(http_request.raw_query_param("key"), None);
338 }
339
340 #[test]
341 fn ok_response_has_status_200() {
342 let response = HttpResponseBuilder::ok().build();
343 assert_eq!(response.status_code, 200);
344 assert!(response.body.is_empty());
345 }
346
347 #[test]
348 fn bad_request_response_has_status_400_and_default_body() {
349 let response = HttpResponseBuilder::bad_request().build();
350 assert_eq!(response.status_code, 400);
351 assert_eq!(response.body, ByteBuf::from("bad request"));
352 }
353
354 #[test]
355 fn not_found_response_has_status_404_and_default_body() {
356 let response = HttpResponseBuilder::not_found().build();
357 assert_eq!(response.status_code, 404);
358 assert_eq!(response.body, ByteBuf::from("not found"));
359 }
360
361 #[test]
362 fn server_error_response_has_status_500_and_custom_body() {
363 let response = HttpResponseBuilder::server_error("internal error").build();
364 assert_eq!(response.status_code, 500);
365 assert_eq!(response.body, ByteBuf::from("internal error"));
366 }
367
368 #[test]
369 fn response_builder_adds_headers_correctly() {
370 let response = HttpResponseBuilder::ok()
371 .header("Content-Type", "application/json")
372 .header("Cache-Control", "no-cache")
373 .build();
374 assert_eq!(
375 response.headers,
376 vec![
377 ("Content-Type".to_string(), "application/json".to_string()),
378 ("Cache-Control".to_string(), "no-cache".to_string())
379 ]
380 );
381 }
382
383 #[test]
384 fn response_builder_sets_body_correctly() {
385 let response = HttpResponseBuilder::ok().body("response body").build();
386 assert_eq!(response.body, ByteBuf::from("response body"));
387 }
388
389 #[test]
390 fn response_builder_sets_body_and_content_length() {
391 let response = HttpResponseBuilder::ok()
392 .with_body_and_content_length("response body")
393 .build();
394 assert_eq!(response.body, ByteBuf::from("response body"));
395 assert_eq!(
396 response.headers,
397 vec![("Content-Length".to_string(), "13".to_string())]
398 );
399 }
400}