Skip to main content

ironic_macros/
lib.rs

1//! Procedural macros for declaring Ironic application metadata.
2
3use proc_macro::TokenStream;
4
5mod controller;
6mod from_row;
7mod injectable;
8mod jwt_guard;
9mod merge;
10mod module;
11mod openapi;
12mod routes;
13mod serializable;
14mod r#test;
15mod ws_gateway;
16
17#[proc_macro_derive(Injectable, attributes(injectable))]
18/// Derives a dependency-injection provider definition.
19pub fn derive_injectable(input: TokenStream) -> TokenStream {
20    injectable::expand(input.into())
21        .unwrap_or_else(syn::Error::into_compile_error)
22        .into()
23}
24
25#[proc_macro_derive(Module, attributes(module, global))]
26/// Derives a static application module definition.
27pub fn derive_module(input: TokenStream) -> TokenStream {
28    module::expand(input.into())
29        .unwrap_or_else(syn::Error::into_compile_error)
30        .into()
31}
32
33#[proc_macro_derive(FromRow, attributes(sqlx))]
34/// Derives `sqlx::FromRow` for a named-field struct with optional column rename,
35/// JSON-deserialization, and default-value support.
36pub fn derive_from_row(input: TokenStream) -> TokenStream {
37    from_row::expand(input.into())
38        .unwrap_or_else(syn::Error::into_compile_error)
39        .into()
40}
41
42#[proc_macro_derive(Merge)]
43/// Derives a `merge_into(&mut self)` method that applies `Option<T>` values
44/// from `self` onto a target of the same type.
45pub fn derive_merge(input: TokenStream) -> TokenStream {
46    merge::expand(input.into())
47        .unwrap_or_else(syn::Error::into_compile_error)
48        .into()
49}
50
51#[proc_macro_derive(OpenApiSchema, attributes(serde, garde))]
52/// Derives an `OpenAPI` schema for a named-field struct.
53///
54/// Reads `#[serde(rename)]`, `#[serde(skip)]`, `#[serde(default)]`, and
55/// `#[garde(...)]` attributes to produce richer schema metadata.
56pub fn derive_openapi_schema(input: TokenStream) -> TokenStream {
57    openapi::expand(syn::parse_macro_input!(input as syn::DeriveInput))
58        .unwrap_or_else(syn::Error::into_compile_error)
59        .into()
60}
61
62#[proc_macro_attribute]
63/// Generates the complete JWT auth pipeline (claims, principal, authenticator,
64/// guard, and middleware) from a concise declaration.
65///
66/// # Example
67///
68/// ```ignore
69/// #[ironic::jwt_guard(
70///     secret = std::env::var("JWT_SECRET").expect("JWT_SECRET must be set"),
71///     claims = UserClaims { sub: String, exp: u64 },
72///     principal = User { id: String },
73///     map = |c: UserClaims| -> Result<User, ironic::auth::AuthError> {
74///         Ok(User { id: c.sub })
75///     }
76/// )]
77/// pub struct Auth;
78///
79/// // Use in application setup:
80/// app.middleware(Auth::auth_middleware());
81/// // And on controllers:
82/// #[guard(Auth::AuthGuard)]
83/// ```
84pub fn jwt_guard(attribute: TokenStream, item: TokenStream) -> TokenStream {
85    jwt_guard::expand(attribute.into(), item.into())
86        .unwrap_or_else(syn::Error::into_compile_error)
87        .into()
88}
89
90#[proc_macro_attribute]
91/// Declares a controller and its path prefix.
92pub fn controller(attribute: TokenStream, item: TokenStream) -> TokenStream {
93    controller::expand(attribute.into(), item.into())
94        .unwrap_or_else(syn::Error::into_compile_error)
95        .into()
96}
97
98#[proc_macro_attribute]
99/// Collects route metadata from an inherent controller implementation.
100pub fn routes(attribute: TokenStream, item: TokenStream) -> TokenStream {
101    routes::expand(attribute.into(), item.into())
102        .unwrap_or_else(syn::Error::into_compile_error)
103        .into()
104}
105
106#[proc_macro_derive(Serializable, attributes(exclude, expose))]
107/// Derives a `field_rules()` method from `#[exclude]` and `#[expose(role = "...")]`
108/// field attributes.
109pub fn derive_serializable(input: TokenStream) -> TokenStream {
110    serializable::expand(syn::parse_macro_input!(input as syn::DeriveInput))
111        .unwrap_or_else(syn::Error::into_compile_error)
112        .into()
113}
114
115macro_rules! marker_attribute {
116    ($($name:ident),+ $(,)?) => {$ (
117        #[doc = concat!("Route metadata consumed by [`macro@routes`].")]
118        #[proc_macro_attribute]
119        pub fn $name(_attribute: TokenStream, item: TokenStream) -> TokenStream {
120            item
121        }
122    )+};
123}
124
125#[proc_macro_attribute]
126/// Declares a WebSocket gateway and its path.
127pub fn web_socket_gateway(attribute: TokenStream, item: TokenStream) -> TokenStream {
128    ws_gateway::expand(attribute.into(), item.into())
129        .unwrap_or_else(syn::Error::into_compile_error)
130        .into()
131}
132
133marker_attribute!(
134    get,
135    post,
136    put,
137    patch,
138    delete,
139    head,
140    options,
141    body,
142    form,
143    query,
144    param,
145    header,
146    decorator,
147    pipe,
148    subscribe_message,
149    guard,
150    interceptor,
151    middleware,
152    cache,
153    cron,
154    interval,
155    timeout,
156    api,
157    resp,
158);
159
160/// Wraps an async test function with Ironic's Tokio runtime, removing the
161/// need for users to depend on `tokio` or use `#[tokio::test]`.
162///
163/// # Usage
164///
165/// ```ignore
166/// use ironic::test;
167///
168/// #[test]
169/// async fn my_test() {
170///     // test body — no tokio dependency needed
171/// }
172/// ```
173#[proc_macro_attribute]
174pub fn r#test(attribute: TokenStream, item: TokenStream) -> TokenStream {
175    r#test::expand(attribute.into(), item.into())
176        .unwrap_or_else(syn::Error::into_compile_error)
177        .into()
178}
179
180/// Configures an async entry point with Ironic's Tokio runtime.
181#[proc_macro_attribute]
182pub fn main(attribute: TokenStream, item: TokenStream) -> TokenStream {
183    if !attribute.is_empty() {
184        return syn::Error::new(
185            proc_macro2::Span::call_site(),
186            "`#[ironic::main]` does not accept arguments",
187        )
188        .into_compile_error()
189        .into();
190    }
191    let mut function = syn::parse_macro_input!(item as syn::ItemFn);
192    if function.sig.asyncness.is_none() {
193        let error = syn::Error::new_spanned(
194            function.sig.fn_token,
195            "`#[ironic::main]` requires an async function",
196        )
197        .into_compile_error();
198        return quote::quote!(#error #function).into();
199    }
200    if !function.sig.inputs.is_empty() {
201        let error = syn::Error::new_spanned(
202            &function.sig.inputs,
203            "`#[ironic::main]` entry points cannot accept arguments",
204        )
205        .into_compile_error();
206        return quote::quote!(#error #function).into();
207    }
208    function.sig.asyncness = None;
209    let body = function.block;
210    function.block = Box::new(syn::parse_quote!({
211        ::ironic::__private::block_on(async move #body)
212    }));
213    quote::quote!(#function).into()
214}