use std::{borrow::Cow, fmt::Display, str::FromStr};
use libdd_telemetry::data::ConfigurationOrigin;
use crate::dd_warn;
use super::supported_configurations::{is_alias_deprecated, SupportedConfigurations};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConfigSourceOrigin {
Default,
EnvVar,
Code,
RemoteConfig,
}
impl From<ConfigSourceOrigin> for ConfigurationOrigin {
fn from(val: ConfigSourceOrigin) -> Self {
match val {
ConfigSourceOrigin::Default => ConfigurationOrigin::Default,
ConfigSourceOrigin::Code => ConfigurationOrigin::Code,
ConfigSourceOrigin::EnvVar => ConfigurationOrigin::EnvVar,
ConfigSourceOrigin::RemoteConfig => ConfigurationOrigin::RemoteConfig,
}
}
}
#[derive(Debug, PartialEq)]
pub(crate) struct ConfigKey<T> {
pub(crate) value: T,
pub(crate) origin: ConfigSourceOrigin,
}
pub(crate) struct CompositeSource {
sources: Vec<Box<dyn ConfigurationSource>>,
}
impl CompositeSource {
pub fn add_source<C: ConfigurationSource + 'static>(&mut self, source: C) {
self.sources.push(Box::new(source));
}
pub fn new() -> Self {
CompositeSource {
sources: Vec::new(),
}
}
pub fn default_sources() -> Self {
let mut sources = Self::new();
sources.add_source(EnvSource);
sources
}
}
#[allow(unused)]
#[derive(Debug, PartialEq)]
pub(crate) struct CompositeParseError {
desired_type: &'static str,
error: Cow<'static, str>,
value: String,
origin: ConfigSourceOrigin,
}
#[derive(Debug, PartialEq)]
pub(crate) struct CompositeConfigSourceResult<T> {
pub name: SupportedConfigurations,
pub value: Option<ConfigKey<T>>,
#[allow(unused)]
pub errors: Vec<CompositeParseError>,
}
impl CompositeSource {
pub fn get(&self, key: SupportedConfigurations) -> CompositeConfigSourceResult<String> {
self.get_parse(key)
}
pub fn get_parse<T: FromStr<Err = impl Display>>(
&self,
name: SupportedConfigurations,
) -> CompositeConfigSourceResult<T> {
let mut errors = Vec::new();
for s in &self.sources {
match s.get(name.as_str()).and_then(|value| {
value
.parse::<T>()
.map_err(|e| ConfigSourceError::FailedParsing {
desired_type: std::any::type_name::<T>(),
error: Cow::Owned(e.to_string()),
value,
})
}) {
Ok(v) => {
if name.is_deprecated() {
dd_warn!("Configuration {} is deprecated and will be removed in the next major release.", name.as_str());
}
return CompositeConfigSourceResult {
name,
value: Some(ConfigKey {
value: v,
origin: s.origin(),
}),
errors,
};
}
Err(ConfigSourceError::Missing) => match s.get_alias_value(name).and_then(|value| {
value
.parse::<T>()
.map_err(|e| ConfigSourceError::FailedParsing {
desired_type: std::any::type_name::<T>(),
error: Cow::Owned(e.to_string()),
value,
})
}) {
Ok(v) => {
return CompositeConfigSourceResult {
name,
value: Some(ConfigKey {
value: v,
origin: s.origin(),
}),
errors,
};
}
Err(ConfigSourceError::Missing) => continue,
Err(ConfigSourceError::FailedParsing {
error,
value,
desired_type,
}) => {
errors.push(CompositeParseError {
desired_type,
error,
value,
origin: s.origin(),
});
}
},
Err(ConfigSourceError::FailedParsing {
error,
value,
desired_type,
}) => {
errors.push(CompositeParseError {
desired_type,
error,
value,
origin: s.origin(),
});
}
}
}
CompositeConfigSourceResult {
name,
value: None,
errors,
}
}
}
pub(crate) enum ConfigSourceError {
Missing,
FailedParsing {
desired_type: &'static str,
error: Cow<'static, str>,
value: String,
},
}
type ConfigSourceResult<T> = Result<T, ConfigSourceError>;
pub(crate) trait ConfigurationSource {
fn origin(&self) -> ConfigSourceOrigin;
fn get(&self, key: &'static str) -> ConfigSourceResult<String>;
fn get_alias_value(&self, key: SupportedConfigurations) -> ConfigSourceResult<String> {
for alias in key.aliases() {
match self.get(alias) {
Ok(value) => {
if is_alias_deprecated(alias) {
dd_warn!("Alias {} is deprecated, please use {} instead. This will be enforced in the next major release.", alias, key.as_str());
}
return Ok(value);
}
Err(ConfigSourceError::Missing) => continue,
Err(e) => return Err(e),
}
}
Err(ConfigSourceError::Missing)
}
}
pub(crate) struct EnvSource;
impl ConfigurationSource for EnvSource {
fn origin(&self) -> ConfigSourceOrigin {
ConfigSourceOrigin::EnvVar
}
fn get(&self, key: &'static str) -> ConfigSourceResult<String> {
#[allow(clippy::disallowed_methods)]
std::env::var(key).map_err(|_| ConfigSourceError::Missing)
}
}
#[allow(unused)]
pub(crate) struct HashMapSource {
map: std::collections::HashMap<String, String>,
origin: ConfigSourceOrigin,
}
impl HashMapSource {
#[allow(unused)]
pub(crate) fn from_iter<U: ToString, V: ToString, T: IntoIterator<Item = (U, V)>>(
map: T,
origin: ConfigSourceOrigin,
) -> Self {
HashMapSource {
map: map
.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
origin,
}
}
}
impl ConfigurationSource for HashMapSource {
fn origin(&self) -> ConfigSourceOrigin {
self.origin
}
fn get(&self, key: &'static str) -> ConfigSourceResult<String> {
self.map.get(key).cloned().ok_or(ConfigSourceError::Missing)
}
}
#[cfg(test)]
mod tests {
use super::{
CompositeConfigSourceResult, CompositeParseError, CompositeSource, ConfigSourceOrigin,
HashMapSource,
};
use crate::core::configuration::sources::ConfigKey;
use crate::core::configuration::supported_configurations::SupportedConfigurations;
#[test]
fn test_composite_source_single_origin() {
let mut source = CompositeSource::new();
source.add_source(HashMapSource::from_iter(
[("DD_SERVICE", "test-service"), ("DD_ENV", "test-env")],
ConfigSourceOrigin::EnvVar,
));
for (key, expected) in [
(
SupportedConfigurations::DD_SERVICE,
CompositeConfigSourceResult {
name: SupportedConfigurations::DD_SERVICE,
value: Some(super::ConfigKey {
value: "test-service".to_string(),
origin: ConfigSourceOrigin::EnvVar,
}),
errors: vec![],
},
),
(
SupportedConfigurations::DD_ENV,
CompositeConfigSourceResult {
name: SupportedConfigurations::DD_ENV,
value: Some(super::ConfigKey {
value: "test-env".to_string(),
origin: ConfigSourceOrigin::EnvVar,
}),
errors: vec![],
},
),
(
SupportedConfigurations::DD_VERSION,
CompositeConfigSourceResult {
name: SupportedConfigurations::DD_VERSION,
value: None,
errors: vec![],
},
),
] {
let result = source.get(key);
assert_eq!(result, expected, "Failed for key: {:?}", key.as_str());
}
}
#[test]
fn test_composite_source_aliases() {
let mut source = CompositeSource::new();
source.add_source(HashMapSource::from_iter(
[("DD_NONEXISTANT_CONFIGURATION_ALIAS", "test-value")],
ConfigSourceOrigin::EnvVar,
));
let key = SupportedConfigurations::DD_NONEXISTANT_CONFIGURATION;
let expected = CompositeConfigSourceResult {
name: SupportedConfigurations::DD_NONEXISTANT_CONFIGURATION,
value: Some(super::ConfigKey {
value: "test-value".to_string(),
origin: ConfigSourceOrigin::EnvVar,
}),
errors: vec![],
};
let result = source.get(key);
assert_eq!(result, expected, "Failed for key: {:?}", key.as_str());
}
#[test]
fn test_composite_priority_order() {
let mut source = CompositeSource::new();
source.add_source(HashMapSource::from_iter(
[("DD_SERVICE", "test-service-env_var")],
ConfigSourceOrigin::EnvVar,
));
source.add_source(HashMapSource::from_iter(
[
("DD_SERVICE", "test-service-default"),
("DD_ENV", "test-env-default"),
],
ConfigSourceOrigin::Default,
));
for (key, expected) in [
(
SupportedConfigurations::DD_SERVICE,
CompositeConfigSourceResult {
name: SupportedConfigurations::DD_SERVICE,
value: Some(super::ConfigKey {
value: "test-service-env_var".to_string(),
origin: ConfigSourceOrigin::EnvVar,
}),
errors: vec![],
},
),
(
SupportedConfigurations::DD_ENV,
CompositeConfigSourceResult {
name: SupportedConfigurations::DD_ENV,
value: Some(super::ConfigKey {
value: "test-env-default".to_string(),
origin: ConfigSourceOrigin::Default,
}),
errors: vec![],
},
),
(
SupportedConfigurations::DD_VERSION,
CompositeConfigSourceResult {
name: SupportedConfigurations::DD_VERSION,
value: None,
errors: vec![],
},
),
] {
let result = source.get(key);
assert_eq!(result, expected, "Failed for key: {:?}", key.as_str());
}
}
#[test]
fn test_composite_parse_error_collection() {
let mut source = CompositeSource::new();
source.add_source(HashMapSource::from_iter(
[("DD_TRACE_ENABLED", "foo")],
ConfigSourceOrigin::Code,
));
source.add_source(HashMapSource::from_iter(
[("DD_TRACE_ENABLED", "true")],
ConfigSourceOrigin::EnvVar,
));
source.add_source(HashMapSource::from_iter(
[("DD_TRACE_ENABLED", "bar")],
ConfigSourceOrigin::Default,
));
let result: CompositeConfigSourceResult<bool> =
source.get_parse(SupportedConfigurations::DD_TRACE_ENABLED);
assert_eq!(
result,
CompositeConfigSourceResult {
name: SupportedConfigurations::DD_TRACE_ENABLED,
value: Some(ConfigKey {
value: true,
origin: ConfigSourceOrigin::EnvVar,
}),
errors: vec![CompositeParseError {
desired_type: "bool",
error: "provided string was not `true` or `false`".into(),
value: "foo".to_string(),
origin: ConfigSourceOrigin::Code,
},],
}
)
}
#[test]
fn test_parse_complex_config() {
#[derive(Debug, serde::Deserialize, PartialEq)]
struct ComplexStruct {
key: String,
}
#[derive(Debug, serde::Deserialize, PartialEq)]
struct ComplexConfig(Vec<ComplexStruct>);
let mut source = CompositeSource::new();
source.add_source(HashMapSource::from_iter(
[(
"DD_COMPLEX_STRUCT",
"[
{\"key\": \"value\"},
{\"key\": \"value2\"}
]",
)],
ConfigSourceOrigin::Code,
));
impl std::str::FromStr for ComplexConfig {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_str(s).map_err(|e| e.to_string())
}
}
let result = source.get_parse(SupportedConfigurations::DD_COMPLEX_STRUCT);
assert_eq!(
result,
CompositeConfigSourceResult {
name: SupportedConfigurations::DD_COMPLEX_STRUCT,
value: Some(ConfigKey {
value: ComplexConfig(vec![
ComplexStruct {
key: "value".to_string()
},
ComplexStruct {
key: "value2".to_string()
}
]),
origin: ConfigSourceOrigin::Code,
}),
errors: vec![],
}
);
}
}