Skip to main content

server_less_parse/
lib.rs

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