Skip to content

Commit df9abb0

Browse files
authored
A normal macro for loading env vars and stuff I guess (#11)
* wip * lol * fix: toml and stuff * chore: remove gitignore line * feat: require desc * feat: crate name * fix: use FromEnv in this crate * fix: remove eprintln * fix: remove infallible errors * fix: re-export names * chore: finish docs * fix: builder * fix: fmt * chore: tuple and nested test * chore: delete commented lines * docs: add a bunch :)
1 parent e2c1c51 commit df9abb0

File tree

13 files changed

+915
-182
lines changed

13 files changed

+915
-182
lines changed

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ homepage = "https://github.com/init4tech/bin-base"
1313
repository = "https://github.com/init4tech/bin-base"
1414

1515
[dependencies]
16+
init4-from-env-derive = { path = "./from-env-derive" }
17+
1618
# Tracing
1719
tracing = "0.1.40"
1820
tracing-core = "0.1.33"

from-env-derive/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/target

from-env-derive/Cargo.toml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[package]
2+
name = "init4-from-env-derive"
3+
description = "The `FromEnv` derive macro"
4+
version = "0.1.0"
5+
edition = "2024"
6+
7+
[dependencies]
8+
heck = "0.5.0"
9+
proc-macro2 = "1.0.95"
10+
quote = "1.0.40"
11+
syn = { version = "2.0.100", features = ["full", "parsing"] }
12+
13+
[lib]
14+
proc-macro = true
15+
16+
[dev-dependencies]
17+
init4-bin-base = "0.2"

from-env-derive/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# init4-from-env-derive

from-env-derive/src/field.rs

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
use heck::ToPascalCase;
2+
use proc_macro2::TokenStream;
3+
use quote::quote;
4+
use syn::{Ident, LitStr, spanned::Spanned};
5+
6+
/// A parsed Field of a struct
7+
pub(crate) struct Field {
8+
env_var: Option<LitStr>,
9+
field_name: Option<Ident>,
10+
field_type: syn::Type,
11+
12+
optional: bool,
13+
infallible: bool,
14+
desc: Option<String>,
15+
16+
_attrs: Vec<syn::Attribute>,
17+
18+
span: proc_macro2::Span,
19+
}
20+
21+
impl TryFrom<&syn::Field> for Field {
22+
type Error = syn::Error;
23+
24+
fn try_from(field: &syn::Field) -> Result<Self, syn::Error> {
25+
let mut optional = false;
26+
let mut env_var = None;
27+
let mut infallible = false;
28+
let mut desc = None;
29+
30+
field
31+
.attrs
32+
.iter()
33+
.filter(|attr| attr.path().is_ident("from_env"))
34+
.for_each(|attr| {
35+
let _ = attr.parse_nested_meta(|meta| {
36+
if meta.path.is_ident("optional") {
37+
optional = true;
38+
return Ok(());
39+
}
40+
if meta.path.is_ident("var") {
41+
env_var = Some(meta.value()?.parse::<LitStr>()?);
42+
return Ok(());
43+
}
44+
if meta.path.is_ident("desc") {
45+
desc = Some(meta.value()?.parse::<LitStr>()?.value());
46+
return Ok(());
47+
}
48+
if meta.path.is_ident("infallible") {
49+
infallible = true;
50+
}
51+
Ok(())
52+
});
53+
});
54+
55+
if desc.is_none() && env_var.is_some() {
56+
return Err(syn::Error::new(
57+
field.span(),
58+
"Missing description for field. Use `#[from_env(desc = \"DESC\")]`",
59+
));
60+
}
61+
62+
let field_type = field.ty.clone();
63+
let field_name = field.ident.clone();
64+
let span = field.span();
65+
66+
Ok(Field {
67+
env_var,
68+
field_name,
69+
field_type,
70+
optional,
71+
infallible,
72+
desc,
73+
_attrs: field
74+
.attrs
75+
.iter()
76+
.filter(|attr| !attr.path().is_ident("from_env"))
77+
.cloned()
78+
.collect(),
79+
span,
80+
})
81+
}
82+
}
83+
84+
impl Field {
85+
pub(crate) fn trait_name(&self) -> TokenStream {
86+
self.env_var
87+
.as_ref()
88+
.map(|_| quote! { FromEnvVar })
89+
.unwrap_or(quote! { FromEnv })
90+
}
91+
92+
pub(crate) fn as_trait(&self) -> TokenStream {
93+
let field_trait = self.trait_name();
94+
let field_type = &self.field_type;
95+
96+
quote! { <#field_type as #field_trait> }
97+
}
98+
99+
pub(crate) fn assoc_err(&self) -> TokenStream {
100+
let as_trait = self.as_trait();
101+
102+
quote! { #as_trait::Error }
103+
}
104+
105+
pub(crate) fn field_name(&self, idx: usize) -> Ident {
106+
if let Some(field_name) = self.field_name.as_ref() {
107+
return field_name.clone();
108+
}
109+
110+
let n = format!("field_{}", idx);
111+
syn::parse_str::<Ident>(&n)
112+
.map_err(|_| syn::Error::new(self.span, "Failed to create field name"))
113+
.unwrap()
114+
}
115+
116+
/// Produces the name of the enum variant for the field
117+
pub(crate) fn enum_variant_name(&self, idx: usize) -> Option<TokenStream> {
118+
if self.infallible {
119+
return None;
120+
}
121+
122+
let n = self.field_name(idx).to_string().to_pascal_case();
123+
124+
let n: Ident = syn::parse_str::<Ident>(&n)
125+
.map_err(|_| syn::Error::new(self.span, "Failed to create field name"))
126+
.unwrap();
127+
128+
Some(quote! { #n })
129+
}
130+
131+
/// Produces the variant, containing the error type
132+
pub(crate) fn expand_enum_variant(&self, idx: usize) -> Option<TokenStream> {
133+
let variant_name = self.enum_variant_name(idx)?;
134+
let var_name_str = variant_name.to_string();
135+
let assoc_err = self.assoc_err();
136+
137+
Some(quote! {
138+
#[doc = "Error for "]
139+
#[doc = #var_name_str]
140+
#variant_name(#assoc_err)
141+
})
142+
}
143+
144+
/// Produces the a line for the `inventory` function
145+
/// of the form
146+
/// items.push(...);
147+
/// or
148+
/// items.extend(...);
149+
pub(crate) fn expand_env_item_info(&self) -> TokenStream {
150+
let description = self.desc.clone().unwrap_or_default();
151+
let optional = self.optional;
152+
153+
if let Some(env_var) = &self.env_var {
154+
let var_name = env_var.value();
155+
156+
return quote! {
157+
items.push(&EnvItemInfo {
158+
var: #var_name,
159+
description: #description,
160+
optional: #optional,
161+
});
162+
};
163+
}
164+
165+
let field_ty = &self.field_type;
166+
quote! {
167+
items.extend(
168+
<#field_ty as FromEnv>::inventory()
169+
);
170+
}
171+
}
172+
173+
pub(crate) fn expand_variant_display(&self, idx: usize) -> Option<TokenStream> {
174+
let variant_name = self.enum_variant_name(idx)?;
175+
176+
Some(quote! {
177+
Self::#variant_name(err) => err.fmt(f)
178+
})
179+
}
180+
181+
pub(crate) fn expand_variant_source(&self, idx: usize) -> Option<TokenStream> {
182+
let variant_name = self.enum_variant_name(idx)?;
183+
184+
Some(quote! {
185+
Self::#variant_name(err) => Some(err)
186+
})
187+
}
188+
189+
pub(crate) fn expand_item_from_env(&self, err_ident: &Ident, idx: usize) -> TokenStream {
190+
// Produces code fo the following form:
191+
// ```rust
192+
// // EITHER
193+
// let field_name = env::var(#self.env_var.unwrap()).map_err(|e| e.map(#ErroEnum::FieldName))?;
194+
195+
// // OR
196+
// let field_name = FromEnvVar::from_env_var(#self.env_var.unwrap()).map_err(|e| e.map(#ErroEnum::FieldName))?;
197+
198+
// // OR
199+
// let field_name = FromEnv::from_env().map_err()?;
200+
//```
201+
let variant = self.enum_variant_name(idx);
202+
let field_name = self.field_name(idx);
203+
204+
let fn_invoc = if let Some(ref env_var) = self.env_var {
205+
quote! { FromEnvVar::from_env_var(#env_var) }
206+
} else {
207+
quote! { FromEnv::from_env() }
208+
};
209+
210+
let map_line = if self.infallible {
211+
quote! { FromEnvErr::infallible_into }
212+
} else {
213+
quote! { |e| e.map(#err_ident::#variant) }
214+
};
215+
216+
quote! {
217+
let #field_name = #fn_invoc
218+
.map_err(#map_line)?;
219+
}
220+
}
221+
}

0 commit comments

Comments
 (0)