use devise::ext::{SpanDiagnosticExt, TypeExt};
use devise::{FromMeta, Result, SpanWrapped, Spanned};
use indexmap::{IndexMap, IndexSet};
use proc_macro2::Span;
use crate::attribute::param::{Dynamic, Guard, Parameter};
use crate::attribute::suppress::Lint;
use crate::http::ext::IntoOwned;
use crate::http::uri::{fmt, Origin};
use crate::http_codegen::{MediaType, Method};
use crate::name::Name;
use crate::proc_macro_ext::Diagnostics;
use crate::syn_ext::FnArgExt;
#[derive(Debug)]
pub struct Route {
pub attr: Attribute,
pub path_params: Vec<Parameter>,
pub query_params: Vec<Parameter>,
pub data_guard: Option<Guard>,
pub request_guards: Vec<Guard>,
pub handler: syn::ItemFn,
pub arguments: Arguments,
}
type ArgumentMap = IndexMap<Name, (syn::Ident, syn::Type)>;
#[derive(Debug)]
pub struct Arguments {
pub span: Span,
pub map: ArgumentMap,
}
#[derive(Debug, FromMeta)]
pub struct Attribute {
#[meta(naked)]
pub uri: RouteUri,
pub method: Option<SpanWrapped<Method>>,
pub data: Option<SpanWrapped<Dynamic>>,
pub format: Option<MediaType>,
pub rank: Option<isize>,
}
#[derive(Debug, FromMeta)]
pub struct MethodAttribute {
#[meta(naked)]
pub uri: RouteUri,
pub data: Option<SpanWrapped<Dynamic>>,
pub format: Option<MediaType>,
pub rank: Option<isize>,
}
#[derive(Debug)]
pub struct RouteUri {
origin: Origin<'static>,
path_span: Span,
query_span: Option<Span>,
}
impl FromMeta for RouteUri {
fn from_meta(meta: &devise::MetaItem) -> Result<Self> {
let string = crate::proc_macro_ext::StringLit::from_meta(meta)?;
let origin = Origin::parse_route(&string).map_err(|e| {
let span = string.subspan(e.index() + 1..(e.index() + 2));
span.error(format!("invalid route URI: {}", e))
.help("expected URI in origin form: \"/path/<param>\"")
})?;
if !origin.is_normalized() {
let normalized = origin.clone().into_normalized();
let span = origin
.path()
.find("//")
.or_else(|| {
origin
.query()
.and_then(|q| q.find("&&"))
.map(|i| origin.path().len() + 1 + i)
})
.map(|i| string.subspan((1 + i)..(1 + i + 2)))
.unwrap_or_else(|| string.span());
return Err(span
.error("route URIs cannot contain empty segments")
.note(format!("expected \"{}\", found \"{}\"", normalized, origin)));
}
let path_span = string.subspan(1..origin.path().len() + 1);
let query_span = origin.query().map(|q| {
let len_to_q = 1 + origin.path().len() + 1;
let end_of_q = len_to_q + q.len();
string.subspan(len_to_q..end_of_q)
});
Ok(RouteUri {
origin: origin.into_owned(),
path_span,
query_span,
})
}
}
impl Route {
pub fn upgrade_param(param: Parameter, args: &Arguments) -> Result<Parameter> {
if param.dynamic().is_none() {
return Ok(param);
}
let param = param.take_dynamic().expect("dynamic() => take_dynamic()");
Route::upgrade_dynamic(param, args).map(Parameter::Guard)
}
pub fn upgrade_dynamic(param: Dynamic, args: &Arguments) -> Result<Guard> {
if let Some((ident, ty)) = args.map.get(¶m.name) {
Ok(Guard::from(param, ident.clone(), ty.clone()))
} else {
let msg = format!("expected argument named `{}` here", param.name);
let diag = param
.span()
.error("unused parameter")
.span_note(args.span, msg);
Err(diag)
}
}
pub fn from(attr: Attribute, handler: syn::ItemFn) -> Result<Route> {
let mut diags = Diagnostics::new();
if let Some(ref data) = attr.data {
let lint = Lint::DubiousPayload;
match attr.method.as_ref() {
Some(m) if m.0.allows_request_body() == Some(false) => diags.push(
data.full_span
.error("`data` cannot be used on this route")
.span_note(m.span, "method does not support request payloads"),
),
Some(m) if m.0.allows_request_body().is_none() && lint.enabled(handler.span()) => {
data.full_span
.warning("`data` used with non-payload-supporting method")
.span_note(
m.span,
format!("'{}' does not typically support payloads", m.0),
)
.note(lint.how_to_suppress())
.emit_as_item_tokens();
}
None if lint.enabled(handler.span()) => {
data.full_span
.warning("`data` used on route with wildcard method")
.note("some methods may not support request payloads")
.note(lint.how_to_suppress())
.emit_as_item_tokens();
}
_ => { }
}
}
let span = handler.sig.paren_token.span.join();
let mut arguments = Arguments {
map: ArgumentMap::new(),
span,
};
for arg in &handler.sig.inputs {
if let Some((ident, ty)) = arg.typed() {
let value = (ident.clone(), ty.with_stripped_lifetimes());
arguments.map.insert(Name::from(ident), value);
} else {
let span = arg.span();
let diag = if arg.wild().is_some() {
span.error("handler arguments must be named")
.help("to name an ignored handler argument, use `_name`")
} else {
span.error("handler arguments must be of the form `ident: Type`")
};
diags.push(diag);
}
}
let (source, span) = (attr.uri.path(), attr.uri.path_span);
let path_params = Parameter::parse_many::<fmt::Path>(source.as_str(), span)
.map(|p| Route::upgrade_param(p?, &arguments))
.filter_map(|p| p.map_err(|e| diags.push(e)).ok())
.collect::<Vec<_>>();
let query_params = match (attr.uri.query(), attr.uri.query_span) {
(Some(q), Some(span)) => Parameter::parse_many::<fmt::Query>(q.as_str(), span)
.map(|p| Route::upgrade_param(p?, &arguments))
.filter_map(|p| p.map_err(|e| diags.push(e)).ok())
.collect::<Vec<_>>(),
_ => vec![],
};
let data_guard = attr
.data
.clone()
.map(|p| Route::upgrade_dynamic(p.value, &arguments))
.and_then(|p| p.map_err(|e| diags.push(e)).ok());
let all_dyn_params = path_params
.iter()
.filter_map(|p| p.dynamic())
.chain(query_params.iter().filter_map(|p| p.dynamic()))
.chain(data_guard.as_ref().map(|g| &g.source));
let mut dyn_params: IndexSet<&Dynamic> = IndexSet::new();
for p in all_dyn_params {
if let Some(prev) = dyn_params.replace(p) {
diags.push(
p.span()
.error(format!("duplicate parameter: `{}`", p.name))
.span_note(prev.span(), "previous parameter with the same name here"),
)
}
}
let request_guards = arguments
.map
.iter()
.filter(|(name, _)| {
let mut all_other_guards = path_params
.iter()
.filter_map(|p| p.guard())
.chain(query_params.iter().filter_map(|p| p.guard()))
.chain(data_guard.as_ref());
all_other_guards.all(|g| &g.name != *name)
})
.enumerate()
.map(|(index, (name, (ident, ty)))| Guard {
source: Dynamic {
index,
name: name.clone(),
trailing: false,
},
fn_ident: ident.clone(),
ty: ty.clone(),
})
.collect();
diags.head_err_or(Route {
attr,
path_params,
query_params,
data_guard,
request_guards,
handler,
arguments,
})
}
}
impl std::ops::Deref for RouteUri {
type Target = Origin<'static>;
fn deref(&self) -> &Self::Target {
&self.origin
}
}