use std::path::{Path, PathBuf};
use mofa::{ConfigLoader, EnvironmentSource, FileSource};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("Configuration error: {0}")]
Error(String),
}
pub type ConfigResult<T> = Result<T, ConfigError>;
#[derive(Clone, Serialize, Deserialize, Debug, Default)]
pub struct ConfigWrapper<T: DeserializeOwned + Serialize + Default> {
pub basic: BasicConfig,
#[serde(bound = "", default)]
pub application: T,
}
#[derive(Clone, Deserialize, Serialize, Debug)]
pub struct BasicConfig {
pub host: String,
pub port: u16,
}
impl Default for BasicConfig {
fn default() -> Self {
Self {
host: "127.0.0.1".to_string(),
port: 3000,
}
}
}
#[derive(Clone, Debug, Default)]
pub struct ConfigState {
pub file_paths: Vec<PathBuf>,
pub env_prefixes: Vec<String>,
pub enable_vars: bool,
}
pub struct ConfigBuilder {
loader: ConfigLoader,
state: ConfigState,
}
impl ConfigBuilder {
pub fn new() -> Self {
Self {
loader: ConfigLoader::new(),
state: ConfigState::default(),
}
}
pub fn env(mut self, prefix: &str) -> Self {
self.state.env_prefixes.push(prefix.to_string());
self.loader.add_source(EnvironmentSource::new(prefix));
self
}
pub fn file<P: AsRef<Path>>(mut self, path: P) -> Self {
let path = path.as_ref().to_path_buf();
self.state.file_paths.push(path.clone());
if path.exists() {
self.loader.add_source(FileSource::new(path));
}
self
}
pub fn file_optional<P: AsRef<Path>>(mut self, path: P) -> Self {
let path = path.as_ref().to_path_buf();
self.state.file_paths.push(path.clone());
if path.exists() {
self.loader.add_source(FileSource::new(path));
}
self
}
pub fn enable_vars(mut self) -> Self {
self.state.enable_vars = true;
self.loader.enable_path_variable_processor();
self.loader.enable_environment_variable_processor();
self
}
pub fn build<T: for<'de> Deserialize<'de>>(mut self) -> ConfigResult<T> {
if self.state.enable_vars {
self.loader.enable_path_variable_processor();
self.loader.enable_environment_variable_processor();
}
self.loader.construct().map_err(|e| ConfigError::Error(e.to_string()))
}
pub fn state(&self) -> ConfigState {
self.state.clone()
}
pub fn from_state(state: ConfigState) -> Self {
let mut builder = Self::new();
for prefix in &state.env_prefixes {
builder = builder.env(prefix);
}
for path in &state.file_paths {
builder = builder.file_optional(path);
}
if state.enable_vars {
builder = builder.enable_vars();
}
builder
}
}
impl Default for ConfigBuilder {
fn default() -> Self {
Self::new()
}
}
pub struct Config;
impl Config {
pub fn builder() -> ConfigBuilder {
ConfigBuilder::new()
}
pub fn load_default<T: for<'de> Deserialize<'de> + Default>() -> T {
Self::builder()
.file_optional("configurations/application.toml")
.env("APP")
.enable_vars()
.build()
.unwrap_or_else(|_| T::default())
}
}
pub struct GotchaConfigLoader;
impl GotchaConfigLoader {
pub fn load<T: for<'de> Deserialize<'de>>(_profile: Option<String>) -> T {
Config::builder()
.file_optional("configurations/application.toml")
.env("APP")
.enable_vars()
.build()
.expect("Failed to load configuration")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
struct TestConfig {
name: String,
value: i32,
}
#[test]
fn test_config_builder() {
let _result: Result<TestConfig, _> = Config::builder().env("TEST").enable_vars().build();
}
#[test]
fn test_config_wrapper() {
let wrapper = ConfigWrapper {
basic: BasicConfig::default(),
application: TestConfig::default(),
};
assert_eq!(wrapper.application.name, "");
}
}