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(
257 method: &MethodInfo,
258 registry: &Option<GroupRegistry>,
259) -> syn::Result<Option<String>> {
260 let group_value = match &method.group {
261 Some(v) => v,
262 None => return Ok(None),
263 };
264
265 match registry {
266 Some(reg) => {
267 for (id, display) in ®.groups {
269 if id == group_value {
270 return Ok(Some(display.clone()));
271 }
272 }
273 let span = method.method.sig.ident.span();
275 Err(syn::Error::new(
276 span,
277 format!(
278 "unknown group `{group_value}`; declared groups are: {}",
279 reg.groups
280 .iter()
281 .map(|(id, _)| format!("`{id}`"))
282 .collect::<Vec<_>>()
283 .join(", ")
284 ),
285 ))
286 }
287 None => {
288 Ok(Some(group_value.clone()))
290 }
291 }
292}
293
294#[derive(Debug, Clone, Default)]
296pub struct ParsedParamAttrs {
297 pub wire_name: Option<String>,
298 pub location: Option<ParamLocation>,
299 pub default_value: Option<String>,
300 pub short_flag: Option<char>,
301 pub help_text: Option<String>,
302 pub positional: bool,
303}
304
305pub fn parse_param_attrs(attrs: &[syn::Attribute]) -> syn::Result<ParsedParamAttrs> {
307 let mut wire_name = None;
308 let mut location = None;
309 let mut default_value = None;
310 let mut short_flag = None;
311 let mut help_text = None;
312 let mut positional = false;
313
314 for attr in attrs {
315 if !attr.path().is_ident("param") {
316 continue;
317 }
318
319 attr.parse_nested_meta(|meta| {
320 if meta.path.is_ident("name") {
322 let value: syn::LitStr = meta.value()?.parse()?;
323 wire_name = Some(value.value());
324 Ok(())
325 }
326 else if meta.path.is_ident("default") {
328 let value = meta.value()?;
330 let lookahead = value.lookahead1();
331 if lookahead.peek(syn::LitStr) {
332 let lit: syn::LitStr = value.parse()?;
333 default_value = Some(format!("\"{}\"", lit.value()));
334 } else if lookahead.peek(syn::LitInt) {
335 let lit: syn::LitInt = value.parse()?;
336 default_value = Some(lit.to_string());
337 } else if lookahead.peek(syn::LitBool) {
338 let lit: syn::LitBool = value.parse()?;
339 default_value = Some(lit.value.to_string());
340 } else {
341 return Err(lookahead.error());
342 }
343 Ok(())
344 }
345 else if meta.path.is_ident("query") {
347 location = Some(ParamLocation::Query);
348 Ok(())
349 } else if meta.path.is_ident("path") {
350 location = Some(ParamLocation::Path);
351 Ok(())
352 } else if meta.path.is_ident("body") {
353 location = Some(ParamLocation::Body);
354 Ok(())
355 } else if meta.path.is_ident("header") {
356 location = Some(ParamLocation::Header);
357 Ok(())
358 }
359 else if meta.path.is_ident("short") {
361 let value: syn::LitChar = meta.value()?.parse()?;
362 short_flag = Some(value.value());
363 Ok(())
364 }
365 else if meta.path.is_ident("help") {
367 let value: syn::LitStr = meta.value()?.parse()?;
368 help_text = Some(value.value());
369 Ok(())
370 }
371 else if meta.path.is_ident("positional") {
373 positional = true;
374 Ok(())
375 } else {
376 Err(meta.error(
377 "unknown attribute\n\
378 \n\
379 Valid attributes: name, default, query, path, body, header, short, help, positional\n\
380 \n\
381 Examples:\n\
382 - #[param(name = \"q\")]\n\
383 - #[param(default = 10)]\n\
384 - #[param(query)]\n\
385 - #[param(header, name = \"X-API-Key\")]\n\
386 - #[param(short = 'v')]\n\
387 - #[param(help = \"Enable verbose output\")]\n\
388 - #[param(positional)]",
389 ))
390 }
391 })?;
392 }
393
394 Ok(ParsedParamAttrs {
395 wire_name,
396 location,
397 default_value,
398 short_flag,
399 help_text,
400 positional,
401 })
402}
403
404pub fn parse_params(
406 inputs: &syn::punctuated::Punctuated<FnArg, syn::Token![,]>,
407) -> syn::Result<Vec<ParamInfo>> {
408 let mut params = Vec::new();
409
410 for arg in inputs {
411 match arg {
412 FnArg::Receiver(_) => continue, FnArg::Typed(pat_type) => {
414 let name = match pat_type.pat.as_ref() {
415 Pat::Ident(pat_ident) => pat_ident.ident.clone(),
416 other => {
417 return Err(syn::Error::new_spanned(
418 other,
419 "unsupported parameter pattern\n\
420 \n\
421 Server-less macros require simple parameter names.\n\
422 Use: name: String\n\
423 Not: (name, _): (String, i32) or &name: &String",
424 ));
425 }
426 };
427
428 let ty = (*pat_type.ty).clone();
429 let is_optional = is_option_type(&ty);
430 let is_bool = is_bool_type(&ty);
431 let vec_inner = extract_vec_type(&ty);
432 let is_vec = vec_inner.is_some();
433 let is_id = is_id_param(&name);
434
435 let parsed = parse_param_attrs(&pat_type.attrs)?;
437
438 let is_positional = parsed.positional || is_id;
440
441 params.push(ParamInfo {
442 name,
443 ty,
444 is_optional,
445 is_bool,
446 is_vec,
447 vec_inner,
448 is_id,
449 is_positional,
450 wire_name: parsed.wire_name,
451 location: parsed.location,
452 default_value: parsed.default_value,
453 short_flag: parsed.short_flag,
454 help_text: parsed.help_text,
455 });
456 }
457 }
458 }
459
460 Ok(params)
461}
462
463pub fn parse_return_type(output: &ReturnType) -> ReturnInfo {
465 match output {
466 ReturnType::Default => ReturnInfo {
467 ty: None,
468 ok_type: None,
469 err_type: None,
470 some_type: None,
471 is_result: false,
472 is_option: false,
473 is_unit: true,
474 is_stream: false,
475 stream_item: None,
476 is_iterator: false,
477 iterator_item: None,
478 is_reference: false,
479 reference_inner: None,
480 },
481 ReturnType::Type(_, ty) => {
482 let ty = ty.as_ref().clone();
483
484 if let Some((ok, err)) = extract_result_types(&ty) {
486 return ReturnInfo {
487 ty: Some(ty),
488 ok_type: Some(ok),
489 err_type: Some(err),
490 some_type: None,
491 is_result: true,
492 is_option: false,
493 is_unit: false,
494 is_stream: false,
495 stream_item: None,
496 is_iterator: false,
497 iterator_item: None,
498 is_reference: false,
499 reference_inner: None,
500 };
501 }
502
503 if let Some(inner) = extract_option_type(&ty) {
505 return ReturnInfo {
506 ty: Some(ty),
507 ok_type: None,
508 err_type: None,
509 some_type: Some(inner),
510 is_result: false,
511 is_option: true,
512 is_unit: false,
513 is_stream: false,
514 stream_item: None,
515 is_iterator: false,
516 iterator_item: None,
517 is_reference: false,
518 reference_inner: None,
519 };
520 }
521
522 if let Some(item) = extract_stream_item(&ty) {
524 return ReturnInfo {
525 ty: Some(ty),
526 ok_type: None,
527 err_type: None,
528 some_type: None,
529 is_result: false,
530 is_option: false,
531 is_unit: false,
532 is_stream: true,
533 stream_item: Some(item),
534 is_iterator: false,
535 iterator_item: None,
536 is_reference: false,
537 reference_inner: None,
538 };
539 }
540
541 if let Some(item) = extract_iterator_item(&ty) {
543 return ReturnInfo {
544 ty: Some(ty),
545 ok_type: None,
546 err_type: None,
547 some_type: None,
548 is_result: false,
549 is_option: false,
550 is_unit: false,
551 is_stream: false,
552 stream_item: None,
553 is_iterator: true,
554 iterator_item: Some(item),
555 is_reference: false,
556 reference_inner: None,
557 };
558 }
559
560 if is_unit_type(&ty) {
562 return ReturnInfo {
563 ty: Some(ty),
564 ok_type: None,
565 err_type: None,
566 some_type: None,
567 is_result: false,
568 is_option: false,
569 is_unit: true,
570 is_stream: false,
571 stream_item: None,
572 is_iterator: false,
573 iterator_item: None,
574 is_reference: false,
575 reference_inner: None,
576 };
577 }
578
579 if let Type::Reference(TypeReference { elem, .. }) = &ty {
581 let inner = elem.as_ref().clone();
582 return ReturnInfo {
583 ty: Some(ty),
584 ok_type: None,
585 err_type: None,
586 some_type: None,
587 is_result: false,
588 is_option: false,
589 is_unit: false,
590 is_stream: false,
591 stream_item: None,
592 is_iterator: false,
593 iterator_item: None,
594 is_reference: true,
595 reference_inner: Some(inner),
596 };
597 }
598
599 ReturnInfo {
601 ty: Some(ty),
602 ok_type: None,
603 err_type: None,
604 some_type: None,
605 is_result: false,
606 is_option: false,
607 is_unit: false,
608 is_stream: false,
609 stream_item: None,
610 is_iterator: false,
611 iterator_item: None,
612 is_reference: false,
613 reference_inner: None,
614 }
615 }
616 }
617}
618
619pub fn is_bool_type(ty: &Type) -> bool {
621 if let Type::Path(type_path) = ty
622 && let Some(segment) = type_path.path.segments.last()
623 && type_path.path.segments.len() == 1
624 {
625 return segment.ident == "bool";
626 }
627 false
628}
629
630pub fn extract_vec_type(ty: &Type) -> Option<Type> {
632 if let Type::Path(type_path) = ty
633 && let Some(segment) = type_path.path.segments.last()
634 && segment.ident == "Vec"
635 && let PathArguments::AngleBracketed(args) = &segment.arguments
636 && let Some(GenericArgument::Type(inner)) = args.args.first()
637 {
638 return Some(inner.clone());
639 }
640 None
641}
642
643pub fn extract_map_type(ty: &Type) -> Option<(Type, Type)> {
645 if let Type::Path(type_path) = ty
646 && let Some(segment) = type_path.path.segments.last()
647 && (segment.ident == "HashMap" || segment.ident == "BTreeMap")
648 && let PathArguments::AngleBracketed(args) = &segment.arguments
649 {
650 let mut iter = args.args.iter();
651 if let (Some(GenericArgument::Type(key)), Some(GenericArgument::Type(val))) =
652 (iter.next(), iter.next())
653 {
654 return Some((key.clone(), val.clone()));
655 }
656 }
657 None
658}
659
660pub fn extract_option_type(ty: &Type) -> Option<Type> {
662 if let Type::Path(type_path) = ty
663 && let Some(segment) = type_path.path.segments.last()
664 && segment.ident == "Option"
665 && let PathArguments::AngleBracketed(args) = &segment.arguments
666 && let Some(GenericArgument::Type(inner)) = args.args.first()
667 {
668 return Some(inner.clone());
669 }
670 None
671}
672
673pub fn is_option_type(ty: &Type) -> bool {
675 extract_option_type(ty).is_some()
676}
677
678pub fn extract_result_types(ty: &Type) -> Option<(Type, Type)> {
680 if let Type::Path(type_path) = ty
681 && let Some(segment) = type_path.path.segments.last()
682 && segment.ident == "Result"
683 && let PathArguments::AngleBracketed(args) = &segment.arguments
684 {
685 let mut iter = args.args.iter();
686 if let (Some(GenericArgument::Type(ok)), Some(GenericArgument::Type(err))) =
687 (iter.next(), iter.next())
688 {
689 return Some((ok.clone(), err.clone()));
690 }
691 }
692 None
693}
694
695pub fn extract_stream_item(ty: &Type) -> Option<Type> {
697 if let Type::ImplTrait(impl_trait) = ty {
698 for bound in &impl_trait.bounds {
699 if let syn::TypeParamBound::Trait(trait_bound) = bound
700 && let Some(segment) = trait_bound.path.segments.last()
701 && segment.ident == "Stream"
702 && let PathArguments::AngleBracketed(args) = &segment.arguments
703 {
704 for arg in &args.args {
705 if let GenericArgument::AssocType(assoc) = arg
706 && assoc.ident == "Item"
707 {
708 return Some(assoc.ty.clone());
709 }
710 }
711 }
712 }
713 }
714 None
715}
716
717pub fn extract_iterator_item(ty: &Type) -> Option<Type> {
719 if let Type::ImplTrait(impl_trait) = ty {
720 for bound in &impl_trait.bounds {
721 if let syn::TypeParamBound::Trait(trait_bound) = bound
722 && let Some(segment) = trait_bound.path.segments.last()
723 && segment.ident == "Iterator"
724 && let PathArguments::AngleBracketed(args) = &segment.arguments
725 {
726 for arg in &args.args {
727 if let GenericArgument::AssocType(assoc) = arg
728 && assoc.ident == "Item"
729 {
730 return Some(assoc.ty.clone());
731 }
732 }
733 }
734 }
735 }
736 None
737}
738
739pub fn is_unit_type(ty: &Type) -> bool {
741 if let Type::Tuple(tuple) = ty {
742 return tuple.elems.is_empty();
743 }
744 false
745}
746
747pub fn is_id_param(name: &Ident) -> bool {
749 let name_str = name.to_string();
750 name_str == "id" || name_str.ends_with("_id")
751}
752
753pub fn extract_methods(impl_block: &ItemImpl) -> syn::Result<Vec<MethodInfo>> {
759 let mut methods = Vec::new();
760
761 for item in &impl_block.items {
762 if let ImplItem::Fn(method) = item {
763 if method.sig.ident.to_string().starts_with('_') {
765 continue;
766 }
767 if let Some(info) = MethodInfo::parse(method)? {
769 methods.push(info);
770 }
771 }
772 }
773
774 Ok(methods)
775}
776
777pub struct PartitionedMethods<'a> {
782 pub leaf: Vec<&'a MethodInfo>,
784 pub static_mounts: Vec<&'a MethodInfo>,
786 pub slug_mounts: Vec<&'a MethodInfo>,
788}
789
790pub fn partition_methods<'a>(
795 methods: &'a [MethodInfo],
796 skip: impl Fn(&MethodInfo) -> bool,
797) -> PartitionedMethods<'a> {
798 let mut result = PartitionedMethods {
799 leaf: Vec::new(),
800 static_mounts: Vec::new(),
801 slug_mounts: Vec::new(),
802 };
803
804 for method in methods {
805 if skip(method) {
806 continue;
807 }
808
809 if method.return_info.is_reference && !method.is_async {
810 if method.params.is_empty() {
811 result.static_mounts.push(method);
812 } else {
813 result.slug_mounts.push(method);
814 }
815 } else {
816 result.leaf.push(method);
817 }
818 }
819
820 result
821}
822
823pub fn get_impl_name(impl_block: &ItemImpl) -> syn::Result<Ident> {
825 if let Type::Path(type_path) = impl_block.self_ty.as_ref()
826 && let Some(segment) = type_path.path.segments.last()
827 {
828 return Ok(segment.ident.clone());
829 }
830 Err(syn::Error::new_spanned(
831 &impl_block.self_ty,
832 "Expected a simple type name",
833 ))
834}
835
836#[cfg(test)]
837mod tests {
838 use super::*;
839 use quote::quote;
840
841 #[test]
844 fn extract_docs_returns_none_when_no_doc_attrs() {
845 let method: ImplItemFn = syn::parse_quote! {
846 fn hello(&self) {}
847 };
848 assert!(extract_docs(&method.attrs).is_none());
849 }
850
851 #[test]
852 fn extract_docs_extracts_single_line() {
853 let method: ImplItemFn = syn::parse_quote! {
854 fn hello(&self) {}
856 };
857 assert_eq!(extract_docs(&method.attrs).unwrap(), "Hello world");
858 }
859
860 #[test]
861 fn extract_docs_joins_multiple_lines() {
862 let method: ImplItemFn = syn::parse_quote! {
863 fn hello(&self) {}
866 };
867 assert_eq!(extract_docs(&method.attrs).unwrap(), "Line one\nLine two");
868 }
869
870 #[test]
871 fn extract_docs_ignores_non_doc_attrs() {
872 let method: ImplItemFn = syn::parse_quote! {
873 #[inline]
874 fn hello(&self) {}
876 };
877 assert_eq!(extract_docs(&method.attrs).unwrap(), "Documented");
878 }
879
880 #[test]
883 fn parse_return_type_default_is_unit() {
884 let ret: ReturnType = syn::parse_quote! {};
885 let info = parse_return_type(&ret);
886 assert!(info.is_unit);
887 assert!(info.ty.is_none());
888 assert!(!info.is_result);
889 assert!(!info.is_option);
890 assert!(!info.is_reference);
891 }
892
893 #[test]
894 fn parse_return_type_regular_type() {
895 let ret: ReturnType = syn::parse_quote! { -> String };
896 let info = parse_return_type(&ret);
897 assert!(!info.is_unit);
898 assert!(!info.is_result);
899 assert!(!info.is_option);
900 assert!(!info.is_reference);
901 assert!(info.ty.is_some());
902 }
903
904 #[test]
905 fn parse_return_type_result() {
906 let ret: ReturnType = syn::parse_quote! { -> Result<String, MyError> };
907 let info = parse_return_type(&ret);
908 assert!(info.is_result);
909 assert!(!info.is_option);
910 assert!(!info.is_unit);
911
912 let ok = info.ok_type.unwrap();
913 assert_eq!(quote!(#ok).to_string(), "String");
914
915 let err = info.err_type.unwrap();
916 assert_eq!(quote!(#err).to_string(), "MyError");
917 }
918
919 #[test]
920 fn parse_return_type_option() {
921 let ret: ReturnType = syn::parse_quote! { -> Option<i32> };
922 let info = parse_return_type(&ret);
923 assert!(info.is_option);
924 assert!(!info.is_result);
925 assert!(!info.is_unit);
926
927 let some = info.some_type.unwrap();
928 assert_eq!(quote!(#some).to_string(), "i32");
929 }
930
931 #[test]
932 fn parse_return_type_unit_tuple() {
933 let ret: ReturnType = syn::parse_quote! { -> () };
934 let info = parse_return_type(&ret);
935 assert!(info.is_unit);
936 assert!(info.ty.is_some());
937 }
938
939 #[test]
940 fn parse_return_type_reference() {
941 let ret: ReturnType = syn::parse_quote! { -> &SubRouter };
942 let info = parse_return_type(&ret);
943 assert!(info.is_reference);
944 assert!(!info.is_unit);
945
946 let inner = info.reference_inner.unwrap();
947 assert_eq!(quote!(#inner).to_string(), "SubRouter");
948 }
949
950 #[test]
951 fn parse_return_type_stream() {
952 let ret: ReturnType = syn::parse_quote! { -> impl Stream<Item = u64> };
953 let info = parse_return_type(&ret);
954 assert!(info.is_stream);
955 assert!(!info.is_result);
956
957 let item = info.stream_item.unwrap();
958 assert_eq!(quote!(#item).to_string(), "u64");
959 }
960
961 #[test]
964 fn is_option_type_true() {
965 let ty: Type = syn::parse_quote! { Option<String> };
966 assert!(is_option_type(&ty));
967 let inner = extract_option_type(&ty).unwrap();
968 assert_eq!(quote!(#inner).to_string(), "String");
969 }
970
971 #[test]
972 fn is_option_type_false_for_non_option() {
973 let ty: Type = syn::parse_quote! { String };
974 assert!(!is_option_type(&ty));
975 assert!(extract_option_type(&ty).is_none());
976 }
977
978 #[test]
981 fn extract_result_types_works() {
982 let ty: Type = syn::parse_quote! { Result<Vec<u8>, std::io::Error> };
983 let (ok, err) = extract_result_types(&ty).unwrap();
984 assert_eq!(quote!(#ok).to_string(), "Vec < u8 >");
985 assert_eq!(quote!(#err).to_string(), "std :: io :: Error");
986 }
987
988 #[test]
989 fn extract_result_types_none_for_non_result() {
990 let ty: Type = syn::parse_quote! { Option<i32> };
991 assert!(extract_result_types(&ty).is_none());
992 }
993
994 #[test]
997 fn is_unit_type_true() {
998 let ty: Type = syn::parse_quote! { () };
999 assert!(is_unit_type(&ty));
1000 }
1001
1002 #[test]
1003 fn is_unit_type_false_for_non_tuple() {
1004 let ty: Type = syn::parse_quote! { String };
1005 assert!(!is_unit_type(&ty));
1006 }
1007
1008 #[test]
1009 fn is_unit_type_false_for_nonempty_tuple() {
1010 let ty: Type = syn::parse_quote! { (i32, i32) };
1011 assert!(!is_unit_type(&ty));
1012 }
1013
1014 #[test]
1017 fn is_id_param_exact_id() {
1018 let ident: Ident = syn::parse_quote! { id };
1019 assert!(is_id_param(&ident));
1020 }
1021
1022 #[test]
1023 fn is_id_param_suffix_id() {
1024 let ident: Ident = syn::parse_quote! { user_id };
1025 assert!(is_id_param(&ident));
1026 }
1027
1028 #[test]
1029 fn is_id_param_false_for_other_names() {
1030 let ident: Ident = syn::parse_quote! { name };
1031 assert!(!is_id_param(&ident));
1032 }
1033
1034 #[test]
1035 fn is_id_param_false_for_identity() {
1036 let ident: Ident = syn::parse_quote! { identity };
1038 assert!(!is_id_param(&ident));
1039 }
1040
1041 #[test]
1044 fn method_info_parse_basic() {
1045 let method: ImplItemFn = syn::parse_quote! {
1046 fn greet(&self, name: String) -> String {
1048 format!("Hello {name}")
1049 }
1050 };
1051 let info = MethodInfo::parse(&method).unwrap().unwrap();
1052 assert_eq!(info.name.to_string(), "greet");
1053 assert!(!info.is_async);
1054 assert_eq!(info.docs.as_deref(), Some("Does a thing"));
1055 assert_eq!(info.params.len(), 1);
1056 assert_eq!(info.params[0].name.to_string(), "name");
1057 assert!(!info.params[0].is_optional);
1058 assert!(!info.params[0].is_id);
1059 }
1060
1061 #[test]
1062 fn method_info_parse_async_method() {
1063 let method: ImplItemFn = syn::parse_quote! {
1064 async fn fetch(&self) -> Vec<u8> {
1065 vec![]
1066 }
1067 };
1068 let info = MethodInfo::parse(&method).unwrap().unwrap();
1069 assert!(info.is_async);
1070 }
1071
1072 #[test]
1073 fn method_info_parse_skips_associated_function() {
1074 let method: ImplItemFn = syn::parse_quote! {
1075 fn new() -> Self {
1076 Self
1077 }
1078 };
1079 assert!(MethodInfo::parse(&method).unwrap().is_none());
1080 }
1081
1082 #[test]
1083 fn method_info_parse_optional_param() {
1084 let method: ImplItemFn = syn::parse_quote! {
1085 fn search(&self, query: Option<String>) {}
1086 };
1087 let info = MethodInfo::parse(&method).unwrap().unwrap();
1088 assert!(info.params[0].is_optional);
1089 }
1090
1091 #[test]
1092 fn method_info_parse_id_param() {
1093 let method: ImplItemFn = syn::parse_quote! {
1094 fn get_user(&self, user_id: u64) -> String {
1095 String::new()
1096 }
1097 };
1098 let info = MethodInfo::parse(&method).unwrap().unwrap();
1099 assert!(info.params[0].is_id);
1100 }
1101
1102 #[test]
1103 fn method_info_parse_no_docs() {
1104 let method: ImplItemFn = syn::parse_quote! {
1105 fn bare(&self) {}
1106 };
1107 let info = MethodInfo::parse(&method).unwrap().unwrap();
1108 assert!(info.docs.is_none());
1109 }
1110
1111 #[test]
1114 fn extract_methods_basic() {
1115 let impl_block: ItemImpl = syn::parse_quote! {
1116 impl MyApi {
1117 fn hello(&self) -> String { String::new() }
1118 fn world(&self) -> String { String::new() }
1119 }
1120 };
1121 let methods = extract_methods(&impl_block).unwrap();
1122 assert_eq!(methods.len(), 2);
1123 assert_eq!(methods[0].name.to_string(), "hello");
1124 assert_eq!(methods[1].name.to_string(), "world");
1125 }
1126
1127 #[test]
1128 fn extract_methods_skips_underscore_prefix() {
1129 let impl_block: ItemImpl = syn::parse_quote! {
1130 impl MyApi {
1131 fn public(&self) {}
1132 fn _private(&self) {}
1133 fn __also_private(&self) {}
1134 }
1135 };
1136 let methods = extract_methods(&impl_block).unwrap();
1137 assert_eq!(methods.len(), 1);
1138 assert_eq!(methods[0].name.to_string(), "public");
1139 }
1140
1141 #[test]
1142 fn extract_methods_skips_associated_functions() {
1143 let impl_block: ItemImpl = syn::parse_quote! {
1144 impl MyApi {
1145 fn new() -> Self { Self }
1146 fn from_config(cfg: Config) -> Self { Self }
1147 fn greet(&self) -> String { String::new() }
1148 }
1149 };
1150 let methods = extract_methods(&impl_block).unwrap();
1151 assert_eq!(methods.len(), 1);
1152 assert_eq!(methods[0].name.to_string(), "greet");
1153 }
1154
1155 #[test]
1158 fn partition_methods_splits_correctly() {
1159 let impl_block: ItemImpl = syn::parse_quote! {
1160 impl Router {
1161 fn leaf_action(&self) -> String { String::new() }
1162 fn static_mount(&self) -> &SubRouter { &self.sub }
1163 fn slug_mount(&self, id: u64) -> &SubRouter { &self.sub }
1164 async fn async_ref(&self) -> &SubRouter { &self.sub }
1165 }
1166 };
1167 let methods = extract_methods(&impl_block).unwrap();
1168 let partitioned = partition_methods(&methods, |_| false);
1169
1170 assert_eq!(partitioned.leaf.len(), 2);
1172 assert_eq!(partitioned.leaf[0].name.to_string(), "leaf_action");
1173 assert_eq!(partitioned.leaf[1].name.to_string(), "async_ref");
1174
1175 assert_eq!(partitioned.static_mounts.len(), 1);
1176 assert_eq!(
1177 partitioned.static_mounts[0].name.to_string(),
1178 "static_mount"
1179 );
1180
1181 assert_eq!(partitioned.slug_mounts.len(), 1);
1182 assert_eq!(partitioned.slug_mounts[0].name.to_string(), "slug_mount");
1183 }
1184
1185 #[test]
1186 fn partition_methods_respects_skip() {
1187 let impl_block: ItemImpl = syn::parse_quote! {
1188 impl Router {
1189 fn keep(&self) -> String { String::new() }
1190 fn skip_me(&self) -> String { String::new() }
1191 }
1192 };
1193 let methods = extract_methods(&impl_block).unwrap();
1194 let partitioned = partition_methods(&methods, |m| m.name == "skip_me");
1195
1196 assert_eq!(partitioned.leaf.len(), 1);
1197 assert_eq!(partitioned.leaf[0].name.to_string(), "keep");
1198 }
1199
1200 #[test]
1203 fn get_impl_name_extracts_struct_name() {
1204 let impl_block: ItemImpl = syn::parse_quote! {
1205 impl MyService {
1206 fn hello(&self) {}
1207 }
1208 };
1209 let name = get_impl_name(&impl_block).unwrap();
1210 assert_eq!(name.to_string(), "MyService");
1211 }
1212
1213 #[test]
1214 fn get_impl_name_with_generics() {
1215 let impl_block: ItemImpl = syn::parse_quote! {
1216 impl MyService<T> {
1217 fn hello(&self) {}
1218 }
1219 };
1220 let name = get_impl_name(&impl_block).unwrap();
1221 assert_eq!(name.to_string(), "MyService");
1222 }
1223}