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