use proc_macro_error::{emit_call_site_error, emit_call_site_warning};
use quote::ToTokens;
use syn::Attribute;
use super::extractor_types::{ExtractorType, validate_extractors};
pub struct RouteInfo {
pub method: String,
pub path: String,
pub extractors: std::collections::HashMap<String, ExtractorType>,
pub response_headers: Vec<(String, String)>, pub content_type: Option<String>,
}
fn validate_path_parameters(
path: &str,
extractors: &std::collections::HashMap<String, ExtractorType>,
) {
let mut path_params = std::collections::HashSet::new();
for capture in path.split('/') {
if let Some(param) = capture.strip_prefix('{').and_then(|s| s.strip_suffix('}')) {
path_params.insert(param.to_string());
} else if let Some(param) = capture.strip_prefix(':') {
path_params.insert(param.to_string());
}
}
for param in &path_params {
match extractors.get(param) {
Some(ExtractorType::Path) => {
}
Some(other_type) => {
emit_call_site_error!(
"Path parameter '{}' in route path '{}' should use Path extractor, but {:?} was specified",
param,
path,
other_type
);
}
None => {
emit_call_site_error!(
"Path parameter '{}' found in route path '{}' but no extractor specified. \
Add 'extract({} = Path)' to the route attribute",
param,
path,
param
);
}
}
}
let path_extractors: Vec<_> = extractors
.iter()
.filter(|(_, ext)| matches!(ext, ExtractorType::Path))
.map(|(name, _)| name)
.collect();
for param_name in path_extractors {
if !path_params.contains(param_name) {
emit_call_site_warning!(
"Path extractor specified for parameter '{}' but it's not found in route path '{}'. \
Make sure the path contains '{{{}}}' or ':{}'",
param_name,
path,
param_name,
param_name
);
}
}
}
pub fn extract_route_from_attrs(attrs: &[Attribute]) -> Option<RouteInfo> {
for attr in attrs {
let path_segments: Vec<String> = attr
.path()
.segments
.iter()
.map(|seg| seg.ident.to_string())
.collect();
if path_segments.len() == 1 {
let method = path_segments[0].to_lowercase();
match method.as_str() {
"get" | "head" | "delete" | "options" | "patch" | "post" | "put" | "trace" | "connect" => {
let mut route_path = "/".to_string();
let mut extractors = std::collections::HashMap::new();
let mut response_headers = Vec::new();
let mut content_type = None;
let attr_str = attr.meta.to_token_stream().to_string();
if let Some(start) = attr_str.find('"') {
if let Some(end) = attr_str[start + 1..].find('"') {
let mut path = attr_str[start + 1..start + 1 + end].to_string();
if !path.is_empty() {
if !path.starts_with('/') {
path = format!("/{}", path);
}
route_path = path;
}
}
}
if let Some(extract_start) = attr_str.find("extract") {
if let Some(paren_start) = attr_str[extract_start..].find('(') {
let paren_start = extract_start + paren_start + 1;
if let Some(paren_end) = attr_str[paren_start..].find(')') {
let extract_content = &attr_str[paren_start..paren_start + paren_end];
for pair in extract_content.split(',') {
let parts: Vec<&str> = pair.trim().split('=').map(|s| s.trim()).collect();
if parts.len() == 2 {
let param_name = parts[0].to_string();
let extractor_str = parts[1];
match ExtractorType::from_str(extractor_str) {
Ok(extractor_type) => {
extractors.insert(param_name, extractor_type);
}
Err(err_msg) => {
emit_call_site_error!(
"{}. Valid extractors are: Json, Form, Path, Query, State, Bytes, Text, Html, Xml, JavaScript, HeaderParam, CookieParam, SessionParam",
err_msg
);
extractors.insert(param_name, ExtractorType::None);
}
}
} else if parts.len() == 1 && !parts[0].is_empty() {
emit_call_site_error!(
"Invalid extractor syntax '{}'. Expected format: 'param_name = ExtractorType'",
pair.trim()
);
}
}
}
}
}
let mut search_pos = 0;
while let Some(header_start) = attr_str[search_pos..].find("header") {
let header_start = search_pos + header_start;
if let Some(paren_start) = attr_str[header_start..].find('(') {
let paren_start = header_start + paren_start + 1;
if let Some(paren_end) = attr_str[paren_start..].find(')') {
let header_content = &attr_str[paren_start..paren_start + paren_end];
if let Some(eq_pos) = header_content.find('=') {
let header_name = header_content[..eq_pos].trim().replace('"', "");
let header_value = header_content[eq_pos + 1..].trim().replace('"', "");
if header_name.is_empty() || header_value.is_empty() {
emit_call_site_warning!("Empty header name or value in header attribute");
}
response_headers.push((header_name, header_value));
} else {
let parts: Vec<&str> = header_content.split(',').collect();
if parts.len() == 2 {
let header_name = parts[0].trim().replace('"', "");
let header_value = parts[1].trim().replace('"', "");
if header_name.is_empty() || header_value.is_empty() {
emit_call_site_warning!("Empty header name or value in header attribute");
}
response_headers.push((header_name, header_value));
} else {
emit_call_site_warning!(
"Invalid header attribute format. Expected: header(\"name\", \"value\") or header(name = \"value\")"
);
}
}
search_pos = paren_start + paren_end + 1;
} else {
break;
}
} else {
break;
}
}
if let Some(ct_start) = attr_str.find("content_type") {
if let Some(paren_start) = attr_str[ct_start..].find('(') {
let paren_start = ct_start + paren_start + 1;
if let Some(paren_end) = attr_str[paren_start..].find(')') {
let ct_content = &attr_str[paren_start..paren_start + paren_end];
let ct_value = ct_content.trim().replace('"', "");
if ct_value.is_empty() {
emit_call_site_warning!("Empty content_type value");
}
content_type = Some(ct_value);
}
}
}
validate_extractors(&extractors, &method);
validate_path_parameters(&route_path, &extractors);
log_verbose!(
"Parsed route: [Method:{}] [Path:{}] [Extractors:{:?}] [Headers:{:?}] [ContentType:{:?}]",
method,
route_path,
extractors,
response_headers,
content_type
);
return Some(RouteInfo {
method,
path: route_path,
extractors,
response_headers,
content_type,
});
}
_ => {
emit_call_site_error!(
"Unknown HTTP method '{}'. Valid methods are: get, post, put, patch, delete, head, options, trace",
method
);
}
}
}
}
None
}