mirror of
https://github.com/OMGeeky/confique.git
synced 2025-12-27 06:29:27 +01:00
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:
@@ -4,7 +4,8 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- `parse_env` attribute which is using for parse env variable in arbitrary type
|
||||
|
||||
## [0.2.0] - 2022-10-21
|
||||
### Added
|
||||
|
||||
@@ -28,6 +28,7 @@ yaml = ["serde_yaml"]
|
||||
[dependencies]
|
||||
confique-macro = { version = "=0.0.5", path = "macro" }
|
||||
json5 = { version = "0.4.1", optional = true }
|
||||
paste = "1.0.9"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_yaml = { version = "0.8", optional = true }
|
||||
toml = { version = "0.5", optional = true }
|
||||
|
||||
83
examples/parse_env.rs
Normal file
83
examples/parse_env.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use confique::{
|
||||
env_utils::{
|
||||
to_collection_by_char_separator, to_collection_by_comma, to_collection_by_semicolon,
|
||||
},
|
||||
Config,
|
||||
};
|
||||
use std::{collections::HashSet, num::NonZeroU64, path::PathBuf, str::FromStr};
|
||||
|
||||
#[derive(Debug, Config)]
|
||||
struct Conf {
|
||||
#[config(
|
||||
env = "PATHS",
|
||||
parse_env = to_collection_by_comma
|
||||
)]
|
||||
paths: HashSet<PathBuf>,
|
||||
#[config(
|
||||
env = "PORTS",
|
||||
parse_env = to_collection_by_semicolon
|
||||
)]
|
||||
ports: Vec<u16>,
|
||||
#[config(
|
||||
env = "NAMES",
|
||||
parse_env = to_collection_by_char_separator::<'|', _, _>
|
||||
)]
|
||||
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,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Error {}
|
||||
|
||||
fn parse_formats(input: &str) -> Result<Vec<Format>, Error> {
|
||||
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",
|
||||
"Mervinc Harmon|Alfreda Valenzuela|Arlen Cabrera|Damon Rice|Willie Schwartz",
|
||||
);
|
||||
std::env::set_var("TIMEOUT", "100");
|
||||
std::env::set_var("FORMATS", "json5,yaml;.env");
|
||||
|
||||
println!("{:#?}", Conf::builder().env().load()?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -147,22 +147,28 @@ 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, .. } => {
|
||||
let field = format!("{}::{}", input.name, f.name);
|
||||
match deserialize_with {
|
||||
None => quote! {
|
||||
confique::internal::from_env(#key, #field)?
|
||||
},
|
||||
Some(d) => quote! {
|
||||
confique::internal::from_env_with(#key, #field, #d)?
|
||||
},
|
||||
}
|
||||
let from_env_fields = input.fields.iter().map(|f| match &f.kind {
|
||||
FieldKind::Leaf {
|
||||
env: Some(key),
|
||||
deserialize_with,
|
||||
parse_env,
|
||||
..
|
||||
} => {
|
||||
let field = format!("{}::{}", input.name, f.name);
|
||||
match (parse_env, deserialize_with) {
|
||||
(None, None) => quote! {
|
||||
confique::internal::from_env(#key, #field)?
|
||||
},
|
||||
(None, Some(deserialize_with)) => quote! {
|
||||
confique::internal::deserialize_from_env_with(#key, #field, #deserialize_with)?
|
||||
},
|
||||
(Some(parse_env), None) | (Some(parse_env), Some(_)) => quote! {
|
||||
confique::internal::parse_from_env_with(#key, #field, #parse_env)?
|
||||
},
|
||||
}
|
||||
FieldKind::Leaf { .. } => quote! { None },
|
||||
FieldKind::Nested { .. } => quote! { confique::Partial::from_env()? },
|
||||
}
|
||||
FieldKind::Leaf { .. } => quote! { None },
|
||||
FieldKind::Nested { .. } => quote! { confique::Partial::from_env()? },
|
||||
});
|
||||
|
||||
let fallbacks = input.fields.iter().map(|f| {
|
||||
|
||||
@@ -24,6 +24,7 @@ pub(crate) enum FieldKind {
|
||||
Leaf {
|
||||
env: Option<String>,
|
||||
deserialize_with: Option<syn::Path>,
|
||||
parse_env: Option<syn::Path>,
|
||||
kind: LeafKind,
|
||||
},
|
||||
|
||||
|
||||
@@ -65,32 +65,27 @@ 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() {
|
||||
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,
|
||||
parse_env: attrs.parse_env,
|
||||
kind: match unwrap_option(&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 = ...)]`)",
|
||||
));
|
||||
}
|
||||
|
||||
FieldKind::Leaf {
|
||||
env: attrs.env,
|
||||
deserialize_with: attrs.deserialize_with,
|
||||
kind: LeafKind::Optional {
|
||||
inner_ty: inner.clone(),
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(inner) => LeafKind::Optional { inner_ty: inner.clone() },
|
||||
None => LeafKind::Required { default: attrs.default, ty: field.ty },
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -242,6 +237,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 +258,7 @@ struct InternalAttrs {
|
||||
default: Option<Expr>,
|
||||
env: Option<String>,
|
||||
deserialize_with: Option<syn::Path>,
|
||||
parse_env: Option<syn::Path>,
|
||||
}
|
||||
|
||||
enum InternalAttr {
|
||||
@@ -266,6 +266,7 @@ enum InternalAttr {
|
||||
Default(Expr),
|
||||
Env(String),
|
||||
DeserializeWith(syn::Path),
|
||||
ParseEnv(syn::Path),
|
||||
}
|
||||
|
||||
impl InternalAttr {
|
||||
@@ -274,6 +275,7 @@ impl InternalAttr {
|
||||
Self::Nested => "nested",
|
||||
Self::Default(_) => "default",
|
||||
Self::Env(_) => "env",
|
||||
Self::ParseEnv(_) => "parse_env",
|
||||
Self::DeserializeWith(_) => "deserialize_with",
|
||||
}
|
||||
}
|
||||
@@ -310,6 +312,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()?;
|
||||
|
||||
103
src/env.rs
103
src/env.rs
@@ -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, ' ');
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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
|
||||
///
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use std::{collections::HashMap, net::IpAddr, path::PathBuf};
|
||||
|
||||
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<const SEPARATOR: char>(Vec<String>);
|
||||
|
||||
pub(crate) fn parse_dummy_collection<const SEPARATOR: char>(input: &str) -> Result<DummyCollection<SEPARATOR>, String> {
|
||||
Ok(DummyCollection(
|
||||
input.split(SEPARATOR).map(ToString::to_string).collect(),
|
||||
))
|
||||
}
|
||||
|
||||
// This only makes sure this compiles and doesn't result in any "cannot infer
|
||||
// type" problems.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user