1use syn::{
7 FnArg, GenericArgument, Ident, ImplItem, ImplItemFn, ItemImpl, Lit, Meta, Pat, PathArguments,
8 ReturnType, Type, TypeReference,
9};
10
11#[derive(Debug, Clone)]
20pub struct MethodInfo {
21 pub method: ImplItemFn,
23 pub name: Ident,
25 pub docs: Option<String>,
27 pub params: Vec<ParamInfo>,
29 pub return_info: ReturnInfo,
31 pub is_async: bool,
33 pub group: Option<String>,
35 pub wire_name: Option<String>,
38}
39
40#[derive(Debug, Clone)]
45pub struct GroupRegistry {
46 pub groups: Vec<(String, String)>,
49}
50
51#[derive(Debug, Clone)]
53pub struct ParamInfo {
54 pub name: Ident,
56 pub ty: Type,
58 pub is_optional: bool,
60 pub is_bool: bool,
62 pub is_vec: bool,
64 pub vec_inner: Option<Type>,
66 pub is_id: bool,
68 pub wire_name: Option<String>,
70 pub location: Option<ParamLocation>,
72 pub default_value: Option<String>,
74 pub short_flag: Option<char>,
76 pub help_text: Option<String>,
78 pub is_positional: bool,
80}
81
82impl MethodInfo {
83 pub fn name_str(&self) -> String {
89 ident_str(&self.name)
90 }
91
92 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 pub fn name_str(&self) -> String {
120 ident_str(&self.name)
121 }
122}
123
124pub 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#[derive(Debug, Clone, PartialEq)]
136pub enum ParamLocation {
137 Query,
138 Path,
139 Body,
140 Header,
141}
142
143#[derive(Debug, Clone)]
145pub struct ReturnInfo {
146 pub ty: Option<Type>,
148 pub ok_type: Option<Type>,
150 pub err_type: Option<Type>,
152 pub some_type: Option<Type>,
154 pub is_result: bool,
156 pub is_option: bool,
158 pub is_unit: bool,
160 pub is_stream: bool,
162 pub stream_item: Option<Type>,
164 pub is_iterator: bool,
166 pub iterator_item: Option<Type>,
168 pub is_reference: bool,
170 pub reference_inner: Option<Type>,
172}
173
174impl MethodInfo {
175 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 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 let docs = extract_docs(&method.attrs);
194
195 let params = parse_params(&method.sig.inputs)?;
197
198 let return_info = parse_return_type(&method.sig.output);
200
201 let group = extract_server_group(&method.attrs);
203
204 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
220pub 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
244const PROTOCOL_ATTRS: &[&str] = &[
246 "server", "cli", "http", "mcp", "jsonrpc", "grpc", "ws", "graphql", "tool",
247];
248
249fn 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
280fn 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 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
304pub 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
343pub 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 ®.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#[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 pub env_var: Option<String>,
402 pub file_key: Option<String>,
404 pub nested: bool,
410 pub env_prefix: Option<String>,
415 pub nested_serde: bool,
423}
424
425#[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
451fn 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
463pub 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 if meta.path.is_ident("name") {
485 let value: syn::LitStr = meta.value()?.parse()?;
486 wire_name = Some(value.value());
487 Ok(())
488 }
489 else if meta.path.is_ident("default") {
491 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 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 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 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 else if meta.path.is_ident("positional") {
536 positional = true;
537 Ok(())
538 }
539 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 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 else if meta.path.is_ident("nested") {
553 nested = true;
554 Ok(())
555 }
556 else if meta.path.is_ident("serde") {
558 nested_serde = true;
559 Ok(())
560 }
561 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
617pub 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, 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 let parsed = parse_param_attrs(&pat_type.attrs)?;
650
651 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
676pub 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 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 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 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 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 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 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 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
832pub 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
843pub 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
856pub 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
873pub 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
886pub fn is_option_type(ty: &Type) -> bool {
888 extract_option_type(ty).is_some()
889}
890
891pub 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
908pub 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
930pub 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
952pub 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
960pub 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
966pub 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 if method.sig.ident.to_string().starts_with('_') {
978 continue;
979 }
980 if let Some(info) = MethodInfo::parse(method)? {
982 methods.push(info);
983 }
984 }
985 }
986
987 Ok(methods)
988}
989
990pub struct PartitionedMethods<'a> {
995 pub leaf: Vec<&'a MethodInfo>,
997 pub static_mounts: Vec<&'a MethodInfo>,
999 pub slug_mounts: Vec<&'a MethodInfo>,
1001}
1002
1003pub 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
1036pub 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 #[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 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 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 fn hello(&self) {}
1089 };
1090 assert_eq!(extract_docs(&method.attrs).unwrap(), "Documented");
1091 }
1092
1093 #[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 #[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 #[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 #[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 #[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 let ident: Ident = syn::parse_quote! { identity };
1251 assert!(!is_id_param(&ident));
1252 }
1253
1254 #[test]
1257 fn method_info_parse_basic() {
1258 let method: ImplItemFn = syn::parse_quote! {
1259 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 #[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 #[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 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 #[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 #[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 assert_eq!(info.params[0].name.to_string(), "r#type");
1460 }
1461}