use secrecy::SecretString;
use std::fmt;
use std::sync::Arc;
use std::time::Duration;
use url::Url;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum WireVersion {
Stu3,
R4,
R4B,
}
impl WireVersion {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Stu3 => "STU3",
Self::R4 => "R4",
Self::R4B => "R4B",
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum Credentials {
Basic {
user: String,
password: SecretString,
},
OAuth(Arc<dyn TokenProvider>),
}
#[derive(Debug, Clone)]
pub struct TokenFailure(Arc<dyn std::error::Error + Send + Sync>);
impl TokenFailure {
#[must_use]
pub fn new(cause: Arc<dyn std::error::Error + Send + Sync>) -> Self {
Self(cause)
}
}
impl fmt::Display for TokenFailure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("the token provider obtained no token")
}
}
impl std::error::Error for TokenFailure {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&*self.0)
}
}
#[async_trait::async_trait]
pub trait TokenProvider: fmt::Debug + Send + Sync {
async fn token(&self) -> Result<SecretString, TokenFailure>;
fn invalidate(&self, stale: &SecretString);
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RetryPolicy {
pub max_attempts: u32,
pub initial_backoff: Duration,
pub max_backoff: Duration,
}
impl Default for RetryPolicy {
fn default() -> Self {
Self {
max_attempts: 3,
initial_backoff: Duration::from_millis(200),
max_backoff: Duration::from_secs(5),
}
}
}
#[derive(Debug, Clone)]
pub struct Config {
pub base_url: Url,
pub wire_version: WireVersion,
pub timeout: Duration,
pub retry: RetryPolicy,
pub credentials: Option<Credentials>,
}
impl Config {
#[must_use]
pub fn new(base_url: Url, wire_version: WireVersion) -> Self {
Self {
base_url,
wire_version,
timeout: Duration::from_secs(30),
retry: RetryPolicy::default(),
credentials: None,
}
}
#[must_use]
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
#[must_use]
pub fn with_retry(mut self, retry: RetryPolicy) -> Self {
self.retry = retry;
self
}
#[must_use]
pub fn with_credentials(mut self, credentials: Credentials) -> Self {
self.credentials = Some(credentials);
self
}
}
#[cfg(test)]
mod tests {
use super::{Config, Credentials, RetryPolicy, WireVersion};
use secrecy::SecretString;
use std::time::Duration;
fn config() -> Config {
Config::new(
"http://tx.invalid/r4".parse().expect("a valid URL"),
WireVersion::R4,
)
}
#[test]
fn a_basic_password_is_not_in_the_debug_rendering() {
let config = config().with_credentials(Credentials::Basic {
user: "bridge".to_owned(),
password: SecretString::from("s3cr3t-password"),
});
let rendered = format!("{config:?}");
assert!(!rendered.contains("s3cr3t-password"));
assert!(rendered.contains("bridge"));
}
#[test]
fn the_defaults_are_three_attempts_and_thirty_seconds() {
let config = config();
assert_eq!(Duration::from_secs(30), config.timeout);
assert_eq!(RetryPolicy::default(), config.retry);
assert_eq!(3, config.retry.max_attempts);
}
#[test]
fn a_wire_version_names_its_release() {
assert_eq!("STU3", WireVersion::Stu3.as_str());
assert_eq!("R4", WireVersion::R4.as_str());
assert_eq!("R4B", WireVersion::R4B.as_str());
}
}