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//!
37//! struct VaultResolver { /* client */ }
38//!
39//! impl ValueResolver for VaultResolver {
40//!     fn resolve_to_string(&self, value: &ConfigValue<String>) -> Result<String, ResolverError> {
41//!         match value {
42//!             ConfigValue::Secret { name } => {
43//!                 // Look up secret in Vault, K8s, etc.
44//!                 Ok("resolved-value".to_string())
45//!             }
46//!             _ => Err(ResolverError::WrongResolverType),
47//!         }
48//!     }
49//! }
50//!
51//! register_secret_resolver(Arc::new(VaultResolver { /* ... */ })).expect("already registered");
52//! ```
53//!
54//! # Custom Resolvers
55//!
56//! For per-mapper overrides, use [`DtoMapper::with_resolver`](crate::mapper::DtoMapper::with_resolver):
57//!
58//! ```rust,ignore
59//! use drasi_plugin_sdk::mapper::DtoMapper;
60//! use std::sync::Arc;
61//!
62//! let mapper = DtoMapper::new()
63//!     .with_resolver("Secret", Arc::new(MyTestResolver));
64//! ```
65
66use crate::config_value::ConfigValue;
67use std::sync::{Arc, OnceLock};
68use thiserror::Error;
69
70/// Errors that can occur during value resolution.
71#[derive(Debug, Error)]
72pub enum ResolverError {
73    /// The referenced environment variable was not found and no default was provided.
74    #[error("Environment variable '{0}' not found and no default provided")]
75    EnvVarNotFound(String),
76
77    /// The requested resolution method is not yet implemented.
78    #[error("Not implemented: {0}")]
79    NotImplemented(String),
80
81    /// No resolver was registered for the given reference type.
82    #[error("No resolver found for reference type: {0}")]
83    NoResolverFound(String),
84
85    /// A resolver was called with a `ConfigValue` variant it doesn't handle.
86    #[error("Wrong resolver type used for this reference")]
87    WrongResolverType,
88
89    /// The resolved string value could not be parsed to the target type.
90    #[error("Failed to parse value: {0}")]
91    ParseError(String),
92}
93
94/// Trait for resolving a specific type of [`ConfigValue`] variant to its actual string value.
95///
96/// Each resolver handles one variant (e.g., `EnvironmentVariable` or `Secret`).
97/// The [`DtoMapper`](crate::mapper::DtoMapper) dispatches to the appropriate resolver
98/// based on the variant.
99pub trait ValueResolver: Send + Sync {
100    /// Resolve a [`ConfigValue`] variant to its actual string value.
101    ///
102    /// Returns `Err(ResolverError::WrongResolverType)` if called with a variant
103    /// this resolver doesn't handle.
104    fn resolve_to_string(&self, value: &ConfigValue<String>) -> Result<String, ResolverError>;
105}
106
107/// Resolves [`ConfigValue::EnvironmentVariable`] references by reading `std::env::var()`.
108///
109/// Falls back to the `default` value if the environment variable is not set.
110/// Returns [`ResolverError::EnvVarNotFound`] if neither the variable nor a default exists.
111pub struct EnvironmentVariableResolver;
112
113impl ValueResolver for EnvironmentVariableResolver {
114    fn resolve_to_string(&self, value: &ConfigValue<String>) -> Result<String, ResolverError> {
115        match value {
116            ConfigValue::EnvironmentVariable { name, default } => {
117                std::env::var(name).or_else(|_| {
118                    default
119                        .clone()
120                        .ok_or_else(|| ResolverError::EnvVarNotFound(name.clone()))
121                })
122            }
123            _ => Err(ResolverError::WrongResolverType),
124        }
125    }
126}
127
128/// Default resolver for [`ConfigValue::Secret`] references.
129///
130/// Returns [`ResolverError::NotImplemented`] unless a custom secret resolver
131/// has been registered via [`register_secret_resolver`].
132pub struct SecretResolver;
133
134impl ValueResolver for SecretResolver {
135    fn resolve_to_string(&self, value: &ConfigValue<String>) -> Result<String, ResolverError> {
136        match value {
137            ConfigValue::Secret { name } => Err(ResolverError::NotImplemented(format!(
138                "Secret resolution not yet implemented for '{name}'"
139            ))),
140            _ => Err(ResolverError::WrongResolverType),
141        }
142    }
143}
144
145/// Global secret resolver registry.
146///
147/// Allows a consuming library to register a concrete [`ValueResolver`] for
148/// secrets once at startup. All subsequent [`DtoMapper::new()`](crate::mapper::DtoMapper::new)
149/// calls will automatically use the registered resolver for
150/// [`ConfigValue::Secret`] references.
151static SECRET_RESOLVER: OnceLock<Arc<dyn ValueResolver>> = OnceLock::new();
152
153/// Register a global secret resolver.
154///
155/// This must be called **once** before any [`DtoMapper`](crate::mapper::DtoMapper)
156/// instances are created. Subsequent calls will return an `Err` containing the
157/// resolver that was not stored.
158///
159/// # Example
160///
161/// ```rust,ignore
162/// use drasi_plugin_sdk::resolver::{register_secret_resolver, ValueResolver, ResolverError};
163/// use drasi_plugin_sdk::ConfigValue;
164/// use std::sync::Arc;
165///
166/// struct VaultResolver;
167///
168/// impl ValueResolver for VaultResolver {
169///     fn resolve_to_string(&self, value: &ConfigValue<String>) -> Result<String, ResolverError> {
170///         match value {
171///             ConfigValue::Secret { name } => Ok(fetch_from_vault(name)),
172///             _ => Err(ResolverError::WrongResolverType),
173///         }
174///     }
175/// }
176///
177/// register_secret_resolver(Arc::new(VaultResolver)).expect("already registered");
178/// ```
179pub fn register_secret_resolver(
180    resolver: Arc<dyn ValueResolver>,
181) -> Result<(), Arc<dyn ValueResolver>> {
182    SECRET_RESOLVER.set(resolver)
183}
184
185/// Returns the globally registered secret resolver, if one has been registered.
186pub(crate) fn get_secret_resolver() -> Option<Arc<dyn ValueResolver>> {
187    SECRET_RESOLVER.get().cloned()
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    #[test]
195    fn test_env_resolver_with_set_var() {
196        std::env::set_var("TEST_SDK_VAR_1", "test_value");
197
198        let resolver = EnvironmentVariableResolver;
199        let value = ConfigValue::EnvironmentVariable {
200            name: "TEST_SDK_VAR_1".to_string(),
201            default: None,
202        };
203
204        let result = resolver.resolve_to_string(&value).expect("resolve");
205        assert_eq!(result, "test_value");
206
207        std::env::remove_var("TEST_SDK_VAR_1");
208    }
209
210    #[test]
211    fn test_env_resolver_with_default() {
212        let resolver = EnvironmentVariableResolver;
213        let value = ConfigValue::EnvironmentVariable {
214            name: "NONEXISTENT_SDK_VAR_12345".to_string(),
215            default: Some("default_value".to_string()),
216        };
217
218        let result = resolver.resolve_to_string(&value).expect("resolve");
219        assert_eq!(result, "default_value");
220    }
221
222    #[test]
223    fn test_env_resolver_missing_var_no_default() {
224        let resolver = EnvironmentVariableResolver;
225        let value = ConfigValue::EnvironmentVariable {
226            name: "NONEXISTENT_SDK_VAR_67890".to_string(),
227            default: None,
228        };
229
230        let result = resolver.resolve_to_string(&value);
231        assert!(result.is_err());
232        assert!(matches!(
233            result.expect_err("should fail"),
234            ResolverError::EnvVarNotFound(_)
235        ));
236    }
237
238    #[test]
239    fn test_env_resolver_wrong_variant() {
240        let resolver = EnvironmentVariableResolver;
241        let value = ConfigValue::Secret {
242            name: "x".to_string(),
243        };
244        assert!(matches!(
245            resolver.resolve_to_string(&value).expect_err("should fail"),
246            ResolverError::WrongResolverType
247        ));
248    }
249
250    #[test]
251    fn test_secret_resolver_not_implemented() {
252        let resolver = SecretResolver;
253        let value = ConfigValue::Secret {
254            name: "my-secret".to_string(),
255        };
256
257        let result = resolver.resolve_to_string(&value);
258        assert!(result.is_err());
259        assert!(matches!(
260            result.expect_err("should fail"),
261            ResolverError::NotImplemented(_)
262        ));
263    }
264
265    #[test]
266    fn test_secret_resolver_wrong_variant() {
267        let resolver = SecretResolver;
268        let value = ConfigValue::Static("x".to_string());
269        assert!(matches!(
270            resolver.resolve_to_string(&value).expect_err("should fail"),
271            ResolverError::WrongResolverType
272        ));
273    }
274}