Skip to main content

rorpc_parse/codegen/
orpc.rs

1//! Code generation for the `#[rorpc(method, path)]` attribute macro.
2//!
3//! Parses the attribute arguments, analyses the handler signature, and emits:
4//! - The original function unchanged
5//! - An `inventory::submit!` for `HandlerMetadata`
6//! - An `inventory::submit!` for `HandlerRegistration` (Axum router factory)
7//! - `inventory::submit!` blocks for `SchemaRegistration` fallback schemas
8
9use proc_macro2::TokenStream;
10use quote::quote;
11use syn::{
12    Expr, ExprLit, ItemFn, Lit, MetaNameValue, Token,
13    parse::{Parse, ParseStream},
14    punctuated::Punctuated,
15};
16
17use crate::{
18    errors::{Error, Result, type_display},
19    functions::extract_handler_signature,
20    types::{JSON, RESULT, innermost_custom_type, is_primitive, try_extract_wrapper},
21};
22
23// ---------------------------------------------------------------------------
24// OrpcArgs — parsed from #[orpc(method = "...", path = "...", stream_event = "...")]
25// ---------------------------------------------------------------------------
26
27/// Parsed arguments for the `#[orpc(...)]` attribute.
28pub struct OrpcArgs {
29    pub method: String,
30    pub path: String,
31    pub stream_event: Option<String>,
32}
33
34const VALID_KEYS: &[&str] = &["method", "path", "stream_event"];
35
36impl Parse for OrpcArgs {
37    fn parse(input: ParseStream) -> syn::Result<Self> {
38        let pairs = Punctuated::<MetaNameValue, Token![,]>::parse_terminated(input)?;
39
40        let mut method = None;
41        let mut path = None;
42        let mut stream_event = None;
43
44        for pair in &pairs {
45            let key = pair
46                .path
47                .get_ident()
48                .map(|i| i.to_string())
49                .unwrap_or_default();
50
51            let span = pair
52                .path
53                .get_ident()
54                .map(|i| i.span())
55                .unwrap_or_else(proc_macro2::Span::call_site);
56
57            let value = match &pair.value {
58                Expr::Lit(ExprLit {
59                    lit: Lit::Str(s), ..
60                }) => s.value(),
61                _ => {
62                    return Err(syn::Error::new(
63                        span,
64                        Error::invalid_attr_value(
65                            span,
66                            &key,
67                            "a string literal",
68                            "non-string expression",
69                        )
70                        .to_string(),
71                    ));
72                }
73            };
74
75            match key.as_str() {
76                "method" => method = Some(value.to_uppercase()),
77                "path" => path = Some(value),
78                "stream_event" => stream_event = Some(value),
79                _ => {
80                    return Err(syn::Error::new(
81                        span,
82                        Error::unknown_key(span, &key, VALID_KEYS).to_string(),
83                    ));
84                }
85            }
86        }
87
88        let method = method.ok_or_else(|| {
89            syn::Error::new(
90                proc_macro2::Span::call_site(),
91                Error::missing_required_attr(
92                    proc_macro2::Span::call_site(),
93                    "method",
94                    "add `method = \"GET\"` to #[rorpc]",
95                )
96                .to_string(),
97            )
98        })?;
99
100        let path = path.ok_or_else(|| {
101            syn::Error::new(
102                proc_macro2::Span::call_site(),
103                Error::missing_required_attr(
104                    proc_macro2::Span::call_site(),
105                    "path",
106                    "add `path = \"/your/route\"` to #[rorpc]",
107                )
108                .to_string(),
109            )
110        })?;
111
112        Ok(OrpcArgs {
113            method,
114            path,
115            stream_event,
116        })
117    }
118}
119
120// ---------------------------------------------------------------------------
121// expand_orpc
122// ---------------------------------------------------------------------------
123
124/// Generate the full expansion for `#[orpc(method, path)] async fn handler(...)`.
125///
126/// Returns the original function unchanged plus all inventory registrations.
127pub fn expand_orpc(args: OrpcArgs, func: ItemFn) -> TokenStream {
128    match try_expand_orpc(args, func) {
129        Ok(ts) => ts,
130        Err(e) => e.to_compile_error(),
131    }
132}
133
134fn try_expand_orpc(args: OrpcArgs, func: ItemFn) -> Result<TokenStream> {
135    let sig = extract_handler_signature(&func)?;
136
137    let fn_name = &func.sig.ident;
138    let fn_name_str = sig.fn_name.as_str();
139    let method = &args.method;
140    let path = &args.path;
141
142    let output_type_str = type_display(&sig.output_type);
143
144    let error_type_token = match &sig.error_type {
145        Some(ty) => {
146            let s = type_display(ty);
147            quote! { Some(#s) }
148        }
149        None => quote! { None },
150    };
151
152    let stream_event_token = match &args.stream_event {
153        Some(name) => quote! { Some(#name) },
154        None => quote! { None },
155    };
156
157    let input_type_str = match &sig.input_type {
158        Some(ty) => type_display(ty),
159        None => "()".to_string(),
160    };
161
162    let registration = emit_handler_registration(fn_name, method, path, &sig.state_type);
163    let schema_registrations = emit_schema_registrations(&func);
164
165    Ok(quote! {
166        #func
167
168        ::rorpc::inventory::submit! {
169            ::rorpc::HandlerMetadata {
170                name: #fn_name_str,
171                method: #method,
172                path: #path,
173                input_type_name: #input_type_str,
174                output_type_name: #output_type_str,
175                module_path: ::std::module_path!(),
176                error_type_name: #error_type_token,
177                stream_event_type_name: #stream_event_token,
178            }
179        }
180
181        #registration
182        #schema_registrations
183    })
184}
185
186// ---------------------------------------------------------------------------
187// Handler registration factory
188// ---------------------------------------------------------------------------
189
190fn emit_handler_registration(
191    fn_name: &syn::Ident,
192    method: &str,
193    path: &str,
194    state_type: &Option<syn::Type>,
195) -> TokenStream {
196    if let Some(state_ty) = state_type {
197        quote! {
198            ::rorpc::inventory::submit! {
199                ::rorpc::HandlerRegistration {
200                    path: #path,
201                    method: #method,
202                    factory: |state: ::std::sync::Arc<dyn ::std::any::Any + Send + Sync>| {
203                        use ::axum::routing::{delete, get, patch, post, put};
204                        let method_router = match #method {
205                            "GET"    => get(#fn_name),
206                            "POST"   => post(#fn_name),
207                            "PUT"    => put(#fn_name),
208                            "PATCH"  => patch(#fn_name),
209                            "DELETE" => delete(#fn_name),
210                            _        => post(#fn_name),
211                        };
212                        if let Some(typed_state) = state.downcast_ref::<#state_ty>() {
213                            ::axum::Router::new()
214                                .route(#path, method_router)
215                                .with_state(typed_state.clone())
216                        } else {
217                            ::axum::Router::new()
218                        }
219                    },
220                }
221            }
222        }
223    } else {
224        quote! {
225            ::rorpc::inventory::submit! {
226                ::rorpc::HandlerRegistration {
227                    path: #path,
228                    method: #method,
229                    factory: |_state: ::std::sync::Arc<dyn ::std::any::Any + Send + Sync>| {
230                        use ::axum::routing::{delete, get, patch, post, put};
231                        let method_router = match #method {
232                            "GET"    => get(#fn_name),
233                            "POST"   => post(#fn_name),
234                            "PUT"    => put(#fn_name),
235                            "PATCH"  => patch(#fn_name),
236                            "DELETE" => delete(#fn_name),
237                            _        => post(#fn_name),
238                        };
239                        ::axum::Router::new().route(#path, method_router)
240                    },
241                }
242            }
243        }
244    }
245}
246
247// ---------------------------------------------------------------------------
248// Schema registrations — z.unknown() fallback for types without #[derive(ZodTs)]
249// ---------------------------------------------------------------------------
250
251fn emit_schema_registrations(func: &ItemFn) -> TokenStream {
252    let mut seen = std::collections::HashSet::new();
253    let mut registrations = Vec::new();
254
255    // Collect candidate types from Json<T> params and return type
256    let mut candidates: Vec<&syn::Type> = Vec::new();
257
258    for arg in &func.sig.inputs {
259        if let syn::FnArg::Typed(pat_type) = arg
260            && let Some(m) = try_extract_wrapper(&pat_type.ty, JSON)
261            && let Some(inner) = m.first_type()
262        {
263            candidates.push(inner);
264        }
265    }
266
267    if let syn::ReturnType::Type(_, ty) = &func.sig.output {
268        // Handle both Json<T> and Result<Json<T>, E>
269        if let Some(m) = try_extract_wrapper(ty, JSON) {
270            if let Some(inner) = m.first_type() {
271                candidates.push(inner);
272            }
273        } else if let Some(result_m) = try_extract_wrapper(ty, RESULT)
274            && let Some(first) = result_m.first_type()
275            && let Some(json_m) = try_extract_wrapper(first, JSON)
276            && let Some(inner) = json_m.first_type()
277        {
278            candidates.push(inner);
279        }
280    }
281
282    for ty in candidates {
283        if let Some(custom_ty) = innermost_custom_type(ty) {
284            if is_primitive(custom_ty) {
285                continue;
286            }
287            let name = type_display(custom_ty);
288            if !seen.insert(name.clone()) {
289                continue;
290            }
291            let fallback = format!(
292                "z.unknown() /* add #[derive(ZodTs)] to {} for a real schema */",
293                name
294            );
295            registrations.push(quote! {
296                ::rorpc::inventory::submit! {
297                    ::rorpc::SchemaRegistration {
298                        type_name: #name,
299                        zod_ts: || #fallback.to_string(),
300                        dependent_types: || vec![],
301                    }
302                }
303            });
304        }
305    }
306
307    quote! { #(#registrations)* }
308}