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) -> ActionResult<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/// # Identity fields (fail-closed)
163///
164/// The authenticated identity is a normal request field of type `IdentityUser`.
165/// Anonymous access must be declared explicitly, so a request that silently
166/// works for anonymous callers cannot happen by accident:
167///
168/// | Field | Declaration | Anonymous callers |
169/// |-------|-------------|-------------------|
170/// | `user: IdentityUser` | `#[fromclaim]` | rejected with `401` |
171/// | `user: Option<IdentityUser>` | `#[fromclaim(optional)]` | allowed, filled with `None` |
172///
173/// A bare `#[fromclaim]` on `Option<IdentityUser>` is a compile error, and so is
174/// `#[fromclaim(optional)]` on anything else. A request may carry at most one
175/// identity field. The derive also implements `RequestUser` for that field.
176///
177/// Unmarked fields follow the request method: `GET`/`HEAD`/`DELETE`/`OPTIONS`
178/// bind from the query string, then the route; `POST`/`PUT`/`PATCH` bind from
179/// the JSON body, then the form, then the query string. Headers and claims are
180/// opt-in only. `#[request(queryall)]` forces unmarked fields to the query.
181///
182/// The derive also registers the request type's `TypeId`, so OpenAPI resolves
183/// its parameter metadata unambiguously — a qualified `self_ty`
184/// (`contracts::ListOrdersRequest`) or two same-named request types in
185/// different modules/crates never lose or share parameters.
186#[proc_macro_derive(
187 WebxRequestMeta,
188 attributes(
189 frompath, fromquery, frombody, fromform, fromfile, fromheader, fromclaim, request
190 )
191)]
192pub fn webx_request_meta(input: TokenStream) -> TokenStream {
193 request_meta::derive_request_meta(input)
194}
195
196// ---------------------------------------------------------------------------
197// Declaration macros
198// ---------------------------------------------------------------------------
199
200// ---------------------------------------------------------------------------
201// Authorization attribute (declarative, handled by emit_endpoint)
202// ---------------------------------------------------------------------------
203
204/// Declares authorization requirements on a route.
205///
206/// Each attribute is one rule and **every** rule must be satisfied. Inside a
207/// rule, `roles` / `permissions` are any-of lists and the two dimensions are
208/// ANDed; a bare `#[authorize]` requires a valid authenticated principal.
209///
210/// ```ignore
211/// #[get("/api/users")]
212/// #[authorize(role = "admin")]
213/// impl IRequest<Vec<UserModel>> for ListUsersRequest {}
214///
215/// #[get("/api/auth/me")]
216/// #[authorize]
217/// impl IRequest<UserView> for AuthMeRequest {}
218///
219/// // Either role is enough.
220/// #[get("/api/reports")]
221/// #[authorize(roles = ["admin", "auditor"])]
222/// impl IRequest<Vec<Report>> for ListReportsRequest {}
223///
224/// // Both rules must hold: admin role and the export permission.
225/// #[post("/api/reports/export")]
226/// #[authorize(role = "admin")]
227/// #[authorize(permission = "reports:export")]
228/// impl IRequest<Response> for ExportReportsRequest {}
229///
230/// // Dynamic mode: the static gate only requires authentication; the declared
231/// // rules are handed to the registered `IDynamicAuthorizer` to decide (e.g. a
232/// // database-backed permission check). `dynamic` combines with role/permission,
233/// // and makes the whole endpoint dynamic when mixed with other `#[authorize]`s.
234/// #[delete("/api/orders/{id}")]
235/// #[authorize(dynamic, permission = "order.delete")]
236/// impl IRequest<()> for DeleteOrderRequest {}
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}