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