mod file;
mod env;
pub mod error;
pub use error::ConfigError;
pub use file::FileConfigProvider;
pub use env::EnvConfigProvider;
use std::fmt::Debug;
use std::sync::Arc;
use serde::de::DeserializeOwned;
use serde_json::Value;
pub trait ConfigProvider: Debug + Send + Sync {
fn has(&self, key: &str) -> bool;
fn provider_name(&self) -> &str;
fn get_raw(&self, key: &str) -> Result<Option<Value>, ConfigError>;
}
pub trait ConfigProviderExt: ConfigProvider {
fn get<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>, ConfigError> {
match self.get_raw(key)? {
Some(value) => {
serde_json::from_value(value)
.map(Some)
.map_err(|e| ConfigError::ParseError(format!("failed to deserialize '{}': {}", key, e)))
},
None => Ok(None),
}
}
}
impl<T: ConfigProvider> ConfigProviderExt for T {}
#[derive(Debug, Default)]
pub struct ConfigBuilder {
providers: Vec<Arc<dyn ConfigProvider>>,
}
impl ConfigBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn with_provider<P: ConfigProvider + 'static>(mut self, provider: P) -> Self {
self.providers.push(Arc::new(provider));
self
}
pub fn build(self) -> Config {
Config {
providers: self.providers,
}
}
}
#[derive(Debug, Clone)]
pub struct Config {
providers: Vec<Arc<dyn ConfigProvider>>,
}
impl Config {
pub fn builder() -> ConfigBuilder {
ConfigBuilder::new()
}
fn get_raw(&self, key: &str) -> Result<Option<Value>, ConfigError> {
for provider in &self.providers {
if provider.has(key) {
return provider.get_raw(key);
}
}
Ok(None)
}
pub fn get<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>, ConfigError> {
match self.get_raw(key)? {
Some(value) => {
serde_json::from_value(value)
.map(Some)
.map_err(|e| ConfigError::ParseError(format!("failed to deserialize '{}': {}", key, e)))
},
None => Ok(None),
}
}
pub fn get_or_default<T: DeserializeOwned>(&self, key: &str, default: T) -> Result<T, ConfigError> {
match self.get(key)? {
Some(value) => Ok(value),
None => Ok(default),
}
}
pub fn default_file(file_path: &str) -> Result<Self, ConfigError> {
let provider = FileConfigProvider::new(file_path)?;
Ok(Self::builder().with_provider(provider).build())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_builder_pattern() {
let config = Config::builder()
.with_provider(MockProvider::new("mock1"))
.with_provider(MockProvider::new("mock2"))
.build();
assert_eq!(config.providers.len(), 2);
}
#[derive(Debug)]
struct MockProvider {
name: String,
}
impl MockProvider {
fn new(name: &str) -> Self {
Self { name: name.to_string() }
}
}
impl ConfigProvider for MockProvider {
fn get_raw(&self, _key: &str) -> Result<Option<Value>, ConfigError> {
Ok(None)
}
fn has(&self, _key: &str) -> bool {
false
}
fn provider_name(&self) -> &str {
&self.name
}
}
}