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}
36
37#[derive(Debug, Clone)]
42pub struct GroupRegistry {
43 pub groups: Vec<(String, String)>,
46}
47
48#[derive(Debug, Clone)]
50pub struct ParamInfo {
51 pub name: Ident,
53 pub ty: Type,
55 pub is_optional: bool,
57 pub is_bool: bool,
59 pub is_vec: bool,
61 pub vec_inner: Option<Type>,
63 pub is_id: bool,
65 pub wire_name: Option<String>,
67 pub location: Option<ParamLocation>,
69 pub default_value: Option<String>,
71 pub short_flag: Option<char>,
73 pub help_text: Option<String>,
75 pub is_positional: bool,
77}
78
79#[derive(Debug, Clone, PartialEq)]
81pub enum ParamLocation {
82 Query,
83 Path,
84 Body,
85 Header,
86}
87
88#[derive(Debug, Clone)]
90pub struct ReturnInfo {
91 pub ty: Option<Type>,
93 pub ok_type: Option<Type>,
95 pub err_type: Option<Type>,
97 pub some_type: Option<Type>,
99 pub is_result: bool,
101 pub is_option: bool,
103 pub is_unit: bool,
105 pub is_stream: bool,
107 pub stream_item: Option<Type>,
109 pub is_iterator: bool,
111 pub iterator_item: Option<Type>,
113 pub is_reference: bool,
115 pub reference_inner: Option<Type>,
117}
118
119impl MethodInfo {
120 pub fn parse(method: &ImplItemFn) -> syn::Result<Option<Self>> {
124 let name = method.sig.ident.clone();
125 let is_async = method.sig.asyncness.is_some();
126
127 let has_receiver = method
129 .sig
130 .inputs
131 .iter()
132 .any(|arg| matches!(arg, FnArg::Receiver(_)));
133 if !has_receiver {
134 return Ok(None);
135 }
136
137 let docs = extract_docs(&method.attrs);
139
140 let params = parse_params(&method.sig.inputs)?;
142
143 let return_info = parse_return_type(&method.sig.output);
145
146 let group = extract_server_group(&method.attrs);
148
149 Ok(Some(Self {
150 method: method.clone(),
151 name,
152 docs,
153 params,
154 return_info,
155 is_async,
156 group,
157 }))
158 }
159}
160
161pub fn extract_docs(attrs: &[syn::Attribute]) -> Option<String> {
163 let docs: Vec<String> = attrs
164 .iter()
165 .filter_map(|attr| {
166 if attr.path().is_ident("doc")
167 && let Meta::NameValue(meta) = &attr.meta
168 && let syn::Expr::Lit(syn::ExprLit {
169 lit: Lit::Str(s), ..
170 }) = &meta.value
171 {
172 return Some(s.value().trim().to_string());
173 }
174 None
175 })
176 .collect();
177
178 if docs.is_empty() {
179 None
180 } else {
181 Some(docs.join("\n"))
182 }
183}
184
185fn extract_server_group(attrs: &[syn::Attribute]) -> Option<String> {
187 for attr in attrs {
188 if attr.path().is_ident("server") {
189 let mut group = None;
190 let _ = attr.parse_nested_meta(|meta| {
191 if meta.path.is_ident("group") {
192 let value = meta.value()?;
193 let s: syn::LitStr = value.parse()?;
194 group = Some(s.value());
195 } else if meta.input.peek(syn::Token![=]) {
196 let _: proc_macro2::TokenStream = meta.value()?.parse()?;
198 }
199 Ok(())
200 });
201 if group.is_some() {
202 return group;
203 }
204 }
205 }
206 None
207}
208
209pub fn extract_groups(impl_block: &ItemImpl) -> syn::Result<Option<GroupRegistry>> {
214 for attr in &impl_block.attrs {
215 if attr.path().is_ident("server") {
216 let mut groups = Vec::new();
217 let mut found_groups = false;
218 attr.parse_nested_meta(|meta| {
219 if meta.path.is_ident("groups") {
220 found_groups = true;
221 meta.parse_nested_meta(|inner| {
222 let id = inner
223 .path
224 .get_ident()
225 .ok_or_else(|| inner.error("expected group identifier"))?
226 .to_string();
227 let value = inner.value()?;
228 let display: syn::LitStr = value.parse()?;
229 groups.push((id, display.value()));
230 Ok(())
231 })?;
232 } else if meta.input.peek(syn::Token![=]) {
233 let _: proc_macro2::TokenStream = meta.value()?.parse()?;
234 } else if meta.input.peek(syn::token::Paren) {
235 let _content;
236 syn::parenthesized!(_content in meta.input);
237 }
238 Ok(())
239 })?;
240 if found_groups {
241 return Ok(Some(GroupRegistry { groups }));
242 }
243 }
244 }
245 Ok(None)
246}
247
248pub fn resolve_method_group(
256 method: &MethodInfo,
257 registry: &Option<GroupRegistry>,
258) -> syn::Result<Option<String>> {
259 let group_value = match &method.group {
260 Some(v) => v,
261 None => return Ok(None),
262 };
263
264 let span = method.method.sig.ident.span();
265
266 match registry {
267 Some(reg) => {
268 for (id, display) in ®.groups {
269 if id == group_value {
270 return Ok(Some(display.clone()));
271 }
272 }
273 Err(syn::Error::new(
274 span,
275 format!(
276 "unknown group `{group_value}`; declared groups are: {}",
277 reg.groups
278 .iter()
279 .map(|(id, _)| format!("`{id}`"))
280 .collect::<Vec<_>>()
281 .join(", ")
282 ),
283 ))
284 }
285 None => Err(syn::Error::new(
286 span,
287 format!(
288 "method has `group = \"{group_value}\"` but no `groups(...)` registry is declared on the impl block\n\
289 \n\
290 help: add `#[server(groups({group_value} = \"Display Name\"))]` to the impl block"
291 ),
292 )),
293 }
294}
295
296#[derive(Debug, Clone, Default)]
298pub struct ParsedParamAttrs {
299 pub wire_name: Option<String>,
300 pub location: Option<ParamLocation>,
301 pub default_value: Option<String>,
302 pub short_flag: Option<char>,
303 pub help_text: Option<String>,
304 pub positional: bool,
305}
306
307fn levenshtein(a: &str, b: &str) -> usize {
309 let a: Vec<char> = a.chars().collect();
310 let b: Vec<char> = b.chars().collect();
311 let m = a.len();
312 let n = b.len();
313 let mut dp = vec![vec![0usize; n + 1]; m + 1];
314 for i in 0..=m {
315 dp[i][0] = i;
316 }
317 for j in 0..=n {
318 dp[0][j] = j;
319 }
320 for i in 1..=m {
321 for j in 1..=n {
322 dp[i][j] = if a[i - 1] == b[j - 1] {
323 dp[i - 1][j - 1]
324 } else {
325 1 + dp[i - 1][j - 1].min(dp[i - 1][j]).min(dp[i][j - 1])
326 };
327 }
328 }
329 dp[m][n]
330}
331
332fn did_you_mean<'a>(input: &str, candidates: &[&'a str]) -> Option<&'a str> {
334 candidates
335 .iter()
336 .filter_map(|&c| {
337 let d = levenshtein(input, c);
338 if d <= 2 { Some((d, c)) } else { None }
339 })
340 .min_by_key(|&(d, _)| d)
341 .map(|(_, c)| c)
342}
343
344pub fn parse_param_attrs(attrs: &[syn::Attribute]) -> syn::Result<ParsedParamAttrs> {
346 let mut wire_name = None;
347 let mut location = None;
348 let mut default_value = None;
349 let mut short_flag = None;
350 let mut help_text = None;
351 let mut positional = false;
352
353 for attr in attrs {
354 if !attr.path().is_ident("param") {
355 continue;
356 }
357
358 attr.parse_nested_meta(|meta| {
359 if meta.path.is_ident("name") {
361 let value: syn::LitStr = meta.value()?.parse()?;
362 wire_name = Some(value.value());
363 Ok(())
364 }
365 else if meta.path.is_ident("default") {
367 let value = meta.value()?;
369 let lookahead = value.lookahead1();
370 if lookahead.peek(syn::LitStr) {
371 let lit: syn::LitStr = value.parse()?;
372 default_value = Some(format!("\"{}\"", lit.value()));
373 } else if lookahead.peek(syn::LitInt) {
374 let lit: syn::LitInt = value.parse()?;
375 default_value = Some(lit.to_string());
376 } else if lookahead.peek(syn::LitBool) {
377 let lit: syn::LitBool = value.parse()?;
378 default_value = Some(lit.value.to_string());
379 } else {
380 return Err(lookahead.error());
381 }
382 Ok(())
383 }
384 else if meta.path.is_ident("query") {
386 location = Some(ParamLocation::Query);
387 Ok(())
388 } else if meta.path.is_ident("path") {
389 location = Some(ParamLocation::Path);
390 Ok(())
391 } else if meta.path.is_ident("body") {
392 location = Some(ParamLocation::Body);
393 Ok(())
394 } else if meta.path.is_ident("header") {
395 location = Some(ParamLocation::Header);
396 Ok(())
397 }
398 else if meta.path.is_ident("short") {
400 let value: syn::LitChar = meta.value()?.parse()?;
401 short_flag = Some(value.value());
402 Ok(())
403 }
404 else if meta.path.is_ident("help") {
406 let value: syn::LitStr = meta.value()?.parse()?;
407 help_text = Some(value.value());
408 Ok(())
409 }
410 else if meta.path.is_ident("positional") {
412 positional = true;
413 Ok(())
414 } else {
415 const VALID: &[&str] = &[
416 "name", "default", "query", "path", "body", "header", "short", "help",
417 "positional",
418 ];
419 let unknown = meta
420 .path
421 .get_ident()
422 .map(|i| i.to_string())
423 .unwrap_or_default();
424 let suggestion = did_you_mean(&unknown, VALID)
425 .map(|s| format!(" — did you mean `{s}`?"))
426 .unwrap_or_default();
427 Err(meta.error(format!(
428 "unknown attribute `{unknown}`{suggestion}\n\
429 \n\
430 Valid attributes: name, default, query, path, body, header, short, help, positional\n\
431 \n\
432 Examples:\n\
433 - #[param(name = \"q\")]\n\
434 - #[param(default = 10)]\n\
435 - #[param(query)]\n\
436 - #[param(header, name = \"X-API-Key\")]\n\
437 - #[param(short = 'v')]\n\
438 - #[param(help = \"Enable verbose output\")]\n\
439 - #[param(positional)]"
440 )))
441 }
442 })?;
443 }
444
445 Ok(ParsedParamAttrs {
446 wire_name,
447 location,
448 default_value,
449 short_flag,
450 help_text,
451 positional,
452 })
453}
454
455pub fn parse_params(
457 inputs: &syn::punctuated::Punctuated<FnArg, syn::Token![,]>,
458) -> syn::Result<Vec<ParamInfo>> {
459 let mut params = Vec::new();
460
461 for arg in inputs {
462 match arg {
463 FnArg::Receiver(_) => continue, FnArg::Typed(pat_type) => {
465 let name = match pat_type.pat.as_ref() {
466 Pat::Ident(pat_ident) => pat_ident.ident.clone(),
467 other => {
468 return Err(syn::Error::new_spanned(
469 other,
470 "unsupported parameter pattern\n\
471 \n\
472 Server-less macros require simple parameter names.\n\
473 Use: name: String\n\
474 Not: (name, _): (String, i32) or &name: &String",
475 ));
476 }
477 };
478
479 let ty = (*pat_type.ty).clone();
480 let is_optional = is_option_type(&ty);
481 let is_bool = is_bool_type(&ty);
482 let vec_inner = extract_vec_type(&ty);
483 let is_vec = vec_inner.is_some();
484 let is_id = is_id_param(&name);
485
486 let parsed = parse_param_attrs(&pat_type.attrs)?;
488
489 let is_positional = parsed.positional || is_id;
491
492 params.push(ParamInfo {
493 name,
494 ty,
495 is_optional,
496 is_bool,
497 is_vec,
498 vec_inner,
499 is_id,
500 is_positional,
501 wire_name: parsed.wire_name,
502 location: parsed.location,
503 default_value: parsed.default_value,
504 short_flag: parsed.short_flag,
505 help_text: parsed.help_text,
506 });
507 }
508 }
509 }
510
511 Ok(params)
512}
513
514pub fn parse_return_type(output: &ReturnType) -> ReturnInfo {
516 match output {
517 ReturnType::Default => ReturnInfo {
518 ty: None,
519 ok_type: None,
520 err_type: None,
521 some_type: None,
522 is_result: false,
523 is_option: false,
524 is_unit: true,
525 is_stream: false,
526 stream_item: None,
527 is_iterator: false,
528 iterator_item: None,
529 is_reference: false,
530 reference_inner: None,
531 },
532 ReturnType::Type(_, ty) => {
533 let ty = ty.as_ref().clone();
534
535 if let Some((ok, err)) = extract_result_types(&ty) {
537 return ReturnInfo {
538 ty: Some(ty),
539 ok_type: Some(ok),
540 err_type: Some(err),
541 some_type: None,
542 is_result: true,
543 is_option: false,
544 is_unit: false,
545 is_stream: false,
546 stream_item: None,
547 is_iterator: false,
548 iterator_item: None,
549 is_reference: false,
550 reference_inner: None,
551 };
552 }
553
554 if let Some(inner) = extract_option_type(&ty) {
556 return ReturnInfo {
557 ty: Some(ty),
558 ok_type: None,
559 err_type: None,
560 some_type: Some(inner),
561 is_result: false,
562 is_option: true,
563 is_unit: false,
564 is_stream: false,
565 stream_item: None,
566 is_iterator: false,
567 iterator_item: None,
568 is_reference: false,
569 reference_inner: None,
570 };
571 }
572
573 if let Some(item) = extract_stream_item(&ty) {
575 return ReturnInfo {
576 ty: Some(ty),
577 ok_type: None,
578 err_type: None,
579 some_type: None,
580 is_result: false,
581 is_option: false,
582 is_unit: false,
583 is_stream: true,
584 stream_item: Some(item),
585 is_iterator: false,
586 iterator_item: None,
587 is_reference: false,
588 reference_inner: None,
589 };
590 }
591
592 if let Some(item) = extract_iterator_item(&ty) {
594 return ReturnInfo {
595 ty: Some(ty),
596 ok_type: None,
597 err_type: None,
598 some_type: None,
599 is_result: false,
600 is_option: false,
601 is_unit: false,
602 is_stream: false,
603 stream_item: None,
604 is_iterator: true,
605 iterator_item: Some(item),
606 is_reference: false,
607 reference_inner: None,
608 };
609 }
610
611 if is_unit_type(&ty) {
613 return ReturnInfo {
614 ty: Some(ty),
615 ok_type: None,
616 err_type: None,
617 some_type: None,
618 is_result: false,
619 is_option: false,
620 is_unit: true,
621 is_stream: false,
622 stream_item: None,
623 is_iterator: false,
624 iterator_item: None,
625 is_reference: false,
626 reference_inner: None,
627 };
628 }
629
630 if let Type::Reference(TypeReference { elem, .. }) = &ty {
632 let inner = elem.as_ref().clone();
633 return ReturnInfo {
634 ty: Some(ty),
635 ok_type: None,
636 err_type: None,
637 some_type: None,
638 is_result: false,
639 is_option: false,
640 is_unit: false,
641 is_stream: false,
642 stream_item: None,
643 is_iterator: false,
644 iterator_item: None,
645 is_reference: true,
646 reference_inner: Some(inner),
647 };
648 }
649
650 ReturnInfo {
652 ty: Some(ty),
653 ok_type: None,
654 err_type: None,
655 some_type: None,
656 is_result: false,
657 is_option: false,
658 is_unit: false,
659 is_stream: false,
660 stream_item: None,
661 is_iterator: false,
662 iterator_item: None,
663 is_reference: false,
664 reference_inner: None,
665 }
666 }
667 }
668}
669
670pub fn is_bool_type(ty: &Type) -> bool {
672 if let Type::Path(type_path) = ty
673 && let Some(segment) = type_path.path.segments.last()
674 && type_path.path.segments.len() == 1
675 {
676 return segment.ident == "bool";
677 }
678 false
679}
680
681pub fn extract_vec_type(ty: &Type) -> Option<Type> {
683 if let Type::Path(type_path) = ty
684 && let Some(segment) = type_path.path.segments.last()
685 && segment.ident == "Vec"
686 && let PathArguments::AngleBracketed(args) = &segment.arguments
687 && let Some(GenericArgument::Type(inner)) = args.args.first()
688 {
689 return Some(inner.clone());
690 }
691 None
692}
693
694pub fn extract_map_type(ty: &Type) -> Option<(Type, Type)> {
696 if let Type::Path(type_path) = ty
697 && let Some(segment) = type_path.path.segments.last()
698 && (segment.ident == "HashMap" || segment.ident == "BTreeMap")
699 && let PathArguments::AngleBracketed(args) = &segment.arguments
700 {
701 let mut iter = args.args.iter();
702 if let (Some(GenericArgument::Type(key)), Some(GenericArgument::Type(val))) =
703 (iter.next(), iter.next())
704 {
705 return Some((key.clone(), val.clone()));
706 }
707 }
708 None
709}
710
711pub fn extract_option_type(ty: &Type) -> Option<Type> {
713 if let Type::Path(type_path) = ty
714 && let Some(segment) = type_path.path.segments.last()
715 && segment.ident == "Option"
716 && let PathArguments::AngleBracketed(args) = &segment.arguments
717 && let Some(GenericArgument::Type(inner)) = args.args.first()
718 {
719 return Some(inner.clone());
720 }
721 None
722}
723
724pub fn is_option_type(ty: &Type) -> bool {
726 extract_option_type(ty).is_some()
727}
728
729pub fn extract_result_types(ty: &Type) -> Option<(Type, Type)> {
731 if let Type::Path(type_path) = ty
732 && let Some(segment) = type_path.path.segments.last()
733 && segment.ident == "Result"
734 && let PathArguments::AngleBracketed(args) = &segment.arguments
735 {
736 let mut iter = args.args.iter();
737 if let (Some(GenericArgument::Type(ok)), Some(GenericArgument::Type(err))) =
738 (iter.next(), iter.next())
739 {
740 return Some((ok.clone(), err.clone()));
741 }
742 }
743 None
744}
745
746pub fn extract_stream_item(ty: &Type) -> Option<Type> {
748 if let Type::ImplTrait(impl_trait) = ty {
749 for bound in &impl_trait.bounds {
750 if let syn::TypeParamBound::Trait(trait_bound) = bound
751 && let Some(segment) = trait_bound.path.segments.last()
752 && segment.ident == "Stream"
753 && let PathArguments::AngleBracketed(args) = &segment.arguments
754 {
755 for arg in &args.args {
756 if let GenericArgument::AssocType(assoc) = arg
757 && assoc.ident == "Item"
758 {
759 return Some(assoc.ty.clone());
760 }
761 }
762 }
763 }
764 }
765 None
766}
767
768pub fn extract_iterator_item(ty: &Type) -> Option<Type> {
770 if let Type::ImplTrait(impl_trait) = ty {
771 for bound in &impl_trait.bounds {
772 if let syn::TypeParamBound::Trait(trait_bound) = bound
773 && let Some(segment) = trait_bound.path.segments.last()
774 && segment.ident == "Iterator"
775 && let PathArguments::AngleBracketed(args) = &segment.arguments
776 {
777 for arg in &args.args {
778 if let GenericArgument::AssocType(assoc) = arg
779 && assoc.ident == "Item"
780 {
781 return Some(assoc.ty.clone());
782 }
783 }
784 }
785 }
786 }
787 None
788}
789
790pub fn is_unit_type(ty: &Type) -> bool {
792 if let Type::Tuple(tuple) = ty {
793 return tuple.elems.is_empty();
794 }
795 false
796}
797
798pub fn is_id_param(name: &Ident) -> bool {
800 let name_str = name.to_string();
801 name_str == "id" || name_str.ends_with("_id")
802}
803
804pub fn extract_methods(impl_block: &ItemImpl) -> syn::Result<Vec<MethodInfo>> {
810 let mut methods = Vec::new();
811
812 for item in &impl_block.items {
813 if let ImplItem::Fn(method) = item {
814 if method.sig.ident.to_string().starts_with('_') {
816 continue;
817 }
818 if let Some(info) = MethodInfo::parse(method)? {
820 methods.push(info);
821 }
822 }
823 }
824
825 Ok(methods)
826}
827
828pub struct PartitionedMethods<'a> {
833 pub leaf: Vec<&'a MethodInfo>,
835 pub static_mounts: Vec<&'a MethodInfo>,
837 pub slug_mounts: Vec<&'a MethodInfo>,
839}
840
841pub fn partition_methods<'a>(
846 methods: &'a [MethodInfo],
847 skip: impl Fn(&MethodInfo) -> bool,
848) -> PartitionedMethods<'a> {
849 let mut result = PartitionedMethods {
850 leaf: Vec::new(),
851 static_mounts: Vec::new(),
852 slug_mounts: Vec::new(),
853 };
854
855 for method in methods {
856 if skip(method) {
857 continue;
858 }
859
860 if method.return_info.is_reference && !method.is_async {
861 if method.params.is_empty() {
862 result.static_mounts.push(method);
863 } else {
864 result.slug_mounts.push(method);
865 }
866 } else {
867 result.leaf.push(method);
868 }
869 }
870
871 result
872}
873
874pub fn get_impl_name(impl_block: &ItemImpl) -> syn::Result<Ident> {
876 if let Type::Path(type_path) = impl_block.self_ty.as_ref()
877 && let Some(segment) = type_path.path.segments.last()
878 {
879 return Ok(segment.ident.clone());
880 }
881 Err(syn::Error::new_spanned(
882 &impl_block.self_ty,
883 "Expected a simple type name",
884 ))
885}
886
887#[cfg(test)]
888mod tests {
889 use super::*;
890 use quote::quote;
891
892 #[test]
895 fn extract_docs_returns_none_when_no_doc_attrs() {
896 let method: ImplItemFn = syn::parse_quote! {
897 fn hello(&self) {}
898 };
899 assert!(extract_docs(&method.attrs).is_none());
900 }
901
902 #[test]
903 fn extract_docs_extracts_single_line() {
904 let method: ImplItemFn = syn::parse_quote! {
905 fn hello(&self) {}
907 };
908 assert_eq!(extract_docs(&method.attrs).unwrap(), "Hello world");
909 }
910
911 #[test]
912 fn extract_docs_joins_multiple_lines() {
913 let method: ImplItemFn = syn::parse_quote! {
914 fn hello(&self) {}
917 };
918 assert_eq!(extract_docs(&method.attrs).unwrap(), "Line one\nLine two");
919 }
920
921 #[test]
922 fn extract_docs_ignores_non_doc_attrs() {
923 let method: ImplItemFn = syn::parse_quote! {
924 #[inline]
925 fn hello(&self) {}
927 };
928 assert_eq!(extract_docs(&method.attrs).unwrap(), "Documented");
929 }
930
931 #[test]
934 fn parse_return_type_default_is_unit() {
935 let ret: ReturnType = syn::parse_quote! {};
936 let info = parse_return_type(&ret);
937 assert!(info.is_unit);
938 assert!(info.ty.is_none());
939 assert!(!info.is_result);
940 assert!(!info.is_option);
941 assert!(!info.is_reference);
942 }
943
944 #[test]
945 fn parse_return_type_regular_type() {
946 let ret: ReturnType = syn::parse_quote! { -> String };
947 let info = parse_return_type(&ret);
948 assert!(!info.is_unit);
949 assert!(!info.is_result);
950 assert!(!info.is_option);
951 assert!(!info.is_reference);
952 assert!(info.ty.is_some());
953 }
954
955 #[test]
956 fn parse_return_type_result() {
957 let ret: ReturnType = syn::parse_quote! { -> Result<String, MyError> };
958 let info = parse_return_type(&ret);
959 assert!(info.is_result);
960 assert!(!info.is_option);
961 assert!(!info.is_unit);
962
963 let ok = info.ok_type.unwrap();
964 assert_eq!(quote!(#ok).to_string(), "String");
965
966 let err = info.err_type.unwrap();
967 assert_eq!(quote!(#err).to_string(), "MyError");
968 }
969
970 #[test]
971 fn parse_return_type_option() {
972 let ret: ReturnType = syn::parse_quote! { -> Option<i32> };
973 let info = parse_return_type(&ret);
974 assert!(info.is_option);
975 assert!(!info.is_result);
976 assert!(!info.is_unit);
977
978 let some = info.some_type.unwrap();
979 assert_eq!(quote!(#some).to_string(), "i32");
980 }
981
982 #[test]
983 fn parse_return_type_unit_tuple() {
984 let ret: ReturnType = syn::parse_quote! { -> () };
985 let info = parse_return_type(&ret);
986 assert!(info.is_unit);
987 assert!(info.ty.is_some());
988 }
989
990 #[test]
991 fn parse_return_type_reference() {
992 let ret: ReturnType = syn::parse_quote! { -> &SubRouter };
993 let info = parse_return_type(&ret);
994 assert!(info.is_reference);
995 assert!(!info.is_unit);
996
997 let inner = info.reference_inner.unwrap();
998 assert_eq!(quote!(#inner).to_string(), "SubRouter");
999 }
1000
1001 #[test]
1002 fn parse_return_type_stream() {
1003 let ret: ReturnType = syn::parse_quote! { -> impl Stream<Item = u64> };
1004 let info = parse_return_type(&ret);
1005 assert!(info.is_stream);
1006 assert!(!info.is_result);
1007
1008 let item = info.stream_item.unwrap();
1009 assert_eq!(quote!(#item).to_string(), "u64");
1010 }
1011
1012 #[test]
1015 fn is_option_type_true() {
1016 let ty: Type = syn::parse_quote! { Option<String> };
1017 assert!(is_option_type(&ty));
1018 let inner = extract_option_type(&ty).unwrap();
1019 assert_eq!(quote!(#inner).to_string(), "String");
1020 }
1021
1022 #[test]
1023 fn is_option_type_false_for_non_option() {
1024 let ty: Type = syn::parse_quote! { String };
1025 assert!(!is_option_type(&ty));
1026 assert!(extract_option_type(&ty).is_none());
1027 }
1028
1029 #[test]
1032 fn extract_result_types_works() {
1033 let ty: Type = syn::parse_quote! { Result<Vec<u8>, std::io::Error> };
1034 let (ok, err) = extract_result_types(&ty).unwrap();
1035 assert_eq!(quote!(#ok).to_string(), "Vec < u8 >");
1036 assert_eq!(quote!(#err).to_string(), "std :: io :: Error");
1037 }
1038
1039 #[test]
1040 fn extract_result_types_none_for_non_result() {
1041 let ty: Type = syn::parse_quote! { Option<i32> };
1042 assert!(extract_result_types(&ty).is_none());
1043 }
1044
1045 #[test]
1048 fn is_unit_type_true() {
1049 let ty: Type = syn::parse_quote! { () };
1050 assert!(is_unit_type(&ty));
1051 }
1052
1053 #[test]
1054 fn is_unit_type_false_for_non_tuple() {
1055 let ty: Type = syn::parse_quote! { String };
1056 assert!(!is_unit_type(&ty));
1057 }
1058
1059 #[test]
1060 fn is_unit_type_false_for_nonempty_tuple() {
1061 let ty: Type = syn::parse_quote! { (i32, i32) };
1062 assert!(!is_unit_type(&ty));
1063 }
1064
1065 #[test]
1068 fn is_id_param_exact_id() {
1069 let ident: Ident = syn::parse_quote! { id };
1070 assert!(is_id_param(&ident));
1071 }
1072
1073 #[test]
1074 fn is_id_param_suffix_id() {
1075 let ident: Ident = syn::parse_quote! { user_id };
1076 assert!(is_id_param(&ident));
1077 }
1078
1079 #[test]
1080 fn is_id_param_false_for_other_names() {
1081 let ident: Ident = syn::parse_quote! { name };
1082 assert!(!is_id_param(&ident));
1083 }
1084
1085 #[test]
1086 fn is_id_param_false_for_identity() {
1087 let ident: Ident = syn::parse_quote! { identity };
1089 assert!(!is_id_param(&ident));
1090 }
1091
1092 #[test]
1095 fn method_info_parse_basic() {
1096 let method: ImplItemFn = syn::parse_quote! {
1097 fn greet(&self, name: String) -> String {
1099 format!("Hello {name}")
1100 }
1101 };
1102 let info = MethodInfo::parse(&method).unwrap().unwrap();
1103 assert_eq!(info.name.to_string(), "greet");
1104 assert!(!info.is_async);
1105 assert_eq!(info.docs.as_deref(), Some("Does a thing"));
1106 assert_eq!(info.params.len(), 1);
1107 assert_eq!(info.params[0].name.to_string(), "name");
1108 assert!(!info.params[0].is_optional);
1109 assert!(!info.params[0].is_id);
1110 }
1111
1112 #[test]
1113 fn method_info_parse_async_method() {
1114 let method: ImplItemFn = syn::parse_quote! {
1115 async fn fetch(&self) -> Vec<u8> {
1116 vec![]
1117 }
1118 };
1119 let info = MethodInfo::parse(&method).unwrap().unwrap();
1120 assert!(info.is_async);
1121 }
1122
1123 #[test]
1124 fn method_info_parse_skips_associated_function() {
1125 let method: ImplItemFn = syn::parse_quote! {
1126 fn new() -> Self {
1127 Self
1128 }
1129 };
1130 assert!(MethodInfo::parse(&method).unwrap().is_none());
1131 }
1132
1133 #[test]
1134 fn method_info_parse_optional_param() {
1135 let method: ImplItemFn = syn::parse_quote! {
1136 fn search(&self, query: Option<String>) {}
1137 };
1138 let info = MethodInfo::parse(&method).unwrap().unwrap();
1139 assert!(info.params[0].is_optional);
1140 }
1141
1142 #[test]
1143 fn method_info_parse_id_param() {
1144 let method: ImplItemFn = syn::parse_quote! {
1145 fn get_user(&self, user_id: u64) -> String {
1146 String::new()
1147 }
1148 };
1149 let info = MethodInfo::parse(&method).unwrap().unwrap();
1150 assert!(info.params[0].is_id);
1151 }
1152
1153 #[test]
1154 fn method_info_parse_no_docs() {
1155 let method: ImplItemFn = syn::parse_quote! {
1156 fn bare(&self) {}
1157 };
1158 let info = MethodInfo::parse(&method).unwrap().unwrap();
1159 assert!(info.docs.is_none());
1160 }
1161
1162 #[test]
1165 fn extract_methods_basic() {
1166 let impl_block: ItemImpl = syn::parse_quote! {
1167 impl MyApi {
1168 fn hello(&self) -> String { String::new() }
1169 fn world(&self) -> String { String::new() }
1170 }
1171 };
1172 let methods = extract_methods(&impl_block).unwrap();
1173 assert_eq!(methods.len(), 2);
1174 assert_eq!(methods[0].name.to_string(), "hello");
1175 assert_eq!(methods[1].name.to_string(), "world");
1176 }
1177
1178 #[test]
1179 fn extract_methods_skips_underscore_prefix() {
1180 let impl_block: ItemImpl = syn::parse_quote! {
1181 impl MyApi {
1182 fn public(&self) {}
1183 fn _private(&self) {}
1184 fn __also_private(&self) {}
1185 }
1186 };
1187 let methods = extract_methods(&impl_block).unwrap();
1188 assert_eq!(methods.len(), 1);
1189 assert_eq!(methods[0].name.to_string(), "public");
1190 }
1191
1192 #[test]
1193 fn extract_methods_skips_associated_functions() {
1194 let impl_block: ItemImpl = syn::parse_quote! {
1195 impl MyApi {
1196 fn new() -> Self { Self }
1197 fn from_config(cfg: Config) -> Self { Self }
1198 fn greet(&self) -> String { String::new() }
1199 }
1200 };
1201 let methods = extract_methods(&impl_block).unwrap();
1202 assert_eq!(methods.len(), 1);
1203 assert_eq!(methods[0].name.to_string(), "greet");
1204 }
1205
1206 #[test]
1209 fn partition_methods_splits_correctly() {
1210 let impl_block: ItemImpl = syn::parse_quote! {
1211 impl Router {
1212 fn leaf_action(&self) -> String { String::new() }
1213 fn static_mount(&self) -> &SubRouter { &self.sub }
1214 fn slug_mount(&self, id: u64) -> &SubRouter { &self.sub }
1215 async fn async_ref(&self) -> &SubRouter { &self.sub }
1216 }
1217 };
1218 let methods = extract_methods(&impl_block).unwrap();
1219 let partitioned = partition_methods(&methods, |_| false);
1220
1221 assert_eq!(partitioned.leaf.len(), 2);
1223 assert_eq!(partitioned.leaf[0].name.to_string(), "leaf_action");
1224 assert_eq!(partitioned.leaf[1].name.to_string(), "async_ref");
1225
1226 assert_eq!(partitioned.static_mounts.len(), 1);
1227 assert_eq!(
1228 partitioned.static_mounts[0].name.to_string(),
1229 "static_mount"
1230 );
1231
1232 assert_eq!(partitioned.slug_mounts.len(), 1);
1233 assert_eq!(partitioned.slug_mounts[0].name.to_string(), "slug_mount");
1234 }
1235
1236 #[test]
1237 fn partition_methods_respects_skip() {
1238 let impl_block: ItemImpl = syn::parse_quote! {
1239 impl Router {
1240 fn keep(&self) -> String { String::new() }
1241 fn skip_me(&self) -> String { String::new() }
1242 }
1243 };
1244 let methods = extract_methods(&impl_block).unwrap();
1245 let partitioned = partition_methods(&methods, |m| m.name == "skip_me");
1246
1247 assert_eq!(partitioned.leaf.len(), 1);
1248 assert_eq!(partitioned.leaf[0].name.to_string(), "keep");
1249 }
1250
1251 #[test]
1254 fn get_impl_name_extracts_struct_name() {
1255 let impl_block: ItemImpl = syn::parse_quote! {
1256 impl MyService {
1257 fn hello(&self) {}
1258 }
1259 };
1260 let name = get_impl_name(&impl_block).unwrap();
1261 assert_eq!(name.to_string(), "MyService");
1262 }
1263
1264 #[test]
1265 fn get_impl_name_with_generics() {
1266 let impl_block: ItemImpl = syn::parse_quote! {
1267 impl MyService<T> {
1268 fn hello(&self) {}
1269 }
1270 };
1271 let name = get_impl_name(&impl_block).unwrap();
1272 assert_eq!(name.to_string(), "MyService");
1273 }
1274}