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 error_type_name: #error_type_token,
344 stream_event_type_name: #stream_event_token,
345 path_param_types: #path_param_types_str,
346 }
347 }
348
349 #registration
350 #schema_registrations
351 })
352}
353
354fn emit_handler_registration(
359 fn_name: &syn::Ident,
360 method: &str,
361 path: &str,
362 state_type: &Option<syn::Type>,
363) -> TokenStream {
364 if let Some(state_ty) = state_type {
365 quote! {
366 ::rorpc::inventory::submit! {
367 ::rorpc::HandlerRegistration {
368 path: #path,
369 method: #method,
370 factory: |state: ::std::sync::Arc<dyn ::std::any::Any + Send + Sync>| {
371 use ::axum::routing::{delete, get, patch, post, put};
372 let method_router = match #method {
373 "GET" => get(#fn_name),
374 "POST" => post(#fn_name),
375 "PUT" => put(#fn_name),
376 "PATCH" => patch(#fn_name),
377 "DELETE" => delete(#fn_name),
378 _ => post(#fn_name),
379 };
380 if let Some(typed_state) = state.downcast_ref::<#state_ty>() {
381 ::axum::Router::new()
382 .route(#path, method_router)
383 .with_state(typed_state.clone())
384 } else {
385 ::axum::Router::new()
386 }
387 },
388 }
389 }
390 }
391 } else {
392 quote! {
393 ::rorpc::inventory::submit! {
394 ::rorpc::HandlerRegistration {
395 path: #path,
396 method: #method,
397 factory: |_state: ::std::sync::Arc<dyn ::std::any::Any + Send + Sync>| {
398 use ::axum::routing::{delete, get, patch, post, put};
399 let method_router = match #method {
400 "GET" => get(#fn_name),
401 "POST" => post(#fn_name),
402 "PUT" => put(#fn_name),
403 "PATCH" => patch(#fn_name),
404 "DELETE" => delete(#fn_name),
405 _ => post(#fn_name),
406 };
407 ::axum::Router::new().route(#path, method_router)
408 },
409 }
410 }
411 }
412 }
413}
414
415fn emit_schema_registrations(func: &ItemFn) -> TokenStream {
420 let mut seen = std::collections::HashSet::new();
421 let mut registrations = Vec::new();
422
423 let mut candidates: Vec<&syn::Type> = Vec::new();
425
426 for arg in &func.sig.inputs {
427 if let syn::FnArg::Typed(pat_type) = arg {
428 if let Some(m) = try_extract_wrapper(&pat_type.ty, JSON)
430 && let Some(inner) = m.first_type()
431 {
432 candidates.push(inner);
433 }
434 if let Some(m) = try_extract_wrapper(&pat_type.ty, QUERY)
436 && let Some(inner) = m.first_type()
437 {
438 candidates.push(inner);
439 }
440 }
441 }
442
443 if let syn::ReturnType::Type(_, ty) = &func.sig.output {
444 if let Some(m) = try_extract_wrapper(ty, JSON) {
446 if let Some(inner) = m.first_type() {
447 candidates.push(inner);
448 }
449 } else if let Some(result_m) = try_extract_wrapper(ty, RESULT)
450 && let Some(first) = result_m.first_type()
451 && let Some(json_m) = try_extract_wrapper(first, JSON)
452 && let Some(inner) = json_m.first_type()
453 {
454 candidates.push(inner);
455 }
456 }
457
458 for ty in candidates {
459 if let Some(custom_ty) = innermost_custom_type(ty) {
460 if is_primitive(custom_ty) {
461 continue;
462 }
463 let name = type_display(custom_ty);
464 if !seen.insert(name.clone()) {
465 continue;
466 }
467 let fallback = format!(
468 "z.unknown() /* add #[derive(ZodTs)] to {} for a real schema */",
469 name
470 );
471 registrations.push(quote! {
472 ::rorpc::inventory::submit! {
473 ::rorpc::SchemaRegistration {
474 type_name: #name,
475 zod_ts: || #fallback.to_string(),
476 dependent_types: || vec![],
477 }
478 }
479 });
480 }
481 }
482
483 quote! { #(#registrations)* }
484}
485
486#[cfg(test)]
491mod tests {
492 use super::*;
493 use syn::parse_quote;
494
495 #[test]
496 fn parse_data_type_string() {
497 let args: OrpcArgs = syn::parse_quote! {
499 method = "GET", path = "/stream", data = "StreamEvent"
500 };
501
502 assert_eq!(args.method, "GET");
503 assert_eq!(args.path, "/stream");
504 assert_eq!(args.stream_event, Some("StreamEvent".to_string()));
505 }
506
507 #[test]
508 fn parse_data_qualified_path_string() {
509 let args: OrpcArgs = syn::parse_quote! {
511 method = "GET", path = "/stream", data = "crate::models::StreamEvent"
512 };
513
514 assert_eq!(
515 args.stream_event,
516 Some("crate::models::StreamEvent".to_string())
517 );
518 }
519
520 #[test]
521 fn parse_data_type_path_backward_compat() {
522 let args: OrpcArgs = syn::parse_quote! {
524 method = "GET", path = "/stream", data = StreamEvent
525 };
526
527 assert_eq!(args.method, "GET");
528 assert_eq!(args.path, "/stream");
529 assert_eq!(args.stream_event, Some("StreamEvent".to_string()));
530 }
531
532 #[test]
533 fn parse_without_data() {
534 let args: OrpcArgs = syn::parse_quote! {
536 method = "POST", path = "/create"
537 };
538
539 assert_eq!(args.method, "POST");
540 assert_eq!(args.path, "/create");
541 assert_eq!(args.stream_event, None);
542 }
543
544 #[test]
545 fn data_type_converts_to_string_literal() {
546 let args: OrpcArgs = syn::parse_quote! {
548 method = "GET", path = "/stream", data = "StreamEvent"
549 };
550
551 let func: syn::ItemFn = parse_quote! {
552 async fn stream_test() -> Sse<impl Stream<Item = Event>> {
553 todo!()
554 }
555 };
556
557 let result = try_expand_orpc(args, func);
558 assert!(result.is_ok(), "expand_orpc should succeed");
559
560 let tokens = result.unwrap().to_string();
562
563 assert!(
566 tokens.contains("stream_event_type_name") && tokens.contains(r#""StreamEvent""#),
567 "Generated code should contain stream_event_type_name: Some(\"StreamEvent\"), got: {}",
568 tokens
569 );
570 assert!(
572 !tokens.contains("Some (StreamEvent)") && !tokens.contains("Some(StreamEvent)"),
573 "stream_event_type_name must be a string literal, not a bare identifier"
574 );
575 }
576}
577
578#[test]
583fn parse_shorthand_path_only() {
584 let args: MethodShorthandArgs = syn::parse_quote! { "/planet/list" };
586
587 assert_eq!(args.path, "/planet/list");
588 assert_eq!(args.data, None);
589}
590
591#[test]
592fn parse_shorthand_with_data_string() {
593 let args: MethodShorthandArgs = syn::parse_quote! { "/stream", data = "EventData" };
595
596 assert_eq!(args.path, "/stream");
597 assert_eq!(args.data, Some("EventData".to_string()));
598}
599
600#[test]
601fn parse_shorthand_with_qualified_data() {
602 let args: MethodShorthandArgs = syn::parse_quote! { "/stream", data = "models::EventData" };
604
605 assert_eq!(args.path, "/stream");
606 assert_eq!(args.data, Some("models::EventData".to_string()));
607}
608
609#[test]
610fn parse_shorthand_with_data_type_path() {
611 let args: MethodShorthandArgs = syn::parse_quote! { "/stream", data = EventData };
613
614 assert_eq!(args.path, "/stream");
615 assert_eq!(args.data, Some("EventData".to_string()));
616}
617
618#[test]
619fn shorthand_converts_to_orpc_args() {
620 let shorthand: MethodShorthandArgs = syn::parse_quote! { "/planet/list" };
621 let args = shorthand.into_orpc_args("GET");
622
623 assert_eq!(args.method, "GET");
624 assert_eq!(args.path, "/planet/list");
625 assert_eq!(args.stream_event, None);
626}
627
628#[test]
629fn shorthand_with_data_converts_to_orpc_args() {
630 let shorthand: MethodShorthandArgs = syn::parse_quote! { "/stream", data = "EventData" };
631 let args = shorthand.into_orpc_args("GET");
632
633 assert_eq!(args.method, "GET");
634 assert_eq!(args.path, "/stream");
635 assert_eq!(args.stream_event, Some("EventData".to_string()));
636}
637
638#[test]
639fn shorthand_method_normalized_to_uppercase() {
640 let shorthand: MethodShorthandArgs = syn::parse_quote! { "/test" };
641 let args = shorthand.into_orpc_args("get"); assert_eq!(args.method, "GET"); }