use std::collections::HashMap;
use crate::{
EnvSource,
error::{SourceError, SourceRegisterError},
urn::is_valid_source_id,
};
pub trait Source: Send + Sync {
fn get(&self, name: &str) -> Result<Vec<u8>, SourceError>;
}
pub struct SourceRegistry {
sources: HashMap<String, Box<dyn Source>>,
}
impl SourceRegistry {
pub fn new() -> Self {
let mut registry = Self {
sources: HashMap::new(),
};
registry
.register("env", EnvSource)
.expect("\"env\" is a valid source_id");
registry
}
pub fn register(
&mut self,
id: impl Into<String>,
source: impl Source + 'static,
) -> Result<(), SourceRegisterError> {
let id = id.into();
if !is_valid_source_id(&id) {
return Err(SourceRegisterError::InvalidSourceId(id));
}
self.sources.insert(id, Box::new(source));
Ok(())
}
pub fn get(&self, source_id: &str) -> Option<&dyn Source> {
self.sources.get(source_id).map(|s| s.as_ref())
}
}
impl Default for SourceRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_pre_registers_env_source() {
let registry = SourceRegistry::new();
assert!(
registry.get("env").is_some(),
"expected \"env\" to be registered by default"
);
}
#[test]
fn new_does_not_register_file_by_default() {
let registry = SourceRegistry::new();
assert!(registry.get("file").is_none());
}
#[test]
fn env_source_resolves_without_explicit_registration() {
unsafe { std::env::set_var("REGISTRY_DEFAULT_ENV_TEST", "works") };
let result = SourceRegistry::new()
.get("env")
.unwrap()
.get("REGISTRY_DEFAULT_ENV_TEST");
unsafe { std::env::remove_var("REGISTRY_DEFAULT_ENV_TEST") };
assert_eq!(result.unwrap(), b"works");
}
#[test]
fn register_replaces_existing_id() {
struct ConstSource(&'static [u8]);
impl Source for ConstSource {
fn get(&self, _name: &str) -> Result<Vec<u8>, SourceError> {
Ok(self.0.to_vec())
}
}
let mut registry = SourceRegistry::new();
registry.register("env", ConstSource(b"replaced")).unwrap();
let result = registry.get("env").unwrap().get("anything").unwrap();
assert_eq!(result, b"replaced");
}
#[test]
fn register_accepts_valid_ids() {
let mut r = SourceRegistry::new();
for id in &[
"file",
"vault-sm",
"aws.secrets",
"gcp_sm",
"my~source",
"ns/sub",
] {
assert!(
r.register(*id, crate::sources::env::EnvSource).is_ok(),
"rejected valid id: {id}"
);
}
}
#[test]
fn register_rejects_empty_id() {
let mut r = SourceRegistry::new();
assert_eq!(
r.register("", crate::sources::env::EnvSource).unwrap_err(),
SourceRegisterError::InvalidSourceId(String::new()),
);
}
#[test]
fn register_rejects_id_with_colon() {
let mut r = SourceRegistry::new();
let err = r
.register("bad:id", crate::sources::env::EnvSource)
.unwrap_err();
assert_eq!(
err,
SourceRegisterError::InvalidSourceId("bad:id".to_owned())
);
}
#[test]
fn register_rejects_id_with_space() {
let mut r = SourceRegistry::new();
let err = r
.register("bad id", crate::sources::env::EnvSource)
.unwrap_err();
assert_eq!(
err,
SourceRegisterError::InvalidSourceId("bad id".to_owned())
);
}
#[test]
fn register_rejects_id_with_non_ascii() {
let mut r = SourceRegistry::new();
let err = r
.register("café", crate::sources::env::EnvSource)
.unwrap_err();
assert_eq!(err, SourceRegisterError::InvalidSourceId("café".to_owned()));
}
}