use crate::parse::OptionsInput;
use crate::Result;
use proc_macro2::TokenStream;
use quote::quote;
use syn::{PathArguments, Type};
fn is_vec_type(ty: &Type) -> bool {
match ty {
Type::Path(type_path) if type_path.qself.is_none() => {
type_path.path.segments.last().is_some_and(|seg| {
seg.ident == "Vec" && matches!(seg.arguments, PathArguments::AngleBracketed(_))
})
}
_ => false,
}
}
pub fn generate_from_env(input: &OptionsInput) -> Result<TokenStream> {
if !input.has_env_fields() {
return Ok(TokenStream::new());
}
let struct_name = &input.name;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let field_inits = input.fields.iter().map(|field| {
let field_name = &field.ident;
if let Some(ref env_var) = field.env_var {
let inner_type = &field.inner_type;
if is_vec_type(inner_type) {
quote! {
#field_name: env_var(#env_var)
.ok()
.map(|v| v.split(',')
.filter_map(|s| {
let trimmed = s.trim();
match trimmed.parse() {
Ok(parsed) => Some(parsed),
Err(_) => {
::tracing::warn!(
env_var = #env_var,
value = trimmed,
"failed to parse element from environment variable; skipping",
);
None
}
}
})
.collect())
}
} else {
quote! {
#field_name: env_var(#env_var)
.ok()
.and_then(|v| match v.parse() {
Ok(parsed) => Some(parsed),
Err(_) => {
::tracing::warn!(
env_var = #env_var,
value = %v,
"failed to parse environment variable; ignoring",
);
None
}
})
}
}
} else {
quote! { #field_name: None }
}
});
Ok(quote! {
#[automatically_derived]
impl #impl_generics #struct_name #ty_generics #where_clause {
pub fn from_env() -> Self {
Self::from_env_vars(|key| ::std::env::var(key))
}
#[doc(hidden)]
pub fn from_env_vars(env_var: impl Fn(&str) -> ::std::result::Result<String, ::std::env::VarError>) -> Self {
Self {
#(#field_inits),*
}
}
}
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parse::OptionsInput;
use quote::quote;
#[test]
fn from_env_generated_when_env_fields_present() {
let input: syn::DeriveInput = syn::parse_quote! {
#[options(layers(runtime, account))]
pub struct TestOptions {
#[option(env = "MY_VAR_A")]
pub field_a: Option<String>,
pub field_b: Option<u32>,
}
};
let parsed = OptionsInput::from_derive_input(&input).unwrap();
let tokens = generate_from_env(&parsed).unwrap();
let expected = quote! {
#[automatically_derived]
impl TestOptions {
pub fn from_env() -> Self {
Self::from_env_vars(|key| ::std::env::var(key))
}
#[doc(hidden)]
pub fn from_env_vars(env_var: impl Fn(&str) -> ::std::result::Result<String, ::std::env::VarError>) -> Self {
Self {
field_a: env_var("MY_VAR_A")
.ok()
.and_then(|v| match v.parse() {
Ok(parsed) => Some(parsed),
Err(_) => {
::tracing::warn!(
env_var = "MY_VAR_A",
value = %v,
"failed to parse environment variable; ignoring",
);
None
}
}),
field_b: None
}
}
}
};
assert_eq!(expected.to_string(), tokens.to_string());
}
#[test]
fn no_output_when_no_env_fields() {
let input: syn::DeriveInput = syn::parse_quote! {
#[options(layers(runtime, account))]
pub struct TestOptions {
pub field_a: Option<String>,
}
};
let parsed = OptionsInput::from_derive_input(&input).unwrap();
let tokens = generate_from_env(&parsed).unwrap();
assert!(tokens.is_empty());
}
}