/// SecretsResolver — loads all required secrets at startup. /// /// The resolver validates that all required secrets are present before the /// application starts. If any are missing, startup fails with a clear error /// listing what's missing — not a runtime panic deep in the app. use std::collections::HashMap; use crate::error::SecretsError; use crate::secret::Secret; use crate::source::SecretsSource; /// A resolved, validated set of secrets. /// /// Created by SecretsResolver at startup. Once resolved, all secrets are /// available and guaranteed to have been present at startup time. pub struct ResolvedSecrets { values: HashMap>, } impl ResolvedSecrets { /// Get a secret by key. /// /// Returns None if the key wasn't declared as required. /// In normal use you will always have the keys you declared. pub fn get(&self, key: &str) -> Option<&Secret> { self.values.get(key) } /// Get a secret or panic with a clear message. /// /// Use this for secrets that are truly required and were declared in /// the resolver — if the resolver passed, this will always succeed. pub fn require(&self, key: &str) -> &Secret { self.values.get(key).unwrap_or_else(|| { panic!( "Secret '{}' was not declared as required in SecretsResolver. \ Declare all required secrets before calling resolve().", key ) }) } /// The number of resolved secrets. pub fn len(&self) -> usize { self.values.len() } pub fn is_empty(&self) -> bool { self.values.is_empty() } } /// Loads and validates all required secrets at startup. /// /// Usage: /// ```ignore /// let secrets = SecretsResolver::new() /// .source(EnvVarSource::new()) /// .require("jwt.secret_key") /// .require("database.password") /// .resolve()?; /// /// let jwt_key = secrets.require("jwt.secret_key"); /// ``` pub struct SecretsResolver { sources: Vec>, required: Vec, } impl SecretsResolver { pub fn new() -> Self { Self { sources: Vec::new(), required: Vec::new(), } } /// Add a secret source. Sources are tried in order; first success wins. pub fn source(mut self, source: Box) -> Self { self.sources.push(source); self } /// Declare a required secret key. /// /// All required keys must be present in at least one source. /// resolve() fails if any are missing. pub fn require(mut self, key: impl Into) -> Self { self.required.push(key.into()); self } /// Declare multiple required secret keys. pub fn require_all(mut self, keys: &[&str]) -> Self { for key in keys { self.required.push(key.to_string()); } self } /// Load all required secrets and validate they are all present. /// /// Returns an error listing ALL missing secrets — not just the first one — /// so you can fix them all in one go. pub fn resolve(self) -> Result { let mut values = HashMap::new(); let mut missing = Vec::new(); for key in &self.required { match self.fetch_from_sources(key) { Ok(secret) => { values.insert(key.clone(), secret); } Err(_) => { missing.push(key.clone()); } } } if !missing.is_empty() { return Err(SecretsError::MissingRequired { keys: missing }); } Ok(ResolvedSecrets { values }) } /// Attempt to fetch a key from sources in order. fn fetch_from_sources(&self, key: &str) -> Result, SecretsError> { for source in &self.sources { if let Ok(secret) = source.get(key) { return Ok(secret); } } Err(SecretsError::NotFoundInAnySource { key: key.to_string(), }) } /// Resolve without requiring all keys — useful when you want /// to load whatever is available. pub fn resolve_optional(self) -> ResolvedSecrets { let mut values = HashMap::new(); for key in &self.required { if let Ok(secret) = self.fetch_from_sources(key) { values.insert(key.clone(), secret); } } ResolvedSecrets { values } } } impl Default for SecretsResolver { fn default() -> Self { Self::new() } } #[cfg(test)] mod tests { use super::*; use crate::source::InMemorySource; fn make_source(pairs: &[(&str, &str)]) -> InMemorySource { let mut src = InMemorySource::new(); for (k, v) in pairs { src.insert(*k, *v); } src } #[test] fn resolve_all_present() { let src = make_source(&[("jwt.key", "secret"), ("db.password", "pass123")]); let secrets = SecretsResolver::new() .source(Box::new(src)) .require("jwt.key") .require("db.password") .resolve() .unwrap(); assert_eq!(secrets.require("jwt.key").expose(), "secret"); assert_eq!(secrets.require("db.password").expose(), "pass123"); } #[test] fn resolve_missing_fails_with_list() { let src = make_source(&[("jwt.key", "secret")]); let result = SecretsResolver::new() .source(Box::new(src)) .require("jwt.key") .require("db.password") // missing .require("stripe.key") // also missing .resolve(); match result { Err(SecretsError::MissingRequired { keys }) => { assert!(keys.contains(&"db.password".to_string())); assert!(keys.contains(&"stripe.key".to_string())); assert_eq!(keys.len(), 2); } _ => panic!("expected MissingRequired error"), } } #[test] fn resolve_optional_skips_missing() { let src = make_source(&[("jwt.key", "secret")]); let secrets = SecretsResolver::new() .source(Box::new(src)) .require("jwt.key") .require("missing.key") .resolve_optional(); assert!(secrets.get("jwt.key").is_some()); assert!(secrets.get("missing.key").is_none()); } #[test] fn multiple_sources_fallback() { let mut primary = InMemorySource::new(); primary.insert("jwt.key", "from-primary"); let mut secondary = InMemorySource::new(); secondary.insert("db.password", "from-secondary"); let secrets = SecretsResolver::new() .source(Box::new(primary)) .source(Box::new(secondary)) .require("jwt.key") .require("db.password") .resolve() .unwrap(); assert_eq!(secrets.require("jwt.key").expose(), "from-primary"); assert_eq!(secrets.require("db.password").expose(), "from-secondary"); } #[test] fn first_source_wins() { let mut s1 = InMemorySource::new(); s1.insert("key", "value-1"); let mut s2 = InMemorySource::new(); s2.insert("key", "value-2"); let secrets = SecretsResolver::new() .source(Box::new(s1)) .source(Box::new(s2)) .require("key") .resolve() .unwrap(); assert_eq!(secrets.require("key").expose(), "value-1"); } #[test] fn require_all() { let src = make_source(&[("a", "1"), ("b", "2"), ("c", "3")]); let secrets = SecretsResolver::new() .source(Box::new(src)) .require_all(&["a", "b", "c"]) .resolve() .unwrap(); assert_eq!(secrets.len(), 3); } #[test] fn resolved_len() { let src = make_source(&[("k", "v")]); let secrets = SecretsResolver::new() .source(Box::new(src)) .require("k") .resolve() .unwrap(); assert_eq!(secrets.len(), 1); } #[test] fn resolved_is_empty() { let resolved = ResolvedSecrets { values: HashMap::new() }; assert!(resolved.is_empty()); } }