Skip to main content

tork_macros/
lib.rs

1//! Procedural macros for the Tork web framework.
2//!
3//! Every macro here emits code that refers to the public API through the `tork`
4//! facade crate (for example `::tork::Router`), never through `tork-core`
5//! directly. This lets generated code compile inside user crates that depend
6//! only on `tork`.
7
8use proc_macro::TokenStream;
9
10mod api_model;
11mod api_router;
12mod app_error;
13mod common;
14mod dependency;
15mod form_model;
16mod inject;
17mod lifespan;
18mod main_macro;
19mod middleware;
20mod resources;
21mod route;
22mod settings;
23mod sse;
24mod websocket;
25
26/// Marks the asynchronous entrypoint of a Tork application.
27///
28/// Rewrites an `async fn main` into a synchronous `main` that builds a
29/// multi-threaded Tokio runtime and blocks on the original body. The function's
30/// return type is preserved, so returning `tork::Result<()>` works as written.
31///
32/// # Example
33///
34/// ```ignore
35/// #[tork::main]
36/// async fn main() -> tork::Result<()> {
37///     App::new().serve("0.0.0.0:8000").await
38/// }
39/// ```
40#[proc_macro_attribute]
41pub fn main(attr: TokenStream, item: TokenStream) -> TokenStream {
42    main_macro::expand(attr, item)
43}
44
45/// Turns an inline module into a router.
46///
47/// Discovers the functions annotated with a route macro and generates a
48/// `router()` function that mounts them under the given `prefix` and `tags`.
49///
50/// # Example
51///
52/// ```ignore
53/// #[api_router(prefix = "/users", tags = ["users"])]
54/// pub mod users_router {
55///     use super::*;
56///
57///     #[get("/{user_id}", response_model = UserOut)]
58///     pub async fn get_user(user_id: i64, service: UserService) -> tork::Result<UserOut> {
59///         service.get_user(user_id).await
60///     }
61/// }
62/// // `users_router::router()` is now available.
63/// ```
64#[proc_macro_attribute]
65pub fn api_router(attr: TokenStream, item: TokenStream) -> TokenStream {
66    api_router::expand(attr, item)
67}
68
69/// Turns a type into a request dependency.
70///
71/// Applied to an `impl` block containing an async `resolve` function, it
72/// generates a `FromRequest` implementation. Each parameter of `resolve` is
73/// itself resolved through `FromRequest` (recursively), then `resolve` is called.
74///
75/// # Example
76///
77/// ```ignore
78/// #[tork::dependency]
79/// impl CurrentUser {
80///     pub async fn resolve(token: BearerToken, users: UserRepository) -> tork::Result<Self> {
81///         // ...resolve the current user from the token...
82///     }
83/// }
84/// ```
85#[proc_macro_attribute]
86pub fn dependency(attr: TokenStream, item: TokenStream) -> TokenStream {
87    dependency::expand(attr, item)
88}
89
90/// Turns a struct into a validated, documented API model.
91///
92/// Derives serde (de)serialization, `garde` validation, and `schemars` JSON
93/// Schema, and translates `#[field(...)]` constraints into the matching `garde`
94/// and `schemars` attributes.
95///
96/// # Supported `#[field(...)]` constraints
97///
98/// - `min_length` / `max_length` — string length bounds
99/// - `ge` / `le` — inclusive numeric bounds
100/// - `gt` / `lt` — exclusive numeric bounds
101/// - `title` / `description` — documentation metadata
102/// - `custom` — a custom validator function (may be repeated); the function
103///   performs the check and returns its own message, e.g.
104///   `#[field(custom = validate_password)]`
105///
106/// # Example
107///
108/// ```ignore
109/// #[api_model(rename_all = "camelCase")]
110/// pub struct CreateOrderInput {
111///     #[field(min_length = 1, max_length = 120)] pub name: String,
112///     #[field(gt = 0, description = "The price must be greater than zero")] pub price: f64,
113/// }
114/// ```
115#[proc_macro_attribute]
116pub fn api_model(attr: TokenStream, item: TokenStream) -> TokenStream {
117    api_model::expand(attr, item)
118}
119
120/// Declares a typed application configuration.
121///
122/// Derives `serde` deserialization and `garde` validation, maps `#[setting(...)]`
123/// constraints to validation rules, turns `#[setting(default = ...)]` into a serde
124/// default, and generates a `load()` method that merges the configured sources
125/// (`.env`, environment variables, config files, a secrets directory, overrides)
126/// into a validated value. Loading is meant to run once at startup.
127///
128/// # Example
129///
130/// ```ignore
131/// #[tork::settings(prefix = "APP", env_file = ".env")]
132/// pub struct Config {
133///     #[setting(default = "Awesome API")] pub app_name: String,
134///     #[setting(email)] pub admin_email: String,
135///     #[setting(default = 50, ge = 1, le = 500)] pub items_per_user: u32,
136///     #[setting(secret)] pub jwt_secret: tork::SecretString,
137/// }
138/// // let config = Config::load()?;
139/// ```
140#[proc_macro_attribute]
141pub fn settings(attr: TokenStream, item: TokenStream) -> TokenStream {
142    settings::expand(attr, item)
143}
144
145/// Declares an application lifespan on a resource container.
146///
147/// Applied to `impl T { async fn startup(ctx) -> tork::Result<Self>; async fn
148/// shutdown(self) -> tork::Result<()> }`, it makes `T` a `Lifespan`. `shutdown`
149/// is optional. `T` must also derive `Resources`.
150///
151/// # Example
152///
153/// ```ignore
154/// #[tork::lifespan]
155/// impl AppState {
156///     async fn startup(ctx: tork::LifespanContext) -> tork::Result<Self> { /* ... */ }
157///     async fn shutdown(self) -> tork::Result<()> { /* ... */ }
158/// }
159/// // App::new().lifespan::<AppState>()
160/// ```
161#[proc_macro_attribute]
162pub fn lifespan(attr: TokenStream, item: TokenStream) -> TokenStream {
163    lifespan::expand(attr, item)
164}
165
166/// Declares a resource container.
167///
168/// Generates a `Resources` implementation that registers each `#[resource]`
169/// field by type, and a `FromRequest` implementation for each resource type so
170/// it can be injected directly.
171#[proc_macro_derive(Resources, attributes(resource))]
172pub fn derive_resources(item: TokenStream) -> TokenStream {
173    resources::expand(item)
174}
175
176/// Derives request-time construction by injecting each field.
177///
178/// Generates a `FromRequest` implementation that resolves every field through
179/// `FromRequest` (a resource, another `Inject` service, or a built-in extractor).
180#[proc_macro_derive(Inject, attributes(inject, logger))]
181pub fn derive_inject(item: TokenStream) -> TokenStream {
182    inject::expand(item)
183}
184
185/// Derives `FromMultipart` for a `multipart/form-data` model.
186///
187/// Each field binds from the parsed form: file fields (`FileBytes` / `UploadFile`,
188/// optionally `Option` or `Vec`) are recognized by a `#[file]` attribute or their
189/// type; every other field is a text field parsed from its string value.
190/// `#[field(...)]` constraints validate text fields.
191///
192/// # Example
193///
194/// ```ignore
195/// #[derive(FormModel)]
196/// pub struct CreateFileForm {
197///     #[file] pub file: FileBytes,
198///     #[field(min_length = 16)] pub token: String,
199/// }
200/// ```
201#[proc_macro_derive(FormModel, attributes(file, form, field))]
202pub fn derive_form_model(item: TokenStream) -> TokenStream {
203    form_model::expand(item)
204}
205
206/// Derives `From<Self> for tork::Error`, storing the value as a typed source.
207///
208/// This lets a user error convert into a framework error through `?` while
209/// preserving the original value, so a registered
210/// `exception_handler::<Self>()` can recover and map it. A type-level
211/// `#[status(...)]` attribute sets the default HTTP status (a status code such as
212/// `503`, or an `ErrorKind` variant name); it defaults to `500`. The type must
213/// implement `Display` and `std::error::Error`.
214///
215/// # Example
216///
217/// ```ignore
218/// #[derive(Debug, tork::AppError)]
219/// #[status(503)]
220/// pub enum DbError { Timeout }
221/// // impl Display + Error for DbError ...
222/// // `repo.query()?` now converts DbError into tork::Error.
223/// ```
224#[proc_macro_derive(AppError, attributes(status))]
225pub fn derive_app_error(item: TokenStream) -> TokenStream {
226    app_error::expand(item)
227}
228
229/// Turns an async function into a middleware layer.
230///
231/// The function must be `async fn(request, next) -> tork::Result<tork::Response>`.
232/// It is rewritten into a unit struct of the same name implementing the
233/// `Middleware` trait, so it can be passed to `App::middleware`.
234///
235/// # Example
236///
237/// ```ignore
238/// #[middleware]
239/// pub async fn add_process_time_header(req: Request, next: Next) -> Result<Response> {
240///     let mut res = next.run(req).await?;
241///     // ...set a header on res...
242///     Ok(res)
243/// }
244/// ```
245#[proc_macro_attribute]
246pub fn middleware(attr: TokenStream, item: TokenStream) -> TokenStream {
247    middleware::expand(attr, item)
248}
249
250/// Declares a `GET` route. See [`macro@get`] and the other method macros for the
251/// supported attributes (`response_model`, `summary`, `description`,
252/// `status_code`, `tags`).
253#[proc_macro_attribute]
254pub fn get(attr: TokenStream, item: TokenStream) -> TokenStream {
255    route::route_impl("GET", attr, item)
256}
257
258/// Declares a `POST` route.
259#[proc_macro_attribute]
260pub fn post(attr: TokenStream, item: TokenStream) -> TokenStream {
261    route::route_impl("POST", attr, item)
262}
263
264/// Declares a `PUT` route.
265#[proc_macro_attribute]
266pub fn put(attr: TokenStream, item: TokenStream) -> TokenStream {
267    route::route_impl("PUT", attr, item)
268}
269
270/// Declares a `PATCH` route.
271#[proc_macro_attribute]
272pub fn patch(attr: TokenStream, item: TokenStream) -> TokenStream {
273    route::route_impl("PATCH", attr, item)
274}
275
276/// Declares a `DELETE` route.
277#[proc_macro_attribute]
278pub fn delete(attr: TokenStream, item: TokenStream) -> TokenStream {
279    route::route_impl("DELETE", attr, item)
280}
281
282/// Declares a Server-Sent Events endpoint (default `GET`).
283///
284/// The handler returns `tork::Result<Sse<T>>`. Attributes: path (first), `method`,
285/// `event` (default event name), `summary`, `description`, `tags`.
286///
287/// # Example
288///
289/// ```ignore
290/// #[sse("/items/stream", event = "item_update")]
291/// pub async fn stream_items(service: ItemService) -> tork::Result<Sse<ItemOut>> {
292///     Ok(Sse::new(service.item_stream()))
293/// }
294/// ```
295#[proc_macro_attribute]
296pub fn sse(attr: TokenStream, item: TokenStream) -> TokenStream {
297    sse::expand("GET", attr, item)
298}
299
300/// Declares a Server-Sent Events endpoint served over `POST`.
301///
302/// Shorthand for [`macro@sse`] with `method = POST`; useful for streaming a
303/// response to a request that carries a body.
304#[proc_macro_attribute]
305pub fn post_sse(attr: TokenStream, item: TokenStream) -> TokenStream {
306    sse::expand("POST", attr, item)
307}
308
309/// Declares a WebSocket endpoint.
310///
311/// The handler takes a `WebSocket` parameter (the upgrade handle) and returns
312/// `tork::Result<()>`. Other parameters are path parameters or dependencies,
313/// resolved before the upgrade. Attributes: path (first), `summary`,
314/// `description`, `tags`.
315///
316/// # Example
317///
318/// ```ignore
319/// #[websocket("/ws")]
320/// pub async fn echo(socket: WebSocket) -> tork::Result<()> {
321///     let mut socket = socket.accept().await?;
322///     while let Some(message) = socket.recv().await? { /* ... */ }
323///     Ok(())
324/// }
325/// ```
326#[proc_macro_attribute]
327pub fn websocket(attr: TokenStream, item: TokenStream) -> TokenStream {
328    websocket::expand(attr, item)
329}