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
23const ATTR_METHOD: &str = "method";
28const ATTR_PATH: &str = "path";
29const ATTR_DATA: &str = "data";
30
31pub struct OrpcArgs {
37 pub method: String,
38 pub path: String,
39 pub stream_event: Option<String>,
40}
41
42pub struct MethodShorthandArgs {
50 pub path: String,
51 pub data: Option<String>,
52}
53
54impl Parse for MethodShorthandArgs {
55 fn parse(input: ParseStream) -> syn::Result<Self> {
56 let path_lit: syn::LitStr = input.parse()?;
58 let path = path_lit.value();
59
60 let mut data = None;
62
63 if input.peek(Token![,]) {
64 input.parse::<Token![,]>()?;
65
66 let pairs = Punctuated::<MetaNameValue, Token![,]>::parse_terminated(input)?;
67
68 for pair in &pairs {
69 let key = pair
70 .path
71 .get_ident()
72 .map(|i| i.to_string())
73 .unwrap_or_default();
74
75 let span = pair
76 .path
77 .get_ident()
78 .map(|i| i.span())
79 .unwrap_or_else(proc_macro2::Span::call_site);
80
81 match key.as_str() {
82 ATTR_DATA => match &pair.value {
83 Expr::Lit(ExprLit {
84 lit: Lit::Str(s), ..
85 }) => {
86 data = Some(s.value());
87 }
88 Expr::Path(expr_path) => {
89 let type_path = syn::TypePath {
90 attrs: vec![],
91 qself: expr_path.qself.clone(),
92 path: expr_path.path.clone(),
93 };
94 data = Some(type_display(&syn::Type::Path(type_path)));
95 }
96 _ => {
97 return Err(syn::Error::new(
98 span,
99 format!(
100 "{} must be a string literal (\"StreamEvent\") or type path (StreamEvent)",
101 ATTR_DATA
102 ),
103 ));
104 }
105 },
106 _ => {
107 return Err(syn::Error::new(
108 span,
109 Error::unknown_key(span, &key, &[ATTR_DATA]).to_string(),
110 ));
111 }
112 }
113 }
114 }
115
116 Ok(MethodShorthandArgs { path, data })
117 }
118}
119
120impl MethodShorthandArgs {
125 pub fn into_orpc_args(self, method: &str) -> OrpcArgs {
126 OrpcArgs {
127 method: method.to_uppercase(),
128 path: self.path,
129 stream_event: self.data,
130 }
131 }
132}
133
134const VALID_KEYS: &[&str] = &[ATTR_METHOD, ATTR_PATH, ATTR_DATA];
135
136impl Parse for OrpcArgs {
137 fn parse(input: ParseStream) -> syn::Result<Self> {
138 let pairs = Punctuated::<MetaNameValue, Token![,]>::parse_terminated(input)?;
139
140 let mut method = None;
141 let mut path = None;
142 let mut stream_event = None;
143
144 for pair in &pairs {
145 let key = pair
146 .path
147 .get_ident()
148 .map(|i| i.to_string())
149 .unwrap_or_default();
150
151 let span = pair
152 .path
153 .get_ident()
154 .map(|i| i.span())
155 .unwrap_or_else(proc_macro2::Span::call_site);
156
157 match key.as_str() {
158 ATTR_METHOD => {
159 if let Expr::Lit(ExprLit {
160 lit: Lit::Str(s), ..
161 }) = &pair.value
162 {
163 method = Some(s.value().to_uppercase());
164 } else {
165 return Err(syn::Error::new(
166 span,
167 Error::invalid_attr_value(
168 span,
169 &key,
170 "a string literal",
171 "non-string expression",
172 )
173 .to_string(),
174 ));
175 }
176 }
177 ATTR_PATH => {
178 if let Expr::Lit(ExprLit {
179 lit: Lit::Str(s), ..
180 }) = &pair.value
181 {
182 path = Some(s.value());
183 } else {
184 return Err(syn::Error::new(
185 span,
186 Error::invalid_attr_value(
187 span,
188 &key,
189 "a string literal",
190 "non-string expression",
191 )
192 .to_string(),
193 ));
194 }
195 }
196 ATTR_DATA => {
197 match &pair.value {
198 Expr::Lit(ExprLit {
201 lit: Lit::Str(s), ..
202 }) => {
203 stream_event = Some(s.value());
204 }
205 Expr::Path(expr_path) => {
207 let type_path = syn::TypePath {
208 attrs: vec![],
209 qself: expr_path.qself.clone(),
210 path: expr_path.path.clone(),
211 };
212 stream_event = Some(type_display(&syn::Type::Path(type_path)));
213 }
214 _ => {
215 return Err(syn::Error::new(
216 span,
217 format!(
218 "{} must be a string literal (\"StreamEvent\") or type path (StreamEvent)",
219 ATTR_DATA
220 ),
221 ));
222 }
223 }
224 }
225 _ => {
226 return Err(syn::Error::new(
227 span,
228 Error::unknown_key(span, &key, VALID_KEYS).to_string(),
229 ));
230 }
231 }
232 }
233
234 let method = method.ok_or_else(|| {
235 syn::Error::new(
236 proc_macro2::Span::call_site(),
237 Error::missing_required_attr(
238 proc_macro2::Span::call_site(),
239 ATTR_METHOD,
240 "add `method = \"GET\"` to #[orpc]",
241 )
242 .to_string(),
243 )
244 })?;
245
246 let path = path.ok_or_else(|| {
247 syn::Error::new(
248 proc_macro2::Span::call_site(),
249 Error::missing_required_attr(
250 proc_macro2::Span::call_site(),
251 ATTR_PATH,
252 "add `path = \"/your/route\"` to #[orpc]",
253 )
254 .to_string(),
255 )
256 })?;
257
258 Ok(OrpcArgs {
259 method,
260 path,
261 stream_event,
262 })
263 }
264}
265
266pub fn expand_orpc(args: OrpcArgs, func: ItemFn) -> TokenStream {
274 match try_expand_orpc(args, func) {
275 Ok(ts) => ts,
276 Err(e) => e.to_compile_error(),
277 }
278}
279
280fn try_expand_orpc(args: OrpcArgs, func: ItemFn) -> Result<TokenStream> {
281 let sig = extract_handler_signature(&func)?;
282
283 let fn_name = &func.sig.ident;
284 let fn_name_str = sig.fn_name.as_str();
285 let method = &args.method;
286 let path = &args.path;
287
288 let output_type_str = type_display(&sig.output_type);
289
290 let error_type_token = match &sig.error_type {
291 Some(ty) => {
292 let s = type_display(ty);
293 quote! { Some(#s) }
294 }
295 None => quote! { None },
296 };
297
298 let stream_event_token = match &args.stream_event {
299 Some(type_name) => {
300 let s = type_name.as_str();
301 quote! { Some(#s) }
302 }
303 None => quote! { None },
304 };
305
306 let input_type_str = match &sig.input_type {
307 Some(ty) => type_display(ty),
308 None => "()".to_string(),
309 };
310
311 let query_type_token = match &sig.query_type {
312 Some(ty) => {
313 let s = type_display(ty);
314 quote! { Some(#s) }
315 }
316 None => quote! { None },
317 };
318
319 let path_param_types_str = sig
322 .path_params
323 .iter()
324 .map(|(_, ty)| type_display(ty))
325 .collect::<Vec<_>>()
326 .join(",");
327
328 let registration = emit_handler_registration(fn_name, method, path, &sig.state_type);
329 let schema_registrations = emit_schema_registrations(&func);
330
331 Ok(quote! {
332 #func
333
334 ::rorpc::inventory::submit! {
335 ::rorpc::HandlerMetadata {
336 name: #fn_name_str,
337 method: #method,
338 path: #path,
339 input_type_name: #input_type_str,
340 query_type_name: #query_type_token,
341 output_type_name: #output_type_str,
342 module_path: ::std::module_path!(),
343 namespace: None,
344 error_type_name: #error_type_token,
345 stream_event_type_name: #stream_event_token,
346 path_param_types: #path_param_types_str,
347 }
348 }
349
350 #registration
351 #schema_registrations
352 })
353}
354
355fn emit_handler_registration(
360 fn_name: &syn::Ident,
361 method: &str,
362 path: &str,
363 state_type: &Option<syn::Type>,
364) -> TokenStream {
365 if let Some(state_ty) = state_type {
366 quote! {
367 ::rorpc::inventory::submit! {
368 ::rorpc::HandlerRegistration {
369 path: #path,
370 method: #method,
371 factory: |state: ::std::sync::Arc<dyn ::std::any::Any + Send + Sync>, final_path: &str| {
372 use ::axum::routing::{delete, get, patch, post, put};
373 let method_router = match #method {
374 "GET" => get(#fn_name),
375 "POST" => post(#fn_name),
376 "PUT" => put(#fn_name),
377 "PATCH" => patch(#fn_name),
378 "DELETE" => delete(#fn_name),
379 _ => post(#fn_name),
380 };
381 if let Some(typed_state) = state.downcast_ref::<#state_ty>() {
382 ::axum::Router::new()
383 .route(final_path, method_router)
384 .with_state(typed_state.clone())
385 } else {
386 ::axum::Router::new()
387 }
388 },
389 }
390 }
391 }
392 } else {
393 quote! {
394 ::rorpc::inventory::submit! {
395 ::rorpc::HandlerRegistration {
396 path: #path,
397 method: #method,
398 factory: |_state: ::std::sync::Arc<dyn ::std::any::Any + Send + Sync>, final_path: &str| {
399 use ::axum::routing::{delete, get, patch, post, put};
400 let method_router = match #method {
401 "GET" => get(#fn_name),
402 "POST" => post(#fn_name),
403 "PUT" => put(#fn_name),
404 "PATCH" => patch(#fn_name),
405 "DELETE" => delete(#fn_name),
406 _ => post(#fn_name),
407 };
408 ::axum::Router::new().route(final_path, method_router)
409 },
410 }
411 }
412 }
413 }
414}
415
416fn emit_schema_registrations(func: &ItemFn) -> TokenStream {
421 let mut seen = std::collections::HashSet::new();
422 let mut registrations = Vec::new();
423
424 let mut candidates: Vec<&syn::Type> = Vec::new();
426
427 for arg in &func.sig.inputs {
428 if let syn::FnArg::Typed(pat_type) = arg {
429 if let Some(m) = try_extract_wrapper(&pat_type.ty, JSON)
431 && let Some(inner) = m.first_type()
432 {
433 candidates.push(inner);
434 }
435 if let Some(m) = try_extract_wrapper(&pat_type.ty, QUERY)
437 && let Some(inner) = m.first_type()
438 {
439 candidates.push(inner);
440 }
441 }
442 }
443
444 if let syn::ReturnType::Type(_, ty) = &func.sig.output {
445 if let Some(m) = try_extract_wrapper(ty, JSON) {
447 if let Some(inner) = m.first_type() {
448 candidates.push(inner);
449 }
450 } else if let Some(result_m) = try_extract_wrapper(ty, RESULT)
451 && let Some(first) = result_m.first_type()
452 && let Some(json_m) = try_extract_wrapper(first, JSON)
453 && let Some(inner) = json_m.first_type()
454 {
455 candidates.push(inner);
456 }
457 }
458
459 for ty in candidates {
460 if let Some(custom_ty) = innermost_custom_type(ty) {
461 if is_primitive(custom_ty) {
462 continue;
463 }
464 let name = type_display(custom_ty);
465 if !seen.insert(name.clone()) {
466 continue;
467 }
468 let fallback = format!(
469 "z.unknown() /* add #[derive(ZodTs)] to {} for a real schema */",
470 name
471 );
472 registrations.push(quote! {
473 ::rorpc::inventory::submit! {
474 ::rorpc::SchemaRegistration {
475 type_name: #name,
476 zod_ts: || #fallback.to_string(),
477 dependent_types: || vec![],
478 }
479 }
480 });
481 }
482 }
483
484 quote! { #(#registrations)* }
485}
486
487#[cfg(test)]
492mod tests {
493 use super::*;
494 use syn::parse_quote;
495
496 #[test]
497 fn parse_data_type_string() {
498 let args: OrpcArgs = syn::parse_quote! {
500 method = "GET", path = "/stream", data = "StreamEvent"
501 };
502
503 assert_eq!(args.method, "GET");
504 assert_eq!(args.path, "/stream");
505 assert_eq!(args.stream_event, Some("StreamEvent".to_string()));
506 }
507
508 #[test]
509 fn parse_data_qualified_path_string() {
510 let args: OrpcArgs = syn::parse_quote! {
512 method = "GET", path = "/stream", data = "crate::models::StreamEvent"
513 };
514
515 assert_eq!(
516 args.stream_event,
517 Some("crate::models::StreamEvent".to_string())
518 );
519 }
520
521 #[test]
522 fn parse_data_type_path_backward_compat() {
523 let args: OrpcArgs = syn::parse_quote! {
525 method = "GET", path = "/stream", data = StreamEvent
526 };
527
528 assert_eq!(args.method, "GET");
529 assert_eq!(args.path, "/stream");
530 assert_eq!(args.stream_event, Some("StreamEvent".to_string()));
531 }
532
533 #[test]
534 fn parse_without_data() {
535 let args: OrpcArgs = syn::parse_quote! {
537 method = "POST", path = "/create"
538 };
539
540 assert_eq!(args.method, "POST");
541 assert_eq!(args.path, "/create");
542 assert_eq!(args.stream_event, None);
543 }
544
545 #[test]
546 fn data_type_converts_to_string_literal() {
547 let args: OrpcArgs = syn::parse_quote! {
549 method = "GET", path = "/stream", data = "StreamEvent"
550 };
551
552 let func: syn::ItemFn = parse_quote! {
553 async fn stream_test() -> Sse<impl Stream<Item = Event>> {
554 todo!()
555 }
556 };
557
558 let result = try_expand_orpc(args, func);
559 assert!(result.is_ok(), "expand_orpc should succeed");
560
561 let tokens = result.unwrap().to_string();
563
564 assert!(
567 tokens.contains("stream_event_type_name") && tokens.contains(r#""StreamEvent""#),
568 "Generated code should contain stream_event_type_name: Some(\"StreamEvent\"), got: {}",
569 tokens
570 );
571 assert!(
573 !tokens.contains("Some (StreamEvent)") && !tokens.contains("Some(StreamEvent)"),
574 "stream_event_type_name must be a string literal, not a bare identifier"
575 );
576 }
577}
578
579#[test]
584fn parse_shorthand_path_only() {
585 let args: MethodShorthandArgs = syn::parse_quote! { "/planet/list" };
587
588 assert_eq!(args.path, "/planet/list");
589 assert_eq!(args.data, None);
590}
591
592#[test]
593fn parse_shorthand_with_data_string() {
594 let args: MethodShorthandArgs = syn::parse_quote! { "/stream", data = "EventData" };
596
597 assert_eq!(args.path, "/stream");
598 assert_eq!(args.data, Some("EventData".to_string()));
599}
600
601#[test]
602fn parse_shorthand_with_qualified_data() {
603 let args: MethodShorthandArgs = syn::parse_quote! { "/stream", data = "models::EventData" };
605
606 assert_eq!(args.path, "/stream");
607 assert_eq!(args.data, Some("models::EventData".to_string()));
608}
609
610#[test]
611fn parse_shorthand_with_data_type_path() {
612 let args: MethodShorthandArgs = syn::parse_quote! { "/stream", data = EventData };
614
615 assert_eq!(args.path, "/stream");
616 assert_eq!(args.data, Some("EventData".to_string()));
617}
618
619#[test]
620fn shorthand_converts_to_orpc_args() {
621 let shorthand: MethodShorthandArgs = syn::parse_quote! { "/planet/list" };
622 let args = shorthand.into_orpc_args("GET");
623
624 assert_eq!(args.method, "GET");
625 assert_eq!(args.path, "/planet/list");
626 assert_eq!(args.stream_event, None);
627}
628
629#[test]
630fn shorthand_with_data_converts_to_orpc_args() {
631 let shorthand: MethodShorthandArgs = syn::parse_quote! { "/stream", data = "EventData" };
632 let args = shorthand.into_orpc_args("GET");
633
634 assert_eq!(args.method, "GET");
635 assert_eq!(args.path, "/stream");
636 assert_eq!(args.stream_event, Some("EventData".to_string()));
637}
638
639#[test]
640fn shorthand_method_normalized_to_uppercase() {
641 let shorthand: MethodShorthandArgs = syn::parse_quote! { "/test" };
642 let args = shorthand.into_orpc_args("get"); assert_eq!(args.method, "GET"); }