1use 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, QUERY, RESULT, innermost_custom_type, is_primitive, try_extract_wrapper},
21};
22
23pub struct OrpcArgs {
29 pub method: String,
30 pub path: String,
31 pub stream_event: Option<syn::Type>,
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 match key.as_str() {
58 "method" => {
59 if let Expr::Lit(ExprLit {
60 lit: Lit::Str(s), ..
61 }) = &pair.value
62 {
63 method = Some(s.value().to_uppercase());
64 } else {
65 return Err(syn::Error::new(
66 span,
67 Error::invalid_attr_value(
68 span,
69 &key,
70 "a string literal",
71 "non-string expression",
72 )
73 .to_string(),
74 ));
75 }
76 }
77 "path" => {
78 if let Expr::Lit(ExprLit {
79 lit: Lit::Str(s), ..
80 }) = &pair.value
81 {
82 path = Some(s.value());
83 } else {
84 return Err(syn::Error::new(
85 span,
86 Error::invalid_attr_value(
87 span,
88 &key,
89 "a string literal",
90 "non-string expression",
91 )
92 .to_string(),
93 ));
94 }
95 }
96 "stream_event" => {
97 if let Expr::Path(expr_path) = &pair.value {
99 let type_path = syn::TypePath {
100 attrs: vec![],
101 qself: expr_path.qself.clone(),
102 path: expr_path.path.clone(),
103 };
104 stream_event = Some(syn::Type::Path(type_path));
105 } else {
106 return Err(syn::Error::new(
107 span,
108 "stream_event must be a type path (e.g., StreamEvent or module::StreamEvent)",
109 ));
110 }
111 }
112 _ => {
113 return Err(syn::Error::new(
114 span,
115 Error::unknown_key(span, &key, VALID_KEYS).to_string(),
116 ));
117 }
118 }
119 }
120
121 let method = method.ok_or_else(|| {
122 syn::Error::new(
123 proc_macro2::Span::call_site(),
124 Error::missing_required_attr(
125 proc_macro2::Span::call_site(),
126 "method",
127 "add `method = \"GET\"` to #[rorpc]",
128 )
129 .to_string(),
130 )
131 })?;
132
133 let path = path.ok_or_else(|| {
134 syn::Error::new(
135 proc_macro2::Span::call_site(),
136 Error::missing_required_attr(
137 proc_macro2::Span::call_site(),
138 "path",
139 "add `path = \"/your/route\"` to #[rorpc]",
140 )
141 .to_string(),
142 )
143 })?;
144
145 Ok(OrpcArgs {
146 method,
147 path,
148 stream_event,
149 })
150 }
151}
152
153pub fn expand_orpc(args: OrpcArgs, func: ItemFn) -> TokenStream {
161 match try_expand_orpc(args, func) {
162 Ok(ts) => ts,
163 Err(e) => e.to_compile_error(),
164 }
165}
166
167fn try_expand_orpc(args: OrpcArgs, func: ItemFn) -> Result<TokenStream> {
168 let sig = extract_handler_signature(&func)?;
169
170 let fn_name = &func.sig.ident;
171 let fn_name_str = sig.fn_name.as_str();
172 let method = &args.method;
173 let path = &args.path;
174
175 let output_type_str = type_display(&sig.output_type);
176
177 let error_type_token = match &sig.error_type {
178 Some(ty) => {
179 let s = type_display(ty);
180 quote! { Some(#s) }
181 }
182 None => quote! { None },
183 };
184
185 let stream_event_token = match &args.stream_event {
186 Some(ty) => {
187 let s = type_display(ty);
188 quote! { Some(#s) }
189 }
190 None => quote! { None },
191 };
192
193 let input_type_str = match &sig.input_type {
194 Some(ty) => type_display(ty),
195 None => "()".to_string(),
196 };
197
198 let query_type_token = match &sig.query_type {
199 Some(ty) => {
200 let s = type_display(ty);
201 quote! { Some(#s) }
202 }
203 None => quote! { None },
204 };
205
206 let registration = emit_handler_registration(fn_name, method, path, &sig.state_type);
207 let schema_registrations = emit_schema_registrations(&func);
208
209 Ok(quote! {
210 #func
211
212 ::rorpc::inventory::submit! {
213 ::rorpc::HandlerMetadata {
214 name: #fn_name_str,
215 method: #method,
216 path: #path,
217 input_type_name: #input_type_str,
218 query_type_name: #query_type_token,
219 output_type_name: #output_type_str,
220 module_path: ::std::module_path!(),
221 error_type_name: #error_type_token,
222 stream_event_type_name: #stream_event_token,
223 }
224 }
225
226 #registration
227 #schema_registrations
228 })
229}
230
231fn emit_handler_registration(
236 fn_name: &syn::Ident,
237 method: &str,
238 path: &str,
239 state_type: &Option<syn::Type>,
240) -> TokenStream {
241 if let Some(state_ty) = state_type {
242 quote! {
243 ::rorpc::inventory::submit! {
244 ::rorpc::HandlerRegistration {
245 path: #path,
246 method: #method,
247 factory: |state: ::std::sync::Arc<dyn ::std::any::Any + Send + Sync>| {
248 use ::axum::routing::{delete, get, patch, post, put};
249 let method_router = match #method {
250 "GET" => get(#fn_name),
251 "POST" => post(#fn_name),
252 "PUT" => put(#fn_name),
253 "PATCH" => patch(#fn_name),
254 "DELETE" => delete(#fn_name),
255 _ => post(#fn_name),
256 };
257 if let Some(typed_state) = state.downcast_ref::<#state_ty>() {
258 ::axum::Router::new()
259 .route(#path, method_router)
260 .with_state(typed_state.clone())
261 } else {
262 ::axum::Router::new()
263 }
264 },
265 }
266 }
267 }
268 } else {
269 quote! {
270 ::rorpc::inventory::submit! {
271 ::rorpc::HandlerRegistration {
272 path: #path,
273 method: #method,
274 factory: |_state: ::std::sync::Arc<dyn ::std::any::Any + Send + Sync>| {
275 use ::axum::routing::{delete, get, patch, post, put};
276 let method_router = match #method {
277 "GET" => get(#fn_name),
278 "POST" => post(#fn_name),
279 "PUT" => put(#fn_name),
280 "PATCH" => patch(#fn_name),
281 "DELETE" => delete(#fn_name),
282 _ => post(#fn_name),
283 };
284 ::axum::Router::new().route(#path, method_router)
285 },
286 }
287 }
288 }
289 }
290}
291
292fn emit_schema_registrations(func: &ItemFn) -> TokenStream {
297 let mut seen = std::collections::HashSet::new();
298 let mut registrations = Vec::new();
299
300 let mut candidates: Vec<&syn::Type> = Vec::new();
302
303 for arg in &func.sig.inputs {
304 if let syn::FnArg::Typed(pat_type) = arg {
305 if let Some(m) = try_extract_wrapper(&pat_type.ty, JSON)
307 && let Some(inner) = m.first_type()
308 {
309 candidates.push(inner);
310 }
311 if let Some(m) = try_extract_wrapper(&pat_type.ty, QUERY)
313 && let Some(inner) = m.first_type()
314 {
315 candidates.push(inner);
316 }
317 }
318 }
319
320 if let syn::ReturnType::Type(_, ty) = &func.sig.output {
321 if let Some(m) = try_extract_wrapper(ty, JSON) {
323 if let Some(inner) = m.first_type() {
324 candidates.push(inner);
325 }
326 } else if let Some(result_m) = try_extract_wrapper(ty, RESULT)
327 && let Some(first) = result_m.first_type()
328 && let Some(json_m) = try_extract_wrapper(first, JSON)
329 && let Some(inner) = json_m.first_type()
330 {
331 candidates.push(inner);
332 }
333 }
334
335 for ty in candidates {
336 if let Some(custom_ty) = innermost_custom_type(ty) {
337 if is_primitive(custom_ty) {
338 continue;
339 }
340 let name = type_display(custom_ty);
341 if !seen.insert(name.clone()) {
342 continue;
343 }
344 let fallback = format!(
345 "z.unknown() /* add #[derive(ZodTs)] to {} for a real schema */",
346 name
347 );
348 registrations.push(quote! {
349 ::rorpc::inventory::submit! {
350 ::rorpc::SchemaRegistration {
351 type_name: #name,
352 zod_ts: || #fallback.to_string(),
353 dependent_types: || vec![],
354 }
355 }
356 });
357 }
358 }
359
360 quote! { #(#registrations)* }
361}
362
363#[cfg(test)]
368mod tests {
369 use super::*;
370 use syn::parse_quote;
371
372 #[test]
373 fn parse_stream_event_type_path() {
374 let args: OrpcArgs = syn::parse_quote! {
376 method = "GET", path = "/stream", stream_event = StreamEvent
377 };
378
379 assert_eq!(args.method, "GET");
380 assert_eq!(args.path, "/stream");
381 assert!(args.stream_event.is_some());
382
383 let ty = args.stream_event.unwrap();
385 let type_str = crate::errors::type_display(&ty);
386 assert_eq!(type_str, "StreamEvent");
387 }
388
389 #[test]
390 fn parse_stream_event_qualified_path() {
391 let args: OrpcArgs = syn::parse_quote! {
393 method = "GET", path = "/stream", stream_event = crate::models::StreamEvent
394 };
395
396 assert!(args.stream_event.is_some());
397 let ty = args.stream_event.unwrap();
398 let type_str = crate::errors::type_display(&ty);
399 assert_eq!(type_str, "crate::models::StreamEvent");
400 }
401
402 #[test]
403 fn parse_without_stream_event() {
404 let args: OrpcArgs = syn::parse_quote! {
406 method = "POST", path = "/create"
407 };
408
409 assert_eq!(args.method, "POST");
410 assert_eq!(args.path, "/create");
411 assert!(args.stream_event.is_none());
412 }
413
414 #[test]
415 fn stream_event_type_converts_to_string_literal() {
416 let args: OrpcArgs = syn::parse_quote! {
419 method = "GET", path = "/stream", stream_event = StreamEvent
420 };
421
422 let func: syn::ItemFn = parse_quote! {
423 async fn stream_test() -> Sse<impl Stream<Item = Event>> {
424 todo!()
425 }
426 };
427
428 let result = try_expand_orpc(args, func);
429 assert!(result.is_ok(), "expand_orpc should succeed");
430
431 let tokens = result.unwrap().to_string();
433
434 assert!(
437 tokens.contains("stream_event_type_name") && tokens.contains(r#""StreamEvent""#),
438 "Generated code should contain stream_event_type_name: Some(\"StreamEvent\"), got: {}",
439 tokens
440 );
441 assert!(
443 !tokens.contains("Some (StreamEvent)") && !tokens.contains("Some(StreamEvent)"),
444 "stream_event_type_name must be a string literal, not a bare identifier"
445 );
446 }
447}