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//!
11//! The database-layer macros (`#[model]`, `#[repository]`, `#[service]`)
12//! live in the sibling crates `autumn-macros-model` and
13//! `autumn-macros-repository`, so a build that never touches the database
14//! never compiles their codegen. All three crates share parsing, path
15//! rewriting, schema, and naming helpers through `autumn-macros-support`.
16//!
17//! Users should not depend on this crate directly — use `autumn-web` instead,
18//! which re-exports everything.
19//!
20//! # Renamed or multi-versioned `autumn-web` dependencies (#1828)
21//!
22//! Every macro generates code that ultimately refers back to types in
23//! `autumn-web` via absolute (`::`-rooted) paths like `::autumn_web::Route`.
24//! By default that name is resolved automatically — via
25//! [`proc-macro-crate`](https://docs.rs/proc-macro-crate) — from the
26//! invoking crate's own `Cargo.toml`, so renaming the dependency (`web = {
27//! package = "autumn-web" }`) just works with no macro changes.
28//!
29//! A crate that must depend on **two** differently-keyed copies of
30//! `autumn-web` at once (e.g. mid-upgrade) is inherently ambiguous for that
31//! automatic detection. Every attribute macro (`#[get]`, `#[model]`,
32//! `#[repository]`, …) therefore also accepts an explicit `crate = "..."`
33//! argument naming the extern-prelude identifier to use instead, e.g.
34//! `#[get("/x", crate = "autumn_web_05")]`.
35
36mod agent_authority;
37mod api_doc;
38mod authorize;
39mod cached;
40mod cached_impl;
41mod collect;
42mod edge;
43mod edge_routes_macro;
44mod event;
45mod feature_flag;
46mod graph;
47mod i18n;
48mod idempotency_guard;
49mod inbound_mail;
50mod job;
51mod jobs_macro;
52mod lifecycle;
53mod listener;
54mod listeners_macro;
55mod mail_previews_macro;
56mod mailer;
57mod mailer_preview;
58mod main_macro;
59mod oauth2_callback;
60mod obligation;
61mod one_off_task;
62mod one_off_tasks_macro;
63mod openapi_schema;
64mod param_helpers;
65mod parse;
66mod paths_macro;
67mod public;
68mod query_budget;
69mod request_gate;
70mod route;
71mod routes_macro;
72mod scheduled;
73mod secured;
74mod sim_test;
75mod static_route;
76mod static_routes_macro;
77mod step_up;
78mod story_macro;
79mod tasks_macro;
80mod throttle;
81mod wire;
82mod ws;
83
84use proc_macro::TokenStream;
85
86/// Annotate an async function as a GET route handler.
87///
88/// Generates a companion `__autumn_route_info_{name}()` function that
89/// returns a `Route` pairing the path with an Axum
90/// handler. In debug builds, `#[axum::debug_handler]` is automatically
91/// applied for improved error messages. This has zero cost in release
92/// builds.
93///
94/// # Example
95///
96/// ```ignore
97/// use autumn_web::get;
98///
99/// #[get("/hello")]
100/// async fn hello() -> &'static str {
101///     "Hello, Autumn!"
102/// }
103/// ```
104///
105/// # Route-level SEO defaults
106///
107/// A `seo(...)` argument declares per-page meta tag values once on the route
108/// instead of rebuilding them in every handler. Take a `SeoMeta` parameter and
109/// it arrives pre-populated; the builder is consuming, so the handler refines
110/// the defaults with per-request data:
111///
112/// ```ignore
113/// use autumn_web::get;
114/// use autumn_web::seo::SeoMeta;
115///
116/// #[get("/about", seo(title = "About • My Blog", description = "Learn about us"))]
117/// async fn about(seo: SeoMeta) -> Markup {
118///     html! { head { (seo.render()) } }
119/// }
120///
121/// #[get("/posts/{slug}", seo(og_type = "article"))]
122/// async fn show(slug: Path<String>, seo: SeoMeta) -> Markup {
123///     let seo = seo.title(format!("{} • Blog", *slug));
124///     html! { head { (seo.render()) } }
125/// }
126/// ```
127///
128/// Accepted keys mirror the `SeoMeta` builder: `title`, `description`,
129/// `canonical`, `og_title`, `og_description`, `og_image`, `og_type`, `og_url`,
130/// `twitter_card`, `twitter_title`, `twitter_description`, `twitter_image`,
131/// and `robots`. Values must be string literals; an unknown, repeated, or
132/// empty `seo(...)` is a compile error. Every HTTP route macro accepts the
133/// argument, as does [`macro@static_get`]; [`macro@ws`] does not, since a
134/// WebSocket upgrade serves no crawlable document.
135///
136/// The argument supplies *values*, not markup: a handler that never takes a
137/// `SeoMeta` parameter renders nothing regardless of what the attribute
138/// declares.
139#[proc_macro_attribute]
140pub fn get(attr: TokenStream, item: TokenStream) -> TokenStream {
141    autumn_macros_support::crate_path::with_override(attr.into(), item.into(), |attr, item| {
142        route::route_macro("GET", "get", attr, item)
143    })
144    .into()
145}
146
147/// Annotate an async function as a POST 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::post;
159///
160/// #[post("/items")]
161/// async fn create_item() -> &'static str {
162///     "created"
163/// }
164/// ```
165#[proc_macro_attribute]
166pub fn post(attr: TokenStream, item: TokenStream) -> TokenStream {
167    autumn_macros_support::crate_path::with_override(attr.into(), item.into(), |attr, item| {
168        route::route_macro("POST", "post", attr, item)
169    })
170    .into()
171}
172
173/// Annotate an async function as a PUT route handler.
174///
175/// Generates a companion `__autumn_route_info_{name}()` function that
176/// returns a `Route` pairing the path with an Axum
177/// handler. In debug builds, `#[axum::debug_handler]` is automatically
178/// applied for improved error messages. This has zero cost in release
179/// builds.
180///
181/// # Example
182///
183/// ```ignore
184/// use autumn_web::put;
185///
186/// #[put("/items/{id}")]
187/// async fn update_item() -> &'static str {
188///     "updated"
189/// }
190/// ```
191#[proc_macro_attribute]
192pub fn put(attr: TokenStream, item: TokenStream) -> TokenStream {
193    autumn_macros_support::crate_path::with_override(attr.into(), item.into(), |attr, item| {
194        route::route_macro("PUT", "put", attr, item)
195    })
196    .into()
197}
198
199/// Annotate an async function as a PATCH route handler.
200///
201/// Generates a companion `__autumn_route_info_{name}()` function and a typed
202/// `__autumn_path_{name}(…) -> String` path helper.
203///
204/// # Example
205///
206/// ```ignore
207/// use autumn_web::patch;
208///
209/// #[patch("/items/{id}")]
210/// async fn patch_item() -> &'static str {
211///     "patched"
212/// }
213/// ```
214#[proc_macro_attribute]
215pub fn patch(attr: TokenStream, item: TokenStream) -> TokenStream {
216    autumn_macros_support::crate_path::with_override(attr.into(), item.into(), |attr, item| {
217        route::route_macro("PATCH", "patch", attr, item)
218    })
219    .into()
220}
221
222/// Annotate an async function as a DELETE route handler.
223///
224/// Generates a companion `__autumn_route_info_{name}()` function that
225/// returns a `Route` pairing the path with an Axum
226/// handler. In debug builds, `#[axum::debug_handler]` is automatically
227/// applied for improved error messages. This has zero cost in release
228/// builds.
229///
230/// # Example
231///
232/// ```ignore
233/// use autumn_web::delete;
234///
235/// #[delete("/items/{id}")]
236/// async fn remove_item() -> &'static str {
237///     "removed"
238/// }
239/// ```
240#[proc_macro_attribute]
241pub fn delete(attr: TokenStream, item: TokenStream) -> TokenStream {
242    autumn_macros_support::crate_path::with_override(attr.into(), item.into(), |attr, item| {
243        route::route_macro("DELETE", "delete", attr, item)
244    })
245    .into()
246}
247
248/// Annotate an OAuth2/OIDC callback handler.
249///
250/// This is a convenience alias for `#[get(\"...\")]`, intended for OAuth
251/// callback endpoints such as `/auth/github/callback`.
252#[proc_macro_attribute]
253pub fn oauth2_callback(attr: TokenStream, item: TokenStream) -> TokenStream {
254    autumn_macros_support::crate_path::with_override(attr.into(), item.into(), |attr, item| {
255        oauth2_callback::oauth2_callback_macro(attr, item)
256    })
257    .into()
258}
259
260/// Collect annotated route handlers into a `Vec<Route>`.
261///
262/// Each handler must have been annotated with a route macro (`#[get]`,
263/// `#[post]`, etc.) which generates a companion
264/// `__autumn_route_info_{name}()` function.
265///
266/// # Example
267///
268/// ```ignore
269/// use autumn_web::{get, post, routes};
270///
271/// #[get("/hello")]
272/// async fn hello() -> &'static str { "hello" }
273///
274/// #[post("/create")]
275/// async fn create() -> &'static str { "created" }
276///
277/// let all_routes = routes![hello, create];
278/// ```
279#[proc_macro]
280pub fn routes(input: TokenStream) -> TokenStream {
281    let _guard = autumn_macros_support::crate_path::set_target(None);
282    autumn_macros_support::crate_path::finalize(routes_macro::routes_macro(input.into())).into()
283}
284
285/// Emit a `pub mod paths { … }` that re-exports each handler's typed path helper.
286///
287/// Takes the same comma-separated handler list as [`routes!`]. Each entry
288/// exposes its `__autumn_path_{name}` companion under the short name:
289///
290/// ```ignore
291/// autumn_web::paths![show_post, create_post, posts::index];
292/// // expands to:
293/// pub mod paths {
294///     pub use super::__autumn_path_show_post as show_post;
295///     pub use super::__autumn_path_create_post as create_post;
296///     pub use super::posts::__autumn_path_index as index;
297/// }
298/// ```
299///
300/// Call this once at the top of the module where your handlers live (or a
301/// sibling module) so consumers can write `use crate::routes::paths;` and
302/// then `paths::show_post(id)`.
303#[proc_macro]
304pub fn paths(input: TokenStream) -> TokenStream {
305    let _guard = autumn_macros_support::crate_path::set_target(None);
306    autumn_macros_support::crate_path::finalize(paths_macro::paths_macro(input.into())).into()
307}
308
309/// Set up the async runtime for an Autumn application.
310///
311/// This is a thin wrapper around `#[tokio::main]`. The real
312/// framework setup happens in `autumn_web::app().run()`.
313///
314/// # Example
315///
316/// ```ignore
317/// #[autumn_web::main]
318/// async fn main() {
319///     autumn_web::app()
320///         .routes(routes![hello])
321///         .run()
322///         .await;
323/// }
324/// ```
325///
326/// # Runtime arguments
327///
328/// All optional; with none of them the runtime is
329/// `Builder::new_multi_thread().enable_all()`, tokio's own defaults.
330///
331/// | Argument | Value | Effect |
332/// | --- | --- | --- |
333/// | `flavor` | `"multi_thread"` (default) or `"current_thread"` | picks the `Builder` constructor |
334/// | `worker_threads` | `usize` expression | `Builder::worker_threads` (multi-thread only) |
335/// | `max_blocking_threads` | `usize` expression | `Builder::max_blocking_threads` |
336/// | `thread_name` | `Into<String>` expression | `Builder::thread_name` |
337/// | `thread_stack_size` | `usize` expression | `Builder::thread_stack_size` |
338/// | `thread_keep_alive` | duration string, e.g. `"30s"` | `Builder::thread_keep_alive` |
339/// | `configure` | path to `fn(&mut tokio::runtime::Builder)` | runs last, after the arguments above |
340///
341/// ```ignore
342/// fn tune_runtime(builder: &mut autumn_web::reexports::tokio::runtime::Builder) {
343///     builder.on_thread_start(|| eprintln!("runtime thread started"));
344/// }
345///
346/// #[autumn_web::main(worker_threads = 4, thread_name = "autumn-worker", configure = tune_runtime)]
347/// async fn main() {
348///     autumn_web::app().routes(routes![hello]).run().await;
349/// }
350/// ```
351///
352/// The numeric arguments take arbitrary expressions, not just literals, so
353/// `worker_threads = std::thread::available_parallelism().map_or(4, |n| n.get())`
354/// works. `configure` is the escape hatch for `Builder` methods this list does
355/// not name.
356#[proc_macro_attribute]
357pub fn main(attr: TokenStream, item: TokenStream) -> TokenStream {
358    autumn_macros_support::crate_path::with_override(attr.into(), item.into(), |attr, item| {
359        main_macro::main_macro(attr, item)
360    })
361    .into()
362}
363
364/// Annotate an async function as a deterministic simulation test (S-1797, W1).
365///
366/// Expands into a synchronous `#[test]` function that reads a seed from the
367/// `AUTUMN_SIM_SEED` environment variable (hex `0x..` or decimal, default `0`),
368/// builds a paused current-thread tokio runtime, constructs a
369/// `autumn_web::sim::Sim` from the seed, runs the async body, and prints a
370/// copy-pasteable replay line on panic before re-propagating the failure.
371///
372/// The annotated function must be `async` and take exactly one argument — the
373/// [`Sim`](../autumn_web/sim/struct.Sim.html) handle.
374///
375/// # Example
376///
377/// ```ignore
378/// use autumn_web::sim::Sim;
379/// use autumn_web::sim_test;
380///
381/// #[sim_test]
382/// async fn deterministic(mut sim: Sim) {
383///     assert_eq!(sim.seed, 0);
384/// }
385/// ```
386#[proc_macro_attribute]
387pub fn sim_test(attr: TokenStream, item: TokenStream) -> TokenStream {
388    autumn_macros_support::crate_path::with_override(attr.into(), item.into(), |attr, item| {
389        sim_test::sim_test_macro(attr, item)
390    })
391    .into()
392}
393
394/// Annotate an async inbound mail handler function.
395///
396/// Generates a companion `{name}_handler_info()` function that returns an
397/// `InboundMailHandlerInfo` ready to be passed to `InboundMailRouter::handler`.
398///
399/// # Attributes
400///
401/// - `to = "address@example.com"` — exact recipient match.
402/// - `to = "replies+{token}@app.example"` — plus-address routing; the captured
403///   token is available via `InboundEmail::plus_token()`.
404/// - `to = "prefix+*"` — local-part prefix match.
405/// - `processing = "sync"` | `"background"` (default: `"background"`).
406///
407/// # Example
408///
409/// ```rust,ignore
410/// #[inbound_mail(to = "support@company.com")]
411/// async fn handle_support(email: InboundEmail) -> AutumnResult<()> {
412///     tracing::info!(from = %email.from, "support email received");
413///     Ok(())
414/// }
415///
416/// // Registration:
417/// InboundMailRouter::new()
418///     .handler(handle_support_handler_info())
419/// ```
420#[proc_macro_attribute]
421pub fn inbound_mail(attr: TokenStream, item: TokenStream) -> TokenStream {
422    autumn_macros_support::crate_path::with_override(attr.into(), item.into(), |attr, item| {
423        inbound_mail::inbound_mail_macro(attr, item)
424    })
425    .into()
426}
427
428/// Generate `send_*` and `deliver_later_*` helpers for a mailer impl block.
429#[proc_macro_attribute]
430pub fn mailer(attr: TokenStream, item: TokenStream) -> TokenStream {
431    autumn_macros_support::crate_path::with_override(attr.into(), item.into(), |attr, item| {
432        mailer::mailer_macro(attr, item)
433    })
434    .into()
435}
436
437/// Register zero-argument mail preview methods for the dev mail preview UI.
438#[proc_macro_attribute]
439pub fn mailer_preview(attr: TokenStream, item: TokenStream) -> TokenStream {
440    autumn_macros_support::crate_path::with_override(attr.into(), item.into(), |attr, item| {
441        mailer_preview::mailer_preview_macro(attr, item)
442    })
443    .into()
444}
445
446/// Collect `#[mailer_preview]` impl blocks into runtime preview registrations.
447#[proc_macro]
448pub fn mail_previews(input: TokenStream) -> TokenStream {
449    let _guard = autumn_macros_support::crate_path::set_target(None);
450    autumn_macros_support::crate_path::finalize(mail_previews_macro::mail_previews_macro(
451        input.into(),
452    ))
453    .into()
454}
455
456/// Define a widget story for the `/_stories` gallery:
457/// `story!{ "Group", "Name", { ... } }`.
458///
459/// The brace-delimited block is **both** executed for the live render and
460/// captured byte-for-byte (comments and formatting included) as the displayed
461/// source snippet, so the shown code is provably the code that rendered. The
462/// block must be a self-contained expression evaluating to `maud::Markup`:
463/// it is coerced to a plain `fn() -> Markup`, so capturing anything from the
464/// surrounding environment is a compile error.
465#[proc_macro]
466pub fn story(input: TokenStream) -> TokenStream {
467    let _guard = autumn_macros_support::crate_path::set_target(None);
468    autumn_macros_support::crate_path::finalize(story_macro::story_macro(input.into())).into()
469}
470
471/// Derive a field-accurate `OpenApiSchema` impl for a plain struct with named
472/// fields, or a unit-variant enum (issue #1972).
473///
474/// Use it on a handler-arg type — a `Query<T>` param struct, a non-`#[model]`
475/// `Json<T>` request body, or an enum appearing in either — so its `OpenAPI`
476/// component schema and MCP tool `inputSchema` describe the real contract
477/// instead of degrading to a generic `{"type":"object"}` placeholder, without a
478/// hand-written impl or an `OpenApiConfig::register_schema` call.
479///
480/// **Structs**: each field becomes a JSON-schema property (nullable `Option<T>`,
481/// `Vec<T>` arrays, inline primitives, `$ref`s for other named types) and every
482/// non-`Option` field is `required` — mirroring the schema `#[model]` already
483/// generates.
484///
485/// **Enums**: all-unit-variant enums become the closed string set
486/// `{"type":"string","enum":[…]}` that serde puts on the wire, honoring
487/// `#[serde(rename)]` / `#[serde(rename_all)]` / `#[serde(skip)]`. A
488/// data-carrying variant is a compile error rather than a guess: serde's
489/// representation for those depends on `#[serde(tag/content/untagged)]`, so an
490/// inferred shape could confidently advertise a contract the handler does not
491/// accept. Write the impl by hand and register it for that case.
492///
493/// **Two copies of `autumn-web`**: use `#[openapi_schema(crate = "name")]` to
494/// name the copy that the generated code must use. A derive cannot take
495/// arguments, so a helper attribute carries the override.
496///
497/// # Examples
498///
499/// ```ignore
500/// use autumn_web::openapi::OpenApiSchema;
501///
502/// #[derive(serde::Deserialize, OpenApiSchema)]
503/// struct SearchParams {
504///     q: String,
505///     limit: Option<i32>,
506///     status: Option<Status>,
507/// }
508///
509/// #[derive(serde::Deserialize, OpenApiSchema)]
510/// #[serde(rename_all = "snake_case")]
511/// enum Status {
512///     Open,
513///     InProgress,
514/// }
515/// ```
516#[proc_macro_derive(OpenApiSchema, attributes(openapi_schema))]
517pub fn derive_openapi_schema(input: TokenStream) -> TokenStream {
518    openapi_schema::derive_openapi_schema(input.into()).into()
519}
520
521/// Declare a scheduled background task.
522///
523/// # Examples
524///
525/// ```ignore
526/// #[scheduled(every = "5m", name = "cleanup")]
527/// async fn cleanup(state: AppState) -> AutumnResult<()> { Ok(()) }
528///
529/// #[scheduled(cron = "0 0 0 * * *", name = "nightly")]
530/// async fn nightly(state: AppState) -> AutumnResult<()> { Ok(()) }
531/// ```
532///
533/// Add `deferrable` to let a task wait while the cost signal is high
534/// (`[cost] defer_threshold`). The tick takes its lease, then waits. It runs
535/// when the signal falls. Later ticks fold into that run. See
536/// `docs/guide/cost.md`.
537///
538/// ```ignore
539/// #[scheduled(every = "15m", deferrable)]
540/// async fn compact(state: AppState) -> AutumnResult<()> { Ok(()) }
541/// ```
542#[proc_macro_attribute]
543pub fn scheduled(attr: TokenStream, item: TokenStream) -> TokenStream {
544    autumn_macros_support::crate_path::with_override(attr.into(), item.into(), |attr, item| {
545        scheduled::scheduled_macro(attr, item)
546    })
547    .into()
548}
549
550/// Declare an on-demand background job.
551///
552/// Route latency-sensitive work to a named queue with `queue = "..."`. Workers
553/// drain queues in the priority order configured under `[jobs] queues` in
554/// `autumn.toml`, so a flood of low-priority jobs can't delay a critical one.
555/// Jobs with no `queue` land on the `"default"` queue.
556///
557/// ```ignore
558/// #[job(queue = "critical", max_attempts = 5)]
559/// async fn send_password_reset(state: AppState, args: ResetArgs) -> AutumnResult<()> {
560///     Ok(())
561/// }
562///
563/// // autumn.toml — strict priority (or weighted: { critical = 4, default = 1 }):
564/// // [jobs]
565/// // queues = ["critical", "default", "low"]
566/// SendPasswordResetJob::enqueue(ResetArgs { user_id: 1 }).await?;
567/// ```
568///
569/// Accept an optional third `JobContext` argument to report progress and
570/// record a terminal result/error for jobs enqueued with `enqueue_tracked`
571/// (the companion struct gains `enqueue_tracked` / `enqueue_tracked_for`
572/// alongside `enqueue`):
573///
574/// ```ignore
575/// #[job(name = "export_orders")]
576/// async fn export_orders(state: AppState, args: ExportArgs, ctx: JobContext) -> AutumnResult<()> {
577///     ctx.set_progress(50, Some("Rows 1200/5000")).await?;
578///     ctx.set_result(serde_json::json!({ "download_url": "/blob/abc.csv" }));
579///     Ok(())
580/// }
581///
582/// let handle = ExportOrdersJob::enqueue_tracked(ExportArgs { account_id: 1 }).await?;
583/// println!("poll at {}", handle.status_path());
584/// ```
585///
586/// Add `deferrable` to let a job wait while the cost signal is high
587/// (`[cost] defer_threshold`). The job runs when the signal falls. The runtime
588/// never drops it. It uses no attempt while it waits. Only the `local` jobs
589/// backend defers jobs. Other backends run the job and log a warning at boot.
590/// See `docs/guide/cost.md`.
591///
592/// ```ignore
593/// #[job(deferrable)]
594/// async fn rebuild_search_index(state: AppState, args: RebuildArgs) -> AutumnResult<()> {
595///     Ok(())
596/// }
597/// ```
598#[proc_macro_attribute]
599pub fn job(attr: TokenStream, item: TokenStream) -> TokenStream {
600    autumn_macros_support::crate_path::with_override(attr.into(), item.into(), |attr, item| {
601        job::job_macro(attr, item)
602    })
603    .into()
604}
605
606/// Declare a typed domain event.
607///
608/// Applies the serde + `Clone`/`Debug` derives the event bus needs and
609/// implements `autumn_web::events::Event` with a stable `NAME` (the struct
610/// name by default, or `#[event(name = "...")]`).
611///
612/// ```ignore
613/// #[event]
614/// struct UserSignedUp { user_id: i64 }
615/// ```
616#[proc_macro_attribute]
617pub fn event(attr: TokenStream, item: TokenStream) -> TokenStream {
618    autumn_macros_support::crate_path::with_override(attr.into(), item.into(), |attr, item| {
619        event::event_macro(attr, item)
620    })
621    .into()
622}
623
624/// Declare an event listener that reacts to a typed `#[event]`.
625///
626/// Runs **synchronously** (in-request) by default, or **durably** (enqueued on
627/// the `#[job]` queue, surviving restarts with retry + DLQ) with `durable`.
628///
629/// ```ignore
630/// #[listener(UserSignedUp, durable, max_attempts = 5)]
631/// async fn send_welcome_email(state: AppState, event: UserSignedUp) -> AutumnResult<()> { Ok(()) }
632/// ```
633#[proc_macro_attribute]
634pub fn listener(attr: TokenStream, item: TokenStream) -> TokenStream {
635    autumn_macros_support::crate_path::with_override(attr.into(), item.into(), |attr, item| {
636        listener::listener_macro(attr, item)
637    })
638    .into()
639}
640
641/// Collect `#[listener]` handlers into a `Vec<ListenerInfo>`.
642#[proc_macro]
643pub fn listeners(input: TokenStream) -> TokenStream {
644    let _guard = autumn_macros_support::crate_path::set_target(None);
645    autumn_macros_support::crate_path::finalize(listeners_macro::listeners_macro(input.into()))
646        .into()
647}
648
649/// Declare a one-off operational task runnable with `autumn task <name>`.
650#[proc_macro_attribute]
651pub fn task(attr: TokenStream, item: TokenStream) -> TokenStream {
652    autumn_macros_support::crate_path::with_override(attr.into(), item.into(), |attr, item| {
653        one_off_task::task_macro(attr, item)
654    })
655    .into()
656}
657
658/// Annotate an async function as a statically pre-rendered GET route.
659///
660/// Like `#[get]`, this generates a route companion function. Additionally,
661/// it generates a `__autumn_static_meta_{name}()` companion that registers
662/// the route for static HTML generation at build time.
663///
664/// Phase 1: path parameters are **not** supported. Use `#[get]` for
665/// parameterized routes.
666///
667/// # Example
668///
669/// ```ignore
670/// use autumn_web::static_get;
671///
672/// #[static_get("/about")]
673/// async fn about() -> &'static str {
674///     "About us"
675/// }
676/// ```
677///
678/// # Route-level SEO defaults
679///
680/// Accepts the same `seo(...)` argument as [`macro@get`], so pre-rendered
681/// pages carry the declared meta tags. Static generation drives the same
682/// router as the live server, so the values reach the handler identically in
683/// both modes:
684///
685/// ```ignore
686/// #[static_get("/about", seo(title = "About • My Blog", og_type = "website"))]
687/// async fn about(seo: SeoMeta) -> Markup {
688///     html! { head { (seo.render()) } }
689/// }
690/// ```
691///
692/// # Declared Content-Type
693///
694/// `content_type = "..."` sets the media type of the pre-rendered page:
695///
696/// ```ignore
697/// #[static_get("/logo.png", content_type = "application/octet-stream")]
698/// async fn logo() -> Vec<u8> {
699///     logo_bytes()
700/// }
701/// ```
702///
703/// Build records the value as written. It replaces any header the handler
704/// sets. Autumn does not swap it for the route extension type. Without the
705/// argument, Autumn records the handler header, and replaces the axum
706/// return-type defaults (`text/plain; charset=utf-8`, `application/octet-stream`)
707/// with the extension type.
708///
709/// The value must be a non-empty media type in visible ASCII. The type only
710/// applies to the page served from `dist/`. A live request that reaches the
711/// handler gets the handler header. Compression follows the declared type.
712#[proc_macro_attribute]
713pub fn static_get(attr: TokenStream, item: TokenStream) -> TokenStream {
714    autumn_macros_support::crate_path::with_override(attr.into(), item.into(), |attr, item| {
715        static_route::static_get_macro(attr, item)
716    })
717    .into()
718}
719
720/// Collect `#[scheduled]` task handlers into a `Vec<TaskInfo>`.
721///
722/// ```ignore
723/// let all_tasks = tasks![cleanup, nightly];
724/// ```
725#[proc_macro]
726pub fn tasks(input: TokenStream) -> TokenStream {
727    let _guard = autumn_macros_support::crate_path::set_target(None);
728    autumn_macros_support::crate_path::finalize(tasks_macro::tasks_macro(input.into())).into()
729}
730
731/// Collect `#[job]` handlers into a `Vec<JobInfo>`.
732#[proc_macro]
733pub fn jobs(input: TokenStream) -> TokenStream {
734    let _guard = autumn_macros_support::crate_path::set_target(None);
735    autumn_macros_support::crate_path::finalize(jobs_macro::jobs_macro(input.into())).into()
736}
737
738/// Collect `#[task]` handlers into a `Vec<OneOffTaskInfo>`.
739#[proc_macro]
740pub fn one_off_tasks(input: TokenStream) -> TokenStream {
741    let _guard = autumn_macros_support::crate_path::set_target(None);
742    autumn_macros_support::crate_path::finalize(one_off_tasks_macro::one_off_tasks_macro(
743        input.into(),
744    ))
745    .into()
746}
747
748/// Secure a route handler with authentication and optional role checks.
749///
750/// Applied before a route macro (`#[get]`, `#[post]`, etc.), this macro
751/// injects an authentication guard at the top of the handler. The guard
752/// checks the session for the configured auth key (default: `"user_id"`)
753/// and, when roles are specified, verifies the user's role matches.
754///
755/// Returns `401 Unauthorized` if not authenticated, or `403 Forbidden`
756/// if the user lacks the required role.
757///
758/// # Forms
759///
760/// - `#[secured]` -- require authentication only
761/// - `#[secured("admin")]` -- require a specific role
762/// - `#[secured("admin", "editor")]` -- require any of the listed roles
763///
764/// # Example
765///
766/// ```ignore
767/// use autumn_web::prelude::*;
768///
769/// #[get("/admin")]
770/// #[secured("admin")]
771/// async fn admin_panel() -> AutumnResult<&'static str> {
772///     Ok("welcome, admin")
773/// }
774/// ```
775#[proc_macro_attribute]
776pub fn secured(attr: TokenStream, item: TokenStream) -> TokenStream {
777    autumn_macros_support::crate_path::with_override(attr.into(), item.into(), |attr, item| {
778        secured::secured_macro(attr, item)
779    })
780    .into()
781}
782
783/// Declare a route handler as deliberately public (unauthenticated).
784///
785/// `#[public]` injects no runtime guard — it is a compile-time *marker* that
786/// records intent. The route macros surface it as `ApiDoc::public`, which the
787/// build-time security classifier (`autumn routes audit`) uses to distinguish
788/// a route that is *meant* to be open from one whose auth posture was simply
789/// never declared. Applying it makes an otherwise-unclassified route pass the
790/// audit gate, exactly like adding a [`#[secured]`](macro@secured) guard does.
791///
792/// # Example
793///
794/// ```ignore
795/// use autumn_web::prelude::*;
796///
797/// #[get("/health")]
798/// #[public]
799/// async fn health() -> &'static str { "ok" }
800/// ```
801#[proc_macro_attribute]
802pub fn public(attr: TokenStream, item: TokenStream) -> TokenStream {
803    autumn_macros_support::crate_path::with_override(attr.into(), item.into(), |attr, item| {
804        public::public_macro(attr, item)
805    })
806    .into()
807}
808
809/// Declare a read-path route as eligible to run in the edge capsule (#1790).
810///
811/// `#[edge]` injects no runtime guard and does not rewrite the handler
812/// signature — it is a compile-time *marker*, like
813/// [`#[public]`](macro@public). The route macro reads it back and emits an
814/// extra `__autumn_edge_route_{name}()` companion returning an
815/// `autumn_edge::EdgeRoute`, while gating the native (`autumn_web`) companions
816/// behind `#[cfg(not(target_arch = "wasm32"))]` so the same handler source
817/// compiles for both the origin binary and the `wasm32-wasip1` capsule.
818/// Marking a handler makes it *eligible*; listing it in
819/// [`edge_routes!`](macro@edge_routes) is what puts it in the capsule.
820///
821/// # Forms
822///
823/// - `#[edge]` — the handler needs no platform seam
824/// - `#[edge(needs(kv))]` — the handler reads the edge key-value cache
825///   (`autumn_edge::EdgeCache`), which the host must provide; a request
826///   arriving at an edge without that capability falls through to the origin
827///
828/// # Restrictions
829///
830/// The edge lane is read-path only and carries no session, auth, or database
831/// state, so these are compile errors:
832///
833/// - a method other than `#[get]`;
834/// - combining with `#[secured]`, `#[authorize]`, `#[step_up]`, or
835///   `#[throttle]` (in either attribute order);
836/// - `#[static_get]` (already pre-rendered), `#[ws]`, or `#[oauth2_callback]`.
837///
838/// # Example
839///
840/// ```ignore
841/// use autumn_edge::prelude::{EdgeCache, Path};
842/// use autumn_web::{edge, get};
843///
844/// #[get("/greet/{name}")]
845/// #[edge]
846/// async fn greet(Path(name): Path<String>) -> String {
847///     format!("Hello, {name}!")
848/// }
849///
850/// #[get("/note/{id}")]
851/// #[edge(needs(kv))]
852/// async fn note(Path(id): Path<String>, cache: EdgeCache) -> String {
853///     cache.get_string(&id).unwrap_or_else(|| "not cached".to_owned())
854/// }
855/// ```
856#[proc_macro_attribute]
857pub fn edge(attr: TokenStream, item: TokenStream) -> TokenStream {
858    autumn_macros_support::crate_path::with_override(attr.into(), item.into(), |attr, item| {
859        edge::edge_macro(attr, item)
860    })
861    .into()
862}
863
864/// Collect `#[edge]` handlers into a `Vec<EdgeRoute>` (#1790).
865///
866/// The edge-lane counterpart of [`routes!`](macro@routes): each entry resolves
867/// to the handler's `__autumn_edge_route_{name}()` companion, so a handler that
868/// was never marked `#[edge]` fails to resolve rather than silently vanishing
869/// from the capsule.
870///
871/// ```ignore
872/// use autumn_web::{edge, edge_routes, get};
873///
874/// #[get("/greet")]
875/// #[edge]
876/// async fn greet() -> &'static str { "hi" }
877///
878/// pub fn edge_routes() -> Vec<autumn_edge::EdgeRoute> {
879///     edge_routes![greet]
880/// }
881/// ```
882#[proc_macro]
883pub fn edge_routes(input: TokenStream) -> TokenStream {
884    let _guard = autumn_macros_support::crate_path::set_target(None);
885    autumn_macros_support::crate_path::finalize(edge_routes_macro::edge_routes_macro(input.into()))
886        .into()
887}
888
889/// Require fresh ("step-up") authentication before a route handler runs.
890///
891/// The handler is guarded by a freshness check on the session's
892/// `last_strong_auth_at` claim. When the claim is missing or older than
893/// `max_age` the request is handled as follows:
894///
895/// - **Browser clients** (no `application/json` in `Accept`): redirect to
896///   `/reauth?return_to=<current-path>`.
897/// - **API / JSON clients** (`Accept: application/json`): `401 Unauthorized`
898///   with an RFC 7807 problem-details body (`type` =
899///   `"https://autumn.rs/probs/step-up-required"`) and a
900///   `WWW-Authenticate: StepUp max-age=N` hint header.
901///
902/// # Forms
903///
904/// - `#[step_up]` — default max-age (5 minutes, or the global `[auth.step_up]`
905///   config override)
906/// - `#[step_up(max_age = "5m")]` — custom per-route max-age
907///
908/// # Example
909///
910/// ```ignore
911/// use autumn_web::prelude::*;
912///
913/// // Requires re-authentication within the last 5 minutes.
914/// #[delete("/account")]
915/// #[step_up]
916/// async fn destroy_account() -> AutumnResult<Redirect> {
917///     // ... delete account ...
918///     Ok(Redirect::to("/bye"))
919/// }
920///
921/// // Custom max-age.
922/// #[post("/auth/mfa/remove")]
923/// #[step_up(max_age = "2m")]
924/// async fn remove_mfa() -> AutumnResult<&'static str> {
925///     Ok("MFA removed")
926/// }
927/// ```
928#[proc_macro_attribute]
929pub fn step_up(attr: TokenStream, item: TokenStream) -> TokenStream {
930    autumn_macros_support::crate_path::with_override(attr.into(), item.into(), |attr, item| {
931        step_up::step_up_macro(attr, item)
932    })
933    .into()
934}
935
936/// Apply a per-route rate limit to a handler.
937///
938/// The handler is guarded by an additional rate limiter that composes with
939/// (and, for the annotated route, is stricter than) the global limiter
940/// configured under `[security.rate_limit]`. Requests denied by either
941/// limiter respond with `429 Too Many Requests` including a `Retry-After`
942/// header and the standard `x-ratelimit-*` headers.
943///
944/// # Forms
945///
946/// - `#[throttle(limit = 5, per = "1m")]` — inline limit; keying strategy
947///   matches the global limiter.
948/// - `#[throttle(limit = 5, per = "1m", key = "ip" | "principal" | "token")]`
949///   — inline limit with an explicit key strategy override.
950/// - `#[throttle("login")]` — reference a named limiter defined in
951///   `[security.rate_limit.named.login]`.
952///
953/// # Example
954///
955/// ```ignore
956/// use autumn_web::prelude::*;
957///
958/// #[post("/login")]
959/// #[throttle(limit = 5, per = "1m", key = "ip")]
960/// async fn login() -> AutumnResult<&'static str> {
961///     Ok("welcome back")
962/// }
963/// ```
964///
965/// # Limitations
966///
967/// Like the sibling `#[secured]` / `#[step_up]` guards it mirrors, the throttle
968/// check runs inside the handler after `FromRequestParts` extractors, but body
969/// extractors (`Json` / `Form` / `Multipart`) are parsed by Axum *before* the
970/// throttle check, so an over-limit client can still incur request-body parsing
971/// before receiving its `429`. For hard pre-body protection, combine with the
972/// global limiter layer under `[security.rate_limit]`.
973///
974/// # Attribute ordering
975///
976/// `#[throttle]` may be written above or below the route method attribute
977/// (`#[get]` / `#[post]` / …) — both orders enforce throttling identically
978/// (including idempotency-replay accounting) and both produce the same
979/// `OpenAPI` response schema. When `#[throttle]` expands first it rewrites
980/// the return type to `Response` (like the sibling `#[secured]` / `#[step_up]`
981/// / `#[authorize]` guards), but the route macro recovers the original
982/// `Json<T>` return type from the guard's generated body, so response-schema
983/// inference does not depend on expansion order (#1677).
984#[proc_macro_attribute]
985pub fn throttle(attr: TokenStream, item: TokenStream) -> TokenStream {
986    autumn_macros_support::crate_path::with_override(attr.into(), item.into(), |attr, item| {
987        throttle::throttle_macro(attr, item)
988    })
989    .into()
990}
991
992/// Bound the number of database queries a handler can issue — at compile time.
993///
994/// `#[query_budget(N)]` fails the build when any statically reachable path
995/// through the handler can issue more than `N` queries. The canonical case is
996/// the N+1: a repository or `Db` call inside a loop over a runtime-sized
997/// collection. Because the gate runs during `cargo build`, it fires on every
998/// branch — including the ones no test exercises.
999///
1000/// # Example
1001///
1002/// ```ignore
1003/// use autumn_web::{get, query_budget};
1004///
1005/// #[get("/posts")]
1006/// #[query_budget(2)]
1007/// async fn index(repo: PgPostRepository) -> AutumnResult<Markup> {
1008///     let posts = repo.find_all().await?;                              // 1
1009///     let posts = repo.preload(posts, Post::preload().author()).await?; // 2
1010///     Ok(render(&posts))
1011/// }
1012/// ```
1013///
1014/// Written with a per-row `repo.find_author(...)` inside a `for` loop instead,
1015/// the same handler fails to compile with a diagnostic pointing at the loop.
1016///
1017/// # How the count is computed
1018///
1019/// * Straight-line statements **sum**; `if` / `match` arms take the
1020///   **maximum** (the worst reachable path).
1021/// * A loop whose body issues a query is **unbounded**, unless the iterable
1022///   has a literal compile-time bound (`for _ in 0..3`), in which case the
1023///   body cost is multiplied. A path that leaves the loop is paid once.
1024/// * A method chain rooted at a `Db` / repository handle is **one** query,
1025///   however many builder methods (`on_primary()`, `scoped()`, …) it carries.
1026/// * `.preload(rows, Post::preload().author().tags())` costs **one query per
1027///   association** — the batched `WHERE ... IN (...)` loads.
1028/// * A path that leaves early (`return`, `break`, `continue`) does not include
1029///   the cost of the code it skips.
1030/// * Anything opaque — a helper function or associated function handed the
1031///   handle (`Post::published(&mut db)`), a macro body that names it, a
1032///   closure that may run per element — is **reported**, never assumed
1033///   query-free.
1034///
1035/// # Escape hatches
1036///
1037/// * `#[query_budget(unbounded, reason = "…")]` — opt the whole handler out.
1038/// * `#[query_cost(N)]` on a statement — declare an opaque call's cost.
1039/// * `#[query_exempt(reason = "…")]` on a statement — drop it from the ledger.
1040///
1041/// Both statement annotations are consumed by this macro and never reach
1042/// rustc. See `docs/guide/query-budgets.md` for the full guide.
1043#[proc_macro_attribute]
1044pub fn query_budget(attr: TokenStream, item: TokenStream) -> TokenStream {
1045    autumn_macros_support::crate_path::with_override(attr.into(), item.into(), |attr, item| {
1046        query_budget::query_budget_macro(attr, item)
1047    })
1048    .into()
1049}
1050
1051/// Declare a handler agent-operable, under a named authority grant.
1052///
1053/// `#[agent_operable(grant = RefundDrafter)]` walks the handler body, derives
1054/// the effects it can prove — row writes, unbounded writes, cross-tenant
1055/// access, outbound HTTP, webhook fan-out, background jobs — and fails the
1056/// build when the named `authority_grant!` does not allow one of them. The
1057/// check is a `const` assertion respanned onto the offending call site, so it
1058/// works across crates and fires during `cargo build` on every branch,
1059/// including the ones no test exercises.
1060///
1061/// # Example
1062///
1063/// ```ignore
1064/// use autumn_web::prelude::*;
1065///
1066/// autumn_web::authority_grant! {
1067///     /// Draft-only refund authority for the support agent.
1068///     pub RefundDrafter {
1069///         writes: [Refund],
1070///         outbound: ["https://api.stripe.com/v1/refunds"],
1071///         jobs: [NotifyFinanceJob],
1072///         reversibility: compensable,
1073///     }
1074/// }
1075///
1076/// #[post("/refunds")]
1077/// #[api_doc(mcp, summary = "Draft a refund")]
1078/// #[agent_operable(grant = RefundDrafter)]
1079/// async fn draft_refund(
1080///     repo: PgRefundRepository,
1081///     payouts: PgPayoutRepository,
1082///     client: Client,
1083///     Json(body): Json<NewRefund>,
1084/// ) -> AutumnResult<Json<Refund>> {
1085///     let refund = repo.create(&body).await?;               // allowed: `writes: [Refund]`
1086///     client.post("https://api.stripe.com/v1/refunds")      // allowed: `outbound: [...]`
1087///         .json(&refund)
1088///         .send()
1089///         .await?;
1090///     NotifyFinanceJob::enqueue(NotifyFinanceArgs { refund_id: refund.id }).await?;
1091///     Ok(Json(refund))
1092/// }
1093/// ```
1094///
1095/// Adding `payouts.delete_all().await?` to that body fails the build: `payouts`
1096/// is a tracked repository handle, and the grant allows no unbounded write to
1097/// `Payout`.
1098///
1099/// # What is proved, and what is only declared
1100///
1101/// * A write, unbounded write or cross-tenant access reached through a handle
1102///   named in the signature is **proved**.
1103/// * An outbound call with a literal absolute URL is **proved**; one whose host
1104///   is resolved from config through a named client is **declared**.
1105/// * A webhook dispatch delivers to subscriber-supplied URLs, so it is granted
1106///   by topic (`webhooks: ["refund.drafted"]`), never by URL prefix.
1107/// * `rate` / `spend` caps are **declared**: this slice records them, it does
1108///   not enforce them.
1109/// * Anything opaque — a helper handed the handle, a `format!`-built URL, a
1110///   `tokio::spawn` that detaches the effect from the request it is audited
1111///   under — is reported, never assumed effect-free. A helper is opaque
1112///   whether it is free (`wipe(repo)`) or associated (`Billing::wipe(repo)`):
1113///   an associated function is another function's business too, and a static
1114///   finder that really only reads (`Post::find_published(&mut db)`) is
1115///   discharged with `#[agent_effect(none, reason = "…")]` rather than
1116///   assumed. A local alias for an effect verb (`let schedule =
1117///   NotifyFinanceJob::enqueue;`) is classified against the path it names.
1118///
1119/// # Escape hatch
1120///
1121/// `#[agent_effect(...)]` on a statement declares what the analysis cannot
1122/// read: `#[agent_effect(writes(Refund), reason = "…")]`, or
1123/// `#[agent_effect(none, reason = "…")]` for a statement verified
1124/// effect-free. Declared effects are still checked against the grant — the
1125/// hatch declares, it never grants — and a `reason` is mandatory. The
1126/// annotation is consumed by this macro and never reaches rustc.
1127///
1128/// See `docs/guide/agent-authority.md` for the full guide.
1129#[proc_macro_attribute]
1130pub fn agent_operable(attr: TokenStream, item: TokenStream) -> TokenStream {
1131    autumn_macros_support::crate_path::with_override(attr.into(), item.into(), |attr, item| {
1132        agent_authority::agent_operable_macro(attr, item)
1133    })
1134    .into()
1135}
1136
1137/// Gate a route handler on a named feature flag.
1138///
1139/// If the flag is disabled for the current actor, the handler responds with
1140/// `404 Not Found` by default. Provide a `fallback` function to return a
1141/// custom response instead.
1142///
1143/// The flag key is resolved against the `FeatureFlagService` stored in the
1144/// `AppState` extensions. Unknown flags are treated as **disabled**
1145/// (fail-closed).
1146///
1147/// # Forms
1148///
1149/// - `#[feature_flag("key")]` — return 404 when disabled
1150/// - `#[feature_flag("key", fallback = my_fn)]` — call `my_fn()` when disabled
1151///
1152/// # Example
1153///
1154/// ```ignore
1155/// use autumn_web::prelude::*;
1156///
1157/// #[get("/beta")]
1158/// #[feature_flag("beta_dashboard")]
1159/// async fn beta_dashboard() -> Markup {
1160///     html! { h1 { "Beta Dashboard" } }
1161/// }
1162/// ```
1163///
1164#[proc_macro_attribute]
1165pub fn feature_flag(attr: TokenStream, item: TokenStream) -> TokenStream {
1166    autumn_macros_support::crate_path::with_override(attr.into(), item.into(), |attr, item| {
1167        feature_flag::feature_flag_macro(attr, item)
1168    })
1169    .into()
1170}
1171
1172/// Enforce a record-level authorization policy on a route handler.
1173///
1174/// Resolves the `Policy`
1175/// registered for the named resource type and calls the matching
1176/// action method. Short-circuits with the configured deny response
1177/// (default `404`, optionally `403`) before the handler body runs.
1178///
1179/// Coexists with `#[secured]`: `#[secured]` answers "are you in?",
1180/// `#[authorize]` answers "are you allowed to act on *this record*?"
1181///
1182/// # Forms
1183///
1184/// ```ignore
1185/// // Resource arg is auto-detected by snake-cased type name (Post -> `post`).
1186/// #[authorize("update", resource = Post)]
1187/// async fn update_post(post: Post) -> AutumnResult<...> { ... }
1188///
1189/// // Explicit binding name (overrides the snake-case default).
1190/// #[authorize("delete", resource = Post, from = target)]
1191/// async fn destroy(target: Post) -> AutumnResult<...> { ... }
1192/// ```
1193#[proc_macro_attribute]
1194pub fn authorize(attr: TokenStream, item: TokenStream) -> TokenStream {
1195    autumn_macros_support::crate_path::with_override(attr.into(), item.into(), |attr, item| {
1196        authorize::authorize_macro(attr, item)
1197    })
1198    .into()
1199}
1200
1201/// Collect `#[static_get]` handlers into a `Vec<StaticRouteMeta>`.
1202///
1203/// ```ignore
1204/// use autumn_web::prelude::*;
1205///
1206/// #[static_get("/about")]
1207/// async fn about() -> &'static str { "About" }
1208///
1209/// let metas = static_routes![about];
1210/// ```
1211#[proc_macro]
1212pub fn static_routes(input: TokenStream) -> TokenStream {
1213    let _guard = autumn_macros_support::crate_path::set_target(None);
1214    autumn_macros_support::crate_path::finalize(static_routes_macro::static_routes_macro(
1215        input.into(),
1216    ))
1217    .into()
1218}
1219
1220/// Mark a typed handler as a service endpoint (issue #1755).
1221///
1222/// **Experimental** — see `STABILITY.md`.
1223///
1224/// Emits a marker type — `<name>_endpoint` — implementing
1225/// `autumn_web::wire::Endpoint`, and writes the endpoint's JSON wire
1226/// descriptor as a build artifact. The handler itself is untouched.
1227///
1228/// Everything comes from the handler's own signature and the route attribute
1229/// below it: the `Json<T>` parameter is the request shape, the `Json<T>` in the
1230/// return type is the response shape, and the route attribute supplies the
1231/// method and path.
1232///
1233/// # Placement
1234///
1235/// `#[endpoint]` must sit **above** the route attribute. A route attribute
1236/// placed outermost expands first and rewrites the signature, leaving nothing
1237/// to read.
1238///
1239/// ```rust,ignore
1240/// #[endpoint(service = "catalog")]
1241/// #[get("/items/{id}")]
1242/// async fn get_item(id: Path<String>) -> AutumnResult<Json<Item>> { … }
1243/// ```
1244///
1245/// # Arguments
1246///
1247/// | Argument | Required | Description |
1248/// |---|---|---|
1249/// | `service` | yes | The service this endpoint belongs to |
1250/// | `name` | no | Endpoint name; defaults to the handler's function name |
1251#[proc_macro_attribute]
1252pub fn endpoint(attr: TokenStream, item: TokenStream) -> TokenStream {
1253    autumn_macros_support::crate_path::with_override(attr.into(), item.into(), |attr, item| {
1254        wire::endpoint::endpoint_macro(attr, &item)
1255    })
1256    .into()
1257}
1258
1259/// Generate a typed client for another Autumn service's endpoints (issue #1755).
1260///
1261/// **Experimental** — see `STABILITY.md`.
1262///
1263/// ```rust,ignore
1264/// wire_client! {
1265///     name = CatalogClient,
1266///     endpoints = [
1267///         catalog::get_item_endpoint(id),
1268///         catalog::create_item_endpoint,
1269///     ],
1270/// }
1271/// ```
1272///
1273/// Each entry names an endpoint marker and, in parentheses, the path
1274/// parameters its route takes. Request and response types come from the
1275/// marker's associated types, so they are the callee's own types. A const
1276/// assertion holds the declared path parameters to the endpoint's real path.
1277#[proc_macro]
1278pub fn wire_client(input: TokenStream) -> TokenStream {
1279    let _guard = autumn_macros_support::crate_path::set_target(None);
1280    autumn_macros_support::crate_path::finalize(wire::client::wire_client_macro(input.into()))
1281        .into()
1282}
1283
1284/// Check every service call in a function against the callee's contract
1285/// (issue #1755).
1286///
1287/// **Experimental** — see `STABILITY.md`.
1288///
1289/// ```rust,ignore
1290/// #[contract_checked(client = CatalogClient)]
1291/// async fn page(catalog: CatalogClient, id: Path<String>) -> AutumnResult<Markup> {
1292///     let item = catalog.get_item(&*id, NoBody).await?;
1293///     Ok(html! { h1 { (item.name) } })
1294/// }
1295/// ```
1296///
1297/// Every response field the function names, and every request field an inline
1298/// literal sets, becomes a const assertion against the callee's own field
1299/// table. A field the endpoint no longer produces — or a required field a
1300/// `..rest` initializer omits — fails the build at the call site.
1301///
1302/// Repeat `client = …` for a function that calls more than one service. A
1303/// declared client with no value in the function is an error, so the attribute
1304/// can never pass by checking nothing.
1305#[proc_macro_attribute]
1306pub fn contract_checked(attr: TokenStream, item: TokenStream) -> TokenStream {
1307    autumn_macros_support::crate_path::with_override(attr.into(), item.into(), |attr, item| {
1308        wire::checked::contract_checked_macro(attr, &item)
1309    })
1310    .into()
1311}
1312
1313/// Derive a type's serde-visible wire shape (issue #1755).
1314///
1315/// **Experimental** — see `STABILITY.md`.
1316///
1317/// Emits two const field tables — what the type puts on the wire and what it
1318/// takes off it — and writes the type's JSON descriptor as a build artifact.
1319/// `#[contract_checked]` asserts against those tables.
1320///
1321/// The two tables differ under directional serde attributes, and that is the
1322/// point: a field carrying `#[serde(skip_serializing)]` is accepted but never
1323/// produced, so a caller reading it is broken even though the code compiles.
1324///
1325/// Refused rather than guessed at: generics, enums, tuple structs,
1326/// `#[serde(flatten)]`, and split `rename(serialize = …, deserialize = …)`.
1327#[proc_macro_derive(WireShape)]
1328pub fn derive_wire_shape(input: TokenStream) -> TokenStream {
1329    let _guard = autumn_macros_support::crate_path::set_target(None);
1330    let parsed = syn::parse_macro_input!(input as syn::DeriveInput);
1331    let out = match wire::shape::derive_wire_shape(&parsed) {
1332        Ok(ts) => ts,
1333        Err(err) => err.to_compile_error(),
1334    };
1335    autumn_macros_support::crate_path::finalize(out).into()
1336}
1337
1338/// Cache the return value of a function based on its arguments.
1339///
1340/// Wraps a function with an in-memory cache backed by a per-function
1341/// static `Cache` (from `autumn_web::cache::Cache`). Key arguments
1342/// must implement `Hash + Eq + Clone`; the return type must be `Clone`.
1343///
1344/// # Attributes
1345///
1346/// | Attribute | Example | Description |
1347/// |-----------|---------|-------------|
1348/// | `ttl` | `"5m"` | Time-to-live per entry (uses `parse_duration` syntax) |
1349/// | `max` | `1000` | Max entries; oldest evicted on overflow |
1350/// | `result` | (flag) | Only cache `Ok` values; pass `Err` through uncached |
1351/// | `key` | `key(tenant_id)` | Build the key from *these* parameters only |
1352/// | `reads` | `reads(Post, Comment)` | Declared cache-coherence dependency set |
1353/// | `acknowledge_stale` | `"5s TTL is tight enough"` | Opt out of the coherence gate |
1354/// | `scope` | `"Products"` | Enclosing type; keeps same-named associated functions apart |
1355///
1356/// # Associated functions
1357///
1358/// The cache key holds the module and function name. Two impls in one module
1359/// with a same-named `#[cached]` function would share keys. Put
1360/// [`cached_impl`] on each `impl` block, or set `scope` by hand.
1361///
1362/// # Cache coherence (issue #1716)
1363///
1364/// Every annotated function publishes which models its value is derived from,
1365/// and `autumn cache audit` fails the build when a `#[repository]` write can
1366/// leave that value stale with no invalidation covering the pair. `reads(...)`
1367/// declares the dependency set; without it the macro derives what it can from
1368/// the signature and body, and a function nothing could be recovered from is
1369/// recorded as `undetermined` — reported, never gated. See
1370/// `docs/guide/cache-coherence.md`.
1371///
1372/// # Examples
1373///
1374/// ```ignore
1375/// use autumn_web::cached;
1376///
1377/// // Cache with 5-minute TTL, max 100 entries, only cache Ok values
1378/// #[cached(ttl = "5m", max = 100, result)]
1379/// async fn get_user(id: i64) -> AutumnResult<User> {
1380///     db.find(id).await
1381/// }
1382///
1383/// // A repository-backed read: the handle is not part of the value's
1384/// // identity, so `key(...)` keeps it out of the cache key, and `reads(...)`
1385/// // tells the coherence gate what a write to `Project` would strand.
1386/// #[cached(ttl = "30s", key(tenant_id), reads(Project), result)]
1387/// async fn project_count(tenant_id: String, repo: &PgProjectRepository)
1388///     -> AutumnResult<i64>
1389/// {
1390///     repo.count().await
1391/// }
1392///
1393/// // Cache forever with no size limit
1394/// #[cached]
1395/// async fn get_config() -> Vec<String> {
1396///     load_config_from_disk()
1397/// }
1398/// ```
1399#[proc_macro_attribute]
1400pub fn cached(attr: TokenStream, item: TokenStream) -> TokenStream {
1401    autumn_macros_support::crate_path::with_override(attr.into(), item.into(), |attr, item| {
1402        cached::cached_macro(attr, item)
1403    })
1404    .into()
1405}
1406
1407/// Scope every `#[cached]` method of an `impl` block by its type.
1408///
1409/// Adds `scope = "<Type>"` to each `#[cached]` method, so `Products::get` and
1410/// `Reviews::get` in one module get different cache keys.
1411///
1412/// ```ignore
1413/// #[autumn_web::cached_impl]
1414/// impl Products {
1415///     #[cached]
1416///     async fn get(id: i64) -> String { /* .. */ }
1417/// }
1418/// ```
1419#[proc_macro_attribute]
1420pub fn cached_impl(attr: TokenStream, item: TokenStream) -> TokenStream {
1421    let (crate_override, attr) =
1422        match autumn_macros_support::crate_path::extract_crate_override(attr.into()) {
1423            Ok(pair) => pair,
1424            Err(err) => return err.into(),
1425        };
1426    let _guard = autumn_macros_support::crate_path::set_target(crate_override.as_deref());
1427    autumn_macros_support::crate_path::finalize(cached_impl::cached_impl_macro(
1428        attr,
1429        item.into(),
1430        crate_override.as_deref(),
1431    ))
1432    .into()
1433}
1434
1435/// Enrich a route handler's auto-generated `OpenAPI` documentation.
1436///
1437/// Applied on top of a route macro (`#[get]`, `#[post]`, etc.), this
1438/// attribute lets you override or add documentation fields that cannot
1439/// be inferred from the handler signature (summaries, descriptions,
1440/// tags, custom success status codes).
1441///
1442/// The route macro consumes this attribute and folds the metadata into
1443/// the route's `ApiDoc`. When no route macro is applied, the attribute
1444/// is a no-op.
1445///
1446/// # Supported keys
1447///
1448/// | Key | Type | Effect |
1449/// |-----|------|--------|
1450/// | `summary` | string | Short one-line description |
1451/// | `description` | string | Longer multi-line description |
1452/// | `tag` | string | Single `OpenAPI` tag for grouping |
1453/// | `tags` | `[string, ...]` | Multiple `OpenAPI` tags |
1454/// | `operation_id` | string | Override the default operation id |
1455/// | `status` | integer | Success HTTP status code (defaults to `200`) |
1456/// | `hidden` | flag / bool | Exclude the route from the generated spec |
1457/// | `mcp` | flag / bool | Expose this endpoint as an MCP tool (`mcp = false` force-excludes it). Requires the `mcp` feature and a `mount_mcp` call. |
1458/// | `stream` | flag / bool | With `mcp`: the tool returns an `Sse` stream, sent as MCP progress. |
1459/// | `task` | flag / bool | With `mcp`: the tool can return a task handle (MCP Tasks extension). |
1460/// | `core` | flag / bool | With `mcp`: a core tool. A client that defers tools loads it first. |
1461/// | `webmcp` | flag / bool | Expose this endpoint as a `WebMCP` tool in the page (`webmcp = false` force-excludes it). `mcp` does not imply it. Requires the `webmcp` feature and a `mount_webmcp` call. |
1462///
1463/// # Examples
1464///
1465/// ```ignore
1466/// use autumn_web::prelude::*;
1467///
1468/// #[get("/users/{id}")]
1469/// #[api_doc(summary = "Fetch a user by id", tag = "users")]
1470/// async fn get_user(Path(id): Path<i32>) -> String {
1471///     format!("User {id}")
1472/// }
1473///
1474/// #[post("/users")]
1475/// #[api_doc(description = "Create a new user", status = 201)]
1476/// async fn create_user(Json(req): Json<serde_json::Value>) -> Json<serde_json::Value> {
1477///     Json(req)
1478/// }
1479///
1480/// #[get("/internal/metrics")]
1481/// #[api_doc(hidden)]
1482/// async fn metrics() -> &'static str { "" }
1483/// ```
1484#[proc_macro_attribute]
1485pub fn api_doc(attr: TokenStream, item: TokenStream) -> TokenStream {
1486    // Rust expands attribute macros top-down (outermost first), so if the
1487    // user writes
1488    //
1489    //   #[api_doc(summary = "...")]
1490    //   #[get("/x")]
1491    //   async fn handler() { ... }
1492    //
1493    // this macro fires BEFORE `#[get]` and would strip `#[api_doc]` from
1494    // the item — the route macro would then never see the overrides.
1495    //
1496    // To support both orderings, we detect any pending route attribute
1497    // (`get`, `post`, etc.) sitting below us and reorder: we remove the
1498    // route attribute and emit it as the NEW outermost attribute, and
1499    // we re-attach `#[api_doc(...)]` to the function body. Rust then
1500    // expands the route macro next, which finds and consumes the
1501    // preserved `#[api_doc]` via the usual attribute-list walk.
1502    //
1503    // `#[api_doc(...)]` never itself emits an `::autumn_web` path — it only
1504    // reorders attributes for the paired route macro to expand next, and
1505    // that route macro's own `crate = "..."` (see `get`, `post`, etc.) is
1506    // what actually governs the combined expansion. Still strip a `crate =
1507    // "..."` given here (rather than leaving it for the reordering below to
1508    // re-embed and only later fail with a confusing raw parse error out of
1509    // `api_doc::extract`), so every attribute macro accepts and validates
1510    // the argument uniformly even though this one has nothing to apply it
1511    // to.
1512    let (_crate_override, attr) =
1513        match autumn_macros_support::crate_path::extract_crate_override(attr.into()) {
1514            Ok(pair) => pair,
1515            Err(err) => return err.into(),
1516        };
1517    let out: proc_macro2::TokenStream = api_doc_standalone(attr.into(), item).into();
1518    let _guard = autumn_macros_support::crate_path::set_target(None);
1519    autumn_macros_support::crate_path::finalize(out).into()
1520}
1521
1522const ROUTE_ATTR_NAMES: &[&str] = &["get", "post", "put", "delete", "patch", "static_get", "ws"];
1523
1524/// Return `true` when an attribute names one of the Autumn route macros.
1525///
1526/// We match on the **last** path segment so qualified forms like
1527/// `#[autumn_web::get("/x")]`, `#[autumn_macros::post("/x")]`, or
1528/// even `#[crate::get("/x")]` are recognized alongside the bare
1529/// `#[get("/x")]`. Unqualified identifiers are covered by the same
1530/// logic because their path has a single segment.
1531fn is_route_attribute(attr: &syn::Attribute) -> bool {
1532    attr.path()
1533        .segments
1534        .last()
1535        .map(|segment| segment.ident.to_string())
1536        .is_some_and(|name| ROUTE_ATTR_NAMES.contains(&name.as_str()))
1537}
1538
1539fn api_doc_standalone(attr: TokenStream, item: TokenStream) -> TokenStream {
1540    let attr_ts: proc_macro2::TokenStream = attr.into();
1541    let mut input_fn: syn::ItemFn = match syn::parse(item.clone()) {
1542        Ok(f) => f,
1543        // Not a function (e.g. applied to a struct) — leave it alone so
1544        // the user sees the usual "expected function" error from rustc.
1545        Err(_) => return item,
1546    };
1547
1548    let route_idx = input_fn.attrs.iter().position(is_route_attribute);
1549
1550    let Some(idx) = route_idx else {
1551        // Standalone `#[api_doc]` with no paired route macro is a no-op;
1552        // route metadata is only emitted through route macros.
1553        return quote::quote! { #input_fn }.into();
1554    };
1555
1556    let route_attr = input_fn.attrs.remove(idx);
1557    let preserved: syn::Attribute = syn::parse_quote! {
1558        #[api_doc(#attr_ts)]
1559    };
1560    input_fn.attrs.insert(0, preserved);
1561
1562    quote::quote! {
1563        #route_attr
1564        #input_fn
1565    }
1566    .into()
1567}
1568
1569/// Annotate an async function as a WebSocket route handler.
1570///
1571/// The function follows the **two-function pattern**: it runs at HTTP
1572/// upgrade time (with access to Axum extractors) and returns a closure
1573/// implementing `WsHandler` (from `autumn_web::ws::WsHandler`) that handles the live WebSocket connection.
1574///
1575/// The macro generates a GET route that performs the WebSocket upgrade,
1576/// so it integrates seamlessly with `routes![]`.
1577///
1578/// # Examples
1579///
1580/// ```ignore
1581/// use autumn_web::prelude::*;
1582/// use autumn_web::ws::{WebSocket, Message, WsHandler};
1583///
1584/// // Minimal echo handler
1585/// #[ws("/echo")]
1586/// async fn echo() -> impl WsHandler {
1587///     |mut socket: WebSocket| async move {
1588///         while let Some(Ok(msg)) = socket.recv().await {
1589///             if let Message::Text(text) = msg {
1590///                 socket.send(Message::Text(text)).await.ok();
1591///             }
1592///         }
1593///     }
1594/// }
1595///
1596/// // With extractors (runs before upgrade)
1597/// #[ws("/chat")]
1598/// async fn chat(state: AppState) -> impl WsHandler {
1599///     let channels = state.channels().clone();
1600///     |mut socket: WebSocket| async move {
1601///         // use channels + socket
1602///     }
1603/// }
1604/// ```
1605#[proc_macro_attribute]
1606pub fn ws(attr: TokenStream, item: TokenStream) -> TokenStream {
1607    autumn_macros_support::crate_path::with_override(attr.into(), item.into(), |attr, item| {
1608        ws::ws_macro(attr, item)
1609    })
1610    .into()
1611}
1612
1613/// Translate an i18n key, with **compile-time validation** that the key
1614/// exists in the default locale's `.ftl` file.
1615///
1616/// Re-exported as `autumn_web::t!` (and `autumn_web::prelude::t!`) when the
1617/// `i18n` feature is enabled on `autumn-web`.
1618///
1619/// # Forms
1620///
1621/// ```ignore
1622/// // Without args:
1623/// t!(locale, "welcome.title")
1624/// // With named args (Project Fluent's `{ $name }` placeable syntax):
1625/// t!(locale, "welcome.greeting", name = "Ada")
1626/// ```
1627///
1628/// # Compile-time behaviour
1629///
1630/// At expansion time the macro reads `$CARGO_MANIFEST_DIR/i18n/<default>.ftl`
1631/// (where `<default>` is the value of the `AUTUMN_I18N_DEFAULT_LOCALE`
1632/// environment variable, defaulting to `"en"`). If the key is not present,
1633/// the macro emits a `compile_error!` pointing at the literal so the build
1634/// fails with a clear diagnostic — including a "did you mean" suggestion
1635/// for typos within Levenshtein distance 3.
1636///
1637/// If the file does not exist (e.g. an app that just enabled the feature
1638/// flag and has not yet authored translations), the macro degrades to a
1639/// pure runtime call so the build still succeeds. The runtime path will
1640/// produce the visible `{$key}` marker on miss.
1641#[proc_macro]
1642pub fn t(input: TokenStream) -> TokenStream {
1643    let _guard = autumn_macros_support::crate_path::set_target(None);
1644    autumn_macros_support::crate_path::finalize(i18n::t_macro(input.into())).into()
1645}
1646
1647/// Turn a plain state enum into a statically-verified lifecycle.
1648///
1649/// Given a declared `initial` state, one or more `terminal` states, and a set
1650/// of `transitions`, `#[lifecycle]` preserves the original enum and appends:
1651///
1652/// 1. Metadata consts (`LIFECYCLE_INITIAL`, `LIFECYCLE_TERMINALS`,
1653///    `LIFECYCLE_STATES`, `LIFECYCLE_TRANSITIONS`) and a `can_transition_to`
1654///    runtime check on the enum. Because these reference the enum's own
1655///    variants, a declared state that is not a real variant is a compile error.
1656/// 2. A typestate transition module named after the enum in `snake_case`, whose
1657///    `Machine<S>` exposes a consuming `to_<target>` method *only* for declared
1658///    edges — firing an undeclared transition does not compile.
1659///
1660/// The declared graph is proven structurally sound at compile time: a state
1661/// unreachable from `initial`, or a reachable non-terminal state with no path
1662/// to a terminal, is a compile error naming the variant.
1663///
1664/// # Example
1665///
1666/// ```ignore
1667/// use autumn_web::lifecycle;
1668///
1669/// #[lifecycle(
1670///     initial = Draft,
1671///     terminal(Archived),
1672///     transitions(
1673///         Draft -> Published,
1674///         Published -> Archived,
1675///         Published -> Draft,
1676///     )
1677/// )]
1678/// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1679/// pub enum ArticleState { Draft, Published, Archived }
1680///
1681/// let m = article_state::Machine::<article_state::Draft>::start();
1682/// let m = m.to_published();      // only declared edges exist as methods
1683/// assert_eq!(m.current(), ArticleState::Published);
1684/// assert!(ArticleState::Draft.can_transition_to(&ArticleState::Published));
1685/// ```
1686#[proc_macro_attribute]
1687pub fn lifecycle(attr: TokenStream, item: TokenStream) -> TokenStream {
1688    autumn_macros_support::crate_path::with_override(attr.into(), item.into(), |attr, item| {
1689        lifecycle::lifecycle_macro(attr, item)
1690    })
1691    .into()
1692}
1693
1694/// Declare a business-time obligation on a struct (issue #1826).
1695///
1696/// It adds `<name>_obligation(&self) -> autumn_web::sla::Obligation`.
1697///
1698/// # Arguments
1699///
1700/// - `name = ident` (required): the obligation name.
1701/// - `within = "..."` (required): the budget, such as `"2 business days"`.
1702///   A bad budget is a compile error.
1703/// - `starts = field` (required): a `DateTime<Utc>` field. The clock starts
1704///   there.
1705/// - `calendar = "..."`: the calendar name. The default is `"default"`.
1706/// - `met = field`: an `Option<DateTime<Utc>>` field. Your code sets it when
1707///   the subject meets the obligation.
1708/// - `zone = field`: a time zone field (`Tz`, an IANA name, or an `Option`).
1709/// - `subject = field`: the identity field. The default is `id`. The subject
1710///   is the `snake_case` type name, a colon, and the field value.
1711///
1712/// # Example
1713///
1714/// ```ignore
1715/// use autumn_web::obligation;
1716///
1717/// #[obligation(name = first_response, within = "2 business days", calendar = "support",
1718///              starts = opened_at, met = responded_at)]
1719/// pub struct Ticket {
1720///     pub id: i64,
1721///     pub opened_at: chrono::DateTime<chrono::Utc>,
1722///     pub responded_at: Option<chrono::DateTime<chrono::Utc>>,
1723/// }
1724/// ```
1725#[proc_macro_attribute]
1726pub fn obligation(attr: TokenStream, item: TokenStream) -> TokenStream {
1727    autumn_macros_support::crate_path::with_override(attr.into(), item.into(), |attr, item| {
1728        obligation::obligation_macro(attr, item)
1729    })
1730    .into()
1731}
1732
1733#[cfg(test)]
1734mod rename_pipeline_tests {
1735    //! Cross-pipeline rename contract (#1828): every macro's expansion must
1736    //! rewrite `::autumn_web` token paths to the active crate target
1737    //! (`finalize`), and the idempotency replay-guard recognizer must accept
1738    //! the renamed target. These tests couple core pipelines (`#[get]`,
1739    //! `#[authorize]`/`#[secured]`) with the shared path rewriting, so they
1740    //! live with the core crate; the `#[model]`/`#[repository]` halves of
1741    //! this contract moved with those macros into their own crates.
1742    use autumn_macros_support::crate_path::{finalize, set_target};
1743    use quote::quote;
1744
1745    fn ts_string(ts: &proc_macro2::TokenStream) -> String {
1746        ts.to_string()
1747    }
1748
1749    /// No genuine `::autumn_web` *token* path (crate-root anchored) may
1750    /// survive `finalize`. `to_string()` renders a real `:: Ident ::` token
1751    /// sequence with spaces around the identifier; a doc comment or string
1752    /// literal's *contents* render with no such surrounding space, so this
1753    /// specifically will not (and must not) flag those.
1754    fn assert_no_leaked_autumn_web_token_path(s: &str) {
1755        assert!(
1756            !s.contains(":: autumn_web"),
1757            "leaked `::autumn_web` token path in: {s}"
1758        );
1759    }
1760
1761    #[test]
1762    fn route_macro_pipeline_has_no_leaked_autumn_web_after_override() {
1763        let _guard = set_target(Some("renamed_autumn_web"));
1764        let generated = crate::route::route_macro(
1765            "GET",
1766            "get",
1767            quote! { "/users/{id}", seo(title = "User") },
1768            quote! {
1769                async fn show_user(Path(id): Path<i64>) -> AutumnResult<Json<User>> {
1770                    Ok(Json(repo.find_by_id(id).await?))
1771                }
1772            },
1773        );
1774        let rewritten = finalize(generated);
1775        let s = ts_string(&rewritten);
1776        assert_no_leaked_autumn_web_token_path(&s);
1777        assert!(s.contains("renamed_autumn_web"), "got: {s}");
1778    }
1779
1780    /// Regression test for the exact scenario a Codex review on #2552 found:
1781    /// stacking `#[authorize]` above `#[secured]` under a rename must still
1782    /// let the route macro recognize the replay guard `#[authorize]`'s own
1783    /// (already-finalized, already-renamed) expansion injected, rather than
1784    /// missing it because the recognizer only knew the literal
1785    /// `"autumn_web"`.
1786    #[test]
1787    fn replay_guard_recognized_after_stacked_macro_rename() {
1788        let _guard = set_target(Some("renamed_autumn_web"));
1789        // Simulate what `#[authorize]` (or `#[secured]`/`#[step_up]`) leaves
1790        // behind once ITS OWN `finalize` has already run: a block whose
1791        // early-return replay check is rooted at the *renamed* crate, not
1792        // `autumn_web` literally.
1793        let block: syn::Block = syn::parse_quote! {{
1794            const __AUTUMN_IDEMPOTENCY_REPLAY_GUARD: () = ();
1795            if let ::core::option::Option::Some(__autumn_response) =
1796                ::renamed_autumn_web::idempotency::__replay_response(&__autumn_idempotency_replay)
1797            {
1798                return __autumn_response;
1799            }
1800        }};
1801        assert!(
1802            crate::idempotency_guard::block_has_replay_guard(&block),
1803            "recognizer must accept the actively-resolved crate name, not just the literal \
1804             \"autumn_web\""
1805        );
1806    }
1807
1808    /// Regression test for a second Codex round on the same #2552 scenario:
1809    /// once the target is a keyword (`"type"`, from automatic resolution —
1810    /// see `ident_for_target`/`rewrite`), an earlier-expanded macro's own
1811    /// `finalize` pass emits the *raw* identifier `r#type`, not the bare
1812    /// `type`. A recognizer comparing against the bare `current_target()`
1813    /// (rather than `current_target_path_segment()`, which accounts for the
1814    /// raw prefix) misses the match — the same class of bug as
1815    /// `replay_guard_recognized_after_stacked_macro_rename` above, just
1816    /// triggered by a keyword target instead of a plain renamed one.
1817    #[test]
1818    fn replay_guard_recognized_after_stacked_macro_rename_with_keyword_target() {
1819        let _guard = set_target(Some("type"));
1820        let block: syn::Block = syn::parse_quote! {{
1821            const __AUTUMN_IDEMPOTENCY_REPLAY_GUARD: () = ();
1822            if let ::core::option::Option::Some(__autumn_response) =
1823                ::r#type::idempotency::__replay_response(&__autumn_idempotency_replay)
1824            {
1825                return __autumn_response;
1826            }
1827        }};
1828        assert!(
1829            crate::idempotency_guard::block_has_replay_guard(&block),
1830            "recognizer must compare against the raw-escaped target, not the bare keyword"
1831        );
1832    }
1833}