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 pub env_var: Option<String>,
338 pub file_key: Option<String>,
340 pub nested: bool,
346 pub env_prefix: Option<String>,
351 pub nested_serde: bool,
359}
360
361#[allow(clippy::needless_range_loop)]
363fn levenshtein(a: &str, b: &str) -> usize {
364 let a: Vec<char> = a.chars().collect();
365 let b: Vec<char> = b.chars().collect();
366 let m = a.len();
367 let n = b.len();
368 let mut dp = vec![vec![0usize; n + 1]; m + 1];
369 for i in 0..=m {
370 dp[i][0] = i;
371 }
372 for j in 0..=n {
373 dp[0][j] = j;
374 }
375 for i in 1..=m {
376 for j in 1..=n {
377 dp[i][j] = if a[i - 1] == b[j - 1] {
378 dp[i - 1][j - 1]
379 } else {
380 1 + dp[i - 1][j - 1].min(dp[i - 1][j]).min(dp[i][j - 1])
381 };
382 }
383 }
384 dp[m][n]
385}
386
387fn did_you_mean<'a>(input: &str, candidates: &[&'a str]) -> Option<&'a str> {
389 candidates
390 .iter()
391 .filter_map(|&c| {
392 let d = levenshtein(input, c);
393 if d <= 2 { Some((d, c)) } else { None }
394 })
395 .min_by_key(|&(d, _)| d)
396 .map(|(_, c)| c)
397}
398
399pub fn parse_param_attrs(attrs: &[syn::Attribute]) -> syn::Result<ParsedParamAttrs> {
401 let mut wire_name = None;
402 let mut location = None;
403 let mut default_value = None;
404 let mut short_flag = None;
405 let mut help_text = None;
406 let mut positional = false;
407 let mut env_var = None;
408 let mut file_key = None;
409 let mut nested = false;
410 let mut env_prefix = None;
411 let mut nested_serde = false;
412
413 for attr in attrs {
414 if !attr.path().is_ident("param") {
415 continue;
416 }
417
418 attr.parse_nested_meta(|meta| {
419 if meta.path.is_ident("name") {
421 let value: syn::LitStr = meta.value()?.parse()?;
422 wire_name = Some(value.value());
423 Ok(())
424 }
425 else if meta.path.is_ident("default") {
427 let value = meta.value()?;
429 let lookahead = value.lookahead1();
430 if lookahead.peek(syn::LitStr) {
431 let lit: syn::LitStr = value.parse()?;
432 default_value = Some(format!("\"{}\"", lit.value()));
433 } else if lookahead.peek(syn::LitInt) {
434 let lit: syn::LitInt = value.parse()?;
435 default_value = Some(lit.to_string());
436 } else if lookahead.peek(syn::LitBool) {
437 let lit: syn::LitBool = value.parse()?;
438 default_value = Some(lit.value.to_string());
439 } else {
440 return Err(lookahead.error());
441 }
442 Ok(())
443 }
444 else if meta.path.is_ident("query") {
446 location = Some(ParamLocation::Query);
447 Ok(())
448 } else if meta.path.is_ident("path") {
449 location = Some(ParamLocation::Path);
450 Ok(())
451 } else if meta.path.is_ident("body") {
452 location = Some(ParamLocation::Body);
453 Ok(())
454 } else if meta.path.is_ident("header") {
455 location = Some(ParamLocation::Header);
456 Ok(())
457 }
458 else if meta.path.is_ident("short") {
460 let value: syn::LitChar = meta.value()?.parse()?;
461 short_flag = Some(value.value());
462 Ok(())
463 }
464 else if meta.path.is_ident("help") {
466 let value: syn::LitStr = meta.value()?.parse()?;
467 help_text = Some(value.value());
468 Ok(())
469 }
470 else if meta.path.is_ident("positional") {
472 positional = true;
473 Ok(())
474 }
475 else if meta.path.is_ident("env") {
477 let value: syn::LitStr = meta.value()?.parse()?;
478 env_var = Some(value.value());
479 Ok(())
480 }
481 else if meta.path.is_ident("file_key") {
483 let value: syn::LitStr = meta.value()?.parse()?;
484 file_key = Some(value.value());
485 Ok(())
486 }
487 else if meta.path.is_ident("nested") {
489 nested = true;
490 Ok(())
491 }
492 else if meta.path.is_ident("serde") {
494 nested_serde = true;
495 Ok(())
496 }
497 else if meta.path.is_ident("env_prefix") {
499 let value: syn::LitStr = meta.value()?.parse()?;
500 env_prefix = Some(value.value());
501 Ok(())
502 } else {
503 const VALID: &[&str] = &[
504 "name", "default", "query", "path", "body", "header", "short", "help",
505 "positional", "env", "file_key", "nested", "serde", "env_prefix",
506 ];
507 let unknown = meta
508 .path
509 .get_ident()
510 .map(|i| i.to_string())
511 .unwrap_or_default();
512 let suggestion = did_you_mean(&unknown, VALID)
513 .map(|s| format!(" — did you mean `{s}`?"))
514 .unwrap_or_default();
515 Err(meta.error(format!(
516 "unknown attribute `{unknown}`{suggestion}\n\
517 \n\
518 Valid attributes: name, default, query, path, body, header, short, help, positional, env, file_key, nested, serde, env_prefix\n\
519 \n\
520 Examples:\n\
521 - #[param(name = \"q\")]\n\
522 - #[param(default = 10)]\n\
523 - #[param(query)]\n\
524 - #[param(header, name = \"X-API-Key\")]\n\
525 - #[param(short = 'v')]\n\
526 - #[param(help = \"Enable verbose output\")]\n\
527 - #[param(positional)]\n\
528 - #[param(env = \"MY_VAR\")]\n\
529 - #[param(file_key = \"database.host\")]\n\
530 - #[param(nested)]\n\
531 - #[param(nested, serde)]\n\
532 - #[param(nested, env_prefix = \"SEARCH\")]"
533 )))
534 }
535 })?;
536 }
537
538 Ok(ParsedParamAttrs {
539 wire_name,
540 location,
541 default_value,
542 short_flag,
543 help_text,
544 positional,
545 env_var,
546 file_key,
547 nested,
548 env_prefix,
549 nested_serde,
550 })
551}
552
553pub fn parse_params(
555 inputs: &syn::punctuated::Punctuated<FnArg, syn::Token![,]>,
556) -> syn::Result<Vec<ParamInfo>> {
557 let mut params = Vec::new();
558
559 for arg in inputs {
560 match arg {
561 FnArg::Receiver(_) => continue, FnArg::Typed(pat_type) => {
563 let name = match pat_type.pat.as_ref() {
564 Pat::Ident(pat_ident) => pat_ident.ident.clone(),
565 other => {
566 return Err(syn::Error::new_spanned(
567 other,
568 "unsupported parameter pattern\n\
569 \n\
570 Server-less macros require simple parameter names.\n\
571 Use: name: String\n\
572 Not: (name, _): (String, i32) or &name: &String",
573 ));
574 }
575 };
576
577 let ty = (*pat_type.ty).clone();
578 let is_optional = is_option_type(&ty);
579 let is_bool = is_bool_type(&ty);
580 let vec_inner = extract_vec_type(&ty);
581 let is_vec = vec_inner.is_some();
582 let is_id = is_id_param(&name);
583
584 let parsed = parse_param_attrs(&pat_type.attrs)?;
586
587 let is_positional = parsed.positional || is_id;
589
590 params.push(ParamInfo {
591 name,
592 ty,
593 is_optional,
594 is_bool,
595 is_vec,
596 vec_inner,
597 is_id,
598 is_positional,
599 wire_name: parsed.wire_name,
600 location: parsed.location,
601 default_value: parsed.default_value,
602 short_flag: parsed.short_flag,
603 help_text: parsed.help_text,
604 });
605 }
606 }
607 }
608
609 Ok(params)
610}
611
612pub fn parse_return_type(output: &ReturnType) -> ReturnInfo {
614 match output {
615 ReturnType::Default => ReturnInfo {
616 ty: None,
617 ok_type: None,
618 err_type: None,
619 some_type: None,
620 is_result: false,
621 is_option: false,
622 is_unit: true,
623 is_stream: false,
624 stream_item: None,
625 is_iterator: false,
626 iterator_item: None,
627 is_reference: false,
628 reference_inner: None,
629 },
630 ReturnType::Type(_, ty) => {
631 let ty = ty.as_ref().clone();
632
633 if let Some((ok, err)) = extract_result_types(&ty) {
635 return ReturnInfo {
636 ty: Some(ty),
637 ok_type: Some(ok),
638 err_type: Some(err),
639 some_type: None,
640 is_result: true,
641 is_option: false,
642 is_unit: false,
643 is_stream: false,
644 stream_item: None,
645 is_iterator: false,
646 iterator_item: None,
647 is_reference: false,
648 reference_inner: None,
649 };
650 }
651
652 if let Some(inner) = extract_option_type(&ty) {
654 return ReturnInfo {
655 ty: Some(ty),
656 ok_type: None,
657 err_type: None,
658 some_type: Some(inner),
659 is_result: false,
660 is_option: true,
661 is_unit: false,
662 is_stream: false,
663 stream_item: None,
664 is_iterator: false,
665 iterator_item: None,
666 is_reference: false,
667 reference_inner: None,
668 };
669 }
670
671 if let Some(item) = extract_stream_item(&ty) {
673 return ReturnInfo {
674 ty: Some(ty),
675 ok_type: None,
676 err_type: None,
677 some_type: None,
678 is_result: false,
679 is_option: false,
680 is_unit: false,
681 is_stream: true,
682 stream_item: Some(item),
683 is_iterator: false,
684 iterator_item: None,
685 is_reference: false,
686 reference_inner: None,
687 };
688 }
689
690 if let Some(item) = extract_iterator_item(&ty) {
692 return ReturnInfo {
693 ty: Some(ty),
694 ok_type: None,
695 err_type: None,
696 some_type: None,
697 is_result: false,
698 is_option: false,
699 is_unit: false,
700 is_stream: false,
701 stream_item: None,
702 is_iterator: true,
703 iterator_item: Some(item),
704 is_reference: false,
705 reference_inner: None,
706 };
707 }
708
709 if is_unit_type(&ty) {
711 return ReturnInfo {
712 ty: Some(ty),
713 ok_type: None,
714 err_type: None,
715 some_type: None,
716 is_result: false,
717 is_option: false,
718 is_unit: true,
719 is_stream: false,
720 stream_item: None,
721 is_iterator: false,
722 iterator_item: None,
723 is_reference: false,
724 reference_inner: None,
725 };
726 }
727
728 if let Type::Reference(TypeReference { elem, .. }) = &ty {
730 let inner = elem.as_ref().clone();
731 return ReturnInfo {
732 ty: Some(ty),
733 ok_type: None,
734 err_type: None,
735 some_type: None,
736 is_result: false,
737 is_option: false,
738 is_unit: false,
739 is_stream: false,
740 stream_item: None,
741 is_iterator: false,
742 iterator_item: None,
743 is_reference: true,
744 reference_inner: Some(inner),
745 };
746 }
747
748 ReturnInfo {
750 ty: Some(ty),
751 ok_type: None,
752 err_type: None,
753 some_type: None,
754 is_result: false,
755 is_option: false,
756 is_unit: false,
757 is_stream: false,
758 stream_item: None,
759 is_iterator: false,
760 iterator_item: None,
761 is_reference: false,
762 reference_inner: None,
763 }
764 }
765 }
766}
767
768pub fn is_bool_type(ty: &Type) -> bool {
770 if let Type::Path(type_path) = ty
771 && let Some(segment) = type_path.path.segments.last()
772 && type_path.path.segments.len() == 1
773 {
774 return segment.ident == "bool";
775 }
776 false
777}
778
779pub fn extract_vec_type(ty: &Type) -> Option<Type> {
781 if let Type::Path(type_path) = ty
782 && let Some(segment) = type_path.path.segments.last()
783 && segment.ident == "Vec"
784 && let PathArguments::AngleBracketed(args) = &segment.arguments
785 && let Some(GenericArgument::Type(inner)) = args.args.first()
786 {
787 return Some(inner.clone());
788 }
789 None
790}
791
792pub fn extract_map_type(ty: &Type) -> Option<(Type, Type)> {
794 if let Type::Path(type_path) = ty
795 && let Some(segment) = type_path.path.segments.last()
796 && (segment.ident == "HashMap" || segment.ident == "BTreeMap")
797 && let PathArguments::AngleBracketed(args) = &segment.arguments
798 {
799 let mut iter = args.args.iter();
800 if let (Some(GenericArgument::Type(key)), Some(GenericArgument::Type(val))) =
801 (iter.next(), iter.next())
802 {
803 return Some((key.clone(), val.clone()));
804 }
805 }
806 None
807}
808
809pub fn extract_option_type(ty: &Type) -> Option<Type> {
811 if let Type::Path(type_path) = ty
812 && let Some(segment) = type_path.path.segments.last()
813 && segment.ident == "Option"
814 && let PathArguments::AngleBracketed(args) = &segment.arguments
815 && let Some(GenericArgument::Type(inner)) = args.args.first()
816 {
817 return Some(inner.clone());
818 }
819 None
820}
821
822pub fn is_option_type(ty: &Type) -> bool {
824 extract_option_type(ty).is_some()
825}
826
827pub fn extract_result_types(ty: &Type) -> Option<(Type, Type)> {
829 if let Type::Path(type_path) = ty
830 && let Some(segment) = type_path.path.segments.last()
831 && segment.ident == "Result"
832 && let PathArguments::AngleBracketed(args) = &segment.arguments
833 {
834 let mut iter = args.args.iter();
835 if let (Some(GenericArgument::Type(ok)), Some(GenericArgument::Type(err))) =
836 (iter.next(), iter.next())
837 {
838 return Some((ok.clone(), err.clone()));
839 }
840 }
841 None
842}
843
844pub fn extract_stream_item(ty: &Type) -> Option<Type> {
846 if let Type::ImplTrait(impl_trait) = ty {
847 for bound in &impl_trait.bounds {
848 if let syn::TypeParamBound::Trait(trait_bound) = bound
849 && let Some(segment) = trait_bound.path.segments.last()
850 && segment.ident == "Stream"
851 && let PathArguments::AngleBracketed(args) = &segment.arguments
852 {
853 for arg in &args.args {
854 if let GenericArgument::AssocType(assoc) = arg
855 && assoc.ident == "Item"
856 {
857 return Some(assoc.ty.clone());
858 }
859 }
860 }
861 }
862 }
863 None
864}
865
866pub fn extract_iterator_item(ty: &Type) -> Option<Type> {
868 if let Type::ImplTrait(impl_trait) = ty {
869 for bound in &impl_trait.bounds {
870 if let syn::TypeParamBound::Trait(trait_bound) = bound
871 && let Some(segment) = trait_bound.path.segments.last()
872 && segment.ident == "Iterator"
873 && let PathArguments::AngleBracketed(args) = &segment.arguments
874 {
875 for arg in &args.args {
876 if let GenericArgument::AssocType(assoc) = arg
877 && assoc.ident == "Item"
878 {
879 return Some(assoc.ty.clone());
880 }
881 }
882 }
883 }
884 }
885 None
886}
887
888pub fn is_unit_type(ty: &Type) -> bool {
890 if let Type::Tuple(tuple) = ty {
891 return tuple.elems.is_empty();
892 }
893 false
894}
895
896pub fn is_id_param(name: &Ident) -> bool {
898 let name_str = ident_str(name);
899 name_str == "id" || name_str.ends_with("_id")
900}
901
902pub fn extract_methods(impl_block: &ItemImpl) -> syn::Result<Vec<MethodInfo>> {
908 let mut methods = Vec::new();
909
910 for item in &impl_block.items {
911 if let ImplItem::Fn(method) = item {
912 if method.sig.ident.to_string().starts_with('_') {
914 continue;
915 }
916 if let Some(info) = MethodInfo::parse(method)? {
918 methods.push(info);
919 }
920 }
921 }
922
923 Ok(methods)
924}
925
926pub struct PartitionedMethods<'a> {
931 pub leaf: Vec<&'a MethodInfo>,
933 pub static_mounts: Vec<&'a MethodInfo>,
935 pub slug_mounts: Vec<&'a MethodInfo>,
937}
938
939pub fn partition_methods<'a>(
944 methods: &'a [MethodInfo],
945 skip: impl Fn(&MethodInfo) -> bool,
946) -> PartitionedMethods<'a> {
947 let mut result = PartitionedMethods {
948 leaf: Vec::new(),
949 static_mounts: Vec::new(),
950 slug_mounts: Vec::new(),
951 };
952
953 for method in methods {
954 if skip(method) {
955 continue;
956 }
957
958 if method.return_info.is_reference && !method.is_async {
959 if method.params.is_empty() {
960 result.static_mounts.push(method);
961 } else {
962 result.slug_mounts.push(method);
963 }
964 } else {
965 result.leaf.push(method);
966 }
967 }
968
969 result
970}
971
972pub fn get_impl_name(impl_block: &ItemImpl) -> syn::Result<Ident> {
974 if let Type::Path(type_path) = impl_block.self_ty.as_ref()
975 && let Some(segment) = type_path.path.segments.last()
976 {
977 return Ok(segment.ident.clone());
978 }
979 Err(syn::Error::new_spanned(
980 &impl_block.self_ty,
981 "Expected a simple type name",
982 ))
983}
984
985#[cfg(test)]
986mod tests {
987 use super::*;
988 use quote::quote;
989
990 #[test]
993 fn extract_docs_returns_none_when_no_doc_attrs() {
994 let method: ImplItemFn = syn::parse_quote! {
995 fn hello(&self) {}
996 };
997 assert!(extract_docs(&method.attrs).is_none());
998 }
999
1000 #[test]
1001 fn extract_docs_extracts_single_line() {
1002 let method: ImplItemFn = syn::parse_quote! {
1003 fn hello(&self) {}
1005 };
1006 assert_eq!(extract_docs(&method.attrs).unwrap(), "Hello world");
1007 }
1008
1009 #[test]
1010 fn extract_docs_joins_multiple_lines() {
1011 let method: ImplItemFn = syn::parse_quote! {
1012 fn hello(&self) {}
1015 };
1016 assert_eq!(extract_docs(&method.attrs).unwrap(), "Line one\nLine two");
1017 }
1018
1019 #[test]
1020 fn extract_docs_ignores_non_doc_attrs() {
1021 let method: ImplItemFn = syn::parse_quote! {
1022 #[inline]
1023 fn hello(&self) {}
1025 };
1026 assert_eq!(extract_docs(&method.attrs).unwrap(), "Documented");
1027 }
1028
1029 #[test]
1032 fn parse_return_type_default_is_unit() {
1033 let ret: ReturnType = syn::parse_quote! {};
1034 let info = parse_return_type(&ret);
1035 assert!(info.is_unit);
1036 assert!(info.ty.is_none());
1037 assert!(!info.is_result);
1038 assert!(!info.is_option);
1039 assert!(!info.is_reference);
1040 }
1041
1042 #[test]
1043 fn parse_return_type_regular_type() {
1044 let ret: ReturnType = syn::parse_quote! { -> String };
1045 let info = parse_return_type(&ret);
1046 assert!(!info.is_unit);
1047 assert!(!info.is_result);
1048 assert!(!info.is_option);
1049 assert!(!info.is_reference);
1050 assert!(info.ty.is_some());
1051 }
1052
1053 #[test]
1054 fn parse_return_type_result() {
1055 let ret: ReturnType = syn::parse_quote! { -> Result<String, MyError> };
1056 let info = parse_return_type(&ret);
1057 assert!(info.is_result);
1058 assert!(!info.is_option);
1059 assert!(!info.is_unit);
1060
1061 let ok = info.ok_type.unwrap();
1062 assert_eq!(quote!(#ok).to_string(), "String");
1063
1064 let err = info.err_type.unwrap();
1065 assert_eq!(quote!(#err).to_string(), "MyError");
1066 }
1067
1068 #[test]
1069 fn parse_return_type_option() {
1070 let ret: ReturnType = syn::parse_quote! { -> Option<i32> };
1071 let info = parse_return_type(&ret);
1072 assert!(info.is_option);
1073 assert!(!info.is_result);
1074 assert!(!info.is_unit);
1075
1076 let some = info.some_type.unwrap();
1077 assert_eq!(quote!(#some).to_string(), "i32");
1078 }
1079
1080 #[test]
1081 fn parse_return_type_unit_tuple() {
1082 let ret: ReturnType = syn::parse_quote! { -> () };
1083 let info = parse_return_type(&ret);
1084 assert!(info.is_unit);
1085 assert!(info.ty.is_some());
1086 }
1087
1088 #[test]
1089 fn parse_return_type_reference() {
1090 let ret: ReturnType = syn::parse_quote! { -> &SubRouter };
1091 let info = parse_return_type(&ret);
1092 assert!(info.is_reference);
1093 assert!(!info.is_unit);
1094
1095 let inner = info.reference_inner.unwrap();
1096 assert_eq!(quote!(#inner).to_string(), "SubRouter");
1097 }
1098
1099 #[test]
1100 fn parse_return_type_stream() {
1101 let ret: ReturnType = syn::parse_quote! { -> impl Stream<Item = u64> };
1102 let info = parse_return_type(&ret);
1103 assert!(info.is_stream);
1104 assert!(!info.is_result);
1105
1106 let item = info.stream_item.unwrap();
1107 assert_eq!(quote!(#item).to_string(), "u64");
1108 }
1109
1110 #[test]
1113 fn is_option_type_true() {
1114 let ty: Type = syn::parse_quote! { Option<String> };
1115 assert!(is_option_type(&ty));
1116 let inner = extract_option_type(&ty).unwrap();
1117 assert_eq!(quote!(#inner).to_string(), "String");
1118 }
1119
1120 #[test]
1121 fn is_option_type_false_for_non_option() {
1122 let ty: Type = syn::parse_quote! { String };
1123 assert!(!is_option_type(&ty));
1124 assert!(extract_option_type(&ty).is_none());
1125 }
1126
1127 #[test]
1130 fn extract_result_types_works() {
1131 let ty: Type = syn::parse_quote! { Result<Vec<u8>, std::io::Error> };
1132 let (ok, err) = extract_result_types(&ty).unwrap();
1133 assert_eq!(quote!(#ok).to_string(), "Vec < u8 >");
1134 assert_eq!(quote!(#err).to_string(), "std :: io :: Error");
1135 }
1136
1137 #[test]
1138 fn extract_result_types_none_for_non_result() {
1139 let ty: Type = syn::parse_quote! { Option<i32> };
1140 assert!(extract_result_types(&ty).is_none());
1141 }
1142
1143 #[test]
1146 fn is_unit_type_true() {
1147 let ty: Type = syn::parse_quote! { () };
1148 assert!(is_unit_type(&ty));
1149 }
1150
1151 #[test]
1152 fn is_unit_type_false_for_non_tuple() {
1153 let ty: Type = syn::parse_quote! { String };
1154 assert!(!is_unit_type(&ty));
1155 }
1156
1157 #[test]
1158 fn is_unit_type_false_for_nonempty_tuple() {
1159 let ty: Type = syn::parse_quote! { (i32, i32) };
1160 assert!(!is_unit_type(&ty));
1161 }
1162
1163 #[test]
1166 fn is_id_param_exact_id() {
1167 let ident: Ident = syn::parse_quote! { id };
1168 assert!(is_id_param(&ident));
1169 }
1170
1171 #[test]
1172 fn is_id_param_suffix_id() {
1173 let ident: Ident = syn::parse_quote! { user_id };
1174 assert!(is_id_param(&ident));
1175 }
1176
1177 #[test]
1178 fn is_id_param_false_for_other_names() {
1179 let ident: Ident = syn::parse_quote! { name };
1180 assert!(!is_id_param(&ident));
1181 }
1182
1183 #[test]
1184 fn is_id_param_false_for_identity() {
1185 let ident: Ident = syn::parse_quote! { identity };
1187 assert!(!is_id_param(&ident));
1188 }
1189
1190 #[test]
1193 fn method_info_parse_basic() {
1194 let method: ImplItemFn = syn::parse_quote! {
1195 fn greet(&self, name: String) -> String {
1197 format!("Hello {name}")
1198 }
1199 };
1200 let info = MethodInfo::parse(&method).unwrap().unwrap();
1201 assert_eq!(info.name.to_string(), "greet");
1202 assert!(!info.is_async);
1203 assert_eq!(info.docs.as_deref(), Some("Does a thing"));
1204 assert_eq!(info.params.len(), 1);
1205 assert_eq!(info.params[0].name.to_string(), "name");
1206 assert!(!info.params[0].is_optional);
1207 assert!(!info.params[0].is_id);
1208 }
1209
1210 #[test]
1211 fn method_info_parse_async_method() {
1212 let method: ImplItemFn = syn::parse_quote! {
1213 async fn fetch(&self) -> Vec<u8> {
1214 vec![]
1215 }
1216 };
1217 let info = MethodInfo::parse(&method).unwrap().unwrap();
1218 assert!(info.is_async);
1219 }
1220
1221 #[test]
1222 fn method_info_parse_skips_associated_function() {
1223 let method: ImplItemFn = syn::parse_quote! {
1224 fn new() -> Self {
1225 Self
1226 }
1227 };
1228 assert!(MethodInfo::parse(&method).unwrap().is_none());
1229 }
1230
1231 #[test]
1232 fn method_info_parse_optional_param() {
1233 let method: ImplItemFn = syn::parse_quote! {
1234 fn search(&self, query: Option<String>) {}
1235 };
1236 let info = MethodInfo::parse(&method).unwrap().unwrap();
1237 assert!(info.params[0].is_optional);
1238 }
1239
1240 #[test]
1241 fn method_info_parse_id_param() {
1242 let method: ImplItemFn = syn::parse_quote! {
1243 fn get_user(&self, user_id: u64) -> String {
1244 String::new()
1245 }
1246 };
1247 let info = MethodInfo::parse(&method).unwrap().unwrap();
1248 assert!(info.params[0].is_id);
1249 }
1250
1251 #[test]
1252 fn method_info_parse_no_docs() {
1253 let method: ImplItemFn = syn::parse_quote! {
1254 fn bare(&self) {}
1255 };
1256 let info = MethodInfo::parse(&method).unwrap().unwrap();
1257 assert!(info.docs.is_none());
1258 }
1259
1260 #[test]
1263 fn extract_methods_basic() {
1264 let impl_block: ItemImpl = syn::parse_quote! {
1265 impl MyApi {
1266 fn hello(&self) -> String { String::new() }
1267 fn world(&self) -> String { String::new() }
1268 }
1269 };
1270 let methods = extract_methods(&impl_block).unwrap();
1271 assert_eq!(methods.len(), 2);
1272 assert_eq!(methods[0].name.to_string(), "hello");
1273 assert_eq!(methods[1].name.to_string(), "world");
1274 }
1275
1276 #[test]
1277 fn extract_methods_skips_underscore_prefix() {
1278 let impl_block: ItemImpl = syn::parse_quote! {
1279 impl MyApi {
1280 fn public(&self) {}
1281 fn _private(&self) {}
1282 fn __also_private(&self) {}
1283 }
1284 };
1285 let methods = extract_methods(&impl_block).unwrap();
1286 assert_eq!(methods.len(), 1);
1287 assert_eq!(methods[0].name.to_string(), "public");
1288 }
1289
1290 #[test]
1291 fn extract_methods_skips_associated_functions() {
1292 let impl_block: ItemImpl = syn::parse_quote! {
1293 impl MyApi {
1294 fn new() -> Self { Self }
1295 fn from_config(cfg: Config) -> Self { Self }
1296 fn greet(&self) -> String { String::new() }
1297 }
1298 };
1299 let methods = extract_methods(&impl_block).unwrap();
1300 assert_eq!(methods.len(), 1);
1301 assert_eq!(methods[0].name.to_string(), "greet");
1302 }
1303
1304 #[test]
1307 fn partition_methods_splits_correctly() {
1308 let impl_block: ItemImpl = syn::parse_quote! {
1309 impl Router {
1310 fn leaf_action(&self) -> String { String::new() }
1311 fn static_mount(&self) -> &SubRouter { &self.sub }
1312 fn slug_mount(&self, id: u64) -> &SubRouter { &self.sub }
1313 async fn async_ref(&self) -> &SubRouter { &self.sub }
1314 }
1315 };
1316 let methods = extract_methods(&impl_block).unwrap();
1317 let partitioned = partition_methods(&methods, |_| false);
1318
1319 assert_eq!(partitioned.leaf.len(), 2);
1321 assert_eq!(partitioned.leaf[0].name.to_string(), "leaf_action");
1322 assert_eq!(partitioned.leaf[1].name.to_string(), "async_ref");
1323
1324 assert_eq!(partitioned.static_mounts.len(), 1);
1325 assert_eq!(
1326 partitioned.static_mounts[0].name.to_string(),
1327 "static_mount"
1328 );
1329
1330 assert_eq!(partitioned.slug_mounts.len(), 1);
1331 assert_eq!(partitioned.slug_mounts[0].name.to_string(), "slug_mount");
1332 }
1333
1334 #[test]
1335 fn partition_methods_respects_skip() {
1336 let impl_block: ItemImpl = syn::parse_quote! {
1337 impl Router {
1338 fn keep(&self) -> String { String::new() }
1339 fn skip_me(&self) -> String { String::new() }
1340 }
1341 };
1342 let methods = extract_methods(&impl_block).unwrap();
1343 let partitioned = partition_methods(&methods, |m| m.name == "skip_me");
1344
1345 assert_eq!(partitioned.leaf.len(), 1);
1346 assert_eq!(partitioned.leaf[0].name.to_string(), "keep");
1347 }
1348
1349 #[test]
1352 fn get_impl_name_extracts_struct_name() {
1353 let impl_block: ItemImpl = syn::parse_quote! {
1354 impl MyService {
1355 fn hello(&self) {}
1356 }
1357 };
1358 let name = get_impl_name(&impl_block).unwrap();
1359 assert_eq!(name.to_string(), "MyService");
1360 }
1361
1362 #[test]
1363 fn get_impl_name_with_generics() {
1364 let impl_block: ItemImpl = syn::parse_quote! {
1365 impl MyService<T> {
1366 fn hello(&self) {}
1367 }
1368 };
1369 let name = get_impl_name(&impl_block).unwrap();
1370 assert_eq!(name.to_string(), "MyService");
1371 }
1372
1373 #[test]
1376 fn ident_str_strips_raw_prefix() {
1377 let ident: Ident = syn::parse_quote!(r#type);
1378 assert_eq!(ident_str(&ident), "type");
1379 }
1380
1381 #[test]
1382 fn ident_str_leaves_normal_ident_unchanged() {
1383 let ident: Ident = syn::parse_quote!(name);
1384 assert_eq!(ident_str(&ident), "name");
1385 }
1386
1387 #[test]
1388 fn name_str_strips_raw_prefix_on_param() {
1389 let method: ImplItemFn = syn::parse_quote! {
1390 fn get(&self, r#type: String) -> String { r#type }
1391 };
1392 let info = MethodInfo::parse(&method).unwrap().unwrap();
1393 assert_eq!(info.params[0].name_str(), "type");
1394 assert_eq!(info.params[0].name.to_string(), "r#type");
1396 }
1397}