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// `from_query`, `from_route`, and `from_body` are helper attributes for
154// `#[derive(WebxRequestMeta)]` — not standalone attribute macros (avoids
155// rustc conflicts on struct fields).
156
157/// Registers OpenAPI parameter metadata from `#[from_query]` / `#[from_route]` / `#[from_body]` / `#[from_form]` fields.
158///
159/// `FormFile` fields are recognised automatically and reported as
160/// `multipart/form-data` binary parts.
161///
162/// Add `#[webx_request(query_all)]` to treat all non-skipped fields as query parameters.
163#[proc_macro_derive(
164 WebxRequestMeta,
165 attributes(from_query, from_route, from_body, from_form, webx_request)
166)]
167pub fn webx_request_meta(input: TokenStream) -> TokenStream {
168 request_meta::derive_request_meta(input)
169}
170
171// ---------------------------------------------------------------------------
172// Declaration macros
173// ---------------------------------------------------------------------------
174
175// ---------------------------------------------------------------------------
176// Claims attribute
177// ---------------------------------------------------------------------------
178
179/// Marks a request struct as carrying authentication claims.
180///
181/// Appends a `#[serde(skip)] pub claims: Option<Box<dyn IClaims>>` field to
182/// the struct and generates an inherent `set_claims` method that shadows the
183/// blanket no-op `IClaimsCarrier::set_claims`. The dispatcher injects claims
184/// into the request *before* calling `IRequestHandler::handle`.
185///
186/// Must be the outermost attribute so that `#[derive(...)]` sees the injected
187/// field:
188///
189/// ```ignore
190/// #[claims]
191/// #[derive(Default, Deserialize)]
192/// pub struct CreateCommentRequest {
193/// pub blog_id: i32,
194/// pub content: String,
195/// }
196/// ```
197#[proc_macro_attribute]
198pub fn claims(attr: TokenStream, item: TokenStream) -> TokenStream {
199 claims::claims_impl(attr, item)
200}
201
202// ---------------------------------------------------------------------------
203// Authorization attribute (declarative, handled by emit_endpoint)
204// ---------------------------------------------------------------------------
205
206/// Declares authorization requirements on a route.
207///
208/// Each attribute is one rule and **every** rule must be satisfied. Inside a
209/// rule, `roles` / `permissions` are any-of lists and the two dimensions are
210/// ANDed; a bare `#[authorize]` requires a valid authenticated principal.
211///
212/// ```ignore
213/// #[get("/api/users")]
214/// #[authorize(role = "admin")]
215/// impl IRequest<Vec<UserModel>> for ListUsersRequest {}
216///
217/// #[get("/api/auth/me")]
218/// #[authorize]
219/// impl IRequest<UserView> for AuthMeRequest {}
220///
221/// // Either role is enough.
222/// #[get("/api/reports")]
223/// #[authorize(roles = ["admin", "auditor"])]
224/// impl IRequest<Vec<Report>> for ListReportsRequest {}
225///
226/// // Both rules must hold: admin role and the export permission.
227/// #[post("/api/reports/export")]
228/// #[authorize(role = "admin")]
229/// #[authorize(permission = "reports:export")]
230/// impl IRequest<ResponseData> for ExportReportsRequest {}
231/// ```
232#[proc_macro_attribute]
233pub fn authorize(_attr: TokenStream, item: TokenStream) -> TokenStream {
234 // Pass through — the attribute is read by emit_endpoint inside
235 // the route macro (get, post, etc.) via item_impl.attrs.
236 item
237}
238
239// ---------------------------------------------------------------------------
240// Endpoint text attributes (declarative, handled by emit_endpoint)
241// ---------------------------------------------------------------------------
242
243/// Declares an endpoint's display name as a resource reference.
244///
245/// `#[display("literal")]` is a fixed name. `#[display(key = "...")]` resolves
246/// the key from the host's resource files; `text` supplies a literal fallback
247/// when the key is absent. Localization is a host-wide culture setting, not one
248/// attribute per language.
249///
250/// ```ignore
251/// #[get("/api/blog")]
252/// #[display(key = "endpoint.blog.list.name", text = "博客模块-博客列表")]
253/// impl IRequest<Vec<BlogModel>> for ListBlogRequest {}
254/// ```
255#[proc_macro_attribute]
256pub fn display(attr: TokenStream, item: TokenStream) -> TokenStream {
257 rewrite_as_hidden(attr, item, "__webx_display", "display")
258}
259
260/// Declares an endpoint's long description as a resource reference.
261///
262/// Takes priority over the impl block's `///` doc comment. Accepts the same
263/// forms as [`display`].
264///
265/// ```ignore
266/// #[get("/api/blog")]
267/// #[description(key = "endpoint.blog.list.description")]
268/// impl IRequest<Vec<BlogModel>> for ListBlogRequest {}
269/// ```
270#[proc_macro_attribute]
271pub fn description(attr: TokenStream, item: TokenStream) -> TokenStream {
272 rewrite_as_hidden(attr, item, "__webx_description", "description")
273}
274
275/// Declares an optional display grouping for the endpoint.
276///
277/// The category is metadata for endpoint listings (permission trees, admin
278/// navigation, OpenAPI tags); it does not affect routing or authorization.
279/// It accepts the same forms as [`display`].
280///
281/// ```ignore
282/// #[get("/api/blog")]
283/// #[category("博客模块")]
284/// impl IRequest<Vec<BlogModel>> for ListBlogRequest {}
285/// ```
286#[proc_macro_attribute]
287pub fn category(attr: TokenStream, item: TokenStream) -> TokenStream {
288 rewrite_as_hidden(attr, item, "__webx_category", "category")
289}
290
291/// Rewrite an endpoint-text attribute into a hidden attribute the route macro
292/// can read regardless of declaration order.
293///
294/// Attribute macros expand outermost-first, so a `#[display]` above `#[post]`
295/// runs first and would otherwise be invisible to the route macro. Carrying the
296/// same arguments on `#[webx::__webx_display(...)]` lets the route macro read it
297/// in either order; the hidden attribute is a no-op passthrough afterwards.
298fn rewrite_as_hidden(
299 attr: TokenStream,
300 item: TokenStream,
301 hidden: &str,
302 public: &str,
303) -> TokenStream {
304 let mut item_impl: syn::ItemImpl = match syn::parse(item) {
305 Ok(item_impl) => item_impl,
306 Err(_) => {
307 return syn::Error::new(
308 proc_macro2::Span::call_site(),
309 format!("`#[{public}]` must be placed on the `impl` block of a route"),
310 )
311 .to_compile_error()
312 .into()
313 }
314 };
315
316 let hidden = syn::Ident::new(hidden, proc_macro2::Span::call_site());
317 let args = proc_macro2::TokenStream::from(attr);
318 let hidden_attr: syn::Attribute = parse_quote!(#[::webx::#hidden(#args)]);
319 item_impl.attrs.push(hidden_attr);
320
321 quote!(#item_impl).into()
322}
323
324/// Internal carrier for [`display`] — do not use directly.
325#[doc(hidden)]
326#[proc_macro_attribute]
327pub fn __webx_display(_attr: TokenStream, item: TokenStream) -> TokenStream {
328 item
329}
330
331/// Internal carrier for [`description`] — do not use directly.
332#[doc(hidden)]
333#[proc_macro_attribute]
334pub fn __webx_description(_attr: TokenStream, item: TokenStream) -> TokenStream {
335 item
336}
337
338/// Internal carrier for [`category`] — do not use directly.
339#[doc(hidden)]
340#[proc_macro_attribute]
341pub fn __webx_category(_attr: TokenStream, item: TokenStream) -> TokenStream {
342 item
343}