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, 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<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
120pub 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
186fn 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
247fn emit_schema_registrations(func: &ItemFn) -> TokenStream {
252 let mut seen = std::collections::HashSet::new();
253 let mut registrations = Vec::new();
254
255 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 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}