Skip to main content

drasi_plugin_sdk/
resolver.rs

1// Copyright 2025 The Drasi Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Value resolvers for [`ConfigValue`] reference types.
16//!
17//! Resolvers convert [`ConfigValue::EnvironmentVariable`] and [`ConfigValue::Secret`]
18//! references into their actual values at runtime. The server provides built-in resolvers
19//! for environment variables and secrets.
20//!
21//! # Built-in Resolvers
22//!
23//! - [`EnvironmentVariableResolver`] — Reads from `std::env::var()`, falls back to default.
24//! - [`SecretResolver`] — Default stub that returns `NotImplemented`.
25//!
26//! # Registering a Secret Resolver
27//!
28//! Consuming libraries should call [`register_secret_resolver`] once at startup to
29//! provide a concrete implementation. All [`DtoMapper::new()`](crate::mapper::DtoMapper::new)
30//! calls will automatically use it:
31//!
32//! ```rust,ignore
33//! use drasi_plugin_sdk::resolver::{register_secret_resolver, ValueResolver, ResolverError};
34//! use drasi_plugin_sdk::ConfigValue;
35//! use std::sync::Arc;
36//! use async_trait::async_trait;
37//!
38//! struct VaultResolver { /* client */ }
39//!
40//! #[async_trait]
41//! impl ValueResolver for VaultResolver {
42//!     async fn resolve_to_string(&self, value: &ConfigValue<String>) -> Result<String, ResolverError> {
43//!         match value {
44//!             ConfigValue::Secret { name } => {
45//!                 // Look up secret in Vault, K8s, etc.
46//!                 Ok("resolved-value".to_string())
47//!             }
48//!             _ => Err(ResolverError::WrongResolverType),
49//!         }
50//!     }
51//! }
52//!
53//! register_secret_resolver(Arc::new(VaultResolver { /* ... */ }));
54//! ```
55//!
56//! # Custom Resolvers
57//!
58//! For per-mapper overrides, use [`DtoMapper::with_resolver`](crate::mapper::DtoMapper::with_resolver):
59//!
60//! ```rust,ignore
61//! use drasi_plugin_sdk::mapper::DtoMapper;
62//! use std::sync::Arc;
63//!
64//! let mapper = DtoMapper::new()
65//!     .with_resolver("Secret", Arc::new(MyTestResolver));
66//! ```
67
68use crate::config_value::ConfigValue;
69use async_trait::async_trait;
70use std::sync::{Arc, RwLock};
71use thiserror::Error;
72
73/// Errors that can occur during value resolution.
74#[derive(Debug, Error)]
75pub enum ResolverError {
76    /// The referenced environment variable was not found and no default was provided.
77    #[error("Environment variable '{0}' not found and no default provided")]
78    EnvVarNotFound(String),
79
80    /// The requested resolution method is not yet implemented.
81    #[error("Not implemented: {0}")]
82    NotImplemented(String),
83
84    /// Secret resolution failed at runtime (store unreachable, secret not found, auth error, etc.)
85    #[error("Secret resolution failed: {0}")]
86    SecretResolutionFailed(String),
87
88    /// No resolver was registered for the given reference type.
89    #[error("No resolver found for reference type: {0}")]
90    NoResolverFound(String),
91
92    /// A resolver was called with a `ConfigValue` variant it doesn't handle.
93    #[error("Wrong resolver type used for this reference")]
94    WrongResolverType,
95
96    /// The resolved string value could not be parsed to the target type.
97    #[error("Failed to parse value: {0}")]
98    ParseError(String),
99}
100
101/// Trait for resolving a specific type of [`ConfigValue`] variant to its actual string value.
102///
103/// Each resolver handles one variant (e.g., `EnvironmentVariable` or `Secret`).
104/// The [`DtoMapper`](crate::mapper::DtoMapper) dispatches to the appropriate resolver
105/// based on the variant.
106#[async_trait]
107pub trait ValueResolver: Send + Sync {
108    /// Resolve a [`ConfigValue`] variant to its actual string value.
109    ///
110    /// Returns `Err(ResolverError::WrongResolverType)` if called with a variant
111    /// this resolver doesn't handle.
112    async fn resolve_to_string(&self, value: &ConfigValue<String>)
113        -> Result<String, ResolverError>;
114}
115
116/// Resolves [`ConfigValue::EnvironmentVariable`] references by reading `std::env::var()`.
117///
118/// Falls back to the `default` value if the environment variable is not set.
119/// Returns [`ResolverError::EnvVarNotFound`] if neither the variable nor a default exists.
120pub struct EnvironmentVariableResolver;
121
122#[async_trait]
123impl ValueResolver for EnvironmentVariableResolver {
124    async fn resolve_to_string(
125        &self,
126        value: &ConfigValue<String>,
127    ) -> Result<String, ResolverError> {
128        match value {
129            ConfigValue::EnvironmentVariable { name, default } => {
130                std::env::var(name).or_else(|_| {
131                    default
132                        .clone()
133                        .ok_or_else(|| ResolverError::EnvVarNotFound(name.clone()))
134                })
135            }
136            _ => Err(ResolverError::WrongResolverType),
137        }
138    }
139}
140
141/// Default resolver for [`ConfigValue::Secret`] references.
142///
143/// Returns [`ResolverError::NotImplemented`] unless a custom secret resolver
144/// has been registered via [`register_secret_resolver`].
145pub struct SecretResolver;
146
147#[async_trait]
148impl ValueResolver for SecretResolver {
149    async fn resolve_to_string(
150        &self,
151        value: &ConfigValue<String>,
152    ) -> Result<String, ResolverError> {
153        match value {
154            ConfigValue::Secret { name } => Err(ResolverError::NotImplemented(format!(
155                "Secret resolution not yet implemented for '{name}'"
156            ))),
157            _ => Err(ResolverError::WrongResolverType),
158        }
159    }
160}
161
162/// Global secret resolver registry.
163///
164/// Allows a consuming library to register a concrete [`ValueResolver`] for
165/// secrets at startup or when a config resolver is injected via FFI.
166/// All subsequent [`DtoMapper::new()`](crate::mapper::DtoMapper::new)
167/// calls will automatically use the registered resolver for
168/// [`ConfigValue::Secret`] references.
169///
170/// Uses `RwLock` instead of `OnceLock` so the resolver can be replaced
171/// (e.g., when the host injects a config resolver callback via FFI).
172static SECRET_RESOLVER: RwLock<Option<Arc<dyn ValueResolver>>> = RwLock::new(None);
173
174/// Register a global secret resolver.
175///
176/// This should be called before any [`DtoMapper`](crate::mapper::DtoMapper)
177/// instances are created. Can be called multiple times — each call replaces
178/// the previously registered resolver.
179///
180/// # Example
181///
182/// ```rust,ignore
183/// use drasi_plugin_sdk::resolver::{register_secret_resolver, ValueResolver, ResolverError};
184/// use drasi_plugin_sdk::ConfigValue;
185/// use std::sync::Arc;
186/// use async_trait::async_trait;
187///
188/// struct VaultResolver;
189///
190/// #[async_trait]
191/// impl ValueResolver for VaultResolver {
192///     async fn resolve_to_string(&self, value: &ConfigValue<String>) -> Result<String, ResolverError> {
193///         match value {
194///             ConfigValue::Secret { name } => Ok(fetch_from_vault(name)),
195///             _ => Err(ResolverError::WrongResolverType),
196///         }
197///     }
198/// }
199///
200/// register_secret_resolver(Arc::new(VaultResolver));
201/// ```
202pub fn register_secret_resolver(resolver: Arc<dyn ValueResolver>) {
203    let mut guard = SECRET_RESOLVER.write().expect("SECRET_RESOLVER poisoned");
204    if guard.is_some() {
205        log::warn!("Secret resolver re-registered — previous resolver replaced");
206    }
207    *guard = Some(resolver);
208}
209
210/// Returns the globally registered secret resolver, if one has been registered.
211pub(crate) fn get_secret_resolver() -> Option<Arc<dyn ValueResolver>> {
212    let guard = SECRET_RESOLVER.read().expect("SECRET_RESOLVER poisoned");
213    guard.clone()
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    #[tokio::test]
221    async fn test_env_resolver_with_set_var() {
222        std::env::set_var("TEST_SDK_VAR_1", "test_value");
223
224        let resolver = EnvironmentVariableResolver;
225        let value = ConfigValue::EnvironmentVariable {
226            name: "TEST_SDK_VAR_1".to_string(),
227            default: None,
228        };
229
230        let result = resolver.resolve_to_string(&value).await.expect("resolve");
231        assert_eq!(result, "test_value");
232
233        std::env::remove_var("TEST_SDK_VAR_1");
234    }
235
236    #[tokio::test]
237    async fn test_env_resolver_with_default() {
238        let resolver = EnvironmentVariableResolver;
239        let value = ConfigValue::EnvironmentVariable {
240            name: "NONEXISTENT_SDK_VAR_12345".to_string(),
241            default: Some("default_value".to_string()),
242        };
243
244        let result = resolver.resolve_to_string(&value).await.expect("resolve");
245        assert_eq!(result, "default_value");
246    }
247
248    #[tokio::test]
249    async fn test_env_resolver_missing_var_no_default() {
250        let resolver = EnvironmentVariableResolver;
251        let value = ConfigValue::EnvironmentVariable {
252            name: "NONEXISTENT_SDK_VAR_67890".to_string(),
253            default: None,
254        };
255
256        let result = resolver.resolve_to_string(&value).await;
257        assert!(result.is_err());
258        assert!(matches!(
259            result.expect_err("should fail"),
260            ResolverError::EnvVarNotFound(_)
261        ));
262    }
263
264    #[tokio::test]
265    async fn test_env_resolver_wrong_variant() {
266        let resolver = EnvironmentVariableResolver;
267        let value = ConfigValue::Secret {
268            name: "x".to_string(),
269        };
270        assert!(matches!(
271            resolver
272                .resolve_to_string(&value)
273                .await
274                .expect_err("should fail"),
275            ResolverError::WrongResolverType
276        ));
277    }
278
279    #[tokio::test]
280    async fn test_secret_resolver_not_implemented() {
281        let resolver = SecretResolver;
282        let value = ConfigValue::Secret {
283            name: "my-secret".to_string(),
284        };
285
286        let result = resolver.resolve_to_string(&value).await;
287        assert!(result.is_err());
288        assert!(matches!(
289            result.expect_err("should fail"),
290            ResolverError::NotImplemented(_)
291        ));
292    }
293
294    #[tokio::test]
295    async fn test_secret_resolver_wrong_variant() {
296        let resolver = SecretResolver;
297        let value = ConfigValue::Static("x".to_string());
298        assert!(matches!(
299            resolver
300                .resolve_to_string(&value)
301                .await
302                .expect_err("should fail"),
303            ResolverError::WrongResolverType
304        ));
305    }
306}