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}