#[cfg_attr(feature = "serde", deprecated(note = "use `derive` feature instead"))]
pub use envir_derive::*;
use std::collections::HashMap;
#[cfg_attr(feature = "serde", deprecated(note = "use `derive` feature instead"))]
pub trait Serialize {
fn export(&self) {
for (k, v) in self.collect() {
crate::set(&k, v);
}
}
fn collect(&self) -> HashMap<String, String>;
}
#[cfg_attr(feature = "serde", deprecated(note = "use `derive` feature instead"))]
pub trait Deserialize {
fn from_env() -> crate::Result<Self>
where
Self: Sized,
{
let env = crate::collect();
Self::from(&env)
}
fn from(env: &HashMap<String, String>) -> crate::Result<Self>
where
Self: Sized;
}
#[cfg_attr(feature = "serde", deprecated(note = "use `derive` feature instead"))]
pub fn from_env<T>() -> crate::Result<T>
where
T: Deserialize,
{
T::from_env()
}
#[cfg_attr(feature = "serde", deprecated(note = "use `derive` feature instead"))]
pub fn from<T>(env: &HashMap<String, String>) -> crate::Result<T>
where
T: Deserialize,
{
T::from(env)
}
#[doc(hidden)]
pub fn load_option<T: std::str::FromStr>(
env: &HashMap<String, String>,
var: &str,
default: Option<String>,
_separator: char,
) -> crate::Result<Option<T>>
where
T::Err: ToString,
{
#[cfg(feature = "extrapolation")]
fn try_replace<'t, F: FnMut(®ex::Captures) -> crate::Result<String>>(
regex: ®ex::Regex,
text: &'t str,
mut rep: F,
) -> crate::Result<std::borrow::Cow<'t, str>> {
let mut it = regex.captures_iter(text).peekable();
if it.peek().is_none() {
return Ok(std::borrow::Cow::Borrowed(text));
}
let mut new = String::with_capacity(text.len());
let mut last_match = 0;
for cap in it {
let m = cap.get(0).unwrap();
new.push_str(&text[last_match..m.start()]);
new.push_str(&rep(&cap)?);
last_match = m.end();
}
new.push_str(&text[last_match..]);
Ok(std::borrow::Cow::Owned(new))
}
#[cfg(feature = "extrapolation")]
let default = {
static REGEX: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
let regex = REGEX.get_or_init(|| {
regex::Regex::new(r#"(\$\{ *(?P<name>.*?) *\})|(\$\( *(?P<cmd>.*?) *\))"#).unwrap()
});
default
.map(|x| {
try_replace(regex, &x, |caps: ®ex::Captures| {
let content = if let Some(name) = caps.name("name") {
crate::get(name.as_str())?
} else if let Some(cmd) = caps.name("cmd") {
let output = std::process::Command::new("sh")
.arg("-c")
.arg(cmd.as_str())
.output()?;
if output.status.success() {
String::from_utf8_lossy(&output.stdout).into_owned()
} else {
return Err(crate::Error::Command {
command: cmd.as_str().to_string(),
output,
});
}
} else {
unreachable!()
};
Ok(content)
})
.map(|x| x.to_string())
})
.transpose()?
};
env.get(var)
.or(default.as_ref())
.map(|x| parse(var, x))
.transpose()
}
#[doc(hidden)]
pub fn load_vec<T: std::str::FromStr>(
env: &HashMap<String, String>,
var: &str,
default: Option<String>,
separator: char,
) -> crate::Result<Option<Vec<T>>>
where
T::Err: ToString,
{
env.get(var)
.or(default.as_ref())
.map(|x| x.split(separator).map(|x| parse(var, x)).collect())
.transpose()
}
fn parse<T: std::str::FromStr>(var: &str, value: &str) -> crate::Result<T>
where
T::Err: ToString,
{
value
.parse::<T>()
.map_err(|e| crate::Error::parse::<T, _>(var, e.to_string()))
}
#[cfg(test)]
mod test {
#[test]
fn deserialize() {
#[derive(Debug, PartialEq, crate::Deserialize)]
#[envir(prefix = "ENV_")]
struct Test {
#[envir(name = "FOO")]
field1: String,
#[envir(default)]
field2: String,
#[envir(default = "field3")]
field3: String,
field4: u8,
#[envir(load_with = "load_field5")]
field5: String,
field6: Option<char>,
field7: Vec<String>,
#[envir(separator = ';')]
field8: Vec<usize>,
field9: Option<Vec<String>>,
}
fn load_field5(_: &std::collections::HashMap<String, String>) -> crate::Result<String> {
Ok("field5".to_string())
}
crate::set("ENV_FOO", "foo");
crate::set("ENV_FIELD4", 4);
crate::set("ENV_FIELD7", "value1,value2");
crate::set("ENV_FIELD8", "1;2");
let test = crate::from_env::<Test>().unwrap();
assert_eq!(
test,
Test {
field1: "foo".to_string(),
field2: String::new(),
field3: "field3".to_string(),
field4: 4,
field5: "field5".to_string(),
field6: None,
field7: vec!["value1".to_string(), "value2".to_string()],
field8: vec![1, 2],
field9: None,
}
);
}
#[test]
fn serialize() {
use crate::Serialize;
#[derive(Debug, PartialEq, crate::Serialize)]
#[envir(prefix = "ENV2_")]
struct Test2 {
#[envir(name = "FOO")]
field1: String,
field2: String,
field3: Vec<String>,
#[envir(separator = ';')]
field4: Vec<usize>,
field5: Option<Vec<String>>,
}
let test = Test2 {
field1: "field1".to_string(),
field2: "field2".to_string(),
field3: vec!["value1".to_string(), "value2".to_string()],
field4: vec![1, 2],
field5: None,
};
assert!(std::env::var("ENV2_FOO").is_err());
assert!(std::env::var("ENV2_FIELD2").is_err());
assert!(std::env::var("ENV2_FIELD3").is_err());
assert!(std::env::var("ENV2_FIELD3").is_err());
assert!(std::env::var("ENV2_FIELD5").is_err());
test.export();
assert_eq!(std::env::var("ENV2_FOO"), Ok("field1".to_string()));
assert_eq!(std::env::var("ENV2_FIELD2"), Ok("field2".to_string()));
assert_eq!(
std::env::var("ENV2_FIELD3"),
Ok("value1,value2".to_string())
);
assert_eq!(std::env::var("ENV2_FIELD4"), Ok("1;2".to_string()));
assert!(std::env::var("ENV2_FIELD5").is_err());
}
#[test]
fn nested() {
#[derive(Debug, PartialEq, crate::Deserialize, crate::Serialize)]
struct Test3 {
#[envir(nested)]
nested: Nested,
}
#[derive(Debug, PartialEq, crate::Deserialize, crate::Serialize)]
#[envir(prefix = "ENV3_")]
struct Nested {
foo: Option<String>,
}
let mut env = std::collections::HashMap::new();
env.insert("ENV3_FOO".to_string(), "foo".to_string());
let test = crate::from::<Test3>(&env).unwrap();
assert_eq!(
test,
Test3 {
nested: Nested {
foo: Some("foo".to_string()),
}
}
);
use crate::Serialize;
assert!(std::env::var("ENV3_FOO").is_err());
test.export();
assert_eq!(std::env::var("ENV3_FOO"), Ok("foo".to_string()));
}
#[test]
#[cfg_attr(
not(feature = "extrapolation"),
ignore = "feature `extrapolation` is disable"
)]
fn env() {
#[derive(Debug, PartialEq, crate::Deserialize)]
struct Test4 {
#[envir(default = "${HOME}/.config")]
config_dir: String,
#[envir(default = "$(echo test)")]
config_content: String,
}
let test = crate::from_env::<Test4>().unwrap();
assert_eq!(
test,
Test4 {
config_dir: format!("{}/.config", std::env::var("HOME").unwrap()),
config_content: "test\n".to_string(),
}
);
}
#[test]
#[cfg_attr(
not(feature = "extrapolation"),
ignore = "feature `extrapolation` is disable"
)]
fn extrapolation_error() {
#[derive(crate::Deserialize)]
struct Test {
#[envir(default = "${MISSING_ENV}/.config")]
_config_dir: String,
}
assert!(crate::from_env::<Test>().is_err());
#[derive(Debug, crate::Deserialize)]
struct Test2 {
#[envir(default = "$(cat ~/.config)")]
_config_content: String,
}
assert!(crate::from_env::<Test2>().is_err());
}
#[test]
fn skip_export() {
use crate::Serialize as _;
#[derive(crate::Serialize)]
struct Test {
#[envir(skip_export)]
#[allow(dead_code)]
skip_export: String,
}
let test = Test {
skip_export: "skip".to_string(),
};
test.export();
assert!(std::env::var("SKIP_EXPORT").is_err());
}
#[test]
fn skip_load() -> crate::Result {
#[derive(crate::Deserialize)]
struct Test {
#[envir(skip_load)]
home: String,
}
let test = crate::from_env::<Test>()?;
assert!(test.home.is_empty());
Ok(())
}
#[test]
fn skip() -> crate::Result {
use crate::Serialize as _;
#[derive(crate::Deserialize, crate::Serialize)]
struct Test {
#[envir(skip)]
home: String,
}
let test = crate::from_env::<Test>()?;
assert!(test.home.is_empty());
test.export();
assert!(!std::env::var("HOME").unwrap().is_empty());
Ok(())
}
#[test]
fn skip_export_if() -> crate::Result {
use crate::Serialize as _;
#[derive(Default, crate::Serialize)]
struct Test {
#[envir(skip_export_if = "String::is_empty")]
skip: String,
}
let test = Test {
skip: "skip_if_empty".to_string(),
};
test.export();
assert!(std::env::var("SKIP_IF_EMPTY").is_err());
Ok(())
}
}