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