cfgloader_core/
lib.rs

1use std::env;
2
3#[doc(hidden)]
4pub mod fallback {
5    pub fn load_or_default<T>(_env_path: &std::path::Path) -> Result<T, crate::CfgError>
6    where
7        T: Default,
8    {
9        // We need a way to detect if T implements FromEnv
10        // Due to Rust limitations, we use a simple approach: try to call T::load directly
11        // If compilation fails, it means T doesn't implement FromEnv, so we use Default
12
13        // Since we can't detect trait implementation at runtime, we return Default
14        // Users need to explicitly use #[env(...)] to load environment variables
15        Ok(T::default())
16    }
17}
18
19#[derive(Debug)]
20pub enum CfgError {
21    MissingEnv(&'static str),
22    ParseError {
23        key: &'static str,
24        value: String,
25        ty: &'static str,
26        source: Box<dyn std::error::Error + Send + Sync>,
27    },
28    LoadError {
29        msg: &'static str,
30        source: Box<dyn std::error::Error + Send + Sync>,
31    },
32}
33
34impl std::fmt::Display for CfgError {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        match self {
37            CfgError::MissingEnv(key) => write!(f, "missing required env: {}", key),
38            CfgError::ParseError { key, value, ty, .. } => {
39                write!(
40                    f,
41                    "failed to parse env {} value `{}` into {}",
42                    key, value, ty
43                )
44            }
45            CfgError::LoadError { msg, .. } => write!(f, "failed to load env: {}", msg),
46        }
47    }
48}
49
50impl std::error::Error for CfgError {
51    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
52        match self {
53            CfgError::MissingEnv(_) => None,
54            CfgError::ParseError { source, .. } => Some(source.as_ref()),
55            CfgError::LoadError { source, .. } => Some(source.as_ref()),
56        }
57    }
58}
59
60pub trait FromEnv: Sized {
61    fn load(env_path: &std::path::Path) -> Result<Self, CfgError>;
62    fn load_iter<I, P>(paths: I) -> Result<Self, CfgError>
63    where
64        I: IntoIterator<Item = P>,
65        P: AsRef<std::path::Path>;
66}
67
68/// Utility function for macros: read env and return `Option<String>`
69pub fn get_env(key: &'static str) -> Option<String> {
70    env::var(key).ok()
71}
72
73/// Utility function for macros: load .env file
74pub fn load_env_file(env_path: &std::path::Path) -> Result<(), CfgError> {
75    // Try to load .env file if it exists, but don't fail if it doesn't
76    match dotenvy::from_path(env_path) {
77        Ok(_) => Ok(()),
78        Err(dotenvy::Error::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
79        Err(e) => Err(CfgError::LoadError {
80            msg: "failed to load .env file",
81            source: Box::new(e),
82        }),
83    }
84}
85
86/// Load .env from multiple paths (any iterable), return on first success.
87/// If none found, return error.
88pub fn load_env_file_iter<I, P>(paths: I) -> Result<(), CfgError>
89where
90    I: IntoIterator<Item = P>,
91    P: AsRef<std::path::Path>,
92{
93    let mut last_err = None;
94    for path in paths {
95        match load_env_file(path.as_ref()) {
96            Ok(_) => return Ok(()),
97            Err(e) => last_err = Some(e),
98        }
99    }
100    Err(last_err.unwrap_or_else(|| CfgError::LoadError {
101        msg: "no .env file found in any provided path",
102        source: Box::new(std::io::Error::new(
103            std::io::ErrorKind::NotFound,
104            "not found",
105        )),
106    }))
107}
108
109/// Utility function for macros: parse string to T
110pub fn parse_scalar<T: std::str::FromStr>(key: &'static str, raw: String) -> Result<T, CfgError>
111where
112    <T as std::str::FromStr>::Err: std::error::Error + Send + Sync + 'static,
113{
114    raw.parse::<T>().map_err(|e| CfgError::ParseError {
115        key,
116        value: raw,
117        ty: std::any::type_name::<T>(),
118        source: Box::new(e),
119    })
120}
121
122/// Split string and parse each part to `Vec<T>`
123pub fn parse_vec<T: std::str::FromStr>(
124    key: &'static str,
125    raw: String,
126    sep: &'static str,
127) -> Result<Vec<T>, CfgError>
128where
129    <T as std::str::FromStr>::Err: std::error::Error + Send + Sync + 'static,
130{
131    if sep.is_empty() {
132        return Ok(Vec::new());
133    }
134    let mut out = Vec::new();
135    for part in raw.split(sep) {
136        let s = part.trim().to_string();
137        if s.is_empty() {
138            continue;
139        }
140        out.push(parse_scalar::<T>(key, s)?);
141    }
142    Ok(out)
143}