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