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//! - `#[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#[proc_macro_attribute]
231pub fn authorize(_attr: TokenStream, item: TokenStream) -> TokenStream {
232    // Pass through — the attribute is read by emit_endpoint inside
233    // the route macro (get, post, etc.) via item_impl.attrs.
234    item
235}
236
237// ---------------------------------------------------------------------------
238// Endpoint text attributes (declarative, handled by emit_endpoint)
239// ---------------------------------------------------------------------------
240
241/// Declares an endpoint's display name as a resource reference.
242///
243/// `#[display("literal")]` is a fixed name. `#[display(key = "...")]` resolves
244/// the key from the host's resource files; `text` supplies a literal fallback
245/// when the key is absent. Localization is a host-wide culture setting, not one
246/// attribute per language.
247///
248/// ```ignore
249/// #[get("/api/blog")]
250/// #[display(key = "endpoint.blog.list.name", text = "博客模块-博客列表")]
251/// impl IRequest<Vec<BlogModel>> for ListBlogRequest {}
252/// ```
253#[proc_macro_attribute]
254pub fn display(attr: TokenStream, item: TokenStream) -> TokenStream {
255    rewrite_as_hidden(attr, item, "__webx_display", "display")
256}
257
258/// Declares an endpoint's long description as a resource reference.
259///
260/// Takes priority over the impl block's `///` doc comment. Accepts the same
261/// forms as [`display`].
262///
263/// ```ignore
264/// #[get("/api/blog")]
265/// #[description(key = "endpoint.blog.list.description")]
266/// impl IRequest<Vec<BlogModel>> for ListBlogRequest {}
267/// ```
268#[proc_macro_attribute]
269pub fn description(attr: TokenStream, item: TokenStream) -> TokenStream {
270    rewrite_as_hidden(attr, item, "__webx_description", "description")
271}
272
273/// Declares an optional display grouping for the endpoint.
274///
275/// The category is metadata for endpoint listings (permission trees, admin
276/// navigation, OpenAPI tags); it does not affect routing or authorization.
277/// It accepts the same forms as [`display`].
278///
279/// ```ignore
280/// #[get("/api/blog")]
281/// #[category("博客模块")]
282/// impl IRequest<Vec<BlogModel>> for ListBlogRequest {}
283/// ```
284#[proc_macro_attribute]
285pub fn category(attr: TokenStream, item: TokenStream) -> TokenStream {
286    rewrite_as_hidden(attr, item, "__webx_category", "category")
287}
288
289/// Rewrite an endpoint-text attribute into a hidden attribute the route macro
290/// can read regardless of declaration order.
291///
292/// Attribute macros expand outermost-first, so a `#[display]` above `#[post]`
293/// runs first and would otherwise be invisible to the route macro. Carrying the
294/// same arguments on `#[webx::__webx_display(...)]` lets the route macro read it
295/// in either order; the hidden attribute is a no-op passthrough afterwards.
296fn rewrite_as_hidden(
297    attr: TokenStream,
298    item: TokenStream,
299    hidden: &str,
300    public: &str,
301) -> TokenStream {
302    let mut item_impl: syn::ItemImpl = match syn::parse(item) {
303        Ok(item_impl) => item_impl,
304        Err(_) => {
305            return syn::Error::new(
306                proc_macro2::Span::call_site(),
307                format!("`#[{public}]` must be placed on the `impl` block of a route"),
308            )
309            .to_compile_error()
310            .into()
311        }
312    };
313
314    let hidden = syn::Ident::new(hidden, proc_macro2::Span::call_site());
315    let args = proc_macro2::TokenStream::from(attr);
316    let hidden_attr: syn::Attribute = parse_quote!(#[::webx::#hidden(#args)]);
317    item_impl.attrs.push(hidden_attr);
318
319    quote!(#item_impl).into()
320}
321
322/// Internal carrier for [`display`] — do not use directly.
323#[doc(hidden)]
324#[proc_macro_attribute]
325pub fn __webx_display(_attr: TokenStream, item: TokenStream) -> TokenStream {
326    item
327}
328
329/// Internal carrier for [`description`] — do not use directly.
330#[doc(hidden)]
331#[proc_macro_attribute]
332pub fn __webx_description(_attr: TokenStream, item: TokenStream) -> TokenStream {
333    item
334}
335
336/// Internal carrier for [`category`] — do not use directly.
337#[doc(hidden)]
338#[proc_macro_attribute]
339pub fn __webx_category(_attr: TokenStream, item: TokenStream) -> TokenStream {
340    item
341}