explicon/
lib.rs

1use std::{env::var, str::FromStr};
2
3use serde::{Deserialize, Serialize};
4
5/// Represents errors that can occur during configuration value resolution.
6#[derive(thiserror::Error, Debug)]
7pub enum ExpliconError {
8    /// Occurs when an environment variable can't be resolved.
9    #[error("Error while resolving env var: {0}")]
10    Var(#[from] std::env::VarError),
11    /// Generic error container for other resolution failures.
12    #[error("{0}")]
13    Other(String),
14}
15
16/// Result type alias using [`ExpliconError`] for error handling in configuration resolution.
17pub type Result<T> = std::result::Result<T, ExpliconError>;
18
19/// A configuration value that can be sourced either directly or from an environment variable.
20///
21/// Supports deserialization from both formats:
22/// - Direct value representation (e.g., `42` or `"direct_value"`)
23/// - Environment variable reference (e.g., `{ "env": "VAR_NAME" }`)
24#[derive(Debug, Clone, Deserialize, Serialize)]
25#[serde(rename_all = "snake_case")]
26pub enum Sourced<T> {
27    /// Value should be read from the specified environment variable
28    Env(String),
29
30    /// Directly provided value that doesn't require resolution
31    #[serde(untagged)]
32    Value(T),
33}
34
35
36impl<T> Sourced<T>
37where
38    T: FromStr,
39    T: Clone,
40    <T as FromStr>::Err: ToString,
41{
42    /// Resolves the configuration value by parsing if sourced from an environment variable.
43    ///
44    /// Use this method when the target type `T` implements [`FromStr`] (e.g., numbers, booleans).
45    ///
46    /// # Returns
47    /// - `Ok(T)` with direct value if using [`Sourced::Value`]
48    /// - `Ok(T)` with **parsed** environment variable value if using [`Sourced::Env`]
49    ///
50    /// # Errors
51    /// - [`ExpliconError::Var`] if environment variable lookup fails
52    /// - [`ExpliconError::Other`] if environment variable value parsing fails (via [`FromStr`])
53    pub fn resolve(&self) -> Result<T> {
54        match self {
55            Self::Value(value) => Ok(value.clone()),
56            Self::Env(var_name) => {
57                let var_value = var(var_name)?; // Получаем String
58                // Используем parse, т.к. T: FromStr
59                let value = var_value
60                    .parse::<T>()
61                    .map_err(|e| ExpliconError::Other(e.to_string()))?;
62                Ok(value)
63            }
64        }
65    }
66
67     /// Resolves the value using `resolve()` or returns type's default if resolution fails.
68    pub fn resolve_or_default(&self) -> Result<T>
69    where
70        T: Default,
71    {
72        self.resolve().or_else(|_| Ok(T::default()))
73    }
74
75    /// Resolves the value using `resolve()` or returns the provided fallback value if resolution fails.
76    pub fn resolve_or(&self, fallback: T) -> T {
77        self.resolve().unwrap_or(fallback)
78    }
79
80    /// Resolves the value using `resolve()` and validates it against a predicate.
81    pub fn resolve_and_validate<F>(&self, validator: F) -> Result<T>
82    where
83        F: FnOnce(&T) -> bool,
84    {
85        let value = self.resolve()?;
86        if validator(&value) {
87            Ok(value)
88        } else {
89            Err(ExpliconError::Other("Validation failed".into()))
90        }
91    }
92}
93
94impl<T> Sourced<T>
95where
96    T: From<String>,
97    T: Clone,
98{
99    /// Resolves the configuration value by converting directly from the environment variable string.
100    ///
101    /// Use this method when the target type `T` implements [`From<String>`] but not necessarily [`FromStr`]
102    /// (e.g., [`secrecy::SecretString`]).
103    ///
104    /// # Returns
105    /// - `Ok(T)` with direct value if using [`Sourced::Value`]
106    /// - `Ok(T)` with value created **directly via `From<String>`** from the environment variable if using [`Sourced::Env`]
107    ///
108    /// # Errors
109    /// - [`ExpliconError::Var`] if environment variable lookup fails. Conversion via `From<String>` is assumed infallible.
110    pub fn resolve_from_string(&self) -> Result<T> {
111        match self {
112            Self::Value(value) => Ok(value.clone()),
113            Self::Env(var_name) => {
114                let var_value = var(var_name)?; // Получаем String
115                // Используем From<String>, т.к. T: From<String>
116                Ok(T::from(var_value))
117            }
118        }
119    }
120
121     /// Resolves the value using `resolve_from_string()` or returns the provided fallback value.
122    /// Note: Does not return default, as `From<String>` types might not have a meaningful default.
123    pub fn resolve_from_string_or(&self, fallback: T) -> T {
124        self.resolve_from_string().unwrap_or(fallback)
125    }
126
127     /// Resolves the value using `resolve_from_string()` and validates it against a predicate.
128     pub fn resolve_from_string_and_validate<F>(&self, validator: F) -> Result<T>
129    where
130        F: FnOnce(&T) -> bool,
131    {
132        let value = self.resolve_from_string()?;
133        if validator(&value) {
134            Ok(value)
135        } else {
136            Err(ExpliconError::Other("Validation failed".into()))
137        }
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn resolve_value() {
147        let sourced = Sourced::Value(42);
148        assert_eq!(sourced.resolve().unwrap(), 42);
149    }
150
151    #[test]
152    fn resolve_env_success() {
153        let var_name = "TEST_RESOLVE_ENV_SUCCESS";
154        let expected_value = 123;
155        unsafe { std::env::set_var(var_name, expected_value.to_string()) };
156        let sourced = Sourced::<i32>::Env(var_name.to_string());
157        let result = sourced.resolve().unwrap();
158        assert_eq!(result, expected_value);
159        unsafe { std::env::remove_var(var_name) };
160    }
161
162    #[test]
163    fn resolve_env_var_not_found() {
164        let var_name = "NON_EXISTENT_VAR_XYZ123";
165        unsafe { std::env::remove_var(var_name) };
166        let sourced = Sourced::<i32>::Env(var_name.to_string());
167        let result = sourced.resolve();
168        assert!(matches!(result, Err(ExpliconError::Var(_))));
169    }
170
171    #[test]
172    fn resolve_env_var_invalid_parse() {
173        let var_name = "TEST_INVALID_PARSE";
174        unsafe { std::env::set_var(var_name, "abc") };
175        let sourced = Sourced::<i32>::Env(var_name.to_string());
176        let result = sourced.resolve();
177        assert!(matches!(result, Err(ExpliconError::Other(_))));
178        unsafe { std::env::remove_var(var_name) };
179    }
180
181    #[test]
182    fn resolve_or_default_env_missing() {
183        let var_name = "NON_EXISTENT_VAR_FOR_DEFAULT";
184        unsafe { std::env::remove_var(var_name) };
185        let sourced = Sourced::<i32>::Env(var_name.to_string());
186        let result = sourced.resolve_or_default().unwrap();
187        assert_eq!(result, i32::default());
188    }
189
190    #[test]
191    fn resolve_or_default_parse_error() {
192        let var_name = "TEST_PARSE_ERROR_DEFAULT";
193        unsafe { std::env::set_var(var_name, "abc") };
194        let sourced = Sourced::<i32>::Env(var_name.to_string());
195        let result = sourced.resolve_or_default().unwrap();
196        assert_eq!(result, i32::default());
197        unsafe { std::env::remove_var(var_name) };
198    }
199
200    #[test]
201    fn resolve_or_default_success() {
202        let var_name = "TEST_RESOLVE_OR_DEFAULT_SUCCESS";
203        unsafe { std::env::set_var(var_name, "5") };
204        let sourced = Sourced::<i32>::Env(var_name.to_string());
205        let result = sourced.resolve_or_default().unwrap();
206        assert_eq!(result, 5);
207        unsafe { std::env::remove_var(var_name) };
208    }
209
210    #[test]
211    fn resolve_and_validate_success() {
212        let sourced = Sourced::Value(5);
213        let result = sourced.resolve_and_validate(|v| *v == 5).unwrap();
214        assert_eq!(result, 5);
215    }
216
217    #[test]
218    fn resolve_and_validate_failure() {
219        let sourced = Sourced::Value(5);
220        let result = sourced.resolve_and_validate(|v| *v == 10);
221        assert!(matches!(result, Err(ExpliconError::Other(_))));
222    }
223
224    #[test]
225    fn resolve_and_validate_env_missing() {
226        let var_name = "NON_EXISTENT_VAR_FOR_VALIDATE";
227        unsafe { std::env::remove_var(var_name) };
228        let sourced = Sourced::<i32>::Env(var_name.to_string());
229        let result = sourced.resolve_and_validate(|_| true);
230        assert!(matches!(result, Err(ExpliconError::Var(_))));
231    }
232
233    #[test]
234    fn resolve_and_validate_env_invalid() {
235        let var_name = "TEST_VALIDATE_ENV_INVALID";
236        unsafe { std::env::set_var(var_name, "10") };
237        let sourced = Sourced::<i32>::Env(var_name.to_string());
238        let result = sourced.resolve_and_validate(|v| *v == 5);
239        assert!(matches!(result, Err(ExpliconError::Other(_))));
240        unsafe { std::env::remove_var(var_name) };
241    }
242
243    #[test]
244    fn resolve_env_string() {
245        let var_name = "TEST_ENV_STRING";
246        let expected = "hello";
247        unsafe { std::env::set_var(var_name, expected) };
248        let sourced = Sourced::<String>::Env(var_name.to_string());
249        let result = sourced.resolve().unwrap();
250        assert_eq!(result, expected);
251        unsafe { std::env::remove_var(var_name) };
252    }
253
254    #[test]
255    fn resolve_env_bool() {
256        let var_name = "TEST_ENV_BOOL";
257        unsafe { std::env::set_var(var_name, "true") };
258        let sourced = Sourced::<bool>::Env(var_name.to_string());
259        let result = sourced.resolve().unwrap();
260        assert!(result);
261        unsafe { std::env::remove_var(var_name) };
262    }
263}