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 #[serde(default)]
224 #[builder(list(item(type = String, into)))]
225 pinned_certs: Vec<String>,
226}
227
228impl SecurityConfig {
229 pub fn ca_file(&self) -> Option<&Path> {
233 self.ca_file.as_deref()
234 }
235
236 pub fn key_file(&self) -> Option<&Path> {
240 self.key_file.as_deref()
241 }
242
243 pub fn cert_file(&self) -> Option<&Path> {
248 self.cert_file.as_deref()
249 }
250
251 pub fn pinned_certs(&self) -> &[String] {
264 &self.pinned_certs
265 }
266}
267
268#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
270#[serde(rename_all = "kebab-case", tag = "type")]
271#[non_exhaustive]
272pub enum ProxyConfig {
273 Direct,
275 Http(HttpProxyConfig),
277}
278
279#[allow(clippy::derivable_impls)]
280impl Default for ProxyConfig {
281 fn default() -> ProxyConfig {
282 ProxyConfig::Direct
283 }
284}
285
286#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
288#[serde(rename_all = "kebab-case")]
289#[staged_builder]
290#[builder(update)]
291pub struct HttpProxyConfig {
292 host_and_port: HostAndPort,
293 #[builder(default, into)]
294 credentials: Option<BasicCredentials>,
295}
296
297impl HttpProxyConfig {
298 pub fn host_and_port(&self) -> &HostAndPort {
300 &self.host_and_port
301 }
302
303 pub fn credentials(&self) -> Option<&BasicCredentials> {
305 self.credentials.as_ref()
306 }
307}
308
309#[derive(Debug, Clone, PartialEq, Eq, Hash)]
311pub struct HostAndPort {
312 host: String,
313 port: u16,
314}
315
316impl fmt::Display for HostAndPort {
317 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
318 write!(fmt, "{}:{}", self.host, self.port)
319 }
320}
321
322impl<'de> Deserialize<'de> for HostAndPort {
323 fn deserialize<D>(deserializer: D) -> Result<HostAndPort, D::Error>
324 where
325 D: Deserializer<'de>,
326 {
327 let expected = "a host:port identifier";
328
329 let mut s = String::deserialize(deserializer)?;
330
331 match s.find(':') {
332 Some(idx) => {
333 let port = s[idx + 1..]
334 .parse()
335 .map_err(|_| D::Error::invalid_value(Unexpected::Str(&s), &expected))?;
336 s.truncate(idx);
337 Ok(HostAndPort { host: s, port })
338 }
339 None => Err(D::Error::invalid_value(Unexpected::Str(&s), &expected)),
340 }
341 }
342}
343
344impl HostAndPort {
345 pub fn new(host: &str, port: u16) -> HostAndPort {
347 HostAndPort {
348 host: host.to_string(),
349 port,
350 }
351 }
352
353 pub fn host(&self) -> &str {
355 &self.host
356 }
357
358 pub fn port(&self) -> u16 {
360 self.port
361 }
362}
363
364#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
366pub struct BasicCredentials {
367 username: String,
368 password: String,
369}
370
371impl BasicCredentials {
372 pub fn new(username: &str, password: &str) -> BasicCredentials {
374 BasicCredentials {
375 username: username.to_string(),
376 password: password.to_string(),
377 }
378 }
379
380 pub fn username(&self) -> &str {
382 &self.username
383 }
384
385 pub fn password(&self) -> &str {
387 &self.password
388 }
389}
390
391fn de_opt_duration<'de, D>(d: D) -> Result<Option<Duration>, D::Error>
392where
393 D: Deserializer<'de>,
394{
395 humantime_serde::Serde::deserialize(d).map(humantime_serde::Serde::into_inner)
396}