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 doc_ident = Ident::new(
114        &format!("__RUstra_doc_{}", fn_name),
115        proc_macro2::Span::call_site(),
116    );
117    // capability 속성이 있으면 메타 상수에 싣는다. register!/build! 가 이를 읽어
118    // require_capability 로 연결한다 — 문자열 이름 재결합(오타 시 런타임 패닉) 대신
119    // 매크로 시점에 같은 심벌에서 파생된다.
120    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    // 플랫폼 속성 — 지원 플랫폼 cfg 조건과 Platform 상수를 파생한다. 매크로가
131    // cfg 게이팅을 소유한다: 사용자 fn 에 #[cfg] 를 직접 붙이면 속성 매크로보다
132    // 먼저 strip 돼 이 전개가 일어나지 않는다(platform 절의 문서 참고).
133    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            // Platform variant 식별자 — quote 가 String 을 문자열 리터럴로
166            // 렌더링하지 않도록 Ident 로 변환한다.
167            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    // 에러 속성 — 커맨드별 도메인 에러 코드 선언. const 생성자 체인으로 상수를
190    // 구성하고 register!/build! 가 errors_meta_if 로 연결한다(capability/platforms
191    // 메타 상수 관례 — None 이면 선언 없는 명령도 체인을 그대로 통과).
192    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    // 디바이스 역량 속성 — 커맨드가 전제하는 역량 토큰 선언. 카탈로그 검증은
208    // 등록 시점(command_devices)에 loud-fail 한다 — 매크로 크레이트는 카탈로그를
209    // 모르므로 토큰 목록만 상수로 싣는다(platforms/errors 메타 상수 관례).
210    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    // (감사 #5) capability 무음 드랍 차단: capability 가 있으면 래퍼를 `unsafe fn`
226    // 으로 생성한다. `unsafe fn` 아이템 타입은 `Fn` 을 구현하지 않으므로
227    // `.command_fn(f)`/`.command(name, f)`/`buffer_command_fn`/`register_fn` 등
228    // 일반 등록 경로의 `F: Fn` 바운드에서 **컴파일 에러**가 된다 — 조용한 공개
229    // 명령화를 원천 차단. register!/build! 는 명시적 unsafe 클로저로 감싸 등록하며
230    // capability 연결(require_capability_if)은 그대로 유지된다.
231    let wrapper_unsafety: TokenStream2 = if attr.capability.is_some() {
232        quote! { unsafe }
233    } else {
234        quote! {}
235    };
236    // register!/build! 가 이름추론 등록에 쓰는 doc(hidden) 안전 어댑터 — fn 아이템이라
237    // `Fn(I) -> Result<O>` 바운드에서 I/O 추론이 그대로 성립한다(클로저 추론과 달리).
238    // (감사 #5 후속) 비공개로 고정 — `#vis` 를 남기면 크로스 크레이트에서
239    // `.command_fn(__rustra_register_{fn})` 우회가 가능해진다(I1). 같은 모듈의
240    // register!/build! 호출(`crate::` 경로)은 fn 저자와 같은 신뢰 도메인이라
241    // 허용 잔여다.
242    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    // Prepare state bindings and call args
253    let mut state_bindings = Vec::new();
254    let mut call_args = Vec::new();
255
256    for param in &params {
257        if param.is_state {
258            let pat = &param.pat;
259            let ty = &param.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    // 미지원 플랫폼용 스텁 inner — 같은 시그니처(파라미터는 unused 허용)로
290    // platform.unavailable 을 반환한다. cfg 배타성으로 지원 플랫폼의 진짜 inner
291    // 와 정확히 하나만 컴파일된다. I/O 타입은 전 플랫폼에 존재해야 한다.
292    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}