Skip to main content

typeway_client/
config.rs

1//! Client configuration with timeout, retry, interceptor, and header settings.
2
3use std::fmt;
4use std::time::Duration;
5
6use reqwest::header::{HeaderMap, HeaderName, HeaderValue, ACCEPT};
7
8use crate::interceptor::{RequestInterceptor, ResponseInterceptor};
9use crate::retry::RetryPolicy;
10
11/// Configuration for the [`Client`](crate::Client).
12///
13/// Controls per-request timeouts, TCP connect timeouts, retry behavior,
14/// request/response interceptors, default headers, and cookie persistence.
15///
16/// # Example
17///
18/// ```
19/// use typeway_client::{ClientConfig, RetryPolicy, RequestInterceptor};
20/// use std::sync::Arc;
21/// use std::time::Duration;
22///
23/// let config = ClientConfig::default()
24///     .timeout(Duration::from_secs(60))
25///     .retry_policy(RetryPolicy::none())
26///     .bearer_auth("my-token")
27///     .cookie_store(true)
28///     .request_interceptor(Arc::new(|req| {
29///         req.header("X-Request-Id", "abc123")
30///     }));
31/// ```
32pub struct ClientConfig {
33    /// Per-request timeout. `None` means no timeout.
34    pub timeout: Option<Duration>,
35    /// TCP connection timeout. `None` means no timeout.
36    pub connect_timeout: Option<Duration>,
37    /// Retry policy for failed requests.
38    pub retry_policy: RetryPolicy,
39    /// Interceptors applied to every outgoing request.
40    pub request_interceptors: Vec<RequestInterceptor>,
41    /// Interceptors called on every incoming response.
42    pub response_interceptors: Vec<ResponseInterceptor>,
43    /// Headers sent with every request.
44    pub default_headers: HeaderMap,
45    /// Whether to enable automatic cookie persistence across requests.
46    pub cookie_store: bool,
47    /// Whether to enable built-in request/response tracing via the `tracing` crate.
48    ///
49    /// When enabled, every request logs the HTTP method, URL, response status,
50    /// and elapsed time at `DEBUG` level.
51    pub enable_tracing: bool,
52}
53
54impl Default for ClientConfig {
55    fn default() -> Self {
56        let mut default_headers = HeaderMap::new();
57        default_headers.insert(ACCEPT, HeaderValue::from_static("application/json"));
58        Self {
59            timeout: Some(Duration::from_secs(30)),
60            connect_timeout: Some(Duration::from_secs(10)),
61            retry_policy: RetryPolicy::default(),
62            request_interceptors: Vec::new(),
63            response_interceptors: Vec::new(),
64            default_headers,
65            cookie_store: false,
66            enable_tracing: false,
67        }
68    }
69}
70
71impl Clone for ClientConfig {
72    fn clone(&self) -> Self {
73        Self {
74            timeout: self.timeout,
75            connect_timeout: self.connect_timeout,
76            retry_policy: self.retry_policy.clone(),
77            request_interceptors: self.request_interceptors.clone(),
78            response_interceptors: self.response_interceptors.clone(),
79            default_headers: self.default_headers.clone(),
80            cookie_store: self.cookie_store,
81            enable_tracing: self.enable_tracing,
82        }
83    }
84}
85
86impl fmt::Debug for ClientConfig {
87    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88        f.debug_struct("ClientConfig")
89            .field("timeout", &self.timeout)
90            .field("connect_timeout", &self.connect_timeout)
91            .field("retry_policy", &self.retry_policy)
92            .field(
93                "request_interceptors",
94                &format!("[{} interceptor(s)]", self.request_interceptors.len()),
95            )
96            .field(
97                "response_interceptors",
98                &format!("[{} interceptor(s)]", self.response_interceptors.len()),
99            )
100            .field("default_headers", &self.default_headers)
101            .field("cookie_store", &self.cookie_store)
102            .field("enable_tracing", &self.enable_tracing)
103            .finish()
104    }
105}
106
107impl ClientConfig {
108    /// Set the per-request timeout.
109    pub fn timeout(mut self, d: Duration) -> Self {
110        self.timeout = Some(d);
111        self
112    }
113
114    /// Disable the per-request timeout.
115    pub fn no_timeout(mut self) -> Self {
116        self.timeout = None;
117        self
118    }
119
120    /// Set the TCP connect timeout.
121    pub fn connect_timeout(mut self, d: Duration) -> Self {
122        self.connect_timeout = Some(d);
123        self
124    }
125
126    /// Disable the TCP connect timeout.
127    pub fn no_connect_timeout(mut self) -> Self {
128        self.connect_timeout = None;
129        self
130    }
131
132    /// Set the retry policy.
133    pub fn retry_policy(mut self, policy: RetryPolicy) -> Self {
134        self.retry_policy = policy;
135        self
136    }
137
138    /// Add a request interceptor that modifies outgoing requests.
139    ///
140    /// Interceptors are applied in the order they are added.
141    ///
142    /// # Example
143    ///
144    /// ```
145    /// use typeway_client::{ClientConfig, RequestInterceptor};
146    /// use std::sync::Arc;
147    ///
148    /// let config = ClientConfig::default()
149    ///     .request_interceptor(Arc::new(|req| {
150    ///         req.header("X-Trace-Id", "abc")
151    ///     }));
152    /// ```
153    pub fn request_interceptor(mut self, interceptor: RequestInterceptor) -> Self {
154        self.request_interceptors.push(interceptor);
155        self
156    }
157
158    /// Add a response interceptor that inspects incoming responses.
159    ///
160    /// Interceptors are called in the order they are added.
161    ///
162    /// # Example
163    ///
164    /// ```
165    /// use typeway_client::{ClientConfig, ResponseInterceptor};
166    /// use std::sync::Arc;
167    ///
168    /// let config = ClientConfig::default()
169    ///     .response_interceptor(Arc::new(|resp| {
170    ///         eprintln!("status: {}", resp.status());
171    ///     }));
172    /// ```
173    pub fn response_interceptor(mut self, interceptor: ResponseInterceptor) -> Self {
174        self.response_interceptors.push(interceptor);
175        self
176    }
177
178    /// Add a default header sent with every request.
179    ///
180    /// # Panics
181    ///
182    /// Panics if `name` or `value` cannot be parsed as valid HTTP header
183    /// components.
184    pub fn default_header(mut self, name: HeaderName, value: HeaderValue) -> Self {
185        self.default_headers.insert(name, value);
186        self
187    }
188
189    /// Convenience method to set a `Bearer` authentication token.
190    ///
191    /// This adds an `Authorization: Bearer <token>` header to every request.
192    pub fn bearer_auth(self, token: &str) -> Self {
193        let value = HeaderValue::from_str(&format!("Bearer {token}"))
194            .expect("bearer token contains invalid header characters");
195        self.default_header(http::header::AUTHORIZATION, value)
196    }
197
198    /// Enable or disable automatic cookie persistence across requests.
199    ///
200    /// When enabled, the underlying HTTP client stores cookies from responses
201    /// and sends them back in subsequent requests, providing session-like
202    /// behavior.
203    pub fn cookie_store(mut self, enabled: bool) -> Self {
204        self.cookie_store = enabled;
205        self
206    }
207
208    /// Enable built-in request/response tracing.
209    ///
210    /// When enabled, every HTTP request logs the method, URL, response status,
211    /// and elapsed time at `DEBUG` level via the [`tracing`] crate.
212    pub fn enable_tracing(mut self) -> Self {
213        self.enable_tracing = true;
214        self
215    }
216}