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/// Parse #[param(...)] attributes from a parameter
308pub fn parse_param_attrs(attrs: &[syn::Attribute]) -> syn::Result<ParsedParamAttrs> {
309    let mut wire_name = None;
310    let mut location = None;
311    let mut default_value = None;
312    let mut short_flag = None;
313    let mut help_text = None;
314    let mut positional = false;
315
316    for attr in attrs {
317        if !attr.path().is_ident("param") {
318            continue;
319        }
320
321        attr.parse_nested_meta(|meta| {
322            // #[param(name = "...")]
323            if meta.path.is_ident("name") {
324                let value: syn::LitStr = meta.value()?.parse()?;
325                wire_name = Some(value.value());
326                Ok(())
327            }
328            // #[param(default = ...)]
329            else if meta.path.is_ident("default") {
330                // Accept various literal types
331                let value = meta.value()?;
332                let lookahead = value.lookahead1();
333                if lookahead.peek(syn::LitStr) {
334                    let lit: syn::LitStr = value.parse()?;
335                    default_value = Some(format!("\"{}\"", lit.value()));
336                } else if lookahead.peek(syn::LitInt) {
337                    let lit: syn::LitInt = value.parse()?;
338                    default_value = Some(lit.to_string());
339                } else if lookahead.peek(syn::LitBool) {
340                    let lit: syn::LitBool = value.parse()?;
341                    default_value = Some(lit.value.to_string());
342                } else {
343                    return Err(lookahead.error());
344                }
345                Ok(())
346            }
347            // #[param(query)] or #[param(path)] etc.
348            else if meta.path.is_ident("query") {
349                location = Some(ParamLocation::Query);
350                Ok(())
351            } else if meta.path.is_ident("path") {
352                location = Some(ParamLocation::Path);
353                Ok(())
354            } else if meta.path.is_ident("body") {
355                location = Some(ParamLocation::Body);
356                Ok(())
357            } else if meta.path.is_ident("header") {
358                location = Some(ParamLocation::Header);
359                Ok(())
360            }
361            // #[param(short = 'v')]
362            else if meta.path.is_ident("short") {
363                let value: syn::LitChar = meta.value()?.parse()?;
364                short_flag = Some(value.value());
365                Ok(())
366            }
367            // #[param(help = "description")]
368            else if meta.path.is_ident("help") {
369                let value: syn::LitStr = meta.value()?.parse()?;
370                help_text = Some(value.value());
371                Ok(())
372            }
373            // #[param(positional)]
374            else if meta.path.is_ident("positional") {
375                positional = true;
376                Ok(())
377            } else {
378                Err(meta.error(
379                    "unknown attribute\n\
380                     \n\
381                     Valid attributes: name, default, query, path, body, header, short, help, positional\n\
382                     \n\
383                     Examples:\n\
384                     - #[param(name = \"q\")]\n\
385                     - #[param(default = 10)]\n\
386                     - #[param(query)]\n\
387                     - #[param(header, name = \"X-API-Key\")]\n\
388                     - #[param(short = 'v')]\n\
389                     - #[param(help = \"Enable verbose output\")]\n\
390                     - #[param(positional)]",
391                ))
392            }
393        })?;
394    }
395
396    Ok(ParsedParamAttrs {
397        wire_name,
398        location,
399        default_value,
400        short_flag,
401        help_text,
402        positional,
403    })
404}
405
406/// Parse function parameters (excluding self)
407pub fn parse_params(
408    inputs: &syn::punctuated::Punctuated<FnArg, syn::Token![,]>,
409) -> syn::Result<Vec<ParamInfo>> {
410    let mut params = Vec::new();
411
412    for arg in inputs {
413        match arg {
414            FnArg::Receiver(_) => continue, // skip self
415            FnArg::Typed(pat_type) => {
416                let name = match pat_type.pat.as_ref() {
417                    Pat::Ident(pat_ident) => pat_ident.ident.clone(),
418                    other => {
419                        return Err(syn::Error::new_spanned(
420                            other,
421                            "unsupported parameter pattern\n\
422                             \n\
423                             Server-less macros require simple parameter names.\n\
424                             Use: name: String\n\
425                             Not: (name, _): (String, i32) or &name: &String",
426                        ));
427                    }
428                };
429
430                let ty = (*pat_type.ty).clone();
431                let is_optional = is_option_type(&ty);
432                let is_bool = is_bool_type(&ty);
433                let vec_inner = extract_vec_type(&ty);
434                let is_vec = vec_inner.is_some();
435                let is_id = is_id_param(&name);
436
437                // Parse #[param(...)] attributes
438                let parsed = parse_param_attrs(&pat_type.attrs)?;
439
440                // is_positional: explicit attribute takes priority, is_id heuristic as fallback
441                let is_positional = parsed.positional || is_id;
442
443                params.push(ParamInfo {
444                    name,
445                    ty,
446                    is_optional,
447                    is_bool,
448                    is_vec,
449                    vec_inner,
450                    is_id,
451                    is_positional,
452                    wire_name: parsed.wire_name,
453                    location: parsed.location,
454                    default_value: parsed.default_value,
455                    short_flag: parsed.short_flag,
456                    help_text: parsed.help_text,
457                });
458            }
459        }
460    }
461
462    Ok(params)
463}
464
465/// Parse return type information
466pub fn parse_return_type(output: &ReturnType) -> ReturnInfo {
467    match output {
468        ReturnType::Default => ReturnInfo {
469            ty: None,
470            ok_type: None,
471            err_type: None,
472            some_type: None,
473            is_result: false,
474            is_option: false,
475            is_unit: true,
476            is_stream: false,
477            stream_item: None,
478            is_iterator: false,
479            iterator_item: None,
480            is_reference: false,
481            reference_inner: None,
482        },
483        ReturnType::Type(_, ty) => {
484            let ty = ty.as_ref().clone();
485
486            // Check for Result<T, E>
487            if let Some((ok, err)) = extract_result_types(&ty) {
488                return ReturnInfo {
489                    ty: Some(ty),
490                    ok_type: Some(ok),
491                    err_type: Some(err),
492                    some_type: None,
493                    is_result: true,
494                    is_option: false,
495                    is_unit: false,
496                    is_stream: false,
497                    stream_item: None,
498                    is_iterator: false,
499                    iterator_item: None,
500                    is_reference: false,
501                    reference_inner: None,
502                };
503            }
504
505            // Check for Option<T>
506            if let Some(inner) = extract_option_type(&ty) {
507                return ReturnInfo {
508                    ty: Some(ty),
509                    ok_type: None,
510                    err_type: None,
511                    some_type: Some(inner),
512                    is_result: false,
513                    is_option: true,
514                    is_unit: false,
515                    is_stream: false,
516                    stream_item: None,
517                    is_iterator: false,
518                    iterator_item: None,
519                    is_reference: false,
520                    reference_inner: None,
521                };
522            }
523
524            // Check for impl Stream<Item=T>
525            if let Some(item) = extract_stream_item(&ty) {
526                return ReturnInfo {
527                    ty: Some(ty),
528                    ok_type: None,
529                    err_type: None,
530                    some_type: None,
531                    is_result: false,
532                    is_option: false,
533                    is_unit: false,
534                    is_stream: true,
535                    stream_item: Some(item),
536                    is_iterator: false,
537                    iterator_item: None,
538                    is_reference: false,
539                    reference_inner: None,
540                };
541            }
542
543            // Check for impl Iterator<Item=T>
544            if let Some(item) = extract_iterator_item(&ty) {
545                return ReturnInfo {
546                    ty: Some(ty),
547                    ok_type: None,
548                    err_type: None,
549                    some_type: None,
550                    is_result: false,
551                    is_option: false,
552                    is_unit: false,
553                    is_stream: false,
554                    stream_item: None,
555                    is_iterator: true,
556                    iterator_item: Some(item),
557                    is_reference: false,
558                    reference_inner: None,
559                };
560            }
561
562            // Check for ()
563            if is_unit_type(&ty) {
564                return ReturnInfo {
565                    ty: Some(ty),
566                    ok_type: None,
567                    err_type: None,
568                    some_type: None,
569                    is_result: false,
570                    is_option: false,
571                    is_unit: true,
572                    is_stream: false,
573                    stream_item: None,
574                    is_iterator: false,
575                    iterator_item: None,
576                    is_reference: false,
577                    reference_inner: None,
578                };
579            }
580
581            // Check for &T (reference return — mount point)
582            if let Type::Reference(TypeReference { elem, .. }) = &ty {
583                let inner = elem.as_ref().clone();
584                return ReturnInfo {
585                    ty: Some(ty),
586                    ok_type: None,
587                    err_type: None,
588                    some_type: None,
589                    is_result: false,
590                    is_option: false,
591                    is_unit: false,
592                    is_stream: false,
593                    stream_item: None,
594                    is_iterator: false,
595                    iterator_item: None,
596                    is_reference: true,
597                    reference_inner: Some(inner),
598                };
599            }
600
601            // Regular type
602            ReturnInfo {
603                ty: Some(ty),
604                ok_type: None,
605                err_type: None,
606                some_type: None,
607                is_result: false,
608                is_option: false,
609                is_unit: false,
610                is_stream: false,
611                stream_item: None,
612                is_iterator: false,
613                iterator_item: None,
614                is_reference: false,
615                reference_inner: None,
616            }
617        }
618    }
619}
620
621/// Check if a type is `bool`
622pub fn is_bool_type(ty: &Type) -> bool {
623    if let Type::Path(type_path) = ty
624        && let Some(segment) = type_path.path.segments.last()
625        && type_path.path.segments.len() == 1
626    {
627        return segment.ident == "bool";
628    }
629    false
630}
631
632/// Check if a type is `Vec<T>` and extract T
633pub fn extract_vec_type(ty: &Type) -> Option<Type> {
634    if let Type::Path(type_path) = ty
635        && let Some(segment) = type_path.path.segments.last()
636        && segment.ident == "Vec"
637        && let PathArguments::AngleBracketed(args) = &segment.arguments
638        && let Some(GenericArgument::Type(inner)) = args.args.first()
639    {
640        return Some(inner.clone());
641    }
642    None
643}
644
645/// Check if a type is `HashMap<K, V>` or `BTreeMap<K, V>` and extract K and V
646pub fn extract_map_type(ty: &Type) -> Option<(Type, Type)> {
647    if let Type::Path(type_path) = ty
648        && let Some(segment) = type_path.path.segments.last()
649        && (segment.ident == "HashMap" || segment.ident == "BTreeMap")
650        && let PathArguments::AngleBracketed(args) = &segment.arguments
651    {
652        let mut iter = args.args.iter();
653        if let (Some(GenericArgument::Type(key)), Some(GenericArgument::Type(val))) =
654            (iter.next(), iter.next())
655        {
656            return Some((key.clone(), val.clone()));
657        }
658    }
659    None
660}
661
662/// Check if a type is `Option<T>` and extract T
663pub fn extract_option_type(ty: &Type) -> Option<Type> {
664    if let Type::Path(type_path) = ty
665        && let Some(segment) = type_path.path.segments.last()
666        && segment.ident == "Option"
667        && let PathArguments::AngleBracketed(args) = &segment.arguments
668        && let Some(GenericArgument::Type(inner)) = args.args.first()
669    {
670        return Some(inner.clone());
671    }
672    None
673}
674
675/// Check if a type is `Option<T>`
676pub fn is_option_type(ty: &Type) -> bool {
677    extract_option_type(ty).is_some()
678}
679
680/// Check if a type is Result<T, E> and extract T and E
681pub fn extract_result_types(ty: &Type) -> Option<(Type, Type)> {
682    if let Type::Path(type_path) = ty
683        && let Some(segment) = type_path.path.segments.last()
684        && segment.ident == "Result"
685        && let PathArguments::AngleBracketed(args) = &segment.arguments
686    {
687        let mut iter = args.args.iter();
688        if let (Some(GenericArgument::Type(ok)), Some(GenericArgument::Type(err))) =
689            (iter.next(), iter.next())
690        {
691            return Some((ok.clone(), err.clone()));
692        }
693    }
694    None
695}
696
697/// Check if a type is impl Stream<Item=T> and extract T
698pub fn extract_stream_item(ty: &Type) -> Option<Type> {
699    if let Type::ImplTrait(impl_trait) = ty {
700        for bound in &impl_trait.bounds {
701            if let syn::TypeParamBound::Trait(trait_bound) = bound
702                && let Some(segment) = trait_bound.path.segments.last()
703                && segment.ident == "Stream"
704                && let PathArguments::AngleBracketed(args) = &segment.arguments
705            {
706                for arg in &args.args {
707                    if let GenericArgument::AssocType(assoc) = arg
708                        && assoc.ident == "Item"
709                    {
710                        return Some(assoc.ty.clone());
711                    }
712                }
713            }
714        }
715    }
716    None
717}
718
719/// Check if a type is impl Iterator<Item=T> and extract T
720pub fn extract_iterator_item(ty: &Type) -> Option<Type> {
721    if let Type::ImplTrait(impl_trait) = ty {
722        for bound in &impl_trait.bounds {
723            if let syn::TypeParamBound::Trait(trait_bound) = bound
724                && let Some(segment) = trait_bound.path.segments.last()
725                && segment.ident == "Iterator"
726                && let PathArguments::AngleBracketed(args) = &segment.arguments
727            {
728                for arg in &args.args {
729                    if let GenericArgument::AssocType(assoc) = arg
730                        && assoc.ident == "Item"
731                    {
732                        return Some(assoc.ty.clone());
733                    }
734                }
735            }
736        }
737    }
738    None
739}
740
741/// Check if a type is ()
742pub fn is_unit_type(ty: &Type) -> bool {
743    if let Type::Tuple(tuple) = ty {
744        return tuple.elems.is_empty();
745    }
746    false
747}
748
749/// Check if a parameter name looks like an ID
750pub fn is_id_param(name: &Ident) -> bool {
751    let name_str = name.to_string();
752    name_str == "id" || name_str.ends_with("_id")
753}
754
755/// Extract all methods from an impl block
756///
757/// Skips:
758/// - Private methods (starting with `_`)
759/// - Associated functions without `&self` receiver (constructors, etc.)
760pub fn extract_methods(impl_block: &ItemImpl) -> syn::Result<Vec<MethodInfo>> {
761    let mut methods = Vec::new();
762
763    for item in &impl_block.items {
764        if let ImplItem::Fn(method) = item {
765            // Skip private methods (those starting with _)
766            if method.sig.ident.to_string().starts_with('_') {
767                continue;
768            }
769            // Parse method - returns None for associated functions without self
770            if let Some(info) = MethodInfo::parse(method)? {
771                methods.push(info);
772            }
773        }
774    }
775
776    Ok(methods)
777}
778
779/// Categorized methods for code generation.
780///
781/// Methods returning `&T` (non-async) are mount points; everything else is a leaf.
782/// Mount points are further split by whether they take parameters (slug) or not (static).
783pub struct PartitionedMethods<'a> {
784    /// Regular leaf methods (no reference return).
785    pub leaf: Vec<&'a MethodInfo>,
786    /// Static mounts: `fn foo(&self) -> &T` (no params).
787    pub static_mounts: Vec<&'a MethodInfo>,
788    /// Slug mounts: `fn foo(&self, id: Id) -> &T` (has params).
789    pub slug_mounts: Vec<&'a MethodInfo>,
790}
791
792/// Partition methods into leaf commands, static mounts, and slug mounts.
793///
794/// The `skip` predicate allows each protocol to apply its own skip logic
795/// (e.g., `#[cli(skip)]`, `#[mcp(skip)]`).
796pub fn partition_methods<'a>(
797    methods: &'a [MethodInfo],
798    skip: impl Fn(&MethodInfo) -> bool,
799) -> PartitionedMethods<'a> {
800    let mut result = PartitionedMethods {
801        leaf: Vec::new(),
802        static_mounts: Vec::new(),
803        slug_mounts: Vec::new(),
804    };
805
806    for method in methods {
807        if skip(method) {
808            continue;
809        }
810
811        if method.return_info.is_reference && !method.is_async {
812            if method.params.is_empty() {
813                result.static_mounts.push(method);
814            } else {
815                result.slug_mounts.push(method);
816            }
817        } else {
818            result.leaf.push(method);
819        }
820    }
821
822    result
823}
824
825/// Get the struct name from an impl block
826pub fn get_impl_name(impl_block: &ItemImpl) -> syn::Result<Ident> {
827    if let Type::Path(type_path) = impl_block.self_ty.as_ref()
828        && let Some(segment) = type_path.path.segments.last()
829    {
830        return Ok(segment.ident.clone());
831    }
832    Err(syn::Error::new_spanned(
833        &impl_block.self_ty,
834        "Expected a simple type name",
835    ))
836}
837
838#[cfg(test)]
839mod tests {
840    use super::*;
841    use quote::quote;
842
843    // ── extract_docs ────────────────────────────────────────────────
844
845    #[test]
846    fn extract_docs_returns_none_when_no_doc_attrs() {
847        let method: ImplItemFn = syn::parse_quote! {
848            fn hello(&self) {}
849        };
850        assert!(extract_docs(&method.attrs).is_none());
851    }
852
853    #[test]
854    fn extract_docs_extracts_single_line() {
855        let method: ImplItemFn = syn::parse_quote! {
856            /// Hello world
857            fn hello(&self) {}
858        };
859        assert_eq!(extract_docs(&method.attrs).unwrap(), "Hello world");
860    }
861
862    #[test]
863    fn extract_docs_joins_multiple_lines() {
864        let method: ImplItemFn = syn::parse_quote! {
865            /// Line one
866            /// Line two
867            fn hello(&self) {}
868        };
869        assert_eq!(extract_docs(&method.attrs).unwrap(), "Line one\nLine two");
870    }
871
872    #[test]
873    fn extract_docs_ignores_non_doc_attrs() {
874        let method: ImplItemFn = syn::parse_quote! {
875            #[inline]
876            /// Documented
877            fn hello(&self) {}
878        };
879        assert_eq!(extract_docs(&method.attrs).unwrap(), "Documented");
880    }
881
882    // ── parse_return_type ───────────────────────────────────────────
883
884    #[test]
885    fn parse_return_type_default_is_unit() {
886        let ret: ReturnType = syn::parse_quote! {};
887        let info = parse_return_type(&ret);
888        assert!(info.is_unit);
889        assert!(info.ty.is_none());
890        assert!(!info.is_result);
891        assert!(!info.is_option);
892        assert!(!info.is_reference);
893    }
894
895    #[test]
896    fn parse_return_type_regular_type() {
897        let ret: ReturnType = syn::parse_quote! { -> String };
898        let info = parse_return_type(&ret);
899        assert!(!info.is_unit);
900        assert!(!info.is_result);
901        assert!(!info.is_option);
902        assert!(!info.is_reference);
903        assert!(info.ty.is_some());
904    }
905
906    #[test]
907    fn parse_return_type_result() {
908        let ret: ReturnType = syn::parse_quote! { -> Result<String, MyError> };
909        let info = parse_return_type(&ret);
910        assert!(info.is_result);
911        assert!(!info.is_option);
912        assert!(!info.is_unit);
913
914        let ok = info.ok_type.unwrap();
915        assert_eq!(quote!(#ok).to_string(), "String");
916
917        let err = info.err_type.unwrap();
918        assert_eq!(quote!(#err).to_string(), "MyError");
919    }
920
921    #[test]
922    fn parse_return_type_option() {
923        let ret: ReturnType = syn::parse_quote! { -> Option<i32> };
924        let info = parse_return_type(&ret);
925        assert!(info.is_option);
926        assert!(!info.is_result);
927        assert!(!info.is_unit);
928
929        let some = info.some_type.unwrap();
930        assert_eq!(quote!(#some).to_string(), "i32");
931    }
932
933    #[test]
934    fn parse_return_type_unit_tuple() {
935        let ret: ReturnType = syn::parse_quote! { -> () };
936        let info = parse_return_type(&ret);
937        assert!(info.is_unit);
938        assert!(info.ty.is_some());
939    }
940
941    #[test]
942    fn parse_return_type_reference() {
943        let ret: ReturnType = syn::parse_quote! { -> &SubRouter };
944        let info = parse_return_type(&ret);
945        assert!(info.is_reference);
946        assert!(!info.is_unit);
947
948        let inner = info.reference_inner.unwrap();
949        assert_eq!(quote!(#inner).to_string(), "SubRouter");
950    }
951
952    #[test]
953    fn parse_return_type_stream() {
954        let ret: ReturnType = syn::parse_quote! { -> impl Stream<Item = u64> };
955        let info = parse_return_type(&ret);
956        assert!(info.is_stream);
957        assert!(!info.is_result);
958
959        let item = info.stream_item.unwrap();
960        assert_eq!(quote!(#item).to_string(), "u64");
961    }
962
963    // ── is_option_type / extract_option_type ────────────────────────
964
965    #[test]
966    fn is_option_type_true() {
967        let ty: Type = syn::parse_quote! { Option<String> };
968        assert!(is_option_type(&ty));
969        let inner = extract_option_type(&ty).unwrap();
970        assert_eq!(quote!(#inner).to_string(), "String");
971    }
972
973    #[test]
974    fn is_option_type_false_for_non_option() {
975        let ty: Type = syn::parse_quote! { String };
976        assert!(!is_option_type(&ty));
977        assert!(extract_option_type(&ty).is_none());
978    }
979
980    // ── extract_result_types ────────────────────────────────────────
981
982    #[test]
983    fn extract_result_types_works() {
984        let ty: Type = syn::parse_quote! { Result<Vec<u8>, std::io::Error> };
985        let (ok, err) = extract_result_types(&ty).unwrap();
986        assert_eq!(quote!(#ok).to_string(), "Vec < u8 >");
987        assert_eq!(quote!(#err).to_string(), "std :: io :: Error");
988    }
989
990    #[test]
991    fn extract_result_types_none_for_non_result() {
992        let ty: Type = syn::parse_quote! { Option<i32> };
993        assert!(extract_result_types(&ty).is_none());
994    }
995
996    // ── is_unit_type ────────────────────────────────────────────────
997
998    #[test]
999    fn is_unit_type_true() {
1000        let ty: Type = syn::parse_quote! { () };
1001        assert!(is_unit_type(&ty));
1002    }
1003
1004    #[test]
1005    fn is_unit_type_false_for_non_tuple() {
1006        let ty: Type = syn::parse_quote! { String };
1007        assert!(!is_unit_type(&ty));
1008    }
1009
1010    #[test]
1011    fn is_unit_type_false_for_nonempty_tuple() {
1012        let ty: Type = syn::parse_quote! { (i32, i32) };
1013        assert!(!is_unit_type(&ty));
1014    }
1015
1016    // ── is_id_param ─────────────────────────────────────────────────
1017
1018    #[test]
1019    fn is_id_param_exact_id() {
1020        let ident: Ident = syn::parse_quote! { id };
1021        assert!(is_id_param(&ident));
1022    }
1023
1024    #[test]
1025    fn is_id_param_suffix_id() {
1026        let ident: Ident = syn::parse_quote! { user_id };
1027        assert!(is_id_param(&ident));
1028    }
1029
1030    #[test]
1031    fn is_id_param_false_for_other_names() {
1032        let ident: Ident = syn::parse_quote! { name };
1033        assert!(!is_id_param(&ident));
1034    }
1035
1036    #[test]
1037    fn is_id_param_false_for_identity() {
1038        // "identity" ends with "id" but not "_id"
1039        let ident: Ident = syn::parse_quote! { identity };
1040        assert!(!is_id_param(&ident));
1041    }
1042
1043    // ── MethodInfo::parse ───────────────────────────────────────────
1044
1045    #[test]
1046    fn method_info_parse_basic() {
1047        let method: ImplItemFn = syn::parse_quote! {
1048            /// Does a thing
1049            fn greet(&self, name: String) -> String {
1050                format!("Hello {name}")
1051            }
1052        };
1053        let info = MethodInfo::parse(&method).unwrap().unwrap();
1054        assert_eq!(info.name.to_string(), "greet");
1055        assert!(!info.is_async);
1056        assert_eq!(info.docs.as_deref(), Some("Does a thing"));
1057        assert_eq!(info.params.len(), 1);
1058        assert_eq!(info.params[0].name.to_string(), "name");
1059        assert!(!info.params[0].is_optional);
1060        assert!(!info.params[0].is_id);
1061    }
1062
1063    #[test]
1064    fn method_info_parse_async_method() {
1065        let method: ImplItemFn = syn::parse_quote! {
1066            async fn fetch(&self) -> Vec<u8> {
1067                vec![]
1068            }
1069        };
1070        let info = MethodInfo::parse(&method).unwrap().unwrap();
1071        assert!(info.is_async);
1072    }
1073
1074    #[test]
1075    fn method_info_parse_skips_associated_function() {
1076        let method: ImplItemFn = syn::parse_quote! {
1077            fn new() -> Self {
1078                Self
1079            }
1080        };
1081        assert!(MethodInfo::parse(&method).unwrap().is_none());
1082    }
1083
1084    #[test]
1085    fn method_info_parse_optional_param() {
1086        let method: ImplItemFn = syn::parse_quote! {
1087            fn search(&self, query: Option<String>) {}
1088        };
1089        let info = MethodInfo::parse(&method).unwrap().unwrap();
1090        assert!(info.params[0].is_optional);
1091    }
1092
1093    #[test]
1094    fn method_info_parse_id_param() {
1095        let method: ImplItemFn = syn::parse_quote! {
1096            fn get_user(&self, user_id: u64) -> String {
1097                String::new()
1098            }
1099        };
1100        let info = MethodInfo::parse(&method).unwrap().unwrap();
1101        assert!(info.params[0].is_id);
1102    }
1103
1104    #[test]
1105    fn method_info_parse_no_docs() {
1106        let method: ImplItemFn = syn::parse_quote! {
1107            fn bare(&self) {}
1108        };
1109        let info = MethodInfo::parse(&method).unwrap().unwrap();
1110        assert!(info.docs.is_none());
1111    }
1112
1113    // ── extract_methods ─────────────────────────────────────────────
1114
1115    #[test]
1116    fn extract_methods_basic() {
1117        let impl_block: ItemImpl = syn::parse_quote! {
1118            impl MyApi {
1119                fn hello(&self) -> String { String::new() }
1120                fn world(&self) -> String { String::new() }
1121            }
1122        };
1123        let methods = extract_methods(&impl_block).unwrap();
1124        assert_eq!(methods.len(), 2);
1125        assert_eq!(methods[0].name.to_string(), "hello");
1126        assert_eq!(methods[1].name.to_string(), "world");
1127    }
1128
1129    #[test]
1130    fn extract_methods_skips_underscore_prefix() {
1131        let impl_block: ItemImpl = syn::parse_quote! {
1132            impl MyApi {
1133                fn public(&self) {}
1134                fn _private(&self) {}
1135                fn __also_private(&self) {}
1136            }
1137        };
1138        let methods = extract_methods(&impl_block).unwrap();
1139        assert_eq!(methods.len(), 1);
1140        assert_eq!(methods[0].name.to_string(), "public");
1141    }
1142
1143    #[test]
1144    fn extract_methods_skips_associated_functions() {
1145        let impl_block: ItemImpl = syn::parse_quote! {
1146            impl MyApi {
1147                fn new() -> Self { Self }
1148                fn from_config(cfg: Config) -> Self { Self }
1149                fn greet(&self) -> String { String::new() }
1150            }
1151        };
1152        let methods = extract_methods(&impl_block).unwrap();
1153        assert_eq!(methods.len(), 1);
1154        assert_eq!(methods[0].name.to_string(), "greet");
1155    }
1156
1157    // ── partition_methods ───────────────────────────────────────────
1158
1159    #[test]
1160    fn partition_methods_splits_correctly() {
1161        let impl_block: ItemImpl = syn::parse_quote! {
1162            impl Router {
1163                fn leaf_action(&self) -> String { String::new() }
1164                fn static_mount(&self) -> &SubRouter { &self.sub }
1165                fn slug_mount(&self, id: u64) -> &SubRouter { &self.sub }
1166                async fn async_ref(&self) -> &SubRouter { &self.sub }
1167            }
1168        };
1169        let methods = extract_methods(&impl_block).unwrap();
1170        let partitioned = partition_methods(&methods, |_| false);
1171
1172        // leaf_action and async_ref (async reference returns are leaf, not mounts)
1173        assert_eq!(partitioned.leaf.len(), 2);
1174        assert_eq!(partitioned.leaf[0].name.to_string(), "leaf_action");
1175        assert_eq!(partitioned.leaf[1].name.to_string(), "async_ref");
1176
1177        assert_eq!(partitioned.static_mounts.len(), 1);
1178        assert_eq!(
1179            partitioned.static_mounts[0].name.to_string(),
1180            "static_mount"
1181        );
1182
1183        assert_eq!(partitioned.slug_mounts.len(), 1);
1184        assert_eq!(partitioned.slug_mounts[0].name.to_string(), "slug_mount");
1185    }
1186
1187    #[test]
1188    fn partition_methods_respects_skip() {
1189        let impl_block: ItemImpl = syn::parse_quote! {
1190            impl Router {
1191                fn keep(&self) -> String { String::new() }
1192                fn skip_me(&self) -> String { String::new() }
1193            }
1194        };
1195        let methods = extract_methods(&impl_block).unwrap();
1196        let partitioned = partition_methods(&methods, |m| m.name == "skip_me");
1197
1198        assert_eq!(partitioned.leaf.len(), 1);
1199        assert_eq!(partitioned.leaf[0].name.to_string(), "keep");
1200    }
1201
1202    // ── get_impl_name ───────────────────────────────────────────────
1203
1204    #[test]
1205    fn get_impl_name_extracts_struct_name() {
1206        let impl_block: ItemImpl = syn::parse_quote! {
1207            impl MyService {
1208                fn hello(&self) {}
1209            }
1210        };
1211        let name = get_impl_name(&impl_block).unwrap();
1212        assert_eq!(name.to_string(), "MyService");
1213    }
1214
1215    #[test]
1216    fn get_impl_name_with_generics() {
1217        let impl_block: ItemImpl = syn::parse_quote! {
1218            impl MyService<T> {
1219                fn hello(&self) {}
1220            }
1221        };
1222        let name = get_impl_name(&impl_block).unwrap();
1223        assert_eq!(name.to_string(), "MyService");
1224    }
1225}