Skip to main content

server_less_parse/
lib.rs

1//! Shared parsing utilities for server-less proc macros.
2//!
3//! This crate provides common types and functions for extracting
4//! method information from impl blocks.
5
6use syn::{
7    FnArg, GenericArgument, Ident, ImplItem, ImplItemFn, ItemImpl, Lit, Meta, Pat, PathArguments,
8    ReturnType, Type, TypeReference,
9};
10
11/// Parsed method information with full `syn` AST types.
12///
13/// This is the rich, compile-time representation used by all proc macros
14/// during code generation. It retains full `syn::Type` and `syn::Ident`
15/// nodes for accurate token generation.
16///
17/// **Not to be confused with [`server_less_core::MethodInfo`]**, which is
18/// a simplified, string-based representation for runtime introspection.
19#[derive(Debug, Clone)]
20pub struct MethodInfo {
21    /// The original method
22    pub method: ImplItemFn,
23    /// Method name
24    pub name: Ident,
25    /// Documentation string
26    pub docs: Option<String>,
27    /// Parameters (excluding self)
28    pub params: Vec<ParamInfo>,
29    /// Return type info
30    pub return_info: ReturnInfo,
31    /// Whether the method is async
32    pub is_async: bool,
33    /// Group assignment from `#[server(group = "...")]`
34    pub group: Option<String>,
35}
36
37/// Registry of declared method groups from `#[server(groups(...))]`.
38///
39/// When present on an impl block, method `group` values are resolved as IDs
40/// against this registry. When absent, `group` values are literal display names.
41#[derive(Debug, Clone)]
42pub struct GroupRegistry {
43    /// Ordered list of (id, display_name) pairs.
44    /// Ordering determines display order in help output and documentation.
45    pub groups: Vec<(String, String)>,
46}
47
48/// Parsed parameter information
49#[derive(Debug, Clone)]
50pub struct ParamInfo {
51    /// Parameter name
52    pub name: Ident,
53    /// Parameter type
54    pub ty: Type,
55    /// Whether this is `Option<T>`
56    pub is_optional: bool,
57    /// Whether this is `bool`
58    pub is_bool: bool,
59    /// Whether this is `Vec<T>`
60    pub is_vec: bool,
61    /// Inner type if `Vec<T>`
62    pub vec_inner: Option<Type>,
63    /// Whether this looks like an ID (ends with _id or is named id)
64    pub is_id: bool,
65    /// Custom wire name (from #[param(name = "...")])
66    pub wire_name: Option<String>,
67    /// Parameter location override (from #[param(query/path/body/header)])
68    pub location: Option<ParamLocation>,
69    /// Default value as a string (from #[param(default = ...)])
70    pub default_value: Option<String>,
71    /// Short flag character (from #[param(short = 'x')])
72    pub short_flag: Option<char>,
73    /// Custom help text (from #[param(help = "...")])
74    pub help_text: Option<String>,
75    /// Whether this is a positional argument (from #[param(positional)] or is_id heuristic)
76    pub is_positional: bool,
77}
78
79impl MethodInfo {
80    /// Method name as a protocol string, stripping the `r#` prefix from raw identifiers.
81    ///
82    /// Use this instead of `.name.to_string()` whenever generating protocol-level names
83    /// (CLI subcommands, HTTP routes, JSON-RPC methods, OpenAPI operation IDs, etc.).
84    /// Raw identifiers like `r#type` must appear as `"type"` in protocols, not `"r#type"`.
85    pub fn name_str(&self) -> String {
86        ident_str(&self.name)
87    }
88}
89
90impl ParamInfo {
91    /// Parameter name as a protocol string, stripping the `r#` prefix from raw identifiers.
92    ///
93    /// Use this instead of `.name.to_string()` whenever generating protocol-level names
94    /// (CLI flags, HTTP query params, JSON schema properties, etc.).
95    pub fn name_str(&self) -> String {
96        ident_str(&self.name)
97    }
98}
99
100/// Convert an identifier to a string, stripping the `r#` prefix for raw identifiers.
101///
102/// `proc_macro2::Ident::to_string()` preserves the `r#` prefix (e.g. `r#type` → `"r#type"`),
103/// which produces incorrect protocol names. Use this function whenever an `Ident` is converted
104/// to a string for protocol-level output (CLI flags, HTTP routes, JSON-RPC method names, etc.).
105pub fn ident_str(ident: &Ident) -> String {
106    let s = ident.to_string();
107    s.strip_prefix("r#").map(str::to_string).unwrap_or(s)
108}
109
110/// Parameter location for HTTP requests
111#[derive(Debug, Clone, PartialEq)]
112pub enum ParamLocation {
113    Query,
114    Path,
115    Body,
116    Header,
117}
118
119/// Parsed return type information
120#[derive(Debug, Clone)]
121pub struct ReturnInfo {
122    /// The full return type
123    pub ty: Option<Type>,
124    /// Inner type if `Result<T, E>`
125    pub ok_type: Option<Type>,
126    /// Error type if `Result<T, E>`
127    pub err_type: Option<Type>,
128    /// Inner type if `Option<T>`
129    pub some_type: Option<Type>,
130    /// Whether it's a Result
131    pub is_result: bool,
132    /// Whether it's an Option (and not Result)
133    pub is_option: bool,
134    /// Whether it returns ()
135    pub is_unit: bool,
136    /// Whether it's impl Stream<Item=T>
137    pub is_stream: bool,
138    /// The stream item type if is_stream
139    pub stream_item: Option<Type>,
140    /// Whether it's impl Iterator<Item=T>
141    pub is_iterator: bool,
142    /// The iterator item type if is_iterator
143    pub iterator_item: Option<Type>,
144    /// Whether the return type is a reference (&T)
145    pub is_reference: bool,
146    /// The inner type T if returning &T
147    pub reference_inner: Option<Type>,
148}
149
150impl MethodInfo {
151    /// Parse a method from an ImplItemFn
152    ///
153    /// Returns None for associated functions without `&self` (constructors, etc.)
154    pub fn parse(method: &ImplItemFn) -> syn::Result<Option<Self>> {
155        let name = method.sig.ident.clone();
156        let is_async = method.sig.asyncness.is_some();
157
158        // Skip associated functions without self receiver (constructors, etc.)
159        let has_receiver = method
160            .sig
161            .inputs
162            .iter()
163            .any(|arg| matches!(arg, FnArg::Receiver(_)));
164        if !has_receiver {
165            return Ok(None);
166        }
167
168        // Extract doc comments
169        let docs = extract_docs(&method.attrs);
170
171        // Parse parameters
172        let params = parse_params(&method.sig.inputs)?;
173
174        // Parse return type
175        let return_info = parse_return_type(&method.sig.output);
176
177        // Extract group from #[server(group = "...")]
178        let group = extract_server_group(&method.attrs);
179
180        Ok(Some(Self {
181            method: method.clone(),
182            name,
183            docs,
184            params,
185            return_info,
186            is_async,
187            group,
188        }))
189    }
190}
191
192/// Extract doc comments from attributes
193pub fn extract_docs(attrs: &[syn::Attribute]) -> Option<String> {
194    let docs: Vec<String> = attrs
195        .iter()
196        .filter_map(|attr| {
197            if attr.path().is_ident("doc")
198                && let Meta::NameValue(meta) = &attr.meta
199                && let syn::Expr::Lit(syn::ExprLit {
200                    lit: Lit::Str(s), ..
201                }) = &meta.value
202            {
203                return Some(s.value().trim().to_string());
204            }
205            None
206        })
207        .collect();
208
209    if docs.is_empty() {
210        None
211    } else {
212        Some(docs.join("\n"))
213    }
214}
215
216/// Extract the `group` value from `#[server(group = "...")]` on a method.
217fn extract_server_group(attrs: &[syn::Attribute]) -> Option<String> {
218    for attr in attrs {
219        if attr.path().is_ident("server") {
220            let mut group = None;
221            let _ = attr.parse_nested_meta(|meta| {
222                if meta.path.is_ident("group") {
223                    let value = meta.value()?;
224                    let s: syn::LitStr = value.parse()?;
225                    group = Some(s.value());
226                } else if meta.input.peek(syn::Token![=]) {
227                    // Consume other `key = value` pairs without error.
228                    let _: proc_macro2::TokenStream = meta.value()?.parse()?;
229                }
230                Ok(())
231            });
232            if group.is_some() {
233                return group;
234            }
235        }
236    }
237    None
238}
239
240/// Extract the group registry from `#[server(groups(...))]` on an impl block.
241///
242/// Returns `None` if no `groups(...)` attribute is present.
243/// Returns ordered `(id, display_name)` pairs matching declaration order.
244pub fn extract_groups(impl_block: &ItemImpl) -> syn::Result<Option<GroupRegistry>> {
245    for attr in &impl_block.attrs {
246        if attr.path().is_ident("server") {
247            let mut groups = Vec::new();
248            let mut found_groups = false;
249            attr.parse_nested_meta(|meta| {
250                if meta.path.is_ident("groups") {
251                    found_groups = true;
252                    meta.parse_nested_meta(|inner| {
253                        let id = inner
254                            .path
255                            .get_ident()
256                            .ok_or_else(|| inner.error("expected group identifier"))?
257                            .to_string();
258                        let value = inner.value()?;
259                        let display: syn::LitStr = value.parse()?;
260                        groups.push((id, display.value()));
261                        Ok(())
262                    })?;
263                } else if meta.input.peek(syn::Token![=]) {
264                    let _: proc_macro2::TokenStream = meta.value()?.parse()?;
265                } else if meta.input.peek(syn::token::Paren) {
266                    let _content;
267                    syn::parenthesized!(_content in meta.input);
268                }
269                Ok(())
270            })?;
271            if found_groups {
272                return Ok(Some(GroupRegistry { groups }));
273            }
274        }
275    }
276    Ok(None)
277}
278
279/// Resolve a method's group against the registry.
280///
281/// When the method has `group = "id"`, the registry must be present and must
282/// contain a matching ID — otherwise a compile error is emitted. The returned
283/// string is the display name from the registry.
284///
285/// When the method has no `group` attribute, returns `None`.
286pub fn resolve_method_group(
287    method: &MethodInfo,
288    registry: &Option<GroupRegistry>,
289) -> syn::Result<Option<String>> {
290    let group_value = match &method.group {
291        Some(v) => v,
292        None => return Ok(None),
293    };
294
295    let span = method.method.sig.ident.span();
296
297    match registry {
298        Some(reg) => {
299            for (id, display) in &reg.groups {
300                if id == group_value {
301                    return Ok(Some(display.clone()));
302                }
303            }
304            Err(syn::Error::new(
305                span,
306                format!(
307                    "unknown group `{group_value}`; declared groups are: {}",
308                    reg.groups
309                        .iter()
310                        .map(|(id, _)| format!("`{id}`"))
311                        .collect::<Vec<_>>()
312                        .join(", ")
313                ),
314            ))
315        }
316        None => Err(syn::Error::new(
317            span,
318            format!(
319                "method has `group = \"{group_value}\"` but no `groups(...)` registry is declared on the impl block\n\
320                 \n\
321                 help: add `#[server(groups({group_value} = \"Display Name\"))]` to the impl block"
322            ),
323        )),
324    }
325}
326
327/// Parsed result of `#[param(...)]` attributes.
328#[derive(Debug, Clone, Default)]
329pub struct ParsedParamAttrs {
330    pub wire_name: Option<String>,
331    pub location: Option<ParamLocation>,
332    pub default_value: Option<String>,
333    pub short_flag: Option<char>,
334    pub help_text: Option<String>,
335    pub positional: bool,
336}
337
338/// Compute Levenshtein edit distance between two strings.
339fn levenshtein(a: &str, b: &str) -> usize {
340    let a: Vec<char> = a.chars().collect();
341    let b: Vec<char> = b.chars().collect();
342    let m = a.len();
343    let n = b.len();
344    let mut dp = vec![vec![0usize; n + 1]; m + 1];
345    for i in 0..=m {
346        dp[i][0] = i;
347    }
348    for j in 0..=n {
349        dp[0][j] = j;
350    }
351    for i in 1..=m {
352        for j in 1..=n {
353            dp[i][j] = if a[i - 1] == b[j - 1] {
354                dp[i - 1][j - 1]
355            } else {
356                1 + dp[i - 1][j - 1].min(dp[i - 1][j]).min(dp[i][j - 1])
357            };
358        }
359    }
360    dp[m][n]
361}
362
363/// Return the closest candidate to `input` within edit distance ≤ 2, or `None`.
364fn did_you_mean<'a>(input: &str, candidates: &[&'a str]) -> Option<&'a str> {
365    candidates
366        .iter()
367        .filter_map(|&c| {
368            let d = levenshtein(input, c);
369            if d <= 2 { Some((d, c)) } else { None }
370        })
371        .min_by_key(|&(d, _)| d)
372        .map(|(_, c)| c)
373}
374
375/// Parse #[param(...)] attributes from a parameter
376pub fn parse_param_attrs(attrs: &[syn::Attribute]) -> syn::Result<ParsedParamAttrs> {
377    let mut wire_name = None;
378    let mut location = None;
379    let mut default_value = None;
380    let mut short_flag = None;
381    let mut help_text = None;
382    let mut positional = false;
383
384    for attr in attrs {
385        if !attr.path().is_ident("param") {
386            continue;
387        }
388
389        attr.parse_nested_meta(|meta| {
390            // #[param(name = "...")]
391            if meta.path.is_ident("name") {
392                let value: syn::LitStr = meta.value()?.parse()?;
393                wire_name = Some(value.value());
394                Ok(())
395            }
396            // #[param(default = ...)]
397            else if meta.path.is_ident("default") {
398                // Accept various literal types
399                let value = meta.value()?;
400                let lookahead = value.lookahead1();
401                if lookahead.peek(syn::LitStr) {
402                    let lit: syn::LitStr = value.parse()?;
403                    default_value = Some(format!("\"{}\"", lit.value()));
404                } else if lookahead.peek(syn::LitInt) {
405                    let lit: syn::LitInt = value.parse()?;
406                    default_value = Some(lit.to_string());
407                } else if lookahead.peek(syn::LitBool) {
408                    let lit: syn::LitBool = value.parse()?;
409                    default_value = Some(lit.value.to_string());
410                } else {
411                    return Err(lookahead.error());
412                }
413                Ok(())
414            }
415            // #[param(query)] or #[param(path)] etc.
416            else if meta.path.is_ident("query") {
417                location = Some(ParamLocation::Query);
418                Ok(())
419            } else if meta.path.is_ident("path") {
420                location = Some(ParamLocation::Path);
421                Ok(())
422            } else if meta.path.is_ident("body") {
423                location = Some(ParamLocation::Body);
424                Ok(())
425            } else if meta.path.is_ident("header") {
426                location = Some(ParamLocation::Header);
427                Ok(())
428            }
429            // #[param(short = 'v')]
430            else if meta.path.is_ident("short") {
431                let value: syn::LitChar = meta.value()?.parse()?;
432                short_flag = Some(value.value());
433                Ok(())
434            }
435            // #[param(help = "description")]
436            else if meta.path.is_ident("help") {
437                let value: syn::LitStr = meta.value()?.parse()?;
438                help_text = Some(value.value());
439                Ok(())
440            }
441            // #[param(positional)]
442            else if meta.path.is_ident("positional") {
443                positional = true;
444                Ok(())
445            } else {
446                const VALID: &[&str] = &[
447                    "name", "default", "query", "path", "body", "header", "short", "help",
448                    "positional",
449                ];
450                let unknown = meta
451                    .path
452                    .get_ident()
453                    .map(|i| i.to_string())
454                    .unwrap_or_default();
455                let suggestion = did_you_mean(&unknown, VALID)
456                    .map(|s| format!(" — did you mean `{s}`?"))
457                    .unwrap_or_default();
458                Err(meta.error(format!(
459                    "unknown attribute `{unknown}`{suggestion}\n\
460                     \n\
461                     Valid attributes: name, default, query, path, body, header, short, help, positional\n\
462                     \n\
463                     Examples:\n\
464                     - #[param(name = \"q\")]\n\
465                     - #[param(default = 10)]\n\
466                     - #[param(query)]\n\
467                     - #[param(header, name = \"X-API-Key\")]\n\
468                     - #[param(short = 'v')]\n\
469                     - #[param(help = \"Enable verbose output\")]\n\
470                     - #[param(positional)]"
471                )))
472            }
473        })?;
474    }
475
476    Ok(ParsedParamAttrs {
477        wire_name,
478        location,
479        default_value,
480        short_flag,
481        help_text,
482        positional,
483    })
484}
485
486/// Parse function parameters (excluding self)
487pub fn parse_params(
488    inputs: &syn::punctuated::Punctuated<FnArg, syn::Token![,]>,
489) -> syn::Result<Vec<ParamInfo>> {
490    let mut params = Vec::new();
491
492    for arg in inputs {
493        match arg {
494            FnArg::Receiver(_) => continue, // skip self
495            FnArg::Typed(pat_type) => {
496                let name = match pat_type.pat.as_ref() {
497                    Pat::Ident(pat_ident) => pat_ident.ident.clone(),
498                    other => {
499                        return Err(syn::Error::new_spanned(
500                            other,
501                            "unsupported parameter pattern\n\
502                             \n\
503                             Server-less macros require simple parameter names.\n\
504                             Use: name: String\n\
505                             Not: (name, _): (String, i32) or &name: &String",
506                        ));
507                    }
508                };
509
510                let ty = (*pat_type.ty).clone();
511                let is_optional = is_option_type(&ty);
512                let is_bool = is_bool_type(&ty);
513                let vec_inner = extract_vec_type(&ty);
514                let is_vec = vec_inner.is_some();
515                let is_id = is_id_param(&name);
516
517                // Parse #[param(...)] attributes
518                let parsed = parse_param_attrs(&pat_type.attrs)?;
519
520                // is_positional: explicit attribute takes priority, is_id heuristic as fallback
521                let is_positional = parsed.positional || is_id;
522
523                params.push(ParamInfo {
524                    name,
525                    ty,
526                    is_optional,
527                    is_bool,
528                    is_vec,
529                    vec_inner,
530                    is_id,
531                    is_positional,
532                    wire_name: parsed.wire_name,
533                    location: parsed.location,
534                    default_value: parsed.default_value,
535                    short_flag: parsed.short_flag,
536                    help_text: parsed.help_text,
537                });
538            }
539        }
540    }
541
542    Ok(params)
543}
544
545/// Parse return type information
546pub fn parse_return_type(output: &ReturnType) -> ReturnInfo {
547    match output {
548        ReturnType::Default => ReturnInfo {
549            ty: None,
550            ok_type: None,
551            err_type: None,
552            some_type: None,
553            is_result: false,
554            is_option: false,
555            is_unit: true,
556            is_stream: false,
557            stream_item: None,
558            is_iterator: false,
559            iterator_item: None,
560            is_reference: false,
561            reference_inner: None,
562        },
563        ReturnType::Type(_, ty) => {
564            let ty = ty.as_ref().clone();
565
566            // Check for Result<T, E>
567            if let Some((ok, err)) = extract_result_types(&ty) {
568                return ReturnInfo {
569                    ty: Some(ty),
570                    ok_type: Some(ok),
571                    err_type: Some(err),
572                    some_type: None,
573                    is_result: true,
574                    is_option: false,
575                    is_unit: false,
576                    is_stream: false,
577                    stream_item: None,
578                    is_iterator: false,
579                    iterator_item: None,
580                    is_reference: false,
581                    reference_inner: None,
582                };
583            }
584
585            // Check for Option<T>
586            if let Some(inner) = extract_option_type(&ty) {
587                return ReturnInfo {
588                    ty: Some(ty),
589                    ok_type: None,
590                    err_type: None,
591                    some_type: Some(inner),
592                    is_result: false,
593                    is_option: true,
594                    is_unit: false,
595                    is_stream: false,
596                    stream_item: None,
597                    is_iterator: false,
598                    iterator_item: None,
599                    is_reference: false,
600                    reference_inner: None,
601                };
602            }
603
604            // Check for impl Stream<Item=T>
605            if let Some(item) = extract_stream_item(&ty) {
606                return ReturnInfo {
607                    ty: Some(ty),
608                    ok_type: None,
609                    err_type: None,
610                    some_type: None,
611                    is_result: false,
612                    is_option: false,
613                    is_unit: false,
614                    is_stream: true,
615                    stream_item: Some(item),
616                    is_iterator: false,
617                    iterator_item: None,
618                    is_reference: false,
619                    reference_inner: None,
620                };
621            }
622
623            // Check for impl Iterator<Item=T>
624            if let Some(item) = extract_iterator_item(&ty) {
625                return ReturnInfo {
626                    ty: Some(ty),
627                    ok_type: None,
628                    err_type: None,
629                    some_type: None,
630                    is_result: false,
631                    is_option: false,
632                    is_unit: false,
633                    is_stream: false,
634                    stream_item: None,
635                    is_iterator: true,
636                    iterator_item: Some(item),
637                    is_reference: false,
638                    reference_inner: None,
639                };
640            }
641
642            // Check for ()
643            if is_unit_type(&ty) {
644                return ReturnInfo {
645                    ty: Some(ty),
646                    ok_type: None,
647                    err_type: None,
648                    some_type: None,
649                    is_result: false,
650                    is_option: false,
651                    is_unit: true,
652                    is_stream: false,
653                    stream_item: None,
654                    is_iterator: false,
655                    iterator_item: None,
656                    is_reference: false,
657                    reference_inner: None,
658                };
659            }
660
661            // Check for &T (reference return — mount point)
662            if let Type::Reference(TypeReference { elem, .. }) = &ty {
663                let inner = elem.as_ref().clone();
664                return ReturnInfo {
665                    ty: Some(ty),
666                    ok_type: None,
667                    err_type: None,
668                    some_type: None,
669                    is_result: false,
670                    is_option: false,
671                    is_unit: false,
672                    is_stream: false,
673                    stream_item: None,
674                    is_iterator: false,
675                    iterator_item: None,
676                    is_reference: true,
677                    reference_inner: Some(inner),
678                };
679            }
680
681            // Regular type
682            ReturnInfo {
683                ty: Some(ty),
684                ok_type: None,
685                err_type: None,
686                some_type: None,
687                is_result: false,
688                is_option: false,
689                is_unit: false,
690                is_stream: false,
691                stream_item: None,
692                is_iterator: false,
693                iterator_item: None,
694                is_reference: false,
695                reference_inner: None,
696            }
697        }
698    }
699}
700
701/// Check if a type is `bool`
702pub fn is_bool_type(ty: &Type) -> bool {
703    if let Type::Path(type_path) = ty
704        && let Some(segment) = type_path.path.segments.last()
705        && type_path.path.segments.len() == 1
706    {
707        return segment.ident == "bool";
708    }
709    false
710}
711
712/// Check if a type is `Vec<T>` and extract T
713pub fn extract_vec_type(ty: &Type) -> Option<Type> {
714    if let Type::Path(type_path) = ty
715        && let Some(segment) = type_path.path.segments.last()
716        && segment.ident == "Vec"
717        && let PathArguments::AngleBracketed(args) = &segment.arguments
718        && let Some(GenericArgument::Type(inner)) = args.args.first()
719    {
720        return Some(inner.clone());
721    }
722    None
723}
724
725/// Check if a type is `HashMap<K, V>` or `BTreeMap<K, V>` and extract K and V
726pub fn extract_map_type(ty: &Type) -> Option<(Type, Type)> {
727    if let Type::Path(type_path) = ty
728        && let Some(segment) = type_path.path.segments.last()
729        && (segment.ident == "HashMap" || segment.ident == "BTreeMap")
730        && let PathArguments::AngleBracketed(args) = &segment.arguments
731    {
732        let mut iter = args.args.iter();
733        if let (Some(GenericArgument::Type(key)), Some(GenericArgument::Type(val))) =
734            (iter.next(), iter.next())
735        {
736            return Some((key.clone(), val.clone()));
737        }
738    }
739    None
740}
741
742/// Check if a type is `Option<T>` and extract T
743pub fn extract_option_type(ty: &Type) -> Option<Type> {
744    if let Type::Path(type_path) = ty
745        && let Some(segment) = type_path.path.segments.last()
746        && segment.ident == "Option"
747        && let PathArguments::AngleBracketed(args) = &segment.arguments
748        && let Some(GenericArgument::Type(inner)) = args.args.first()
749    {
750        return Some(inner.clone());
751    }
752    None
753}
754
755/// Check if a type is `Option<T>`
756pub fn is_option_type(ty: &Type) -> bool {
757    extract_option_type(ty).is_some()
758}
759
760/// Check if a type is Result<T, E> and extract T and E
761pub fn extract_result_types(ty: &Type) -> Option<(Type, Type)> {
762    if let Type::Path(type_path) = ty
763        && let Some(segment) = type_path.path.segments.last()
764        && segment.ident == "Result"
765        && let PathArguments::AngleBracketed(args) = &segment.arguments
766    {
767        let mut iter = args.args.iter();
768        if let (Some(GenericArgument::Type(ok)), Some(GenericArgument::Type(err))) =
769            (iter.next(), iter.next())
770        {
771            return Some((ok.clone(), err.clone()));
772        }
773    }
774    None
775}
776
777/// Check if a type is impl Stream<Item=T> and extract T
778pub fn extract_stream_item(ty: &Type) -> Option<Type> {
779    if let Type::ImplTrait(impl_trait) = ty {
780        for bound in &impl_trait.bounds {
781            if let syn::TypeParamBound::Trait(trait_bound) = bound
782                && let Some(segment) = trait_bound.path.segments.last()
783                && segment.ident == "Stream"
784                && let PathArguments::AngleBracketed(args) = &segment.arguments
785            {
786                for arg in &args.args {
787                    if let GenericArgument::AssocType(assoc) = arg
788                        && assoc.ident == "Item"
789                    {
790                        return Some(assoc.ty.clone());
791                    }
792                }
793            }
794        }
795    }
796    None
797}
798
799/// Check if a type is impl Iterator<Item=T> and extract T
800pub fn extract_iterator_item(ty: &Type) -> Option<Type> {
801    if let Type::ImplTrait(impl_trait) = ty {
802        for bound in &impl_trait.bounds {
803            if let syn::TypeParamBound::Trait(trait_bound) = bound
804                && let Some(segment) = trait_bound.path.segments.last()
805                && segment.ident == "Iterator"
806                && let PathArguments::AngleBracketed(args) = &segment.arguments
807            {
808                for arg in &args.args {
809                    if let GenericArgument::AssocType(assoc) = arg
810                        && assoc.ident == "Item"
811                    {
812                        return Some(assoc.ty.clone());
813                    }
814                }
815            }
816        }
817    }
818    None
819}
820
821/// Check if a type is ()
822pub fn is_unit_type(ty: &Type) -> bool {
823    if let Type::Tuple(tuple) = ty {
824        return tuple.elems.is_empty();
825    }
826    false
827}
828
829/// Check if a parameter name looks like an ID
830pub fn is_id_param(name: &Ident) -> bool {
831    let name_str = ident_str(name);
832    name_str == "id" || name_str.ends_with("_id")
833}
834
835/// Extract all methods from an impl block
836///
837/// Skips:
838/// - Private methods (starting with `_`)
839/// - Associated functions without `&self` receiver (constructors, etc.)
840pub fn extract_methods(impl_block: &ItemImpl) -> syn::Result<Vec<MethodInfo>> {
841    let mut methods = Vec::new();
842
843    for item in &impl_block.items {
844        if let ImplItem::Fn(method) = item {
845            // Skip private methods (those starting with _)
846            if method.sig.ident.to_string().starts_with('_') {
847                continue;
848            }
849            // Parse method - returns None for associated functions without self
850            if let Some(info) = MethodInfo::parse(method)? {
851                methods.push(info);
852            }
853        }
854    }
855
856    Ok(methods)
857}
858
859/// Categorized methods for code generation.
860///
861/// Methods returning `&T` (non-async) are mount points; everything else is a leaf.
862/// Mount points are further split by whether they take parameters (slug) or not (static).
863pub struct PartitionedMethods<'a> {
864    /// Regular leaf methods (no reference return).
865    pub leaf: Vec<&'a MethodInfo>,
866    /// Static mounts: `fn foo(&self) -> &T` (no params).
867    pub static_mounts: Vec<&'a MethodInfo>,
868    /// Slug mounts: `fn foo(&self, id: Id) -> &T` (has params).
869    pub slug_mounts: Vec<&'a MethodInfo>,
870}
871
872/// Partition methods into leaf commands, static mounts, and slug mounts.
873///
874/// The `skip` predicate allows each protocol to apply its own skip logic
875/// (e.g., `#[cli(skip)]`, `#[mcp(skip)]`).
876pub fn partition_methods<'a>(
877    methods: &'a [MethodInfo],
878    skip: impl Fn(&MethodInfo) -> bool,
879) -> PartitionedMethods<'a> {
880    let mut result = PartitionedMethods {
881        leaf: Vec::new(),
882        static_mounts: Vec::new(),
883        slug_mounts: Vec::new(),
884    };
885
886    for method in methods {
887        if skip(method) {
888            continue;
889        }
890
891        if method.return_info.is_reference && !method.is_async {
892            if method.params.is_empty() {
893                result.static_mounts.push(method);
894            } else {
895                result.slug_mounts.push(method);
896            }
897        } else {
898            result.leaf.push(method);
899        }
900    }
901
902    result
903}
904
905/// Get the struct name from an impl block
906pub fn get_impl_name(impl_block: &ItemImpl) -> syn::Result<Ident> {
907    if let Type::Path(type_path) = impl_block.self_ty.as_ref()
908        && let Some(segment) = type_path.path.segments.last()
909    {
910        return Ok(segment.ident.clone());
911    }
912    Err(syn::Error::new_spanned(
913        &impl_block.self_ty,
914        "Expected a simple type name",
915    ))
916}
917
918#[cfg(test)]
919mod tests {
920    use super::*;
921    use quote::quote;
922
923    // ── extract_docs ────────────────────────────────────────────────
924
925    #[test]
926    fn extract_docs_returns_none_when_no_doc_attrs() {
927        let method: ImplItemFn = syn::parse_quote! {
928            fn hello(&self) {}
929        };
930        assert!(extract_docs(&method.attrs).is_none());
931    }
932
933    #[test]
934    fn extract_docs_extracts_single_line() {
935        let method: ImplItemFn = syn::parse_quote! {
936            /// Hello world
937            fn hello(&self) {}
938        };
939        assert_eq!(extract_docs(&method.attrs).unwrap(), "Hello world");
940    }
941
942    #[test]
943    fn extract_docs_joins_multiple_lines() {
944        let method: ImplItemFn = syn::parse_quote! {
945            /// Line one
946            /// Line two
947            fn hello(&self) {}
948        };
949        assert_eq!(extract_docs(&method.attrs).unwrap(), "Line one\nLine two");
950    }
951
952    #[test]
953    fn extract_docs_ignores_non_doc_attrs() {
954        let method: ImplItemFn = syn::parse_quote! {
955            #[inline]
956            /// Documented
957            fn hello(&self) {}
958        };
959        assert_eq!(extract_docs(&method.attrs).unwrap(), "Documented");
960    }
961
962    // ── parse_return_type ───────────────────────────────────────────
963
964    #[test]
965    fn parse_return_type_default_is_unit() {
966        let ret: ReturnType = syn::parse_quote! {};
967        let info = parse_return_type(&ret);
968        assert!(info.is_unit);
969        assert!(info.ty.is_none());
970        assert!(!info.is_result);
971        assert!(!info.is_option);
972        assert!(!info.is_reference);
973    }
974
975    #[test]
976    fn parse_return_type_regular_type() {
977        let ret: ReturnType = syn::parse_quote! { -> String };
978        let info = parse_return_type(&ret);
979        assert!(!info.is_unit);
980        assert!(!info.is_result);
981        assert!(!info.is_option);
982        assert!(!info.is_reference);
983        assert!(info.ty.is_some());
984    }
985
986    #[test]
987    fn parse_return_type_result() {
988        let ret: ReturnType = syn::parse_quote! { -> Result<String, MyError> };
989        let info = parse_return_type(&ret);
990        assert!(info.is_result);
991        assert!(!info.is_option);
992        assert!(!info.is_unit);
993
994        let ok = info.ok_type.unwrap();
995        assert_eq!(quote!(#ok).to_string(), "String");
996
997        let err = info.err_type.unwrap();
998        assert_eq!(quote!(#err).to_string(), "MyError");
999    }
1000
1001    #[test]
1002    fn parse_return_type_option() {
1003        let ret: ReturnType = syn::parse_quote! { -> Option<i32> };
1004        let info = parse_return_type(&ret);
1005        assert!(info.is_option);
1006        assert!(!info.is_result);
1007        assert!(!info.is_unit);
1008
1009        let some = info.some_type.unwrap();
1010        assert_eq!(quote!(#some).to_string(), "i32");
1011    }
1012
1013    #[test]
1014    fn parse_return_type_unit_tuple() {
1015        let ret: ReturnType = syn::parse_quote! { -> () };
1016        let info = parse_return_type(&ret);
1017        assert!(info.is_unit);
1018        assert!(info.ty.is_some());
1019    }
1020
1021    #[test]
1022    fn parse_return_type_reference() {
1023        let ret: ReturnType = syn::parse_quote! { -> &SubRouter };
1024        let info = parse_return_type(&ret);
1025        assert!(info.is_reference);
1026        assert!(!info.is_unit);
1027
1028        let inner = info.reference_inner.unwrap();
1029        assert_eq!(quote!(#inner).to_string(), "SubRouter");
1030    }
1031
1032    #[test]
1033    fn parse_return_type_stream() {
1034        let ret: ReturnType = syn::parse_quote! { -> impl Stream<Item = u64> };
1035        let info = parse_return_type(&ret);
1036        assert!(info.is_stream);
1037        assert!(!info.is_result);
1038
1039        let item = info.stream_item.unwrap();
1040        assert_eq!(quote!(#item).to_string(), "u64");
1041    }
1042
1043    // ── is_option_type / extract_option_type ────────────────────────
1044
1045    #[test]
1046    fn is_option_type_true() {
1047        let ty: Type = syn::parse_quote! { Option<String> };
1048        assert!(is_option_type(&ty));
1049        let inner = extract_option_type(&ty).unwrap();
1050        assert_eq!(quote!(#inner).to_string(), "String");
1051    }
1052
1053    #[test]
1054    fn is_option_type_false_for_non_option() {
1055        let ty: Type = syn::parse_quote! { String };
1056        assert!(!is_option_type(&ty));
1057        assert!(extract_option_type(&ty).is_none());
1058    }
1059
1060    // ── extract_result_types ────────────────────────────────────────
1061
1062    #[test]
1063    fn extract_result_types_works() {
1064        let ty: Type = syn::parse_quote! { Result<Vec<u8>, std::io::Error> };
1065        let (ok, err) = extract_result_types(&ty).unwrap();
1066        assert_eq!(quote!(#ok).to_string(), "Vec < u8 >");
1067        assert_eq!(quote!(#err).to_string(), "std :: io :: Error");
1068    }
1069
1070    #[test]
1071    fn extract_result_types_none_for_non_result() {
1072        let ty: Type = syn::parse_quote! { Option<i32> };
1073        assert!(extract_result_types(&ty).is_none());
1074    }
1075
1076    // ── is_unit_type ────────────────────────────────────────────────
1077
1078    #[test]
1079    fn is_unit_type_true() {
1080        let ty: Type = syn::parse_quote! { () };
1081        assert!(is_unit_type(&ty));
1082    }
1083
1084    #[test]
1085    fn is_unit_type_false_for_non_tuple() {
1086        let ty: Type = syn::parse_quote! { String };
1087        assert!(!is_unit_type(&ty));
1088    }
1089
1090    #[test]
1091    fn is_unit_type_false_for_nonempty_tuple() {
1092        let ty: Type = syn::parse_quote! { (i32, i32) };
1093        assert!(!is_unit_type(&ty));
1094    }
1095
1096    // ── is_id_param ─────────────────────────────────────────────────
1097
1098    #[test]
1099    fn is_id_param_exact_id() {
1100        let ident: Ident = syn::parse_quote! { id };
1101        assert!(is_id_param(&ident));
1102    }
1103
1104    #[test]
1105    fn is_id_param_suffix_id() {
1106        let ident: Ident = syn::parse_quote! { user_id };
1107        assert!(is_id_param(&ident));
1108    }
1109
1110    #[test]
1111    fn is_id_param_false_for_other_names() {
1112        let ident: Ident = syn::parse_quote! { name };
1113        assert!(!is_id_param(&ident));
1114    }
1115
1116    #[test]
1117    fn is_id_param_false_for_identity() {
1118        // "identity" ends with "id" but not "_id"
1119        let ident: Ident = syn::parse_quote! { identity };
1120        assert!(!is_id_param(&ident));
1121    }
1122
1123    // ── MethodInfo::parse ───────────────────────────────────────────
1124
1125    #[test]
1126    fn method_info_parse_basic() {
1127        let method: ImplItemFn = syn::parse_quote! {
1128            /// Does a thing
1129            fn greet(&self, name: String) -> String {
1130                format!("Hello {name}")
1131            }
1132        };
1133        let info = MethodInfo::parse(&method).unwrap().unwrap();
1134        assert_eq!(info.name.to_string(), "greet");
1135        assert!(!info.is_async);
1136        assert_eq!(info.docs.as_deref(), Some("Does a thing"));
1137        assert_eq!(info.params.len(), 1);
1138        assert_eq!(info.params[0].name.to_string(), "name");
1139        assert!(!info.params[0].is_optional);
1140        assert!(!info.params[0].is_id);
1141    }
1142
1143    #[test]
1144    fn method_info_parse_async_method() {
1145        let method: ImplItemFn = syn::parse_quote! {
1146            async fn fetch(&self) -> Vec<u8> {
1147                vec![]
1148            }
1149        };
1150        let info = MethodInfo::parse(&method).unwrap().unwrap();
1151        assert!(info.is_async);
1152    }
1153
1154    #[test]
1155    fn method_info_parse_skips_associated_function() {
1156        let method: ImplItemFn = syn::parse_quote! {
1157            fn new() -> Self {
1158                Self
1159            }
1160        };
1161        assert!(MethodInfo::parse(&method).unwrap().is_none());
1162    }
1163
1164    #[test]
1165    fn method_info_parse_optional_param() {
1166        let method: ImplItemFn = syn::parse_quote! {
1167            fn search(&self, query: Option<String>) {}
1168        };
1169        let info = MethodInfo::parse(&method).unwrap().unwrap();
1170        assert!(info.params[0].is_optional);
1171    }
1172
1173    #[test]
1174    fn method_info_parse_id_param() {
1175        let method: ImplItemFn = syn::parse_quote! {
1176            fn get_user(&self, user_id: u64) -> String {
1177                String::new()
1178            }
1179        };
1180        let info = MethodInfo::parse(&method).unwrap().unwrap();
1181        assert!(info.params[0].is_id);
1182    }
1183
1184    #[test]
1185    fn method_info_parse_no_docs() {
1186        let method: ImplItemFn = syn::parse_quote! {
1187            fn bare(&self) {}
1188        };
1189        let info = MethodInfo::parse(&method).unwrap().unwrap();
1190        assert!(info.docs.is_none());
1191    }
1192
1193    // ── extract_methods ─────────────────────────────────────────────
1194
1195    #[test]
1196    fn extract_methods_basic() {
1197        let impl_block: ItemImpl = syn::parse_quote! {
1198            impl MyApi {
1199                fn hello(&self) -> String { String::new() }
1200                fn world(&self) -> String { String::new() }
1201            }
1202        };
1203        let methods = extract_methods(&impl_block).unwrap();
1204        assert_eq!(methods.len(), 2);
1205        assert_eq!(methods[0].name.to_string(), "hello");
1206        assert_eq!(methods[1].name.to_string(), "world");
1207    }
1208
1209    #[test]
1210    fn extract_methods_skips_underscore_prefix() {
1211        let impl_block: ItemImpl = syn::parse_quote! {
1212            impl MyApi {
1213                fn public(&self) {}
1214                fn _private(&self) {}
1215                fn __also_private(&self) {}
1216            }
1217        };
1218        let methods = extract_methods(&impl_block).unwrap();
1219        assert_eq!(methods.len(), 1);
1220        assert_eq!(methods[0].name.to_string(), "public");
1221    }
1222
1223    #[test]
1224    fn extract_methods_skips_associated_functions() {
1225        let impl_block: ItemImpl = syn::parse_quote! {
1226            impl MyApi {
1227                fn new() -> Self { Self }
1228                fn from_config(cfg: Config) -> Self { Self }
1229                fn greet(&self) -> String { String::new() }
1230            }
1231        };
1232        let methods = extract_methods(&impl_block).unwrap();
1233        assert_eq!(methods.len(), 1);
1234        assert_eq!(methods[0].name.to_string(), "greet");
1235    }
1236
1237    // ── partition_methods ───────────────────────────────────────────
1238
1239    #[test]
1240    fn partition_methods_splits_correctly() {
1241        let impl_block: ItemImpl = syn::parse_quote! {
1242            impl Router {
1243                fn leaf_action(&self) -> String { String::new() }
1244                fn static_mount(&self) -> &SubRouter { &self.sub }
1245                fn slug_mount(&self, id: u64) -> &SubRouter { &self.sub }
1246                async fn async_ref(&self) -> &SubRouter { &self.sub }
1247            }
1248        };
1249        let methods = extract_methods(&impl_block).unwrap();
1250        let partitioned = partition_methods(&methods, |_| false);
1251
1252        // leaf_action and async_ref (async reference returns are leaf, not mounts)
1253        assert_eq!(partitioned.leaf.len(), 2);
1254        assert_eq!(partitioned.leaf[0].name.to_string(), "leaf_action");
1255        assert_eq!(partitioned.leaf[1].name.to_string(), "async_ref");
1256
1257        assert_eq!(partitioned.static_mounts.len(), 1);
1258        assert_eq!(
1259            partitioned.static_mounts[0].name.to_string(),
1260            "static_mount"
1261        );
1262
1263        assert_eq!(partitioned.slug_mounts.len(), 1);
1264        assert_eq!(partitioned.slug_mounts[0].name.to_string(), "slug_mount");
1265    }
1266
1267    #[test]
1268    fn partition_methods_respects_skip() {
1269        let impl_block: ItemImpl = syn::parse_quote! {
1270            impl Router {
1271                fn keep(&self) -> String { String::new() }
1272                fn skip_me(&self) -> String { String::new() }
1273            }
1274        };
1275        let methods = extract_methods(&impl_block).unwrap();
1276        let partitioned = partition_methods(&methods, |m| m.name == "skip_me");
1277
1278        assert_eq!(partitioned.leaf.len(), 1);
1279        assert_eq!(partitioned.leaf[0].name.to_string(), "keep");
1280    }
1281
1282    // ── get_impl_name ───────────────────────────────────────────────
1283
1284    #[test]
1285    fn get_impl_name_extracts_struct_name() {
1286        let impl_block: ItemImpl = syn::parse_quote! {
1287            impl MyService {
1288                fn hello(&self) {}
1289            }
1290        };
1291        let name = get_impl_name(&impl_block).unwrap();
1292        assert_eq!(name.to_string(), "MyService");
1293    }
1294
1295    #[test]
1296    fn get_impl_name_with_generics() {
1297        let impl_block: ItemImpl = syn::parse_quote! {
1298            impl MyService<T> {
1299                fn hello(&self) {}
1300            }
1301        };
1302        let name = get_impl_name(&impl_block).unwrap();
1303        assert_eq!(name.to_string(), "MyService");
1304    }
1305
1306    // ── ident_str ────────────────────────────────────────────────────
1307
1308    #[test]
1309    fn ident_str_strips_raw_prefix() {
1310        let ident: Ident = syn::parse_quote!(r#type);
1311        assert_eq!(ident_str(&ident), "type");
1312    }
1313
1314    #[test]
1315    fn ident_str_leaves_normal_ident_unchanged() {
1316        let ident: Ident = syn::parse_quote!(name);
1317        assert_eq!(ident_str(&ident), "name");
1318    }
1319
1320    #[test]
1321    fn name_str_strips_raw_prefix_on_param() {
1322        let method: ImplItemFn = syn::parse_quote! {
1323            fn get(&self, r#type: String) -> String { r#type }
1324        };
1325        let info = MethodInfo::parse(&method).unwrap().unwrap();
1326        assert_eq!(info.params[0].name_str(), "type");
1327        // The Ident itself still has the raw prefix for code generation
1328        assert_eq!(info.params[0].name.to_string(), "r#type");
1329    }
1330}