1#[proc_macro_attribute]
16pub fn command(attr: TokenStream, item: TokenStream) -> TokenStream {
17 let attr = parse_macro_input!(attr as CommandAttr);
18 let func = parse_macro_input!(item as ItemFn);
19
20 let doc_comment = command_doc_comment(&func);
21
22 let is_async = func.sig.asyncness.is_some();
23 let fn_name = &func.sig.ident;
24 let vis = &func.vis;
25 let output_type = match &func.sig.output {
26 ReturnType::Type(_, ty) => match extract_result_inner(ty) {
27 Some(inner) => inner,
28 None => {
29 return syn::Error::new_spanned(
30 ty,
31 "#[command] must return `Result<O>` where O: Serialize + JsonSchema",
32 )
33 .to_compile_error()
34 .into();
35 }
36 },
37 _ => {
38 return syn::Error::new_spanned(
39 &func.sig,
40 "#[command] must have an explicit return type `Result<O>`",
41 )
42 .to_compile_error()
43 .into();
44 }
45 };
46
47 struct ParamInfo {
49 pat: syn::Pat,
50 ty: Type,
51 is_state: bool,
52 state_inner: Option<Type>,
53 }
54
55 let mut params = Vec::new();
56 for input in &func.sig.inputs {
57 match input {
58 syn::FnArg::Receiver(_) => {
59 return syn::Error::new_spanned(input, "#[command] functions cannot accept `self`")
60 .to_compile_error()
61 .into();
62 }
63 syn::FnArg::Typed(pat_type) => {
64 let is_state_inner = extract_state_inner(&pat_type.ty);
65 let is_state = is_state_inner.is_some();
66 params.push(ParamInfo {
67 pat: (*pat_type.pat).clone(),
68 ty: (*pat_type.ty).clone(),
69 is_state,
70 state_inner: is_state_inner,
71 });
72 }
73 }
74 }
75
76 let data_params: Vec<&ParamInfo> = params.iter().filter(|p| !p.is_state).collect();
77 if data_params.len() > 1 {
78 return syn::Error::new_spanned(
79 &func.sig.inputs,
80 "#[command] supports at most one input data parameter (plus optional State<T> parameters)",
81 )
82 .to_compile_error()
83 .into();
84 }
85
86 let input_type = if let Some(data) = data_params.first() {
87 let ty = &data.ty;
88 quote! { #ty }
89 } else {
90 quote! { () }
91 };
92
93 let inner_fn_name = Ident::new(
94 &format!("__rustra_inner_{}", fn_name),
95 proc_macro2::Span::call_site(),
96 );
97 let mut inner_func = func.clone();
98 inner_func.sig.ident = inner_fn_name.clone();
99 inner_func.vis = syn::Visibility::Inherited;
104
105 let command_name = attr.name.unwrap_or_else(|| {
106 let raw = fn_name.to_string();
107 snake_to_lower_camel(raw.trim_end_matches("_command"))
108 });
109 let meta_ident = Ident::new(
110 &format!("__RUstra_meta_{}", fn_name),
111 proc_macro2::Span::call_site(),
112 );
113 let doc_ident = Ident::new(
114 &format!("__RUstra_doc_{}", fn_name),
115 proc_macro2::Span::call_site(),
116 );
117 let capability_ident = Ident::new(
121 &format!("__RUstra_cap_{}", fn_name),
122 proc_macro2::Span::call_site(),
123 );
124 let capability_const = meta_opt_const(
125 &capability_ident,
126 quote! { Option<&str> },
127 attr.capability.as_ref().map(|cap| quote! { #cap }),
128 );
129
130 let platforms_ident = Ident::new(
134 &format!("__RUstra_platforms_{}", fn_name),
135 proc_macro2::Span::call_site(),
136 );
137 let platforms_const_ty = quote! { Option<&'static [rustra::platform::Platform]> };
138 let (supported_cfg, unsupported_cfg, platform_paths, platforms_const): (
139 TokenStream2,
140 TokenStream2,
141 Vec<TokenStream2>,
142 TokenStream2,
143 ) = if let Some(platforms) = &attr.platforms {
144 let mut os_checks = Vec::new();
145 let mut paths = Vec::new();
146 for platform in platforms {
147 let (os, variant_name) = match platform.as_str() {
148 "windows" => ("windows", "Windows"),
149 "macos" => ("macos", "Macos"),
150 "linux" => ("linux", "Linux"),
151 "android" => ("android", "Android"),
152 "ios" => ("ios", "Ios"),
153 other => {
154 return syn::Error::new_spanned(
155 &func.sig.ident,
156 format!(
157 "unknown platform '{other}'; supported: windows, macos, linux, android, ios"
158 ),
159 )
160 .to_compile_error()
161 .into();
162 }
163 };
164 os_checks.push(quote! { target_os = #os });
165 let variant = Ident::new(variant_name, proc_macro2::Span::call_site());
168 paths.push(quote! { rustra::platform::Platform::#variant });
169 }
170 (
171 quote! { any(#(#os_checks),*) },
172 quote! { not(any(#(#os_checks),*)) },
173 paths.clone(),
174 meta_opt_const(
175 &platforms_ident,
176 platforms_const_ty.clone(),
177 Some(quote! { &[#(#paths),*] }),
178 ),
179 )
180 } else {
181 (
182 quote! {},
183 quote! {},
184 Vec::new(),
185 meta_opt_const(&platforms_ident, platforms_const_ty, None),
186 )
187 };
188
189 let errors_ident = Ident::new(
193 &format!("__RUstra_errors_{}", fn_name),
194 proc_macro2::Span::call_site(),
195 );
196 let errors_const = meta_opt_const(
197 &errors_ident,
198 quote! { Option<&'static [rustra::CommandErrorVariant]> },
199 attr.errors.as_ref().map(|errors| {
200 let variants = errors
201 .iter()
202 .map(|code| quote! { rustra::CommandErrorVariant::new(#code) });
203 quote! { &[#(#variants),*] }
204 }),
205 );
206
207 let devices_ident = Ident::new(
211 &format!("__RUstra_devices_{}", fn_name),
212 proc_macro2::Span::call_site(),
213 );
214 let devices_const = meta_opt_const(
215 &devices_ident,
216 quote! { Option<&'static [rustra::device_capabilities::DeviceCapability]> },
217 attr.devices.as_ref().map(|devices| {
218 let capabilities = devices
219 .iter()
220 .map(|token| quote! { rustra::device_capabilities::DeviceCapability::new(#token) });
221 quote! { &[#(#capabilities),*] }
222 }),
223 );
224
225 let wrapper_unsafety: TokenStream2 = if attr.capability.is_some() {
232 quote! { unsafe }
233 } else {
234 quote! {}
235 };
236 let register_ident = Ident::new(
243 &format!("__rustra_register_{}", fn_name),
244 proc_macro2::Span::call_site(),
245 );
246 let register_call: TokenStream2 = if attr.capability.is_some() {
247 quote! { unsafe { #fn_name(__rustra_input) } }
248 } else {
249 quote! { #fn_name(__rustra_input) }
250 };
251
252 let mut state_bindings = Vec::new();
254 let mut call_args = Vec::new();
255
256 for param in ¶ms {
257 if param.is_state {
258 let pat = ¶m.pat;
259 let ty = ¶m.ty;
260 let inner_ty = param.state_inner.as_ref().unwrap();
261 state_bindings.push(quote! {
262 let #pat: #ty = rustra::get_state::<#inner_ty>()
263 .ok_or_else(|| rustra::RustraError::internal(concat!("State<", stringify!(#inner_ty), "> not managed in package")))?;
264 });
265 call_args.push(quote! { #pat });
266 } else {
267 call_args.push(quote! { __rustra_input });
268 }
269 }
270
271 let outer_input_arg = if data_params.is_empty() {
272 quote! { _: () }
273 } else {
274 quote! { __rustra_input: #input_type }
275 };
276
277 let inner_invocation = if is_async {
278 quote! {
279 rustra::__private::block_on(async move {
280 #inner_fn_name(#(#call_args),*).await
281 })
282 }
283 } else {
284 quote! {
285 #inner_fn_name(#(#call_args),*)
286 }
287 };
288
289 let mut stub_func = func.clone();
293 stub_func.sig.ident = inner_fn_name.clone();
294 stub_func.vis = syn::Visibility::Inherited;
295 stub_func.block = syn::parse_quote! {
296 {
297 Err(rustra::RustraError::platform_unavailable(
298 #command_name,
299 &[#(#platform_paths),*],
300 ))
301 }
302 };
303
304 let (real_inner, stub_inner): (TokenStream2, TokenStream2) = if attr.platforms.is_some() {
305 (
306 quote! { #[cfg(#supported_cfg)] #inner_func },
307 quote! { #[cfg(#unsupported_cfg)] #[allow(unused_variables)] #stub_func },
308 )
309 } else {
310 (quote! { #inner_func }, quote! {})
311 };
312
313 let expanded = quote! {
314 #real_inner
315 #stub_inner
316
317 #vis #wrapper_unsafety fn #fn_name(#outer_input_arg) -> rustra::Result<#output_type> {
318 #(#state_bindings)*
319 #inner_invocation
320 }
321
322 #capability_const
323
324 #platforms_const
325
326 #errors_const
327
328 #devices_const
329
330 #[doc(hidden)]
331 fn #register_ident(__rustra_input: #input_type) -> rustra::Result<#output_type> {
332 #register_call
333 }
334
335 #[allow(non_upper_case_globals, dead_code)]
336 const #meta_ident: &str = #command_name;
337
338 #[allow(non_upper_case_globals, dead_code)]
339 const #doc_ident: &str = #doc_comment;
340
341 #[allow(dead_code)]
342 const _: () = {
343 fn _assert_command_bounds<
344 __I: rustra::__private::CommandInput,
345 __O: rustra::__private::CommandOutput,
346 >() {
347 }
348 fn _check_command_bounds() {
349 _assert_command_bounds::<#input_type, #output_type>();
350 }
351 };
352 };
353
354 expanded.into()
355}