use std::time::Duration;
mod def;
mod error;
mod resolved;
mod secret;
pub use def::{ConfigDef, ConfigKey, ConfigKind, RawConfig};
pub use error::{ConfigError, ConfigResult};
pub use resolved::ResolvedConfig;
pub use secret::{
EnvSecretResolver, ResolveOptions, SecretRef, SecretResolutionError, SecretResolver,
SecretString,
};
pub trait ConnectorConfig: Sized {
fn config_def() -> ConfigDef;
fn from_resolved(config: &ResolvedConfig) -> ConfigResult<Self>;
}
pub trait FromResolvedValue: Sized {
const KIND: ConfigKind;
fn from_resolved_value(config: &ResolvedConfig, key: &str) -> ConfigResult<Self>;
}
impl FromResolvedValue for String {
const KIND: ConfigKind = ConfigKind::String;
fn from_resolved_value(config: &ResolvedConfig, key: &str) -> ConfigResult<Self> {
config.get_string(key)
}
}
impl FromResolvedValue for bool {
const KIND: ConfigKind = ConfigKind::Bool;
fn from_resolved_value(config: &ResolvedConfig, key: &str) -> ConfigResult<Self> {
config.get_bool(key)
}
}
impl FromResolvedValue for i64 {
const KIND: ConfigKind = ConfigKind::Integer;
fn from_resolved_value(config: &ResolvedConfig, key: &str) -> ConfigResult<Self> {
config.get_i64(key)
}
}
impl FromResolvedValue for u64 {
const KIND: ConfigKind = ConfigKind::UnsignedInteger;
fn from_resolved_value(config: &ResolvedConfig, key: &str) -> ConfigResult<Self> {
config.get_u64(key)
}
}
impl FromResolvedValue for f64 {
const KIND: ConfigKind = ConfigKind::Float;
fn from_resolved_value(config: &ResolvedConfig, key: &str) -> ConfigResult<Self> {
config.get_f64(key)
}
}
impl FromResolvedValue for Vec<String> {
const KIND: ConfigKind = ConfigKind::StringList;
fn from_resolved_value(config: &ResolvedConfig, key: &str) -> ConfigResult<Self> {
config.get_string_list(key)
}
}
impl FromResolvedValue for serde_json::Value {
const KIND: ConfigKind = ConfigKind::Json;
fn from_resolved_value(config: &ResolvedConfig, key: &str) -> ConfigResult<Self> {
config.get_json(key)
}
}
impl FromResolvedValue for SecretString {
const KIND: ConfigKind = ConfigKind::Secret;
fn from_resolved_value(config: &ResolvedConfig, key: &str) -> ConfigResult<Self> {
config.get_secret(key)
}
}
macro_rules! impl_signed_integer_config {
($($ty:ty),* $(,)?) => {
$(
impl FromResolvedValue for $ty {
const KIND: ConfigKind = ConfigKind::Integer;
fn from_resolved_value(config: &ResolvedConfig, key: &str) -> ConfigResult<Self> {
let value = config.get_i64(key)?;
<$ty>::try_from(value).map_err(|_| ConfigError::WrongType {
key: key.into(),
expected: concat!("integer in range for ", stringify!($ty)),
})
}
}
)*
};
}
macro_rules! impl_unsigned_integer_config {
($($ty:ty),* $(,)?) => {
$(
impl FromResolvedValue for $ty {
const KIND: ConfigKind = ConfigKind::UnsignedInteger;
fn from_resolved_value(config: &ResolvedConfig, key: &str) -> ConfigResult<Self> {
let value = config.get_u64(key)?;
<$ty>::try_from(value).map_err(|_| ConfigError::WrongType {
key: key.into(),
expected: concat!("unsigned integer in range for ", stringify!($ty)),
})
}
}
)*
};
}
impl_signed_integer_config!(i8, i16, i32, isize);
impl_unsigned_integer_config!(u8, u16, u32, usize);
impl FromResolvedValue for f32 {
const KIND: ConfigKind = ConfigKind::Float;
fn from_resolved_value(config: &ResolvedConfig, key: &str) -> ConfigResult<Self> {
let value = config.get_f64(key)?;
if !value.is_finite() || value < f64::from(f32::MIN) || value > f64::from(f32::MAX) {
return Err(ConfigError::WrongType {
key: key.into(),
expected: "float in range for f32",
});
}
#[allow(clippy::cast_possible_truncation)]
Ok(value as f32)
}
}
impl<T: FromResolvedValue> FromResolvedValue for Option<T> {
const KIND: ConfigKind = T::KIND;
fn from_resolved_value(config: &ResolvedConfig, key: &str) -> ConfigResult<Self> {
if config.contains_key(key) {
T::from_resolved_value(config, key).map(Some)
} else {
Ok(None)
}
}
}
impl FromResolvedValue for Duration {
const KIND: ConfigKind = ConfigKind::DurationMillis;
fn from_resolved_value(config: &ResolvedConfig, key: &str) -> ConfigResult<Self> {
let value = config.get_i64(key)?;
let millis = u64::try_from(value).map_err(|_| ConfigError::WrongType {
key: key.into(),
expected: "non-negative duration milliseconds",
})?;
Ok(Self::from_millis(millis))
}
}
#[cfg(test)]
mod tests {
use serde_json::{Number, Value};
use super::*;
fn resolved_float(value: f64) -> ResolvedConfig {
let mut config = ResolvedConfig::default();
config.insert_plain(
"ratio",
Value::Number(Number::from_f64(value).expect("finite test float")),
);
config
}
#[test]
fn f32_extraction_accepts_inclusive_boundaries() {
let min = resolved_float(f64::from(f32::MIN));
let max = resolved_float(f64::from(f32::MAX));
assert_eq!(
f32::from_resolved_value(&min, "ratio").unwrap().to_bits(),
f32::MIN.to_bits()
);
assert_eq!(
f32::from_resolved_value(&max, "ratio").unwrap().to_bits(),
f32::MAX.to_bits()
);
}
#[test]
fn f32_extraction_rejects_values_beyond_boundaries() {
let below_min = resolved_float(f64::from(f32::MIN) * 2.0);
let above_max = resolved_float(f64::from(f32::MAX) * 2.0);
for config in [&below_min, &above_max] {
assert!(matches!(
f32::from_resolved_value(config, "ratio"),
Err(ConfigError::WrongType {
key,
expected: "float in range for f32",
}) if key == "ratio"
));
}
}
}