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 a plain Axum handler to register its metadata with rorpc.
11///
12/// The function is left completely unchanged — it remains a valid Axum handler.
13/// Two `inventory::submit!` calls are added alongside it:
14/// one for [`rorpc::HandlerMetadata`] (used by contract generation) and one for
15/// [`rorpc::HandlerRegistration`] (used by [`router!`]).
16///
17/// # Syntax
18///
19/// ```rust,ignore
20/// #[orpc(method = "POST", path = "/planet/list")]
21/// async fn list_planets(State(db): State<Db>) -> Json<Vec<Planet>> {
22/// Json(db.list().await)
23/// }
24/// ```
25///
26/// # Arguments
27///
28/// - `method` — HTTP method string (`"GET"`, `"post"`, etc.), normalised to uppercase. Required.
29/// - `path` — Route path string (e.g. `"/planet/list"`). Required.
30/// - `stream_event` — Type path for the SSE event type for streaming handlers (e.g. `StreamEvent`). Optional.
31#[proc_macro_attribute]
32pub fn orpc(attr: TokenStream, item: TokenStream) -> TokenStream {
33 let args = parse_macro_input!(attr as rorpc_parse::codegen::OrpcArgs);
34 let func = parse_macro_input!(item as syn::ItemFn);
35 rorpc_parse::codegen::expand_orpc(args, func).into()
36}
37
38/// Auto-discovery router macro with optional module path filtering.
39///
40/// Discovers all `#[rorpc]`-annotated handlers via the `inventory` crate and
41/// builds an Axum `Router`. Accepts an optional state expression and/or a
42/// module path pattern to restrict which handlers are included.
43///
44/// # Syntax
45///
46/// ```text
47/// router!() // all handlers, no state
48/// router!(state) // all handlers, with state
49/// router!("pattern") // filtered, no state
50/// router!("pattern", state) // filtered + state (any order)
51/// router!(state, "pattern") // filtered + state (any order)
52/// router!(["pat1", "pat2"]) // multiple patterns
53/// router!("prefix::{a,b}") // brace expansion
54/// router!("prefix::*") // wildcard
55/// ```
56///
57/// # Pattern matching
58///
59/// Patterns match against the handler's `module_path!()` value:
60/// - `"handlers::planet"` — exact module or any child
61/// - `"handlers::*"` — all direct and nested children of `handlers::`
62/// - `"handlers::{planet,user}"` — brace expansion
63/// - `["handlers::planet", "api::v1"]` — explicit list
64#[proc_macro]
65pub fn router(input: TokenStream) -> TokenStream {
66 let args = parse_macro_input!(input as rorpc_parse::codegen::RouterArgs);
67 rorpc_parse::codegen::expand_router(args).into()
68}
69
70/// Derive macro that generates a `fn zod_ts() -> String` method on structs and enums.
71///
72/// The generated method returns a complete TypeScript block with a Zod schema
73/// and a `z.infer` type alias. An `inventory::submit!` call registers the real
74/// schema so contract generation prefers it over the `z.unknown()` fallback
75/// emitted by `#[rorpc]`.
76///
77/// # Example
78///
79/// ```rust,ignore
80/// #[derive(Serialize, Deserialize, ZodTs)]
81/// pub struct Planet {
82/// pub id: i32,
83/// #[zod(min_length(1), max_length(100))]
84/// pub name: String,
85/// pub description: Option<String>,
86/// }
87/// ```
88///
89/// # Supported `#[zod(...)]` field attributes
90///
91/// **Strings:** `min_length(n)`, `max_length(n)`, `length(n)`, `email`, `url`,
92/// `regex("pattern")`, `starts_with("s")`, `ends_with("s")`, `includes("s")`
93///
94/// **Numbers:** `min(n)`, `max(n)`, `int`, `positive`, `negative`,
95/// `nonnegative`, `nonpositive`, `finite`
96///
97/// **Arrays (`Vec<T>`):** `min_length(n)`, `max_length(n)`, `length(n)`
98#[proc_macro_derive(ZodTs, attributes(zod))]
99pub fn derive_zod_ts(input: TokenStream) -> TokenStream {
100 let input = parse_macro_input!(input as syn::DeriveInput);
101 match rorpc_parse::codegen::derive_zod_ts(input) {
102 Ok(tokens) => tokens.into(),
103 Err(err) => err.to_compile_error().into(),
104 }
105}
106
107/// Derive macro for registering error enum variants with rorpc.
108///
109/// Annotate an error enum so `generate_contract()` can emit TypeScript
110/// `.errors({...})` entries. Variant names are converted to `SCREAMING_SNAKE_CASE`.
111///
112/// # Example
113///
114/// ```rust,ignore
115/// #[derive(OrpcError)]
116/// pub enum AppError {
117/// NotFound,
118/// Conflict { reason: String },
119/// DatabaseError(String),
120/// }
121/// ```
122///
123/// # Variant mapping
124///
125/// - Unit variants: `NotFound` → `NOT_FOUND: {}`
126/// - Struct variants: `Conflict { reason: String }` → `CONFLICT: { data: z.object({...}) }`
127/// - Tuple variants: `DatabaseError(String)` → `DATABASE_ERROR: { data: z.string() }`
128#[proc_macro_derive(OrpcError)]
129pub fn derive_orpc_errors(input: TokenStream) -> TokenStream {
130 let input = parse_macro_input!(input as syn::DeriveInput);
131 match rorpc_parse::codegen::expand_orpc_errors(input) {
132 Ok(tokens) => tokens.into(),
133 Err(err) => err.to_compile_error().into(),
134 }
135}