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("No mapper found for config type: {0}")]
NoMapperFound(String),
#[error("Mapper type mismatch")]
MapperTypeMismatch,
#[error("Failed to create source: {0}")]
SourceCreationError(String),
#[error("Failed to create reaction: {0}")]
ReactionCreationError(String),
#[error("Invalid value: {0}")]
InvalidValue(String),
}
pub trait ConfigMapper<TDto, TDomain>: Send + Sync {
fn map(&self, dto: &TDto, resolver: &DtoMapper) -> Result<TDomain, MappingError>;
}
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 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)
}
ConfigValue::EnvironmentVariable { .. } => {
let resolver = self.resolvers.get("EnvironmentVariable").ok_or_else(|| {
ResolverError::NoResolverFound("EnvironmentVariable".to_string())
})?;
resolver.resolve_to_string(value)
}
}
}
pub 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)?;
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 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,
{
value.as_ref().map(|v| self.resolve_typed(v)).transpose()
}
pub fn resolve_optional_string(
&self,
value: &Option<ConfigValue<String>>,
) -> Result<Option<String>, ResolverError> {
value.as_ref().map(|v| self.resolve_string(v)).transpose()
}
pub fn resolve_string_vec(
&self,
values: &[ConfigValue<String>],
) -> Result<Vec<String>, ResolverError> {
values.iter().map(|v| self.resolve_string(v)).collect()
}
pub fn map_with<TDto, TDomain>(
&self,
dto: &TDto,
mapper: &impl ConfigMapper<TDto, TDomain>,
) -> Result<TDomain, MappingError> {
mapper.map(dto, self)
}
}
impl Default for DtoMapper {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resolve_string_static() {
let mapper = DtoMapper::new();
let value = ConfigValue::Static("hello".to_string());
let result = mapper.resolve_string(&value).expect("resolve");
assert_eq!(result, "hello");
}
#[test]
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).expect("resolve");
assert_eq!(result, "mapped_value");
std::env::remove_var("TEST_SDK_MAPPER_VAR");
}
#[test]
fn test_resolve_typed_u16() {
let mapper = DtoMapper::new();
let value = ConfigValue::Static(5432u16);
let result = mapper.resolve_typed(&value).expect("resolve");
assert_eq!(result, 5432u16);
}
#[test]
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).expect("resolve");
assert_eq!(result, 8080u16);
std::env::remove_var("TEST_SDK_PORT");
}
#[test]
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);
assert!(result.is_err());
assert!(matches!(
result.expect_err("should fail"),
ResolverError::ParseError(_)
));
std::env::remove_var("TEST_SDK_INVALID_PORT");
}
#[test]
fn test_resolve_optional_some() {
let mapper = DtoMapper::new();
let value = Some(ConfigValue::Static("test".to_string()));
let result = mapper.resolve_optional(&value).expect("resolve");
assert_eq!(result, Some("test".to_string()));
}
#[test]
fn test_resolve_optional_none() {
let mapper = DtoMapper::new();
let value: Option<ConfigValue<String>> = None;
let result = mapper.resolve_optional(&value).expect("resolve");
assert_eq!(result, None);
}
#[test]
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).expect("resolve");
assert_eq!(result, vec!["a", "b"]);
}
#[test]
fn test_config_mapper_trait() {
struct TestMapper;
#[derive(Debug)]
struct TestDto {
host: ConfigValue<String>,
}
struct TestDomain {
host: String,
}
impl ConfigMapper<TestDto, TestDomain> for TestMapper {
fn map(&self, dto: &TestDto, resolver: &DtoMapper) -> Result<TestDomain, MappingError> {
Ok(TestDomain {
host: resolver.resolve_string(&dto.host)?,
})
}
}
let mapper = DtoMapper::new();
let dto = TestDto {
host: ConfigValue::Static("localhost".to_string()),
};
let domain = mapper.map_with(&dto, &TestMapper).expect("map");
assert_eq!(domain.host, "localhost");
}
#[test]
fn test_custom_resolver() {
struct AlwaysResolver;
impl ValueResolver for AlwaysResolver {
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).expect("resolve");
assert_eq!(result, "custom-resolved");
}
}