Merge pull request #22 from cyphersnake/10-lists-maps-from-env

Add parse_env attribute
This commit is contained in:
Lukas Kalbertodt
2022-11-06 12:29:43 +01:00
committed by GitHub
10 changed files with 255 additions and 50 deletions

View File

@@ -4,7 +4,9 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
### Added
- `parse_env` attribute for custom parsing of environment variables (allows you
to load lists and other complex objects from env vars).
## [0.2.0] - 2022-10-21
### Added

63
examples/parse_env.rs Normal file
View File

@@ -0,0 +1,63 @@
#![allow(dead_code)]
use confique::Config;
use std::{collections::HashSet, num::NonZeroU64, path::PathBuf, str::FromStr, convert::Infallible};
#[derive(Debug, Config)]
struct Conf {
#[config(env = "PATHS", parse_env = confique::env::parse::list_by_colon)]
paths: HashSet<PathBuf>,
#[config(env = "PORTS", parse_env = confique::env::parse::list_by_comma)]
ports: Vec<u16>,
#[config(env = "NAMES", parse_env = confique::env::parse::list_by_sep::<'|', _, _>)]
names: Vec<String>,
#[config(env = "TIMEOUT", parse_env = NonZeroU64::from_str)]
timeout_seconds: NonZeroU64,
#[config(env = "FORMATS", parse_env = parse_formats)]
formats: Vec<Format>,
}
#[derive(Debug, serde::Deserialize)]
enum Format {
Env,
Toml,
Json5,
Yaml,
}
/// Example custom parser.
fn parse_formats(input: &str) -> Result<Vec<Format>, Infallible> {
let mut result = Vec::new();
if input.contains("toml") {
result.push(Format::Toml);
}
if input.contains("env") {
result.push(Format::Env);
}
if input.contains("yaml") {
result.push(Format::Yaml);
}
if input.contains("json5") {
result.push(Format::Json5);
}
Ok(result)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
std::env::set_var("PATHS", "/bin/ls,/usr/local/bin,/usr/bin/ls");
std::env::set_var("PORTS", "8080,8888,8000");
std::env::set_var("NAMES", "Alex|Peter|Mary");
std::env::set_var("TIMEOUT", "100");
std::env::set_var("FORMATS", "json5,yaml;.env");
println!("{:#?}", Conf::builder().env().load()?);
Ok(())
}

View File

@@ -149,14 +149,18 @@ fn gen_partial_mod(input: &ir::Input) -> TokenStream {
let from_env_fields = input.fields.iter().map(|f| {
match &f.kind {
FieldKind::Leaf { env: Some(key), deserialize_with, .. } => {
FieldKind::Leaf { env: Some(key), deserialize_with, parse_env, .. } => {
let field = format!("{}::{}", input.name, f.name);
match deserialize_with {
None => quote! {
match (parse_env, deserialize_with) {
(None, None) => quote! {
confique::internal::from_env(#key, #field)?
},
Some(d) => quote! {
confique::internal::from_env_with(#key, #field, #d)?
(None, Some(deserialize_with)) => quote! {
confique::internal::from_env_with_deserializer(
#key, #field, #deserialize_with)?
},
(Some(parse_env), _) => quote! {
confique::internal::from_env_with_parser(#key, #field, #parse_env)?
},
}
}

View File

@@ -24,6 +24,7 @@ pub(crate) enum FieldKind {
Leaf {
env: Option<String>,
deserialize_with: Option<syn::Path>,
parse_env: Option<syn::Path>,
kind: LeafKind,
},

View File

@@ -65,32 +65,30 @@ impl Field {
FieldKind::Nested { ty: field.ty }
} else {
match unwrap_option(&field.ty) {
None => FieldKind::Leaf {
env: attrs.env,
deserialize_with: attrs.deserialize_with,
kind: LeafKind::Required {
default: attrs.default,
ty: field.ty,
},
},
Some(inner) => {
if attrs.default.is_some() {
return Err(Error::new(
field.ident.span(),
"optional fields (type `Option<_>`) cannot have default \
values (`#[config(default = ...)]`)",
));
}
if attrs.env.is_none() && attrs.parse_env.is_some() {
return Err(Error::new(
field.ident.span(),
"A `parse_env` attribute, cannot be provided without the `env` attribute",
));
}
FieldKind::Leaf {
env: attrs.env,
deserialize_with: attrs.deserialize_with,
kind: LeafKind::Optional {
inner_ty: inner.clone(),
},
}
}
let kind = match unwrap_option(&field.ty) {
Some(_) if attrs.default.is_some() => {
return Err(Error::new(
field.ident.span(),
"optional fields (type `Option<_>`) cannot have default \
values (`#[config(default = ...)]`)",
));
},
Some(inner) => LeafKind::Optional { inner_ty: inner.clone() },
None => LeafKind::Required { default: attrs.default, ty: field.ty },
};
FieldKind::Leaf {
env: attrs.env,
deserialize_with: attrs.deserialize_with,
parse_env: attrs.parse_env,
kind,
}
};
@@ -242,6 +240,10 @@ fn extract_internal_attrs(
duplicate_if!(out.env.is_some());
out.env = Some(key);
}
InternalAttr::ParseEnv(path) => {
duplicate_if!(out.parse_env.is_some());
out.parse_env = Some(path);
}
InternalAttr::DeserializeWith(path) => {
duplicate_if!(out.deserialize_with.is_some());
out.deserialize_with = Some(path);
@@ -259,6 +261,7 @@ struct InternalAttrs {
default: Option<Expr>,
env: Option<String>,
deserialize_with: Option<syn::Path>,
parse_env: Option<syn::Path>,
}
enum InternalAttr {
@@ -266,6 +269,7 @@ enum InternalAttr {
Default(Expr),
Env(String),
DeserializeWith(syn::Path),
ParseEnv(syn::Path),
}
impl InternalAttr {
@@ -274,6 +278,7 @@ impl InternalAttr {
Self::Nested => "nested",
Self::Default(_) => "default",
Self::Env(_) => "env",
Self::ParseEnv(_) => "parse_env",
Self::DeserializeWith(_) => "deserialize_with",
}
}
@@ -310,6 +315,14 @@ impl Parse for InternalAttr {
}
}
"parse_env" => {
let _: Token![=] = input.parse()?;
let path: syn::Path = input.parse()?;
assert_empty_or_comma(input)?;
Ok(Self::ParseEnv(path))
}
"deserialize_with" => {
let _: Token![=] = input.parse()?;
let path: syn::Path = input.parse()?;

View File

@@ -11,6 +11,7 @@ use serde::de::IntoDeserializer;
/// module. Gets converted into `ErrorKind::EnvDeserialization` before reaching
/// the real public API.
#[derive(PartialEq, Eq)]
#[doc(hidden)]
pub struct DeError(pub(crate) String);
impl std::error::Error for DeError {}
@@ -38,6 +39,7 @@ impl serde::de::Error for DeError {
/// Deserializer type. Semantically private (see `DeError`).
#[doc(hidden)]
pub struct Deserializer {
value: String,
}
@@ -166,3 +168,61 @@ mod tests {
assert_eq!(de("-123.456"), Ok(-123.456f64));
}
}
/// Functions for the `#[config(parse_env = ...)]` attribute.
pub mod parse {
use std::str::FromStr;
/// Splits the environment variable by separator `SEP`, parses each element
/// with [`FromStr`] and collects everything via [`FromIterator`].
///
/// To avoid having to specify the separator via `::<>` syntax, see the
/// other functions in this module.
///
/// [`FromStr`]: std::str::FromStr
/// [`FromIterator`]: std::iter::FromIterator
///
///
/// # Example
///
/// ```
/// use confique::Config;
///
/// #[derive(Debug, confique::Config)]
/// struct Conf {
/// #[config(env = "PORTS", parse_env = confique::env::parse::list_by_sep::<',', _, _>)]
/// ports: Vec<u16>,
/// }
///
/// std::env::set_var("PORTS", "8080,8000,8888");
/// let conf = Conf::builder().env().load()?;
/// assert_eq!(conf.ports, vec![8080, 8000, 8888]);
/// # Ok::<_, confique::Error>(())
/// ```
pub fn list_by_sep<const SEP: char, T, C>(input: &str) -> Result<C, <T as FromStr>::Err>
where
T: FromStr,
C: FromIterator<T>,
{
input.split(SEP).map(T::from_str).collect()
}
macro_rules! specify_fn_wrapper {
($fn_name:ident, $sep:literal) => {
#[doc = concat!("Like [`list_by_sep`] with `", $sep, "` separator.")]
pub fn $fn_name<T, C>(input: &str) -> Result<C, <T as FromStr>::Err>
where
T: FromStr,
C: FromIterator<T>,
{
list_by_sep::<$sep, _, _>(input)
}
}
}
specify_fn_wrapper!(list_by_comma, ',');
specify_fn_wrapper!(list_by_semicolon, ';');
specify_fn_wrapper!(list_by_colon, ':');
specify_fn_wrapper!(list_by_space, ' ');
}

View File

@@ -50,6 +50,13 @@ pub(crate) enum ErrorInner {
msg: String,
},
/// When a custom `parse_env` function fails.
EnvParseError {
field: String,
key: String,
err: Box<dyn std::error::Error + Send + Sync>,
},
/// Returned by the [`Source`] impls for `Path` and `PathBuf` if the file
/// extension is not supported by confique or if the corresponding Cargo
/// feature of confique was not enabled.
@@ -71,6 +78,7 @@ impl std::error::Error for Error {
ErrorInner::MissingValue(_) => None,
ErrorInner::EnvNotUnicode { .. } => None,
ErrorInner::EnvDeserialization { .. } => None,
ErrorInner::EnvParseError { err, .. } => Some(&**err),
ErrorInner::UnsupportedFileFormat { .. } => None,
ErrorInner::MissingFileExtension { .. } => None,
ErrorInner::MissingRequiredFile { .. } => None,
@@ -107,6 +115,10 @@ impl fmt::Display for Error {
std::write!(f, "failed to deserialize value `{field}` from \
environment variable `{key}`: {msg}")
}
ErrorInner::EnvParseError { field, key, err } => {
std::write!(f, "failed to parse environment variable `{key}` into \
field `{field}`: {err}")
}
ErrorInner::UnsupportedFileFormat { path } => {
std::write!(f,
"unknown configuration file format/extension: '{}'",

View File

@@ -4,7 +4,6 @@
use crate::{error::ErrorInner, Error};
pub fn deserialize_default<I, O>(src: I) -> Result<O, serde::de::value::Error>
where
I: for<'de> serde::de::IntoDeserializer<'de>,
@@ -37,29 +36,53 @@ pub fn map_err_prefix_path<T>(res: Result<T, Error>, prefix: &str) -> Result<T,
})
}
macro_rules! get_env_var {
($key:expr, $field:expr) => {
match std::env::var($key) {
Err(std::env::VarError::NotPresent) => return Ok(None),
Err(std::env::VarError::NotUnicode(_)) => {
let err = ErrorInner::EnvNotUnicode {
key: $key.into(),
field: $field.into(),
};
return Err(err.into());
}
Ok(s) => s,
}
};
}
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))
from_env_with_deserializer(key, field, |de| T::deserialize(de))
}
pub fn from_env_with<T>(
pub fn from_env_with_parser<T, E: std::error::Error + Send + Sync + 'static>(
key: &str,
field: &str,
parse: fn(&str) -> Result<T, E>,
) -> Result<Option<T>, Error> {
let v = get_env_var!(key, field);
parse(&v)
.map(Some)
.map_err(|err| {
ErrorInner::EnvParseError {
field: field.to_owned(),
key: key.to_owned(),
err: Box::new(err),
}.into()
})
}
pub fn from_env_with_deserializer<T>(
key: &str,
field: &str,
deserialize: fn(crate::env::Deserializer) -> Result<T, crate::env::DeError>,
) -> Result<Option<T>, Error> {
let s = match std::env::var(key) {
Err(std::env::VarError::NotPresent) => return Ok(None),
Err(std::env::VarError::NotUnicode(_)) => {
let err = ErrorInner::EnvNotUnicode {
key: key.into(),
field: field.into(),
};
return Err(err.into());
}
Ok(s) => s,
};
let s = get_env_var!(key, field);
match deserialize(crate::env::Deserializer::new(s)) {
Ok(v) => Ok(Some(v)),

View File

@@ -175,7 +175,7 @@ use serde::Deserialize;
pub mod internal;
mod builder;
mod env;
pub mod env;
mod error;
pub mod meta;
@@ -320,8 +320,14 @@ pub use crate::{
/// - **`#[config(deserialize_with = path::to::function)]`**: like
/// [serde's `deserialize_with` attribute][serde-deser].
///
/// [serde-deser]: https://serde.rs/field-attrs.html#deserialize_with
/// - **`#[config(parse_env = path::to::function)]`**: function used to parse
/// environment variables. Mostly useful if you need to parse lists or
/// other complex objects from env vars. Function needs signature
/// `fn(&str) -> Result<T, impl std::error::Error>` where `T` is the type of
/// the field.. Can only be present if the `env` attribute is present. Also
/// see [`env::parse`].
///
/// [serde-deser]: https://serde.rs/field-attrs.html#deserialize_with
///
/// ## Special types for leaf fields
///

View File

@@ -1,5 +1,7 @@
use std::{collections::HashMap, net::IpAddr, path::PathBuf};
use std::{collections::HashMap, net::IpAddr, path::PathBuf, convert::Infallible};
use pretty_assertions::assert_eq;
use serde::Deserialize;
use confique::{meta, Config, Partial};
@@ -110,6 +112,9 @@ mod full {
#[config(env = "ENV_TEST_FULL_3")]
optional: Option<PathBuf>,
#[config(env = "ENV_TEST_FULL_4", parse_env = parse_dummy_collection)]
env_collection: DummyCollection,
}
}
@@ -247,6 +252,14 @@ fn full() {
kind: meta::LeafKind::Optional,
},
},
meta::Field {
name: "env_collection",
doc: &[],
kind: meta::FieldKind::Leaf {
env: Some("ENV_TEST_FULL_4"),
kind: meta::LeafKind::Required { default: None },
},
},
],
},
},
@@ -266,6 +279,7 @@ fn full() {
assert_eq!(def.env.required, None);
assert_eq!(def.env.with_default, Some(8080));
assert_eq!(def.env.optional, None);
assert_eq!(def.env.env_collection, None);
}
#[derive(Debug, PartialEq)]
@@ -275,12 +289,19 @@ pub(crate) fn deserialize_dummy<'de, D>(deserializer: D) -> Result<Dummy, D::Err
where
D: serde::Deserializer<'de>,
{
use serde::Deserialize;
let s = String::deserialize(deserializer)?;
Ok(Dummy(format!("dummy {s}")))
}
#[derive(Debug, PartialEq, Deserialize)]
struct DummyCollection(Vec<String>);
pub(crate) fn parse_dummy_collection(input: &str) -> Result<DummyCollection, Infallible> {
Ok(DummyCollection(
input.split(',').map(ToString::to_string).collect(),
))
}
// This only makes sure this compiles and doesn't result in any "cannot infer
// type" problems.
#[test]