use serde::de::DeserializeOwned;
use std::fmt;
#[derive(Debug)]
pub enum ConfigError {
EnvyError(envy::Error),
MissingVar(String),
}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ConfigError::EnvyError(e) => write!(f, "Configuration error: {}", e),
ConfigError::MissingVar(var) => {
write!(f, "Missing required environment variable: {}", var)
}
}
}
}
impl std::error::Error for ConfigError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
ConfigError::EnvyError(e) => Some(e),
ConfigError::MissingVar(_) => None,
}
}
}
impl From<envy::Error> for ConfigError {
fn from(err: envy::Error) -> Self {
ConfigError::EnvyError(err)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Environment {
Development,
Production,
Custom(String),
}
impl Environment {
pub fn current() -> Self {
match std::env::var("RUSTAPI_ENV").as_deref() {
Ok("production") | Ok("prod") => Self::Production,
Ok("development") | Ok("dev") => Self::Development,
Ok(other) => Self::Custom(other.to_string()),
Err(_) => Self::Development,
}
}
pub fn is_production(&self) -> bool {
matches!(self, Self::Production)
}
pub fn is_development(&self) -> bool {
matches!(self, Self::Development)
}
pub fn as_str(&self) -> &str {
match self {
Self::Development => "development",
Self::Production => "production",
Self::Custom(name) => name,
}
}
pub fn show_error_details(&self) -> bool {
!self.is_production()
}
pub fn enable_debug_logging(&self) -> bool {
self.is_development()
}
pub fn default_log_level(&self) -> &'static str {
match self {
Self::Development => "debug",
Self::Production => "info",
Self::Custom(_) => "info",
}
}
}
impl Default for Environment {
fn default() -> Self {
Self::current()
}
}
impl fmt::Display for Environment {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone)]
pub struct Config<T>(pub T);
impl<T: DeserializeOwned> Config<T> {
pub fn from_env() -> Result<Self, ConfigError> {
envy::from_env::<T>().map(Config).map_err(ConfigError::from)
}
pub fn from_env_prefixed(prefix: &str) -> Result<Self, ConfigError> {
envy::prefixed(format!("{}_", prefix))
.from_env::<T>()
.map(Config)
.map_err(ConfigError::from)
}
pub fn into_inner(self) -> T {
self.0
}
}
impl<T> std::ops::Deref for Config<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T> std::ops::DerefMut for Config<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
pub fn load_dotenv() {
let _ = dotenvy::dotenv();
}
pub fn load_dotenv_from<P: AsRef<std::path::Path>>(path: P) {
let _ = dotenvy::from_path(path);
}
pub fn require_env(name: &str) -> String {
std::env::var(name).unwrap_or_else(|_| {
panic!(
"Required environment variable '{}' is not set. \
Please set it in your .env file or environment.",
name
)
})
}
pub fn try_require_env(name: &str) -> Result<String, ConfigError> {
std::env::var(name).map_err(|_| ConfigError::MissingVar(name.to_string()))
}
pub fn env_or(name: &str, default: &str) -> String {
std::env::var(name).unwrap_or_else(|_| default.to_string())
}
pub fn env_parse<T: std::str::FromStr>(name: &str) -> Option<T> {
std::env::var(name).ok().and_then(|v| v.parse().ok())
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
use serde::Deserialize;
use serial_test::serial;
#[test]
#[serial]
fn test_environment_detection_development() {
std::env::remove_var("RUSTAPI_ENV");
let env = Environment::current();
assert!(env.is_development());
assert!(!env.is_production());
assert_eq!(env.as_str(), "development");
}
#[test]
#[serial]
fn test_environment_detection_production() {
std::env::set_var("RUSTAPI_ENV", "production");
let env = Environment::current();
assert!(env.is_production());
assert!(!env.is_development());
assert_eq!(env.as_str(), "production");
std::env::set_var("RUSTAPI_ENV", "prod");
let env = Environment::current();
assert!(env.is_production());
std::env::remove_var("RUSTAPI_ENV");
}
#[test]
#[serial]
fn test_environment_detection_custom() {
std::env::set_var("RUSTAPI_ENV", "staging");
let env = Environment::current();
assert!(!env.is_production());
assert!(!env.is_development());
assert_eq!(env.as_str(), "staging");
std::env::remove_var("RUSTAPI_ENV");
}
#[test]
fn test_environment_display() {
assert_eq!(format!("{}", Environment::Development), "development");
assert_eq!(format!("{}", Environment::Production), "production");
assert_eq!(
format!("{}", Environment::Custom("staging".to_string())),
"staging"
);
}
#[test]
fn test_environment_defaults() {
let dev = Environment::Development;
assert!(dev.show_error_details());
assert!(dev.enable_debug_logging());
assert_eq!(dev.default_log_level(), "debug");
let prod = Environment::Production;
assert!(!prod.show_error_details());
assert!(!prod.enable_debug_logging());
assert_eq!(prod.default_log_level(), "info");
let custom = Environment::Custom("staging".to_string());
assert!(custom.show_error_details()); assert!(!custom.enable_debug_logging()); assert_eq!(custom.default_log_level(), "info");
}
#[test]
#[serial]
fn test_env_or_with_default() {
let var_name = "RUSTAPI_TEST_ENV_OR_12345";
std::env::remove_var(var_name);
let value = env_or(var_name, "default_value");
assert_eq!(value, "default_value");
std::env::set_var(var_name, "actual_value");
let value = env_or(var_name, "default_value");
assert_eq!(value, "actual_value");
std::env::remove_var(var_name);
}
#[test]
#[serial]
fn test_env_parse() {
let var_name = "RUSTAPI_TEST_PARSE_12345";
std::env::set_var(var_name, "42");
let value: Option<u32> = env_parse(var_name);
assert_eq!(value, Some(42));
std::env::set_var(var_name, "true");
let value: Option<bool> = env_parse(var_name);
assert_eq!(value, Some(true));
std::env::set_var(var_name, "not_a_number");
let value: Option<u32> = env_parse(var_name);
assert_eq!(value, None);
std::env::remove_var(var_name);
}
#[derive(Debug, Deserialize, PartialEq)]
struct TestConfig {
unit_test_string: String,
unit_test_number: u32,
}
#[test]
#[serial]
fn test_config_from_env() {
std::env::set_var("UNIT_TEST_STRING", "hello");
std::env::set_var("UNIT_TEST_NUMBER", "42");
let config = Config::<TestConfig>::from_env().unwrap();
assert_eq!(config.unit_test_string, "hello");
assert_eq!(config.unit_test_number, 42);
std::env::remove_var("UNIT_TEST_STRING");
std::env::remove_var("UNIT_TEST_NUMBER");
}
#[derive(Debug, Deserialize, PartialEq)]
struct MissingVarTestConfig {
missing_var_test_string: String,
missing_var_test_number: u32,
}
#[test]
#[serial]
fn test_config_from_env_missing_var() {
std::env::remove_var("MISSING_VAR_TEST_STRING");
std::env::remove_var("MISSING_VAR_TEST_NUMBER");
let result = Config::<MissingVarTestConfig>::from_env();
assert!(result.is_err());
}
#[derive(Debug, Deserialize, PartialEq)]
struct PrefixedConfig {
url: String,
port: u16,
}
#[test]
#[serial]
fn test_config_from_env_prefixed() {
std::env::set_var("MYAPP_URL", "http://localhost");
std::env::set_var("MYAPP_PORT", "3000");
let config = Config::<PrefixedConfig>::from_env_prefixed("MYAPP").unwrap();
assert_eq!(config.url, "http://localhost");
assert_eq!(config.port, 3000);
std::env::remove_var("MYAPP_URL");
std::env::remove_var("MYAPP_PORT");
}
#[derive(Debug, Deserialize, PartialEq)]
struct DerefTestConfig {
deref_test_string: String,
deref_test_number: u32,
}
#[test]
#[serial]
fn test_config_deref() {
std::env::set_var("DEREF_TEST_STRING", "deref_test");
std::env::set_var("DEREF_TEST_NUMBER", "100");
let config = Config::<DerefTestConfig>::from_env().unwrap();
assert_eq!(config.deref_test_string, "deref_test");
assert_eq!(config.deref_test_number, 100);
std::env::remove_var("DEREF_TEST_STRING");
std::env::remove_var("DEREF_TEST_NUMBER");
}
#[derive(Debug, Deserialize, PartialEq)]
struct InnerTestConfig {
inner_test_string: String,
inner_test_number: u32,
}
#[test]
#[serial]
fn test_config_into_inner() {
std::env::set_var("INNER_TEST_STRING", "inner_test");
std::env::set_var("INNER_TEST_NUMBER", "200");
let config = Config::<InnerTestConfig>::from_env().unwrap();
let inner = config.into_inner();
assert_eq!(inner.inner_test_string, "inner_test");
assert_eq!(inner.inner_test_number, 200);
std::env::remove_var("INNER_TEST_STRING");
std::env::remove_var("INNER_TEST_NUMBER");
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
#[serial]
fn prop_config_deserialization(
string_value in "[a-zA-Z0-9_]{1,50}",
number_value in 0u32..10000u32,
) {
let string_var = format!("PROP_TEST_STR_{}", std::process::id());
let number_var = format!("PROP_TEST_NUM_{}", std::process::id());
std::env::set_var(&string_var, &string_value);
std::env::set_var(&number_var, number_value.to_string());
#[derive(Debug, Deserialize, PartialEq)]
#[allow(dead_code)]
struct PropTestConfig {
prop_test_str: String,
prop_test_num: u32,
}
std::env::remove_var(&string_var);
std::env::remove_var(&number_var);
std::env::set_var("PROP_CONFIG_STRING", &string_value);
std::env::set_var("PROP_CONFIG_NUMBER", number_value.to_string());
#[derive(Debug, Deserialize, PartialEq)]
struct PropConfig {
prop_config_string: String,
prop_config_number: u32,
}
let result = Config::<PropConfig>::from_env();
std::env::remove_var("PROP_CONFIG_STRING");
std::env::remove_var("PROP_CONFIG_NUMBER");
prop_assert!(result.is_ok(), "Config deserialization should succeed");
let config = result.unwrap();
prop_assert_eq!(&config.prop_config_string, &string_value, "String value should match");
prop_assert_eq!(config.prop_config_number, number_value, "Number value should match");
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
#[serial]
fn prop_config_optional_fields(
required_value in "[a-zA-Z0-9]{1,30}",
optional_present in prop::bool::ANY,
optional_value in "[a-zA-Z0-9]{1,30}",
) {
#[derive(Debug, Deserialize, PartialEq)]
struct OptionalConfig {
required_field: String,
#[serde(default)]
optional_field: Option<String>,
}
std::env::set_var("REQUIRED_FIELD", &required_value);
if optional_present {
std::env::set_var("OPTIONAL_FIELD", &optional_value);
} else {
std::env::remove_var("OPTIONAL_FIELD");
}
let result = Config::<OptionalConfig>::from_env();
std::env::remove_var("REQUIRED_FIELD");
std::env::remove_var("OPTIONAL_FIELD");
prop_assert!(result.is_ok(), "Config with optional fields should deserialize");
let config = result.unwrap();
prop_assert_eq!(&config.required_field, &required_value);
if optional_present {
prop_assert_eq!(&config.optional_field, &Some(optional_value));
} else {
prop_assert!(config.optional_field.is_none());
}
}
}
}