Skip to content

Commit 39df2f9

Browse files
committed
wip
1 parent f0caa0b commit 39df2f9

File tree

6 files changed

+222
-0
lines changed

6 files changed

+222
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
/target
2+
from-env-macro/target
23
Cargo.lock

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ axum = "0.8.1"
4444
serial_test = "3.2.0"
4545
signal-hook = "0.3.17"
4646
tokio = { version = "1.43.0", features = ["macros"] }
47+
from-env-macro = { path = "./from-env-macro" }
4748

4849
[features]
4950
default = ["alloy"]

from-env-macro/Cargo.toml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[package]
2+
name = "from-env-macro"
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 = { path = ".." }

from-env-macro/src/field.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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+
span: proc_macro2::Span,
13+
}
14+
15+
impl From<&syn::Field> for Field {
16+
fn from(field: &syn::Field) -> Self {
17+
let env_var = field
18+
.attrs
19+
.iter()
20+
.filter_map(|attr| attr.meta.require_list().ok())
21+
.find(|attr| attr.path.is_ident("from_env_var"))
22+
.and_then(|attr| attr.parse_args::<LitStr>().ok());
23+
24+
let field_type = field.ty.clone();
25+
let field_name = field.ident.clone();
26+
let span = field.span();
27+
28+
Field {
29+
env_var,
30+
field_name,
31+
field_type,
32+
span,
33+
}
34+
}
35+
}
36+
37+
impl Field {
38+
pub(crate) fn enum_variant_name(&self, idx: usize) -> TokenStream {
39+
eprintln!("Field name: {:?}", self.field_name);
40+
let n = if let Some(field_name) = self.field_name.as_ref() {
41+
field_name.to_string()
42+
} else {
43+
format!("Field{}", idx)
44+
}
45+
.to_pascal_case();
46+
47+
syn::parse_str::<Ident>(&n)
48+
.map_err(|_| syn::Error::new(self.span, "Failed to create field name"))
49+
.unwrap();
50+
51+
eprintln!("Field name: {}", n);
52+
53+
return quote! { #n };
54+
}
55+
56+
pub(crate) fn expand_enum_variant(&self, idx: usize) -> TokenStream {
57+
let field_name = self.enum_variant_name(idx);
58+
let field_type = &self.field_type;
59+
let field_trait = if self.env_var.is_some() {
60+
quote! { FromEnv }
61+
} else {
62+
quote! { FromEnvErr }
63+
};
64+
quote! {
65+
#[doc = "Error for" #field_name]
66+
#field_name(<#field_type as #field_trait>::Error)
67+
}
68+
}
69+
}

from-env-macro/src/lib.rs

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
use heck::ToPascalCase;
2+
use proc_macro::TokenStream as Ts;
3+
use proc_macro2::TokenStream;
4+
use quote::quote;
5+
use syn::{DeriveInput, parse_macro_input, spanned::Spanned};
6+
7+
mod field;
8+
use field::Field;
9+
10+
#[proc_macro_derive(FromEnv, attributes(from_env_var))]
11+
pub fn derive(input: Ts) -> Ts {
12+
let input = parse_macro_input!(input as DeriveInput);
13+
14+
if !matches!(input.data, syn::Data::Struct(_)) {
15+
syn::Error::new(
16+
input.ident.span(),
17+
"FromEnv can only be derived for structs",
18+
)
19+
.to_compile_error();
20+
};
21+
22+
let syn::Data::Struct(data) = &input.data else {
23+
unreachable!()
24+
};
25+
26+
if matches!(data.fields, syn::Fields::Unit) {
27+
syn::Error::new(
28+
input.ident.span(),
29+
"FromEnv can only be derived for structs with fields",
30+
)
31+
.to_compile_error();
32+
}
33+
34+
expand_mod(&input).into()
35+
}
36+
37+
fn expand_mod(input: &syn::DeriveInput) -> TokenStream {
38+
let expanded_impl = expand_struct(input);
39+
let expanded_error = expand_error(input);
40+
41+
quote! {
42+
#[automatically_derived]
43+
const _: () = {
44+
use ::init4_bin_base::utils::from_env::{FromEnv, FromEnvErr, FromEnvVar};
45+
46+
#expanded_impl
47+
48+
#expanded_error
49+
};
50+
}
51+
}
52+
53+
fn expand_struct(input: &syn::DeriveInput) -> TokenStream {
54+
let struct_name = &input.ident;
55+
56+
quote! {
57+
58+
// #[automatically_derived]
59+
// impl FromEnv for #struct_name {
60+
61+
// }
62+
}
63+
}
64+
65+
fn error_ident(input: &syn::DeriveInput) -> syn::Ident {
66+
let error_name = format!("{}Error", input.ident);
67+
syn::parse_str::<syn::Ident>(&error_name)
68+
.map_err(|_| {
69+
syn::Error::new(input.ident.span(), "Failed to parse error ident").to_compile_error()
70+
})
71+
.unwrap()
72+
}
73+
74+
fn expand_error(input: &syn::DeriveInput) -> TokenStream {
75+
let error_ident = error_ident(input);
76+
77+
let syn::Data::Struct(data) = &input.data else {
78+
unreachable!()
79+
};
80+
let fields = match &data.fields {
81+
syn::Fields::Named(fields) => fields.named.iter().map(Field::from).collect::<Vec<_>>(),
82+
syn::Fields::Unnamed(fields) => fields.unnamed.iter().map(Field::from).collect::<Vec<_>>(),
83+
syn::Fields::Unit => unreachable!(),
84+
};
85+
86+
let error_variants = fields
87+
.iter()
88+
.enumerate()
89+
.map(|(idx, field)| field.expand_enum_variant(idx))
90+
.collect::<Vec<_>>();
91+
92+
let variant_names = fields
93+
.iter()
94+
.enumerate()
95+
.map(|(idx, field)| field.enum_variant_name(idx))
96+
.collect::<Vec<_>>();
97+
98+
let s = quote! {
99+
#[doc("Generated error type for `FromEnv`")]
100+
#[derive(Debug, PartialEq, Eq)]
101+
pub enum #error_ident {
102+
#(#error_variants),*
103+
}
104+
105+
impl ::core::error::Error for #error_ident {
106+
fn source(&self) -> Option<&(dyn ::core::any::Any + ::core::marker::Send + 'static)> {
107+
match self {
108+
#(
109+
Self::#variant_names(err) => Some(err),
110+
)*
111+
}
112+
}
113+
114+
fn description(&self) -> &str {
115+
match self {
116+
#(
117+
Self::#variant_names(err) => err.description(),
118+
)*
119+
}
120+
}
121+
}
122+
};
123+
eprintln!("{s}");
124+
s
125+
}

tests/macro.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
use from_env_macro::FromEnv;
2+
3+
#[derive(FromEnv, Debug)]
4+
pub struct FromEnvTest {
5+
pub tony: String,
6+
7+
#[from_env_var("FIELD2")]
8+
pub charles: u64,
9+
}

0 commit comments

Comments
 (0)