use syn::{
FnArg, GenericArgument, Ident, ImplItem, ImplItemFn, ItemImpl, Lit, Meta, Pat, PathArguments,
ReturnType, Type, TypeReference,
};
#[derive(Debug, Clone)]
pub struct MethodInfo {
pub method: ImplItemFn,
pub name: Ident,
pub docs: Option<String>,
pub params: Vec<ParamInfo>,
pub return_info: ReturnInfo,
pub is_async: bool,
pub group: Option<String>,
pub wire_name: Option<String>,
pub cfg_attrs: Vec<syn::Attribute>,
}
#[derive(Debug, Clone)]
pub struct GroupRegistry {
pub groups: Vec<(String, String)>,
}
#[derive(Debug, Clone)]
pub struct ParamInfo {
pub name: Ident,
pub ty: Type,
pub is_optional: bool,
pub is_bool: bool,
pub is_vec: bool,
pub vec_inner: Option<Type>,
pub is_id: bool,
pub wire_name: Option<String>,
pub location: Option<ParamLocation>,
pub default_value: Option<String>,
pub short_flag: Option<char>,
pub help_text: Option<String>,
pub is_positional: bool,
}
impl MethodInfo {
pub fn name_str(&self) -> String {
ident_str(&self.name)
}
pub fn wire_name_or(&self, transform: impl FnOnce(String) -> String) -> String {
if let Some(ref wn) = self.wire_name {
wn.clone()
} else {
transform(self.name_str())
}
}
}
impl ParamInfo {
pub fn name_str(&self) -> String {
ident_str(&self.name)
}
}
pub fn ident_str(ident: &Ident) -> String {
let s = ident.to_string();
s.strip_prefix("r#").map(str::to_string).unwrap_or(s)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HttpMethod {
Get,
Post,
Put,
Patch,
Delete,
}
impl HttpMethod {
pub fn as_str(&self) -> &'static str {
match self {
HttpMethod::Get => "GET",
HttpMethod::Post => "POST",
HttpMethod::Put => "PUT",
HttpMethod::Patch => "PATCH",
HttpMethod::Delete => "DELETE",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s.to_uppercase().as_str() {
"GET" => Some(HttpMethod::Get),
"POST" => Some(HttpMethod::Post),
"PUT" => Some(HttpMethod::Put),
"PATCH" => Some(HttpMethod::Patch),
"DELETE" => Some(HttpMethod::Delete),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ParamLocation {
Query,
Path,
Body,
Header,
}
#[derive(Debug, Clone)]
pub struct ReturnInfo {
pub ty: Option<Type>,
pub ok_type: Option<Type>,
pub err_type: Option<Type>,
pub some_type: Option<Type>,
pub is_result: bool,
pub is_option: bool,
pub is_unit: bool,
pub is_stream: bool,
pub stream_item: Option<Type>,
pub is_iterator: bool,
pub iterator_item: Option<Type>,
pub is_reference: bool,
pub reference_inner: Option<Type>,
}
struct AwaitFinder {
found: Option<proc_macro2::Span>,
}
impl<'ast> syn::visit::Visit<'ast> for AwaitFinder {
fn visit_expr_await(&mut self, node: &'ast syn::ExprAwait) {
if self.found.is_none() {
self.found = Some(node.await_token.span);
}
syn::visit::visit_expr(self, &node.base);
}
fn visit_expr_async(&mut self, _node: &'ast syn::ExprAsync) {
}
fn visit_expr_closure(&mut self, node: &'ast syn::ExprClosure) {
if node.asyncness.is_none() {
syn::visit::visit_expr_closure(self, node);
}
}
}
impl MethodInfo {
pub fn parse(method: &ImplItemFn) -> syn::Result<Option<Self>> {
let name = method.sig.ident.clone();
let is_async = method.sig.asyncness.is_some();
let has_receiver = method
.sig
.inputs
.iter()
.any(|arg| matches!(arg, FnArg::Receiver(_)));
if !has_receiver {
return Ok(None);
}
if method.sig.asyncness.is_none() {
let mut finder = AwaitFinder { found: None };
syn::visit::Visit::visit_block(&mut finder, &method.block);
if let Some(span) = finder.found {
return Err(syn::Error::new(
span,
"this method uses `.await` but is not declared `async`\n\n\
server-less projects each method onto a protocol surface; an \
awaiting method must be `async` so the projection can drive it.\n\n\
Hint: add `async` to the signature, e.g. `async fn NAME(&self, ...) -> ...`",
));
}
}
let docs = extract_docs(&method.attrs);
let params = parse_params(&method.sig.inputs)?;
let return_info = parse_return_type(&method.sig.output);
let group = extract_server_group(&method.attrs);
let wire_name = extract_wire_name(&method.attrs);
let cfg_attrs: Vec<syn::Attribute> = method
.attrs
.iter()
.filter(|a| a.path().is_ident("cfg"))
.cloned()
.collect();
Ok(Some(Self {
method: method.clone(),
name,
docs,
params,
return_info,
is_async,
group,
wire_name,
cfg_attrs,
}))
}
}
pub fn extract_docs(attrs: &[syn::Attribute]) -> Option<String> {
let docs: Vec<String> = attrs
.iter()
.filter_map(|attr| {
if attr.path().is_ident("doc")
&& let Meta::NameValue(meta) = &attr.meta
&& let syn::Expr::Lit(syn::ExprLit {
lit: Lit::Str(s), ..
}) = &meta.value
{
return Some(s.value().trim().to_string());
}
None
})
.collect();
if docs.is_empty() {
None
} else {
Some(docs.join("\n"))
}
}
const PROTOCOL_ATTRS: &[&str] = &[
"server", "cli", "http", "mcp", "jsonrpc", "grpc", "ws", "graphql", "tool",
];
fn extract_wire_name(attrs: &[syn::Attribute]) -> Option<String> {
for attr in attrs {
let is_protocol = attr
.path()
.get_ident()
.is_some_and(|id| PROTOCOL_ATTRS.iter().any(|p| id == p));
if !is_protocol {
continue;
}
let mut found = None;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("name") {
let value = meta.value()?;
let s: syn::LitStr = value.parse()?;
found = Some(s.value());
} else if meta.input.peek(syn::Token![=]) {
let _: proc_macro2::TokenStream = meta.value()?.parse()?;
}
Ok(())
});
if found.is_some() {
return found;
}
}
None
}
fn extract_server_group(attrs: &[syn::Attribute]) -> Option<String> {
for attr in attrs {
if attr.path().is_ident("server") {
let mut group = None;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("group") {
let value = meta.value()?;
let s: syn::LitStr = value.parse()?;
group = Some(s.value());
} else if meta.input.peek(syn::Token![=]) {
let _: proc_macro2::TokenStream = meta.value()?.parse()?;
}
Ok(())
});
if group.is_some() {
return group;
}
}
}
None
}
pub fn extract_groups(impl_block: &ItemImpl) -> syn::Result<Option<GroupRegistry>> {
for attr in &impl_block.attrs {
if attr.path().is_ident("server") {
let mut groups = Vec::new();
let mut found_groups = false;
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("groups") {
found_groups = true;
meta.parse_nested_meta(|inner| {
let id = inner
.path
.get_ident()
.ok_or_else(|| inner.error("expected group identifier"))?
.to_string();
let value = inner.value()?;
let display: syn::LitStr = value.parse()?;
groups.push((id, display.value()));
Ok(())
})?;
} else if meta.input.peek(syn::Token![=]) {
let _: proc_macro2::TokenStream = meta.value()?.parse()?;
} else if meta.input.peek(syn::token::Paren) {
let _content;
syn::parenthesized!(_content in meta.input);
}
Ok(())
})?;
if found_groups {
return Ok(Some(GroupRegistry { groups }));
}
}
}
Ok(None)
}
pub fn resolve_method_group(
method: &MethodInfo,
registry: &Option<GroupRegistry>,
) -> syn::Result<Option<String>> {
let group_value = match &method.group {
Some(v) => v,
None => return Ok(None),
};
let span = method.method.sig.ident.span();
match registry {
Some(reg) => {
for (id, display) in ®.groups {
if id == group_value {
return Ok(Some(display.clone()));
}
}
Err(syn::Error::new(
span,
format!(
"unknown group `{group_value}`; declared groups are: {}",
reg.groups
.iter()
.map(|(id, _)| format!("`{id}`"))
.collect::<Vec<_>>()
.join(", ")
),
))
}
None => Err(syn::Error::new(
span,
format!(
"method has `group = \"{group_value}\"` but no `groups(...)` registry is declared on the impl block\n\
\n\
help: add `#[server(groups({group_value} = \"Display Name\"))]` to the impl block"
),
)),
}
}
#[derive(Debug, Clone, Default)]
pub struct ParsedParamAttrs {
pub wire_name: Option<String>,
pub location: Option<ParamLocation>,
pub default_value: Option<String>,
pub short_flag: Option<char>,
pub help_text: Option<String>,
pub positional: bool,
pub env_var: Option<String>,
pub file_key: Option<String>,
pub nested: bool,
pub env_prefix: Option<String>,
pub nested_serde: bool,
}
#[allow(clippy::needless_range_loop)]
pub fn levenshtein(a: &str, b: &str) -> usize {
let a: Vec<char> = a.chars().collect();
let b: Vec<char> = b.chars().collect();
let m = a.len();
let n = b.len();
let mut dp = vec![vec![0usize; n + 1]; m + 1];
for i in 0..=m {
dp[i][0] = i;
}
for j in 0..=n {
dp[0][j] = j;
}
for i in 1..=m {
for j in 1..=n {
dp[i][j] = if a[i - 1] == b[j - 1] {
dp[i - 1][j - 1]
} else {
1 + dp[i - 1][j - 1].min(dp[i - 1][j]).min(dp[i][j - 1])
};
}
}
dp[m][n]
}
pub fn did_you_mean<'a>(input: &str, candidates: &[&'a str]) -> Option<&'a str> {
candidates
.iter()
.filter_map(|&c| {
let d = levenshtein(input, c);
if d <= 2 { Some((d, c)) } else { None }
})
.min_by_key(|&(d, _)| d)
.map(|(_, c)| c)
}
pub fn parse_param_attrs(attrs: &[syn::Attribute]) -> syn::Result<ParsedParamAttrs> {
let mut wire_name = None;
let mut location = None;
let mut default_value = None;
let mut short_flag = None;
let mut help_text = None;
let mut positional = false;
let mut env_var = None;
let mut file_key = None;
let mut nested = false;
let mut env_prefix = None;
let mut nested_serde = false;
for attr in attrs {
if !attr.path().is_ident("param") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("name") {
let value: syn::LitStr = meta.value()?.parse()?;
wire_name = Some(value.value());
Ok(())
}
else if meta.path.is_ident("default") {
let value = meta.value()?;
let lookahead = value.lookahead1();
if lookahead.peek(syn::LitStr) {
let lit: syn::LitStr = value.parse()?;
default_value = Some(format!("\"{}\"", lit.value()));
} else if lookahead.peek(syn::LitInt) {
let lit: syn::LitInt = value.parse()?;
default_value = Some(lit.to_string());
} else if lookahead.peek(syn::LitBool) {
let lit: syn::LitBool = value.parse()?;
default_value = Some(lit.value.to_string());
} else {
return Err(lookahead.error());
}
Ok(())
}
else if meta.path.is_ident("query") {
location = Some(ParamLocation::Query);
Ok(())
} else if meta.path.is_ident("path") {
location = Some(ParamLocation::Path);
Ok(())
} else if meta.path.is_ident("body") {
location = Some(ParamLocation::Body);
Ok(())
} else if meta.path.is_ident("header") {
location = Some(ParamLocation::Header);
Ok(())
}
else if meta.path.is_ident("short") {
let value: syn::LitChar = meta.value()?.parse()?;
short_flag = Some(value.value());
Ok(())
}
else if meta.path.is_ident("help") {
let value: syn::LitStr = meta.value()?.parse()?;
help_text = Some(value.value());
Ok(())
}
else if meta.path.is_ident("positional") {
positional = true;
Ok(())
}
else if meta.path.is_ident("env") {
let value: syn::LitStr = meta.value()?.parse()?;
env_var = Some(value.value());
Ok(())
}
else if meta.path.is_ident("file_key") {
let value: syn::LitStr = meta.value()?.parse()?;
file_key = Some(value.value());
Ok(())
}
else if meta.path.is_ident("nested") {
nested = true;
Ok(())
}
else if meta.path.is_ident("serde") {
nested_serde = true;
Ok(())
}
else if meta.path.is_ident("env_prefix") {
let value: syn::LitStr = meta.value()?.parse()?;
env_prefix = Some(value.value());
Ok(())
} else {
const VALID: &[&str] = &[
"name", "default", "query", "path", "body", "header", "short", "help",
"positional", "env", "file_key", "nested", "serde", "env_prefix",
];
let unknown = meta
.path
.get_ident()
.map(|i| i.to_string())
.unwrap_or_default();
let suggestion = did_you_mean(&unknown, VALID)
.map(|s| format!(" — did you mean `{s}`?"))
.unwrap_or_default();
Err(meta.error(format!(
"unknown attribute `{unknown}`{suggestion}\n\
\n\
Valid attributes: name, default, query, path, body, header, short, help, positional, env, file_key, nested, serde, env_prefix\n\
\n\
Examples:\n\
- #[param(name = \"q\")]\n\
- #[param(default = 10)]\n\
- #[param(query)]\n\
- #[param(header, name = \"X-API-Key\")]\n\
- #[param(short = 'v')]\n\
- #[param(help = \"Enable verbose output\")]\n\
- #[param(positional)]\n\
- #[param(env = \"MY_VAR\")]\n\
- #[param(file_key = \"database.host\")]\n\
- #[param(nested)]\n\
- #[param(nested, serde)]\n\
- #[param(nested, env_prefix = \"SEARCH\")]"
)))
}
})?;
}
if nested_serde {
nested = true;
}
Ok(ParsedParamAttrs {
wire_name,
location,
default_value,
short_flag,
help_text,
positional,
env_var,
file_key,
nested,
env_prefix,
nested_serde,
})
}
pub fn parse_params(
inputs: &syn::punctuated::Punctuated<FnArg, syn::Token![,]>,
) -> syn::Result<Vec<ParamInfo>> {
let mut params = Vec::new();
for arg in inputs {
match arg {
FnArg::Receiver(_) => continue, FnArg::Typed(pat_type) => {
let name = match pat_type.pat.as_ref() {
Pat::Ident(pat_ident) => pat_ident.ident.clone(),
other => {
return Err(syn::Error::new_spanned(
other,
"unsupported parameter pattern\n\
\n\
Server-less macros require simple parameter names.\n\
Use: name: String\n\
Not: (name, _): (String, i32) or &name: &String",
));
}
};
let ty = (*pat_type.ty).clone();
let is_optional = is_option_type(&ty);
let is_bool = is_bool_type(&ty);
let vec_inner = extract_vec_type(&ty);
let is_vec = vec_inner.is_some();
let is_id = is_id_param(&name);
let parsed = parse_param_attrs(&pat_type.attrs)?;
let is_positional = parsed.positional || is_id;
params.push(ParamInfo {
name,
ty,
is_optional,
is_bool,
is_vec,
vec_inner,
is_id,
is_positional,
wire_name: parsed.wire_name,
location: parsed.location,
default_value: parsed.default_value,
short_flag: parsed.short_flag,
help_text: parsed.help_text,
});
}
}
}
Ok(params)
}
pub fn parse_return_type(output: &ReturnType) -> ReturnInfo {
match output {
ReturnType::Default => ReturnInfo {
ty: None,
ok_type: None,
err_type: None,
some_type: None,
is_result: false,
is_option: false,
is_unit: true,
is_stream: false,
stream_item: None,
is_iterator: false,
iterator_item: None,
is_reference: false,
reference_inner: None,
},
ReturnType::Type(_, ty) => {
let ty = ty.as_ref().clone();
if let Some((ok, err)) = extract_result_types(&ty) {
return ReturnInfo {
ty: Some(ty),
ok_type: Some(ok),
err_type: Some(err),
some_type: None,
is_result: true,
is_option: false,
is_unit: false,
is_stream: false,
stream_item: None,
is_iterator: false,
iterator_item: None,
is_reference: false,
reference_inner: None,
};
}
if let Some(inner) = extract_option_type(&ty) {
return ReturnInfo {
ty: Some(ty),
ok_type: None,
err_type: None,
some_type: Some(inner),
is_result: false,
is_option: true,
is_unit: false,
is_stream: false,
stream_item: None,
is_iterator: false,
iterator_item: None,
is_reference: false,
reference_inner: None,
};
}
if let Some(item) = extract_stream_item(&ty) {
return ReturnInfo {
ty: Some(ty),
ok_type: None,
err_type: None,
some_type: None,
is_result: false,
is_option: false,
is_unit: false,
is_stream: true,
stream_item: Some(item),
is_iterator: false,
iterator_item: None,
is_reference: false,
reference_inner: None,
};
}
if let Some(item) = extract_iterator_item(&ty) {
return ReturnInfo {
ty: Some(ty),
ok_type: None,
err_type: None,
some_type: None,
is_result: false,
is_option: false,
is_unit: false,
is_stream: false,
stream_item: None,
is_iterator: true,
iterator_item: Some(item),
is_reference: false,
reference_inner: None,
};
}
if is_unit_type(&ty) {
return ReturnInfo {
ty: Some(ty),
ok_type: None,
err_type: None,
some_type: None,
is_result: false,
is_option: false,
is_unit: true,
is_stream: false,
stream_item: None,
is_iterator: false,
iterator_item: None,
is_reference: false,
reference_inner: None,
};
}
if let Type::Reference(TypeReference { elem, .. }) = &ty {
let inner = elem.as_ref().clone();
return ReturnInfo {
ty: Some(ty),
ok_type: None,
err_type: None,
some_type: None,
is_result: false,
is_option: false,
is_unit: false,
is_stream: false,
stream_item: None,
is_iterator: false,
iterator_item: None,
is_reference: true,
reference_inner: Some(inner),
};
}
ReturnInfo {
ty: Some(ty),
ok_type: None,
err_type: None,
some_type: None,
is_result: false,
is_option: false,
is_unit: false,
is_stream: false,
stream_item: None,
is_iterator: false,
iterator_item: None,
is_reference: false,
reference_inner: None,
}
}
}
}
pub fn is_bool_type(ty: &Type) -> bool {
if let Type::Path(type_path) = ty
&& let Some(segment) = type_path.path.segments.last()
&& type_path.path.segments.len() == 1
{
return segment.ident == "bool";
}
false
}
pub fn extract_vec_type(ty: &Type) -> Option<Type> {
if let Type::Path(type_path) = ty
&& let Some(segment) = type_path.path.segments.last()
&& segment.ident == "Vec"
&& let PathArguments::AngleBracketed(args) = &segment.arguments
&& let Some(GenericArgument::Type(inner)) = args.args.first()
{
return Some(inner.clone());
}
None
}
pub fn extract_map_type(ty: &Type) -> Option<(Type, Type)> {
if let Type::Path(type_path) = ty
&& let Some(segment) = type_path.path.segments.last()
&& (segment.ident == "HashMap" || segment.ident == "BTreeMap")
&& let PathArguments::AngleBracketed(args) = &segment.arguments
{
let mut iter = args.args.iter();
if let (Some(GenericArgument::Type(key)), Some(GenericArgument::Type(val))) =
(iter.next(), iter.next())
{
return Some((key.clone(), val.clone()));
}
}
None
}
pub fn extract_option_type(ty: &Type) -> Option<Type> {
if let Type::Path(type_path) = ty
&& let Some(segment) = type_path.path.segments.last()
&& segment.ident == "Option"
&& let PathArguments::AngleBracketed(args) = &segment.arguments
&& let Some(GenericArgument::Type(inner)) = args.args.first()
{
return Some(inner.clone());
}
None
}
pub fn is_option_type(ty: &Type) -> bool {
extract_option_type(ty).is_some()
}
pub fn unwrap_option_type(ty: &Type) -> Option<&Type> {
if let Type::Path(type_path) = ty {
let seg = type_path.path.segments.last()?;
if seg.ident != "Option" { return None; }
if let PathArguments::AngleBracketed(args) = &seg.arguments
&& let Some(GenericArgument::Type(inner)) = args.args.first() {
return Some(inner);
}
}
None
}
pub fn unwrap_vec_type(ty: &Type) -> Option<&Type> {
if let Type::Path(type_path) = ty {
let seg = type_path.path.segments.last()?;
if seg.ident != "Vec" { return None; }
if let PathArguments::AngleBracketed(args) = &seg.arguments
&& let Some(GenericArgument::Type(inner)) = args.args.first() {
return Some(inner);
}
}
None
}
pub fn unwrap_result_ok_type(ty: &Type) -> Option<&Type> {
if let Type::Path(type_path) = ty {
let seg = type_path.path.segments.last()?;
if seg.ident != "Result" { return None; }
if let PathArguments::AngleBracketed(args) = &seg.arguments
&& let Some(GenericArgument::Type(inner)) = args.args.first() {
return Some(inner);
}
}
None
}
pub fn extract_result_types(ty: &Type) -> Option<(Type, Type)> {
if let Type::Path(type_path) = ty
&& let Some(segment) = type_path.path.segments.last()
&& segment.ident == "Result"
&& let PathArguments::AngleBracketed(args) = &segment.arguments
{
let mut iter = args.args.iter();
if let (Some(GenericArgument::Type(ok)), Some(GenericArgument::Type(err))) =
(iter.next(), iter.next())
{
return Some((ok.clone(), err.clone()));
}
}
None
}
pub fn extract_stream_item(ty: &Type) -> Option<Type> {
if let Type::ImplTrait(impl_trait) = ty {
for bound in &impl_trait.bounds {
if let syn::TypeParamBound::Trait(trait_bound) = bound
&& let Some(segment) = trait_bound.path.segments.last()
&& segment.ident == "Stream"
&& let PathArguments::AngleBracketed(args) = &segment.arguments
{
for arg in &args.args {
if let GenericArgument::AssocType(assoc) = arg
&& assoc.ident == "Item"
{
return Some(assoc.ty.clone());
}
}
}
}
}
None
}
pub fn extract_iterator_item(ty: &Type) -> Option<Type> {
if let Type::ImplTrait(impl_trait) = ty {
for bound in &impl_trait.bounds {
if let syn::TypeParamBound::Trait(trait_bound) = bound
&& let Some(segment) = trait_bound.path.segments.last()
&& segment.ident == "Iterator"
&& let PathArguments::AngleBracketed(args) = &segment.arguments
{
for arg in &args.args {
if let GenericArgument::AssocType(assoc) = arg
&& assoc.ident == "Item"
{
return Some(assoc.ty.clone());
}
}
}
}
}
None
}
pub fn is_unit_type(ty: &Type) -> bool {
if let Type::Tuple(tuple) = ty {
return tuple.elems.is_empty();
}
false
}
pub fn is_id_param(name: &Ident) -> bool {
let name_str = ident_str(name);
name_str == "id" || name_str.ends_with("_id")
}
pub fn extract_methods(impl_block: &ItemImpl) -> syn::Result<Vec<MethodInfo>> {
let mut methods = Vec::new();
for item in &impl_block.items {
if let ImplItem::Fn(method) = item {
if method.sig.ident.to_string().starts_with('_') {
continue;
}
if let Some(info) = MethodInfo::parse(method)? {
methods.push(info);
}
}
}
Ok(methods)
}
pub struct PartitionedMethods<'a> {
pub leaf: Vec<&'a MethodInfo>,
pub static_mounts: Vec<&'a MethodInfo>,
pub slug_mounts: Vec<&'a MethodInfo>,
}
pub fn partition_methods<'a>(
methods: &'a [MethodInfo],
skip: impl Fn(&MethodInfo) -> bool,
) -> PartitionedMethods<'a> {
let mut result = PartitionedMethods {
leaf: Vec::new(),
static_mounts: Vec::new(),
slug_mounts: Vec::new(),
};
for method in methods {
if skip(method) {
continue;
}
if method.return_info.is_reference && !method.is_async {
if method.params.is_empty() {
result.static_mounts.push(method);
} else {
result.slug_mounts.push(method);
}
} else {
result.leaf.push(method);
}
}
result
}
pub fn get_impl_name(impl_block: &ItemImpl) -> syn::Result<Ident> {
if let Type::Path(type_path) = impl_block.self_ty.as_ref()
&& let Some(segment) = type_path.path.segments.last()
{
return Ok(segment.ident.clone());
}
Err(syn::Error::new_spanned(
&impl_block.self_ty,
"Expected a simple type name",
))
}
#[cfg(test)]
mod tests {
use super::*;
use quote::quote;
#[test]
fn extract_docs_returns_none_when_no_doc_attrs() {
let method: ImplItemFn = syn::parse_quote! {
fn hello(&self) {}
};
assert!(extract_docs(&method.attrs).is_none());
}
#[test]
fn extract_docs_extracts_single_line() {
let method: ImplItemFn = syn::parse_quote! {
fn hello(&self) {}
};
assert_eq!(extract_docs(&method.attrs).unwrap(), "Hello world");
}
#[test]
fn extract_docs_joins_multiple_lines() {
let method: ImplItemFn = syn::parse_quote! {
fn hello(&self) {}
};
assert_eq!(extract_docs(&method.attrs).unwrap(), "Line one\nLine two");
}
#[test]
fn extract_docs_ignores_non_doc_attrs() {
let method: ImplItemFn = syn::parse_quote! {
#[inline]
fn hello(&self) {}
};
assert_eq!(extract_docs(&method.attrs).unwrap(), "Documented");
}
#[test]
fn parse_return_type_default_is_unit() {
let ret: ReturnType = syn::parse_quote! {};
let info = parse_return_type(&ret);
assert!(info.is_unit);
assert!(info.ty.is_none());
assert!(!info.is_result);
assert!(!info.is_option);
assert!(!info.is_reference);
}
#[test]
fn parse_return_type_regular_type() {
let ret: ReturnType = syn::parse_quote! { -> String };
let info = parse_return_type(&ret);
assert!(!info.is_unit);
assert!(!info.is_result);
assert!(!info.is_option);
assert!(!info.is_reference);
assert!(info.ty.is_some());
}
#[test]
fn parse_return_type_result() {
let ret: ReturnType = syn::parse_quote! { -> Result<String, MyError> };
let info = parse_return_type(&ret);
assert!(info.is_result);
assert!(!info.is_option);
assert!(!info.is_unit);
let ok = info.ok_type.unwrap();
assert_eq!(quote!(#ok).to_string(), "String");
let err = info.err_type.unwrap();
assert_eq!(quote!(#err).to_string(), "MyError");
}
#[test]
fn parse_return_type_option() {
let ret: ReturnType = syn::parse_quote! { -> Option<i32> };
let info = parse_return_type(&ret);
assert!(info.is_option);
assert!(!info.is_result);
assert!(!info.is_unit);
let some = info.some_type.unwrap();
assert_eq!(quote!(#some).to_string(), "i32");
}
#[test]
fn parse_return_type_unit_tuple() {
let ret: ReturnType = syn::parse_quote! { -> () };
let info = parse_return_type(&ret);
assert!(info.is_unit);
assert!(info.ty.is_some());
}
#[test]
fn parse_return_type_reference() {
let ret: ReturnType = syn::parse_quote! { -> &SubRouter };
let info = parse_return_type(&ret);
assert!(info.is_reference);
assert!(!info.is_unit);
let inner = info.reference_inner.unwrap();
assert_eq!(quote!(#inner).to_string(), "SubRouter");
}
#[test]
fn parse_return_type_stream() {
let ret: ReturnType = syn::parse_quote! { -> impl Stream<Item = u64> };
let info = parse_return_type(&ret);
assert!(info.is_stream);
assert!(!info.is_result);
let item = info.stream_item.unwrap();
assert_eq!(quote!(#item).to_string(), "u64");
}
#[test]
fn is_option_type_true() {
let ty: Type = syn::parse_quote! { Option<String> };
assert!(is_option_type(&ty));
let inner = extract_option_type(&ty).unwrap();
assert_eq!(quote!(#inner).to_string(), "String");
}
#[test]
fn is_option_type_false_for_non_option() {
let ty: Type = syn::parse_quote! { String };
assert!(!is_option_type(&ty));
assert!(extract_option_type(&ty).is_none());
}
#[test]
fn extract_result_types_works() {
let ty: Type = syn::parse_quote! { Result<Vec<u8>, std::io::Error> };
let (ok, err) = extract_result_types(&ty).unwrap();
assert_eq!(quote!(#ok).to_string(), "Vec < u8 >");
assert_eq!(quote!(#err).to_string(), "std :: io :: Error");
}
#[test]
fn extract_result_types_none_for_non_result() {
let ty: Type = syn::parse_quote! { Option<i32> };
assert!(extract_result_types(&ty).is_none());
}
#[test]
fn is_unit_type_true() {
let ty: Type = syn::parse_quote! { () };
assert!(is_unit_type(&ty));
}
#[test]
fn is_unit_type_false_for_non_tuple() {
let ty: Type = syn::parse_quote! { String };
assert!(!is_unit_type(&ty));
}
#[test]
fn is_unit_type_false_for_nonempty_tuple() {
let ty: Type = syn::parse_quote! { (i32, i32) };
assert!(!is_unit_type(&ty));
}
#[test]
fn is_id_param_exact_id() {
let ident: Ident = syn::parse_quote! { id };
assert!(is_id_param(&ident));
}
#[test]
fn is_id_param_suffix_id() {
let ident: Ident = syn::parse_quote! { user_id };
assert!(is_id_param(&ident));
}
#[test]
fn is_id_param_false_for_other_names() {
let ident: Ident = syn::parse_quote! { name };
assert!(!is_id_param(&ident));
}
#[test]
fn is_id_param_false_for_identity() {
let ident: Ident = syn::parse_quote! { identity };
assert!(!is_id_param(&ident));
}
#[test]
fn method_info_parse_basic() {
let method: ImplItemFn = syn::parse_quote! {
fn greet(&self, name: String) -> String {
format!("Hello {name}")
}
};
let info = MethodInfo::parse(&method).unwrap().unwrap();
assert_eq!(info.name.to_string(), "greet");
assert!(!info.is_async);
assert_eq!(info.docs.as_deref(), Some("Does a thing"));
assert_eq!(info.params.len(), 1);
assert_eq!(info.params[0].name.to_string(), "name");
assert!(!info.params[0].is_optional);
assert!(!info.params[0].is_id);
}
#[test]
fn method_info_parse_async_method() {
let method: ImplItemFn = syn::parse_quote! {
async fn fetch(&self) -> Vec<u8> {
vec![]
}
};
let info = MethodInfo::parse(&method).unwrap().unwrap();
assert!(info.is_async);
}
#[test]
fn method_info_parse_skips_associated_function() {
let method: ImplItemFn = syn::parse_quote! {
fn new() -> Self {
Self
}
};
assert!(MethodInfo::parse(&method).unwrap().is_none());
}
#[test]
fn method_info_parse_optional_param() {
let method: ImplItemFn = syn::parse_quote! {
fn search(&self, query: Option<String>) {}
};
let info = MethodInfo::parse(&method).unwrap().unwrap();
assert!(info.params[0].is_optional);
}
#[test]
fn method_info_parse_id_param() {
let method: ImplItemFn = syn::parse_quote! {
fn get_user(&self, user_id: u64) -> String {
String::new()
}
};
let info = MethodInfo::parse(&method).unwrap().unwrap();
assert!(info.params[0].is_id);
}
#[test]
fn method_info_parse_no_docs() {
let method: ImplItemFn = syn::parse_quote! {
fn bare(&self) {}
};
let info = MethodInfo::parse(&method).unwrap().unwrap();
assert!(info.docs.is_none());
}
#[test]
fn extract_methods_basic() {
let impl_block: ItemImpl = syn::parse_quote! {
impl MyApi {
fn hello(&self) -> String { String::new() }
fn world(&self) -> String { String::new() }
}
};
let methods = extract_methods(&impl_block).unwrap();
assert_eq!(methods.len(), 2);
assert_eq!(methods[0].name.to_string(), "hello");
assert_eq!(methods[1].name.to_string(), "world");
}
#[test]
fn extract_methods_skips_underscore_prefix() {
let impl_block: ItemImpl = syn::parse_quote! {
impl MyApi {
fn public(&self) {}
fn _private(&self) {}
fn __also_private(&self) {}
}
};
let methods = extract_methods(&impl_block).unwrap();
assert_eq!(methods.len(), 1);
assert_eq!(methods[0].name.to_string(), "public");
}
#[test]
fn extract_methods_skips_associated_functions() {
let impl_block: ItemImpl = syn::parse_quote! {
impl MyApi {
fn new() -> Self { Self }
fn from_config(cfg: Config) -> Self { Self }
fn greet(&self) -> String { String::new() }
}
};
let methods = extract_methods(&impl_block).unwrap();
assert_eq!(methods.len(), 1);
assert_eq!(methods[0].name.to_string(), "greet");
}
#[test]
fn partition_methods_splits_correctly() {
let impl_block: ItemImpl = syn::parse_quote! {
impl Router {
fn leaf_action(&self) -> String { String::new() }
fn static_mount(&self) -> &SubRouter { &self.sub }
fn slug_mount(&self, id: u64) -> &SubRouter { &self.sub }
async fn async_ref(&self) -> &SubRouter { &self.sub }
}
};
let methods = extract_methods(&impl_block).unwrap();
let partitioned = partition_methods(&methods, |_| false);
assert_eq!(partitioned.leaf.len(), 2);
assert_eq!(partitioned.leaf[0].name.to_string(), "leaf_action");
assert_eq!(partitioned.leaf[1].name.to_string(), "async_ref");
assert_eq!(partitioned.static_mounts.len(), 1);
assert_eq!(
partitioned.static_mounts[0].name.to_string(),
"static_mount"
);
assert_eq!(partitioned.slug_mounts.len(), 1);
assert_eq!(partitioned.slug_mounts[0].name.to_string(), "slug_mount");
}
#[test]
fn partition_methods_respects_skip() {
let impl_block: ItemImpl = syn::parse_quote! {
impl Router {
fn keep(&self) -> String { String::new() }
fn skip_me(&self) -> String { String::new() }
}
};
let methods = extract_methods(&impl_block).unwrap();
let partitioned = partition_methods(&methods, |m| m.name == "skip_me");
assert_eq!(partitioned.leaf.len(), 1);
assert_eq!(partitioned.leaf[0].name.to_string(), "keep");
}
#[test]
fn get_impl_name_extracts_struct_name() {
let impl_block: ItemImpl = syn::parse_quote! {
impl MyService {
fn hello(&self) {}
}
};
let name = get_impl_name(&impl_block).unwrap();
assert_eq!(name.to_string(), "MyService");
}
#[test]
fn get_impl_name_with_generics() {
let impl_block: ItemImpl = syn::parse_quote! {
impl MyService<T> {
fn hello(&self) {}
}
};
let name = get_impl_name(&impl_block).unwrap();
assert_eq!(name.to_string(), "MyService");
}
#[test]
fn ident_str_strips_raw_prefix() {
let ident: Ident = syn::parse_quote!(r#type);
assert_eq!(ident_str(&ident), "type");
}
#[test]
fn ident_str_leaves_normal_ident_unchanged() {
let ident: Ident = syn::parse_quote!(name);
assert_eq!(ident_str(&ident), "name");
}
#[test]
fn name_str_strips_raw_prefix_on_param() {
let method: ImplItemFn = syn::parse_quote! {
fn get(&self, r#type: String) -> String { r#type }
};
let info = MethodInfo::parse(&method).unwrap().unwrap();
assert_eq!(info.params[0].name_str(), "type");
assert_eq!(info.params[0].name.to_string(), "r#type");
}
#[test]
fn sync_fn_with_top_level_await_is_err() {
let method: ImplItemFn = syn::parse_quote! {
fn f(&self) {
something().await;
}
};
assert!(MethodInfo::parse(&method).is_err());
}
#[test]
fn sync_fn_with_nested_async_block_await_is_ok() {
let method: ImplItemFn = syn::parse_quote! {
fn f(&self, x: Thing) {
let _fut = async { x.await };
}
};
assert!(MethodInfo::parse(&method).is_ok());
}
}