use crate::config_value::ConfigValue;
use crate::resolver::{
get_secret_resolver, EnvironmentVariableResolver, ResolverError, SecretResolver, ValueResolver,
};
use std::collections::HashMap;
use std::str::FromStr;
use std::sync::Arc;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum MappingError {
#[error("Failed to resolve config value: {0}")]
ResolutionError(#[from] ResolverError),
#[error("Failed to create source: {0}")]
SourceCreationError(String),
#[error("Failed to create reaction: {0}")]
ReactionCreationError(String),
#[error("Invalid value: {0}")]
InvalidValue(String),
}
pub struct DtoMapper {
resolvers: HashMap<&'static str, Arc<dyn ValueResolver>>,
}
impl DtoMapper {
pub fn new() -> Self {
let mut resolvers: HashMap<&'static str, Arc<dyn ValueResolver>> = HashMap::new();
resolvers.insert("EnvironmentVariable", Arc::new(EnvironmentVariableResolver));
let secret_resolver = get_secret_resolver().unwrap_or_else(|| Arc::new(SecretResolver));
resolvers.insert("Secret", secret_resolver);
Self { resolvers }
}
pub fn with_resolver(mut self, kind: &'static str, resolver: Arc<dyn ValueResolver>) -> Self {
self.resolvers.insert(kind, resolver);
self
}
pub async fn resolve_string(
&self,
value: &ConfigValue<String>,
) -> Result<String, ResolverError> {
match value {
ConfigValue::Static(s) => Ok(s.clone()),
ConfigValue::Secret { .. } => {
let resolver = self
.resolvers
.get("Secret")
.ok_or_else(|| ResolverError::NoResolverFound("Secret".to_string()))?;
resolver.resolve_to_string(value).await
}
ConfigValue::EnvironmentVariable { .. } => {
let resolver = self.resolvers.get("EnvironmentVariable").ok_or_else(|| {
ResolverError::NoResolverFound("EnvironmentVariable".to_string())
})?;
resolver.resolve_to_string(value).await
}
}
}
pub async fn resolve_typed<T>(&self, value: &ConfigValue<T>) -> Result<T, ResolverError>
where
T: FromStr + Clone + serde::Serialize + serde::de::DeserializeOwned,
T::Err: std::fmt::Display,
{
match value {
ConfigValue::Static(v) => Ok(v.clone()),
ConfigValue::Secret { name } => {
let resolver = self
.resolvers
.get("Secret")
.ok_or_else(|| ResolverError::NoResolverFound("Secret".to_string()))?;
let string_cv = ConfigValue::Secret { name: name.clone() };
let string_val = resolver.resolve_to_string(&string_cv).await?;
string_val.parse::<T>().map_err(|e| {
ResolverError::ParseError(format!("Failed to parse secret '{name}': {e}"))
})
}
ConfigValue::EnvironmentVariable { name, default } => {
let string_val = std::env::var(name).or_else(|_| {
default
.clone()
.ok_or_else(|| ResolverError::EnvVarNotFound(name.clone()))
})?;
string_val.parse::<T>().map_err(|e| {
ResolverError::ParseError(format!("Failed to parse env var '{name}': {e}"))
})
}
}
}
pub async fn resolve_optional<T>(
&self,
value: &Option<ConfigValue<T>>,
) -> Result<Option<T>, ResolverError>
where
T: FromStr + Clone + serde::Serialize + serde::de::DeserializeOwned,
T::Err: std::fmt::Display,
{
match value {
Some(v) => self.resolve_typed(v).await.map(Some),
None => Ok(None),
}
}
pub async fn resolve_optional_string(
&self,
value: &Option<ConfigValue<String>>,
) -> Result<Option<String>, ResolverError> {
match value {
Some(v) => self.resolve_string(v).await.map(Some),
None => Ok(None),
}
}
pub async fn resolve_string_vec(
&self,
values: &[ConfigValue<String>],
) -> Result<Vec<String>, ResolverError> {
let mut result = Vec::with_capacity(values.len());
for v in values {
result.push(self.resolve_string(v).await?);
}
Ok(result)
}
}
impl Default for DtoMapper {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_resolve_string_static() {
let mapper = DtoMapper::new();
let value = ConfigValue::Static("hello".to_string());
let result = mapper.resolve_string(&value).await.expect("resolve");
assert_eq!(result, "hello");
}
#[tokio::test]
async fn test_resolve_string_env_var() {
std::env::set_var("TEST_SDK_MAPPER_VAR", "mapped_value");
let mapper = DtoMapper::new();
let value = ConfigValue::EnvironmentVariable {
name: "TEST_SDK_MAPPER_VAR".to_string(),
default: None,
};
let result = mapper.resolve_string(&value).await.expect("resolve");
assert_eq!(result, "mapped_value");
std::env::remove_var("TEST_SDK_MAPPER_VAR");
}
#[tokio::test]
async fn test_resolve_typed_u16() {
let mapper = DtoMapper::new();
let value = ConfigValue::Static(5432u16);
let result = mapper.resolve_typed(&value).await.expect("resolve");
assert_eq!(result, 5432u16);
}
#[tokio::test]
async fn test_resolve_typed_u16_from_env() {
std::env::set_var("TEST_SDK_PORT", "8080");
let mapper = DtoMapper::new();
let value: ConfigValue<u16> = ConfigValue::EnvironmentVariable {
name: "TEST_SDK_PORT".to_string(),
default: None,
};
let result = mapper.resolve_typed(&value).await.expect("resolve");
assert_eq!(result, 8080u16);
std::env::remove_var("TEST_SDK_PORT");
}
#[tokio::test]
async fn test_resolve_typed_parse_error() {
std::env::set_var("TEST_SDK_INVALID_PORT", "not_a_number");
let mapper = DtoMapper::new();
let value: ConfigValue<u16> = ConfigValue::EnvironmentVariable {
name: "TEST_SDK_INVALID_PORT".to_string(),
default: None,
};
let result = mapper.resolve_typed(&value).await;
assert!(result.is_err());
assert!(matches!(
result.expect_err("should fail"),
ResolverError::ParseError(_)
));
std::env::remove_var("TEST_SDK_INVALID_PORT");
}
#[tokio::test]
async fn test_resolve_optional_some() {
let mapper = DtoMapper::new();
let value = Some(ConfigValue::Static("test".to_string()));
let result = mapper.resolve_optional(&value).await.expect("resolve");
assert_eq!(result, Some("test".to_string()));
}
#[tokio::test]
async fn test_resolve_optional_none() {
let mapper = DtoMapper::new();
let value: Option<ConfigValue<String>> = None;
let result = mapper.resolve_optional(&value).await.expect("resolve");
assert_eq!(result, None);
}
#[tokio::test]
async fn test_resolve_string_vec() {
let mapper = DtoMapper::new();
let values = vec![
ConfigValue::Static("a".to_string()),
ConfigValue::Static("b".to_string()),
];
let result = mapper.resolve_string_vec(&values).await.expect("resolve");
assert_eq!(result, vec!["a", "b"]);
}
#[tokio::test]
async fn test_custom_resolver() {
struct AlwaysResolver;
#[async_trait::async_trait]
impl ValueResolver for AlwaysResolver {
async fn resolve_to_string(
&self,
_value: &ConfigValue<String>,
) -> Result<String, ResolverError> {
Ok("custom-resolved".to_string())
}
}
let mapper = DtoMapper::new().with_resolver("Secret", Arc::new(AlwaysResolver));
let value = ConfigValue::Secret {
name: "test".to_string(),
};
let result = mapper.resolve_string(&value).await.expect("resolve");
assert_eq!(result, "custom-resolved");
}
}