Skip to content

A normal macro for loading env vars and stuff I guess #11

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Apr 30, 2025
Merged
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ homepage = "https://github.com/init4tech/bin-base"
repository = "https://github.com/init4tech/bin-base"

[dependencies]
init4-from-env-derive = { path = "./from-env-derive" }

# Tracing
tracing = "0.1.40"
tracing-core = "0.1.33"
Expand Down
1 change: 1 addition & 0 deletions from-env-derive/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/target
17 changes: 17 additions & 0 deletions from-env-derive/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "init4-from-env-derive"
description = "The `FromEnv` derive macro"
version = "0.1.0"
edition = "2024"

[dependencies]
heck = "0.5.0"
proc-macro2 = "1.0.95"
quote = "1.0.40"
syn = { version = "2.0.100", features = ["full", "parsing"] }

[lib]
proc-macro = true

[dev-dependencies]
init4-bin-base = "0.2"
1 change: 1 addition & 0 deletions from-env-derive/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# init4-from-env-derive
221 changes: 221 additions & 0 deletions from-env-derive/src/field.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
use heck::ToPascalCase;
use proc_macro2::TokenStream;
use quote::quote;
use syn::{Ident, LitStr, spanned::Spanned};

/// A parsed Field of a struct
pub(crate) struct Field {
env_var: Option<LitStr>,
field_name: Option<Ident>,
field_type: syn::Type,

optional: bool,
infallible: bool,
desc: Option<String>,

_attrs: Vec<syn::Attribute>,

span: proc_macro2::Span,
}

impl TryFrom<&syn::Field> for Field {
type Error = syn::Error;

fn try_from(field: &syn::Field) -> Result<Self, syn::Error> {
let mut optional = false;
let mut env_var = None;
let mut infallible = false;
let mut desc = None;

field
.attrs
.iter()
.filter(|attr| attr.path().is_ident("from_env"))
.for_each(|attr| {
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("optional") {
optional = true;
return Ok(());
}
if meta.path.is_ident("var") {
env_var = Some(meta.value()?.parse::<LitStr>()?);
return Ok(());
}
if meta.path.is_ident("desc") {
desc = Some(meta.value()?.parse::<LitStr>()?.value());
return Ok(());
}
if meta.path.is_ident("infallible") {
infallible = true;
}
Ok(())
});
});

if desc.is_none() && env_var.is_some() {
return Err(syn::Error::new(
field.span(),
"Missing description for field. Use `#[from_env(desc = \"DESC\")]`",
));
}

let field_type = field.ty.clone();
let field_name = field.ident.clone();
let span = field.span();

Ok(Field {
env_var,
field_name,
field_type,
optional,
infallible,
desc,
_attrs: field
.attrs
.iter()
.filter(|attr| !attr.path().is_ident("from_env"))
.cloned()
.collect(),
span,
})
}
}

impl Field {
pub(crate) fn trait_name(&self) -> TokenStream {
self.env_var
.as_ref()
.map(|_| quote! { FromEnvVar })
.unwrap_or(quote! { FromEnv })
}

pub(crate) fn as_trait(&self) -> TokenStream {
let field_trait = self.trait_name();
let field_type = &self.field_type;

quote! { <#field_type as #field_trait> }
}

pub(crate) fn assoc_err(&self) -> TokenStream {
let as_trait = self.as_trait();

quote! { #as_trait::Error }
}

pub(crate) fn field_name(&self, idx: usize) -> Ident {
if let Some(field_name) = self.field_name.as_ref() {
return field_name.clone();
}

let n = format!("field_{}", idx);
syn::parse_str::<Ident>(&n)
.map_err(|_| syn::Error::new(self.span, "Failed to create field name"))
.unwrap()
}
Comment on lines +105 to +114
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, so for tuple structs, we'd get instead of the struct prop name, field_0, field_1...,field_n and so forth? This seems fine to me to support tuple structs although I'm not sure in which cases you'd want to do a config with a tuple struct.


/// Produces the name of the enum variant for the field
pub(crate) fn enum_variant_name(&self, idx: usize) -> Option<TokenStream> {
if self.infallible {
return None;
}

let n = self.field_name(idx).to_string().to_pascal_case();

let n: Ident = syn::parse_str::<Ident>(&n)
.map_err(|_| syn::Error::new(self.span, "Failed to create field name"))
.unwrap();

Some(quote! { #n })
}

/// Produces the variant, containing the error type
pub(crate) fn expand_enum_variant(&self, idx: usize) -> Option<TokenStream> {
let variant_name = self.enum_variant_name(idx)?;
let var_name_str = variant_name.to_string();
let assoc_err = self.assoc_err();

Some(quote! {
#[doc = "Error for "]
#[doc = #var_name_str]
#variant_name(#assoc_err)
})
}

/// Produces the a line for the `inventory` function
/// of the form
/// items.push(...);
/// or
/// items.extend(...);
pub(crate) fn expand_env_item_info(&self) -> TokenStream {
let description = self.desc.clone().unwrap_or_default();
let optional = self.optional;

if let Some(env_var) = &self.env_var {
let var_name = env_var.value();

return quote! {
items.push(&EnvItemInfo {
var: #var_name,
description: #description,
optional: #optional,
});
};
}

let field_ty = &self.field_type;
quote! {
items.extend(
<#field_ty as FromEnv>::inventory()
);
}
}

pub(crate) fn expand_variant_display(&self, idx: usize) -> Option<TokenStream> {
let variant_name = self.enum_variant_name(idx)?;

Some(quote! {
Self::#variant_name(err) => err.fmt(f)
})
}

pub(crate) fn expand_variant_source(&self, idx: usize) -> Option<TokenStream> {
let variant_name = self.enum_variant_name(idx)?;

Some(quote! {
Self::#variant_name(err) => Some(err)
})
}

pub(crate) fn expand_item_from_env(&self, err_ident: &Ident, idx: usize) -> TokenStream {
// Produces code fo the following form:
// ```rust
// // EITHER
// let field_name = env::var(#self.env_var.unwrap()).map_err(|e| e.map(#ErroEnum::FieldName))?;

// // OR
// let field_name = FromEnvVar::from_env_var(#self.env_var.unwrap()).map_err(|e| e.map(#ErroEnum::FieldName))?;

// // OR
// let field_name = FromEnv::from_env().map_err()?;
//```
let variant = self.enum_variant_name(idx);
let field_name = self.field_name(idx);

let fn_invoc = if let Some(ref env_var) = self.env_var {
quote! { FromEnvVar::from_env_var(#env_var) }
} else {
quote! { FromEnv::from_env() }
};

let map_line = if self.infallible {
quote! { FromEnvErr::infallible_into }
} else {
quote! { |e| e.map(#err_ident::#variant) }
};

quote! {
let #field_name = #fn_invoc
.map_err(#map_line)?;
}
}
}
Loading