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