Skip to main content

rorpc_macros/
lib.rs

1//! Thin proc-macro bridge for [`rorpc_parse`].
2//!
3//! This crate contains only proc-macro entry points. All parsing, validation,
4//! and code generation logic lives in `rorpc-parse` where it can be tested
5//! with normal `#[test]` functions.
6
7use proc_macro::TokenStream;
8use syn::parse_macro_input;
9
10/// Annotate an Axum handler with explicit HTTP method and path.
11///
12/// This is the canonical syntax for specifying both method and path explicitly.
13/// The function remains a valid Axum handler with metadata registered for contract
14/// generation and router discovery.
15///
16/// # Syntax
17///
18/// ```rust,ignore
19/// #[rorpc::route(method = "POST", path = "/planet/list")]
20/// async fn list_planets(State(db): State<Db>) -> Json<Vec<Planet>> {
21///     Json(db.list().await)
22/// }
23/// ```
24///
25/// # Arguments
26///
27/// - `method` — HTTP method string (`"GET"`, `"POST"`, etc.), normalized to uppercase. Required.
28/// - `path` — Route path string (e.g. `"/planet/list"`). Required.
29/// - `data` — String literal naming the SSE data payload type for streaming handlers (e.g. `"StreamEvent"`). Optional.
30///
31/// # For Shorthand Syntax
32///
33/// Consider using method-specific macros for brevity:
34/// - `#[rorpc::get("/path")]`
35/// - `#[rorpc::post("/path")]`
36/// - `#[rorpc::put("/path")]`
37/// - `#[rorpc::patch("/path")]`
38/// - `#[rorpc::delete("/path")]`
39#[proc_macro_attribute]
40pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
41    let args = parse_macro_input!(attr as rorpc_parse::codegen::OrpcArgs);
42    let func = parse_macro_input!(item as syn::ItemFn);
43    rorpc_parse::codegen::expand_orpc(args, func).into()
44}
45
46/// Shorthand for `#[orpc::route(method = "GET", path = "...")]`.
47///
48/// # Syntax
49///
50/// ```rust,ignore
51/// #[orpc::get("/planet/list")]
52/// async fn list_planets(State(db): State<Db>) -> Json<Vec<Planet>> {
53///     Json(db.list().await)
54/// }
55/// ```
56///
57/// # With streaming data type
58///
59/// ```rust,ignore
60/// #[orpc::get("/stream", data = "StreamEvent")]
61/// async fn stream_events() -> Sse<impl Stream<Item = Event>> {
62///     // data takes a string literal for IDE support
63/// }
64/// ```
65#[proc_macro_attribute]
66pub fn get(attr: TokenStream, item: TokenStream) -> TokenStream {
67    let shorthand_args = parse_macro_input!(attr as rorpc_parse::codegen::MethodShorthandArgs);
68    let func = parse_macro_input!(item as syn::ItemFn);
69    let args = shorthand_args.into_orpc_args("GET");
70    rorpc_parse::codegen::expand_orpc(args, func).into()
71}
72
73/// Shorthand for `#[orpc::route(method = "POST", path = "...")]`.
74///
75/// # Syntax
76///
77/// ```rust,ignore
78/// #[orpc::post("/planet/create")]
79/// async fn create_planet(
80///     State(db): State<Db>,
81///     Json(input): Json<CreateInput>,
82/// ) -> Result<Json<Planet>, AppError> {
83///     // ...
84/// }
85/// ```
86#[proc_macro_attribute]
87pub fn post(attr: TokenStream, item: TokenStream) -> TokenStream {
88    let shorthand_args = parse_macro_input!(attr as rorpc_parse::codegen::MethodShorthandArgs);
89    let func = parse_macro_input!(item as syn::ItemFn);
90    let args = shorthand_args.into_orpc_args("POST");
91    rorpc_parse::codegen::expand_orpc(args, func).into()
92}
93
94/// Shorthand for `#[orpc::route(method = "PUT", path = "...")]`.
95///
96/// # Syntax
97///
98/// ```rust,ignore
99/// #[orpc::put("/planet/{id}")]
100/// async fn update_planet(
101///     State(db): State<Db>,
102///     Json(input): Json<UpdateInput>,
103/// ) -> Result<Json<Planet>, AppError> {
104///     // ...
105/// }
106/// ```
107#[proc_macro_attribute]
108pub fn put(attr: TokenStream, item: TokenStream) -> TokenStream {
109    let shorthand_args = parse_macro_input!(attr as rorpc_parse::codegen::MethodShorthandArgs);
110    let func = parse_macro_input!(item as syn::ItemFn);
111    let args = shorthand_args.into_orpc_args("PUT");
112    rorpc_parse::codegen::expand_orpc(args, func).into()
113}
114
115/// Shorthand for `#[orpc::route(method = "PATCH", path = "...")]`.
116///
117/// # Syntax
118///
119/// ```rust,ignore
120/// #[orpc::patch("/planet/{id}")]
121/// async fn patch_planet(
122///     State(db): State<Db>,
123///     Json(input): Json<PatchInput>,
124/// ) -> Result<Json<Planet>, AppError> {
125///     // ...
126/// }
127/// ```
128#[proc_macro_attribute]
129pub fn patch(attr: TokenStream, item: TokenStream) -> TokenStream {
130    let shorthand_args = parse_macro_input!(attr as rorpc_parse::codegen::MethodShorthandArgs);
131    let func = parse_macro_input!(item as syn::ItemFn);
132    let args = shorthand_args.into_orpc_args("PATCH");
133    rorpc_parse::codegen::expand_orpc(args, func).into()
134}
135
136/// Shorthand for `#[orpc::route(method = "DELETE", path = "...")]`.
137///
138/// # Syntax
139///
140/// ```rust,ignore
141/// #[orpc::delete("/planet/{id}")]
142/// async fn delete_planet(
143///     State(db): State<Db>,
144///     Json(input): Json<DeleteInput>,
145/// ) -> Result<Json<()>, AppError> {
146///     // ...
147/// }
148/// ```
149#[proc_macro_attribute]
150pub fn delete(attr: TokenStream, item: TokenStream) -> TokenStream {
151    let shorthand_args = parse_macro_input!(attr as rorpc_parse::codegen::MethodShorthandArgs);
152    let func = parse_macro_input!(item as syn::ItemFn);
153    let args = shorthand_args.into_orpc_args("DELETE");
154    rorpc_parse::codegen::expand_orpc(args, func).into()
155}
156
157/// Auto-discovery router macro with optional module path filtering.
158///
159/// Discovers all `#[rorpc]`-annotated handlers via the `inventory` crate and
160/// builds an Axum `Router`. Accepts an optional state expression and/or a
161/// module path pattern to restrict which handlers are included.
162///
163/// # Syntax
164///
165/// ```text
166/// router!()                            // all handlers, no state
167/// router!(state)                       // all handlers, with state
168/// router!("pattern")                   // filtered, no state
169/// router!("pattern", state)            // filtered + state (any order)
170/// router!(state, "pattern")            // filtered + state (any order)
171/// router!(["pat1", "pat2"])            // multiple patterns
172/// router!("prefix::{a,b}")             // brace expansion
173/// router!("prefix::*")                 // wildcard
174/// ```
175///
176/// # Pattern matching
177///
178/// Patterns match against the handler's `module_path!()` value:
179/// - `"handlers::planet"` — exact module or any child
180/// - `"handlers::*"` — all direct and nested children of `handlers::`
181/// - `"handlers::{planet,user}"` — brace expansion
182/// - `["handlers::planet", "api::v1"]` — explicit list
183#[proc_macro]
184pub fn router(input: TokenStream) -> TokenStream {
185    let args = parse_macro_input!(input as rorpc_parse::codegen::RouterArgs);
186    rorpc_parse::codegen::expand_router(args).into()
187}
188
189/// Derive macro that generates a `fn zod_ts() -> String` method on structs and enums.
190///
191/// The generated method returns a complete TypeScript block with a Zod schema
192/// and a `z.infer` type alias. An `inventory::submit!` call registers the real
193/// schema so contract generation prefers it over the `z.unknown()` fallback
194/// emitted by `#[rorpc]`.
195///
196/// # Example
197///
198/// ```rust,ignore
199/// #[derive(Serialize, Deserialize, ZodTs)]
200/// pub struct Planet {
201///     pub id: i32,
202///     #[zod(min_length(1), max_length(100))]
203///     pub name: String,
204///     pub description: Option<String>,
205/// }
206/// ```
207///
208/// # Supported `#[zod(...)]` field attributes
209///
210/// **Strings:** `min_length(n)`, `max_length(n)`, `length(n)`, `email`, `url`,
211/// `regex("pattern")`, `starts_with("s")`, `ends_with("s")`, `includes("s")`
212///
213/// **Numbers:** `min(n)`, `max(n)`, `int`, `positive`, `negative`,
214/// `nonnegative`, `nonpositive`, `finite`
215///
216/// **Arrays (`Vec<T>`):** `min_length(n)`, `max_length(n)`, `length(n)`
217#[proc_macro_derive(ZodTs, attributes(zod))]
218pub fn derive_zod_ts(input: TokenStream) -> TokenStream {
219    let input = parse_macro_input!(input as syn::DeriveInput);
220    match rorpc_parse::codegen::derive_zod_ts(input) {
221        Ok(tokens) => tokens.into(),
222        Err(err) => err.to_compile_error().into(),
223    }
224}
225
226/// Derive macro for registering error enum variants with rorpc.
227///
228/// Annotate an error enum so `generate_contract()` can emit TypeScript
229/// `.errors({...})` entries. Variant names are converted to `SCREAMING_SNAKE_CASE`.
230///
231/// # Example
232///
233/// ```rust,ignore
234/// #[derive(OrpcError)]
235/// pub enum AppError {
236///     NotFound,
237///     Conflict { reason: String },
238///     DatabaseError(String),
239/// }
240/// ```
241///
242/// # Variant mapping
243///
244/// - Unit variants: `NotFound` → `NOT_FOUND: {}`
245/// - Struct variants: `Conflict { reason: String }` → `CONFLICT: { data: z.object({...}) }`
246/// - Tuple variants: `DatabaseError(String)` → `DATABASE_ERROR: { data: z.string() }`
247#[proc_macro_derive(OrpcError)]
248pub fn derive_orpc_errors(input: TokenStream) -> TokenStream {
249    let input = parse_macro_input!(input as syn::DeriveInput);
250    match rorpc_parse::codegen::expand_orpc_errors(input) {
251        Ok(tokens) => tokens.into(),
252        Err(err) => err.to_compile_error().into(),
253    }
254}
255
256/// Automatically generate TypeScript contract before `fn main()` runs (debug builds only).
257///
258/// This attribute wraps the main function to call `rorpc::generate_contract().output(path)`
259/// before executing the original body. Only active in debug builds (`#[cfg(debug_assertions)]`).
260///
261/// # Syntax
262///
263/// ```rust,ignore
264/// // Default: env!("RORPC_CLIENT_PATH")
265/// #[rorpc::contract]
266/// fn main() {
267///     // Your main logic
268/// }
269///
270/// // String literal
271/// #[rorpc::contract("../client/src/rpc/bindings.ts")]
272/// fn main() { }
273///
274/// // Environment variable
275/// #[rorpc::contract(env!("RORPC_CLIENT_PATH"))]
276/// fn main() { }
277///
278/// // concat! expression
279/// #[rorpc::contract(concat!(env!("CARGO_MANIFEST_DIR"), "/../client/bindings.ts"))]
280/// fn main() { }
281///
282/// // Constant
283/// const CLIENT_PATH: &str = "../client/bindings.ts";
284/// #[rorpc::contract(CLIENT_PATH)]
285/// fn main() { }
286/// ```
287///
288/// # Setting the output path
289///
290/// ## Recommended: `[package.metadata.rorpc]` in `Cargo.toml`
291///
292/// ```toml
293/// [package.metadata.rorpc]
294/// client_path = "../client/src/rpc/bindings.ts"
295/// ```
296///
297/// Then just use `#[rorpc::contract]` with no arguments. The macro reads
298/// `Cargo.toml` at compile time and bakes in the resolved absolute path.
299///
300/// ## Alternative: explicit argument
301///
302/// String literal:
303/// ```rust,ignore
304/// #[rorpc::contract("../client/src/rpc/bindings.ts")]
305/// fn main() { }
306/// ```
307///
308/// `concat!` expression (absolute path):
309/// ```rust,ignore
310/// #[rorpc::contract(concat!(env!("CARGO_MANIFEST_DIR"), "/../client/bindings.ts"))]
311/// fn main() { }
312/// ```
313///
314/// Constant:
315/// ```rust,ignore
316/// const CLIENT_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../client/bindings.ts");
317///
318/// #[rorpc::contract(CLIENT_PATH)]
319/// fn main() { }
320/// ```
321///
322/// ## Fallback: `env!("RORPC_CLIENT_PATH")`
323///
324/// If no argument is given and `[package.metadata.rorpc] client_path` is absent,
325/// the macro falls back to `env!("RORPC_CLIENT_PATH")`. Set it via `build.rs`:
326/// ```rust,ignore
327/// fn main() {
328///     println!("cargo:rustc-env=RORPC_CLIENT_PATH=../client/src/rpc/bindings.ts");
329/// }
330/// ```
331///
332/// In `Cargo.toml`:
333/// ```toml
334/// [package.metadata.rorpc]
335/// client_path = "../client/src/rpc/bindings.ts"
336/// ```
337///
338/// In `build.rs`:
339/// ```rust,ignore
340/// fn main() {
341///     let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
342///     let manifest_path = std::path::Path::new(&manifest_dir).join("Cargo.toml");
343///     let manifest = std::fs::read_to_string(manifest_path).unwrap();
344///     
345///     let toml: toml::Value = toml::from_str(&manifest).unwrap();
346///     if let Some(client_path) = toml.get("package")
347///         .and_then(|p| p.get("metadata"))
348///         .and_then(|m| m.get("rorpc"))
349///         .and_then(|r| r.get("client_path"))
350///         .and_then(|c| c.as_str())
351///     {
352///         println!("cargo:rustc-env=RORPC_CLIENT_PATH={}", client_path);
353///     }
354/// }
355/// ```
356///
357/// Add to `Cargo.toml` dependencies:
358/// ```toml
359/// [build-dependencies]
360/// toml = "0.8"
361/// ```
362///
363/// ## Other options
364///
365/// ```toml
366/// [env]
367/// RORPC_CLIENT_PATH = "../client/src/rpc/bindings.ts"
368/// ```
369///
370/// Shell environment variable:
371/// ```bash
372/// export RORPC_CLIENT_PATH="../client/src/rpc/bindings.ts"
373/// cargo run
374/// ```
375///
376///
377/// # Compatibility
378///
379/// This attribute preserves the function signature and can be combined with other
380/// attributes like `#[tokio::main]`, `#[actix_web::main]`, etc.:
381///
382/// ```rust,ignore
383/// #[rorpc::contract]
384/// #[tokio::main]
385/// async fn main() {
386///     // Contract generated before async runtime starts
387/// }
388/// ```
389#[proc_macro_attribute]
390pub fn contract(attr: TokenStream, item: TokenStream) -> TokenStream {
391    let args = parse_macro_input!(attr as rorpc_parse::codegen::ContractArgs);
392    let func = parse_macro_input!(item as syn::ItemFn);
393    rorpc_parse::codegen::expand_contract(args, func).into()
394}
395
396/// Apply a namespace prefix to all handlers within a module.
397///
398/// This attribute registers a namespace that will be prepended to all handler
399/// routes within the annotated module at runtime (during router construction
400/// and contract generation).
401///
402/// # Syntax
403///
404/// ```rust,ignore
405/// #[rorpc::namespace("/planet")]
406/// mod planet_handlers {
407///     #[rorpc::get("/list")]           // Becomes /planet/list
408///     async fn list() { ... }
409///
410///     #[rorpc::get("/{id}")]           // Becomes /planet/{id}
411///     async fn find() { ... }
412///
413///     #[rorpc::post("/create")]        // Becomes /planet/create
414///     async fn create() { ... }
415/// }
416/// ```
417///
418/// # Rules
419///
420/// - Prefix must start with `/`
421/// - Prefix cannot contain `..` path traversal
422/// - Prefix should not end with `/` (except for root `/`)
423/// - Namespace always concatenates with handler path (e.g., `/api` + `/planet/list` = `/api/planet/list`)
424///
425/// # Nested Namespaces
426///
427/// You can nest namespaced modules for deeper path structures:
428///
429/// ```rust,ignore
430/// #[rorpc::namespace("/api")]
431/// mod api {
432///     #[rorpc::namespace("/v1")]
433///     mod v1 {
434///         #[rorpc::get("/status")]  // Becomes /api/v1/status
435///         async fn status() { ... }
436///     }
437/// }
438/// ```
439///
440/// # Composition
441///
442/// The namespace prefix is applied to the handler's path at runtime when:
443/// - Building the router with `router!()`
444/// - Generating the TypeScript contract with `generate_contract()`
445///
446/// Handlers without a namespace behave exactly as before (no prefix applied).
447#[proc_macro_attribute]
448pub fn namespace(attr: TokenStream, item: TokenStream) -> TokenStream {
449    let args = parse_macro_input!(attr as rorpc_parse::codegen::NamespaceArgs);
450    let module_item = parse_macro_input!(item as syn::Item);
451    match rorpc_parse::codegen::expand_namespace(args, module_item) {
452        Ok(tokens) => tokens.into(),
453        Err(err) => err.to_compile_error().into(),
454    }
455}