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 event;
20mod feature_flag;
21mod i18n;
22mod idempotency_guard;
23mod inbound_mail;
24mod job;
25mod jobs_macro;
26mod lifecycle;
27mod listener;
28mod listeners_macro;
29mod mail_previews_macro;
30mod mailer;
31mod mailer_preview;
32mod main_macro;
33mod model;
34mod oauth2_callback;
35mod one_off_task;
36mod one_off_tasks_macro;
37mod openapi_schema;
38mod param_helpers;
39mod parse;
40mod paths_macro;
41mod public;
42mod repository;
43mod route;
44mod routes_macro;
45mod scheduled;
46mod secured;
47mod service;
48mod static_route;
49mod static_routes_macro;
50mod step_up;
51mod story_macro;
52mod tasks_macro;
53mod throttle;
54mod ws;
55
56use proc_macro::TokenStream;
57
58/// Annotate an async function as a GET route handler.
59///
60/// Generates a companion `__autumn_route_info_{name}()` function that
61/// returns a `Route` pairing the path with an Axum
62/// handler. In debug builds, `#[axum::debug_handler]` is automatically
63/// applied for improved error messages. This has zero cost in release
64/// builds.
65///
66/// # Example
67///
68/// ```ignore
69/// use autumn_web::get;
70///
71/// #[get("/hello")]
72/// async fn hello() -> &'static str {
73///     "Hello, Autumn!"
74/// }
75/// ```
76#[proc_macro_attribute]
77pub fn get(attr: TokenStream, item: TokenStream) -> TokenStream {
78    route::route_macro("GET", "get", attr.into(), item.into()).into()
79}
80
81/// Annotate an async function as a POST route handler.
82///
83/// Generates a companion `__autumn_route_info_{name}()` function that
84/// returns a `Route` pairing the path with an Axum
85/// handler. In debug builds, `#[axum::debug_handler]` is automatically
86/// applied for improved error messages. This has zero cost in release
87/// builds.
88///
89/// # Example
90///
91/// ```ignore
92/// use autumn_web::post;
93///
94/// #[post("/items")]
95/// async fn create_item() -> &'static str {
96///     "created"
97/// }
98/// ```
99#[proc_macro_attribute]
100pub fn post(attr: TokenStream, item: TokenStream) -> TokenStream {
101    route::route_macro("POST", "post", attr.into(), item.into()).into()
102}
103
104/// Annotate an async function as a PUT route handler.
105///
106/// Generates a companion `__autumn_route_info_{name}()` function that
107/// returns a `Route` pairing the path with an Axum
108/// handler. In debug builds, `#[axum::debug_handler]` is automatically
109/// applied for improved error messages. This has zero cost in release
110/// builds.
111///
112/// # Example
113///
114/// ```ignore
115/// use autumn_web::put;
116///
117/// #[put("/items/{id}")]
118/// async fn update_item() -> &'static str {
119///     "updated"
120/// }
121/// ```
122#[proc_macro_attribute]
123pub fn put(attr: TokenStream, item: TokenStream) -> TokenStream {
124    route::route_macro("PUT", "put", attr.into(), item.into()).into()
125}
126
127/// Annotate an async function as a PATCH route handler.
128///
129/// Generates a companion `__autumn_route_info_{name}()` function and a typed
130/// `__autumn_path_{name}(…) -> String` path helper.
131///
132/// # Example
133///
134/// ```ignore
135/// use autumn_web::patch;
136///
137/// #[patch("/items/{id}")]
138/// async fn patch_item() -> &'static str {
139///     "patched"
140/// }
141/// ```
142#[proc_macro_attribute]
143pub fn patch(attr: TokenStream, item: TokenStream) -> TokenStream {
144    route::route_macro("PATCH", "patch", attr.into(), item.into()).into()
145}
146
147/// Annotate an async function as a DELETE route handler.
148///
149/// Generates a companion `__autumn_route_info_{name}()` function that
150/// returns a `Route` pairing the path with an Axum
151/// handler. In debug builds, `#[axum::debug_handler]` is automatically
152/// applied for improved error messages. This has zero cost in release
153/// builds.
154///
155/// # Example
156///
157/// ```ignore
158/// use autumn_web::delete;
159///
160/// #[delete("/items/{id}")]
161/// async fn remove_item() -> &'static str {
162///     "removed"
163/// }
164/// ```
165#[proc_macro_attribute]
166pub fn delete(attr: TokenStream, item: TokenStream) -> TokenStream {
167    route::route_macro("DELETE", "delete", attr.into(), item.into()).into()
168}
169
170/// Annotate an OAuth2/OIDC callback handler.
171///
172/// This is a convenience alias for `#[get(\"...\")]`, intended for OAuth
173/// callback endpoints such as `/auth/github/callback`.
174#[proc_macro_attribute]
175pub fn oauth2_callback(attr: TokenStream, item: TokenStream) -> TokenStream {
176    oauth2_callback::oauth2_callback_macro(attr.into(), item.into()).into()
177}
178
179/// Collect annotated route handlers into a `Vec<Route>`.
180///
181/// Each handler must have been annotated with a route macro (`#[get]`,
182/// `#[post]`, etc.) which generates a companion
183/// `__autumn_route_info_{name}()` function.
184///
185/// # Example
186///
187/// ```ignore
188/// use autumn_web::{get, post, routes};
189///
190/// #[get("/hello")]
191/// async fn hello() -> &'static str { "hello" }
192///
193/// #[post("/create")]
194/// async fn create() -> &'static str { "created" }
195///
196/// let all_routes = routes![hello, create];
197/// ```
198#[proc_macro]
199pub fn routes(input: TokenStream) -> TokenStream {
200    routes_macro::routes_macro(input.into()).into()
201}
202
203/// Emit a `pub mod paths { … }` that re-exports each handler's typed path helper.
204///
205/// Takes the same comma-separated handler list as [`routes!`]. Each entry
206/// exposes its `__autumn_path_{name}` companion under the short name:
207///
208/// ```ignore
209/// autumn_web::paths![show_post, create_post, posts::index];
210/// // expands to:
211/// pub mod paths {
212///     pub use super::__autumn_path_show_post as show_post;
213///     pub use super::__autumn_path_create_post as create_post;
214///     pub use super::posts::__autumn_path_index as index;
215/// }
216/// ```
217///
218/// Call this once at the top of the module where your handlers live (or a
219/// sibling module) so consumers can write `use crate::routes::paths;` and
220/// then `paths::show_post(id)`.
221#[proc_macro]
222pub fn paths(input: TokenStream) -> TokenStream {
223    paths_macro::paths_macro(input.into()).into()
224}
225
226/// Set up the async runtime for an Autumn application.
227///
228/// This is a thin wrapper around `#[tokio::main]`. The real
229/// framework setup happens in `autumn_web::app().run()`.
230///
231/// # Example
232///
233/// ```ignore
234/// #[autumn_web::main]
235/// async fn main() {
236///     autumn_web::app()
237///         .routes(routes![hello])
238///         .run()
239///         .await;
240/// }
241/// ```
242#[proc_macro_attribute]
243pub fn main(_attr: TokenStream, item: TokenStream) -> TokenStream {
244    main_macro::main_macro(item.into()).into()
245}
246
247/// Annotate an async inbound mail handler function.
248///
249/// Generates a companion `{name}_handler_info()` function that returns an
250/// `InboundMailHandlerInfo` ready to be passed to `InboundMailRouter::handler`.
251///
252/// # Attributes
253///
254/// - `to = "address@example.com"` — exact recipient match.
255/// - `to = "replies+{token}@app.example"` — plus-address routing; the captured
256///   token is available via `InboundEmail::plus_token()`.
257/// - `to = "prefix+*"` — local-part prefix match.
258/// - `processing = "sync"` | `"background"` (default: `"background"`).
259///
260/// # Example
261///
262/// ```rust,ignore
263/// #[inbound_mail(to = "support@company.com")]
264/// async fn handle_support(email: InboundEmail) -> AutumnResult<()> {
265///     tracing::info!(from = %email.from, "support email received");
266///     Ok(())
267/// }
268///
269/// // Registration:
270/// InboundMailRouter::new()
271///     .handler(handle_support_handler_info())
272/// ```
273#[proc_macro_attribute]
274pub fn inbound_mail(attr: TokenStream, item: TokenStream) -> TokenStream {
275    inbound_mail::inbound_mail_macro(attr.into(), item.into()).into()
276}
277
278/// Generate `send_*` and `deliver_later_*` helpers for a mailer impl block.
279#[proc_macro_attribute]
280pub fn mailer(attr: TokenStream, item: TokenStream) -> TokenStream {
281    mailer::mailer_macro(attr.into(), item.into()).into()
282}
283
284/// Register zero-argument mail preview methods for the dev mail preview UI.
285#[proc_macro_attribute]
286pub fn mailer_preview(attr: TokenStream, item: TokenStream) -> TokenStream {
287    mailer_preview::mailer_preview_macro(attr.into(), item.into()).into()
288}
289
290/// Collect `#[mailer_preview]` impl blocks into runtime preview registrations.
291#[proc_macro]
292pub fn mail_previews(input: TokenStream) -> TokenStream {
293    mail_previews_macro::mail_previews_macro(input.into()).into()
294}
295
296/// Define a widget story for the `/_stories` gallery:
297/// `story!{ "Group", "Name", { ... } }`.
298///
299/// The brace-delimited block is **both** executed for the live render and
300/// captured byte-for-byte (comments and formatting included) as the displayed
301/// source snippet, so the shown code is provably the code that rendered. The
302/// block must be a self-contained expression evaluating to `maud::Markup`:
303/// it is coerced to a plain `fn() -> Markup`, so capturing anything from the
304/// surrounding environment is a compile error.
305#[proc_macro]
306pub fn story(input: TokenStream) -> TokenStream {
307    story_macro::story_macro(input.into()).into()
308}
309
310/// Attribute macro for Autumn database models.
311///
312/// Applies Diesel (`Queryable`, `Selectable`, `Insertable`) and Serde
313/// (`Serialize`, `Deserialize`) derives, plus a `#[diesel(table_name)]`
314/// attribute. The table name can be specified explicitly or inferred
315/// from the struct name by converting `PascalCase` to `snake_case`
316/// and appending `s`.
317///
318/// # Examples
319///
320/// Explicit table name:
321///
322/// ```ignore
323/// use autumn_web::model;
324///
325/// #[model(table = "users")]
326/// pub struct User {
327///     pub id: i64,
328///     pub name: String,
329/// }
330/// ```
331///
332/// Inferred table name (`BlogPost` -> `blog_posts`):
333///
334/// ```ignore
335/// use autumn_web::model;
336///
337/// #[model]
338/// pub struct BlogPost {
339///     pub id: i64,
340///     pub title: String,
341/// }
342/// ```
343///
344/// # Associations
345///
346/// Declare `#[belongs_to]`, `#[has_many]`, and `#[has_one]` on the struct to
347/// get batched eager preloading for free — no hand-written join queries, no
348/// N+1. Foreign keys and accessor names are inferred from the target's type
349/// name, with `fk = ...` / `name = ...` overrides:
350///
351/// ```ignore
352/// #[model]
353/// #[belongs_to(User, fk = author_id)]  // fk on THIS model
354/// #[has_many(Comment)]                 // fk (post_id) on the TARGET
355/// pub struct Post {
356///     #[id]
357///     pub id: i64,
358///     pub author_id: i64,
359///     pub title: String,
360/// }
361/// ```
362///
363/// Preload associations through a repository (`Model::preload()` builds the
364/// spec; `_with` nests into the related model's own associations):
365///
366/// ```ignore
367/// let posts = repo.find_all().await?;
368/// let posts = repo.preload(posts, Post::preload().author().comments()).await?;
369/// for post in &posts {
370///     let author = post.author()?;      // Result<Option<&Preloaded<User>>, NotLoaded>
371///     let comments = post.comments()?;  // Result<&[Preloaded<Comment>], NotLoaded>
372/// }
373/// ```
374///
375/// An association that was not preloaded returns `NotLoaded` from its
376/// accessor rather than issuing SQL — autumn never lazy-loads.
377///
378/// ## Many-to-many (`through =`)
379///
380/// Add `through = <join_table>` to `#[has_many]` to declare a many-to-many
381/// association backed by a join table, with the same batched preload
382/// semantics as `belongs_to`/`has_many`/`has_one`:
383///
384/// ```ignore
385/// #[model]
386/// #[has_many(Tag, through = post_tags)]  // join columns default to post_id / tag_id
387/// pub struct Post {
388///     #[id]
389///     pub id: i64,
390///     pub title: String,
391/// }
392/// ```
393///
394/// Join columns default to `{source}_id` / `{target}_id` and can be
395/// overridden with `fk = ...` and `target_fk = ...`; the join table itself
396/// needs no hand-written `diesel::table!` — the macro emits one and requires
397/// a composite primary key on `(fk, target_fk)`:
398///
399/// ```sql
400/// CREATE TABLE post_tags (
401///     post_id BIGINT NOT NULL REFERENCES posts(id),
402///     tag_id  BIGINT NOT NULL REFERENCES tags(id),
403///     PRIMARY KEY (post_id, tag_id)
404/// );
405/// ```
406///
407/// `Post::preload().tags()` issues one batched `INNER JOIN` query (plus one
408/// more per level of `_with` nesting) — a fixed number of queries regardless
409/// of how many tags each post has. The generated `tags()` accessor returns
410/// `&[Arc<Preloaded<Tag>>]` (rather than `has_many`'s plain
411/// `&[Preloaded<Tag>]`): the same tag can legitimately be linked to more than
412/// one currently-loaded post, so it's shared via `Arc` instead of being
413/// duplicated per parent.
414///
415/// The association also generates three mutation helpers on the model's
416/// `#[repository]` — `add_{singular}`, `remove_{singular}`, and
417/// `set_{plural}` (replace-all) — each idempotent and requiring no
418/// hand-written SQL:
419///
420/// ```ignore
421/// repo.add_tag(post_id, tag_id).await?;      // idempotent: ON CONFLICT DO NOTHING
422/// repo.remove_tag(post_id, tag_id).await?;   // idempotent: no-op if unlinked
423/// repo.set_tags(post_id, &tag_ids).await?;   // replace-all, one transaction
424/// ```
425///
426/// The `add_`/`remove_` singular is derived from the target *type* name, so a
427/// model may declare at most one m2m association per target type by default —
428/// a second one to the same target would generate colliding helpers (a compile
429/// error). To declare two m2m associations to the same target (e.g. a
430/// self-referential `followers`/`following` pair through one `Friendship` join
431/// table), give each a distinct explicit `helper = "..."` override, which sets
432/// the singular used for its `add_`/`remove_` helpers:
433///
434/// ```ignore
435/// #[model]
436/// #[has_many(User, through = friendships, name = followers,
437///            fk = followed_id, target_fk = follower_id, helper = "follower")]
438/// #[has_many(User, through = friendships, name = following,
439///            fk = follower_id, target_fk = followed_id, helper = "following")]
440/// pub struct User { /* ... */ }
441/// // -> add_follower/remove_follower and add_following/remove_following
442/// ```
443#[proc_macro_attribute]
444pub fn model(attr: TokenStream, item: TokenStream) -> TokenStream {
445    model::model_macro(attr.into(), item.into()).into()
446}
447
448/// Derive a field-accurate `OpenApiSchema` impl for a plain struct with named
449/// fields (issue #1972).
450///
451/// Use it on a handler-arg struct — a `Query<T>` param struct or a
452/// non-`#[model]` `Json<T>` request body — so its `OpenAPI` component schema and
453/// MCP tool `inputSchema` describe the real fields instead of degrading to a
454/// generic `{"type":"object"}` placeholder, without a hand-written impl or an
455/// `OpenApiConfig::register_schema` call.
456///
457/// Each field becomes a JSON-schema property (nullable `Option<T>`, `Vec<T>`
458/// arrays, inline primitives, `$ref`s for other named types) and every
459/// non-`Option` field is `required` — mirroring the schema `#[model]` already
460/// generates. The derive also registers the schema in the compile-time
461/// inventory the spec/MCP back-fill consults, so the referencing route resolves
462/// it automatically.
463///
464/// # Examples
465///
466/// ```ignore
467/// use autumn_web::openapi::OpenApiSchema;
468///
469/// #[derive(serde::Deserialize, OpenApiSchema)]
470/// struct SearchParams {
471///     q: String,
472///     limit: Option<i32>,
473/// }
474/// ```
475#[proc_macro_derive(OpenApiSchema)]
476pub fn derive_openapi_schema(input: TokenStream) -> TokenStream {
477    openapi_schema::derive_openapi_schema(input)
478}
479
480/// Derive a repository with CRUD operations and derived queries.
481///
482/// Generates a `PgXxxRepository` struct implementing the annotated trait,
483/// with auto-generated CRUD methods and query-by-name derived methods.
484///
485/// # Read replica routing
486///
487/// When `database.replica_url` is configured, generated read-only methods
488/// (`find_by_id`, `find_all`, `count`, `paginate`, `cursor_page`, derived
489/// `find_by_*`, search reads) acquire their connection from the replica
490/// pool; mutating methods always use the primary. Add `primary_reads` to
491/// pin a read-after-write-sensitive repository's reads to the primary, or
492/// call the generated `on_primary()` method to pin a single call chain
493/// (read-your-writes).
494///
495/// # Examples
496///
497/// ```ignore
498/// use autumn_web::repository;
499///
500/// #[repository(Post)]
501/// trait PostRepository {
502///     fn find_by_published(published: bool) -> Vec<Post>;
503/// }
504///
505/// // Reads pinned to the primary even when a replica is configured.
506/// #[repository(LedgerEntry, primary_reads)]
507/// trait LedgerEntryRepository {}
508/// ```
509#[proc_macro_attribute]
510pub fn repository(attr: TokenStream, item: TokenStream) -> TokenStream {
511    repository::repository_macro(attr.into(), item.into()).into()
512}
513
514/// Declare a scheduled background task.
515///
516/// # Examples
517///
518/// ```ignore
519/// #[scheduled(every = "5m", name = "cleanup")]
520/// async fn cleanup(state: AppState) -> AutumnResult<()> { Ok(()) }
521///
522/// #[scheduled(cron = "0 0 0 * * *", name = "nightly")]
523/// async fn nightly(state: AppState) -> AutumnResult<()> { Ok(()) }
524/// ```
525#[proc_macro_attribute]
526pub fn scheduled(attr: TokenStream, item: TokenStream) -> TokenStream {
527    scheduled::scheduled_macro(attr.into(), item.into()).into()
528}
529
530/// Declare an on-demand background job.
531///
532/// Route latency-sensitive work to a named queue with `queue = "..."`. Workers
533/// drain queues in the priority order configured under `[jobs] queues` in
534/// `autumn.toml`, so a flood of low-priority jobs can't delay a critical one.
535/// Jobs with no `queue` land on the `"default"` queue.
536///
537/// ```ignore
538/// #[job(queue = "critical", max_attempts = 5)]
539/// async fn send_password_reset(state: AppState, args: ResetArgs) -> AutumnResult<()> {
540///     Ok(())
541/// }
542///
543/// // autumn.toml — strict priority (or weighted: { critical = 4, default = 1 }):
544/// // [jobs]
545/// // queues = ["critical", "default", "low"]
546/// SendPasswordResetJob::enqueue(ResetArgs { user_id: 1 }).await?;
547/// ```
548///
549/// Accept an optional third `JobContext` argument to report progress and
550/// record a terminal result/error for jobs enqueued with `enqueue_tracked`
551/// (the companion struct gains `enqueue_tracked` / `enqueue_tracked_for`
552/// alongside `enqueue`):
553///
554/// ```ignore
555/// #[job(name = "export_orders")]
556/// async fn export_orders(state: AppState, args: ExportArgs, ctx: JobContext) -> AutumnResult<()> {
557///     ctx.set_progress(50, Some("Rows 1200/5000")).await?;
558///     ctx.set_result(serde_json::json!({ "download_url": "/blob/abc.csv" }));
559///     Ok(())
560/// }
561///
562/// let handle = ExportOrdersJob::enqueue_tracked(ExportArgs { account_id: 1 }).await?;
563/// println!("poll at {}", handle.status_path());
564/// ```
565#[proc_macro_attribute]
566pub fn job(attr: TokenStream, item: TokenStream) -> TokenStream {
567    job::job_macro(attr.into(), item.into()).into()
568}
569
570/// Declare a typed domain event.
571///
572/// Applies the serde + `Clone`/`Debug` derives the event bus needs and
573/// implements `autumn_web::events::Event` with a stable `NAME` (the struct
574/// name by default, or `#[event(name = "...")]`).
575///
576/// ```ignore
577/// #[event]
578/// struct UserSignedUp { user_id: i64 }
579/// ```
580#[proc_macro_attribute]
581pub fn event(attr: TokenStream, item: TokenStream) -> TokenStream {
582    event::event_macro(attr.into(), item.into()).into()
583}
584
585/// Declare an event listener that reacts to a typed `#[event]`.
586///
587/// Runs **synchronously** (in-request) by default, or **durably** (enqueued on
588/// the `#[job]` queue, surviving restarts with retry + DLQ) with `durable`.
589///
590/// ```ignore
591/// #[listener(UserSignedUp, durable, max_attempts = 5)]
592/// async fn send_welcome_email(state: AppState, event: UserSignedUp) -> AutumnResult<()> { Ok(()) }
593/// ```
594#[proc_macro_attribute]
595pub fn listener(attr: TokenStream, item: TokenStream) -> TokenStream {
596    listener::listener_macro(attr.into(), item.into()).into()
597}
598
599/// Collect `#[listener]` handlers into a `Vec<ListenerInfo>`.
600#[proc_macro]
601pub fn listeners(input: TokenStream) -> TokenStream {
602    listeners_macro::listeners_macro(input.into()).into()
603}
604
605/// Declare a one-off operational task runnable with `autumn task <name>`.
606#[proc_macro_attribute]
607pub fn task(attr: TokenStream, item: TokenStream) -> TokenStream {
608    one_off_task::task_macro(attr.into(), item.into()).into()
609}
610
611/// Annotate an async function as a statically pre-rendered GET route.
612///
613/// Like `#[get]`, this generates a route companion function. Additionally,
614/// it generates a `__autumn_static_meta_{name}()` companion that registers
615/// the route for static HTML generation at build time.
616///
617/// Phase 1: path parameters are **not** supported. Use `#[get]` for
618/// parameterized routes.
619///
620/// # Example
621///
622/// ```ignore
623/// use autumn_web::static_get;
624///
625/// #[static_get("/about")]
626/// async fn about() -> &'static str {
627///     "About us"
628/// }
629/// ```
630#[proc_macro_attribute]
631pub fn static_get(attr: TokenStream, item: TokenStream) -> TokenStream {
632    static_route::static_get_macro(attr.into(), item.into()).into()
633}
634
635/// Collect `#[scheduled]` task handlers into a `Vec<TaskInfo>`.
636///
637/// ```ignore
638/// let all_tasks = tasks![cleanup, nightly];
639/// ```
640#[proc_macro]
641pub fn tasks(input: TokenStream) -> TokenStream {
642    tasks_macro::tasks_macro(input.into()).into()
643}
644
645/// Collect `#[job]` handlers into a `Vec<JobInfo>`.
646#[proc_macro]
647pub fn jobs(input: TokenStream) -> TokenStream {
648    jobs_macro::jobs_macro(input.into()).into()
649}
650
651/// Collect `#[task]` handlers into a `Vec<OneOffTaskInfo>`.
652#[proc_macro]
653pub fn one_off_tasks(input: TokenStream) -> TokenStream {
654    one_off_tasks_macro::one_off_tasks_macro(input.into()).into()
655}
656
657/// Secure a route handler with authentication and optional role checks.
658///
659/// Applied before a route macro (`#[get]`, `#[post]`, etc.), this macro
660/// injects an authentication guard at the top of the handler. The guard
661/// checks the session for the configured auth key (default: `"user_id"`)
662/// and, when roles are specified, verifies the user's role matches.
663///
664/// Returns `401 Unauthorized` if not authenticated, or `403 Forbidden`
665/// if the user lacks the required role.
666///
667/// # Forms
668///
669/// - `#[secured]` -- require authentication only
670/// - `#[secured("admin")]` -- require a specific role
671/// - `#[secured("admin", "editor")]` -- require any of the listed roles
672///
673/// # Example
674///
675/// ```ignore
676/// use autumn_web::prelude::*;
677///
678/// #[get("/admin")]
679/// #[secured("admin")]
680/// async fn admin_panel() -> AutumnResult<&'static str> {
681///     Ok("welcome, admin")
682/// }
683/// ```
684#[proc_macro_attribute]
685pub fn secured(attr: TokenStream, item: TokenStream) -> TokenStream {
686    secured::secured_macro(attr.into(), item.into()).into()
687}
688
689/// Declare a route handler as deliberately public (unauthenticated).
690///
691/// `#[public]` injects no runtime guard — it is a compile-time *marker* that
692/// records intent. The route macros surface it as `ApiDoc::public`, which the
693/// build-time security classifier (`autumn routes audit`) uses to distinguish
694/// a route that is *meant* to be open from one whose auth posture was simply
695/// never declared. Applying it makes an otherwise-unclassified route pass the
696/// audit gate, exactly like adding a [`#[secured]`](macro@secured) guard does.
697///
698/// # Example
699///
700/// ```ignore
701/// use autumn_web::prelude::*;
702///
703/// #[get("/health")]
704/// #[public]
705/// async fn health() -> &'static str { "ok" }
706/// ```
707#[proc_macro_attribute]
708pub fn public(attr: TokenStream, item: TokenStream) -> TokenStream {
709    public::public_macro(attr.into(), item.into()).into()
710}
711
712/// Require fresh ("step-up") authentication before a route handler runs.
713///
714/// The handler is guarded by a freshness check on the session's
715/// `last_strong_auth_at` claim. When the claim is missing or older than
716/// `max_age` the request is handled as follows:
717///
718/// - **Browser clients** (no `application/json` in `Accept`): redirect to
719///   `/reauth?return_to=<current-path>`.
720/// - **API / JSON clients** (`Accept: application/json`): `401 Unauthorized`
721///   with an RFC 7807 problem-details body (`type` =
722///   `"https://autumn.rs/probs/step-up-required"`) and a
723///   `WWW-Authenticate: StepUp max-age=N` hint header.
724///
725/// # Forms
726///
727/// - `#[step_up]` — default max-age (5 minutes, or the global `[auth.step_up]`
728///   config override)
729/// - `#[step_up(max_age = "5m")]` — custom per-route max-age
730///
731/// # Example
732///
733/// ```ignore
734/// use autumn_web::prelude::*;
735///
736/// // Requires re-authentication within the last 5 minutes.
737/// #[delete("/account")]
738/// #[step_up]
739/// async fn destroy_account() -> AutumnResult<Redirect> {
740///     // ... delete account ...
741///     Ok(Redirect::to("/bye"))
742/// }
743///
744/// // Custom max-age.
745/// #[post("/auth/mfa/remove")]
746/// #[step_up(max_age = "2m")]
747/// async fn remove_mfa() -> AutumnResult<&'static str> {
748///     Ok("MFA removed")
749/// }
750/// ```
751#[proc_macro_attribute]
752pub fn step_up(attr: TokenStream, item: TokenStream) -> TokenStream {
753    step_up::step_up_macro(attr.into(), item.into()).into()
754}
755
756/// Apply a per-route rate limit to a handler.
757///
758/// The handler is guarded by an additional rate limiter that composes with
759/// (and, for the annotated route, is stricter than) the global limiter
760/// configured under `[security.rate_limit]`. Requests denied by either
761/// limiter respond with `429 Too Many Requests` including a `Retry-After`
762/// header and the standard `x-ratelimit-*` headers.
763///
764/// # Forms
765///
766/// - `#[throttle(limit = 5, per = "1m")]` — inline limit; keying strategy
767///   matches the global limiter.
768/// - `#[throttle(limit = 5, per = "1m", key = "ip" | "principal" | "token")]`
769///   — inline limit with an explicit key strategy override.
770/// - `#[throttle("login")]` — reference a named limiter defined in
771///   `[security.rate_limit.named.login]`.
772///
773/// # Example
774///
775/// ```ignore
776/// use autumn_web::prelude::*;
777///
778/// #[post("/login")]
779/// #[throttle(limit = 5, per = "1m", key = "ip")]
780/// async fn login() -> AutumnResult<&'static str> {
781///     Ok("welcome back")
782/// }
783/// ```
784///
785/// # Limitations
786///
787/// Like the sibling `#[secured]` / `#[step_up]` guards it mirrors, the throttle
788/// check runs inside the handler after `FromRequestParts` extractors, but body
789/// extractors (`Json` / `Form` / `Multipart`) are parsed by Axum *before* the
790/// throttle check, so an over-limit client can still incur request-body parsing
791/// before receiving its `429`. For hard pre-body protection, combine with the
792/// global limiter layer under `[security.rate_limit]`.
793///
794/// # Attribute ordering
795///
796/// Place the route method attribute (`#[get]` / `#[post]` / …) *above*
797/// `#[throttle]`, i.e. method attribute outermost:
798///
799/// ```ignore
800/// #[post("/login")]           // method attribute outermost
801/// #[throttle(limit = 5, per = "1m", key = "ip")]
802/// async fn login() -> Json<Session> { /* … */ }
803/// ```
804///
805/// Both orders enforce throttling correctly (including idempotency-replay
806/// accounting). However, only the method-attribute-outermost order lets the
807/// route macro see the handler's real return type for `OpenAPI` response-schema
808/// generation. When `#[throttle]` expands first it rewrites the return type to
809/// `Response` (like the sibling `#[secured]` / `#[step_up]` / `#[authorize]`
810/// guards), so a `Json<T>` response schema would be lost from the generated
811/// `OpenAPI` document.
812#[proc_macro_attribute]
813pub fn throttle(attr: TokenStream, item: TokenStream) -> TokenStream {
814    throttle::throttle_macro(attr.into(), item.into()).into()
815}
816
817/// Gate a route handler on a named feature flag.
818///
819/// If the flag is disabled for the current actor, the handler responds with
820/// `404 Not Found` by default. Provide a `fallback` function to return a
821/// custom response instead.
822///
823/// The flag key is resolved against the `FeatureFlagService` stored in the
824/// `AppState` extensions. Unknown flags are treated as **disabled**
825/// (fail-closed).
826///
827/// # Forms
828///
829/// - `#[feature_flag("key")]` — return 404 when disabled
830/// - `#[feature_flag("key", fallback = my_fn)]` — call `my_fn()` when disabled
831///
832/// # Example
833///
834/// ```ignore
835/// use autumn_web::prelude::*;
836///
837/// #[get("/beta")]
838/// #[feature_flag("beta_dashboard")]
839/// async fn beta_dashboard() -> Markup {
840///     html! { h1 { "Beta Dashboard" } }
841/// }
842/// ```
843///
844#[proc_macro_attribute]
845pub fn feature_flag(attr: TokenStream, item: TokenStream) -> TokenStream {
846    feature_flag::feature_flag_macro(attr.into(), item.into()).into()
847}
848
849/// Enforce a record-level authorization policy on a route handler.
850///
851/// Resolves the `Policy`
852/// registered for the named resource type and calls the matching
853/// action method. Short-circuits with the configured deny response
854/// (default `404`, optionally `403`) before the handler body runs.
855///
856/// Coexists with `#[secured]`: `#[secured]` answers "are you in?",
857/// `#[authorize]` answers "are you allowed to act on *this record*?"
858///
859/// # Forms
860///
861/// ```ignore
862/// // Resource arg is auto-detected by snake-cased type name (Post -> `post`).
863/// #[authorize("update", resource = Post)]
864/// async fn update_post(post: Post) -> AutumnResult<...> { ... }
865///
866/// // Explicit binding name (overrides the snake-case default).
867/// #[authorize("delete", resource = Post, from = target)]
868/// async fn destroy(target: Post) -> AutumnResult<...> { ... }
869/// ```
870#[proc_macro_attribute]
871pub fn authorize(attr: TokenStream, item: TokenStream) -> TokenStream {
872    authorize::authorize_macro(attr.into(), item.into()).into()
873}
874
875/// Collect `#[static_get]` handlers into a `Vec<StaticRouteMeta>`.
876///
877/// ```ignore
878/// use autumn_web::prelude::*;
879///
880/// #[static_get("/about")]
881/// async fn about() -> &'static str { "About" }
882///
883/// let metas = static_routes![about];
884/// ```
885#[proc_macro]
886pub fn static_routes(input: TokenStream) -> TokenStream {
887    static_routes_macro::static_routes_macro(input.into()).into()
888}
889
890/// Define a service for cross-model orchestration and non-DB side effects.
891///
892/// Generates a `XxxServiceImpl` struct with dependency injection via
893/// `FromRequestParts`, so it can be used as a handler parameter just
894/// like repositories.
895///
896/// Use `#[service]` when your logic orchestrates **multiple repositories**
897/// or involves **non-DB side effects** (email, API calls, etc.).
898/// For single-model CRUD and validation, use `#[repository]` instead.
899///
900/// # Examples
901///
902/// ```ignore
903/// use autumn_web::service;
904///
905/// #[service]
906/// pub trait OrderService {
907///     fn deps(order_repo: PgOrderRepository, inventory_repo: PgInventoryRepository);
908///
909///     async fn place_order(&self, req: PlaceOrderRequest) -> AutumnResult<Order>;
910/// }
911///
912/// // You implement the business logic:
913/// impl OrderServiceImpl {
914///     pub async fn place_order(&self, req: PlaceOrderRequest) -> AutumnResult<Order> {
915///         let order = self.order_repo.save(&req.into()).await?;
916///         self.inventory_repo.reserve(order.id).await?;
917///         Ok(order)
918///     }
919/// }
920///
921/// // Then use it in handlers, just like a repository:
922/// #[get("/orders/{id}")]
923/// async fn get_order(svc: OrderServiceImpl) -> AutumnResult<Json<Order>> {
924///     // ...
925/// }
926/// ```
927#[proc_macro_attribute]
928pub fn service(attr: TokenStream, item: TokenStream) -> TokenStream {
929    service::service_macro(attr.into(), item.into()).into()
930}
931
932/// Cache the return value of a function based on its arguments.
933///
934/// Wraps a function with an in-memory cache backed by a per-function
935/// static `Cache` (from `autumn_web::cache::Cache`). Arguments
936/// must implement `Hash + Eq + Clone`; the return type must be `Clone`.
937///
938/// # Attributes
939///
940/// | Attribute | Example | Description |
941/// |-----------|---------|-------------|
942/// | `ttl` | `"5m"` | Time-to-live per entry (uses `parse_duration` syntax) |
943/// | `max` | `1000` | Max entries; oldest evicted on overflow |
944/// | `result` | (flag) | Only cache `Ok` values; pass `Err` through uncached |
945///
946/// # Examples
947///
948/// ```ignore
949/// use autumn_web::cached;
950///
951/// // Cache with 5-minute TTL, max 100 entries, only cache Ok values
952/// #[cached(ttl = "5m", max = 100, result)]
953/// async fn get_user(id: i64) -> AutumnResult<User> {
954///     db.find(id).await
955/// }
956///
957/// // Cache forever with no size limit
958/// #[cached]
959/// async fn get_config() -> Vec<String> {
960///     load_config_from_disk()
961/// }
962/// ```
963#[proc_macro_attribute]
964pub fn cached(attr: TokenStream, item: TokenStream) -> TokenStream {
965    cached::cached_macro(attr.into(), item.into()).into()
966}
967
968/// Enrich a route handler's auto-generated `OpenAPI` documentation.
969///
970/// Applied on top of a route macro (`#[get]`, `#[post]`, etc.), this
971/// attribute lets you override or add documentation fields that cannot
972/// be inferred from the handler signature (summaries, descriptions,
973/// tags, custom success status codes).
974///
975/// The route macro consumes this attribute and folds the metadata into
976/// the route's `ApiDoc`. When no route macro is applied, the attribute
977/// is a no-op.
978///
979/// # Supported keys
980///
981/// | Key | Type | Effect |
982/// |-----|------|--------|
983/// | `summary` | string | Short one-line description |
984/// | `description` | string | Longer multi-line description |
985/// | `tag` | string | Single `OpenAPI` tag for grouping |
986/// | `tags` | `[string, ...]` | Multiple `OpenAPI` tags |
987/// | `operation_id` | string | Override the default operation id |
988/// | `status` | integer | Success HTTP status code (defaults to `200`) |
989/// | `hidden` | flag / bool | Exclude the route from the generated spec |
990/// | `mcp` | flag / bool | Expose this endpoint as an MCP tool (`mcp = false` force-excludes it). Requires the `mcp` feature and a `mount_mcp` call. |
991///
992/// # Examples
993///
994/// ```ignore
995/// use autumn_web::prelude::*;
996///
997/// #[get("/users/{id}")]
998/// #[api_doc(summary = "Fetch a user by id", tag = "users")]
999/// async fn get_user(Path(id): Path<i32>) -> String {
1000///     format!("User {id}")
1001/// }
1002///
1003/// #[post("/users")]
1004/// #[api_doc(description = "Create a new user", status = 201)]
1005/// async fn create_user(Json(req): Json<serde_json::Value>) -> Json<serde_json::Value> {
1006///     Json(req)
1007/// }
1008///
1009/// #[get("/internal/metrics")]
1010/// #[api_doc(hidden)]
1011/// async fn metrics() -> &'static str { "" }
1012/// ```
1013#[proc_macro_attribute]
1014pub fn api_doc(attr: TokenStream, item: TokenStream) -> TokenStream {
1015    // Rust expands attribute macros top-down (outermost first), so if the
1016    // user writes
1017    //
1018    //   #[api_doc(summary = "...")]
1019    //   #[get("/x")]
1020    //   async fn handler() { ... }
1021    //
1022    // this macro fires BEFORE `#[get]` and would strip `#[api_doc]` from
1023    // the item — the route macro would then never see the overrides.
1024    //
1025    // To support both orderings, we detect any pending route attribute
1026    // (`get`, `post`, etc.) sitting below us and reorder: we remove the
1027    // route attribute and emit it as the NEW outermost attribute, and
1028    // we re-attach `#[api_doc(...)]` to the function body. Rust then
1029    // expands the route macro next, which finds and consumes the
1030    // preserved `#[api_doc]` via the usual attribute-list walk.
1031    api_doc_standalone(attr, item)
1032}
1033
1034const ROUTE_ATTR_NAMES: &[&str] = &["get", "post", "put", "delete", "patch", "static_get", "ws"];
1035
1036/// Return `true` when an attribute names one of the Autumn route macros.
1037///
1038/// We match on the **last** path segment so qualified forms like
1039/// `#[autumn_web::get("/x")]`, `#[autumn_macros::post("/x")]`, or
1040/// even `#[crate::get("/x")]` are recognized alongside the bare
1041/// `#[get("/x")]`. Unqualified identifiers are covered by the same
1042/// logic because their path has a single segment.
1043fn is_route_attribute(attr: &syn::Attribute) -> bool {
1044    attr.path()
1045        .segments
1046        .last()
1047        .map(|segment| segment.ident.to_string())
1048        .is_some_and(|name| ROUTE_ATTR_NAMES.contains(&name.as_str()))
1049}
1050
1051fn api_doc_standalone(attr: TokenStream, item: TokenStream) -> TokenStream {
1052    let attr_ts: proc_macro2::TokenStream = attr.into();
1053    let mut input_fn: syn::ItemFn = match syn::parse(item.clone()) {
1054        Ok(f) => f,
1055        // Not a function (e.g. applied to a struct) — leave it alone so
1056        // the user sees the usual "expected function" error from rustc.
1057        Err(_) => return item,
1058    };
1059
1060    let route_idx = input_fn.attrs.iter().position(is_route_attribute);
1061
1062    let Some(idx) = route_idx else {
1063        // Standalone `#[api_doc]` with no paired route macro is a no-op;
1064        // route metadata is only emitted through route macros.
1065        return quote::quote! { #input_fn }.into();
1066    };
1067
1068    let route_attr = input_fn.attrs.remove(idx);
1069    let preserved: syn::Attribute = syn::parse_quote! {
1070        #[api_doc(#attr_ts)]
1071    };
1072    input_fn.attrs.insert(0, preserved);
1073
1074    quote::quote! {
1075        #route_attr
1076        #input_fn
1077    }
1078    .into()
1079}
1080
1081/// Annotate an async function as a WebSocket route handler.
1082///
1083/// The function follows the **two-function pattern**: it runs at HTTP
1084/// upgrade time (with access to Axum extractors) and returns a closure
1085/// implementing `WsHandler` (from `autumn_web::ws::WsHandler`) that handles the live WebSocket connection.
1086///
1087/// The macro generates a GET route that performs the WebSocket upgrade,
1088/// so it integrates seamlessly with `routes![]`.
1089///
1090/// # Examples
1091///
1092/// ```ignore
1093/// use autumn_web::prelude::*;
1094/// use autumn_web::ws::{WebSocket, Message, WsHandler};
1095///
1096/// // Minimal echo handler
1097/// #[ws("/echo")]
1098/// async fn echo() -> impl WsHandler {
1099///     |mut socket: WebSocket| async move {
1100///         while let Some(Ok(msg)) = socket.recv().await {
1101///             if let Message::Text(text) = msg {
1102///                 socket.send(Message::Text(text)).await.ok();
1103///             }
1104///         }
1105///     }
1106/// }
1107///
1108/// // With extractors (runs before upgrade)
1109/// #[ws("/chat")]
1110/// async fn chat(state: AppState) -> impl WsHandler {
1111///     let channels = state.channels().clone();
1112///     |mut socket: WebSocket| async move {
1113///         // use channels + socket
1114///     }
1115/// }
1116/// ```
1117#[proc_macro_attribute]
1118pub fn ws(attr: TokenStream, item: TokenStream) -> TokenStream {
1119    ws::ws_macro(attr.into(), item.into()).into()
1120}
1121
1122/// Translate an i18n key, with **compile-time validation** that the key
1123/// exists in the default locale's `.ftl` file.
1124///
1125/// Re-exported as `autumn_web::t!` (and `autumn_web::prelude::t!`) when the
1126/// `i18n` feature is enabled on `autumn-web`.
1127///
1128/// # Forms
1129///
1130/// ```ignore
1131/// // Without args:
1132/// t!(locale, "welcome.title")
1133/// // With named args (Project Fluent's `{ $name }` placeable syntax):
1134/// t!(locale, "welcome.greeting", name = "Ada")
1135/// ```
1136///
1137/// # Compile-time behaviour
1138///
1139/// At expansion time the macro reads `$CARGO_MANIFEST_DIR/i18n/<default>.ftl`
1140/// (where `<default>` is the value of the `AUTUMN_I18N_DEFAULT_LOCALE`
1141/// environment variable, defaulting to `"en"`). If the key is not present,
1142/// the macro emits a `compile_error!` pointing at the literal so the build
1143/// fails with a clear diagnostic — including a "did you mean" suggestion
1144/// for typos within Levenshtein distance 3.
1145///
1146/// If the file does not exist (e.g. an app that just enabled the feature
1147/// flag and has not yet authored translations), the macro degrades to a
1148/// pure runtime call so the build still succeeds. The runtime path will
1149/// produce the visible `{$key}` marker on miss.
1150#[proc_macro]
1151pub fn t(input: TokenStream) -> TokenStream {
1152    i18n::t_macro(input.into()).into()
1153}
1154
1155/// Turn a plain state enum into a statically-verified lifecycle.
1156///
1157/// Given a declared `initial` state, one or more `terminal` states, and a set
1158/// of `transitions`, `#[lifecycle]` preserves the original enum and appends:
1159///
1160/// 1. Metadata consts (`LIFECYCLE_INITIAL`, `LIFECYCLE_TERMINALS`,
1161///    `LIFECYCLE_STATES`, `LIFECYCLE_TRANSITIONS`) and a `can_transition_to`
1162///    runtime check on the enum. Because these reference the enum's own
1163///    variants, a declared state that is not a real variant is a compile error.
1164/// 2. A typestate transition module named after the enum in `snake_case`, whose
1165///    `Machine<S>` exposes a consuming `to_<target>` method *only* for declared
1166///    edges — firing an undeclared transition does not compile.
1167///
1168/// # Example
1169///
1170/// ```ignore
1171/// use autumn_web::lifecycle;
1172///
1173/// #[lifecycle(
1174///     initial = Draft,
1175///     terminal(Archived),
1176///     transitions(
1177///         Draft -> Published,
1178///         Published -> Archived,
1179///         Published -> Draft,
1180///     )
1181/// )]
1182/// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1183/// pub enum ArticleState { Draft, Published, Archived }
1184///
1185/// let m = article_state::Machine::<article_state::Draft>::start();
1186/// let m = m.to_published();      // only declared edges exist as methods
1187/// assert_eq!(m.current(), ArticleState::Published);
1188/// assert!(ArticleState::Draft.can_transition_to(&ArticleState::Published));
1189/// ```
1190#[proc_macro_attribute]
1191pub fn lifecycle(attr: TokenStream, item: TokenStream) -> TokenStream {
1192    lifecycle::lifecycle_macro(attr.into(), item.into()).into()
1193}