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