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 execution_ident = Ident::new(
114 &format!("__RUstra_execution_{}", fn_name),
115 proc_macro2::Span::call_site(),
116 );
117 let execution = if is_async {
118 quote! { rustra::CommandExecution::Async }
119 } else {
120 quote! { rustra::CommandExecution::Sync }
121 };
122 let doc_ident = Ident::new(
123 &format!("__RUstra_doc_{}", fn_name),
124 proc_macro2::Span::call_site(),
125 );
126 let capability_ident = Ident::new(
130 &format!("__RUstra_cap_{}", fn_name),
131 proc_macro2::Span::call_site(),
132 );
133 let capability_const = meta_opt_const(
134 &capability_ident,
135 quote! { Option<&str> },
136 attr.capability.as_ref().map(|cap| quote! { #cap }),
137 );
138
139 let platforms_ident = Ident::new(
143 &format!("__RUstra_platforms_{}", fn_name),
144 proc_macro2::Span::call_site(),
145 );
146 let platforms_const_ty = quote! { Option<&'static [rustra::platform::Platform]> };
147 let (supported_cfg, unsupported_cfg, platform_paths, platforms_const): (
148 TokenStream2,
149 TokenStream2,
150 Vec<TokenStream2>,
151 TokenStream2,
152 ) = if let Some(platforms) = &attr.platforms {
153 let mut os_checks = Vec::new();
154 let mut paths = Vec::new();
155 for platform in platforms {
156 let (os, variant_name) = match platform.as_str() {
157 "windows" => ("windows", "Windows"),
158 "macos" => ("macos", "Macos"),
159 "linux" => ("linux", "Linux"),
160 "android" => ("android", "Android"),
161 "ios" => ("ios", "Ios"),
162 other => {
163 return syn::Error::new_spanned(
164 &func.sig.ident,
165 format!(
166 "unknown platform '{other}'; supported: windows, macos, linux, android, ios"
167 ),
168 )
169 .to_compile_error()
170 .into();
171 }
172 };
173 os_checks.push(quote! { target_os = #os });
174 let variant = Ident::new(variant_name, proc_macro2::Span::call_site());
177 paths.push(quote! { rustra::platform::Platform::#variant });
178 }
179 (
180 quote! { any(#(#os_checks),*) },
181 quote! { not(any(#(#os_checks),*)) },
182 paths.clone(),
183 meta_opt_const(
184 &platforms_ident,
185 platforms_const_ty.clone(),
186 Some(quote! { &[#(#paths),*] }),
187 ),
188 )
189 } else {
190 (
191 quote! {},
192 quote! {},
193 Vec::new(),
194 meta_opt_const(&platforms_ident, platforms_const_ty, None),
195 )
196 };
197
198 let errors_ident = Ident::new(
202 &format!("__RUstra_errors_{}", fn_name),
203 proc_macro2::Span::call_site(),
204 );
205 let errors_const = meta_opt_const(
206 &errors_ident,
207 quote! { Option<&'static [rustra::CommandErrorVariant]> },
208 attr.errors.as_ref().map(|errors| {
209 let variants = errors
210 .iter()
211 .map(|code| quote! { rustra::CommandErrorVariant::new(#code) });
212 quote! { &[#(#variants),*] }
213 }),
214 );
215
216 let devices_ident = Ident::new(
220 &format!("__RUstra_devices_{}", fn_name),
221 proc_macro2::Span::call_site(),
222 );
223 let devices_const = meta_opt_const(
224 &devices_ident,
225 quote! { Option<&'static [rustra::device_capabilities::DeviceCapability]> },
226 attr.devices.as_ref().map(|devices| {
227 let capabilities = devices
228 .iter()
229 .map(|token| quote! { rustra::device_capabilities::DeviceCapability::new(#token) });
230 quote! { &[#(#capabilities),*] }
231 }),
232 );
233
234 let wrapper_unsafety: TokenStream2 = if attr.capability.is_some() {
241 quote! { unsafe }
242 } else {
243 quote! {}
244 };
245 let register_ident = Ident::new(
252 &format!("__rustra_register_{}", fn_name),
253 proc_macro2::Span::call_site(),
254 );
255 let register_call: TokenStream2 = if attr.capability.is_some() {
256 quote! { unsafe { #fn_name(__rustra_input) } }
257 } else {
258 quote! { #fn_name(__rustra_input) }
259 };
260
261 let mut state_bindings = Vec::new();
263 let mut call_args = Vec::new();
264
265 for param in ¶ms {
266 if param.is_state {
267 let pat = ¶m.pat;
268 let ty = ¶m.ty;
269 let inner_ty = param.state_inner.as_ref().unwrap();
270 state_bindings.push(quote! {
271 let #pat: #ty = rustra::get_state::<#inner_ty>()
272 .ok_or_else(|| rustra::RustraError::internal(concat!("State<", stringify!(#inner_ty), "> not managed in package")))?;
273 });
274 call_args.push(quote! { #pat });
275 } else {
276 call_args.push(quote! { __rustra_input });
277 }
278 }
279
280 let outer_input_arg = if data_params.is_empty() {
281 quote! { _: () }
282 } else {
283 quote! { __rustra_input: #input_type }
284 };
285
286 let inner_invocation = if is_async {
287 quote! {
288 rustra::__private::block_on(async move {
289 #inner_fn_name(#(#call_args),*).await
290 })
291 }
292 } else {
293 quote! {
294 #inner_fn_name(#(#call_args),*)
295 }
296 };
297
298 let mut stub_func = func.clone();
302 stub_func.sig.ident = inner_fn_name.clone();
303 stub_func.vis = syn::Visibility::Inherited;
304 stub_func.block = syn::parse_quote! {
305 {
306 Err(rustra::RustraError::platform_unavailable(
307 #command_name,
308 &[#(#platform_paths),*],
309 ))
310 }
311 };
312
313 let (real_inner, stub_inner): (TokenStream2, TokenStream2) = if attr.platforms.is_some() {
314 (
315 quote! { #[cfg(#supported_cfg)] #inner_func },
316 quote! { #[cfg(#unsupported_cfg)] #[allow(unused_variables)] #stub_func },
317 )
318 } else {
319 (quote! { #inner_func }, quote! {})
320 };
321
322 let expanded = quote! {
323 #real_inner
324 #stub_inner
325
326 #vis #wrapper_unsafety fn #fn_name(#outer_input_arg) -> rustra::Result<#output_type> {
327 #(#state_bindings)*
328 #inner_invocation
329 }
330
331 #[allow(non_upper_case_globals, dead_code)]
332 const #execution_ident: rustra::CommandExecution = #execution;
333
334 #capability_const
335
336 #platforms_const
337
338 #errors_const
339
340 #devices_const
341
342 #[doc(hidden)]
343 fn #register_ident(__rustra_input: #input_type) -> rustra::Result<#output_type> {
344 #register_call
345 }
346
347 #[allow(non_upper_case_globals, dead_code)]
348 const #meta_ident: &str = #command_name;
349
350 #[allow(non_upper_case_globals, dead_code)]
351 const #doc_ident: &str = #doc_comment;
352
353 #[allow(dead_code)]
354 const _: () = {
355 fn _assert_command_bounds<
356 __I: rustra::__private::CommandInput,
357 __O: rustra::__private::CommandOutput,
358 >() {
359 }
360 fn _check_command_bounds() {
361 _assert_command_bounds::<#input_type, #output_type>();
362 }
363 };
364 };
365
366 expanded.into()
367}