1use proc_macro::TokenStream;
22use proc_macro2::TokenStream as TokenStream2;
23use quote::quote;
24use syn::{
25 DeriveInput, GenericArgument, Ident, ItemFn, LitStr, PathArguments, ReturnType, Token, Type,
26 parse::Parse, parse::ParseStream, parse_macro_input,
27};
28
29struct CommandAttr {
33 name: Option<String>,
35}
36
37impl Parse for CommandAttr {
41 fn parse(input: ParseStream) -> syn::Result<Self> {
42 if input.is_empty() {
43 return Ok(CommandAttr { name: None });
44 }
45
46 let key: Ident = input.parse()?;
47 if key != "name" {
48 return Err(syn::Error::new(key.span(), "expected `name`"));
49 }
50 let _: Token![=] = input.parse()?;
51 let name: LitStr = input.parse()?;
52
53 Ok(CommandAttr {
54 name: Some(name.value()),
55 })
56 }
57}
58
59#[proc_macro_attribute]
74pub fn command(attr: TokenStream, item: TokenStream) -> TokenStream {
75 let attr = parse_macro_input!(attr as CommandAttr);
76 let func = parse_macro_input!(item as ItemFn);
77
78 let docs: Vec<String> = func
80 .attrs
81 .iter()
82 .filter_map(|attr| {
83 if attr.path().is_ident("doc")
84 && let syn::Meta::NameValue(nv) = &attr.meta
85 && let syn::Expr::Lit(syn::ExprLit {
86 lit: syn::Lit::Str(s),
87 ..
88 }) = &nv.value
89 {
90 return Some(s.value().trim().to_string());
91 }
92 None
93 })
94 .collect();
95 let doc_comment = docs.join("\n");
96
97 let is_async = func.sig.asyncness.is_some();
98 let fn_name = &func.sig.ident;
99 let vis = &func.vis;
100 let output_type = match &func.sig.output {
101 ReturnType::Type(_, ty) => match extract_result_inner(ty) {
102 Some(inner) => inner,
103 None => {
104 return syn::Error::new_spanned(
105 ty,
106 "#[command] must return `Result<O>` where O: Serialize + JsonSchema",
107 )
108 .to_compile_error()
109 .into();
110 }
111 },
112 _ => {
113 return syn::Error::new_spanned(
114 &func.sig,
115 "#[command] must have an explicit return type `Result<O>`",
116 )
117 .to_compile_error()
118 .into();
119 }
120 };
121
122 struct ParamInfo {
124 pat: syn::Pat,
125 ty: Type,
126 is_state: bool,
127 state_inner: Option<Type>,
128 }
129
130 let mut params = Vec::new();
131 for input in &func.sig.inputs {
132 match input {
133 syn::FnArg::Receiver(_) => {
134 return syn::Error::new_spanned(input, "#[command] functions cannot accept `self`")
135 .to_compile_error()
136 .into();
137 }
138 syn::FnArg::Typed(pat_type) => {
139 let is_state_inner = extract_state_inner(&pat_type.ty);
140 let is_state = is_state_inner.is_some();
141 params.push(ParamInfo {
142 pat: (*pat_type.pat).clone(),
143 ty: (*pat_type.ty).clone(),
144 is_state,
145 state_inner: is_state_inner,
146 });
147 }
148 }
149 }
150
151 let data_params: Vec<&ParamInfo> = params.iter().filter(|p| !p.is_state).collect();
152 if data_params.len() > 1 {
153 return syn::Error::new_spanned(
154 &func.sig.inputs,
155 "#[command] supports at most one input data parameter (plus optional State<T> parameters)",
156 )
157 .to_compile_error()
158 .into();
159 }
160
161 let input_type = if let Some(data) = data_params.first() {
162 let ty = &data.ty;
163 quote! { #ty }
164 } else {
165 quote! { () }
166 };
167
168 let inner_fn_name = Ident::new(
169 &format!("__rustra_inner_{}", fn_name),
170 proc_macro2::Span::call_site(),
171 );
172 let mut inner_func = func.clone();
173 inner_func.sig.ident = inner_fn_name.clone();
174
175 let command_name = attr.name.unwrap_or_else(|| {
176 let raw = fn_name.to_string();
177 snake_to_lower_camel(raw.trim_end_matches("_command"))
178 });
179 let meta_ident = Ident::new(
180 &format!("__RUstra_meta_{}", fn_name),
181 proc_macro2::Span::call_site(),
182 );
183 let doc_ident = Ident::new(
184 &format!("__RUstra_doc_{}", fn_name),
185 proc_macro2::Span::call_site(),
186 );
187
188 let mut state_bindings = Vec::new();
190 let mut call_args = Vec::new();
191
192 for param in ¶ms {
193 if param.is_state {
194 let pat = ¶m.pat;
195 let ty = ¶m.ty;
196 let inner_ty = param.state_inner.as_ref().unwrap();
197 state_bindings.push(quote! {
198 let #pat: #ty = rustra::get_state::<#inner_ty>()
199 .ok_or_else(|| rustra::RustraError::internal(concat!("State<", stringify!(#inner_ty), "> not managed in package")))?;
200 });
201 call_args.push(quote! { #pat });
202 } else {
203 call_args.push(quote! { __rustra_input });
204 }
205 }
206
207 let outer_input_arg = if data_params.is_empty() {
208 quote! { _: () }
209 } else {
210 quote! { __rustra_input: #input_type }
211 };
212
213 let inner_invocation = if is_async {
214 quote! {
215 rustra::__private::block_on(async move {
216 #inner_fn_name(#(#call_args),*).await
217 })
218 }
219 } else {
220 quote! {
221 #inner_fn_name(#(#call_args),*)
222 }
223 };
224
225 let expanded = quote! {
226 #inner_func
227
228 #vis fn #fn_name(#outer_input_arg) -> rustra::Result<#output_type> {
229 #(#state_bindings)*
230 #inner_invocation
231 }
232
233 #[allow(non_upper_case_globals, dead_code)]
234 const #meta_ident: &str = #command_name;
235
236 #[allow(non_upper_case_globals, dead_code)]
237 const #doc_ident: &str = #doc_comment;
238
239 #[allow(dead_code)]
240 const _: () = {
241 fn _assert_command_bounds<
242 __I: rustra::__private::CommandInput,
243 __O: rustra::__private::CommandOutput,
244 >() {
245 }
246 fn _check_command_bounds() {
247 _assert_command_bounds::<#input_type, #output_type>();
248 }
249 };
250 };
251
252 expanded.into()
253}
254
255fn extract_state_inner(ty: &Type) -> Option<Type> {
257 let Type::Path(type_path) = ty else {
258 return None;
259 };
260 let segment = type_path.path.segments.last()?;
261 if segment.ident == "State"
262 && let PathArguments::AngleBracketed(args) = &segment.arguments
263 && let Some(GenericArgument::Type(inner_ty)) = args.args.first()
264 {
265 return Some(inner_ty.clone());
266 }
267 None
268}
269
270struct RegisterInput {
274 builder: syn::Expr,
276 commands: Vec<Ident>,
278}
279
280impl Parse for RegisterInput {
284 fn parse(input: ParseStream) -> syn::Result<Self> {
285 let builder: syn::Expr = input.parse()?;
286 let _: Token![,] = input.parse()?;
287
288 let mut commands = Vec::new();
289 loop {
290 let name: Ident = input.parse()?;
291 commands.push(name);
292 if input.parse::<Token![,]>().is_err() {
293 break;
294 }
295 }
296
297 Ok(RegisterInput { builder, commands })
298 }
299}
300
301#[proc_macro]
319pub fn register(input: TokenStream) -> TokenStream {
320 let input = parse_macro_input!(input as RegisterInput);
321
322 if input.commands.is_empty() {
323 return syn::Error::new(
324 proc_macro2::Span::call_site(),
325 "register! requires at least one command function after the builder expression",
326 )
327 .to_compile_error()
328 .into();
329 }
330
331 let builder = &input.builder;
332 let chain: TokenStream2 = input
333 .commands
334 .iter()
335 .map(|fn_name| {
336 let meta_ident = Ident::new(
337 &format!("__RUstra_meta_{}", fn_name),
338 proc_macro2::Span::call_site(),
339 );
340 quote! { .command(#meta_ident, #fn_name) }
341 })
342 .collect();
343
344 let expanded = quote! {
345 #builder #chain
346 };
347
348 expanded.into()
349}
350
351fn extract_result_inner(ty: &Type) -> Option<TokenStream2> {
355 let Type::Path(type_path) = ty else {
356 return None;
357 };
358 let segment = type_path.path.segments.last()?;
359 if segment.ident != "Result" {
360 return None;
361 }
362 let PathArguments::AngleBracketed(args) = &segment.arguments else {
363 return None;
364 };
365 let GenericArgument::Type(inner_ty) = args.args.first()? else {
366 return None;
367 };
368 Some(quote! { #inner_ty })
369}
370
371#[proc_macro_attribute]
387pub fn bridge_type(_attr: TokenStream, item: TokenStream) -> TokenStream {
388 let mut input = parse_macro_input!(item as DeriveInput);
389
390 input.attrs.push(syn::parse_quote! {
392 #[derive(Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
393 });
394
395 let has_serde_rename = input.attrs.iter().any(|attr| {
397 if !attr.path().is_ident("serde") {
398 return false;
399 }
400 let Ok(nested) = attr.parse_args_with(
401 syn::punctuated::Punctuated::<syn::MetaNameValue, syn::Token![,]>::parse_terminated,
402 ) else {
403 return false;
404 };
405 nested.iter().any(|nv| nv.path.is_ident("rename_all"))
406 });
407
408 if !has_serde_rename {
409 input.attrs.push(syn::parse_quote! {
410 #[serde(rename_all = "camelCase")]
411 });
412 }
413
414 quote! { #input }.into()
415}
416
417struct BuildInput {
421 package_name: LitStr,
422 commands: Vec<Ident>,
423}
424
425impl Parse for BuildInput {
426 fn parse(input: ParseStream) -> syn::Result<Self> {
427 let package_name: LitStr = input.parse()?;
428 let _: Token![,] = input.parse()?;
429
430 let mut commands = Vec::new();
431 loop {
432 if input.is_empty() {
433 break;
434 }
435 let name: Ident = input.parse()?;
436 commands.push(name);
437 if input.parse::<Token![,]>().is_err() {
438 break;
439 }
440 }
441
442 if commands.is_empty() {
443 return Err(syn::Error::new(
444 package_name.span(),
445 "build! requires at least one command function after the package name",
446 ));
447 }
448
449 Ok(BuildInput {
450 package_name,
451 commands,
452 })
453 }
454}
455
456#[proc_macro]
479pub fn build(input: TokenStream) -> TokenStream {
480 let input = parse_macro_input!(input as BuildInput);
481
482 let package_name = &input.package_name;
483 let chain: TokenStream2 = input
484 .commands
485 .iter()
486 .map(|fn_name| {
487 let meta_ident = Ident::new(
488 &format!("__RUstra_meta_{}", fn_name),
489 proc_macro2::Span::call_site(),
490 );
491 quote! { .command(#meta_ident, #fn_name) }
492 })
493 .collect();
494
495 let expanded = quote! {
496 rustra::Package::builder(#package_name) #chain
497 };
498
499 expanded.into()
500}
501
502fn snake_to_lower_camel(name: &str) -> String {
506 let mut output = String::new();
507 let mut uppercase_next = false;
508
509 for character in name.chars() {
510 if character == '_' || character == '-' || character == '.' {
511 uppercase_next = true;
512 continue;
513 }
514
515 if output.is_empty() {
516 output.push(character.to_ascii_lowercase());
517 } else if uppercase_next {
518 output.push(character.to_ascii_uppercase());
519 uppercase_next = false;
520 } else {
521 output.push(character);
522 }
523 }
524
525 output
526}