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,
}
}
fn is_bool_type(ty: &Type) -> bool {
match ty {
Type::Path(type_path) if type_path.qself.is_none() => type_path.path.is_ident("bool"),
_ => false,
}
}
fn field_init(
field_name: &syn::Ident,
inner_type: &Type,
env_var: &str,
parser: Option<&syn::Path>,
) -> TokenStream {
if let Some(parser) = parser {
return quote! {
#field_name: env_var(#env_var)
.ok()
.and_then(|v| {
let parsed = #parser(&v);
if parsed.is_none() {
::tracing::warn!(
env_var = #env_var,
value = %v,
"failed to parse environment variable; ignoring",
);
}
parsed
})
};
}
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 if is_bool_type(inner_type) {
quote! {
#field_name: env_var(#env_var)
.ok()
.and_then(|v| match v.trim().to_ascii_lowercase().as_str() {
"true" | "1" | "yes" | "on" => Some(true),
"false" | "0" | "no" | "off" => Some(false),
_ => {
::tracing::warn!(
env_var = #env_var,
value = %v,
"failed to parse boolean environment variable; ignoring",
);
None
}
})
}
} 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
}
})
}
}
}
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 {
field_init(
field_name,
&field.inner_type,
env_var,
field.parser.as_ref(),
)
} else {
quote! { #field_name: None }
}
});
let override_tokens = generate_from_env_override(input);
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),*
}
}
#override_tokens
}
})
}
fn generate_from_env_override(input: &OptionsInput) -> TokenStream {
if !input.has_overridable_fields() {
return TokenStream::new();
}
let field_inits = input.fields.iter().map(|field| {
let field_name = &field.ident;
if let Some(override_var) = field.override_env_var() {
field_init(
field_name,
&field.inner_type,
&override_var,
field.parser.as_ref(),
)
} else {
quote! { #field_name: None }
}
});
quote! {
pub fn from_env_override() -> Self {
Self::from_env_override_vars(|key| ::std::env::var(key))
}
#[doc(hidden)]
pub fn from_env_override_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());
}
#[test]
fn from_env_override_generated_for_overridable_fields() {
let input: syn::DeriveInput = syn::parse_quote! {
#[options(layers(runtime, account, operation))]
pub struct TestOptions {
#[option(env = "MY_VAR_A", overridable)]
pub field_a: Option<bool>,
#[option(env = "MY_VAR_B")]
pub field_b: Option<u32>,
}
};
let parsed = OptionsInput::from_derive_input(&input).unwrap();
let tokens = generate_from_env(&parsed).unwrap().to_string();
assert!(tokens.contains("fn from_env_override ()"));
assert!(tokens.contains("fn from_env_override_vars ("));
assert!(tokens.contains("MY_VAR_A_OVERRIDE"));
assert!(!tokens.contains("MY_VAR_B_OVERRIDE"));
}
#[test]
fn no_from_env_override_without_overridable_fields() {
let input: syn::DeriveInput = syn::parse_quote! {
#[options(layers(runtime, account))]
pub struct TestOptions {
#[option(env = "MY_VAR_A")]
pub field_a: Option<String>,
}
};
let parsed = OptionsInput::from_derive_input(&input).unwrap();
let tokens = generate_from_env(&parsed).unwrap().to_string();
assert!(tokens.contains("from_env_vars"));
assert!(!tokens.contains("from_env_override"));
}
}