1use syn::{
12 FnArg, GenericArgument, Ident, ImplItem, ImplItemFn, ItemImpl, Lit, Meta, Pat, PathArguments,
13 ReturnType, Type, TypeReference,
14};
15
16#[derive(Debug, Clone)]
25pub struct MethodInfo {
26 pub method: ImplItemFn,
28 pub name: Ident,
30 pub docs: Option<String>,
32 pub params: Vec<ParamInfo>,
34 pub return_info: ReturnInfo,
36 pub is_async: bool,
38 pub group: Option<String>,
40 pub wire_name: Option<String>,
43 pub cfg_attrs: Vec<syn::Attribute>,
45}
46
47#[derive(Debug, Clone)]
52pub struct GroupRegistry {
53 pub groups: Vec<(String, String)>,
56}
57
58#[derive(Debug, Clone)]
60pub struct ParamInfo {
61 pub name: Ident,
63 pub ty: Type,
65 pub is_optional: bool,
67 pub is_bool: bool,
69 pub is_vec: bool,
71 pub vec_inner: Option<Type>,
73 pub is_id: bool,
75 pub wire_name: Option<String>,
77 pub location: Option<ParamLocation>,
79 pub default_value: Option<String>,
81 pub short_flag: Option<char>,
83 pub help_text: Option<String>,
85 pub is_positional: bool,
87}
88
89impl MethodInfo {
90 pub fn name_str(&self) -> String {
96 ident_str(&self.name)
97 }
98
99 pub fn wire_name_or(&self, transform: impl FnOnce(String) -> String) -> String {
113 if let Some(ref wn) = self.wire_name {
114 wn.clone()
115 } else {
116 transform(self.name_str())
117 }
118 }
119}
120
121impl ParamInfo {
122 pub fn name_str(&self) -> String {
127 ident_str(&self.name)
128 }
129}
130
131pub fn ident_str(ident: &Ident) -> String {
137 let s = ident.to_string();
138 s.strip_prefix("r#").map(str::to_string).unwrap_or(s)
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub enum HttpMethod {
147 Get,
148 Post,
149 Put,
150 Patch,
151 Delete,
152}
153
154impl HttpMethod {
155 pub fn as_str(&self) -> &'static str {
157 match self {
158 HttpMethod::Get => "GET",
159 HttpMethod::Post => "POST",
160 HttpMethod::Put => "PUT",
161 HttpMethod::Patch => "PATCH",
162 HttpMethod::Delete => "DELETE",
163 }
164 }
165
166 pub fn parse(s: &str) -> Option<Self> {
168 match s.to_uppercase().as_str() {
169 "GET" => Some(HttpMethod::Get),
170 "POST" => Some(HttpMethod::Post),
171 "PUT" => Some(HttpMethod::Put),
172 "PATCH" => Some(HttpMethod::Patch),
173 "DELETE" => Some(HttpMethod::Delete),
174 _ => None,
175 }
176 }
177}
178
179#[derive(Debug, Clone, PartialEq)]
181pub enum ParamLocation {
182 Query,
183 Path,
184 Body,
185 Header,
186}
187
188#[derive(Debug, Clone)]
190pub struct ReturnInfo {
191 pub ty: Option<Type>,
193 pub ok_type: Option<Type>,
195 pub err_type: Option<Type>,
197 pub some_type: Option<Type>,
199 pub is_result: bool,
201 pub is_option: bool,
203 pub is_unit: bool,
205 pub is_stream: bool,
207 pub stream_item: Option<Type>,
209 pub is_iterator: bool,
211 pub iterator_item: Option<Type>,
213 pub is_reference: bool,
215 pub reference_inner: Option<Type>,
217}
218
219struct AwaitFinder {
229 found: Option<proc_macro2::Span>,
231}
232
233impl<'ast> syn::visit::Visit<'ast> for AwaitFinder {
234 fn visit_expr_await(&mut self, node: &'ast syn::ExprAwait) {
235 if self.found.is_none() {
236 self.found = Some(node.await_token.span);
237 }
238 syn::visit::visit_expr(self, &node.base);
241 }
242
243 fn visit_expr_async(&mut self, _node: &'ast syn::ExprAsync) {
244 }
247
248 fn visit_expr_closure(&mut self, node: &'ast syn::ExprClosure) {
249 if node.asyncness.is_none() {
252 syn::visit::visit_expr_closure(self, node);
253 }
254 }
255}
256
257impl MethodInfo {
258 pub fn parse(method: &ImplItemFn) -> syn::Result<Option<Self>> {
262 let name = method.sig.ident.clone();
263 let is_async = method.sig.asyncness.is_some();
264
265 let has_receiver = method
267 .sig
268 .inputs
269 .iter()
270 .any(|arg| matches!(arg, FnArg::Receiver(_)));
271 if !has_receiver {
272 return Ok(None);
273 }
274
275 if method.sig.asyncness.is_none() {
279 let mut finder = AwaitFinder { found: None };
280 syn::visit::Visit::visit_block(&mut finder, &method.block);
281 if let Some(span) = finder.found {
282 return Err(syn::Error::new(
283 span,
284 "this method uses `.await` but is not declared `async`\n\n\
285 server-less projects each method onto a protocol surface; an \
286 awaiting method must be `async` so the projection can drive it.\n\n\
287 Hint: add `async` to the signature, e.g. `async fn NAME(&self, ...) -> ...`",
288 ));
289 }
290 }
291
292 let docs = extract_docs(&method.attrs);
294
295 let params = parse_params(&method.sig.inputs)?;
297
298 let return_info = parse_return_type(&method.sig.output);
300
301 let group = extract_server_group(&method.attrs);
303
304 let wire_name = extract_wire_name(&method.attrs);
306
307 let cfg_attrs: Vec<syn::Attribute> = method
309 .attrs
310 .iter()
311 .filter(|a| a.path().is_ident("cfg"))
312 .cloned()
313 .collect();
314
315 Ok(Some(Self {
316 method: method.clone(),
317 name,
318 docs,
319 params,
320 return_info,
321 is_async,
322 group,
323 wire_name,
324 cfg_attrs,
325 }))
326 }
327}
328
329pub fn extract_docs(attrs: &[syn::Attribute]) -> Option<String> {
331 let docs: Vec<String> = attrs
332 .iter()
333 .filter_map(|attr| {
334 if attr.path().is_ident("doc")
335 && let Meta::NameValue(meta) = &attr.meta
336 && let syn::Expr::Lit(syn::ExprLit {
337 lit: Lit::Str(s), ..
338 }) = &meta.value
339 {
340 return Some(s.value().trim().to_string());
341 }
342 None
343 })
344 .collect();
345
346 if docs.is_empty() {
347 None
348 } else {
349 Some(docs.join("\n"))
350 }
351}
352
353const PROTOCOL_ATTRS: &[&str] = &[
355 "server", "cli", "http", "mcp", "jsonrpc", "grpc", "ws", "graphql", "tool",
356];
357
358fn extract_wire_name(attrs: &[syn::Attribute]) -> Option<String> {
363 for attr in attrs {
364 let is_protocol = attr
365 .path()
366 .get_ident()
367 .is_some_and(|id| PROTOCOL_ATTRS.iter().any(|p| id == p));
368 if !is_protocol {
369 continue;
370 }
371 let mut found = None;
372 let _ = attr.parse_nested_meta(|meta| {
373 if meta.path.is_ident("name") {
374 let value = meta.value()?;
375 let s: syn::LitStr = value.parse()?;
376 found = Some(s.value());
377 } else if meta.input.peek(syn::Token![=]) {
378 let _: proc_macro2::TokenStream = meta.value()?.parse()?;
379 }
380 Ok(())
381 });
382 if found.is_some() {
383 return found;
384 }
385 }
386 None
387}
388
389fn extract_server_group(attrs: &[syn::Attribute]) -> Option<String> {
391 for attr in attrs {
392 if attr.path().is_ident("server") {
393 let mut group = None;
394 let _ = attr.parse_nested_meta(|meta| {
395 if meta.path.is_ident("group") {
396 let value = meta.value()?;
397 let s: syn::LitStr = value.parse()?;
398 group = Some(s.value());
399 } else if meta.input.peek(syn::Token![=]) {
400 let _: proc_macro2::TokenStream = meta.value()?.parse()?;
402 }
403 Ok(())
404 });
405 if group.is_some() {
406 return group;
407 }
408 }
409 }
410 None
411}
412
413pub fn extract_groups(impl_block: &ItemImpl) -> syn::Result<Option<GroupRegistry>> {
418 for attr in &impl_block.attrs {
419 if attr.path().is_ident("server") {
420 let mut groups = Vec::new();
421 let mut found_groups = false;
422 attr.parse_nested_meta(|meta| {
423 if meta.path.is_ident("groups") {
424 found_groups = true;
425 meta.parse_nested_meta(|inner| {
426 let id = inner
427 .path
428 .get_ident()
429 .ok_or_else(|| inner.error("expected group identifier"))?
430 .to_string();
431 let value = inner.value()?;
432 let display: syn::LitStr = value.parse()?;
433 groups.push((id, display.value()));
434 Ok(())
435 })?;
436 } else if meta.input.peek(syn::Token![=]) {
437 let _: proc_macro2::TokenStream = meta.value()?.parse()?;
438 } else if meta.input.peek(syn::token::Paren) {
439 let _content;
440 syn::parenthesized!(_content in meta.input);
441 }
442 Ok(())
443 })?;
444 if found_groups {
445 return Ok(Some(GroupRegistry { groups }));
446 }
447 }
448 }
449 Ok(None)
450}
451
452pub fn resolve_method_group(
460 method: &MethodInfo,
461 registry: &Option<GroupRegistry>,
462) -> syn::Result<Option<String>> {
463 let group_value = match &method.group {
464 Some(v) => v,
465 None => return Ok(None),
466 };
467
468 let span = method.method.sig.ident.span();
469
470 match registry {
471 Some(reg) => {
472 for (id, display) in ®.groups {
473 if id == group_value {
474 return Ok(Some(display.clone()));
475 }
476 }
477 Err(syn::Error::new(
478 span,
479 format!(
480 "unknown group `{group_value}`; declared groups are: {}",
481 reg.groups
482 .iter()
483 .map(|(id, _)| format!("`{id}`"))
484 .collect::<Vec<_>>()
485 .join(", ")
486 ),
487 ))
488 }
489 None => Err(syn::Error::new(
490 span,
491 format!(
492 "method has `group = \"{group_value}\"` but no `groups(...)` registry is declared on the impl block\n\
493 \n\
494 help: add `#[server(groups({group_value} = \"Display Name\"))]` to the impl block"
495 ),
496 )),
497 }
498}
499
500#[derive(Debug, Clone, Default)]
502pub struct ParsedParamAttrs {
503 pub wire_name: Option<String>,
506 pub location: Option<ParamLocation>,
509 pub default_value: Option<String>,
512 pub short_flag: Option<char>,
515 pub help_text: Option<String>,
518 pub positional: bool,
521 pub env_var: Option<String>,
523 pub file_key: Option<String>,
525 pub nested: bool,
531 pub env_prefix: Option<String>,
536 pub nested_serde: bool,
544}
545
546#[allow(clippy::needless_range_loop)]
548pub fn levenshtein(a: &str, b: &str) -> usize {
549 let a: Vec<char> = a.chars().collect();
550 let b: Vec<char> = b.chars().collect();
551 let m = a.len();
552 let n = b.len();
553 let mut dp = vec![vec![0usize; n + 1]; m + 1];
554 for i in 0..=m {
555 dp[i][0] = i;
556 }
557 for j in 0..=n {
558 dp[0][j] = j;
559 }
560 for i in 1..=m {
561 for j in 1..=n {
562 dp[i][j] = if a[i - 1] == b[j - 1] {
563 dp[i - 1][j - 1]
564 } else {
565 1 + dp[i - 1][j - 1].min(dp[i - 1][j]).min(dp[i][j - 1])
566 };
567 }
568 }
569 dp[m][n]
570}
571
572pub fn did_you_mean<'a>(input: &str, candidates: &[&'a str]) -> Option<&'a str> {
574 candidates
575 .iter()
576 .filter_map(|&c| {
577 let d = levenshtein(input, c);
578 if d <= 2 { Some((d, c)) } else { None }
579 })
580 .min_by_key(|&(d, _)| d)
581 .map(|(_, c)| c)
582}
583
584pub fn parse_param_attrs(attrs: &[syn::Attribute]) -> syn::Result<ParsedParamAttrs> {
586 let mut wire_name = None;
587 let mut location = None;
588 let mut default_value = None;
589 let mut short_flag = None;
590 let mut help_text = None;
591 let mut positional = false;
592 let mut env_var = None;
593 let mut file_key = None;
594 let mut nested = false;
595 let mut env_prefix = None;
596 let mut nested_serde = false;
597
598 for attr in attrs {
599 if !attr.path().is_ident("param") {
600 continue;
601 }
602
603 attr.parse_nested_meta(|meta| {
604 if meta.path.is_ident("name") {
606 let value: syn::LitStr = meta.value()?.parse()?;
607 wire_name = Some(value.value());
608 Ok(())
609 }
610 else if meta.path.is_ident("default") {
612 let value = meta.value()?;
614 let lookahead = value.lookahead1();
615 if lookahead.peek(syn::LitStr) {
616 let lit: syn::LitStr = value.parse()?;
617 default_value = Some(format!("\"{}\"", lit.value()));
618 } else if lookahead.peek(syn::LitInt) {
619 let lit: syn::LitInt = value.parse()?;
620 default_value = Some(lit.to_string());
621 } else if lookahead.peek(syn::LitBool) {
622 let lit: syn::LitBool = value.parse()?;
623 default_value = Some(lit.value.to_string());
624 } else {
625 return Err(lookahead.error());
626 }
627 Ok(())
628 }
629 else if meta.path.is_ident("query") {
631 location = Some(ParamLocation::Query);
632 Ok(())
633 } else if meta.path.is_ident("path") {
634 location = Some(ParamLocation::Path);
635 Ok(())
636 } else if meta.path.is_ident("body") {
637 location = Some(ParamLocation::Body);
638 Ok(())
639 } else if meta.path.is_ident("header") {
640 location = Some(ParamLocation::Header);
641 Ok(())
642 }
643 else if meta.path.is_ident("short") {
645 let value: syn::LitChar = meta.value()?.parse()?;
646 short_flag = Some(value.value());
647 Ok(())
648 }
649 else if meta.path.is_ident("help") {
651 let value: syn::LitStr = meta.value()?.parse()?;
652 help_text = Some(value.value());
653 Ok(())
654 }
655 else if meta.path.is_ident("positional") {
657 positional = true;
658 Ok(())
659 }
660 else if meta.path.is_ident("env") {
662 let value: syn::LitStr = meta.value()?.parse()?;
663 env_var = Some(value.value());
664 Ok(())
665 }
666 else if meta.path.is_ident("file_key") {
668 let value: syn::LitStr = meta.value()?.parse()?;
669 file_key = Some(value.value());
670 Ok(())
671 }
672 else if meta.path.is_ident("nested") {
674 nested = true;
675 Ok(())
676 }
677 else if meta.path.is_ident("serde") {
679 nested_serde = true;
680 Ok(())
681 }
682 else if meta.path.is_ident("env_prefix") {
684 let value: syn::LitStr = meta.value()?.parse()?;
685 env_prefix = Some(value.value());
686 Ok(())
687 } else {
688 const VALID: &[&str] = &[
689 "name", "default", "query", "path", "body", "header", "short", "help",
690 "positional", "env", "file_key", "nested", "serde", "env_prefix",
691 ];
692 let unknown = meta
693 .path
694 .get_ident()
695 .map(|i| i.to_string())
696 .unwrap_or_default();
697 let suggestion = did_you_mean(&unknown, VALID)
698 .map(|s| format!(" — did you mean `{s}`?"))
699 .unwrap_or_default();
700 Err(meta.error(format!(
701 "unknown attribute `{unknown}`{suggestion}\n\
702 \n\
703 Valid attributes: name, default, query, path, body, header, short, help, positional, env, file_key, nested, serde, env_prefix\n\
704 \n\
705 Examples:\n\
706 - #[param(name = \"q\")]\n\
707 - #[param(default = 10)]\n\
708 - #[param(query)]\n\
709 - #[param(header, name = \"X-API-Key\")]\n\
710 - #[param(short = 'v')]\n\
711 - #[param(help = \"Enable verbose output\")]\n\
712 - #[param(positional)]\n\
713 - #[param(env = \"MY_VAR\")]\n\
714 - #[param(file_key = \"database.host\")]\n\
715 - #[param(nested)]\n\
716 - #[param(nested, serde)]\n\
717 - #[param(nested, env_prefix = \"SEARCH\")]"
718 )))
719 }
720 })?;
721 }
722
723 if nested_serde {
726 nested = true;
727 }
728
729 Ok(ParsedParamAttrs {
730 wire_name,
731 location,
732 default_value,
733 short_flag,
734 help_text,
735 positional,
736 env_var,
737 file_key,
738 nested,
739 env_prefix,
740 nested_serde,
741 })
742}
743
744pub fn parse_params(
746 inputs: &syn::punctuated::Punctuated<FnArg, syn::Token![,]>,
747) -> syn::Result<Vec<ParamInfo>> {
748 let mut params = Vec::new();
749
750 for arg in inputs {
751 match arg {
752 FnArg::Receiver(_) => continue, FnArg::Typed(pat_type) => {
754 let name = match pat_type.pat.as_ref() {
755 Pat::Ident(pat_ident) => pat_ident.ident.clone(),
756 other => {
757 return Err(syn::Error::new_spanned(
758 other,
759 "unsupported parameter pattern\n\
760 \n\
761 Server-less macros require simple parameter names.\n\
762 Use: name: String\n\
763 Not: (name, _): (String, i32) or &name: &String",
764 ));
765 }
766 };
767
768 let ty = (*pat_type.ty).clone();
769 let is_optional = is_option_type(&ty);
770 let is_bool = is_bool_type(&ty);
771 let vec_inner = extract_vec_type(&ty);
772 let is_vec = vec_inner.is_some();
773 let is_id = is_id_param(&name);
774
775 let parsed = parse_param_attrs(&pat_type.attrs)?;
777
778 let is_positional = parsed.positional || is_id;
780
781 params.push(ParamInfo {
782 name,
783 ty,
784 is_optional,
785 is_bool,
786 is_vec,
787 vec_inner,
788 is_id,
789 is_positional,
790 wire_name: parsed.wire_name,
791 location: parsed.location,
792 default_value: parsed.default_value,
793 short_flag: parsed.short_flag,
794 help_text: parsed.help_text,
795 });
796 }
797 }
798 }
799
800 Ok(params)
801}
802
803pub fn parse_return_type(output: &ReturnType) -> ReturnInfo {
805 match output {
806 ReturnType::Default => ReturnInfo {
807 ty: None,
808 ok_type: None,
809 err_type: None,
810 some_type: None,
811 is_result: false,
812 is_option: false,
813 is_unit: true,
814 is_stream: false,
815 stream_item: None,
816 is_iterator: false,
817 iterator_item: None,
818 is_reference: false,
819 reference_inner: None,
820 },
821 ReturnType::Type(_, ty) => {
822 let ty = ty.as_ref().clone();
823
824 if let Some((ok, err)) = extract_result_types(&ty) {
826 return ReturnInfo {
827 ty: Some(ty),
828 ok_type: Some(ok),
829 err_type: Some(err),
830 some_type: None,
831 is_result: true,
832 is_option: false,
833 is_unit: false,
834 is_stream: false,
835 stream_item: None,
836 is_iterator: false,
837 iterator_item: None,
838 is_reference: false,
839 reference_inner: None,
840 };
841 }
842
843 if let Some(inner) = extract_option_type(&ty) {
845 return ReturnInfo {
846 ty: Some(ty),
847 ok_type: None,
848 err_type: None,
849 some_type: Some(inner),
850 is_result: false,
851 is_option: true,
852 is_unit: false,
853 is_stream: false,
854 stream_item: None,
855 is_iterator: false,
856 iterator_item: None,
857 is_reference: false,
858 reference_inner: None,
859 };
860 }
861
862 if let Some(item) = extract_stream_item(&ty) {
864 return ReturnInfo {
865 ty: Some(ty),
866 ok_type: None,
867 err_type: None,
868 some_type: None,
869 is_result: false,
870 is_option: false,
871 is_unit: false,
872 is_stream: true,
873 stream_item: Some(item),
874 is_iterator: false,
875 iterator_item: None,
876 is_reference: false,
877 reference_inner: None,
878 };
879 }
880
881 if let Some(item) = extract_iterator_item(&ty) {
883 return ReturnInfo {
884 ty: Some(ty),
885 ok_type: None,
886 err_type: None,
887 some_type: None,
888 is_result: false,
889 is_option: false,
890 is_unit: false,
891 is_stream: false,
892 stream_item: None,
893 is_iterator: true,
894 iterator_item: Some(item),
895 is_reference: false,
896 reference_inner: None,
897 };
898 }
899
900 if is_unit_type(&ty) {
902 return ReturnInfo {
903 ty: Some(ty),
904 ok_type: None,
905 err_type: None,
906 some_type: None,
907 is_result: false,
908 is_option: false,
909 is_unit: true,
910 is_stream: false,
911 stream_item: None,
912 is_iterator: false,
913 iterator_item: None,
914 is_reference: false,
915 reference_inner: None,
916 };
917 }
918
919 if let Type::Reference(TypeReference { elem, .. }) = &ty {
921 let inner = elem.as_ref().clone();
922 return ReturnInfo {
923 ty: Some(ty),
924 ok_type: None,
925 err_type: None,
926 some_type: None,
927 is_result: false,
928 is_option: false,
929 is_unit: false,
930 is_stream: false,
931 stream_item: None,
932 is_iterator: false,
933 iterator_item: None,
934 is_reference: true,
935 reference_inner: Some(inner),
936 };
937 }
938
939 ReturnInfo {
941 ty: Some(ty),
942 ok_type: None,
943 err_type: None,
944 some_type: None,
945 is_result: false,
946 is_option: false,
947 is_unit: false,
948 is_stream: false,
949 stream_item: None,
950 is_iterator: false,
951 iterator_item: None,
952 is_reference: false,
953 reference_inner: None,
954 }
955 }
956 }
957}
958
959pub fn is_bool_type(ty: &Type) -> bool {
961 if let Type::Path(type_path) = ty
962 && let Some(segment) = type_path.path.segments.last()
963 && type_path.path.segments.len() == 1
964 {
965 return segment.ident == "bool";
966 }
967 false
968}
969
970pub fn extract_vec_type(ty: &Type) -> Option<Type> {
972 if let Type::Path(type_path) = ty
973 && let Some(segment) = type_path.path.segments.last()
974 && segment.ident == "Vec"
975 && let PathArguments::AngleBracketed(args) = &segment.arguments
976 && let Some(GenericArgument::Type(inner)) = args.args.first()
977 {
978 return Some(inner.clone());
979 }
980 None
981}
982
983pub fn extract_map_type(ty: &Type) -> Option<(Type, Type)> {
985 if let Type::Path(type_path) = ty
986 && let Some(segment) = type_path.path.segments.last()
987 && (segment.ident == "HashMap" || segment.ident == "BTreeMap")
988 && let PathArguments::AngleBracketed(args) = &segment.arguments
989 {
990 let mut iter = args.args.iter();
991 if let (Some(GenericArgument::Type(key)), Some(GenericArgument::Type(val))) =
992 (iter.next(), iter.next())
993 {
994 return Some((key.clone(), val.clone()));
995 }
996 }
997 None
998}
999
1000pub fn extract_option_type(ty: &Type) -> Option<Type> {
1002 if let Type::Path(type_path) = ty
1003 && let Some(segment) = type_path.path.segments.last()
1004 && segment.ident == "Option"
1005 && let PathArguments::AngleBracketed(args) = &segment.arguments
1006 && let Some(GenericArgument::Type(inner)) = args.args.first()
1007 {
1008 return Some(inner.clone());
1009 }
1010 None
1011}
1012
1013pub fn is_option_type(ty: &Type) -> bool {
1015 extract_option_type(ty).is_some()
1016}
1017
1018pub fn unwrap_option_type(ty: &Type) -> Option<&Type> {
1020 if let Type::Path(type_path) = ty {
1021 let seg = type_path.path.segments.last()?;
1022 if seg.ident != "Option" { return None; }
1023 if let PathArguments::AngleBracketed(args) = &seg.arguments
1024 && let Some(GenericArgument::Type(inner)) = args.args.first() {
1025 return Some(inner);
1026 }
1027 }
1028 None
1029}
1030
1031pub fn unwrap_vec_type(ty: &Type) -> Option<&Type> {
1033 if let Type::Path(type_path) = ty {
1034 let seg = type_path.path.segments.last()?;
1035 if seg.ident != "Vec" { return None; }
1036 if let PathArguments::AngleBracketed(args) = &seg.arguments
1037 && let Some(GenericArgument::Type(inner)) = args.args.first() {
1038 return Some(inner);
1039 }
1040 }
1041 None
1042}
1043
1044pub fn unwrap_result_ok_type(ty: &Type) -> Option<&Type> {
1046 if let Type::Path(type_path) = ty {
1047 let seg = type_path.path.segments.last()?;
1048 if seg.ident != "Result" { return None; }
1049 if let PathArguments::AngleBracketed(args) = &seg.arguments
1050 && let Some(GenericArgument::Type(inner)) = args.args.first() {
1051 return Some(inner);
1052 }
1053 }
1054 None
1055}
1056
1057pub fn extract_result_types(ty: &Type) -> Option<(Type, Type)> {
1059 if let Type::Path(type_path) = ty
1060 && let Some(segment) = type_path.path.segments.last()
1061 && segment.ident == "Result"
1062 && let PathArguments::AngleBracketed(args) = &segment.arguments
1063 {
1064 let mut iter = args.args.iter();
1065 if let (Some(GenericArgument::Type(ok)), Some(GenericArgument::Type(err))) =
1066 (iter.next(), iter.next())
1067 {
1068 return Some((ok.clone(), err.clone()));
1069 }
1070 }
1071 None
1072}
1073
1074pub fn extract_stream_item(ty: &Type) -> Option<Type> {
1076 if let Type::ImplTrait(impl_trait) = ty {
1077 for bound in &impl_trait.bounds {
1078 if let syn::TypeParamBound::Trait(trait_bound) = bound
1079 && let Some(segment) = trait_bound.path.segments.last()
1080 && segment.ident == "Stream"
1081 && let PathArguments::AngleBracketed(args) = &segment.arguments
1082 {
1083 for arg in &args.args {
1084 if let GenericArgument::AssocType(assoc) = arg
1085 && assoc.ident == "Item"
1086 {
1087 return Some(assoc.ty.clone());
1088 }
1089 }
1090 }
1091 }
1092 }
1093 None
1094}
1095
1096pub fn extract_iterator_item(ty: &Type) -> Option<Type> {
1098 if let Type::ImplTrait(impl_trait) = ty {
1099 for bound in &impl_trait.bounds {
1100 if let syn::TypeParamBound::Trait(trait_bound) = bound
1101 && let Some(segment) = trait_bound.path.segments.last()
1102 && segment.ident == "Iterator"
1103 && let PathArguments::AngleBracketed(args) = &segment.arguments
1104 {
1105 for arg in &args.args {
1106 if let GenericArgument::AssocType(assoc) = arg
1107 && assoc.ident == "Item"
1108 {
1109 return Some(assoc.ty.clone());
1110 }
1111 }
1112 }
1113 }
1114 }
1115 None
1116}
1117
1118pub fn is_unit_type(ty: &Type) -> bool {
1120 if let Type::Tuple(tuple) = ty {
1121 return tuple.elems.is_empty();
1122 }
1123 false
1124}
1125
1126pub fn is_id_param(name: &Ident) -> bool {
1128 let name_str = ident_str(name);
1129 name_str == "id" || name_str.ends_with("_id")
1130}
1131
1132pub fn extract_methods(impl_block: &ItemImpl) -> syn::Result<Vec<MethodInfo>> {
1138 let mut methods = Vec::new();
1139
1140 for item in &impl_block.items {
1141 if let ImplItem::Fn(method) = item {
1142 if method.sig.ident.to_string().starts_with('_') {
1144 continue;
1145 }
1146 if let Some(info) = MethodInfo::parse(method)? {
1148 methods.push(info);
1149 }
1150 }
1151 }
1152
1153 Ok(methods)
1154}
1155
1156pub struct PartitionedMethods<'a> {
1161 pub leaf: Vec<&'a MethodInfo>,
1163 pub static_mounts: Vec<&'a MethodInfo>,
1165 pub slug_mounts: Vec<&'a MethodInfo>,
1167}
1168
1169pub fn partition_methods<'a>(
1174 methods: &'a [MethodInfo],
1175 skip: impl Fn(&MethodInfo) -> bool,
1176) -> PartitionedMethods<'a> {
1177 let mut result = PartitionedMethods {
1178 leaf: Vec::new(),
1179 static_mounts: Vec::new(),
1180 slug_mounts: Vec::new(),
1181 };
1182
1183 for method in methods {
1184 if skip(method) {
1185 continue;
1186 }
1187
1188 if method.return_info.is_reference && !method.is_async {
1189 if method.params.is_empty() {
1190 result.static_mounts.push(method);
1191 } else {
1192 result.slug_mounts.push(method);
1193 }
1194 } else {
1195 result.leaf.push(method);
1196 }
1197 }
1198
1199 result
1200}
1201
1202pub fn get_impl_name(impl_block: &ItemImpl) -> syn::Result<Ident> {
1204 if let Type::Path(type_path) = impl_block.self_ty.as_ref()
1205 && let Some(segment) = type_path.path.segments.last()
1206 {
1207 return Ok(segment.ident.clone());
1208 }
1209 Err(syn::Error::new_spanned(
1210 &impl_block.self_ty,
1211 "Expected a simple type name",
1212 ))
1213}
1214
1215#[cfg(test)]
1216mod tests {
1217 use super::*;
1218 use quote::quote;
1219
1220 #[test]
1223 fn extract_docs_returns_none_when_no_doc_attrs() {
1224 let method: ImplItemFn = syn::parse_quote! {
1225 fn hello(&self) {}
1226 };
1227 assert!(extract_docs(&method.attrs).is_none());
1228 }
1229
1230 #[test]
1231 fn extract_docs_extracts_single_line() {
1232 let method: ImplItemFn = syn::parse_quote! {
1233 fn hello(&self) {}
1235 };
1236 assert_eq!(extract_docs(&method.attrs).unwrap(), "Hello world");
1237 }
1238
1239 #[test]
1240 fn extract_docs_joins_multiple_lines() {
1241 let method: ImplItemFn = syn::parse_quote! {
1242 fn hello(&self) {}
1245 };
1246 assert_eq!(extract_docs(&method.attrs).unwrap(), "Line one\nLine two");
1247 }
1248
1249 #[test]
1250 fn extract_docs_ignores_non_doc_attrs() {
1251 let method: ImplItemFn = syn::parse_quote! {
1252 #[inline]
1253 fn hello(&self) {}
1255 };
1256 assert_eq!(extract_docs(&method.attrs).unwrap(), "Documented");
1257 }
1258
1259 #[test]
1262 fn parse_return_type_default_is_unit() {
1263 let ret: ReturnType = syn::parse_quote! {};
1264 let info = parse_return_type(&ret);
1265 assert!(info.is_unit);
1266 assert!(info.ty.is_none());
1267 assert!(!info.is_result);
1268 assert!(!info.is_option);
1269 assert!(!info.is_reference);
1270 }
1271
1272 #[test]
1273 fn parse_return_type_regular_type() {
1274 let ret: ReturnType = syn::parse_quote! { -> String };
1275 let info = parse_return_type(&ret);
1276 assert!(!info.is_unit);
1277 assert!(!info.is_result);
1278 assert!(!info.is_option);
1279 assert!(!info.is_reference);
1280 assert!(info.ty.is_some());
1281 }
1282
1283 #[test]
1284 fn parse_return_type_result() {
1285 let ret: ReturnType = syn::parse_quote! { -> Result<String, MyError> };
1286 let info = parse_return_type(&ret);
1287 assert!(info.is_result);
1288 assert!(!info.is_option);
1289 assert!(!info.is_unit);
1290
1291 let ok = info.ok_type.unwrap();
1292 assert_eq!(quote!(#ok).to_string(), "String");
1293
1294 let err = info.err_type.unwrap();
1295 assert_eq!(quote!(#err).to_string(), "MyError");
1296 }
1297
1298 #[test]
1299 fn parse_return_type_option() {
1300 let ret: ReturnType = syn::parse_quote! { -> Option<i32> };
1301 let info = parse_return_type(&ret);
1302 assert!(info.is_option);
1303 assert!(!info.is_result);
1304 assert!(!info.is_unit);
1305
1306 let some = info.some_type.unwrap();
1307 assert_eq!(quote!(#some).to_string(), "i32");
1308 }
1309
1310 #[test]
1311 fn parse_return_type_unit_tuple() {
1312 let ret: ReturnType = syn::parse_quote! { -> () };
1313 let info = parse_return_type(&ret);
1314 assert!(info.is_unit);
1315 assert!(info.ty.is_some());
1316 }
1317
1318 #[test]
1319 fn parse_return_type_reference() {
1320 let ret: ReturnType = syn::parse_quote! { -> &SubRouter };
1321 let info = parse_return_type(&ret);
1322 assert!(info.is_reference);
1323 assert!(!info.is_unit);
1324
1325 let inner = info.reference_inner.unwrap();
1326 assert_eq!(quote!(#inner).to_string(), "SubRouter");
1327 }
1328
1329 #[test]
1330 fn parse_return_type_stream() {
1331 let ret: ReturnType = syn::parse_quote! { -> impl Stream<Item = u64> };
1332 let info = parse_return_type(&ret);
1333 assert!(info.is_stream);
1334 assert!(!info.is_result);
1335
1336 let item = info.stream_item.unwrap();
1337 assert_eq!(quote!(#item).to_string(), "u64");
1338 }
1339
1340 #[test]
1343 fn is_option_type_true() {
1344 let ty: Type = syn::parse_quote! { Option<String> };
1345 assert!(is_option_type(&ty));
1346 let inner = extract_option_type(&ty).unwrap();
1347 assert_eq!(quote!(#inner).to_string(), "String");
1348 }
1349
1350 #[test]
1351 fn is_option_type_false_for_non_option() {
1352 let ty: Type = syn::parse_quote! { String };
1353 assert!(!is_option_type(&ty));
1354 assert!(extract_option_type(&ty).is_none());
1355 }
1356
1357 #[test]
1360 fn extract_result_types_works() {
1361 let ty: Type = syn::parse_quote! { Result<Vec<u8>, std::io::Error> };
1362 let (ok, err) = extract_result_types(&ty).unwrap();
1363 assert_eq!(quote!(#ok).to_string(), "Vec < u8 >");
1364 assert_eq!(quote!(#err).to_string(), "std :: io :: Error");
1365 }
1366
1367 #[test]
1368 fn extract_result_types_none_for_non_result() {
1369 let ty: Type = syn::parse_quote! { Option<i32> };
1370 assert!(extract_result_types(&ty).is_none());
1371 }
1372
1373 #[test]
1376 fn is_unit_type_true() {
1377 let ty: Type = syn::parse_quote! { () };
1378 assert!(is_unit_type(&ty));
1379 }
1380
1381 #[test]
1382 fn is_unit_type_false_for_non_tuple() {
1383 let ty: Type = syn::parse_quote! { String };
1384 assert!(!is_unit_type(&ty));
1385 }
1386
1387 #[test]
1388 fn is_unit_type_false_for_nonempty_tuple() {
1389 let ty: Type = syn::parse_quote! { (i32, i32) };
1390 assert!(!is_unit_type(&ty));
1391 }
1392
1393 #[test]
1396 fn is_id_param_exact_id() {
1397 let ident: Ident = syn::parse_quote! { id };
1398 assert!(is_id_param(&ident));
1399 }
1400
1401 #[test]
1402 fn is_id_param_suffix_id() {
1403 let ident: Ident = syn::parse_quote! { user_id };
1404 assert!(is_id_param(&ident));
1405 }
1406
1407 #[test]
1408 fn is_id_param_false_for_other_names() {
1409 let ident: Ident = syn::parse_quote! { name };
1410 assert!(!is_id_param(&ident));
1411 }
1412
1413 #[test]
1414 fn is_id_param_false_for_identity() {
1415 let ident: Ident = syn::parse_quote! { identity };
1417 assert!(!is_id_param(&ident));
1418 }
1419
1420 #[test]
1423 fn method_info_parse_basic() {
1424 let method: ImplItemFn = syn::parse_quote! {
1425 fn greet(&self, name: String) -> String {
1427 format!("Hello {name}")
1428 }
1429 };
1430 let info = MethodInfo::parse(&method).unwrap().unwrap();
1431 assert_eq!(info.name.to_string(), "greet");
1432 assert!(!info.is_async);
1433 assert_eq!(info.docs.as_deref(), Some("Does a thing"));
1434 assert_eq!(info.params.len(), 1);
1435 assert_eq!(info.params[0].name.to_string(), "name");
1436 assert!(!info.params[0].is_optional);
1437 assert!(!info.params[0].is_id);
1438 }
1439
1440 #[test]
1441 fn method_info_parse_async_method() {
1442 let method: ImplItemFn = syn::parse_quote! {
1443 async fn fetch(&self) -> Vec<u8> {
1444 vec![]
1445 }
1446 };
1447 let info = MethodInfo::parse(&method).unwrap().unwrap();
1448 assert!(info.is_async);
1449 }
1450
1451 #[test]
1452 fn method_info_parse_skips_associated_function() {
1453 let method: ImplItemFn = syn::parse_quote! {
1454 fn new() -> Self {
1455 Self
1456 }
1457 };
1458 assert!(MethodInfo::parse(&method).unwrap().is_none());
1459 }
1460
1461 #[test]
1462 fn method_info_parse_optional_param() {
1463 let method: ImplItemFn = syn::parse_quote! {
1464 fn search(&self, query: Option<String>) {}
1465 };
1466 let info = MethodInfo::parse(&method).unwrap().unwrap();
1467 assert!(info.params[0].is_optional);
1468 }
1469
1470 #[test]
1471 fn method_info_parse_id_param() {
1472 let method: ImplItemFn = syn::parse_quote! {
1473 fn get_user(&self, user_id: u64) -> String {
1474 String::new()
1475 }
1476 };
1477 let info = MethodInfo::parse(&method).unwrap().unwrap();
1478 assert!(info.params[0].is_id);
1479 }
1480
1481 #[test]
1482 fn method_info_parse_no_docs() {
1483 let method: ImplItemFn = syn::parse_quote! {
1484 fn bare(&self) {}
1485 };
1486 let info = MethodInfo::parse(&method).unwrap().unwrap();
1487 assert!(info.docs.is_none());
1488 }
1489
1490 #[test]
1493 fn extract_methods_basic() {
1494 let impl_block: ItemImpl = syn::parse_quote! {
1495 impl MyApi {
1496 fn hello(&self) -> String { String::new() }
1497 fn world(&self) -> String { String::new() }
1498 }
1499 };
1500 let methods = extract_methods(&impl_block).unwrap();
1501 assert_eq!(methods.len(), 2);
1502 assert_eq!(methods[0].name.to_string(), "hello");
1503 assert_eq!(methods[1].name.to_string(), "world");
1504 }
1505
1506 #[test]
1507 fn extract_methods_skips_underscore_prefix() {
1508 let impl_block: ItemImpl = syn::parse_quote! {
1509 impl MyApi {
1510 fn public(&self) {}
1511 fn _private(&self) {}
1512 fn __also_private(&self) {}
1513 }
1514 };
1515 let methods = extract_methods(&impl_block).unwrap();
1516 assert_eq!(methods.len(), 1);
1517 assert_eq!(methods[0].name.to_string(), "public");
1518 }
1519
1520 #[test]
1521 fn extract_methods_skips_associated_functions() {
1522 let impl_block: ItemImpl = syn::parse_quote! {
1523 impl MyApi {
1524 fn new() -> Self { Self }
1525 fn from_config(cfg: Config) -> Self { Self }
1526 fn greet(&self) -> String { String::new() }
1527 }
1528 };
1529 let methods = extract_methods(&impl_block).unwrap();
1530 assert_eq!(methods.len(), 1);
1531 assert_eq!(methods[0].name.to_string(), "greet");
1532 }
1533
1534 #[test]
1537 fn partition_methods_splits_correctly() {
1538 let impl_block: ItemImpl = syn::parse_quote! {
1539 impl Router {
1540 fn leaf_action(&self) -> String { String::new() }
1541 fn static_mount(&self) -> &SubRouter { &self.sub }
1542 fn slug_mount(&self, id: u64) -> &SubRouter { &self.sub }
1543 async fn async_ref(&self) -> &SubRouter { &self.sub }
1544 }
1545 };
1546 let methods = extract_methods(&impl_block).unwrap();
1547 let partitioned = partition_methods(&methods, |_| false);
1548
1549 assert_eq!(partitioned.leaf.len(), 2);
1551 assert_eq!(partitioned.leaf[0].name.to_string(), "leaf_action");
1552 assert_eq!(partitioned.leaf[1].name.to_string(), "async_ref");
1553
1554 assert_eq!(partitioned.static_mounts.len(), 1);
1555 assert_eq!(
1556 partitioned.static_mounts[0].name.to_string(),
1557 "static_mount"
1558 );
1559
1560 assert_eq!(partitioned.slug_mounts.len(), 1);
1561 assert_eq!(partitioned.slug_mounts[0].name.to_string(), "slug_mount");
1562 }
1563
1564 #[test]
1565 fn partition_methods_respects_skip() {
1566 let impl_block: ItemImpl = syn::parse_quote! {
1567 impl Router {
1568 fn keep(&self) -> String { String::new() }
1569 fn skip_me(&self) -> String { String::new() }
1570 }
1571 };
1572 let methods = extract_methods(&impl_block).unwrap();
1573 let partitioned = partition_methods(&methods, |m| m.name == "skip_me");
1574
1575 assert_eq!(partitioned.leaf.len(), 1);
1576 assert_eq!(partitioned.leaf[0].name.to_string(), "keep");
1577 }
1578
1579 #[test]
1582 fn get_impl_name_extracts_struct_name() {
1583 let impl_block: ItemImpl = syn::parse_quote! {
1584 impl MyService {
1585 fn hello(&self) {}
1586 }
1587 };
1588 let name = get_impl_name(&impl_block).unwrap();
1589 assert_eq!(name.to_string(), "MyService");
1590 }
1591
1592 #[test]
1593 fn get_impl_name_with_generics() {
1594 let impl_block: ItemImpl = syn::parse_quote! {
1595 impl MyService<T> {
1596 fn hello(&self) {}
1597 }
1598 };
1599 let name = get_impl_name(&impl_block).unwrap();
1600 assert_eq!(name.to_string(), "MyService");
1601 }
1602
1603 #[test]
1606 fn ident_str_strips_raw_prefix() {
1607 let ident: Ident = syn::parse_quote!(r#type);
1608 assert_eq!(ident_str(&ident), "type");
1609 }
1610
1611 #[test]
1612 fn ident_str_leaves_normal_ident_unchanged() {
1613 let ident: Ident = syn::parse_quote!(name);
1614 assert_eq!(ident_str(&ident), "name");
1615 }
1616
1617 #[test]
1618 fn name_str_strips_raw_prefix_on_param() {
1619 let method: ImplItemFn = syn::parse_quote! {
1620 fn get(&self, r#type: String) -> String { r#type }
1621 };
1622 let info = MethodInfo::parse(&method).unwrap().unwrap();
1623 assert_eq!(info.params[0].name_str(), "type");
1624 assert_eq!(info.params[0].name.to_string(), "r#type");
1626 }
1627
1628 #[test]
1631 fn sync_fn_with_top_level_await_is_err() {
1632 let method: ImplItemFn = syn::parse_quote! {
1633 fn f(&self) {
1634 something().await;
1635 }
1636 };
1637 assert!(MethodInfo::parse(&method).is_err());
1638 }
1639
1640 #[test]
1641 fn sync_fn_with_nested_async_block_await_is_ok() {
1642 let method: ImplItemFn = syn::parse_quote! {
1643 fn f(&self, x: Thing) {
1644 let _fut = async { x.await };
1645 }
1646 };
1647 assert!(MethodInfo::parse(&method).is_ok());
1648 }
1649}