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