1use crate::config_value::ConfigValue;
76use crate::resolver::{
77 get_secret_resolver, EnvironmentVariableResolver, ResolverError, SecretResolver, ValueResolver,
78};
79use std::collections::HashMap;
80use std::str::FromStr;
81use std::sync::Arc;
82use thiserror::Error;
83
84#[derive(Debug, Error)]
86pub enum MappingError {
87 #[error("Failed to resolve config value: {0}")]
89 ResolutionError(#[from] ResolverError),
90
91 #[error("No mapper found for config type: {0}")]
93 NoMapperFound(String),
94
95 #[error("Mapper type mismatch")]
97 MapperTypeMismatch,
98
99 #[error("Failed to create source: {0}")]
101 SourceCreationError(String),
102
103 #[error("Failed to create reaction: {0}")]
105 ReactionCreationError(String),
106
107 #[error("Invalid value: {0}")]
109 InvalidValue(String),
110}
111
112pub trait ConfigMapper<TDto, TDomain>: Send + Sync {
123 fn map(&self, dto: &TDto, resolver: &DtoMapper) -> Result<TDomain, MappingError>;
125}
126
127pub struct DtoMapper {
137 resolvers: HashMap<&'static str, Arc<dyn ValueResolver>>,
138}
139
140impl DtoMapper {
141 pub fn new() -> Self {
148 let mut resolvers: HashMap<&'static str, Arc<dyn ValueResolver>> = HashMap::new();
149 resolvers.insert("EnvironmentVariable", Arc::new(EnvironmentVariableResolver));
150
151 let secret_resolver = get_secret_resolver().unwrap_or_else(|| Arc::new(SecretResolver));
152 resolvers.insert("Secret", secret_resolver);
153
154 Self { resolvers }
155 }
156
157 pub fn with_resolver(mut self, kind: &'static str, resolver: Arc<dyn ValueResolver>) -> Self {
161 self.resolvers.insert(kind, resolver);
162 self
163 }
164
165 pub fn resolve_string(&self, value: &ConfigValue<String>) -> Result<String, ResolverError> {
167 match value {
168 ConfigValue::Static(s) => Ok(s.clone()),
169
170 ConfigValue::Secret { .. } => {
171 let resolver = self
172 .resolvers
173 .get("Secret")
174 .ok_or_else(|| ResolverError::NoResolverFound("Secret".to_string()))?;
175 resolver.resolve_to_string(value)
176 }
177
178 ConfigValue::EnvironmentVariable { .. } => {
179 let resolver = self.resolvers.get("EnvironmentVariable").ok_or_else(|| {
180 ResolverError::NoResolverFound("EnvironmentVariable".to_string())
181 })?;
182 resolver.resolve_to_string(value)
183 }
184 }
185 }
186
187 pub fn resolve_typed<T>(&self, value: &ConfigValue<T>) -> Result<T, ResolverError>
192 where
193 T: FromStr + Clone + serde::Serialize + serde::de::DeserializeOwned,
194 T::Err: std::fmt::Display,
195 {
196 match value {
197 ConfigValue::Static(v) => Ok(v.clone()),
198
199 ConfigValue::Secret { name } => {
200 let resolver = self
201 .resolvers
202 .get("Secret")
203 .ok_or_else(|| ResolverError::NoResolverFound("Secret".to_string()))?;
204 let string_cv = ConfigValue::Secret { name: name.clone() };
205 let string_val = resolver.resolve_to_string(&string_cv)?;
206 string_val.parse::<T>().map_err(|e| {
207 ResolverError::ParseError(format!("Failed to parse secret '{name}': {e}"))
208 })
209 }
210
211 ConfigValue::EnvironmentVariable { name, default } => {
212 let string_val = std::env::var(name).or_else(|_| {
213 default
214 .clone()
215 .ok_or_else(|| ResolverError::EnvVarNotFound(name.clone()))
216 })?;
217
218 string_val.parse::<T>().map_err(|e| {
219 ResolverError::ParseError(format!("Failed to parse env var '{name}': {e}"))
220 })
221 }
222 }
223 }
224
225 pub fn resolve_optional<T>(
227 &self,
228 value: &Option<ConfigValue<T>>,
229 ) -> Result<Option<T>, ResolverError>
230 where
231 T: FromStr + Clone + serde::Serialize + serde::de::DeserializeOwned,
232 T::Err: std::fmt::Display,
233 {
234 value.as_ref().map(|v| self.resolve_typed(v)).transpose()
235 }
236
237 pub fn resolve_optional_string(
239 &self,
240 value: &Option<ConfigValue<String>>,
241 ) -> Result<Option<String>, ResolverError> {
242 value.as_ref().map(|v| self.resolve_string(v)).transpose()
243 }
244
245 pub fn resolve_string_vec(
247 &self,
248 values: &[ConfigValue<String>],
249 ) -> Result<Vec<String>, ResolverError> {
250 values.iter().map(|v| self.resolve_string(v)).collect()
251 }
252
253 pub fn map_with<TDto, TDomain>(
255 &self,
256 dto: &TDto,
257 mapper: &impl ConfigMapper<TDto, TDomain>,
258 ) -> Result<TDomain, MappingError> {
259 mapper.map(dto, self)
260 }
261}
262
263impl Default for DtoMapper {
264 fn default() -> Self {
265 Self::new()
266 }
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272
273 #[test]
274 fn test_resolve_string_static() {
275 let mapper = DtoMapper::new();
276 let value = ConfigValue::Static("hello".to_string());
277
278 let result = mapper.resolve_string(&value).expect("resolve");
279 assert_eq!(result, "hello");
280 }
281
282 #[test]
283 fn test_resolve_string_env_var() {
284 std::env::set_var("TEST_SDK_MAPPER_VAR", "mapped_value");
285
286 let mapper = DtoMapper::new();
287 let value = ConfigValue::EnvironmentVariable {
288 name: "TEST_SDK_MAPPER_VAR".to_string(),
289 default: None,
290 };
291
292 let result = mapper.resolve_string(&value).expect("resolve");
293 assert_eq!(result, "mapped_value");
294
295 std::env::remove_var("TEST_SDK_MAPPER_VAR");
296 }
297
298 #[test]
299 fn test_resolve_typed_u16() {
300 let mapper = DtoMapper::new();
301 let value = ConfigValue::Static(5432u16);
302
303 let result = mapper.resolve_typed(&value).expect("resolve");
304 assert_eq!(result, 5432u16);
305 }
306
307 #[test]
308 fn test_resolve_typed_u16_from_env() {
309 std::env::set_var("TEST_SDK_PORT", "8080");
310
311 let mapper = DtoMapper::new();
312 let value: ConfigValue<u16> = ConfigValue::EnvironmentVariable {
313 name: "TEST_SDK_PORT".to_string(),
314 default: None,
315 };
316
317 let result = mapper.resolve_typed(&value).expect("resolve");
318 assert_eq!(result, 8080u16);
319
320 std::env::remove_var("TEST_SDK_PORT");
321 }
322
323 #[test]
324 fn test_resolve_typed_parse_error() {
325 std::env::set_var("TEST_SDK_INVALID_PORT", "not_a_number");
326
327 let mapper = DtoMapper::new();
328 let value: ConfigValue<u16> = ConfigValue::EnvironmentVariable {
329 name: "TEST_SDK_INVALID_PORT".to_string(),
330 default: None,
331 };
332
333 let result = mapper.resolve_typed(&value);
334 assert!(result.is_err());
335 assert!(matches!(
336 result.expect_err("should fail"),
337 ResolverError::ParseError(_)
338 ));
339
340 std::env::remove_var("TEST_SDK_INVALID_PORT");
341 }
342
343 #[test]
344 fn test_resolve_optional_some() {
345 let mapper = DtoMapper::new();
346 let value = Some(ConfigValue::Static("test".to_string()));
347
348 let result = mapper.resolve_optional(&value).expect("resolve");
349 assert_eq!(result, Some("test".to_string()));
350 }
351
352 #[test]
353 fn test_resolve_optional_none() {
354 let mapper = DtoMapper::new();
355 let value: Option<ConfigValue<String>> = None;
356
357 let result = mapper.resolve_optional(&value).expect("resolve");
358 assert_eq!(result, None);
359 }
360
361 #[test]
362 fn test_resolve_string_vec() {
363 let mapper = DtoMapper::new();
364 let values = vec![
365 ConfigValue::Static("a".to_string()),
366 ConfigValue::Static("b".to_string()),
367 ];
368
369 let result = mapper.resolve_string_vec(&values).expect("resolve");
370 assert_eq!(result, vec!["a", "b"]);
371 }
372
373 #[test]
374 fn test_config_mapper_trait() {
375 struct TestMapper;
376
377 #[derive(Debug)]
378 struct TestDto {
379 host: ConfigValue<String>,
380 }
381
382 struct TestDomain {
383 host: String,
384 }
385
386 impl ConfigMapper<TestDto, TestDomain> for TestMapper {
387 fn map(&self, dto: &TestDto, resolver: &DtoMapper) -> Result<TestDomain, MappingError> {
388 Ok(TestDomain {
389 host: resolver.resolve_string(&dto.host)?,
390 })
391 }
392 }
393
394 let mapper = DtoMapper::new();
395 let dto = TestDto {
396 host: ConfigValue::Static("localhost".to_string()),
397 };
398
399 let domain = mapper.map_with(&dto, &TestMapper).expect("map");
400 assert_eq!(domain.host, "localhost");
401 }
402
403 #[test]
404 fn test_custom_resolver() {
405 struct AlwaysResolver;
406 impl ValueResolver for AlwaysResolver {
407 fn resolve_to_string(
408 &self,
409 _value: &ConfigValue<String>,
410 ) -> Result<String, ResolverError> {
411 Ok("custom-resolved".to_string())
412 }
413 }
414
415 let mapper = DtoMapper::new().with_resolver("Secret", Arc::new(AlwaysResolver));
416 let value = ConfigValue::Secret {
417 name: "test".to_string(),
418 };
419
420 let result = mapper.resolve_string(&value).expect("resolve");
421 assert_eq!(result, "custom-resolved");
422 }
423}