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}
34
35#[derive(Debug, Clone)]
37pub struct ParamInfo {
38 pub name: Ident,
40 pub ty: Type,
42 pub is_optional: bool,
44 pub is_bool: bool,
46 pub is_vec: bool,
48 pub vec_inner: Option<Type>,
50 pub is_id: bool,
52 pub wire_name: Option<String>,
54 pub location: Option<ParamLocation>,
56 pub default_value: Option<String>,
58 pub short_flag: Option<char>,
60 pub help_text: Option<String>,
62 pub is_positional: bool,
64}
65
66#[derive(Debug, Clone, PartialEq)]
68pub enum ParamLocation {
69 Query,
70 Path,
71 Body,
72 Header,
73}
74
75#[derive(Debug, Clone)]
77pub struct ReturnInfo {
78 pub ty: Option<Type>,
80 pub ok_type: Option<Type>,
82 pub err_type: Option<Type>,
84 pub some_type: Option<Type>,
86 pub is_result: bool,
88 pub is_option: bool,
90 pub is_unit: bool,
92 pub is_stream: bool,
94 pub stream_item: Option<Type>,
96 pub is_iterator: bool,
98 pub iterator_item: Option<Type>,
100 pub is_reference: bool,
102 pub reference_inner: Option<Type>,
104}
105
106impl MethodInfo {
107 pub fn parse(method: &ImplItemFn) -> syn::Result<Option<Self>> {
111 let name = method.sig.ident.clone();
112 let is_async = method.sig.asyncness.is_some();
113
114 let has_receiver = method
116 .sig
117 .inputs
118 .iter()
119 .any(|arg| matches!(arg, FnArg::Receiver(_)));
120 if !has_receiver {
121 return Ok(None);
122 }
123
124 let docs = extract_docs(&method.attrs);
126
127 let params = parse_params(&method.sig.inputs)?;
129
130 let return_info = parse_return_type(&method.sig.output);
132
133 Ok(Some(Self {
134 method: method.clone(),
135 name,
136 docs,
137 params,
138 return_info,
139 is_async,
140 }))
141 }
142}
143
144pub fn extract_docs(attrs: &[syn::Attribute]) -> Option<String> {
146 let docs: Vec<String> = attrs
147 .iter()
148 .filter_map(|attr| {
149 if attr.path().is_ident("doc")
150 && let Meta::NameValue(meta) = &attr.meta
151 && let syn::Expr::Lit(syn::ExprLit {
152 lit: Lit::Str(s), ..
153 }) = &meta.value
154 {
155 return Some(s.value().trim().to_string());
156 }
157 None
158 })
159 .collect();
160
161 if docs.is_empty() {
162 None
163 } else {
164 Some(docs.join("\n"))
165 }
166}
167
168#[derive(Debug, Clone, Default)]
170pub struct ParsedParamAttrs {
171 pub wire_name: Option<String>,
172 pub location: Option<ParamLocation>,
173 pub default_value: Option<String>,
174 pub short_flag: Option<char>,
175 pub help_text: Option<String>,
176 pub positional: bool,
177}
178
179pub fn parse_param_attrs(attrs: &[syn::Attribute]) -> syn::Result<ParsedParamAttrs> {
181 let mut wire_name = None;
182 let mut location = None;
183 let mut default_value = None;
184 let mut short_flag = None;
185 let mut help_text = None;
186 let mut positional = false;
187
188 for attr in attrs {
189 if !attr.path().is_ident("param") {
190 continue;
191 }
192
193 attr.parse_nested_meta(|meta| {
194 if meta.path.is_ident("name") {
196 let value: syn::LitStr = meta.value()?.parse()?;
197 wire_name = Some(value.value());
198 Ok(())
199 }
200 else if meta.path.is_ident("default") {
202 let value = meta.value()?;
204 let lookahead = value.lookahead1();
205 if lookahead.peek(syn::LitStr) {
206 let lit: syn::LitStr = value.parse()?;
207 default_value = Some(format!("\"{}\"", lit.value()));
208 } else if lookahead.peek(syn::LitInt) {
209 let lit: syn::LitInt = value.parse()?;
210 default_value = Some(lit.to_string());
211 } else if lookahead.peek(syn::LitBool) {
212 let lit: syn::LitBool = value.parse()?;
213 default_value = Some(lit.value.to_string());
214 } else {
215 return Err(lookahead.error());
216 }
217 Ok(())
218 }
219 else if meta.path.is_ident("query") {
221 location = Some(ParamLocation::Query);
222 Ok(())
223 } else if meta.path.is_ident("path") {
224 location = Some(ParamLocation::Path);
225 Ok(())
226 } else if meta.path.is_ident("body") {
227 location = Some(ParamLocation::Body);
228 Ok(())
229 } else if meta.path.is_ident("header") {
230 location = Some(ParamLocation::Header);
231 Ok(())
232 }
233 else if meta.path.is_ident("short") {
235 let value: syn::LitChar = meta.value()?.parse()?;
236 short_flag = Some(value.value());
237 Ok(())
238 }
239 else if meta.path.is_ident("help") {
241 let value: syn::LitStr = meta.value()?.parse()?;
242 help_text = Some(value.value());
243 Ok(())
244 }
245 else if meta.path.is_ident("positional") {
247 positional = true;
248 Ok(())
249 } else {
250 Err(meta.error(
251 "unknown attribute\n\
252 \n\
253 Valid attributes: name, default, query, path, body, header, short, help, positional\n\
254 \n\
255 Examples:\n\
256 - #[param(name = \"q\")]\n\
257 - #[param(default = 10)]\n\
258 - #[param(query)]\n\
259 - #[param(header, name = \"X-API-Key\")]\n\
260 - #[param(short = 'v')]\n\
261 - #[param(help = \"Enable verbose output\")]\n\
262 - #[param(positional)]",
263 ))
264 }
265 })?;
266 }
267
268 Ok(ParsedParamAttrs {
269 wire_name,
270 location,
271 default_value,
272 short_flag,
273 help_text,
274 positional,
275 })
276}
277
278pub fn parse_params(
280 inputs: &syn::punctuated::Punctuated<FnArg, syn::Token![,]>,
281) -> syn::Result<Vec<ParamInfo>> {
282 let mut params = Vec::new();
283
284 for arg in inputs {
285 match arg {
286 FnArg::Receiver(_) => continue, FnArg::Typed(pat_type) => {
288 let name = match pat_type.pat.as_ref() {
289 Pat::Ident(pat_ident) => pat_ident.ident.clone(),
290 other => {
291 return Err(syn::Error::new_spanned(
292 other,
293 "unsupported parameter pattern\n\
294 \n\
295 Server-less macros require simple parameter names.\n\
296 Use: name: String\n\
297 Not: (name, _): (String, i32) or &name: &String",
298 ));
299 }
300 };
301
302 let ty = (*pat_type.ty).clone();
303 let is_optional = is_option_type(&ty);
304 let is_bool = is_bool_type(&ty);
305 let vec_inner = extract_vec_type(&ty);
306 let is_vec = vec_inner.is_some();
307 let is_id = is_id_param(&name);
308
309 let parsed = parse_param_attrs(&pat_type.attrs)?;
311
312 let is_positional = parsed.positional || is_id;
314
315 params.push(ParamInfo {
316 name,
317 ty,
318 is_optional,
319 is_bool,
320 is_vec,
321 vec_inner,
322 is_id,
323 is_positional,
324 wire_name: parsed.wire_name,
325 location: parsed.location,
326 default_value: parsed.default_value,
327 short_flag: parsed.short_flag,
328 help_text: parsed.help_text,
329 });
330 }
331 }
332 }
333
334 Ok(params)
335}
336
337pub fn parse_return_type(output: &ReturnType) -> ReturnInfo {
339 match output {
340 ReturnType::Default => ReturnInfo {
341 ty: None,
342 ok_type: None,
343 err_type: None,
344 some_type: None,
345 is_result: false,
346 is_option: false,
347 is_unit: true,
348 is_stream: false,
349 stream_item: None,
350 is_iterator: false,
351 iterator_item: None,
352 is_reference: false,
353 reference_inner: None,
354 },
355 ReturnType::Type(_, ty) => {
356 let ty = ty.as_ref().clone();
357
358 if let Some((ok, err)) = extract_result_types(&ty) {
360 return ReturnInfo {
361 ty: Some(ty),
362 ok_type: Some(ok),
363 err_type: Some(err),
364 some_type: None,
365 is_result: true,
366 is_option: false,
367 is_unit: false,
368 is_stream: false,
369 stream_item: None,
370 is_iterator: false,
371 iterator_item: None,
372 is_reference: false,
373 reference_inner: None,
374 };
375 }
376
377 if let Some(inner) = extract_option_type(&ty) {
379 return ReturnInfo {
380 ty: Some(ty),
381 ok_type: None,
382 err_type: None,
383 some_type: Some(inner),
384 is_result: false,
385 is_option: true,
386 is_unit: false,
387 is_stream: false,
388 stream_item: None,
389 is_iterator: false,
390 iterator_item: None,
391 is_reference: false,
392 reference_inner: None,
393 };
394 }
395
396 if let Some(item) = extract_stream_item(&ty) {
398 return ReturnInfo {
399 ty: Some(ty),
400 ok_type: None,
401 err_type: None,
402 some_type: None,
403 is_result: false,
404 is_option: false,
405 is_unit: false,
406 is_stream: true,
407 stream_item: Some(item),
408 is_iterator: false,
409 iterator_item: None,
410 is_reference: false,
411 reference_inner: None,
412 };
413 }
414
415 if let Some(item) = extract_iterator_item(&ty) {
417 return ReturnInfo {
418 ty: Some(ty),
419 ok_type: None,
420 err_type: None,
421 some_type: None,
422 is_result: false,
423 is_option: false,
424 is_unit: false,
425 is_stream: false,
426 stream_item: None,
427 is_iterator: true,
428 iterator_item: Some(item),
429 is_reference: false,
430 reference_inner: None,
431 };
432 }
433
434 if is_unit_type(&ty) {
436 return ReturnInfo {
437 ty: Some(ty),
438 ok_type: None,
439 err_type: None,
440 some_type: None,
441 is_result: false,
442 is_option: false,
443 is_unit: true,
444 is_stream: false,
445 stream_item: None,
446 is_iterator: false,
447 iterator_item: None,
448 is_reference: false,
449 reference_inner: None,
450 };
451 }
452
453 if let Type::Reference(TypeReference { elem, .. }) = &ty {
455 let inner = elem.as_ref().clone();
456 return ReturnInfo {
457 ty: Some(ty),
458 ok_type: None,
459 err_type: None,
460 some_type: None,
461 is_result: false,
462 is_option: false,
463 is_unit: false,
464 is_stream: false,
465 stream_item: None,
466 is_iterator: false,
467 iterator_item: None,
468 is_reference: true,
469 reference_inner: Some(inner),
470 };
471 }
472
473 ReturnInfo {
475 ty: Some(ty),
476 ok_type: None,
477 err_type: None,
478 some_type: None,
479 is_result: false,
480 is_option: false,
481 is_unit: false,
482 is_stream: false,
483 stream_item: None,
484 is_iterator: false,
485 iterator_item: None,
486 is_reference: false,
487 reference_inner: None,
488 }
489 }
490 }
491}
492
493pub fn is_bool_type(ty: &Type) -> bool {
495 if let Type::Path(type_path) = ty
496 && let Some(segment) = type_path.path.segments.last()
497 && type_path.path.segments.len() == 1
498 {
499 return segment.ident == "bool";
500 }
501 false
502}
503
504pub fn extract_vec_type(ty: &Type) -> Option<Type> {
506 if let Type::Path(type_path) = ty
507 && let Some(segment) = type_path.path.segments.last()
508 && segment.ident == "Vec"
509 && let PathArguments::AngleBracketed(args) = &segment.arguments
510 && let Some(GenericArgument::Type(inner)) = args.args.first()
511 {
512 return Some(inner.clone());
513 }
514 None
515}
516
517pub fn extract_map_type(ty: &Type) -> Option<(Type, Type)> {
519 if let Type::Path(type_path) = ty
520 && let Some(segment) = type_path.path.segments.last()
521 && (segment.ident == "HashMap" || segment.ident == "BTreeMap")
522 && let PathArguments::AngleBracketed(args) = &segment.arguments
523 {
524 let mut iter = args.args.iter();
525 if let (Some(GenericArgument::Type(key)), Some(GenericArgument::Type(val))) =
526 (iter.next(), iter.next())
527 {
528 return Some((key.clone(), val.clone()));
529 }
530 }
531 None
532}
533
534pub fn extract_option_type(ty: &Type) -> Option<Type> {
536 if let Type::Path(type_path) = ty
537 && let Some(segment) = type_path.path.segments.last()
538 && segment.ident == "Option"
539 && let PathArguments::AngleBracketed(args) = &segment.arguments
540 && let Some(GenericArgument::Type(inner)) = args.args.first()
541 {
542 return Some(inner.clone());
543 }
544 None
545}
546
547pub fn is_option_type(ty: &Type) -> bool {
549 extract_option_type(ty).is_some()
550}
551
552pub fn extract_result_types(ty: &Type) -> Option<(Type, Type)> {
554 if let Type::Path(type_path) = ty
555 && let Some(segment) = type_path.path.segments.last()
556 && segment.ident == "Result"
557 && let PathArguments::AngleBracketed(args) = &segment.arguments
558 {
559 let mut iter = args.args.iter();
560 if let (Some(GenericArgument::Type(ok)), Some(GenericArgument::Type(err))) =
561 (iter.next(), iter.next())
562 {
563 return Some((ok.clone(), err.clone()));
564 }
565 }
566 None
567}
568
569pub fn extract_stream_item(ty: &Type) -> Option<Type> {
571 if let Type::ImplTrait(impl_trait) = ty {
572 for bound in &impl_trait.bounds {
573 if let syn::TypeParamBound::Trait(trait_bound) = bound
574 && let Some(segment) = trait_bound.path.segments.last()
575 && segment.ident == "Stream"
576 && let PathArguments::AngleBracketed(args) = &segment.arguments
577 {
578 for arg in &args.args {
579 if let GenericArgument::AssocType(assoc) = arg
580 && assoc.ident == "Item"
581 {
582 return Some(assoc.ty.clone());
583 }
584 }
585 }
586 }
587 }
588 None
589}
590
591pub fn extract_iterator_item(ty: &Type) -> Option<Type> {
593 if let Type::ImplTrait(impl_trait) = ty {
594 for bound in &impl_trait.bounds {
595 if let syn::TypeParamBound::Trait(trait_bound) = bound
596 && let Some(segment) = trait_bound.path.segments.last()
597 && segment.ident == "Iterator"
598 && let PathArguments::AngleBracketed(args) = &segment.arguments
599 {
600 for arg in &args.args {
601 if let GenericArgument::AssocType(assoc) = arg
602 && assoc.ident == "Item"
603 {
604 return Some(assoc.ty.clone());
605 }
606 }
607 }
608 }
609 }
610 None
611}
612
613pub fn is_unit_type(ty: &Type) -> bool {
615 if let Type::Tuple(tuple) = ty {
616 return tuple.elems.is_empty();
617 }
618 false
619}
620
621pub fn is_id_param(name: &Ident) -> bool {
623 let name_str = name.to_string();
624 name_str == "id" || name_str.ends_with("_id")
625}
626
627pub fn extract_methods(impl_block: &ItemImpl) -> syn::Result<Vec<MethodInfo>> {
633 let mut methods = Vec::new();
634
635 for item in &impl_block.items {
636 if let ImplItem::Fn(method) = item {
637 if method.sig.ident.to_string().starts_with('_') {
639 continue;
640 }
641 if let Some(info) = MethodInfo::parse(method)? {
643 methods.push(info);
644 }
645 }
646 }
647
648 Ok(methods)
649}
650
651pub struct PartitionedMethods<'a> {
656 pub leaf: Vec<&'a MethodInfo>,
658 pub static_mounts: Vec<&'a MethodInfo>,
660 pub slug_mounts: Vec<&'a MethodInfo>,
662}
663
664pub fn partition_methods<'a>(
669 methods: &'a [MethodInfo],
670 skip: impl Fn(&MethodInfo) -> bool,
671) -> PartitionedMethods<'a> {
672 let mut result = PartitionedMethods {
673 leaf: Vec::new(),
674 static_mounts: Vec::new(),
675 slug_mounts: Vec::new(),
676 };
677
678 for method in methods {
679 if skip(method) {
680 continue;
681 }
682
683 if method.return_info.is_reference && !method.is_async {
684 if method.params.is_empty() {
685 result.static_mounts.push(method);
686 } else {
687 result.slug_mounts.push(method);
688 }
689 } else {
690 result.leaf.push(method);
691 }
692 }
693
694 result
695}
696
697pub fn get_impl_name(impl_block: &ItemImpl) -> syn::Result<Ident> {
699 if let Type::Path(type_path) = impl_block.self_ty.as_ref()
700 && let Some(segment) = type_path.path.segments.last()
701 {
702 return Ok(segment.ident.clone());
703 }
704 Err(syn::Error::new_spanned(
705 &impl_block.self_ty,
706 "Expected a simple type name",
707 ))
708}
709
710#[cfg(test)]
711mod tests {
712 use super::*;
713 use quote::quote;
714
715 #[test]
718 fn extract_docs_returns_none_when_no_doc_attrs() {
719 let method: ImplItemFn = syn::parse_quote! {
720 fn hello(&self) {}
721 };
722 assert!(extract_docs(&method.attrs).is_none());
723 }
724
725 #[test]
726 fn extract_docs_extracts_single_line() {
727 let method: ImplItemFn = syn::parse_quote! {
728 fn hello(&self) {}
730 };
731 assert_eq!(extract_docs(&method.attrs).unwrap(), "Hello world");
732 }
733
734 #[test]
735 fn extract_docs_joins_multiple_lines() {
736 let method: ImplItemFn = syn::parse_quote! {
737 fn hello(&self) {}
740 };
741 assert_eq!(extract_docs(&method.attrs).unwrap(), "Line one\nLine two");
742 }
743
744 #[test]
745 fn extract_docs_ignores_non_doc_attrs() {
746 let method: ImplItemFn = syn::parse_quote! {
747 #[inline]
748 fn hello(&self) {}
750 };
751 assert_eq!(extract_docs(&method.attrs).unwrap(), "Documented");
752 }
753
754 #[test]
757 fn parse_return_type_default_is_unit() {
758 let ret: ReturnType = syn::parse_quote! {};
759 let info = parse_return_type(&ret);
760 assert!(info.is_unit);
761 assert!(info.ty.is_none());
762 assert!(!info.is_result);
763 assert!(!info.is_option);
764 assert!(!info.is_reference);
765 }
766
767 #[test]
768 fn parse_return_type_regular_type() {
769 let ret: ReturnType = syn::parse_quote! { -> String };
770 let info = parse_return_type(&ret);
771 assert!(!info.is_unit);
772 assert!(!info.is_result);
773 assert!(!info.is_option);
774 assert!(!info.is_reference);
775 assert!(info.ty.is_some());
776 }
777
778 #[test]
779 fn parse_return_type_result() {
780 let ret: ReturnType = syn::parse_quote! { -> Result<String, MyError> };
781 let info = parse_return_type(&ret);
782 assert!(info.is_result);
783 assert!(!info.is_option);
784 assert!(!info.is_unit);
785
786 let ok = info.ok_type.unwrap();
787 assert_eq!(quote!(#ok).to_string(), "String");
788
789 let err = info.err_type.unwrap();
790 assert_eq!(quote!(#err).to_string(), "MyError");
791 }
792
793 #[test]
794 fn parse_return_type_option() {
795 let ret: ReturnType = syn::parse_quote! { -> Option<i32> };
796 let info = parse_return_type(&ret);
797 assert!(info.is_option);
798 assert!(!info.is_result);
799 assert!(!info.is_unit);
800
801 let some = info.some_type.unwrap();
802 assert_eq!(quote!(#some).to_string(), "i32");
803 }
804
805 #[test]
806 fn parse_return_type_unit_tuple() {
807 let ret: ReturnType = syn::parse_quote! { -> () };
808 let info = parse_return_type(&ret);
809 assert!(info.is_unit);
810 assert!(info.ty.is_some());
811 }
812
813 #[test]
814 fn parse_return_type_reference() {
815 let ret: ReturnType = syn::parse_quote! { -> &SubRouter };
816 let info = parse_return_type(&ret);
817 assert!(info.is_reference);
818 assert!(!info.is_unit);
819
820 let inner = info.reference_inner.unwrap();
821 assert_eq!(quote!(#inner).to_string(), "SubRouter");
822 }
823
824 #[test]
825 fn parse_return_type_stream() {
826 let ret: ReturnType = syn::parse_quote! { -> impl Stream<Item = u64> };
827 let info = parse_return_type(&ret);
828 assert!(info.is_stream);
829 assert!(!info.is_result);
830
831 let item = info.stream_item.unwrap();
832 assert_eq!(quote!(#item).to_string(), "u64");
833 }
834
835 #[test]
838 fn is_option_type_true() {
839 let ty: Type = syn::parse_quote! { Option<String> };
840 assert!(is_option_type(&ty));
841 let inner = extract_option_type(&ty).unwrap();
842 assert_eq!(quote!(#inner).to_string(), "String");
843 }
844
845 #[test]
846 fn is_option_type_false_for_non_option() {
847 let ty: Type = syn::parse_quote! { String };
848 assert!(!is_option_type(&ty));
849 assert!(extract_option_type(&ty).is_none());
850 }
851
852 #[test]
855 fn extract_result_types_works() {
856 let ty: Type = syn::parse_quote! { Result<Vec<u8>, std::io::Error> };
857 let (ok, err) = extract_result_types(&ty).unwrap();
858 assert_eq!(quote!(#ok).to_string(), "Vec < u8 >");
859 assert_eq!(quote!(#err).to_string(), "std :: io :: Error");
860 }
861
862 #[test]
863 fn extract_result_types_none_for_non_result() {
864 let ty: Type = syn::parse_quote! { Option<i32> };
865 assert!(extract_result_types(&ty).is_none());
866 }
867
868 #[test]
871 fn is_unit_type_true() {
872 let ty: Type = syn::parse_quote! { () };
873 assert!(is_unit_type(&ty));
874 }
875
876 #[test]
877 fn is_unit_type_false_for_non_tuple() {
878 let ty: Type = syn::parse_quote! { String };
879 assert!(!is_unit_type(&ty));
880 }
881
882 #[test]
883 fn is_unit_type_false_for_nonempty_tuple() {
884 let ty: Type = syn::parse_quote! { (i32, i32) };
885 assert!(!is_unit_type(&ty));
886 }
887
888 #[test]
891 fn is_id_param_exact_id() {
892 let ident: Ident = syn::parse_quote! { id };
893 assert!(is_id_param(&ident));
894 }
895
896 #[test]
897 fn is_id_param_suffix_id() {
898 let ident: Ident = syn::parse_quote! { user_id };
899 assert!(is_id_param(&ident));
900 }
901
902 #[test]
903 fn is_id_param_false_for_other_names() {
904 let ident: Ident = syn::parse_quote! { name };
905 assert!(!is_id_param(&ident));
906 }
907
908 #[test]
909 fn is_id_param_false_for_identity() {
910 let ident: Ident = syn::parse_quote! { identity };
912 assert!(!is_id_param(&ident));
913 }
914
915 #[test]
918 fn method_info_parse_basic() {
919 let method: ImplItemFn = syn::parse_quote! {
920 fn greet(&self, name: String) -> String {
922 format!("Hello {name}")
923 }
924 };
925 let info = MethodInfo::parse(&method).unwrap().unwrap();
926 assert_eq!(info.name.to_string(), "greet");
927 assert!(!info.is_async);
928 assert_eq!(info.docs.as_deref(), Some("Does a thing"));
929 assert_eq!(info.params.len(), 1);
930 assert_eq!(info.params[0].name.to_string(), "name");
931 assert!(!info.params[0].is_optional);
932 assert!(!info.params[0].is_id);
933 }
934
935 #[test]
936 fn method_info_parse_async_method() {
937 let method: ImplItemFn = syn::parse_quote! {
938 async fn fetch(&self) -> Vec<u8> {
939 vec![]
940 }
941 };
942 let info = MethodInfo::parse(&method).unwrap().unwrap();
943 assert!(info.is_async);
944 }
945
946 #[test]
947 fn method_info_parse_skips_associated_function() {
948 let method: ImplItemFn = syn::parse_quote! {
949 fn new() -> Self {
950 Self
951 }
952 };
953 assert!(MethodInfo::parse(&method).unwrap().is_none());
954 }
955
956 #[test]
957 fn method_info_parse_optional_param() {
958 let method: ImplItemFn = syn::parse_quote! {
959 fn search(&self, query: Option<String>) {}
960 };
961 let info = MethodInfo::parse(&method).unwrap().unwrap();
962 assert!(info.params[0].is_optional);
963 }
964
965 #[test]
966 fn method_info_parse_id_param() {
967 let method: ImplItemFn = syn::parse_quote! {
968 fn get_user(&self, user_id: u64) -> String {
969 String::new()
970 }
971 };
972 let info = MethodInfo::parse(&method).unwrap().unwrap();
973 assert!(info.params[0].is_id);
974 }
975
976 #[test]
977 fn method_info_parse_no_docs() {
978 let method: ImplItemFn = syn::parse_quote! {
979 fn bare(&self) {}
980 };
981 let info = MethodInfo::parse(&method).unwrap().unwrap();
982 assert!(info.docs.is_none());
983 }
984
985 #[test]
988 fn extract_methods_basic() {
989 let impl_block: ItemImpl = syn::parse_quote! {
990 impl MyApi {
991 fn hello(&self) -> String { String::new() }
992 fn world(&self) -> String { String::new() }
993 }
994 };
995 let methods = extract_methods(&impl_block).unwrap();
996 assert_eq!(methods.len(), 2);
997 assert_eq!(methods[0].name.to_string(), "hello");
998 assert_eq!(methods[1].name.to_string(), "world");
999 }
1000
1001 #[test]
1002 fn extract_methods_skips_underscore_prefix() {
1003 let impl_block: ItemImpl = syn::parse_quote! {
1004 impl MyApi {
1005 fn public(&self) {}
1006 fn _private(&self) {}
1007 fn __also_private(&self) {}
1008 }
1009 };
1010 let methods = extract_methods(&impl_block).unwrap();
1011 assert_eq!(methods.len(), 1);
1012 assert_eq!(methods[0].name.to_string(), "public");
1013 }
1014
1015 #[test]
1016 fn extract_methods_skips_associated_functions() {
1017 let impl_block: ItemImpl = syn::parse_quote! {
1018 impl MyApi {
1019 fn new() -> Self { Self }
1020 fn from_config(cfg: Config) -> Self { Self }
1021 fn greet(&self) -> String { String::new() }
1022 }
1023 };
1024 let methods = extract_methods(&impl_block).unwrap();
1025 assert_eq!(methods.len(), 1);
1026 assert_eq!(methods[0].name.to_string(), "greet");
1027 }
1028
1029 #[test]
1032 fn partition_methods_splits_correctly() {
1033 let impl_block: ItemImpl = syn::parse_quote! {
1034 impl Router {
1035 fn leaf_action(&self) -> String { String::new() }
1036 fn static_mount(&self) -> &SubRouter { &self.sub }
1037 fn slug_mount(&self, id: u64) -> &SubRouter { &self.sub }
1038 async fn async_ref(&self) -> &SubRouter { &self.sub }
1039 }
1040 };
1041 let methods = extract_methods(&impl_block).unwrap();
1042 let partitioned = partition_methods(&methods, |_| false);
1043
1044 assert_eq!(partitioned.leaf.len(), 2);
1046 assert_eq!(partitioned.leaf[0].name.to_string(), "leaf_action");
1047 assert_eq!(partitioned.leaf[1].name.to_string(), "async_ref");
1048
1049 assert_eq!(partitioned.static_mounts.len(), 1);
1050 assert_eq!(
1051 partitioned.static_mounts[0].name.to_string(),
1052 "static_mount"
1053 );
1054
1055 assert_eq!(partitioned.slug_mounts.len(), 1);
1056 assert_eq!(partitioned.slug_mounts[0].name.to_string(), "slug_mount");
1057 }
1058
1059 #[test]
1060 fn partition_methods_respects_skip() {
1061 let impl_block: ItemImpl = syn::parse_quote! {
1062 impl Router {
1063 fn keep(&self) -> String { String::new() }
1064 fn skip_me(&self) -> String { String::new() }
1065 }
1066 };
1067 let methods = extract_methods(&impl_block).unwrap();
1068 let partitioned = partition_methods(&methods, |m| m.name == "skip_me");
1069
1070 assert_eq!(partitioned.leaf.len(), 1);
1071 assert_eq!(partitioned.leaf[0].name.to_string(), "keep");
1072 }
1073
1074 #[test]
1077 fn get_impl_name_extracts_struct_name() {
1078 let impl_block: ItemImpl = syn::parse_quote! {
1079 impl MyService {
1080 fn hello(&self) {}
1081 }
1082 };
1083 let name = get_impl_name(&impl_block).unwrap();
1084 assert_eq!(name.to_string(), "MyService");
1085 }
1086
1087 #[test]
1088 fn get_impl_name_with_generics() {
1089 let impl_block: ItemImpl = syn::parse_quote! {
1090 impl MyService<T> {
1091 fn hello(&self) {}
1092 }
1093 };
1094 let name = get_impl_name(&impl_block).unwrap();
1095 assert_eq!(name.to_string(), "MyService");
1096 }
1097}