Skip to main content

webx_macros/
lib.rs

1//! Procedural macros for the rust-webx framework (package `rust-webx-macros`,
2//! imported as `webx_macros`).
3//!
4//! - `#[get]` / `#[post]` / `#[put]` / `#[delete]` / `#[endpoint]` — route registration
5//! - `#[handler]` — handler auto-registration via `inventory`
6//! - `#[webx::main]` / `#[webx::main(embed)]` — application entry + asset embedding
7//! - `#[webx::embed_assets]` — asset table include for integration tests
8//! - `#[derive(WebxRequestMeta)]` — OpenAPI parameter metadata
9//! - `#[claims]` / `#[authorize]` — authentication and authorization
10//! - `#[display]` / `#[description]` — endpoint name and description resource
11//!   references
12//! - `#[category]` — optional display grouping for endpoint listings
13//!
14//! Endpoint text priority is `#[display]` / `#[description]` first, then the
15//! `impl` block's `///` doc comment. A doc comment's first line is the name
16//! (OpenAPI `summary`); the full comment is the description. When there is no
17//! text at all, the name is derived from the request type.
18//!
19//! `#[display(key = "endpoint.blog.list.name")]` stores a resource key; the
20//! translation lives in a resource file and is resolved by the host's
21//! `Culture.Default`. `#[display("博客列表")]` stores a literal instead.
22
23mod app_main;
24mod claims;
25mod endpoint;
26mod handler;
27mod request_meta;
28
29use proc_macro::TokenStream;
30use quote::quote;
31use syn::parse_quote;
32
33/// Application entry — ASP.NET Core `Main` analogue (`#[tokio::main]`).
34///
35/// | Attribute | Baked-in wwwroot |
36/// |-----------|------------------|
37/// | `#[webx::main]` | No — disk SPA only (`.use_spa` / auto-detect) |
38/// | `#[webx::main(embed)]` | Yes — requires `build.rs` `web_root(...).build()` |
39///
40/// ```ignore
41/// // Disk-only (no build.rs embed step)
42/// #[webx::main]
43/// async fn main() {
44///     Host::builder().use_spa("wwwroot").build().run().await.unwrap();
45/// }
46///
47/// // Baked into the binary
48/// // build.rs: webx::builder().web_root("wwwroot").build()?;
49/// #[webx::main(embed)]
50/// async fn main() {
51///     Host::builder().use_spa("wwwroot").build().run().await.unwrap();
52/// }
53/// ```
54///
55/// - **`web_root` (build.rs)** — which files are compiled into the executable  
56/// - **`.use_spa(...)` (runtime)** — disk directory consulted first (operator overrides)  
57/// - Without `build.rs` / without `(embed)` — nothing is baked in
58#[proc_macro_attribute]
59pub fn main(attr: TokenStream, item: TokenStream) -> TokenStream {
60    app_main::main_impl(attr, item)
61}
62
63/// Include the `OUT_DIR` asset table (for integration tests that never run `main`).
64#[proc_macro_attribute]
65pub fn embed_assets(attr: TokenStream, item: TokenStream) -> TokenStream {
66    app_main::embed_assets_impl(attr, item)
67}
68
69// ---------------------------------------------------------------------------
70// Route macros: full form + shortcuts
71// ---------------------------------------------------------------------------
72
73/// Full form: marks an IRequest impl with HTTP method and route path.
74///
75/// ```ignore
76/// #[endpoint(HttpGet, "/users/{id}")]
77/// impl IRequest<UserModel> for GetUserRequest {}
78/// ```
79#[proc_macro_attribute]
80pub fn endpoint(attr: TokenStream, item: TokenStream) -> TokenStream {
81    endpoint::endpoint_impl(attr, item)
82}
83
84/// Shortcut: registers an IRequest impl at GET /path.
85///
86/// ```ignore
87/// #[get("/users/{id}")]
88/// impl IRequest<UserModel> for GetUserRequest {}
89/// ```
90#[proc_macro_attribute]
91pub fn get(attr: TokenStream, item: TokenStream) -> TokenStream {
92    endpoint::shortcut_get(attr, item)
93}
94
95/// Shortcut: registers an IRequest impl at POST /path.
96///
97/// ```ignore
98/// #[post("/users")]
99/// impl IRequest<UserModel> for CreateUserRequest {}
100/// ```
101#[proc_macro_attribute]
102pub fn post(attr: TokenStream, item: TokenStream) -> TokenStream {
103    endpoint::shortcut_post(attr, item)
104}
105
106/// Shortcut: registers an IRequest impl at PUT /path.
107///
108/// ```ignore
109/// #[put("/users/{id}")]
110/// impl IRequest<UserModel> for UpdateUserRequest {}
111/// ```
112#[proc_macro_attribute]
113pub fn put(attr: TokenStream, item: TokenStream) -> TokenStream {
114    endpoint::shortcut_put(attr, item)
115}
116
117/// Shortcut: registers an IRequest impl at DELETE /path.
118///
119/// ```ignore
120/// #[delete("/users/{id}")]
121/// impl IRequest<String> for DeleteUserRequest {}
122/// ```
123#[proc_macro_attribute]
124pub fn delete(attr: TokenStream, item: TokenStream) -> TokenStream {
125    endpoint::shortcut_delete(attr, item)
126}
127
128// ---------------------------------------------------------------------------
129// Handler auto-registration
130// ---------------------------------------------------------------------------
131
132/// Auto-registers an IRequestHandler implementation at compile time via inventory.
133///
134/// Place on `impl IRequestHandler<T, R> for Handler` blocks.
135/// The handler struct MUST implement `Default`.
136///
137/// ```ignore
138/// #[handler]
139/// #[async_trait]
140/// impl IRequestHandler<HelloRequest, String> for HelloHandler {
141///     async fn handle(&mut self, _req: HelloRequest) -> Result<String> { ... }
142/// }
143/// ```
144#[proc_macro_attribute]
145pub fn handler(attr: TokenStream, item: TokenStream) -> TokenStream {
146    handler::handler_impl(attr, item)
147}
148
149// ---------------------------------------------------------------------------
150// Parameter binding (OpenAPI metadata via WebxRequestMeta derive helpers)
151// ---------------------------------------------------------------------------
152//
153// `frompath`, `fromquery`, `frombody`, `fromform`, `fromfile`, `fromheader`
154// and `fromclaim` are helper attributes for `#[derive(WebxRequestMeta)]` — not
155// standalone attribute macros (avoids rustc conflicts on struct fields).
156
157/// Registers parameter metadata and the request binder from `#[from*]` fields.
158///
159/// Markers: `#[frompath]`, `#[fromquery]`, `#[frombody]`, `#[fromform]`,
160/// `#[fromfile]`, `#[fromheader("Name")]`, `#[fromclaim("key")]`. Every marker
161/// except `#[frombody]` accepts an optional source key.
162///
163/// Unmarked fields follow the request method: `GET`/`HEAD`/`DELETE`/`OPTIONS`
164/// bind from the query string, then the route; `POST`/`PUT`/`PATCH` bind from
165/// the JSON body, then the form, then the query string. Headers and claims are
166/// opt-in only. `#[request(queryall)]` forces unmarked fields to the query.
167#[proc_macro_derive(
168    WebxRequestMeta,
169    attributes(
170        frompath, fromquery, frombody, fromform, fromfile, fromheader, fromclaim, request
171    )
172)]
173pub fn webx_request_meta(input: TokenStream) -> TokenStream {
174    request_meta::derive_request_meta(input)
175}
176
177// ---------------------------------------------------------------------------
178// Declaration macros
179// ---------------------------------------------------------------------------
180
181// ---------------------------------------------------------------------------
182// Claims attribute
183// ---------------------------------------------------------------------------
184
185/// Marks a request struct as carrying authentication claims.
186///
187/// Appends a `#[serde(skip)] pub claims: Option<Box<dyn IClaims>>` field to
188/// the struct and generates an inherent `set_claims` method that shadows the
189/// blanket no-op `IClaimsCarrier::set_claims`. The dispatcher injects claims
190/// into the request *before* calling `IRequestHandler::handle`.
191///
192/// Must be the outermost attribute so that `#[derive(...)]` sees the injected
193/// field:
194///
195/// ```ignore
196/// #[claims]
197/// #[derive(Default, Deserialize)]
198/// pub struct CreateCommentRequest {
199///     pub blog_id: i32,
200///     pub content: String,
201/// }
202/// ```
203#[proc_macro_attribute]
204pub fn claims(attr: TokenStream, item: TokenStream) -> TokenStream {
205    claims::claims_impl(attr, item)
206}
207
208// ---------------------------------------------------------------------------
209// Authorization attribute (declarative, handled by emit_endpoint)
210// ---------------------------------------------------------------------------
211
212/// Declares authorization requirements on a route.
213///
214/// Each attribute is one rule and **every** rule must be satisfied. Inside a
215/// rule, `roles` / `permissions` are any-of lists and the two dimensions are
216/// ANDed; a bare `#[authorize]` requires a valid authenticated principal.
217///
218/// ```ignore
219/// #[get("/api/users")]
220/// #[authorize(role = "admin")]
221/// impl IRequest<Vec<UserModel>> for ListUsersRequest {}
222///
223/// #[get("/api/auth/me")]
224/// #[authorize]
225/// impl IRequest<UserView> for AuthMeRequest {}
226///
227/// // Either role is enough.
228/// #[get("/api/reports")]
229/// #[authorize(roles = ["admin", "auditor"])]
230/// impl IRequest<Vec<Report>> for ListReportsRequest {}
231///
232/// // Both rules must hold: admin role and the export permission.
233/// #[post("/api/reports/export")]
234/// #[authorize(role = "admin")]
235/// #[authorize(permission = "reports:export")]
236/// impl IRequest<ResponseData> for ExportReportsRequest {}
237/// ```
238#[proc_macro_attribute]
239pub fn authorize(_attr: TokenStream, item: TokenStream) -> TokenStream {
240    // Pass through — the attribute is read by emit_endpoint inside
241    // the route macro (get, post, etc.) via item_impl.attrs.
242    item
243}
244
245// ---------------------------------------------------------------------------
246// Endpoint text attributes (declarative, handled by emit_endpoint)
247// ---------------------------------------------------------------------------
248
249/// Declares an endpoint's display name as a resource reference.
250///
251/// `#[display("literal")]` is a fixed name. `#[display(key = "...")]` resolves
252/// the key from the host's resource files; `text` supplies a literal fallback
253/// when the key is absent. Localization is a host-wide culture setting, not one
254/// attribute per language.
255///
256/// ```ignore
257/// #[get("/api/blog")]
258/// #[display(key = "endpoint.blog.list.name", text = "博客模块-博客列表")]
259/// impl IRequest<Vec<BlogModel>> for ListBlogRequest {}
260/// ```
261#[proc_macro_attribute]
262pub fn display(attr: TokenStream, item: TokenStream) -> TokenStream {
263    rewrite_as_hidden(attr, item, "__webx_display", "display")
264}
265
266/// Declares an endpoint's long description as a resource reference.
267///
268/// Takes priority over the impl block's `///` doc comment. Accepts the same
269/// forms as [`display`].
270///
271/// ```ignore
272/// #[get("/api/blog")]
273/// #[description(key = "endpoint.blog.list.description")]
274/// impl IRequest<Vec<BlogModel>> for ListBlogRequest {}
275/// ```
276#[proc_macro_attribute]
277pub fn description(attr: TokenStream, item: TokenStream) -> TokenStream {
278    rewrite_as_hidden(attr, item, "__webx_description", "description")
279}
280
281/// Declares an optional display grouping for the endpoint.
282///
283/// The category is metadata for endpoint listings (permission trees, admin
284/// navigation, OpenAPI tags); it does not affect routing or authorization.
285/// It accepts the same forms as [`display`].
286///
287/// ```ignore
288/// #[get("/api/blog")]
289/// #[category("博客模块")]
290/// impl IRequest<Vec<BlogModel>> for ListBlogRequest {}
291/// ```
292#[proc_macro_attribute]
293pub fn category(attr: TokenStream, item: TokenStream) -> TokenStream {
294    rewrite_as_hidden(attr, item, "__webx_category", "category")
295}
296
297/// Rewrite an endpoint-text attribute into a hidden attribute the route macro
298/// can read regardless of declaration order.
299///
300/// Attribute macros expand outermost-first, so a `#[display]` above `#[post]`
301/// runs first and would otherwise be invisible to the route macro. Carrying the
302/// same arguments on `#[webx::__webx_display(...)]` lets the route macro read it
303/// in either order; the hidden attribute is a no-op passthrough afterwards.
304fn rewrite_as_hidden(
305    attr: TokenStream,
306    item: TokenStream,
307    hidden: &str,
308    public: &str,
309) -> TokenStream {
310    let mut item_impl: syn::ItemImpl = match syn::parse(item) {
311        Ok(item_impl) => item_impl,
312        Err(_) => {
313            return syn::Error::new(
314                proc_macro2::Span::call_site(),
315                format!("`#[{public}]` must be placed on the `impl` block of a route"),
316            )
317            .to_compile_error()
318            .into()
319        }
320    };
321
322    let hidden = syn::Ident::new(hidden, proc_macro2::Span::call_site());
323    let args = proc_macro2::TokenStream::from(attr);
324    let hidden_attr: syn::Attribute = parse_quote!(#[::webx::#hidden(#args)]);
325    item_impl.attrs.push(hidden_attr);
326
327    quote!(#item_impl).into()
328}
329
330/// Internal carrier for [`display`] — do not use directly.
331#[doc(hidden)]
332#[proc_macro_attribute]
333pub fn __webx_display(_attr: TokenStream, item: TokenStream) -> TokenStream {
334    item
335}
336
337/// Internal carrier for [`description`] — do not use directly.
338#[doc(hidden)]
339#[proc_macro_attribute]
340pub fn __webx_description(_attr: TokenStream, item: TokenStream) -> TokenStream {
341    item
342}
343
344/// Internal carrier for [`category`] — do not use directly.
345#[doc(hidden)]
346#[proc_macro_attribute]
347pub fn __webx_category(_attr: TokenStream, item: TokenStream) -> TokenStream {
348    item
349}