Skip to main content

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}