use proc_macro2::Span;
use syn::punctuated::Punctuated;
use syn::{Attribute, Expr, Lit, Member, Token};
const ANY: &str = "ANY";
pub(super) fn route_call(node: &syn::ExprMethodCall) -> Vec<(&'static str, String)> {
let mut arguments = node.args.iter();
let Some(path) = arguments.next().and_then(string_literal) else {
return Vec::new();
};
let Some(handler) = arguments.next() else {
return Vec::new();
};
if !path.starts_with('/') {
return Vec::new();
}
served_methods(handler_methods(handler))
.into_iter()
.map(|method| (method, path.clone()))
.collect()
}
pub(super) fn attribute_routes(attributes: &[Attribute]) -> Vec<(&'static str, String, Span)> {
attributes.iter().flat_map(attribute_route).collect()
}
fn attribute_route(attribute: &Attribute) -> Vec<(&'static str, String, Span)> {
let Some(declared) = attribute_method(attribute) else {
return Vec::new();
};
let Ok(arguments) = attribute.parse_args_with(Punctuated::<Expr, Token![,]>::parse_terminated)
else {
return Vec::new();
};
let Some(path) = arguments.iter().find_map(argument_path) else {
return Vec::new();
};
if !path.starts_with('/') {
return Vec::new();
}
let methods = if declared == ANY {
served_methods(arguments.iter().filter_map(argument_method).collect())
} else {
vec![declared]
};
let span = syn::spanned::Spanned::span(attribute);
methods
.into_iter()
.map(|method| (method, path.clone(), span))
.collect()
}
fn served_methods(methods: Vec<&'static str>) -> Vec<&'static str> {
if methods.is_empty() {
vec![ANY]
} else {
methods
}
}
fn argument_path(argument: &Expr) -> Option<String> {
let Expr::Assign(assignment) = argument else {
return string_literal(argument);
};
let key = callable_name(&assignment.left)?;
matches!(key.as_str(), "path" | "uri")
.then(|| string_literal(&assignment.right))
.flatten()
}
fn argument_method(argument: &Expr) -> Option<&'static str> {
let Expr::Assign(assignment) = argument else {
return http_method(&callable_name(argument)?);
};
if callable_name(&assignment.left)? != "method" {
return None;
}
let value = &assignment.right;
http_method(&string_literal(value).or_else(|| callable_name(value))?)
}
fn handler_methods(handler: &Expr) -> Vec<&'static str> {
match unwrapped(handler) {
Expr::MethodCall(chained) => {
let mut methods = handler_methods(&chained.receiver);
methods.extend(http_method(&chained.method.to_string()));
methods
}
Expr::Call(call) => handler_method(&call.func),
expression => handler_method(expression),
}
}
fn handler_method(expression: &Expr) -> Vec<&'static str> {
callable_name(expression)
.as_deref()
.and_then(http_method)
.into_iter()
.collect()
}
fn string_literal(expression: &Expr) -> Option<String> {
match unwrapped(expression) {
Expr::Lit(literal) => match &literal.lit {
Lit::Str(value) => Some(value.value()),
_ => None,
},
_ => None,
}
}
pub(super) fn callable_name(expression: &Expr) -> Option<String> {
match unwrapped(expression) {
Expr::Path(path) => path
.path
.segments
.last()
.map(|segment| segment.ident.to_string()),
Expr::Field(field) => match &field.member {
Member::Named(name) => Some(name.to_string()),
Member::Unnamed(_) => None,
},
_ => None,
}
}
fn unwrapped(mut expression: &Expr) -> &Expr {
loop {
expression = match expression {
Expr::Group(group) => &group.expr,
Expr::Paren(parenthesized) => &parenthesized.expr,
_ => return expression,
};
}
}
fn attribute_method(attribute: &Attribute) -> Option<&'static str> {
let name = attribute.path().segments.last()?.ident.to_string();
match name.as_str() {
"route" | "operation" => Some(ANY),
_ => http_method(&name),
}
}
fn http_method(name: &str) -> Option<&'static str> {
Some(match name.to_ascii_uppercase().as_str() {
"GET" => "GET",
"POST" => "POST",
"PUT" => "PUT",
"PATCH" => "PATCH",
"DELETE" => "DELETE",
"HEAD" => "HEAD",
"OPTIONS" => "OPTIONS",
"TRACE" => "TRACE",
"CONNECT" => "CONNECT",
_ => return None,
})
}