injectable_rs_macros/lib.rs
1//! Proc macros for the `injectable` DI framework.
2//!
3//! # Provided Macros
4//!
5//! All macros use `#[injectable(...)]` as the unified entry point:
6//!
7//! - `#[injectable]` on struct — field injection
8//! - `#[injectable]` on impl block — constructor injection
9//! - `#[injectable(ctor)]` on method — marks the injection constructor
10//! - `#[injectable(post_construct)]` on method — lifecycle: runs after construction
11//! - `#[injectable(pre_destruct)]` on method — lifecycle: runs before shutdown
12//! - `#[injectable(trait)]` on trait — dynamic dispatch support for `Inject<dyn Trait>`
13//! - `#[injectable(factory)]` on fn — transforms a function into a DI-compatible async factory
14//! - `bind!()` — creates a static binding from a trait to a concrete type
15
16#![forbid(unsafe_code)]
17
18mod attrs;
19mod container_macro;
20mod derive;
21mod factory_fn;
22mod injectable_impl;
23mod metadata;
24mod provider_gen;
25
26use proc_macro::TokenStream;
27use syn::parse_macro_input;
28
29/// Macro to create a static binding from a trait to a concrete type.
30///
31/// # Syntax
32///
33/// ```rust,ignore
34/// bind!(dyn EmailSender => SmtpSender);
35/// ```
36///
37/// This generates the `Extract` implementation for `Inject<dyn EmailSender>`
38/// that delegates to `SmtpSender::Provider`.
39#[proc_macro]
40pub fn bind(input: TokenStream) -> TokenStream {
41 let input = parse_macro_input!(input as derive::BindInput);
42 match derive::expand_bind(input) {
43 Ok(tokens) => tokens.into(),
44 Err(err) => err.to_compile_error().into(),
45 }
46}
47
48/// Macro for compile-time dependency graph validation and container construction.
49///
50/// This macro validates the dependency graph at compile time and emits
51/// `compile_error!()` if any issues are found (circular dependencies,
52/// scope mismatches, missing dependencies, duplicate registrations).
53#[proc_macro]
54pub fn container(input: TokenStream) -> TokenStream {
55 container_macro::expand_container(input.into()).into()
56}
57
58/// Unified DI attribute macro.
59///
60/// Applied to **structs**, **impl blocks**, **traits**, and **functions**
61/// depending on the sub-argument provided:
62///
63/// # On a struct (field injection)
64///
65/// ```rust,ignore
66/// #[injectable]
67/// pub struct UserService {
68/// db: Inject<Database>, // auto-injected
69/// #[injectable(inject)]
70/// pool: sqlx::SqlitePool, // requires annotation
71/// }
72/// ```
73///
74/// # On an impl block (constructor / lifecycle)
75///
76/// ```rust,ignore
77/// #[injectable]
78/// impl UserService {
79/// #[injectable(ctor)]
80/// pub fn new(db: Inject<Database>) -> Self { Self { db } }
81///
82/// #[injectable(post_construct)]
83/// async fn init(&self) -> HookResult { Ok(()) }
84///
85/// #[injectable(pre_destruct)]
86/// async fn shutdown(&self) -> HookResult { Ok(()) }
87/// }
88/// ```
89///
90/// # On a trait (`#[injectable(trait)]`)
91///
92/// Generates the infrastructure needed for `Inject<dyn Trait>` injection.
93/// Use `bind!(dyn Trait => Concrete)` to wire a concrete implementation.
94///
95/// ```rust,ignore
96/// #[injectable(trait)]
97/// pub trait EmailSender: Send + Sync {
98/// async fn send(&self, to: &str, body: &str);
99/// }
100///
101/// bind!(dyn EmailSender => SmtpSender);
102/// ```
103///
104/// # On a function (`#[injectable(factory)]`)
105///
106/// Transforms a function whose parameters carry `#[injectable(inject)]`
107/// annotations into an async factory compatible with
108/// `#[injectable(inject(use_factory_async = path))]`.
109///
110/// ```rust,ignore
111/// #[injectable(factory)]
112/// pub async fn make_client(
113/// #[injectable(inject)] cfg: Arc<AppConfig>,
114/// ) -> Result<reqwest::Client, reqwest::Error> {
115/// reqwest::Client::builder()
116/// .timeout(Duration::from_secs(cfg.timeout_secs))
117/// .build()
118/// }
119///
120/// #[injectable]
121/// pub struct WeatherService {
122/// #[injectable(inject(use_factory_async = self::make_client))]
123/// client: reqwest::Client,
124/// }
125/// ```
126///
127/// # Scope (on structs and impl blocks)
128///
129/// Type-safe idents (recommended):
130/// - `scope = Singleton` (default)
131/// - `scope = Transient`
132/// - `scope = RequestScoped`
133#[proc_macro_attribute]
134pub fn injectable(attr: TokenStream, item: TokenStream) -> TokenStream {
135 let attr2: proc_macro2::TokenStream = attr.into();
136 let item2: proc_macro2::TokenStream = item.into();
137
138 // Dispatch based on the first ident in the attribute argument.
139 match first_attr_ident(&attr2).as_deref() {
140 Some("trait") => {
141 // #[injectable(trait)] pub trait Foo { ... }
142 match syn::parse2::<syn::ItemTrait>(item2) {
143 Ok(input) => match derive::expand_injectable_trait(input) {
144 Ok(tokens) => tokens.into(),
145 Err(e) => e.to_compile_error().into(),
146 },
147 Err(_) => syn::Error::new(
148 proc_macro2::Span::call_site(),
149 "#[injectable(trait)] can only be applied to a trait",
150 )
151 .to_compile_error()
152 .into(),
153 }
154 }
155 Some("factory") => {
156 // #[injectable(factory)] fn make_something(...) -> T { ... }
157 match syn::parse2::<syn::ItemFn>(item2) {
158 Ok(input) => match factory_fn::expand_inject_fn(input) {
159 Ok(tokens) => tokens.into(),
160 Err(e) => e.to_compile_error().into(),
161 },
162 Err(_) => syn::Error::new(
163 proc_macro2::Span::call_site(),
164 "#[injectable(factory)] can only be applied to a function",
165 )
166 .to_compile_error()
167 .into(),
168 }
169 }
170 _ => {
171 // struct, impl block, or scope = ... forms
172 if let Ok(mut struct_item) = syn::parse2::<syn::ItemStruct>(item2.clone()) {
173 let normalized = normalize_scope_attr(attr2.clone());
174 let fake_derive_input = quote::quote! {
175 #[injectable(#normalized)]
176 #item2
177 };
178 match syn::parse2::<syn::DeriveInput>(fake_derive_input) {
179 Ok(input) => match derive::expand_derive_injectable(input) {
180 Ok(tokens) => {
181 // Strip #[injectable(...)] field attrs — they're inert after
182 // the macro has read them.
183 strip_inject_attrs_from_struct(&mut struct_item);
184 return quote::quote! { #struct_item #tokens }.into();
185 }
186 Err(e) => return e.to_compile_error().into(),
187 },
188 Err(e) => return e.to_compile_error().into(),
189 }
190 }
191
192 if syn::parse2::<syn::ItemImpl>(item2.clone()).is_ok() {
193 let normalized = normalize_scope_attr(attr2);
194 return match injectable_impl::expand_injectable_impl(normalized, item2) {
195 Ok(tokens) => tokens.into(),
196 Err(e) => e.to_compile_error().into(),
197 };
198 }
199
200 syn::Error::new(
201 proc_macro2::Span::call_site(),
202 "#[injectable] can only be applied to a struct, impl block, trait \
203 (with `#[injectable(trait)]`), or function (with `#[injectable(factory)]`)",
204 )
205 .to_compile_error()
206 .into()
207 }
208 }
209}
210
211/// Extract the first identifier from an attribute token stream.
212///
213/// Used to dispatch `#[injectable(trait)]` and `#[injectable(factory)]`
214/// before the normal struct/impl paths are tried.
215fn first_attr_ident(attr: &proc_macro2::TokenStream) -> Option<String> {
216 attr.clone().into_iter().next().and_then(|tt| {
217 if let proc_macro2::TokenTree::Ident(id) = tt {
218 Some(id.to_string())
219 } else {
220 None
221 }
222 })
223}
224
225/// Strip `#[injectable(...)]` attributes from all struct fields.
226///
227/// Field-level `#[injectable(inject)]` annotations are inert after the macro
228/// has read them for code generation. Without stripping them the compiler
229/// would see an unknown/duplicate attribute in the emitted struct.
230fn strip_inject_attrs_from_struct(s: &mut syn::ItemStruct) {
231 match &mut s.fields {
232 syn::Fields::Named(named) => {
233 for field in named.named.iter_mut() {
234 field.attrs.retain(|a| !a.path().is_ident("injectable"));
235 }
236 }
237 syn::Fields::Unnamed(unnamed) => {
238 for field in unnamed.unnamed.iter_mut() {
239 field.attrs.retain(|a| !a.path().is_ident("injectable"));
240 }
241 }
242 syn::Fields::Unit => {}
243 }
244}
245
246/// Rewrite `scope = Ident` → `scope = "string"` so the attrs parser
247/// can handle both type-safe idents and legacy strings.
248fn normalize_scope_attr(attr: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
249 use proc_macro2::{TokenStream, TokenTree};
250 use quote::quote;
251
252 let tokens: Vec<TokenTree> = attr.into_iter().collect();
253 let mut out = TokenStream::new();
254 let mut i = 0;
255 while i < tokens.len() {
256 // Panic Safety: i < tokens.len() guaranteed by while condition.
257 let tok = &tokens[i];
258 if let TokenTree::Ident(kw) = tok {
259 if kw == "scope"
260 && i + 2 < tokens.len()
261 && matches!(tokens.get(i + 1), Some(TokenTree::Punct(_)))
262 && matches!(tokens.get(i + 2), Some(TokenTree::Ident(_)))
263 {
264 if let Some(TokenTree::Ident(scope_ident)) = tokens.get(i + 2) {
265 let name = scope_ident.to_string();
266 let scope_str = match name.as_str() {
267 "Singleton" => "singleton",
268 "Transient" => "transient",
269 "RequestScoped" | "Request" => "request",
270 other => other,
271 };
272 let lit = proc_macro2::Literal::string(scope_str);
273 out.extend(quote! { scope = #lit });
274 i += 3;
275 continue;
276 }
277 }
278 }
279 out.extend(std::iter::once(tok.clone()));
280 i += 1;
281 }
282 out
283}