Add parse_env attribute to confique::Config macro

Since there is no established format for representing collections within env variables, to enable their support, the feature with a custom parser attribute added.

Add helpers for parsing env variables into collections

Improve rustdoc for `env_utils` functions

Improve `parse_env` example
This commit is contained in:
Cyphersnake
2022-10-24 17:25:10 +04:00
parent 337fb3204f
commit cb8f879b92
10 changed files with 290 additions and 42 deletions

View File

@@ -128,7 +128,6 @@ impl<'de> serde::Deserializer<'de> for Deserializer {
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -166,3 +165,105 @@ mod tests {
assert_eq!(de("-123.456"), Ok(-123.456f64));
}
}
/// This module contains helper methods to simplify configuration via environment variables
pub mod env_utils {
use std::str::FromStr;
/// Helper function to parse any type that implements [`std::str::FromStr`]
/// into a collection that implements [`std::iter::FromIterator`], spliting original string by
/// const char separator.
///
/// # To Vec
/// ```
/// use crate::confique::Config;
/// #[derive(Debug, confique::Config)]
/// struct Conf {
/// #[config(
/// env = "PORTS",
/// parse_env = confique::env_utils::to_collection_by_char_separator::<',', _, _>
/// )]
/// ports: Vec<u16>
/// }
///
/// std::env::set_var("PORTS", "8080,8000,8888");
/// println!("{:?}", Conf::builder().env().load().unwrap())
/// ```
///
/// # To HashSet
/// ```
/// use crate::confique::Config;
/// #[derive(Debug, confique::Config)]
/// struct Conf {
/// #[config(
/// env = "PATHS",
/// parse_env = confique::env_utils::to_collection_by_char_separator::<';', _, _>
/// )]
/// paths: std::collections::HashSet<std::path::PathBuf>,
/// }
///
/// std::env::set_var("PATHS", "/bin;/user/bin;/home/user/.cargo/bin");
/// println!("{:?}", Conf::builder().env().load().unwrap())
/// ```
pub fn to_collection_by_char_separator<
const SEPARATOR: char,
T: FromStr,
C: FromIterator<Result<T, <T as FromStr>::Err>>,
>(
input: &str,
) -> C {
input.split(SEPARATOR.to_owned()).map(T::from_str).collect()
}
macro_rules! specify_fn_wrapper {
($symbol_name:ident, $symbol:tt) => {
::paste::paste! {
/// Helper function to parse any type that implements [`std::str::FromStr`]
/// into a collection that implements [`std::iter::FromIterator`], spliting original string by
#[doc = stringify!($symbol_name)]
/// .
///
/// # To Vec
/// ```
/// use crate::confique::Config;
/// #[derive(Debug, confique::Config)]
/// struct Conf {
/// #[config(
/// env = "PORTS",
#[doc = concat!(" parse_env = confique::env_utils::", stringify!([<to_collection_by_ $symbol_name>],))]
/// )]
/// ports: Vec<u16>
/// }
///
#[doc = concat!("std::env::set_var(\"PORTS\", \"8080", $symbol, "8000", $symbol, "8888", "\");")]
/// println!("{:#?}", Conf::builder().env().load().unwrap())
/// ```
///
/// # To HashSet
/// ```
/// use crate::confique::Config;
/// #[derive(Debug, confique::Config)]
/// struct Conf {
/// #[config(
/// env = "PATHS",
#[doc = concat!(" parse_env = confique::env_utils::", stringify!([<to_collection_by_ $symbol_name>],))]
/// )]
/// paths: std::collections::HashSet<std::path::PathBuf>,
/// }
///
#[doc = concat!("std::env::set_var(\"PATHS\", \"/bin", $symbol, "/user/bin", $symbol, "/home/user/.cargo/bin", "\");")]
/// println!("{:#?}", Conf::builder().env().load().unwrap())
/// ```
pub fn [<to_collection_by_ $symbol_name>]<T: FromStr, C: FromIterator<Result<T, <T as FromStr>::Err>>>(
input: &str,
) -> C {
to_collection_by_char_separator::<$symbol, _, _>(input)
}
}
}
}
specify_fn_wrapper!(comma, ',');
specify_fn_wrapper!(semicolon, ';');
specify_fn_wrapper!(space, ' ');
}

View File

@@ -2,8 +2,9 @@
//! intended to be used directly. None of this is covered by semver! Do not use
//! any of this directly.
use crate::{error::ErrorInner, Error};
use std::fmt::Debug;
use crate::{error::ErrorInner, Error};
pub fn deserialize_default<I, O>(src: I) -> Result<O, serde::de::value::Error>
where
@@ -41,10 +42,29 @@ pub fn from_env<'de, T: serde::Deserialize<'de>>(
key: &str,
field: &str,
) -> Result<Option<T>, Error> {
from_env_with(key, field, |de| T::deserialize(de))
deserialize_from_env_with(key, field, |de| T::deserialize(de))
}
pub fn from_env_with<T>(
pub fn parse_from_env_with<T, E: Debug>(
key: &str,
field: &str,
parse: fn(&str) -> Result<T, E>,
) -> Result<Option<T>, Error> {
from_env::<String>(key, field)?
.as_deref()
.map(parse)
.transpose()
.map_err(|err| {
ErrorInner::EnvDeserialization {
field: field.to_owned(),
key: key.to_owned(),
msg: format!("Error while parse: {:?}", err),
}
.into()
})
}
pub fn deserialize_from_env_with<T>(
key: &str,
field: &str,
deserialize: fn(crate::env::Deserializer) -> Result<T, crate::env::DeError>,

View File

@@ -176,6 +176,7 @@ pub mod internal;
mod builder;
mod env;
pub use env::env_utils;
mod error;
pub mod meta;
@@ -322,6 +323,9 @@ pub use crate::{
///
/// [serde-deser]: https://serde.rs/field-attrs.html#deserialize_with
///
/// - **`#[config(from_env = path::to::function, env = "KEY")]`**: like
/// deserialize_with` attribute, but only for fields from an environment variable.
/// Can only be present if the `env` attribute is present
///
/// ## Special types for leaf fields
///