drasi_plugin_sdk/
mapper.rs1use crate::config_value::ConfigValue;
56use crate::resolver::{
57 get_secret_resolver, EnvironmentVariableResolver, ResolverError, SecretResolver, ValueResolver,
58};
59use std::collections::HashMap;
60use std::str::FromStr;
61use std::sync::Arc;
62use thiserror::Error;
63
64#[derive(Debug, Error)]
66pub enum MappingError {
67 #[error("Failed to resolve config value: {0}")]
69 ResolutionError(#[from] ResolverError),
70
71 #[error("Failed to create source: {0}")]
73 SourceCreationError(String),
74
75 #[error("Failed to create reaction: {0}")]
77 ReactionCreationError(String),
78
79 #[error("Invalid value: {0}")]
81 InvalidValue(String),
82}
83
84pub struct DtoMapper {
94 resolvers: HashMap<&'static str, Arc<dyn ValueResolver>>,
95}
96
97impl DtoMapper {
98 pub fn new() -> Self {
105 let mut resolvers: HashMap<&'static str, Arc<dyn ValueResolver>> = HashMap::new();
106 resolvers.insert("EnvironmentVariable", Arc::new(EnvironmentVariableResolver));
107
108 let secret_resolver = get_secret_resolver().unwrap_or_else(|| Arc::new(SecretResolver));
109 resolvers.insert("Secret", secret_resolver);
110
111 Self { resolvers }
112 }
113
114 pub fn with_resolver(mut self, kind: &'static str, resolver: Arc<dyn ValueResolver>) -> Self {
118 self.resolvers.insert(kind, resolver);
119 self
120 }
121
122 pub async fn resolve_string(
124 &self,
125 value: &ConfigValue<String>,
126 ) -> Result<String, ResolverError> {
127 match value {
128 ConfigValue::Static(s) => Ok(s.clone()),
129
130 ConfigValue::Secret { .. } => {
131 let resolver = self
132 .resolvers
133 .get("Secret")
134 .ok_or_else(|| ResolverError::NoResolverFound("Secret".to_string()))?;
135 resolver.resolve_to_string(value).await
136 }
137
138 ConfigValue::EnvironmentVariable { .. } => {
139 let resolver = self.resolvers.get("EnvironmentVariable").ok_or_else(|| {
140 ResolverError::NoResolverFound("EnvironmentVariable".to_string())
141 })?;
142 resolver.resolve_to_string(value).await
143 }
144 }
145 }
146
147 pub async fn resolve_typed<T>(&self, value: &ConfigValue<T>) -> Result<T, ResolverError>
152 where
153 T: FromStr + Clone + serde::Serialize + serde::de::DeserializeOwned,
154 T::Err: std::fmt::Display,
155 {
156 match value {
157 ConfigValue::Static(v) => Ok(v.clone()),
158
159 ConfigValue::Secret { name } => {
160 let resolver = self
161 .resolvers
162 .get("Secret")
163 .ok_or_else(|| ResolverError::NoResolverFound("Secret".to_string()))?;
164 let string_cv = ConfigValue::Secret { name: name.clone() };
165 let string_val = resolver.resolve_to_string(&string_cv).await?;
166 string_val.parse::<T>().map_err(|e| {
167 ResolverError::ParseError(format!("Failed to parse secret '{name}': {e}"))
168 })
169 }
170
171 ConfigValue::EnvironmentVariable { name, default } => {
172 let string_val = std::env::var(name).or_else(|_| {
173 default
174 .clone()
175 .ok_or_else(|| ResolverError::EnvVarNotFound(name.clone()))
176 })?;
177
178 string_val.parse::<T>().map_err(|e| {
179 ResolverError::ParseError(format!("Failed to parse env var '{name}': {e}"))
180 })
181 }
182 }
183 }
184
185 pub async fn resolve_optional<T>(
187 &self,
188 value: &Option<ConfigValue<T>>,
189 ) -> Result<Option<T>, ResolverError>
190 where
191 T: FromStr + Clone + serde::Serialize + serde::de::DeserializeOwned,
192 T::Err: std::fmt::Display,
193 {
194 match value {
195 Some(v) => self.resolve_typed(v).await.map(Some),
196 None => Ok(None),
197 }
198 }
199
200 pub async fn resolve_optional_string(
202 &self,
203 value: &Option<ConfigValue<String>>,
204 ) -> Result<Option<String>, ResolverError> {
205 match value {
206 Some(v) => self.resolve_string(v).await.map(Some),
207 None => Ok(None),
208 }
209 }
210
211 pub async fn resolve_string_vec(
213 &self,
214 values: &[ConfigValue<String>],
215 ) -> Result<Vec<String>, ResolverError> {
216 let mut result = Vec::with_capacity(values.len());
217 for v in values {
218 result.push(self.resolve_string(v).await?);
219 }
220 Ok(result)
221 }
222}
223
224impl Default for DtoMapper {
225 fn default() -> Self {
226 Self::new()
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 #[tokio::test]
235 async fn test_resolve_string_static() {
236 let mapper = DtoMapper::new();
237 let value = ConfigValue::Static("hello".to_string());
238
239 let result = mapper.resolve_string(&value).await.expect("resolve");
240 assert_eq!(result, "hello");
241 }
242
243 #[tokio::test]
244 async fn test_resolve_string_env_var() {
245 std::env::set_var("TEST_SDK_MAPPER_VAR", "mapped_value");
246
247 let mapper = DtoMapper::new();
248 let value = ConfigValue::EnvironmentVariable {
249 name: "TEST_SDK_MAPPER_VAR".to_string(),
250 default: None,
251 };
252
253 let result = mapper.resolve_string(&value).await.expect("resolve");
254 assert_eq!(result, "mapped_value");
255
256 std::env::remove_var("TEST_SDK_MAPPER_VAR");
257 }
258
259 #[tokio::test]
260 async fn test_resolve_typed_u16() {
261 let mapper = DtoMapper::new();
262 let value = ConfigValue::Static(5432u16);
263
264 let result = mapper.resolve_typed(&value).await.expect("resolve");
265 assert_eq!(result, 5432u16);
266 }
267
268 #[tokio::test]
269 async fn test_resolve_typed_u16_from_env() {
270 std::env::set_var("TEST_SDK_PORT", "8080");
271
272 let mapper = DtoMapper::new();
273 let value: ConfigValue<u16> = ConfigValue::EnvironmentVariable {
274 name: "TEST_SDK_PORT".to_string(),
275 default: None,
276 };
277
278 let result = mapper.resolve_typed(&value).await.expect("resolve");
279 assert_eq!(result, 8080u16);
280
281 std::env::remove_var("TEST_SDK_PORT");
282 }
283
284 #[tokio::test]
285 async fn test_resolve_typed_parse_error() {
286 std::env::set_var("TEST_SDK_INVALID_PORT", "not_a_number");
287
288 let mapper = DtoMapper::new();
289 let value: ConfigValue<u16> = ConfigValue::EnvironmentVariable {
290 name: "TEST_SDK_INVALID_PORT".to_string(),
291 default: None,
292 };
293
294 let result = mapper.resolve_typed(&value).await;
295 assert!(result.is_err());
296 assert!(matches!(
297 result.expect_err("should fail"),
298 ResolverError::ParseError(_)
299 ));
300
301 std::env::remove_var("TEST_SDK_INVALID_PORT");
302 }
303
304 #[tokio::test]
305 async fn test_resolve_optional_some() {
306 let mapper = DtoMapper::new();
307 let value = Some(ConfigValue::Static("test".to_string()));
308
309 let result = mapper.resolve_optional(&value).await.expect("resolve");
310 assert_eq!(result, Some("test".to_string()));
311 }
312
313 #[tokio::test]
314 async fn test_resolve_optional_none() {
315 let mapper = DtoMapper::new();
316 let value: Option<ConfigValue<String>> = None;
317
318 let result = mapper.resolve_optional(&value).await.expect("resolve");
319 assert_eq!(result, None);
320 }
321
322 #[tokio::test]
323 async fn test_resolve_string_vec() {
324 let mapper = DtoMapper::new();
325 let values = vec![
326 ConfigValue::Static("a".to_string()),
327 ConfigValue::Static("b".to_string()),
328 ];
329
330 let result = mapper.resolve_string_vec(&values).await.expect("resolve");
331 assert_eq!(result, vec!["a", "b"]);
332 }
333
334 #[tokio::test]
335 async fn test_custom_resolver() {
336 struct AlwaysResolver;
337 #[async_trait::async_trait]
338 impl ValueResolver for AlwaysResolver {
339 async fn resolve_to_string(
340 &self,
341 _value: &ConfigValue<String>,
342 ) -> Result<String, ResolverError> {
343 Ok("custom-resolved".to_string())
344 }
345 }
346
347 let mapper = DtoMapper::new().with_resolver("Secret", Arc::new(AlwaysResolver));
348 let value = ConfigValue::Secret {
349 name: "test".to_string(),
350 };
351
352 let result = mapper.resolve_string(&value).await.expect("resolve");
353 assert_eq!(result, "custom-resolved");
354 }
355}