use crate::provider_config::ProviderConfig;
use crate::retry::error::{RetryConfigError, RetryConfigErrorKind};
use aws_runtime::env_config::{EnvConfigError, EnvConfigValue};
use aws_smithy_types::error::display::DisplayErrorContext;
use aws_smithy_types::retry::{RetryConfig, RetryMode};
use std::str::FromStr;
pub fn default_provider() -> Builder {
Builder::default()
}
mod env {
pub(super) const MAX_ATTEMPTS: &str = "AWS_MAX_ATTEMPTS";
pub(super) const RETRY_MODE: &str = "AWS_RETRY_MODE";
}
mod profile_keys {
pub(super) const MAX_ATTEMPTS: &str = "max_attempts";
pub(super) const RETRY_MODE: &str = "retry_mode";
}
#[derive(Debug, Default)]
pub struct Builder {
provider_config: ProviderConfig,
}
impl Builder {
pub fn configure(mut self, configuration: &ProviderConfig) -> Self {
self.provider_config = configuration.clone();
self
}
pub fn profile_name(mut self, name: &str) -> Self {
self.provider_config = self.provider_config.with_profile_name(name.to_string());
self
}
pub async fn retry_config(self) -> RetryConfig {
match self.try_retry_config().await {
Ok(conf) => conf,
Err(e) => panic!("{}", DisplayErrorContext(e)),
}
}
pub(crate) async fn try_retry_config(
self,
) -> Result<RetryConfig, EnvConfigError<RetryConfigError>> {
let env = self.provider_config.env();
let profiles = self.provider_config.profile().await;
let mut retry_config = RetryConfig::standard();
let max_attempts = EnvConfigValue::new()
.env(env::MAX_ATTEMPTS)
.profile(profile_keys::MAX_ATTEMPTS)
.validate(&env, profiles, validate_max_attempts);
let retry_mode = EnvConfigValue::new()
.env(env::RETRY_MODE)
.profile(profile_keys::RETRY_MODE)
.validate(&env, profiles, |s| {
RetryMode::from_str(s)
.map_err(|err| RetryConfigErrorKind::InvalidRetryMode { source: err }.into())
});
if let Some(max_attempts) = max_attempts? {
retry_config = retry_config.with_max_attempts(max_attempts);
}
if let Some(retry_mode) = retry_mode? {
retry_config = retry_config.with_retry_mode(retry_mode);
}
Ok(retry_config)
}
}
fn validate_max_attempts(max_attempts: &str) -> Result<u32, RetryConfigError> {
match max_attempts.parse::<u32>() {
Ok(0) => Err(RetryConfigErrorKind::MaxAttemptsMustNotBeZero.into()),
Ok(max_attempts) => Ok(max_attempts),
Err(source) => Err(RetryConfigErrorKind::FailedToParseMaxAttempts { source }.into()),
}
}
#[cfg(test)]
mod test {
use crate::default_provider::retry_config::env;
use crate::provider_config::ProviderConfig;
use crate::retry::{
error::RetryConfigError, error::RetryConfigErrorKind, RetryConfig, RetryMode,
};
use aws_runtime::env_config::EnvConfigError;
use aws_types::os_shim_internal::{Env, Fs};
async fn test_provider(
vars: &[(&str, &str)],
) -> Result<RetryConfig, EnvConfigError<RetryConfigError>> {
super::Builder::default()
.configure(&ProviderConfig::no_configuration().with_env(Env::from_slice(vars)))
.try_retry_config()
.await
}
#[tokio::test]
async fn test_returns_default_retry_config_from_empty_profile() {
let env = Env::from_slice(&[("AWS_CONFIG_FILE", "config")]);
let fs = Fs::from_slice(&[("config", "[default]\n")]);
let provider_config = ProviderConfig::no_configuration().with_env(env).with_fs(fs);
let actual_retry_config = super::default_provider()
.configure(&provider_config)
.retry_config()
.await;
let expected_retry_config = RetryConfig::standard();
assert_eq!(actual_retry_config, expected_retry_config);
assert_eq!(actual_retry_config.max_attempts(), 3);
assert_eq!(actual_retry_config.mode(), RetryMode::Standard);
}
#[tokio::test]
async fn test_no_retry_config_in_empty_profile() {
let env = Env::from_slice(&[("AWS_CONFIG_FILE", "config")]);
let fs = Fs::from_slice(&[("config", "[default]\n")]);
let provider_config = ProviderConfig::no_configuration().with_env(env).with_fs(fs);
let actual_retry_config = super::default_provider()
.configure(&provider_config)
.retry_config()
.await;
let expected_retry_config = RetryConfig::standard();
assert_eq!(actual_retry_config, expected_retry_config)
}
#[tokio::test]
async fn test_creation_of_retry_config_from_profile() {
let env = Env::from_slice(&[("AWS_CONFIG_FILE", "config")]);
let fs = Fs::from_slice(&[(
"config",
r#"[default]
max_attempts = 1
retry_mode = standard
"#,
)]);
let provider_config = ProviderConfig::no_configuration().with_env(env).with_fs(fs);
let actual_retry_config = super::default_provider()
.configure(&provider_config)
.retry_config()
.await;
let expected_retry_config = RetryConfig::standard().with_max_attempts(1);
assert_eq!(actual_retry_config, expected_retry_config)
}
#[tokio::test]
async fn test_env_retry_config_takes_precedence_over_profile_retry_config() {
let env = Env::from_slice(&[
("AWS_CONFIG_FILE", "config"),
("AWS_MAX_ATTEMPTS", "42"),
("AWS_RETRY_MODE", "standard"),
]);
let fs = Fs::from_slice(&[(
"config",
r#"[default]
max_attempts = 88
retry_mode = standard
"#,
)]);
let provider_config = ProviderConfig::no_configuration().with_env(env).with_fs(fs);
let actual_retry_config = super::default_provider()
.configure(&provider_config)
.retry_config()
.await;
let expected_retry_config = RetryConfig::standard().with_max_attempts(42);
assert_eq!(actual_retry_config, expected_retry_config)
}
#[tokio::test]
#[should_panic = "failed to parse max attempts. source: global profile (`default`) key: `max_attempts`: invalid digit found in string"]
async fn test_invalid_profile_retry_config_panics() {
let env = Env::from_slice(&[("AWS_CONFIG_FILE", "config")]);
let fs = Fs::from_slice(&[(
"config",
r#"[default]
max_attempts = potato
"#,
)]);
let provider_config = ProviderConfig::no_configuration().with_env(env).with_fs(fs);
let _ = super::default_provider()
.configure(&provider_config)
.retry_config()
.await;
}
#[tokio::test]
async fn defaults() {
let built = test_provider(&[]).await.unwrap();
assert_eq!(built.mode(), RetryMode::Standard);
assert_eq!(built.max_attempts(), 3);
}
#[tokio::test]
async fn max_attempts_is_read_correctly() {
assert_eq!(
test_provider(&[(env::MAX_ATTEMPTS, "88")]).await.unwrap(),
RetryConfig::standard().with_max_attempts(88)
);
}
#[tokio::test]
async fn max_attempts_errors_when_it_cant_be_parsed_as_an_integer() {
assert!(matches!(
test_provider(&[(env::MAX_ATTEMPTS, "not an integer")])
.await
.unwrap_err()
.err(),
RetryConfigError {
kind: RetryConfigErrorKind::FailedToParseMaxAttempts { .. }
}
));
}
#[tokio::test]
async fn retry_mode_is_read_correctly() {
assert_eq!(
test_provider(&[(env::RETRY_MODE, "standard")])
.await
.unwrap(),
RetryConfig::standard()
);
}
#[tokio::test]
async fn both_fields_can_be_set_at_once() {
assert_eq!(
test_provider(&[(env::RETRY_MODE, "standard"), (env::MAX_ATTEMPTS, "13")])
.await
.unwrap(),
RetryConfig::standard().with_max_attempts(13)
);
}
#[tokio::test]
async fn disallow_zero_max_attempts() {
let err = test_provider(&[(env::MAX_ATTEMPTS, "0")])
.await
.unwrap_err();
let err = err.err();
assert!(matches!(
err,
RetryConfigError {
kind: RetryConfigErrorKind::MaxAttemptsMustNotBeZero { .. }
}
));
}
}