1#![warn(missing_docs, clippy::all)]
16#![allow(clippy::derive_partial_eq_without_eq)]
18
19use serde::de::{Deserializer, Error as _, Unexpected};
20use serde::Deserialize;
21use staged_builder::staged_builder;
22use std::collections::HashMap;
23use std::fmt;
24use std::path::{Path, PathBuf};
25use std::time::Duration;
26use url::Url;
27
28#[cfg(test)]
29mod test;
30
31#[derive(Debug, Clone, PartialEq, Default, Deserialize)]
54#[serde(rename_all = "kebab-case", default)]
55#[staged_builder]
56#[builder(update)]
57pub struct ServicesConfig {
58 #[builder(map(key(type = String, into), value(type = ServiceConfig)))]
59 services: HashMap<String, ServiceConfig>,
60 #[builder(default, into)]
61 security: Option<SecurityConfig>,
62 #[builder(default, into)]
63 proxy: Option<ProxyConfig>,
64 #[builder(default, into)]
65 #[serde(deserialize_with = "de_opt_duration")]
66 connect_timeout: Option<Duration>,
67 #[builder(default, into)]
68 #[serde(deserialize_with = "de_opt_duration")]
69 read_timeout: Option<Duration>,
70 #[builder(default, into)]
71 #[serde(deserialize_with = "de_opt_duration")]
72 write_timeout: Option<Duration>,
73 #[builder(default, into)]
74 #[serde(deserialize_with = "de_opt_duration")]
75 backoff_slot_size: Option<Duration>,
76}
77
78impl ServicesConfig {
79 pub fn merged_service(&self, name: &str) -> Option<ServiceConfig> {
81 let mut service = self.services.get(name).cloned()?;
82
83 if service.security.is_none() {
84 service.security = self.security.clone();
85 }
86
87 if service.proxy.is_none() {
88 service.proxy = self.proxy.clone();
89 }
90
91 if service.connect_timeout.is_none() {
92 service.connect_timeout = self.connect_timeout;
93 }
94
95 if service.read_timeout.is_none() {
96 service.read_timeout = self.read_timeout;
97 }
98
99 if service.write_timeout.is_none() {
100 service.write_timeout = self.write_timeout;
101 }
102
103 if service.backoff_slot_size.is_none() {
104 service.backoff_slot_size = self.backoff_slot_size;
105 }
106
107 Some(service)
108 }
109
110 pub fn security(&self) -> Option<&SecurityConfig> {
112 self.security.as_ref()
113 }
114
115 pub fn proxy(&self) -> Option<&ProxyConfig> {
117 self.proxy.as_ref()
118 }
119
120 pub fn connect_timeout(&self) -> Option<Duration> {
122 self.connect_timeout
123 }
124
125 pub fn read_timeout(&self) -> Option<Duration> {
127 self.read_timeout
128 }
129
130 pub fn write_timeout(&self) -> Option<Duration> {
132 self.write_timeout
133 }
134
135 pub fn backoff_slot_size(&self) -> Option<Duration> {
137 self.backoff_slot_size
138 }
139}
140
141#[derive(Debug, Default, Clone, PartialEq, Deserialize)]
143#[serde(rename_all = "kebab-case", default)]
144#[staged_builder]
145#[builder(update)]
146pub struct ServiceConfig {
147 #[builder(list(item(type = Url)))]
148 uris: Vec<Url>,
149 #[builder(default, into)]
150 security: Option<SecurityConfig>,
151 #[builder(default, into)]
152 proxy: Option<ProxyConfig>,
153 #[builder(default, into)]
154 #[serde(deserialize_with = "de_opt_duration")]
155 connect_timeout: Option<Duration>,
156 #[builder(default, into)]
157 #[serde(deserialize_with = "de_opt_duration")]
158 read_timeout: Option<Duration>,
159 #[builder(default, into)]
160 #[serde(deserialize_with = "de_opt_duration")]
161 write_timeout: Option<Duration>,
162 #[builder(default, into)]
163 #[serde(deserialize_with = "de_opt_duration")]
164 backoff_slot_size: Option<Duration>,
165 #[builder(default, into)]
166 max_num_retries: Option<u32>,
167}
168
169impl ServiceConfig {
170 pub fn uris(&self) -> &[Url] {
172 &self.uris
173 }
174
175 pub fn security(&self) -> Option<&SecurityConfig> {
177 self.security.as_ref()
178 }
179
180 pub fn proxy(&self) -> Option<&ProxyConfig> {
182 self.proxy.as_ref()
183 }
184
185 pub fn connect_timeout(&self) -> Option<Duration> {
187 self.connect_timeout
188 }
189
190 pub fn read_timeout(&self) -> Option<Duration> {
192 self.read_timeout
193 }
194
195 pub fn write_timeout(&self) -> Option<Duration> {
197 self.write_timeout
198 }
199
200 pub fn backoff_slot_size(&self) -> Option<Duration> {
202 self.backoff_slot_size
203 }
204
205 pub fn max_num_retries(&self) -> Option<u32> {
207 self.max_num_retries
208 }
209}
210
211#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Deserialize)]
213#[serde(rename_all = "kebab-case")]
214#[staged_builder]
215#[builder(update)]
216pub struct SecurityConfig {
217 #[builder(default, into)]
218 ca_file: Option<PathBuf>,
219 #[builder(default, into)]
220 key_file: Option<PathBuf>,
221 #[builder(default, into)]
222 cert_file: Option<PathBuf>,
223}
224
225impl SecurityConfig {
226 pub fn ca_file(&self) -> Option<&Path> {
230 self.ca_file.as_deref()
231 }
232
233 pub fn key_file(&self) -> Option<&Path> {
237 self.key_file.as_deref()
238 }
239
240 pub fn cert_file(&self) -> Option<&Path> {
245 self.cert_file.as_deref()
246 }
247}
248
249#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
251#[serde(rename_all = "kebab-case", tag = "type")]
252#[non_exhaustive]
253pub enum ProxyConfig {
254 Direct,
256 Http(HttpProxyConfig),
258}
259
260#[allow(clippy::derivable_impls)]
261impl Default for ProxyConfig {
262 fn default() -> ProxyConfig {
263 ProxyConfig::Direct
264 }
265}
266
267#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
269#[serde(rename_all = "kebab-case")]
270#[staged_builder]
271#[builder(update)]
272pub struct HttpProxyConfig {
273 host_and_port: HostAndPort,
274 #[builder(default, into)]
275 credentials: Option<BasicCredentials>,
276}
277
278impl HttpProxyConfig {
279 pub fn host_and_port(&self) -> &HostAndPort {
281 &self.host_and_port
282 }
283
284 pub fn credentials(&self) -> Option<&BasicCredentials> {
286 self.credentials.as_ref()
287 }
288}
289
290#[derive(Debug, Clone, PartialEq, Eq, Hash)]
292pub struct HostAndPort {
293 host: String,
294 port: u16,
295}
296
297impl fmt::Display for HostAndPort {
298 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
299 write!(fmt, "{}:{}", self.host, self.port)
300 }
301}
302
303impl<'de> Deserialize<'de> for HostAndPort {
304 fn deserialize<D>(deserializer: D) -> Result<HostAndPort, D::Error>
305 where
306 D: Deserializer<'de>,
307 {
308 let expected = "a host:port identifier";
309
310 let mut s = String::deserialize(deserializer)?;
311
312 match s.find(':') {
313 Some(idx) => {
314 let port = s[idx + 1..]
315 .parse()
316 .map_err(|_| D::Error::invalid_value(Unexpected::Str(&s), &expected))?;
317 s.truncate(idx);
318 Ok(HostAndPort { host: s, port })
319 }
320 None => Err(D::Error::invalid_value(Unexpected::Str(&s), &expected)),
321 }
322 }
323}
324
325impl HostAndPort {
326 pub fn new(host: &str, port: u16) -> HostAndPort {
328 HostAndPort {
329 host: host.to_string(),
330 port,
331 }
332 }
333
334 pub fn host(&self) -> &str {
336 &self.host
337 }
338
339 pub fn port(&self) -> u16 {
341 self.port
342 }
343}
344
345#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
347pub struct BasicCredentials {
348 username: String,
349 password: String,
350}
351
352impl BasicCredentials {
353 pub fn new(username: &str, password: &str) -> BasicCredentials {
355 BasicCredentials {
356 username: username.to_string(),
357 password: password.to_string(),
358 }
359 }
360
361 pub fn username(&self) -> &str {
363 &self.username
364 }
365
366 pub fn password(&self) -> &str {
368 &self.password
369 }
370}
371
372fn de_opt_duration<'de, D>(d: D) -> Result<Option<Duration>, D::Error>
373where
374 D: Deserializer<'de>,
375{
376 humantime_serde::Serde::deserialize(d).map(humantime_serde::Serde::into_inner)
377}