#![warn(missing_docs, clippy::all)]
#![allow(clippy::derive_partial_eq_without_eq)]
use serde::de::{Deserializer, Error as _, Unexpected};
use serde::Deserialize;
use staged_builder::staged_builder;
use std::collections::HashMap;
use std::fmt;
use std::path::{Path, PathBuf};
use std::time::Duration;
use url::Url;
#[cfg(test)]
mod test;
#[derive(Debug, Clone, PartialEq, Default, Deserialize)]
#[serde(rename_all = "kebab-case", default)]
#[staged_builder]
#[builder(update)]
pub struct ServicesConfig {
#[builder(map(key(type = String, into), value(type = ServiceConfig)))]
services: HashMap<String, ServiceConfig>,
#[builder(default, into)]
security: Option<SecurityConfig>,
#[builder(default, into)]
proxy: Option<ProxyConfig>,
#[builder(default, into)]
#[serde(deserialize_with = "de_opt_duration")]
connect_timeout: Option<Duration>,
#[builder(default, into)]
#[serde(deserialize_with = "de_opt_duration")]
read_timeout: Option<Duration>,
#[builder(default, into)]
#[serde(deserialize_with = "de_opt_duration")]
write_timeout: Option<Duration>,
#[builder(default, into)]
#[serde(deserialize_with = "de_opt_duration")]
backoff_slot_size: Option<Duration>,
}
impl ServicesConfig {
pub fn merged_service(&self, name: &str) -> Option<ServiceConfig> {
let mut service = self.services.get(name).cloned()?;
if service.security.is_none() {
service.security = self.security.clone();
}
if service.proxy.is_none() {
service.proxy = self.proxy.clone();
}
if service.connect_timeout.is_none() {
service.connect_timeout = self.connect_timeout;
}
if service.read_timeout.is_none() {
service.read_timeout = self.read_timeout;
}
if service.write_timeout.is_none() {
service.write_timeout = self.write_timeout;
}
if service.backoff_slot_size.is_none() {
service.backoff_slot_size = self.backoff_slot_size;
}
Some(service)
}
pub fn security(&self) -> Option<&SecurityConfig> {
self.security.as_ref()
}
pub fn proxy(&self) -> Option<&ProxyConfig> {
self.proxy.as_ref()
}
pub fn connect_timeout(&self) -> Option<Duration> {
self.connect_timeout
}
pub fn read_timeout(&self) -> Option<Duration> {
self.read_timeout
}
pub fn write_timeout(&self) -> Option<Duration> {
self.write_timeout
}
pub fn backoff_slot_size(&self) -> Option<Duration> {
self.backoff_slot_size
}
}
#[derive(Debug, Default, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "kebab-case", default)]
#[staged_builder]
#[builder(update)]
pub struct ServiceConfig {
#[builder(list(item(type = Url)))]
uris: Vec<Url>,
#[builder(default, into)]
security: Option<SecurityConfig>,
#[builder(default, into)]
proxy: Option<ProxyConfig>,
#[builder(default, into)]
#[serde(deserialize_with = "de_opt_duration")]
connect_timeout: Option<Duration>,
#[builder(default, into)]
#[serde(deserialize_with = "de_opt_duration")]
read_timeout: Option<Duration>,
#[builder(default, into)]
#[serde(deserialize_with = "de_opt_duration")]
write_timeout: Option<Duration>,
#[builder(default, into)]
#[serde(deserialize_with = "de_opt_duration")]
backoff_slot_size: Option<Duration>,
#[builder(default, into)]
max_num_retries: Option<u32>,
}
impl ServiceConfig {
pub fn uris(&self) -> &[Url] {
&self.uris
}
pub fn security(&self) -> Option<&SecurityConfig> {
self.security.as_ref()
}
pub fn proxy(&self) -> Option<&ProxyConfig> {
self.proxy.as_ref()
}
pub fn connect_timeout(&self) -> Option<Duration> {
self.connect_timeout
}
pub fn read_timeout(&self) -> Option<Duration> {
self.read_timeout
}
pub fn write_timeout(&self) -> Option<Duration> {
self.write_timeout
}
pub fn backoff_slot_size(&self) -> Option<Duration> {
self.backoff_slot_size
}
pub fn max_num_retries(&self) -> Option<u32> {
self.max_num_retries
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[staged_builder]
#[builder(update)]
pub struct SecurityConfig {
#[builder(default, into)]
ca_file: Option<PathBuf>,
#[builder(default, into)]
key_file: Option<PathBuf>,
#[builder(default, into)]
cert_file: Option<PathBuf>,
}
impl SecurityConfig {
pub fn ca_file(&self) -> Option<&Path> {
self.ca_file.as_deref()
}
pub fn key_file(&self) -> Option<&Path> {
self.key_file.as_deref()
}
pub fn cert_file(&self) -> Option<&Path> {
self.cert_file.as_deref()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
#[serde(rename_all = "kebab-case", tag = "type")]
#[non_exhaustive]
pub enum ProxyConfig {
Direct,
Http(HttpProxyConfig),
}
#[allow(clippy::derivable_impls)]
impl Default for ProxyConfig {
fn default() -> ProxyConfig {
ProxyConfig::Direct
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[staged_builder]
#[builder(update)]
pub struct HttpProxyConfig {
host_and_port: HostAndPort,
#[builder(default, into)]
credentials: Option<BasicCredentials>,
}
impl HttpProxyConfig {
pub fn host_and_port(&self) -> &HostAndPort {
&self.host_and_port
}
pub fn credentials(&self) -> Option<&BasicCredentials> {
self.credentials.as_ref()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct HostAndPort {
host: String,
port: u16,
}
impl fmt::Display for HostAndPort {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(fmt, "{}:{}", self.host, self.port)
}
}
impl<'de> Deserialize<'de> for HostAndPort {
fn deserialize<D>(deserializer: D) -> Result<HostAndPort, D::Error>
where
D: Deserializer<'de>,
{
let expected = "a host:port identifier";
let mut s = String::deserialize(deserializer)?;
match s.find(':') {
Some(idx) => {
let port = s[idx + 1..]
.parse()
.map_err(|_| D::Error::invalid_value(Unexpected::Str(&s), &expected))?;
s.truncate(idx);
Ok(HostAndPort { host: s, port })
}
None => Err(D::Error::invalid_value(Unexpected::Str(&s), &expected)),
}
}
}
impl HostAndPort {
pub fn new(host: &str, port: u16) -> HostAndPort {
HostAndPort {
host: host.to_string(),
port,
}
}
pub fn host(&self) -> &str {
&self.host
}
pub fn port(&self) -> u16 {
self.port
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
pub struct BasicCredentials {
username: String,
password: String,
}
impl BasicCredentials {
pub fn new(username: &str, password: &str) -> BasicCredentials {
BasicCredentials {
username: username.to_string(),
password: password.to_string(),
}
}
pub fn username(&self) -> &str {
&self.username
}
pub fn password(&self) -> &str {
&self.password
}
}
fn de_opt_duration<'de, D>(d: D) -> Result<Option<Duration>, D::Error>
where
D: Deserializer<'de>,
{
humantime_serde::Serde::deserialize(d).map(humantime_serde::Serde::into_inner)
}