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