Skip to main content

autumn_macros/
lib.rs

1#![allow(clippy::collapsible_else_if)]
2//! # Autumn Macros
3//!
4//! Proc macros for the Autumn web framework.
5//!
6//! This crate provides:
7//! - Route annotation macros (`#[get]`, `#[post]`, etc.)
8//! - The `routes![]` collection macro
9//! - The `#[autumn_web::main]` entry point macro (S-008)
10//! - The `#[model]` attribute macro (S-018)
11//!
12//! Users should not depend on this crate directly — use `autumn-web` instead,
13//! which re-exports everything.
14
15mod api_doc;
16mod authorize;
17mod cached;
18mod collect;
19mod feature_flag;
20mod i18n;
21mod idempotency_guard;
22mod inbound_mail;
23mod job;
24mod jobs_macro;
25mod mail_previews_macro;
26mod mailer;
27mod mailer_preview;
28mod main_macro;
29mod model;
30mod oauth2_callback;
31mod one_off_task;
32mod one_off_tasks_macro;
33mod param_helpers;
34mod parse;
35mod paths_macro;
36mod repository;
37mod route;
38mod routes_macro;
39mod scheduled;
40mod secured;
41mod service;
42mod static_route;
43mod static_routes_macro;
44mod step_up;
45mod tasks_macro;
46mod ws;
47
48use proc_macro::TokenStream;
49
50/// Annotate an async function as a GET route handler.
51///
52/// Generates a companion `__autumn_route_info_{name}()` function that
53/// returns a `Route` pairing the path with an Axum
54/// handler. In debug builds, `#[axum::debug_handler]` is automatically
55/// applied for improved error messages. This has zero cost in release
56/// builds.
57///
58/// # Example
59///
60/// ```ignore
61/// use autumn_web::get;
62///
63/// #[get("/hello")]
64/// async fn hello() -> &'static str {
65///     "Hello, Autumn!"
66/// }
67/// ```
68#[proc_macro_attribute]
69pub fn get(attr: TokenStream, item: TokenStream) -> TokenStream {
70    route::route_macro("GET", "get", attr.into(), item.into()).into()
71}
72
73/// Annotate an async function as a POST route handler.
74///
75/// Generates a companion `__autumn_route_info_{name}()` function that
76/// returns a `Route` pairing the path with an Axum
77/// handler. In debug builds, `#[axum::debug_handler]` is automatically
78/// applied for improved error messages. This has zero cost in release
79/// builds.
80///
81/// # Example
82///
83/// ```ignore
84/// use autumn_web::post;
85///
86/// #[post("/items")]
87/// async fn create_item() -> &'static str {
88///     "created"
89/// }
90/// ```
91#[proc_macro_attribute]
92pub fn post(attr: TokenStream, item: TokenStream) -> TokenStream {
93    route::route_macro("POST", "post", attr.into(), item.into()).into()
94}
95
96/// Annotate an async function as a PUT route handler.
97///
98/// Generates a companion `__autumn_route_info_{name}()` function that
99/// returns a `Route` pairing the path with an Axum
100/// handler. In debug builds, `#[axum::debug_handler]` is automatically
101/// applied for improved error messages. This has zero cost in release
102/// builds.
103///
104/// # Example
105///
106/// ```ignore
107/// use autumn_web::put;
108///
109/// #[put("/items/{id}")]
110/// async fn update_item() -> &'static str {
111///     "updated"
112/// }
113/// ```
114#[proc_macro_attribute]
115pub fn put(attr: TokenStream, item: TokenStream) -> TokenStream {
116    route::route_macro("PUT", "put", attr.into(), item.into()).into()
117}
118
119/// Annotate an async function as a PATCH route handler.
120///
121/// Generates a companion `__autumn_route_info_{name}()` function and a typed
122/// `__autumn_path_{name}(…) -> String` path helper.
123///
124/// # Example
125///
126/// ```ignore
127/// use autumn_web::patch;
128///
129/// #[patch("/items/{id}")]
130/// async fn patch_item() -> &'static str {
131///     "patched"
132/// }
133/// ```
134#[proc_macro_attribute]
135pub fn patch(attr: TokenStream, item: TokenStream) -> TokenStream {
136    route::route_macro("PATCH", "patch", attr.into(), item.into()).into()
137}
138
139/// Annotate an async function as a DELETE route handler.
140///
141/// Generates a companion `__autumn_route_info_{name}()` function that
142/// returns a `Route` pairing the path with an Axum
143/// handler. In debug builds, `#[axum::debug_handler]` is automatically
144/// applied for improved error messages. This has zero cost in release
145/// builds.
146///
147/// # Example
148///
149/// ```ignore
150/// use autumn_web::delete;
151///
152/// #[delete("/items/{id}")]
153/// async fn remove_item() -> &'static str {
154///     "removed"
155/// }
156/// ```
157#[proc_macro_attribute]
158pub fn delete(attr: TokenStream, item: TokenStream) -> TokenStream {
159    route::route_macro("DELETE", "delete", attr.into(), item.into()).into()
160}
161
162/// Annotate an OAuth2/OIDC callback handler.
163///
164/// This is a convenience alias for `#[get(\"...\")]`, intended for OAuth
165/// callback endpoints such as `/auth/github/callback`.
166#[proc_macro_attribute]
167pub fn oauth2_callback(attr: TokenStream, item: TokenStream) -> TokenStream {
168    oauth2_callback::oauth2_callback_macro(attr.into(), item.into()).into()
169}
170
171/// Collect annotated route handlers into a `Vec<Route>`.
172///
173/// Each handler must have been annotated with a route macro (`#[get]`,
174/// `#[post]`, etc.) which generates a companion
175/// `__autumn_route_info_{name}()` function.
176///
177/// # Example
178///
179/// ```ignore
180/// use autumn_web::{get, post, routes};
181///
182/// #[get("/hello")]
183/// async fn hello() -> &'static str { "hello" }
184///
185/// #[post("/create")]
186/// async fn create() -> &'static str { "created" }
187///
188/// let all_routes = routes![hello, create];
189/// ```
190#[proc_macro]
191pub fn routes(input: TokenStream) -> TokenStream {
192    routes_macro::routes_macro(input.into()).into()
193}
194
195/// Emit a `pub mod paths { … }` that re-exports each handler's typed path helper.
196///
197/// Takes the same comma-separated handler list as [`routes!`]. Each entry
198/// exposes its `__autumn_path_{name}` companion under the short name:
199///
200/// ```ignore
201/// autumn_web::paths![show_post, create_post, posts::index];
202/// // expands to:
203/// pub mod paths {
204///     pub use super::__autumn_path_show_post as show_post;
205///     pub use super::__autumn_path_create_post as create_post;
206///     pub use super::posts::__autumn_path_index as index;
207/// }
208/// ```
209///
210/// Call this once at the top of the module where your handlers live (or a
211/// sibling module) so consumers can write `use crate::routes::paths;` and
212/// then `paths::show_post(id)`.
213#[proc_macro]
214pub fn paths(input: TokenStream) -> TokenStream {
215    paths_macro::paths_macro(input.into()).into()
216}
217
218/// Set up the async runtime for an Autumn application.
219///
220/// This is a thin wrapper around `#[tokio::main]`. The real
221/// framework setup happens in `autumn_web::app().run()`.
222///
223/// # Example
224///
225/// ```ignore
226/// #[autumn_web::main]
227/// async fn main() {
228///     autumn_web::app()
229///         .routes(routes![hello])
230///         .run()
231///         .await;
232/// }
233/// ```
234#[proc_macro_attribute]
235pub fn main(_attr: TokenStream, item: TokenStream) -> TokenStream {
236    main_macro::main_macro(item.into()).into()
237}
238
239/// Annotate an async inbound mail handler function.
240///
241/// Generates a companion `{name}_handler_info()` function that returns an
242/// `InboundMailHandlerInfo` ready to be passed to `InboundMailRouter::handler`.
243///
244/// # Attributes
245///
246/// - `to = "address@example.com"` — exact recipient match.
247/// - `to = "replies+{token}@app.example"` — plus-address routing; the captured
248///   token is available via `InboundEmail::plus_token()`.
249/// - `to = "prefix+*"` — local-part prefix match.
250/// - `processing = "sync"` | `"background"` (default: `"background"`).
251///
252/// # Example
253///
254/// ```rust,ignore
255/// #[inbound_mail(to = "support@company.com")]
256/// async fn handle_support(email: InboundEmail) -> AutumnResult<()> {
257///     tracing::info!(from = %email.from, "support email received");
258///     Ok(())
259/// }
260///
261/// // Registration:
262/// InboundMailRouter::new()
263///     .handler(handle_support_handler_info())
264/// ```
265#[proc_macro_attribute]
266pub fn inbound_mail(attr: TokenStream, item: TokenStream) -> TokenStream {
267    inbound_mail::inbound_mail_macro(attr.into(), item.into()).into()
268}
269
270/// Generate `send_*` and `deliver_later_*` helpers for a mailer impl block.
271#[proc_macro_attribute]
272pub fn mailer(attr: TokenStream, item: TokenStream) -> TokenStream {
273    mailer::mailer_macro(attr.into(), item.into()).into()
274}
275
276/// Register zero-argument mail preview methods for the dev mail preview UI.
277#[proc_macro_attribute]
278pub fn mailer_preview(attr: TokenStream, item: TokenStream) -> TokenStream {
279    mailer_preview::mailer_preview_macro(attr.into(), item.into()).into()
280}
281
282/// Collect `#[mailer_preview]` impl blocks into runtime preview registrations.
283#[proc_macro]
284pub fn mail_previews(input: TokenStream) -> TokenStream {
285    mail_previews_macro::mail_previews_macro(input.into()).into()
286}
287
288/// Attribute macro for Autumn database models.
289///
290/// Applies Diesel (`Queryable`, `Selectable`, `Insertable`) and Serde
291/// (`Serialize`, `Deserialize`) derives, plus a `#[diesel(table_name)]`
292/// attribute. The table name can be specified explicitly or inferred
293/// from the struct name by converting `PascalCase` to `snake_case`
294/// and appending `s`.
295///
296/// # Examples
297///
298/// Explicit table name:
299///
300/// ```ignore
301/// use autumn_web::model;
302///
303/// #[model(table = "users")]
304/// pub struct User {
305///     pub id: i64,
306///     pub name: String,
307/// }
308/// ```
309///
310/// Inferred table name (`BlogPost` -> `blog_posts`):
311///
312/// ```ignore
313/// use autumn_web::model;
314///
315/// #[model]
316/// pub struct BlogPost {
317///     pub id: i64,
318///     pub title: String,
319/// }
320/// ```
321#[proc_macro_attribute]
322pub fn model(attr: TokenStream, item: TokenStream) -> TokenStream {
323    model::model_macro(attr.into(), item.into()).into()
324}
325
326/// Derive a repository with CRUD operations and derived queries.
327///
328/// Generates a `PgXxxRepository` struct implementing the annotated trait,
329/// with auto-generated CRUD methods and query-by-name derived methods.
330///
331/// # Read replica routing
332///
333/// When `database.replica_url` is configured, generated read-only methods
334/// (`find_by_id`, `find_all`, `count`, `paginate`, `cursor_page`, derived
335/// `find_by_*`, search reads) acquire their connection from the replica
336/// pool; mutating methods always use the primary. Add `primary_reads` to
337/// pin a read-after-write-sensitive repository's reads to the primary, or
338/// call the generated `on_primary()` method to pin a single call chain
339/// (read-your-writes).
340///
341/// # Examples
342///
343/// ```ignore
344/// use autumn_web::repository;
345///
346/// #[repository(Post)]
347/// trait PostRepository {
348///     fn find_by_published(published: bool) -> Vec<Post>;
349/// }
350///
351/// // Reads pinned to the primary even when a replica is configured.
352/// #[repository(LedgerEntry, primary_reads)]
353/// trait LedgerEntryRepository {}
354/// ```
355#[proc_macro_attribute]
356pub fn repository(attr: TokenStream, item: TokenStream) -> TokenStream {
357    repository::repository_macro(attr.into(), item.into()).into()
358}
359
360/// Declare a scheduled background task.
361///
362/// # Examples
363///
364/// ```ignore
365/// #[scheduled(every = "5m", name = "cleanup")]
366/// async fn cleanup(state: AppState) -> AutumnResult<()> { Ok(()) }
367///
368/// #[scheduled(cron = "0 0 0 * * *", name = "nightly")]
369/// async fn nightly(state: AppState) -> AutumnResult<()> { Ok(()) }
370/// ```
371#[proc_macro_attribute]
372pub fn scheduled(attr: TokenStream, item: TokenStream) -> TokenStream {
373    scheduled::scheduled_macro(attr.into(), item.into()).into()
374}
375
376/// Declare an on-demand background job.
377#[proc_macro_attribute]
378pub fn job(attr: TokenStream, item: TokenStream) -> TokenStream {
379    job::job_macro(attr.into(), item.into()).into()
380}
381
382/// Declare a one-off operational task runnable with `autumn task <name>`.
383#[proc_macro_attribute]
384pub fn task(attr: TokenStream, item: TokenStream) -> TokenStream {
385    one_off_task::task_macro(attr.into(), item.into()).into()
386}
387
388/// Annotate an async function as a statically pre-rendered GET route.
389///
390/// Like `#[get]`, this generates a route companion function. Additionally,
391/// it generates a `__autumn_static_meta_{name}()` companion that registers
392/// the route for static HTML generation at build time.
393///
394/// Phase 1: path parameters are **not** supported. Use `#[get]` for
395/// parameterized routes.
396///
397/// # Example
398///
399/// ```ignore
400/// use autumn_web::static_get;
401///
402/// #[static_get("/about")]
403/// async fn about() -> &'static str {
404///     "About us"
405/// }
406/// ```
407#[proc_macro_attribute]
408pub fn static_get(attr: TokenStream, item: TokenStream) -> TokenStream {
409    static_route::static_get_macro(attr.into(), item.into()).into()
410}
411
412/// Collect `#[scheduled]` task handlers into a `Vec<TaskInfo>`.
413///
414/// ```ignore
415/// let all_tasks = tasks![cleanup, nightly];
416/// ```
417#[proc_macro]
418pub fn tasks(input: TokenStream) -> TokenStream {
419    tasks_macro::tasks_macro(input.into()).into()
420}
421
422/// Collect `#[job]` handlers into a `Vec<JobInfo>`.
423#[proc_macro]
424pub fn jobs(input: TokenStream) -> TokenStream {
425    jobs_macro::jobs_macro(input.into()).into()
426}
427
428/// Collect `#[task]` handlers into a `Vec<OneOffTaskInfo>`.
429#[proc_macro]
430pub fn one_off_tasks(input: TokenStream) -> TokenStream {
431    one_off_tasks_macro::one_off_tasks_macro(input.into()).into()
432}
433
434/// Secure a route handler with authentication and optional role checks.
435///
436/// Applied before a route macro (`#[get]`, `#[post]`, etc.), this macro
437/// injects an authentication guard at the top of the handler. The guard
438/// checks the session for the configured auth key (default: `"user_id"`)
439/// and, when roles are specified, verifies the user's role matches.
440///
441/// Returns `401 Unauthorized` if not authenticated, or `403 Forbidden`
442/// if the user lacks the required role.
443///
444/// # Forms
445///
446/// - `#[secured]` -- require authentication only
447/// - `#[secured("admin")]` -- require a specific role
448/// - `#[secured("admin", "editor")]` -- require any of the listed roles
449///
450/// # Example
451///
452/// ```ignore
453/// use autumn_web::prelude::*;
454///
455/// #[get("/admin")]
456/// #[secured("admin")]
457/// async fn admin_panel() -> AutumnResult<&'static str> {
458///     Ok("welcome, admin")
459/// }
460/// ```
461#[proc_macro_attribute]
462pub fn secured(attr: TokenStream, item: TokenStream) -> TokenStream {
463    secured::secured_macro(attr.into(), item.into()).into()
464}
465
466/// Require fresh ("step-up") authentication before a route handler runs.
467///
468/// The handler is guarded by a freshness check on the session's
469/// `last_strong_auth_at` claim. When the claim is missing or older than
470/// `max_age` the request is handled as follows:
471///
472/// - **Browser clients** (no `application/json` in `Accept`): redirect to
473///   `/reauth?return_to=<current-path>`.
474/// - **API / JSON clients** (`Accept: application/json`): `401 Unauthorized`
475///   with an RFC 7807 problem-details body (`type` =
476///   `"https://autumn.rs/probs/step-up-required"`) and a
477///   `WWW-Authenticate: StepUp max-age=N` hint header.
478///
479/// # Forms
480///
481/// - `#[step_up]` — default max-age (5 minutes, or the global `[auth.step_up]`
482///   config override)
483/// - `#[step_up(max_age = "5m")]` — custom per-route max-age
484///
485/// # Example
486///
487/// ```ignore
488/// use autumn_web::prelude::*;
489///
490/// // Requires re-authentication within the last 5 minutes.
491/// #[delete("/account")]
492/// #[step_up]
493/// async fn destroy_account() -> AutumnResult<Redirect> {
494///     // ... delete account ...
495///     Ok(Redirect::to("/bye"))
496/// }
497///
498/// // Custom max-age.
499/// #[post("/auth/mfa/remove")]
500/// #[step_up(max_age = "2m")]
501/// async fn remove_mfa() -> AutumnResult<&'static str> {
502///     Ok("MFA removed")
503/// }
504/// ```
505#[proc_macro_attribute]
506pub fn step_up(attr: TokenStream, item: TokenStream) -> TokenStream {
507    step_up::step_up_macro(attr.into(), item.into()).into()
508}
509
510/// Gate a route handler on a named feature flag.
511///
512/// If the flag is disabled for the current actor, the handler responds with
513/// `404 Not Found` by default. Provide a `fallback` function to return a
514/// custom response instead.
515///
516/// The flag key is resolved against the `FeatureFlagService` stored in the
517/// `AppState` extensions. Unknown flags are treated as **disabled**
518/// (fail-closed).
519///
520/// # Forms
521///
522/// - `#[feature_flag("key")]` — return 404 when disabled
523/// - `#[feature_flag("key", fallback = my_fn)]` — call `my_fn()` when disabled
524///
525/// # Example
526///
527/// ```ignore
528/// use autumn_web::prelude::*;
529///
530/// #[get("/beta")]
531/// #[feature_flag("beta_dashboard")]
532/// async fn beta_dashboard() -> Markup {
533///     html! { h1 { "Beta Dashboard" } }
534/// }
535/// ```
536///
537#[proc_macro_attribute]
538pub fn feature_flag(attr: TokenStream, item: TokenStream) -> TokenStream {
539    feature_flag::feature_flag_macro(attr.into(), item.into()).into()
540}
541
542/// Enforce a record-level authorization policy on a route handler.
543///
544/// Resolves the `Policy`
545/// registered for the named resource type and calls the matching
546/// action method. Short-circuits with the configured deny response
547/// (default `404`, optionally `403`) before the handler body runs.
548///
549/// Coexists with `#[secured]`: `#[secured]` answers "are you in?",
550/// `#[authorize]` answers "are you allowed to act on *this record*?"
551///
552/// # Forms
553///
554/// ```ignore
555/// // Resource arg is auto-detected by snake-cased type name (Post -> `post`).
556/// #[authorize("update", resource = Post)]
557/// async fn update_post(post: Post) -> AutumnResult<...> { ... }
558///
559/// // Explicit binding name (overrides the snake-case default).
560/// #[authorize("delete", resource = Post, from = target)]
561/// async fn destroy(target: Post) -> AutumnResult<...> { ... }
562/// ```
563#[proc_macro_attribute]
564pub fn authorize(attr: TokenStream, item: TokenStream) -> TokenStream {
565    authorize::authorize_macro(attr.into(), item.into()).into()
566}
567
568/// Collect `#[static_get]` handlers into a `Vec<StaticRouteMeta>`.
569///
570/// ```ignore
571/// use autumn_web::prelude::*;
572///
573/// #[static_get("/about")]
574/// async fn about() -> &'static str { "About" }
575///
576/// let metas = static_routes![about];
577/// ```
578#[proc_macro]
579pub fn static_routes(input: TokenStream) -> TokenStream {
580    static_routes_macro::static_routes_macro(input.into()).into()
581}
582
583/// Define a service for cross-model orchestration and non-DB side effects.
584///
585/// Generates a `XxxServiceImpl` struct with dependency injection via
586/// `FromRequestParts`, so it can be used as a handler parameter just
587/// like repositories.
588///
589/// Use `#[service]` when your logic orchestrates **multiple repositories**
590/// or involves **non-DB side effects** (email, API calls, etc.).
591/// For single-model CRUD and validation, use `#[repository]` instead.
592///
593/// # Examples
594///
595/// ```ignore
596/// use autumn_web::service;
597///
598/// #[service]
599/// pub trait OrderService {
600///     fn deps(order_repo: PgOrderRepository, inventory_repo: PgInventoryRepository);
601///
602///     async fn place_order(&self, req: PlaceOrderRequest) -> AutumnResult<Order>;
603/// }
604///
605/// // You implement the business logic:
606/// impl OrderServiceImpl {
607///     pub async fn place_order(&self, req: PlaceOrderRequest) -> AutumnResult<Order> {
608///         let order = self.order_repo.save(&req.into()).await?;
609///         self.inventory_repo.reserve(order.id).await?;
610///         Ok(order)
611///     }
612/// }
613///
614/// // Then use it in handlers, just like a repository:
615/// #[get("/orders/{id}")]
616/// async fn get_order(svc: OrderServiceImpl) -> AutumnResult<Json<Order>> {
617///     // ...
618/// }
619/// ```
620#[proc_macro_attribute]
621pub fn service(attr: TokenStream, item: TokenStream) -> TokenStream {
622    service::service_macro(attr.into(), item.into()).into()
623}
624
625/// Cache the return value of a function based on its arguments.
626///
627/// Wraps a function with an in-memory cache backed by a per-function
628/// static `Cache` (from `autumn_web::cache::Cache`). Arguments
629/// must implement `Hash + Eq + Clone`; the return type must be `Clone`.
630///
631/// # Attributes
632///
633/// | Attribute | Example | Description |
634/// |-----------|---------|-------------|
635/// | `ttl` | `"5m"` | Time-to-live per entry (uses `parse_duration` syntax) |
636/// | `max` | `1000` | Max entries; oldest evicted on overflow |
637/// | `result` | (flag) | Only cache `Ok` values; pass `Err` through uncached |
638///
639/// # Examples
640///
641/// ```ignore
642/// use autumn_web::cached;
643///
644/// // Cache with 5-minute TTL, max 100 entries, only cache Ok values
645/// #[cached(ttl = "5m", max = 100, result)]
646/// async fn get_user(id: i64) -> AutumnResult<User> {
647///     db.find(id).await
648/// }
649///
650/// // Cache forever with no size limit
651/// #[cached]
652/// async fn get_config() -> Vec<String> {
653///     load_config_from_disk()
654/// }
655/// ```
656#[proc_macro_attribute]
657pub fn cached(attr: TokenStream, item: TokenStream) -> TokenStream {
658    cached::cached_macro(attr.into(), item.into()).into()
659}
660
661/// Enrich a route handler's auto-generated `OpenAPI` documentation.
662///
663/// Applied on top of a route macro (`#[get]`, `#[post]`, etc.), this
664/// attribute lets you override or add documentation fields that cannot
665/// be inferred from the handler signature (summaries, descriptions,
666/// tags, custom success status codes).
667///
668/// The route macro consumes this attribute and folds the metadata into
669/// the route's `ApiDoc`. When no route macro is applied, the attribute
670/// is a no-op.
671///
672/// # Supported keys
673///
674/// | Key | Type | Effect |
675/// |-----|------|--------|
676/// | `summary` | string | Short one-line description |
677/// | `description` | string | Longer multi-line description |
678/// | `tag` | string | Single `OpenAPI` tag for grouping |
679/// | `tags` | `[string, ...]` | Multiple `OpenAPI` tags |
680/// | `operation_id` | string | Override the default operation id |
681/// | `status` | integer | Success HTTP status code (defaults to `200`) |
682/// | `hidden` | flag / bool | Exclude the route from the generated spec |
683/// | `mcp` | flag / bool | Expose this endpoint as an MCP tool (`mcp = false` force-excludes it). Requires the `mcp` feature and a `mount_mcp` call. |
684///
685/// # Examples
686///
687/// ```ignore
688/// use autumn_web::prelude::*;
689///
690/// #[get("/users/{id}")]
691/// #[api_doc(summary = "Fetch a user by id", tag = "users")]
692/// async fn get_user(Path(id): Path<i32>) -> String {
693///     format!("User {id}")
694/// }
695///
696/// #[post("/users")]
697/// #[api_doc(description = "Create a new user", status = 201)]
698/// async fn create_user(Json(req): Json<serde_json::Value>) -> Json<serde_json::Value> {
699///     Json(req)
700/// }
701///
702/// #[get("/internal/metrics")]
703/// #[api_doc(hidden)]
704/// async fn metrics() -> &'static str { "" }
705/// ```
706#[proc_macro_attribute]
707pub fn api_doc(attr: TokenStream, item: TokenStream) -> TokenStream {
708    // Rust expands attribute macros top-down (outermost first), so if the
709    // user writes
710    //
711    //   #[api_doc(summary = "...")]
712    //   #[get("/x")]
713    //   async fn handler() { ... }
714    //
715    // this macro fires BEFORE `#[get]` and would strip `#[api_doc]` from
716    // the item — the route macro would then never see the overrides.
717    //
718    // To support both orderings, we detect any pending route attribute
719    // (`get`, `post`, etc.) sitting below us and reorder: we remove the
720    // route attribute and emit it as the NEW outermost attribute, and
721    // we re-attach `#[api_doc(...)]` to the function body. Rust then
722    // expands the route macro next, which finds and consumes the
723    // preserved `#[api_doc]` via the usual attribute-list walk.
724    api_doc_standalone(attr, item)
725}
726
727const ROUTE_ATTR_NAMES: &[&str] = &["get", "post", "put", "delete", "patch", "static_get", "ws"];
728
729/// Return `true` when an attribute names one of the Autumn route macros.
730///
731/// We match on the **last** path segment so qualified forms like
732/// `#[autumn_web::get("/x")]`, `#[autumn_macros::post("/x")]`, or
733/// even `#[crate::get("/x")]` are recognized alongside the bare
734/// `#[get("/x")]`. Unqualified identifiers are covered by the same
735/// logic because their path has a single segment.
736fn is_route_attribute(attr: &syn::Attribute) -> bool {
737    attr.path()
738        .segments
739        .last()
740        .map(|segment| segment.ident.to_string())
741        .is_some_and(|name| ROUTE_ATTR_NAMES.contains(&name.as_str()))
742}
743
744fn api_doc_standalone(attr: TokenStream, item: TokenStream) -> TokenStream {
745    let attr_ts: proc_macro2::TokenStream = attr.into();
746    let mut input_fn: syn::ItemFn = match syn::parse(item.clone()) {
747        Ok(f) => f,
748        // Not a function (e.g. applied to a struct) — leave it alone so
749        // the user sees the usual "expected function" error from rustc.
750        Err(_) => return item,
751    };
752
753    let route_idx = input_fn.attrs.iter().position(is_route_attribute);
754
755    let Some(idx) = route_idx else {
756        // Standalone `#[api_doc]` with no paired route macro is a no-op;
757        // route metadata is only emitted through route macros.
758        return quote::quote! { #input_fn }.into();
759    };
760
761    let route_attr = input_fn.attrs.remove(idx);
762    let preserved: syn::Attribute = syn::parse_quote! {
763        #[api_doc(#attr_ts)]
764    };
765    input_fn.attrs.insert(0, preserved);
766
767    quote::quote! {
768        #route_attr
769        #input_fn
770    }
771    .into()
772}
773
774/// Annotate an async function as a WebSocket route handler.
775///
776/// The function follows the **two-function pattern**: it runs at HTTP
777/// upgrade time (with access to Axum extractors) and returns a closure
778/// implementing `WsHandler` (from `autumn_web::ws::WsHandler`) that handles the live WebSocket connection.
779///
780/// The macro generates a GET route that performs the WebSocket upgrade,
781/// so it integrates seamlessly with `routes![]`.
782///
783/// # Examples
784///
785/// ```ignore
786/// use autumn_web::prelude::*;
787/// use autumn_web::ws::{WebSocket, Message, WsHandler};
788///
789/// // Minimal echo handler
790/// #[ws("/echo")]
791/// async fn echo() -> impl WsHandler {
792///     |mut socket: WebSocket| async move {
793///         while let Some(Ok(msg)) = socket.recv().await {
794///             if let Message::Text(text) = msg {
795///                 socket.send(Message::Text(text)).await.ok();
796///             }
797///         }
798///     }
799/// }
800///
801/// // With extractors (runs before upgrade)
802/// #[ws("/chat")]
803/// async fn chat(state: AppState) -> impl WsHandler {
804///     let channels = state.channels().clone();
805///     |mut socket: WebSocket| async move {
806///         // use channels + socket
807///     }
808/// }
809/// ```
810#[proc_macro_attribute]
811pub fn ws(attr: TokenStream, item: TokenStream) -> TokenStream {
812    ws::ws_macro(attr.into(), item.into()).into()
813}
814
815/// Translate an i18n key, with **compile-time validation** that the key
816/// exists in the default locale's `.ftl` file.
817///
818/// Re-exported as `autumn_web::t!` (and `autumn_web::prelude::t!`) when the
819/// `i18n` feature is enabled on `autumn-web`.
820///
821/// # Forms
822///
823/// ```ignore
824/// // Without args:
825/// t!(locale, "welcome.title")
826/// // With named args (Project Fluent's `{ $name }` placeable syntax):
827/// t!(locale, "welcome.greeting", name = "Ada")
828/// ```
829///
830/// # Compile-time behaviour
831///
832/// At expansion time the macro reads `$CARGO_MANIFEST_DIR/i18n/<default>.ftl`
833/// (where `<default>` is the value of the `AUTUMN_I18N_DEFAULT_LOCALE`
834/// environment variable, defaulting to `"en"`). If the key is not present,
835/// the macro emits a `compile_error!` pointing at the literal so the build
836/// fails with a clear diagnostic — including a "did you mean" suggestion
837/// for typos within Levenshtein distance 3.
838///
839/// If the file does not exist (e.g. an app that just enabled the feature
840/// flag and has not yet authored translations), the macro degrades to a
841/// pure runtime call so the build still succeeds. The runtime path will
842/// produce the visible `{$key}` marker on miss.
843#[proc_macro]
844pub fn t(input: TokenStream) -> TokenStream {
845    i18n::t_macro(input.into()).into()
846}