Skip to main content

service_kit_macros/
lib.rs

1extern crate proc_macro;
2
3use proc_macro::TokenStream;
4use quote::{quote, format_ident};
5use syn::{
6    parse::Parse, parse::ParseStream, parse_macro_input, Attribute, FnArg, Ident, ItemFn, LitStr,
7    Pat, Result, ReturnType, Token, Type
8};
9
10
11// --- Macro Implementations ---
12
13struct ApiMacroArgs {
14    method: Ident,
15    path: LitStr,
16    responses: Vec<(syn::LitInt, LitStr, Option<Type>)>,
17    output: Option<(Ident, LitStr)>, // Some( (Detail|List), type_name_literal )
18}
19
20impl Parse for ApiMacroArgs {
21    fn parse(input: ParseStream) -> Result<Self> {
22        let method: Ident = input.parse()?;
23        input.parse::<Token![,]>()?;
24        let path: LitStr = input.parse()?;
25
26        let mut responses: Vec<(syn::LitInt, LitStr, Option<Type>)> = Vec::new();
27        let mut output: Option<(Ident, LitStr)> = None;
28
29        // Parse optional trailing args separated by commas: output(detail|list = "Type"), responses(...)
30        while input.parse::<Option<Token![,]>>()?.is_some() {
31            if input.peek(Ident) {
32                let kw: Ident = input.parse()?;
33                let kw_str = kw.to_string();
34                if kw_str == "responses" {
35                    let content;
36                    syn::parenthesized!(content in input);
37                    while !content.is_empty() {
38                        let group;
39                        syn::parenthesized!(group in content);
40                        let code: syn::LitInt = group.parse()?;
41                        group.parse::<Token![,]>()?;
42                        let desc: LitStr = group.parse()?;
43                        let ty: Option<Type> = if group.parse::<Option<Token![,]>>()?.is_some() {
44                            Some(group.parse()?)
45                        } else { None };
46                        responses.push((code, desc, ty));
47                        let _ = content.parse::<Option<Token![,]>>()?;
48                    }
49                } else if kw_str == "output" {
50                    let content;
51                    syn::parenthesized!(content in input);
52                    let kind: Ident = content.parse()?; // detail | list
53                    content.parse::<Token![=]>()?;
54                    let ty_lit: LitStr = content.parse()?; // "TypeName"
55                    output = Some((kind, ty_lit));
56                } else {
57                    // Unknown: stop parsing further
58                    break;
59                }
60            } else {
61                break;
62            }
63        }
64
65        Ok(ApiMacroArgs { method, path, responses, output })
66    }
67}
68
69#[proc_macro_attribute]
70pub fn api(args: TokenStream, input: TokenStream) -> TokenStream {
71    let item_fn = parse_macro_input!(input as ItemFn);
72    let args_parsed = parse_macro_input!(args as ApiMacroArgs);
73
74    let fn_ident = &item_fn.sig.ident;
75    let fn_name_str = fn_ident.to_string();
76    let method_str = args_parsed.method.to_string();
77    let path_str = args_parsed.path.value();
78    let (summary, description) = parse_doc_comments(&item_fn.attrs);
79
80    // --- Parse Parameters and Request Body ---
81    let mut params_tokens = Vec::new();
82    let mut request_body_token = quote! { None };
83    let mut _needs_header_map: bool = false;
84    let mut _needs_state: bool = false;
85    let mut dto_inventory_tokens: Vec<proc_macro2::TokenStream> = Vec::new();
86
87    // For building runtime wrapper
88    let mut arg_prepare_tokens = Vec::new();
89    let mut call_args_tokens = Vec::new();
90    let mut _state_type_tokens: Option<proc_macro2::TokenStream> = None;
91
92    for arg in &item_fn.sig.inputs {
93        if let FnArg::Typed(pat_type) = arg {
94            if let Some(inner_type) = get_inner_type(&pat_type.ty, "Path") {
95                if let Pat::TupleStruct(pat_tuple) = &*pat_type.pat {
96                     if let Some(Pat::Ident(inner_pat)) = pat_tuple.elems.first() {
97                        let param_name = inner_pat.ident.to_string();
98                        let type_name = type_to_string(inner_type);
99                        let type_name_s = sanitize_component_name(&type_name);
100                        params_tokens.push(quote! {
101                            ::service_kit::ApiParameter {
102                                name: #param_name,
103                                param_in: ::service_kit::ParamIn::Path,
104                                description: "", // TODO: Parse from attributes
105                                required: true,
106                                type_name: #type_name_s,
107                            }
108                        });
109                        // runtime wrapper: read string and wrap
110                        let var_ident = &inner_pat.ident;
111                        let inner_ty_tokens = quote! { #inner_type };
112                        arg_prepare_tokens.push(quote! {
113                            let #var_ident: #inner_ty_tokens = match params.get(#param_name).and_then(|v| v.as_str()) {
114                                Some(s) => match s.parse::<#inner_ty_tokens>() {
115                                    Ok(v) => v,
116                                    Err(_) => return Err(::service_kit::error::Error::SpecError(format!("invalid path param `{}`", #param_name))),
117                                },
118                                None => return Err(::service_kit::error::Error::SpecError(format!("missing path param `{}`", #param_name))),
119                            };
120                            let #var_ident = axum::extract::Path::<#inner_ty_tokens>(#var_ident);
121                        });
122                        call_args_tokens.push(quote! { #var_ident });
123
124                        // auto-register DTO for Path<T>
125                        let type_name = type_to_string(inner_type);
126                        let type_name_s = sanitize_component_name(&type_name);
127                        dto_inventory_tokens.push(quote! {
128                            ::service_kit::inventory::submit! {
129                                ::service_kit::ApiDtoMetadata {
130                                    name: #type_name_s,
131                                    schema_provider: || (
132                                        #type_name_s.to_string(),
133                                        <#inner_type as ::service_kit::utoipa::PartialSchema>::schema(),
134                                    ),
135                                }
136                            }
137                        });
138                    }
139                } else if let Pat::Ident(pat_ident) = &*pat_type.pat {
140                    // Also support `id: Path<String>` style
141                    let param_name = pat_ident.ident.to_string();
142                    let type_name = type_to_string(inner_type);
143                    let type_name_s = sanitize_component_name(&type_name);
144                    params_tokens.push(quote! {
145                        ::service_kit::ApiParameter {
146                            name: #param_name,
147                            param_in: ::service_kit::ParamIn::Path,
148                            description: "",
149                            required: true,
150                            type_name: #type_name_s,
151                        }
152                    });
153                    let var_ident = &pat_ident.ident;
154                    let inner_ty_tokens = quote! { #inner_type };
155                    arg_prepare_tokens.push(quote! {
156                        let #var_ident: #inner_ty_tokens = match params.get(#param_name).and_then(|v| v.as_str()) {
157                            Some(s) => match s.parse::<#inner_ty_tokens>() {
158                                Ok(v) => v,
159                                Err(_) => return Err(::service_kit::error::Error::SpecError(format!("invalid path param `{}`", #param_name))),
160                            },
161                            None => return Err(::service_kit::error::Error::SpecError(format!("missing path param `{}`", #param_name))),
162                        };
163                        let #var_ident = axum::extract::Path::<#inner_ty_tokens>(#var_ident);
164                    });
165                    call_args_tokens.push(quote! { #var_ident });
166
167                    // auto-register DTO for Path<T>
168                    let type_name = type_to_string(inner_type);
169                    let type_name_s = sanitize_component_name(&type_name);
170                    dto_inventory_tokens.push(quote! {
171                        ::service_kit::inventory::submit! {
172                            ::service_kit::ApiDtoMetadata {
173                                name: #type_name_s,
174                                schema_provider: || (
175                                    #type_name_s.to_string(),
176                                    <#inner_type as ::service_kit::utoipa::PartialSchema>::schema(),
177                                ),
178                            }
179                        }
180                    });
181                }
182            } else if let Some(inner_type) = get_inner_type(&pat_type.ty, "Query") {
183                // Support both `Query(params): Query<T>` and `params: Query<T>` patterns
184                let param_name_opt = if let Pat::TupleStruct(pat_tuple) = &*pat_type.pat {
185                    pat_tuple
186                        .elems
187                        .first()
188                        .and_then(|p| match p { Pat::Ident(pi) => Some(pi.ident.to_string()), _ => None })
189                } else if let Pat::Ident(pat_ident) = &*pat_type.pat {
190                    Some(pat_ident.ident.to_string())
191                } else { None };
192
193                if let Some(param_name) = param_name_opt {
194                    let type_name = type_to_string(inner_type);
195                    let type_name_s = sanitize_component_name(&type_name);
196                    // 跳过在 OpenAPI 中暴露 AuthQuery(避免把 authorization 暴露为查询参数)
197                    if type_name != "AuthQuery" && !type_name.ends_with("::AuthQuery") {
198                        params_tokens.push(quote! {
199                            ::service_kit::ApiParameter {
200                                name: #param_name,
201                                param_in: ::service_kit::ParamIn::Query,
202                                description: "", // TODO: Parse from attributes
203                                required: true, // TODO: Detect Option
204                                type_name: #type_name_s,
205                            }
206                        });
207                    }
208                    // runtime wrapper: deserialize whole params into T
209                    let var_ident = format_ident!("{}", param_name);
210                     let inner_ty_tokens = quote! { #inner_type };
211                    arg_prepare_tokens.push(quote! {
212                        let #var_ident: #inner_ty_tokens = match __raw_query_str.as_deref() {
213                            Some(s) if !s.is_empty() => match ::service_kit::serde_urlencoded::from_str::<#inner_ty_tokens>(s) {
214                                Ok(v) => v,
215                                Err(e) => return Err(::service_kit::error::Error::SerdeUrlencoded(e)),
216                            },
217                            _ => match ::service_kit::serde_urlencoded::from_str::<#inner_ty_tokens>("") {
218                                Ok(v) => v,
219                                Err(e) => return Err(::service_kit::error::Error::SerdeUrlencoded(e)),
220                            }
221                        };
222                        let #var_ident = axum::extract::Query::<#inner_ty_tokens>(#var_ident);
223                    });
224                    call_args_tokens.push(quote! { #var_ident });
225
226                    // auto-register DTO for Query<T>, except AuthQuery to avoid exposing authorization in OpenAPI
227                    let type_name = type_to_string(inner_type);
228                    let type_name_s = sanitize_component_name(&type_name);
229                    if type_name != "AuthQuery" && !type_name.ends_with("::AuthQuery") {
230                        dto_inventory_tokens.push(quote! {
231                            ::service_kit::inventory::submit! {
232                                ::service_kit::ApiDtoMetadata {
233                                    name: #type_name_s,
234                                    schema_provider: || (
235                                        #type_name_s.to_string(),
236                                        <#inner_type as ::service_kit::utoipa::PartialSchema>::schema(),
237                                    ),
238                                }
239                            }
240                        });
241                    }
242                }
243            } else if type_to_string(&pat_type.ty) == "axum :: http :: HeaderMap" || type_to_string(&pat_type.ty).ends_with("::HeaderMap") || type_to_string(&pat_type.ty) == "HeaderMap" {
244                // HeaderMap 注入支持(不进入 OpenAPI 参数)
245                _needs_header_map = true;
246                if let Pat::Ident(pat_ident) = &*pat_type.pat {
247                    let var_ident = &pat_ident.ident;
248                    arg_prepare_tokens.push(quote! {
249                        let #var_ident: axum::http::HeaderMap = headers.clone();
250                    });
251                    call_args_tokens.push(quote! { #var_ident });
252                }
253            } else if let Some(inner_type) = get_inner_type(&pat_type.ty, "State") {
254                // State<T> 注入(不进入 OpenAPI 参数)。支持 `State(s): State<T>` 与 `s: State<T>` 两种形态。
255                _needs_state = true;
256                _state_type_tokens = Some(quote! { #inner_type });
257                let state_var_ident = format_ident!("__state_arg_{}", call_args_tokens.len());
258                arg_prepare_tokens.push(quote! {
259                    let #state_var_ident: axum::extract::State<#inner_type> = {
260                        let any_arc = state_any.ok_or_else(|| ::service_kit::error::Error::SpecError("missing router state".to_string()))?;
261                        let typed = (&*any_arc).downcast_ref::<#inner_type>().ok_or_else(|| ::service_kit::error::Error::SpecError("invalid router state type".to_string()))?;
262                        axum::extract::State::<#inner_type>(typed.clone())
263                    };
264                });
265                call_args_tokens.push(quote! { #state_var_ident });
266            } else if let Some(inner_type) = get_inner_type(&pat_type.ty, "Json") {
267                // 禁止在请求体中直接使用 Json<Vec<T>>,要求使用具名 DTO 包裹
268                if get_inner_type(inner_type, "Vec").is_some() {
269                    panic!("Json<Vec<T>> in request body is not allowed. Wrap it in a DTO with a Vec<T> field.");
270                }
271                let type_name = type_to_string(inner_type);
272                let type_name_s = sanitize_component_name(&type_name);
273                request_body_token = quote! {
274                    Some(&::service_kit::ApiRequestBody {
275                        description: "", // TODO: Parse from attributes
276                        required: true,
277                        type_name: #type_name_s,
278                    })
279                };
280                // runtime wrapper: deserialize whole params into body T
281                 let inner_ty_tokens = quote! { #inner_type };
282                 let json_ident = syn::Ident::new("__json_body", proc_macro2::Span::call_site());
283                arg_prepare_tokens.push(quote! {
284                    // Prefer decoding from raw body when content-type is application/json
285                    let __body_json_source: serde_json::Value = match __content_type_opt.as_deref() {
286                        Some(ct) if ct.contains("application/json") => {
287                            if __body_bytes.is_empty() {
288                                serde_json::Value::Null
289                            } else {
290                                match serde_json::from_slice::<serde_json::Value>(&__body_bytes) {
291                                    Ok(v) => v,
292                                    Err(e) => return Err(::service_kit::error::Error::SerdeJson(e)),
293                                }
294                            }
295                        },
296                        _ => serde_json::Value::Null,
297                    };
298                    let #json_ident: #inner_ty_tokens = if !__body_json_source.is_null() {
299                        match serde_json::from_value(__body_json_source) {
300                            Ok(v) => v,
301                            Err(e) => return Err(::service_kit::error::Error::SerdeJson(e)),
302                        }
303                    } else {
304                        // Fallback (no body): try to construct from merged params (query/path only)
305                        match serde_json::from_value(params.clone()) {
306                            Ok(v) => v,
307                            Err(e) => return Err(::service_kit::error::Error::SerdeJson(e)),
308                        }
309                    };
310                    let #json_ident = axum::Json::<#inner_ty_tokens>(#json_ident);
311                });
312                call_args_tokens.push(quote! { #json_ident });
313
314                // auto-register DTO for Json<T>
315                let type_name = type_to_string(inner_type);
316                let type_name_s = sanitize_component_name(&type_name);
317                dto_inventory_tokens.push(quote! {
318                    ::service_kit::inventory::submit! {
319                        ::service_kit::ApiDtoMetadata {
320                            name: #type_name_s,
321                            schema_provider: || (
322                                #type_name_s.to_string(),
323                                <#inner_type as ::service_kit::utoipa::PartialSchema>::schema(),
324                            ),
325                        }
326                    }
327                });
328
329                // auto-register nested generic instances appearing within Json<T>
330                let nested_types = collect_concrete_generic_types(inner_type);
331                for (nested_name, nested_ty) in nested_types {
332                    let nested_name_lit = syn::LitStr::new(&nested_name, proc_macro2::Span::call_site());
333                    let nested_ty_tokens = quote! { #nested_ty };
334                    dto_inventory_tokens.push(quote! {
335                        ::service_kit::inventory::submit! {
336                            ::service_kit::ApiDtoMetadata {
337                                name: #nested_name_lit,
338                                schema_provider: || (
339                                    #nested_name_lit.to_string(),
340                                    <#nested_ty_tokens as ::service_kit::utoipa::PartialSchema>::schema(),
341                                ),
342                            }
343                        }
344                    });
345                }
346
347                // 数组不再注册为独立组件;仅依赖元素类型 T 的 schema 注册(已在递归收集中覆盖)
348            }
349            // Support Form<T> similar to Json<T> for OpenAPI and runtime adapter
350            else if let Some(inner_type) = get_inner_type(&pat_type.ty, "Form") {
351                if get_inner_type(inner_type, "Vec").is_some() {
352                    panic!("Form<Vec<T>> in request body is not allowed. Wrap it in a DTO with a Vec<T> field.");
353                }
354                let type_name = type_to_string(inner_type);
355                let type_name_s = sanitize_component_name(&type_name);
356                request_body_token = quote! {
357                    Some(&::service_kit::ApiRequestBody {
358                        description: "", // TODO
359                        required: true,
360                        type_name: #type_name_s,
361                    })
362                };
363                let inner_ty_tokens = quote! { #inner_type };
364                let form_ident = syn::Ident::new("__form_body", proc_macro2::Span::call_site());
365                arg_prepare_tokens.push(quote! {
366                    // Decode application/x-www-form-urlencoded into a map then into T; allow empty body to fallback
367                    let #form_ident: #inner_ty_tokens = match __content_type_opt.as_deref() {
368                        Some(ct) if ct.contains("application/x-www-form-urlencoded") => {
369                            let raw = String::from_utf8(__body_bytes.clone()).unwrap_or_default();
370                            match ::service_kit::serde_urlencoded::from_str::<#inner_ty_tokens>(&raw) {
371                                Ok(v) => v,
372                                Err(_e) => {
373                                    // As a fallback, attempt JSON decode for tolerant clients
374                                    match serde_json::from_slice::<#inner_ty_tokens>(&__body_bytes) {
375                                        Ok(v) => v,
376                                        Err(e2) => return Err(::service_kit::error::Error::SerdeJson(e2)),
377                                    }
378                                }
379                            }
380                        },
381                        _ => {
382                            // No form content-type; try to build from merged params (query/path)
383                            match serde_json::from_value(params.clone()) {
384                                Ok(v) => v,
385                                Err(e) => return Err(::service_kit::error::Error::SerdeJson(e)),
386                            }
387                        }
388                    };
389                    let #form_ident = axum::Form::<#inner_ty_tokens>(#form_ident);
390                });
391                call_args_tokens.push(quote! { #form_ident });
392                // auto-register DTO for Form<T>
393                let type_name = type_to_string(inner_type);
394                let type_name_s = sanitize_component_name(&type_name);
395                dto_inventory_tokens.push(quote! {
396                    ::service_kit::inventory::submit! {
397                        ::service_kit::ApiDtoMetadata {
398                            name: #type_name_s,
399                            schema_provider: || (
400                                #type_name_s.to_string(),
401                                <#inner_type as ::service_kit::utoipa::PartialSchema>::schema(),
402                            ),
403                        }
404                    }
405                });
406                let nested_types = collect_concrete_generic_types(inner_type);
407                for (nested_name, nested_ty) in nested_types {
408                    let nested_name_lit = syn::LitStr::new(&nested_name, proc_macro2::Span::call_site());
409                    let nested_ty_tokens = quote! { #nested_ty };
410                    dto_inventory_tokens.push(quote! {
411                        ::service_kit::inventory::submit! {
412                            ::service_kit::ApiDtoMetadata {
413                                name: #nested_name_lit,
414                                schema_provider: || (
415                                    #nested_name_lit.to_string(),
416                                    <#nested_ty_tokens as ::service_kit::utoipa::PartialSchema>::schema(),
417                                ),
418                            }
419                        }
420                    });
421                }
422            }
423        }
424    }
425
426    // --- Parse Responses ---
427    let mut responses_tokens = Vec::new();
428    let mut response_dto_inventory_tokens: Vec<proc_macro2::TokenStream> = Vec::new();
429    if let ReturnType::Type(_, ty) = &item_fn.sig.output {
430        if let Some(inner_type) = get_inner_type(ty, "Json") {
431            // 针对返回 Json<Vec<U>>:要求使用 output(list = "U"),并且不注册 Vec 组件
432            if let Some(vec_inner) = get_inner_type(inner_type, "Vec") {
433                if let Some((ref kind_ident, _)) = args_parsed.output {
434                    if kind_ident.to_string().eq_ignore_ascii_case("list") {
435                        // paths 由 output(list) 决定,这里不设置组件类型名
436                        responses_tokens.push(quote! { ::service_kit::ApiResponse { status_code: 200, description: #summary, type_name: None } });
437                        // 注册 U 及其允许的泛型具体化
438                        let u_name = sanitize_component_name(&type_to_string(vec_inner));
439                        let u_name_lit = syn::LitStr::new(&u_name, proc_macro2::Span::call_site());
440                        let u_tokens = quote! { #vec_inner };
441                        response_dto_inventory_tokens.push(quote! {
442                            ::service_kit::inventory::submit! {
443                                ::service_kit::ApiDtoMetadata {
444                                    name: #u_name_lit,
445                                    schema_provider: || (
446                                        #u_name_lit.to_string(),
447                                        <#u_tokens as ::service_kit::utoipa::PartialSchema>::schema(),
448                                    ),
449                                }
450                            }
451                        });
452                        let nested_types = collect_concrete_generic_types(vec_inner);
453                        for (nested_name, nested_ty) in nested_types {
454                            let nested_name_lit = syn::LitStr::new(&nested_name, proc_macro2::Span::call_site());
455                            let nested_ty_tokens = quote! { #nested_ty };
456                            response_dto_inventory_tokens.push(quote! {
457                                ::service_kit::inventory::submit! {
458                                    ::service_kit::ApiDtoMetadata {
459                                        name: #nested_name_lit,
460                                        schema_provider: || (
461                                            #nested_name_lit.to_string(),
462                                            <#nested_ty_tokens as ::service_kit::utoipa::PartialSchema>::schema(),
463                                        ),
464                                    }
465                                }
466                            });
467                        }
468                    } else {
469                        panic!("Json<Vec<T>> return type must use output(list = \"T\").");
470                    }
471                } else {
472                    panic!("Json<Vec<T>> return type must use output(list = \"T\").");
473                }
474            } else {
475                // 普通 Json<T> 响应:注册 T 组件
476                let type_name = type_to_string(inner_type);
477                let type_name_s = sanitize_component_name(&type_name);
478                let type_name_lit = syn::LitStr::new(&type_name_s, proc_macro2::Span::call_site());
479                responses_tokens.push(quote! {
480                    ::service_kit::ApiResponse {
481                        status_code: 200,
482                        description: #summary,
483                        type_name: Some(#type_name_lit),
484                    }
485                });
486                let type_ident_tokens = quote! { #inner_type };
487                response_dto_inventory_tokens.push(quote! {
488                    ::service_kit::inventory::submit! {
489                        ::service_kit::ApiDtoMetadata {
490                            name: #type_name_lit,
491                            schema_provider: || (
492                                #type_name_lit.to_string(),
493                                <#type_ident_tokens as ::service_kit::utoipa::PartialSchema>::schema(),
494                            ),
495                        }
496                    }
497                });
498                let nested_types = collect_concrete_generic_types(inner_type);
499                for (nested_name, nested_ty) in nested_types {
500                    let nested_name_lit = syn::LitStr::new(&nested_name, proc_macro2::Span::call_site());
501                    let nested_ty_tokens = quote! { #nested_ty };
502                    response_dto_inventory_tokens.push(quote! {
503                        ::service_kit::inventory::submit! {
504                            ::service_kit::ApiDtoMetadata {
505                                name: #nested_name_lit,
506                                schema_provider: || (
507                                    #nested_name_lit.to_string(),
508                                    <#nested_ty_tokens as ::service_kit::utoipa::PartialSchema>::schema(),
509                                ),
510                            }
511                        }
512                    });
513                }
514            }
515        }
516    }
517    // Add a default response if none was parsed
518    if responses_tokens.is_empty() {
519        responses_tokens.push(quote! {
520            ::service_kit::ApiResponse { status_code: 200, description: "Success", type_name: None }
521        });
522    }
523
524    // Append extra responses from macro args
525    for (code_lit, desc_lit, ty_opt) in &args_parsed.responses {
526        if let Some(ty) = ty_opt {
527            let ty_str = type_to_string(ty);
528            let ty_str_s = sanitize_component_name(&ty_str);
529            let ty_str_lit = syn::LitStr::new(&ty_str_s, proc_macro2::Span::call_site());
530            responses_tokens.push(quote! {
531                ::service_kit::ApiResponse {
532                    status_code: #code_lit,
533                    description: #desc_lit,
534                    type_name: Some(#ty_str_lit),
535                }
536            });
537            let ty_tokens = quote! { #ty };
538            response_dto_inventory_tokens.push(quote! {
539                ::service_kit::inventory::submit! {
540                    ::service_kit::ApiDtoMetadata {
541                        name: #ty_str_lit,
542                        schema_provider: || (
543                            #ty_str_lit.to_string(),
544                            <#ty_tokens as ::service_kit::utoipa::PartialSchema>::schema(),
545                        ),
546                    }
547                }
548            });
549
550            // Recursively register nested concrete generic instances
551            let nested_types = collect_concrete_generic_types(ty);
552            for (nested_name, nested_ty) in nested_types {
553                let nested_name_lit = syn::LitStr::new(&nested_name, proc_macro2::Span::call_site());
554                let nested_ty_tokens = quote! { #nested_ty };
555                response_dto_inventory_tokens.push(quote! {
556                    ::service_kit::inventory::submit! {
557                        ::service_kit::ApiDtoMetadata {
558                            name: #nested_name_lit,
559                            schema_provider: || (
560                                #nested_name_lit.to_string(),
561                                <#nested_ty_tokens as ::service_kit::utoipa::PartialSchema>::schema(),
562                            ),
563                        }
564                    }
565                });
566            }
567        } else {
568            responses_tokens.push(quote! {
569                ::service_kit::ApiResponse {
570                    status_code: #code_lit,
571                    description: #desc_lit,
572                    type_name: None,
573                }
574            });
575        }
576    }
577
578    // --- Generate Static Metadata ---
579    let params_ident = format_ident!("__API_PARAMS_{}", fn_name_str.to_uppercase());
580    let responses_ident = format_ident!("__API_RESPONSES_{}", fn_name_str.to_uppercase());
581    let request_body_ident = format_ident!("__API_REQ_BODY_{}", fn_name_str.to_uppercase());
582
583    let exec_fn_ident = format_ident!("__API_EXEC_{}", fn_name_str.to_uppercase());
584
585    // Build output shape if provided
586    let output_shape_token = if let Some((ref kind_ident, ref ty_lit)) = args_parsed.output {
587        let kind = kind_ident.to_string();
588        if kind.eq_ignore_ascii_case("detail") {
589            quote! { Some(::service_kit::ApiOutputShape::Detail { type_name: #ty_lit }) }
590        } else if kind.eq_ignore_ascii_case("list") {
591            quote! { Some(::service_kit::ApiOutputShape::List { type_name: #ty_lit }) }
592        } else {
593            quote! { None }
594        }
595    } else { quote! { None } };
596
597    let static_metadata = quote! {
598        #[allow(non_upper_case_globals)]
599        const #params_ident: &[::service_kit::ApiParameter] = &[#(#params_tokens),*];
600        #[allow(non_upper_case_globals)]
601        const #responses_ident: &[::service_kit::ApiResponse] = &[#(#responses_tokens),*];
602        #[allow(non_upper_case_globals)]
603        const #request_body_ident: Option<&'static ::service_kit::ApiRequestBody> = #request_body_token;
604
605        ::service_kit::inventory::submit! {
606            ::service_kit::ApiMetadata {
607                operation_id: #fn_name_str,
608                method: #method_str,
609                path: #path_str,
610                summary: #summary,
611                description: #description,
612                parameters: #params_ident,
613                request_body: #request_body_ident,
614                responses: #responses_ident,
615                output: #output_shape_token,
616            }
617        }
618
619        // Static handler function for REST/MCP routers
620        fn #exec_fn_ident(__params_ref: &serde_json::Value, headers: axum::http::HeaderMap, __raw_query_str: Option<String>, state_any: Option<std::sync::Arc<dyn std::any::Any + Send + Sync>>, __body_bytes: Vec<u8>, __content_type_opt: Option<String>) -> ::service_kit::handler::DynHandlerFuture {
621            let __params_json = __params_ref.clone();
622            Box::pin(async move {
623                let params = __params_json.clone();
624                #(#arg_prepare_tokens)*
625                let __resp = #fn_ident(#(#call_args_tokens),*).await;
626                let __resp = ::axum::response::IntoResponse::into_response(__resp);
627                Ok(__resp)
628            })
629        }
630
631        // Register executable handler
632        ::service_kit::inventory::submit! {
633            ::service_kit::handler::ApiHandlerInventory {
634                operation_id: #fn_name_str,
635                handler: #exec_fn_ident,
636            }
637        }
638    };
639
640    // --- Final Output ---
641    let output = quote! {
642        #static_metadata
643        #(#dto_inventory_tokens)*
644        #(#response_dto_inventory_tokens)*
645        #item_fn
646    };
647
648    output.into()
649}
650
651fn type_to_string(ty: &Type) -> String {
652    quote!(#ty).to_string().replace(' ', "")
653}
654
655// --- Component name sanitization for generic types ---
656// Convert type strings like `ApiResponse<PaginatedResponse<ApiDefinition<FlowDefinitionData>>>`
657// into a stable, generator-friendly name: `ApiResponseOfPaginatedResponseOfApiDefinitionOfFlowDefinitionData`.
658fn sanitize_component_name(type_str: &str) -> String {
659    // Fast path: no generics
660    if !type_str.contains('<') {
661        return strip_modules(type_str).to_string();
662    }
663
664    // Recursive descent parser over the string
665    fn parse_type(input: &str, i: &mut usize) -> String {
666        // parse identifier (may include module paths ::)
667        let ident = parse_ident(input, i);
668        let mut name = strip_modules(&ident).to_string();
669        skip_ws(input, i);
670        if *i < input.len() && input.as_bytes()[*i] as char == '<' {
671            *i += 1; // consume '<'
672            let mut args: Vec<String> = Vec::new();
673            loop {
674                skip_ws(input, i);
675                let arg = parse_type(input, i);
676                args.push(arg);
677                skip_ws(input, i);
678                if *i >= input.len() { break; }
679                let ch = input.as_bytes()[*i] as char;
680                if ch == ',' { *i += 1; continue; }
681                if ch == '>' { *i += 1; break; }
682                // unexpected character; advance to break potential loop
683                *i += 1;
684            }
685            // Join generic args with underscores to match utoipa naming (Base_Arg1_Arg2)
686            for (idx, arg) in args.into_iter().enumerate() {
687                if idx == 0 { name.push('_'); } else { name.push('_'); }
688                name.push_str(&arg);
689            }
690        }
691        name
692    }
693
694    fn parse_ident(input: &str, i: &mut usize) -> String {
695        let start = *i;
696        while *i < input.len() {
697            let ch = input.as_bytes()[*i] as char;
698            match ch {
699                '<' | '>' | ',' => break,
700                _ => { *i += 1; }
701            }
702        }
703        input[start..*i].to_string()
704    }
705
706    fn strip_modules(ident: &str) -> String {
707        // remove module paths (keep the last segment after ::)
708        let simple = ident.rsplit("::").next().unwrap_or(ident);
709        // remove non-alphanumeric/underscore characters just in case
710        simple.chars().filter(|c| c.is_ascii_alphanumeric() || *c == '_').collect()
711    }
712
713    fn skip_ws(_input: &str, _i: &mut usize) {}
714
715    let mut idx = 0;
716    let result = parse_type(type_str, &mut idx);
717    // Fail-fast on clearly illegal names to avoid producing unusable component keys
718    // Allowed: [A-Za-z0-9_], plus embedded "Of" sequences produced by our sanitizer
719    if result.is_empty() || result.chars().any(|c| !(c.is_ascii_alphanumeric() || c == '_')) {
720        panic!("Illegal sanitized component name generated: {} from {}", result, type_str);
721    }
722    result
723}
724
725// removed unused short_hash helper
726
727fn get_inner_type<'a>(ty: &'a Type, type_name: &str) -> Option<&'a Type> {
728    if let Type::Path(type_path) = ty {
729        if let Some(segment) = type_path.path.segments.last() {
730            if segment.ident == type_name {
731                if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
732                    if let Some(syn::GenericArgument::Type(inner)) = args.args.first() {
733                        return Some(inner);
734                    }
735                }
736            }
737        }
738    }
739    None
740}
741
742// Collect all nested concrete generic type instantiations within a type. For example, for
743// ApiResponse<PaginatedResponse<ApiDefinition<FlowDefinitionData>>> it returns pairs of
744// (sanitized_name, Type) for:
745// - ApiResponse<PaginatedResponse<ApiDefinition<FlowDefinitionData>>> (optional, caller already handles top)
746// - PaginatedResponse<ApiDefinition<FlowDefinitionData>>
747// - ApiDefinition<FlowDefinitionData>
748fn collect_concrete_generic_types(ty: &Type) -> Vec<(String, Type)> {
749    let mut acc: Vec<(String, Type)> = Vec::new();
750    fn is_allowed_generic_base(ident: &syn::Ident) -> bool {
751        let name = ident.to_string();
752        matches!(name.as_str(), "ApiDefinition" | "PaginatedResponse" | "ApiResponse")
753    }
754    fn visit(t: &Type, out: &mut Vec<(String, Type)>) {
755        match t {
756            Type::Path(tp) => {
757                if let Some(seg) = tp.path.segments.last() {
758                    if let syn::PathArguments::AngleBracketed(args) = &seg.arguments {
759                        // Only record allowed generic bases (avoid Option<Value>, Vec<...>, DateTime, etc.)
760                        if is_allowed_generic_base(&seg.ident) {
761                            let name = sanitize_component_name(&type_to_string(t));
762                            out.push((name, t.clone()));
763                        }
764                        // Recurse into generic argument types
765                        for arg in &args.args {
766                            if let syn::GenericArgument::Type(inner_ty) = arg {
767                                visit(inner_ty, out);
768                            }
769                        }
770                    }
771                }
772            }
773            _ => {}
774        }
775    }
776    visit(ty, &mut acc);
777    // Deduplicate by name to avoid duplicate inventory submissions
778    let mut seen = std::collections::HashSet::new();
779    acc.into_iter()
780        .filter(|(name, _)| seen.insert(name.clone()))
781        .collect()
782}
783
784// Helper: for Vec<U>, recursively collect allowed generic instances inside U
785// 移除针对 Vec 的特殊辅助,数组一律内联
786
787fn parse_doc_comments(attrs: &[Attribute]) -> (String, String) {
788    let doc_comments: Vec<String> = attrs
789        .iter()
790        .filter_map(|attr| {
791            if attr.path().is_ident("doc") {
792                if let syn::Meta::NameValue(nv) = &attr.meta {
793                    if let syn::Expr::Lit(expr_lit) = &nv.value {
794                        if let syn::Lit::Str(lit) = &expr_lit.lit {
795                            return Some(lit.value().trim().to_string());
796                        }
797                    }
798                }
799            }
800            None
801        })
802        .collect();
803
804    let description = doc_comments.join("\n");
805    let summary = description.lines().next().unwrap_or("").to_string();
806    (summary, description)
807}
808
809// --- `api_dto` and its helpers ---
810
811#[derive(Debug, Default)]
812struct ApiDtoArgs {
813    // No arguments needed - we use Rust field names directly
814}
815
816impl syn::parse::Parse for ApiDtoArgs {
817    fn parse(_input: ParseStream) -> Result<Self> {
818        Ok(ApiDtoArgs::default())
819    }
820}
821
822#[proc_macro_attribute]
823pub fn api_dto(attr: TokenStream, item: TokenStream) -> TokenStream {
824    let _args = parse_macro_input!(attr as ApiDtoArgs);
825    let mut input = parse_macro_input!(item as syn::DeriveInput);
826    
827    let type_name = input.ident.clone();
828    let type_name_str = type_name.to_string();
829
830    // Use Rust field names directly - no rename_all needed
831    let attributes_to_add = quote! {
832        #[derive(
833            Debug,
834            Clone,
835            serde::Serialize,
836            serde::Deserialize,
837            ::service_kit::utoipa::ToSchema
838        )]
839        #[serde(deny_unknown_fields)]
840    };
841
842    let mut parsed_attrs: Vec<syn::Attribute> =
843        syn::parse::Parser::parse(syn::Attribute::parse_outer, attributes_to_add.into())
844            .expect("Failed to parse attributes");
845    // Prepend derives so helper attributes like #[serde(...)] appear after derives
846    let mut new_attrs: Vec<syn::Attribute> = Vec::with_capacity(parsed_attrs.len() + input.attrs.len());
847    new_attrs.append(&mut parsed_attrs);
848    new_attrs.append(&mut input.attrs);
849    input.attrs = new_attrs;
850
851    if let syn::Data::Struct(ref mut data_struct) = input.data {
852        if let syn::Fields::Named(ref mut fields) = data_struct.fields {
853            for field in fields.named.iter_mut() {
854                if let Type::Path(type_path) = &field.ty {
855                    if is_recursive_type(&type_path.path, &input.ident.to_string()) {
856                        field.attrs.push(syn::parse_quote! {
857                            #[schema(value_type = Object)]
858                        });
859                    }
860                }
861            }
862        }
863    }
864
865    // 针对字段中的泛型实例,收集并注册其具体实例(例如 ApiDefinition<AgentDefinitionData>)
866    let mut nested_regs: Vec<proc_macro2::TokenStream> = Vec::new();
867    if let syn::Data::Struct(ref data_struct) = input.data {
868        if let syn::Fields::Named(ref fields_named) = data_struct.fields {
869            for field in &fields_named.named {
870                let nested = collect_concrete_generic_types(&field.ty);
871                for (name, ty) in nested {
872                    let name_lit = syn::LitStr::new(&name, proc_macro2::Span::call_site());
873                    let ty_tokens = quote! { #ty };
874                    nested_regs.push(quote! {
875                        ::service_kit::inventory::submit! {
876                            ::service_kit::ApiDtoMetadata {
877                                name: #name_lit,
878                                schema_provider: || (
879                                    #name_lit.to_string(),
880                                    <#ty_tokens as ::service_kit::utoipa::PartialSchema>::schema(),
881                                ),
882                            }
883                        }
884                    });
885                }
886            }
887        }
888    }
889
890    // 注册 DTO schema 到 inventory(仅对非泛型类型注册;泛型类型需在具体实例处注册)
891    let registration = if input.generics.params.is_empty() {
892        quote! {
893            ::service_kit::inventory::submit! {
894                ::service_kit::ApiDtoMetadata {
895                    name: #type_name_str,
896                    schema_provider: || {
897                        (
898                            #type_name_str.to_string(),
899                            <#type_name as ::service_kit::utoipa::PartialSchema>::schema(),
900                        )
901                    },
902                }
903            }
904        }
905    } else {
906        quote! {}
907    };
908
909    let output = quote! {
910        #input
911        #registration
912        #(#nested_regs)*
913    };
914
915    output.into()
916}
917
918fn is_recursive_type(path: &syn::Path, self_name: &str) -> bool {
919    if let Some(segment) = path.segments.last() {
920        let type_name = segment.ident.to_string();
921        if type_name == "Box" || type_name == "Option" {
922            if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
923                if let Some(syn::GenericArgument::Type(Type::Path(inner_type_path))) = args.args.first()
924                {
925                    if type_name == "Option" {
926                        if let Some(inner_segment) = inner_type_path.path.segments.last() {
927                            if inner_segment.ident == "Box" {
928                                return is_recursive_boxed_type(inner_segment, self_name);
929                            }
930                        }
931                    } else {
932                        return is_recursive_boxed_type(segment, self_name);
933                    }
934                }
935            }
936        }
937    }
938    false
939}
940
941fn is_recursive_boxed_type(segment: &syn::PathSegment, self_name: &str) -> bool {
942    if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
943        if let Some(syn::GenericArgument::Type(Type::Path(inner_type))) = args.args.first() {
944            if let Some(inner_segment) = inner_type.path.segments.last() {
945                return inner_segment.ident == self_name;
946            }
947        }
948    }
949    false
950}