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