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