diff --git a/macro/src/gen.rs b/macro/src/gen.rs index 7984cfe..bd5f115 100644 --- a/macro/src/gen.rs +++ b/macro/src/gen.rs @@ -120,6 +120,19 @@ 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), .. } => { + let field = format!("{}::{}", input.name, f.name); + quote! { + confique::internal::from_env(#key, #field)? + } + } + FieldKind::Leaf { .. } => quote! { None }, + FieldKind::Nested { .. } => quote! { confique::Partial::from_env()? }, + } + }); + let fallbacks= input.fields.iter().map(|f| { let name = &f.name; if f.is_leaf() { @@ -174,6 +187,12 @@ fn gen_partial_mod(input: &ir::Input) -> TokenStream { } } + fn from_env() -> Result { + Ok(Self { + #( #field_names: #from_env_fields, )* + }) + } + fn with_fallback(self, fallback: Self) -> Self { Self { #( #field_names: #fallbacks, )* diff --git a/src/error.rs b/src/error.rs index 3c9d00d..017a417 100644 --- a/src/error.rs +++ b/src/error.rs @@ -28,6 +28,14 @@ pub(crate) enum ErrorInner { err: Box, }, + /// When deserialization via `env` fails. The string is what is passed to + /// `serde::de::Error::custom`. + EnvDeserialization { + field: String, + key: String, + msg: String, + }, + /// 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. @@ -53,6 +61,7 @@ impl std::error::Error for Error { ErrorInner::Io { err, .. } => Some(err), ErrorInner::Deserialization { err, .. } => Some(&**err), ErrorInner::MissingValue(_) + | ErrorInner::EnvDeserialization { .. } | ErrorInner::UnsupportedFileFormat { .. } | ErrorInner::MissingFileExtension { .. } | ErrorInner::MissingRequiredFile { .. } => None, @@ -81,6 +90,14 @@ impl fmt::Display for Error { ErrorInner::Deserialization { source: None, .. } => { std::write!(f, "failed to deserialize configuration") } + ErrorInner::EnvDeserialization { field, key, msg } => { + std::write!(f, + "failed to deserialize value `{}` from environment variable `{}`: {}", + field, + key, + msg, + ) + } ErrorInner::UnsupportedFileFormat { path } => { std::write!(f, "unknown configuration file format/extension: '{}'", diff --git a/src/internal.rs b/src/internal.rs index 3ee4e75..33816bf 100644 --- a/src/internal.rs +++ b/src/internal.rs @@ -1,4 +1,4 @@ -//! These functions are used by the code generated by the macro, but is not +//! These functions are used by the code generated by the macro, but are not //! intended to be used directly. None of this is covered by semver! Do not use //! any of this directly. @@ -24,3 +24,13 @@ pub fn prepend_missing_value_error(e: Error, prefix: &str) -> Error { e => e.into(), } } + +pub fn from_env<'de, T: serde::Deserialize<'de>>(key: &str, field: &str) -> Result { + crate::env::deserialize(std::env::var(key).ok()).map_err(|e| { + ErrorInner::EnvDeserialization { + key: key.into(), + field: field.into(), + msg: e.0, + }.into() + }) +} diff --git a/src/lib.rs b/src/lib.rs index 865f30c..24b31de 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -105,6 +105,15 @@ pub trait Partial: for<'de> Deserialize<'de> { /// values/fields set to `None`/being empty. fn default_values() -> Self; + /// Loads values from environment variables. This is only relevant for + /// fields annotated with `#[config(env = "...")]`: all fields not + /// annotated `env` will be `None`. + /// + /// If the env variable corresponding to a field is not set, that field is + /// `None`. If it is set but it failed to deserialize into the target type, + /// an error is returned. + fn from_env() -> Result; + /// Combines two partial configuration objects. `self` has a higher /// priority; missing values in `self` are filled with values in `fallback`, /// if they exist. The semantics of this method is basically like in