ic_http_types/
lib.rs

1//! This crate provides types for representing HTTP requests and responses. These types are
2//! designed to simplify working with HTTP communication in canister development on the Internet
3//! Computer.
4//!
5//! It includes:
6//! - `HttpRequest`: A struct for encapsulating HTTP requests.
7//! - `HttpResponse`: A struct for encapsulating HTTP responses.
8//! - `HttpResponseBuilder`: A builder for constructing `HttpResponse` objects.
9
10use candid::{CandidType, Deserialize};
11use serde_bytes::ByteBuf;
12
13/// Represents an HTTP request.
14///
15/// This struct is used to encapsulate the details of an HTTP request, including
16/// the HTTP method, URL, headers, and body.
17#[derive(Clone, Debug, CandidType, Deserialize)]
18pub struct HttpRequest {
19    /// The HTTP method (e.g., "GET", "POST").
20    pub method: String,
21    /// The URL of the request.
22    pub url: String,
23    /// A list of headers, where each header is represented as a key-value pair.
24    pub headers: Vec<(String, String)>,
25    /// The body of the request, represented as a byte buffer.
26    pub body: ByteBuf,
27}
28
29impl HttpRequest {
30    /// Extracts the path from the URL.
31    ///
32    /// If the URL contains a query string, the path is the portion before the `?`.
33    /// If no query string is present, the entire URL is returned.
34    ///
35    /// # Examples
36    ///
37    /// ```
38    /// use ic_http_types::{HttpRequest};
39    /// use serde_bytes::ByteBuf;
40    ///
41    /// let request = HttpRequest {
42    ///     method: "GET".to_string(),
43    ///     url: "/path/to/resource?query=1".to_string(),
44    ///     headers: vec![],
45    ///     body: ByteBuf::default(),
46    /// };
47    /// assert_eq!(request.path(), "/path/to/resource");
48    /// ```
49    pub fn path(&self) -> &str {
50        match self.url.find('?') {
51            None => &self.url[..],
52            Some(index) => &self.url[..index],
53        }
54    }
55
56    /// Searches for the first appearance of a parameter in the request URL.
57    ///
58    /// Returns `None` if the given parameter does not appear in the query string.
59    ///
60    /// # Parameters
61    /// - `param`: The name of the query parameter to search for.
62    ///
63    /// # Examples
64    ///
65    /// ```
66    /// use ic_http_types::{HttpRequest};
67    /// use serde_bytes::ByteBuf;
68    ///
69    /// let request = HttpRequest {
70    ///     method: "GET".to_string(),
71    ///     url: "/path?key=value".to_string(),
72    ///     headers: vec![],
73    ///     body: ByteBuf::default(),
74    /// };
75    /// assert_eq!(request.raw_query_param("key"), Some("value"));
76    /// ```
77    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/// Represents an HTTP response.
97///
98/// This struct is used to encapsulate the details of an HTTP response, including
99/// the status code, headers, and body.
100///
101/// # Examples
102///
103/// ```
104/// use ic_http_types::{HttpResponse};
105/// use serde_bytes::ByteBuf;
106///
107/// let response = HttpResponse {
108///     status_code: 200,
109///     headers: vec![("Content-Type".to_string(), "application/json".to_string())],
110///     body: ByteBuf::from("response body"),
111/// };
112///
113/// assert_eq!(response.status_code, 200);
114/// assert_eq!(response.headers.len(), 1);
115/// assert_eq!(response.body, ByteBuf::from("response body"));
116/// ```
117#[derive(Clone, Debug, CandidType, Deserialize)]
118pub struct HttpResponse {
119    /// The HTTP status code (e.g., 200 for OK, 404 for Not Found).
120    pub status_code: u16,
121    /// A list of headers, where each header is represented as a key-value pair.
122    pub headers: Vec<(String, String)>,
123    /// The body of the response, represented as a byte buffer.
124    pub body: ByteBuf,
125}
126
127/// A builder for constructing `HttpResponse` objects.
128///
129/// This struct provides a convenient way to create HTTP responses with
130/// customizable status codes, headers, and bodies.
131///
132///
133/// # Examples
134///
135/// ```
136/// use ic_http_types::{HttpResponseBuilder};
137/// use serde_bytes::ByteBuf;
138///
139/// let response = HttpResponseBuilder::ok()
140///     .header("Content-Type", "application/json")
141///     .body("response body")
142///     .build();
143///
144/// assert_eq!(response.status_code, 200);
145/// assert_eq!(response.headers, vec![("Content-Type".to_string(), "application/json".to_string())]);
146/// assert_eq!(response.body, ByteBuf::from("response body"));
147/// ```
148///
149/// ```
150/// use ic_http_types::{HttpResponseBuilder};
151/// use serde_bytes::ByteBuf;
152///
153/// let response = HttpResponseBuilder::server_error("internal error")
154///     .header("Retry-After", "120")
155///     .build();
156///
157/// assert_eq!(response.status_code, 500);
158/// assert_eq!(response.headers, vec![("Retry-After".to_string(), "120".to_string())]);
159/// assert_eq!(response.body, ByteBuf::from("internal error"));
160/// ```
161pub struct HttpResponseBuilder(HttpResponse);
162
163impl HttpResponseBuilder {
164    /// Creates a new `HttpResponse` with a 200 OK status.
165    pub fn ok() -> Self {
166        Self(HttpResponse {
167            status_code: 200,
168            headers: vec![],
169            body: ByteBuf::default(),
170        })
171    }
172
173    /// Creates a new `HttpResponse` with a 400 Bad Request status.
174    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    /// Creates a new `HttpResponse` with a 404 Not Found status.
183    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    /// Creates a new `HttpResponse` with a 500 Internal Server Error status.
192    ///
193    /// # Parameters
194    /// - `reason`: A string describing the reason for the server error.
195    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    /// Adds a header to the `HttpResponse`.
204    ///
205    /// # Parameters
206    /// - `name`: The name of the header.
207    /// - `value`: The value of the header.
208    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    /// Sets the body of the `HttpResponse`.
214    ///
215    /// # Parameters
216    /// - `bytes`: The body content as a byte array.
217    pub fn body(mut self, bytes: impl Into<Vec<u8>>) -> Self {
218        self.0.body = ByteBuf::from(bytes.into());
219        self
220    }
221
222    /// Sets the body of the `HttpResponse` and adds a `Content-Length` header.
223    ///
224    /// # Parameters
225    /// - `bytes`: The body content as a byte array.
226    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    /// Finalizes the builder and returns the constructed `HttpResponse`.
232    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}