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