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 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
68pub fn get_env(key: &'static str) -> Option<String> {
70 env::var(key).ok()
71}
72
73pub fn load_env_file(env_path: &std::path::Path) -> Result<(), CfgError> {
75 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
86pub 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
109pub 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
122pub 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}