Skip to main content

rustra_macros/
macro_command.rs

1/// 함수를 rustra 명령으로 등록하는 속성 매크로입니다.
2///
3/// ## 지원 기능
4///
5/// - 동기(`fn`) 및 비동기(`async fn`) 함수 지원
6/// - 0개 인자(`fn ping() -> Result<()>`) 및 1개 데이터 인자(`fn add(input: Input) -> Result<Output>`)
7/// - `State<T>` 의존성 자동 주입 (`fn get(input: In, db: State<Db>) -> Result<Out>`)
8/// - Rust doc comment(`///`)를 추출하여 메타데이터에 보존
9///
10/// ## 명령 이름 규칙
11///
12/// - `#[command]`: 함수 이름에서 `_command` 접미사를 제거한 뒤 lowerCamelCase로 변환
13///   (예: `add_numbers` → `addNumbers`)
14/// - `#[command(name = "customName")]`: 지정한 이름을 그대로 사용
15#[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    // Analyze parameters
48    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    // (감사 #5 후속) inner 클론은 반드시 비공개 — `#vis` 를 남기면
100    // `.command_fn(__rustra_inner_{fn})` 이 안전 `fn` 으로 조용히 통과하는
101    // 우회 경로가 된다(C1). 래퍼/어댑터와 같은 모듈에서만 호출되므로 비공개로
102    // 충분하다.
103    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    // capability 속성이 있으면 메타 상수에 싣는다. register!/build! 가 이를 읽어
127    // require_capability 로 연결한다 — 문자열 이름 재결합(오타 시 런타임 패닉) 대신
128    // 매크로 시점에 같은 심벌에서 파생된다.
129    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    // 플랫폼 속성 — 지원 플랫폼 cfg 조건과 Platform 상수를 파생한다. 매크로가
140    // cfg 게이팅을 소유한다: 사용자 fn 에 #[cfg] 를 직접 붙이면 속성 매크로보다
141    // 먼저 strip 돼 이 전개가 일어나지 않는다(platform 절의 문서 참고).
142    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            // Platform variant 식별자 — quote 가 String 을 문자열 리터럴로
175            // 렌더링하지 않도록 Ident 로 변환한다.
176            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    // 에러 속성 — 커맨드별 도메인 에러 코드 선언. const 생성자 체인으로 상수를
199    // 구성하고 register!/build! 가 errors_meta_if 로 연결한다(capability/platforms
200    // 메타 상수 관례 — None 이면 선언 없는 명령도 체인을 그대로 통과).
201    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    // 디바이스 역량 속성 — 커맨드가 전제하는 역량 토큰 선언. 카탈로그 검증은
217    // 등록 시점(command_devices)에 loud-fail 한다 — 매크로 크레이트는 카탈로그를
218    // 모르므로 토큰 목록만 상수로 싣는다(platforms/errors 메타 상수 관례).
219    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    // (감사 #5) capability 무음 드랍 차단: capability 가 있으면 래퍼를 `unsafe fn`
235    // 으로 생성한다. `unsafe fn` 아이템 타입은 `Fn` 을 구현하지 않으므로
236    // `.command_fn(f)`/`.command(name, f)`/`buffer_command_fn`/`register_fn` 등
237    // 일반 등록 경로의 `F: Fn` 바운드에서 **컴파일 에러**가 된다 — 조용한 공개
238    // 명령화를 원천 차단. register!/build! 는 명시적 unsafe 클로저로 감싸 등록하며
239    // capability 연결(require_capability_if)은 그대로 유지된다.
240    let wrapper_unsafety: TokenStream2 = if attr.capability.is_some() {
241        quote! { unsafe }
242    } else {
243        quote! {}
244    };
245    // register!/build! 가 이름추론 등록에 쓰는 doc(hidden) 안전 어댑터 — fn 아이템이라
246    // `Fn(I) -> Result<O>` 바운드에서 I/O 추론이 그대로 성립한다(클로저 추론과 달리).
247    // (감사 #5 후속) 비공개로 고정 — `#vis` 를 남기면 크로스 크레이트에서
248    // `.command_fn(__rustra_register_{fn})` 우회가 가능해진다(I1). 같은 모듈의
249    // register!/build! 호출(`crate::` 경로)은 fn 저자와 같은 신뢰 도메인이라
250    // 허용 잔여다.
251    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    // Prepare state bindings and call args
262    let mut state_bindings = Vec::new();
263    let mut call_args = Vec::new();
264
265    for param in &params {
266        if param.is_state {
267            let pat = &param.pat;
268            let ty = &param.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    // 미지원 플랫폼용 스텁 inner — 같은 시그니처(파라미터는 unused 허용)로
299    // platform.unavailable 을 반환한다. cfg 배타성으로 지원 플랫폼의 진짜 inner
300    // 와 정확히 하나만 컴파일된다. I/O 타입은 전 플랫폼에 존재해야 한다.
301    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}