Skip to main content

crux_http/
config.rs

1//! Configuration for `HttpClient`s.
2
3use http::{HeaderMap, HeaderValue};
4use std::fmt::Debug;
5use url::Url;
6
7use crate::{HttpError, Result};
8
9/// Configuration for `crux_http::Http`s and their underlying HTTP client.
10#[non_exhaustive]
11#[derive(Clone, Debug, Default)]
12pub struct Config {
13    /// The base URL for a client. All request URLs will be relative to this URL.
14    ///
15    /// Note: a trailing slash is significant.
16    /// Without it, the last path component is considered to be a "file" name
17    /// to be removed to get at the "directory" that is used as the base.
18    pub base_url: Option<Url>,
19    /// Headers to be applied to every request made by this client.
20    pub headers: HeaderMap,
21}
22
23impl Config {
24    /// Construct new empty config.
25    #[must_use]
26    pub fn new() -> Self {
27        Self::default()
28    }
29}
30
31impl Config {
32    /// Adds a header to be added to every request by this config.
33    ///
34    /// Default: No extra headers.
35    ///
36    /// # Errors
37    /// Returns an error if the header value is invalid.
38    pub fn add_header(
39        mut self,
40        name: impl http::header::IntoHeaderName,
41        value: impl AsRef<str>,
42    ) -> Result<Self> {
43        let value =
44            HeaderValue::from_str(value.as_ref()).map_err(|e| HttpError::Io(e.to_string()))?;
45        self.headers.append(name, value);
46        Ok(self)
47    }
48
49    /// Sets the base URL for this config.
50    #[must_use]
51    pub fn set_base_url(mut self, base: Url) -> Self {
52        self.base_url = Some(base);
53        self
54    }
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    #[test]
62    fn add_header_stores_valid_value() {
63        let config = Config::default().add_header("x-api-key", "secret").unwrap();
64        let val = config.headers.get("x-api-key").unwrap();
65        assert_eq!(val.to_str().unwrap(), "secret");
66    }
67
68    #[test]
69    fn add_header_called_twice_preserves_both_values() {
70        let config = Config::default()
71            .add_header("accept", "text/html")
72            .unwrap()
73            .add_header("accept", "application/json")
74            .unwrap();
75        let values: Vec<&str> = config
76            .headers
77            .get_all("accept")
78            .iter()
79            .map(|v| v.to_str().unwrap())
80            .collect();
81        assert_eq!(values, ["text/html", "application/json"]);
82    }
83
84    #[test]
85    fn add_header_rejects_invalid_value() {
86        // Control characters (other than tab) are rejected by HeaderValue::from_str.
87        let result = Config::default().add_header("x-bad", "val\x00ue");
88        assert!(
89            matches!(result, Err(HttpError::Io(_))),
90            "invalid header value must return HttpError::Io"
91        );
92    }
93}