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}