mirror of
https://gitlab.com/msrd0/gotham-restful.git
synced 2025-05-09 08:00:41 +00:00
Replace methods with more flexible endpoints
This commit is contained in:
parent
0ac0f0f504
commit
b807ae2796
87 changed files with 1497 additions and 1512 deletions
469
derive/src/endpoint.rs
Normal file
469
derive/src/endpoint.rs
Normal file
|
@ -0,0 +1,469 @@
|
|||
use crate::util::{CollectToResult, PathEndsWith};
|
||||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
use quote::{format_ident, quote};
|
||||
use std::str::FromStr;
|
||||
use syn::{
|
||||
spanned::Spanned, Attribute, AttributeArgs, Error, FnArg, ItemFn, Lit, LitBool, Meta, NestedMeta, PatType, Result,
|
||||
ReturnType, Type
|
||||
};
|
||||
|
||||
pub enum EndpointType {
|
||||
ReadAll,
|
||||
Read,
|
||||
Search,
|
||||
Create,
|
||||
UpdateAll,
|
||||
Update,
|
||||
DeleteAll,
|
||||
Delete
|
||||
}
|
||||
|
||||
impl FromStr for EndpointType {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(str: &str) -> Result<Self> {
|
||||
match str {
|
||||
"ReadAll" | "read_all" => Ok(Self::ReadAll),
|
||||
"Read" | "read" => Ok(Self::Read),
|
||||
"Search" | "search" => Ok(Self::Search),
|
||||
"Create" | "create" => Ok(Self::Create),
|
||||
"ChangeAll" | "change_all" => Ok(Self::UpdateAll),
|
||||
"Change" | "change" => Ok(Self::Update),
|
||||
"RemoveAll" | "remove_all" => Ok(Self::DeleteAll),
|
||||
"Remove" | "remove" => Ok(Self::Delete),
|
||||
_ => Err(Error::new(Span::call_site(), format!("Unknown method: `{}'", str)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EndpointType {
|
||||
fn http_method(&self) -> TokenStream {
|
||||
match self {
|
||||
Self::ReadAll | Self::Read | Self::Search => quote!(::gotham_restful::gotham::hyper::Method::GET),
|
||||
Self::Create => quote!(::gotham_restful::gotham::hyper::Method::POST),
|
||||
Self::UpdateAll | Self::Update => quote!(::gotham_restful::gotham::hyper::Method::PUT),
|
||||
Self::DeleteAll | Self::Delete => quote!(::gotham_restful::gotham::hyper::Method::DELETE)
|
||||
}
|
||||
}
|
||||
|
||||
fn uri(&self) -> TokenStream {
|
||||
match self {
|
||||
Self::ReadAll | Self::Create | Self::UpdateAll | Self::DeleteAll => quote!(""),
|
||||
Self::Read | Self::Update | Self::Delete => quote!(":id"),
|
||||
Self::Search => quote!("search")
|
||||
}
|
||||
}
|
||||
|
||||
fn has_placeholders(&self) -> LitBool {
|
||||
match self {
|
||||
Self::ReadAll | Self::Search | Self::Create | Self::UpdateAll | Self::DeleteAll => LitBool {
|
||||
value: false,
|
||||
span: Span::call_site()
|
||||
},
|
||||
Self::Read | Self::Update | Self::Delete => LitBool {
|
||||
value: true,
|
||||
span: Span::call_site()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn placeholders_ty(&self, arg_ty: Option<&Type>) -> TokenStream {
|
||||
match self {
|
||||
Self::ReadAll | Self::Search | Self::Create | Self::UpdateAll | Self::DeleteAll => {
|
||||
quote!(::gotham_restful::gotham::extractor::NoopPathExtractor)
|
||||
},
|
||||
Self::Read | Self::Update | Self::Delete => quote!(::gotham_restful::export::IdPlaceholder::<#arg_ty>)
|
||||
}
|
||||
}
|
||||
|
||||
fn needs_params(&self) -> LitBool {
|
||||
match self {
|
||||
Self::ReadAll | Self::Read | Self::Create | Self::UpdateAll | Self::Update | Self::DeleteAll | Self::Delete => {
|
||||
LitBool {
|
||||
value: false,
|
||||
span: Span::call_site()
|
||||
}
|
||||
},
|
||||
Self::Search => LitBool {
|
||||
value: true,
|
||||
span: Span::call_site()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn params_ty(&self, arg_ty: Option<&Type>) -> TokenStream {
|
||||
match self {
|
||||
Self::ReadAll | Self::Read | Self::Create | Self::UpdateAll | Self::Update | Self::DeleteAll | Self::Delete => {
|
||||
quote!(::gotham_restful::gotham::extractor::NoopQueryStringExtractor)
|
||||
},
|
||||
Self::Search => quote!(#arg_ty)
|
||||
}
|
||||
}
|
||||
|
||||
fn needs_body(&self) -> LitBool {
|
||||
match self {
|
||||
Self::ReadAll | Self::Read | Self::Search | Self::DeleteAll | Self::Delete => LitBool {
|
||||
value: false,
|
||||
span: Span::call_site()
|
||||
},
|
||||
Self::Create | Self::UpdateAll | Self::Update => LitBool {
|
||||
value: true,
|
||||
span: Span::call_site()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn body_ty(&self, arg_ty: Option<&Type>) -> TokenStream {
|
||||
match self {
|
||||
Self::ReadAll | Self::Read | Self::Search | Self::DeleteAll | Self::Delete => quote!(()),
|
||||
Self::Create | Self::UpdateAll | Self::Update => quote!(#arg_ty)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
enum HandlerArgType {
|
||||
StateRef,
|
||||
StateMutRef,
|
||||
MethodArg(Type),
|
||||
DatabaseConnection(Type),
|
||||
AuthStatus(Type),
|
||||
AuthStatusRef(Type)
|
||||
}
|
||||
|
||||
impl HandlerArgType {
|
||||
fn is_method_arg(&self) -> bool {
|
||||
matches!(self, Self::MethodArg(_))
|
||||
}
|
||||
|
||||
fn is_database_conn(&self) -> bool {
|
||||
matches!(self, Self::DatabaseConnection(_))
|
||||
}
|
||||
|
||||
fn is_auth_status(&self) -> bool {
|
||||
matches!(self, Self::AuthStatus(_) | Self::AuthStatusRef(_))
|
||||
}
|
||||
|
||||
fn ty(&self) -> Option<&Type> {
|
||||
match self {
|
||||
Self::MethodArg(ty) | Self::DatabaseConnection(ty) | Self::AuthStatus(ty) | Self::AuthStatusRef(ty) => Some(ty),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
|
||||
fn quote_ty(&self) -> Option<TokenStream> {
|
||||
self.ty().map(|ty| quote!(#ty))
|
||||
}
|
||||
}
|
||||
|
||||
struct HandlerArg {
|
||||
ident_span: Span,
|
||||
ty: HandlerArgType
|
||||
}
|
||||
|
||||
impl Spanned for HandlerArg {
|
||||
fn span(&self) -> Span {
|
||||
self.ident_span
|
||||
}
|
||||
}
|
||||
|
||||
fn interpret_arg_ty(attrs: &[Attribute], name: &str, ty: Type) -> Result<HandlerArgType> {
|
||||
let attr = attrs
|
||||
.iter()
|
||||
.find(|arg| arg.path.segments.iter().any(|path| &path.ident.to_string() == "rest_arg"))
|
||||
.map(|arg| arg.tokens.to_string());
|
||||
|
||||
// TODO issue a warning for _state usage once diagnostics become stable
|
||||
if attr.as_deref() == Some("state") || (attr.is_none() && (name == "state" || name == "_state")) {
|
||||
return match ty {
|
||||
Type::Reference(ty) => Ok(if ty.mutability.is_none() {
|
||||
HandlerArgType::StateRef
|
||||
} else {
|
||||
HandlerArgType::StateMutRef
|
||||
}),
|
||||
_ => Err(Error::new(
|
||||
ty.span(),
|
||||
"The state parameter has to be a (mutable) reference to gotham_restful::State"
|
||||
))
|
||||
};
|
||||
}
|
||||
|
||||
if cfg!(feature = "auth") && (attr.as_deref() == Some("auth") || (attr.is_none() && name == "auth")) {
|
||||
return Ok(match ty {
|
||||
Type::Reference(ty) => HandlerArgType::AuthStatusRef(*ty.elem),
|
||||
ty => HandlerArgType::AuthStatus(ty)
|
||||
});
|
||||
}
|
||||
|
||||
if cfg!(feature = "database")
|
||||
&& (attr.as_deref() == Some("connection") || attr.as_deref() == Some("conn") || (attr.is_none() && name == "conn"))
|
||||
{
|
||||
return Ok(HandlerArgType::DatabaseConnection(match ty {
|
||||
Type::Reference(ty) => *ty.elem,
|
||||
ty => ty
|
||||
}));
|
||||
}
|
||||
|
||||
Ok(HandlerArgType::MethodArg(ty))
|
||||
}
|
||||
|
||||
fn interpret_arg(_index: usize, arg: &PatType) -> Result<HandlerArg> {
|
||||
let pat = &arg.pat;
|
||||
let orig_name = quote!(#pat);
|
||||
let ty = interpret_arg_ty(&arg.attrs, &orig_name.to_string(), *arg.ty.clone())?;
|
||||
|
||||
Ok(HandlerArg {
|
||||
ident_span: arg.pat.span(),
|
||||
ty
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
fn expand_operation_id(operation_id: Option<Lit>) -> Option<TokenStream> {
|
||||
match operation_id {
|
||||
Some(operation_id) => Some(quote! {
|
||||
fn operation_id() -> Option<String> {
|
||||
Some(#operation_id.to_string())
|
||||
}
|
||||
}),
|
||||
None => None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "openapi"))]
|
||||
fn expand_operation_id(_: Option<Lit>) -> Option<TokenStream> {
|
||||
None
|
||||
}
|
||||
|
||||
fn expand_wants_auth(wants_auth: Option<Lit>, default: bool) -> TokenStream {
|
||||
let wants_auth = wants_auth.unwrap_or_else(|| {
|
||||
Lit::Bool(LitBool {
|
||||
value: default,
|
||||
span: Span::call_site()
|
||||
})
|
||||
});
|
||||
|
||||
quote! {
|
||||
fn wants_auth() -> bool {
|
||||
#wants_auth
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn endpoint_ident(fn_ident: &Ident) -> Ident {
|
||||
format_ident!("{}___gotham_restful_endpoint", fn_ident)
|
||||
}
|
||||
|
||||
fn expand_endpoint_type(ty: EndpointType, attrs: AttributeArgs, fun: &ItemFn) -> Result<TokenStream> {
|
||||
// reject unsafe functions
|
||||
if let Some(unsafety) = fun.sig.unsafety {
|
||||
return Err(Error::new(unsafety.span(), "Endpoint handler methods must not be unsafe"));
|
||||
}
|
||||
|
||||
// parse arguments
|
||||
let mut operation_id: Option<Lit> = None;
|
||||
let mut wants_auth: Option<Lit> = None;
|
||||
for meta in attrs {
|
||||
match meta {
|
||||
NestedMeta::Meta(Meta::NameValue(kv)) => {
|
||||
if kv.path.ends_with("operation_id") {
|
||||
operation_id = Some(kv.lit);
|
||||
} else if kv.path.ends_with("wants_auth") {
|
||||
wants_auth = Some(kv.lit);
|
||||
} else {
|
||||
return Err(Error::new(kv.path.span(), "Unknown attribute"));
|
||||
}
|
||||
},
|
||||
_ => return Err(Error::new(meta.span(), "Invalid attribute syntax"))
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "openapi"))]
|
||||
if let Some(operation_id) = operation_id {
|
||||
return Err(Error::new(
|
||||
operation_id.span(),
|
||||
"`operation_id` is only supported with the openapi feature"
|
||||
));
|
||||
}
|
||||
|
||||
// extract arguments into pattern, ident and type
|
||||
let args = fun
|
||||
.sig
|
||||
.inputs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, arg)| match arg {
|
||||
FnArg::Typed(arg) => interpret_arg(i, arg),
|
||||
FnArg::Receiver(_) => Err(Error::new(arg.span(), "Didn't expect self parameter"))
|
||||
})
|
||||
.collect_to_result()?;
|
||||
|
||||
let fun_vis = &fun.vis;
|
||||
let fun_ident = &fun.sig.ident;
|
||||
let fun_is_async = fun.sig.asyncness.is_some();
|
||||
|
||||
let ident = endpoint_ident(fun_ident);
|
||||
let dummy_ident = format_ident!("_IMPL_Endpoint_for_{}", ident);
|
||||
let (output_ty, is_no_content) = match &fun.sig.output {
|
||||
ReturnType::Default => (quote!(::gotham_restful::NoContent), true),
|
||||
ReturnType::Type(_, ty) => (quote!(#ty), false)
|
||||
};
|
||||
|
||||
let arg_tys = args.iter().filter(|arg| arg.ty.is_method_arg()).collect::<Vec<_>>();
|
||||
let mut arg_ty_idx = 0;
|
||||
let mut next_arg_ty = |return_none: bool| {
|
||||
if return_none {
|
||||
return Ok(None);
|
||||
}
|
||||
if arg_ty_idx >= arg_tys.len() {
|
||||
return Err(Error::new(fun_ident.span(), "Too few arguments"));
|
||||
}
|
||||
let ty = arg_tys[arg_ty_idx].ty.ty().unwrap();
|
||||
arg_ty_idx += 1;
|
||||
Ok(Some(ty))
|
||||
};
|
||||
|
||||
let http_method = ty.http_method();
|
||||
let uri = ty.uri();
|
||||
let has_placeholders = ty.has_placeholders();
|
||||
let placeholder_ty = ty.placeholders_ty(next_arg_ty(!has_placeholders.value)?);
|
||||
let needs_params = ty.needs_params();
|
||||
let params_ty = ty.params_ty(next_arg_ty(!needs_params.value)?);
|
||||
let needs_body = ty.needs_body();
|
||||
let body_ty = ty.body_ty(next_arg_ty(!needs_body.value)?);
|
||||
|
||||
if arg_ty_idx < arg_tys.len() {
|
||||
return Err(Error::new(fun_ident.span(), "Too many arguments"));
|
||||
}
|
||||
|
||||
let mut handle_args: Vec<TokenStream> = Vec::new();
|
||||
if has_placeholders.value {
|
||||
handle_args.push(quote!(placeholders.id));
|
||||
}
|
||||
if needs_params.value {
|
||||
handle_args.push(quote!(params));
|
||||
}
|
||||
if needs_body.value {
|
||||
handle_args.push(quote!(body.unwrap()));
|
||||
}
|
||||
let handle_args = args.iter().map(|arg| match arg.ty {
|
||||
HandlerArgType::StateRef | HandlerArgType::StateMutRef => quote!(state),
|
||||
HandlerArgType::MethodArg(_) => handle_args.remove(0),
|
||||
HandlerArgType::DatabaseConnection(_) => quote!(&conn),
|
||||
HandlerArgType::AuthStatus(_) => quote!(auth),
|
||||
HandlerArgType::AuthStatusRef(_) => quote!(&auth)
|
||||
});
|
||||
|
||||
let expand_handle_content = || {
|
||||
let mut state_block = quote!();
|
||||
if let Some(arg) = args.iter().find(|arg| arg.ty.is_auth_status()) {
|
||||
let auth_ty = arg.ty.quote_ty();
|
||||
state_block = quote! {
|
||||
#state_block
|
||||
let auth: #auth_ty = state.borrow::<#auth_ty>().clone();
|
||||
}
|
||||
}
|
||||
|
||||
let mut handle_content = quote!(#fun_ident(#(#handle_args),*));
|
||||
if fun_is_async {
|
||||
if let Some(arg) = args.iter().find(|arg| matches!(arg.ty, HandlerArgType::StateRef)) {
|
||||
return Err(Error::new(arg.span(), "Endpoint handler functions that are async must not take `&State` as an argument, consider taking `&mut State`"));
|
||||
}
|
||||
handle_content = quote!(#handle_content.await);
|
||||
}
|
||||
if is_no_content {
|
||||
handle_content = quote!(#handle_content; ::gotham_restful::NoContent)
|
||||
}
|
||||
|
||||
if let Some(arg) = args.iter().find(|arg| arg.ty.is_database_conn()) {
|
||||
let conn_ty = arg.ty.quote_ty();
|
||||
state_block = quote! {
|
||||
#state_block
|
||||
let repo = <::gotham_restful::export::Repo<#conn_ty>>::borrow_from(state).clone();
|
||||
};
|
||||
handle_content = quote! {
|
||||
repo.run::<_, _, ()>(move |conn| {
|
||||
Ok({ #handle_content })
|
||||
}).await.unwrap()
|
||||
};
|
||||
}
|
||||
|
||||
Ok(quote! {
|
||||
use ::gotham_restful::export::FutureExt as _;
|
||||
#state_block
|
||||
async move {
|
||||
#handle_content
|
||||
}.boxed()
|
||||
})
|
||||
};
|
||||
let handle_content = match expand_handle_content() {
|
||||
Ok(content) => content,
|
||||
Err(err) => err.to_compile_error()
|
||||
};
|
||||
|
||||
let tr8 = if cfg!(feature = "openapi") {
|
||||
quote!(::gotham_restful::EndpointWithSchema)
|
||||
} else {
|
||||
quote!(::gotham_restful::Endpoint)
|
||||
};
|
||||
let operation_id = expand_operation_id(operation_id);
|
||||
let wants_auth = expand_wants_auth(wants_auth, args.iter().any(|arg| arg.ty.is_auth_status()));
|
||||
Ok(quote! {
|
||||
#[doc(hidden)]
|
||||
/// `gotham_restful` implementation detail
|
||||
#[allow(non_camel_case_types)]
|
||||
#fun_vis struct #ident;
|
||||
|
||||
#[allow(non_upper_case_globals)]
|
||||
static #dummy_ident: () = {
|
||||
impl #tr8 for #ident {
|
||||
fn http_method() -> ::gotham_restful::gotham::hyper::Method {
|
||||
#http_method
|
||||
}
|
||||
|
||||
fn uri() -> ::std::borrow::Cow<'static, str> {
|
||||
{ #uri }.into()
|
||||
}
|
||||
|
||||
type Output = #output_ty;
|
||||
|
||||
fn has_placeholders() -> bool {
|
||||
#has_placeholders
|
||||
}
|
||||
type Placeholders = #placeholder_ty;
|
||||
|
||||
fn needs_params() -> bool {
|
||||
#needs_params
|
||||
}
|
||||
type Params = #params_ty;
|
||||
|
||||
fn needs_body() -> bool {
|
||||
#needs_body
|
||||
}
|
||||
type Body = #body_ty;
|
||||
|
||||
fn handle(
|
||||
state: &mut ::gotham_restful::gotham::state::State,
|
||||
placeholders: Self::Placeholders,
|
||||
params: Self::Params,
|
||||
body: ::std::option::Option<Self::Body>
|
||||
) -> ::gotham_restful::export::BoxFuture<'static, Self::Output> {
|
||||
#handle_content
|
||||
}
|
||||
|
||||
#operation_id
|
||||
#wants_auth
|
||||
}
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
pub fn expand_endpoint(ty: EndpointType, attrs: AttributeArgs, fun: ItemFn) -> Result<TokenStream> {
|
||||
let endpoint_type = match expand_endpoint_type(ty, attrs, &fun) {
|
||||
Ok(code) => code,
|
||||
Err(err) => err.to_compile_error()
|
||||
};
|
||||
Ok(quote! {
|
||||
#fun
|
||||
#endpoint_type
|
||||
})
|
||||
}
|
|
@ -5,24 +5,32 @@ use syn::{parse_macro_input, parse_macro_input::ParseMacroInput, DeriveInput, Re
|
|||
|
||||
mod util;
|
||||
|
||||
mod endpoint;
|
||||
use endpoint::{expand_endpoint, EndpointType};
|
||||
|
||||
mod from_body;
|
||||
use from_body::expand_from_body;
|
||||
mod method;
|
||||
use method::{expand_method, Method};
|
||||
|
||||
mod request_body;
|
||||
use request_body::expand_request_body;
|
||||
|
||||
mod resource;
|
||||
use resource::expand_resource;
|
||||
|
||||
mod resource_error;
|
||||
use resource_error::expand_resource_error;
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
mod openapi_type;
|
||||
#[cfg(feature = "openapi")]
|
||||
use openapi_type::expand_openapi_type;
|
||||
|
||||
mod private_openapi_trait;
|
||||
use private_openapi_trait::expand_private_openapi_trait;
|
||||
|
||||
#[inline]
|
||||
fn print_tokens(tokens: TokenStream2) -> TokenStream {
|
||||
//eprintln!("{}", tokens);
|
||||
// eprintln!("{}", tokens);
|
||||
tokens.into()
|
||||
}
|
||||
|
||||
|
@ -77,40 +85,47 @@ pub fn derive_resource_error(input: TokenStream) -> TokenStream {
|
|||
|
||||
#[proc_macro_attribute]
|
||||
pub fn read_all(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
expand_macro(attr, item, |attr, item| expand_method(Method::ReadAll, attr, item))
|
||||
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::ReadAll, attr, item))
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn read(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
expand_macro(attr, item, |attr, item| expand_method(Method::Read, attr, item))
|
||||
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::Read, attr, item))
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn search(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
expand_macro(attr, item, |attr, item| expand_method(Method::Search, attr, item))
|
||||
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::Search, attr, item))
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn create(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
expand_macro(attr, item, |attr, item| expand_method(Method::Create, attr, item))
|
||||
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::Create, attr, item))
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn change_all(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
expand_macro(attr, item, |attr, item| expand_method(Method::ChangeAll, attr, item))
|
||||
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::UpdateAll, attr, item))
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn change(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
expand_macro(attr, item, |attr, item| expand_method(Method::Change, attr, item))
|
||||
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::Update, attr, item))
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn remove_all(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
expand_macro(attr, item, |attr, item| expand_method(Method::RemoveAll, attr, item))
|
||||
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::DeleteAll, attr, item))
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn remove(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
expand_macro(attr, item, |attr, item| expand_method(Method::Remove, attr, item))
|
||||
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::Delete, attr, item))
|
||||
}
|
||||
|
||||
/// PRIVATE MACRO - DO NOT USE
|
||||
#[doc(hidden)]
|
||||
#[proc_macro_attribute]
|
||||
pub fn _private_openapi_trait(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
expand_macro(attr, item, expand_private_openapi_trait)
|
||||
}
|
||||
|
|
|
@ -1,466 +0,0 @@
|
|||
use crate::util::CollectToResult;
|
||||
use heck::{CamelCase, SnakeCase};
|
||||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
use quote::{format_ident, quote};
|
||||
use std::str::FromStr;
|
||||
use syn::{
|
||||
spanned::Spanned, Attribute, AttributeArgs, Error, FnArg, ItemFn, Lit, LitBool, Meta, NestedMeta, PatType, Path, Result,
|
||||
ReturnType, Type
|
||||
};
|
||||
|
||||
pub enum Method {
|
||||
ReadAll,
|
||||
Read,
|
||||
Search,
|
||||
Create,
|
||||
ChangeAll,
|
||||
Change,
|
||||
RemoveAll,
|
||||
Remove
|
||||
}
|
||||
|
||||
impl FromStr for Method {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(str: &str) -> Result<Self> {
|
||||
match str {
|
||||
"ReadAll" | "read_all" => Ok(Self::ReadAll),
|
||||
"Read" | "read" => Ok(Self::Read),
|
||||
"Search" | "search" => Ok(Self::Search),
|
||||
"Create" | "create" => Ok(Self::Create),
|
||||
"ChangeAll" | "change_all" => Ok(Self::ChangeAll),
|
||||
"Change" | "change" => Ok(Self::Change),
|
||||
"RemoveAll" | "remove_all" => Ok(Self::RemoveAll),
|
||||
"Remove" | "remove" => Ok(Self::Remove),
|
||||
_ => Err(Error::new(Span::call_site(), format!("Unknown method: `{}'", str)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Method {
|
||||
pub fn type_names(&self) -> Vec<&'static str> {
|
||||
use Method::*;
|
||||
|
||||
match self {
|
||||
ReadAll | RemoveAll => vec![],
|
||||
Read | Remove => vec!["ID"],
|
||||
Search => vec!["Query"],
|
||||
Create | ChangeAll => vec!["Body"],
|
||||
Change => vec!["ID", "Body"]
|
||||
}
|
||||
}
|
||||
|
||||
pub fn trait_ident(&self) -> Ident {
|
||||
use Method::*;
|
||||
|
||||
let name = match self {
|
||||
ReadAll => "ReadAll",
|
||||
Read => "Read",
|
||||
Search => "Search",
|
||||
Create => "Create",
|
||||
ChangeAll => "ChangeAll",
|
||||
Change => "Change",
|
||||
RemoveAll => "RemoveAll",
|
||||
Remove => "Remove"
|
||||
};
|
||||
format_ident!("Resource{}", name)
|
||||
}
|
||||
|
||||
pub fn fn_ident(&self) -> Ident {
|
||||
use Method::*;
|
||||
|
||||
let name = match self {
|
||||
ReadAll => "read_all",
|
||||
Read => "read",
|
||||
Search => "search",
|
||||
Create => "create",
|
||||
ChangeAll => "change_all",
|
||||
Change => "change",
|
||||
RemoveAll => "remove_all",
|
||||
Remove => "remove"
|
||||
};
|
||||
format_ident!("{}", name)
|
||||
}
|
||||
|
||||
pub fn handler_struct_ident(&self, resource: &str) -> Ident {
|
||||
format_ident!("{}{}Handler", resource.to_camel_case(), self.trait_ident())
|
||||
}
|
||||
|
||||
pub fn setup_ident(&self, resource: &str) -> Ident {
|
||||
format_ident!("_gotham_restful_{}_{}_setup_impl", resource.to_snake_case(), self.fn_ident())
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
enum MethodArgumentType {
|
||||
StateRef,
|
||||
StateMutRef,
|
||||
MethodArg(Type),
|
||||
DatabaseConnection(Type),
|
||||
AuthStatus(Type),
|
||||
AuthStatusRef(Type)
|
||||
}
|
||||
|
||||
impl MethodArgumentType {
|
||||
fn is_method_arg(&self) -> bool {
|
||||
matches!(self, Self::MethodArg(_))
|
||||
}
|
||||
|
||||
fn is_database_conn(&self) -> bool {
|
||||
matches!(self, Self::DatabaseConnection(_))
|
||||
}
|
||||
|
||||
fn is_auth_status(&self) -> bool {
|
||||
matches!(self, Self::AuthStatus(_) | Self::AuthStatusRef(_))
|
||||
}
|
||||
|
||||
fn ty(&self) -> Option<&Type> {
|
||||
match self {
|
||||
Self::MethodArg(ty) | Self::DatabaseConnection(ty) | Self::AuthStatus(ty) | Self::AuthStatusRef(ty) => Some(ty),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
|
||||
fn quote_ty(&self) -> Option<TokenStream> {
|
||||
self.ty().map(|ty| quote!(#ty))
|
||||
}
|
||||
}
|
||||
|
||||
struct MethodArgument {
|
||||
ident: Ident,
|
||||
ident_span: Span,
|
||||
ty: MethodArgumentType
|
||||
}
|
||||
|
||||
impl Spanned for MethodArgument {
|
||||
fn span(&self) -> Span {
|
||||
self.ident_span
|
||||
}
|
||||
}
|
||||
|
||||
fn interpret_arg_ty(attrs: &[Attribute], name: &str, ty: Type) -> Result<MethodArgumentType> {
|
||||
let attr = attrs
|
||||
.iter()
|
||||
.find(|arg| arg.path.segments.iter().any(|path| &path.ident.to_string() == "rest_arg"))
|
||||
.map(|arg| arg.tokens.to_string());
|
||||
|
||||
// TODO issue a warning for _state usage once diagnostics become stable
|
||||
if attr.as_deref() == Some("state") || (attr.is_none() && (name == "state" || name == "_state")) {
|
||||
return match ty {
|
||||
Type::Reference(ty) => Ok(if ty.mutability.is_none() {
|
||||
MethodArgumentType::StateRef
|
||||
} else {
|
||||
MethodArgumentType::StateMutRef
|
||||
}),
|
||||
_ => Err(Error::new(
|
||||
ty.span(),
|
||||
"The state parameter has to be a (mutable) reference to gotham_restful::State"
|
||||
))
|
||||
};
|
||||
}
|
||||
|
||||
if cfg!(feature = "auth") && (attr.as_deref() == Some("auth") || (attr.is_none() && name == "auth")) {
|
||||
return Ok(match ty {
|
||||
Type::Reference(ty) => MethodArgumentType::AuthStatusRef(*ty.elem),
|
||||
ty => MethodArgumentType::AuthStatus(ty)
|
||||
});
|
||||
}
|
||||
|
||||
if cfg!(feature = "database")
|
||||
&& (attr.as_deref() == Some("connection") || attr.as_deref() == Some("conn") || (attr.is_none() && name == "conn"))
|
||||
{
|
||||
return Ok(MethodArgumentType::DatabaseConnection(match ty {
|
||||
Type::Reference(ty) => *ty.elem,
|
||||
ty => ty
|
||||
}));
|
||||
}
|
||||
|
||||
Ok(MethodArgumentType::MethodArg(ty))
|
||||
}
|
||||
|
||||
fn interpret_arg(index: usize, arg: &PatType) -> Result<MethodArgument> {
|
||||
let pat = &arg.pat;
|
||||
let ident = format_ident!("arg{}", index);
|
||||
let orig_name = quote!(#pat);
|
||||
let ty = interpret_arg_ty(&arg.attrs, &orig_name.to_string(), *arg.ty.clone())?;
|
||||
|
||||
Ok(MethodArgument {
|
||||
ident,
|
||||
ident_span: arg.pat.span(),
|
||||
ty
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
fn expand_operation_id(attrs: &[NestedMeta]) -> TokenStream {
|
||||
let mut operation_id: Option<&Lit> = None;
|
||||
for meta in attrs {
|
||||
if let NestedMeta::Meta(Meta::NameValue(kv)) = meta {
|
||||
if kv.path.segments.last().map(|p| p.ident.to_string()) == Some("operation_id".to_owned()) {
|
||||
operation_id = Some(&kv.lit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match operation_id {
|
||||
Some(operation_id) => quote! {
|
||||
fn operation_id() -> Option<String>
|
||||
{
|
||||
Some(#operation_id.to_string())
|
||||
}
|
||||
},
|
||||
None => quote!()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "openapi"))]
|
||||
fn expand_operation_id(_: &[NestedMeta]) -> TokenStream {
|
||||
quote!()
|
||||
}
|
||||
|
||||
fn expand_wants_auth(attrs: &[NestedMeta], default: bool) -> TokenStream {
|
||||
let default_lit = Lit::Bool(LitBool {
|
||||
value: default,
|
||||
span: Span::call_site()
|
||||
});
|
||||
let mut wants_auth = &default_lit;
|
||||
for meta in attrs {
|
||||
if let NestedMeta::Meta(Meta::NameValue(kv)) = meta {
|
||||
if kv.path.segments.last().map(|p| p.ident.to_string()) == Some("wants_auth".to_owned()) {
|
||||
wants_auth = &kv.lit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
quote! {
|
||||
fn wants_auth() -> bool
|
||||
{
|
||||
#wants_auth
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::comparison_chain)]
|
||||
fn setup_body(
|
||||
method: &Method,
|
||||
fun: &ItemFn,
|
||||
attrs: &[NestedMeta],
|
||||
resource_name: &str,
|
||||
resource_path: &Path
|
||||
) -> Result<TokenStream> {
|
||||
let krate = super::krate();
|
||||
|
||||
let fun_ident = &fun.sig.ident;
|
||||
let fun_is_async = fun.sig.asyncness.is_some();
|
||||
|
||||
if let Some(unsafety) = fun.sig.unsafety {
|
||||
return Err(Error::new(unsafety.span(), "Resource methods must not be unsafe"));
|
||||
}
|
||||
|
||||
let trait_ident = method.trait_ident();
|
||||
let method_ident = method.fn_ident();
|
||||
let handler_ident = method.handler_struct_ident(resource_name);
|
||||
|
||||
let (ret, is_no_content) = match &fun.sig.output {
|
||||
ReturnType::Default => (quote!(#krate::NoContent), true),
|
||||
ReturnType::Type(_, ty) => (quote!(#ty), false)
|
||||
};
|
||||
|
||||
// some default idents we'll need
|
||||
let state_ident = format_ident!("state");
|
||||
let repo_ident = format_ident!("repo");
|
||||
let conn_ident = format_ident!("conn");
|
||||
let auth_ident = format_ident!("auth");
|
||||
let res_ident = format_ident!("res");
|
||||
|
||||
// extract arguments into pattern, ident and type
|
||||
let args = fun
|
||||
.sig
|
||||
.inputs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, arg)| match arg {
|
||||
FnArg::Typed(arg) => interpret_arg(i, arg),
|
||||
FnArg::Receiver(_) => Err(Error::new(arg.span(), "Didn't expect self parameter"))
|
||||
})
|
||||
.collect_to_result()?;
|
||||
|
||||
// extract the generic parameters to use
|
||||
let ty_names = method.type_names();
|
||||
let ty_len = ty_names.len();
|
||||
let generics_args: Vec<&MethodArgument> = args.iter().filter(|arg| (*arg).ty.is_method_arg()).collect();
|
||||
if generics_args.len() > ty_len {
|
||||
return Err(Error::new(generics_args[ty_len].span(), "Too many arguments"));
|
||||
} else if generics_args.len() < ty_len {
|
||||
return Err(Error::new(fun_ident.span(), "Too few arguments"));
|
||||
}
|
||||
let generics: Vec<TokenStream> = generics_args
|
||||
.iter()
|
||||
.map(|arg| arg.ty.quote_ty().unwrap())
|
||||
.zip(ty_names)
|
||||
.map(|(arg, name)| {
|
||||
let ident = format_ident!("{}", name);
|
||||
quote!(type #ident = #arg;)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// extract the definition of our method
|
||||
let mut args_def: Vec<TokenStream> = args
|
||||
.iter()
|
||||
.filter(|arg| (*arg).ty.is_method_arg())
|
||||
.map(|arg| {
|
||||
let ident = &arg.ident;
|
||||
let ty = arg.ty.quote_ty();
|
||||
quote!(#ident : #ty)
|
||||
})
|
||||
.collect();
|
||||
args_def.insert(0, quote!(mut #state_ident : #krate::State));
|
||||
|
||||
// extract the arguments to pass over to the supplied method
|
||||
let args_pass: Vec<TokenStream> = args
|
||||
.iter()
|
||||
.map(|arg| match (&arg.ty, &arg.ident) {
|
||||
(MethodArgumentType::StateRef, _) => quote!(&#state_ident),
|
||||
(MethodArgumentType::StateMutRef, _) => quote!(&mut #state_ident),
|
||||
(MethodArgumentType::MethodArg(_), ident) => quote!(#ident),
|
||||
(MethodArgumentType::DatabaseConnection(_), _) => quote!(&#conn_ident),
|
||||
(MethodArgumentType::AuthStatus(_), _) => quote!(#auth_ident),
|
||||
(MethodArgumentType::AuthStatusRef(_), _) => quote!(&#auth_ident)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// prepare the method block
|
||||
let mut block = quote!(#fun_ident(#(#args_pass),*));
|
||||
let mut state_block = quote!();
|
||||
if fun_is_async {
|
||||
if let Some(arg) = args.iter().find(|arg| matches!((*arg).ty, MethodArgumentType::StateRef)) {
|
||||
return Err(Error::new(
|
||||
arg.span(),
|
||||
"async fn must not take &State as an argument as State is not Sync, consider taking &mut State"
|
||||
));
|
||||
}
|
||||
block = quote!(#block.await);
|
||||
}
|
||||
if is_no_content {
|
||||
block = quote!(#block; Default::default())
|
||||
}
|
||||
if let Some(arg) = args.iter().find(|arg| (*arg).ty.is_database_conn()) {
|
||||
if fun_is_async {
|
||||
return Err(Error::new(
|
||||
arg.span(),
|
||||
"async fn is not supported when database support is required, consider boxing"
|
||||
));
|
||||
}
|
||||
let conn_ty = arg.ty.quote_ty();
|
||||
state_block = quote! {
|
||||
#state_block
|
||||
let #repo_ident = <#krate::export::Repo<#conn_ty>>::borrow_from(&#state_ident).clone();
|
||||
};
|
||||
block = quote! {
|
||||
{
|
||||
let #res_ident = #repo_ident.run::<_, (#krate::State, #ret), ()>(move |#conn_ident| {
|
||||
let #res_ident = { #block };
|
||||
Ok((#state_ident, #res_ident))
|
||||
}).await.unwrap();
|
||||
#state_ident = #res_ident.0;
|
||||
#res_ident.1
|
||||
}
|
||||
};
|
||||
}
|
||||
if let Some(arg) = args.iter().find(|arg| (*arg).ty.is_auth_status()) {
|
||||
let auth_ty = arg.ty.quote_ty();
|
||||
state_block = quote! {
|
||||
#state_block
|
||||
let #auth_ident : #auth_ty = <#auth_ty>::borrow_from(&#state_ident).clone();
|
||||
};
|
||||
}
|
||||
|
||||
// prepare the where clause
|
||||
let mut where_clause = quote!(#resource_path : #krate::Resource,);
|
||||
for arg in args.iter().filter(|arg| (*arg).ty.is_auth_status()) {
|
||||
let auth_ty = arg.ty.quote_ty();
|
||||
where_clause = quote!(#where_clause #auth_ty : Clone,);
|
||||
}
|
||||
|
||||
// attribute generated code
|
||||
let operation_id = expand_operation_id(attrs);
|
||||
let wants_auth = expand_wants_auth(attrs, args.iter().any(|arg| (*arg).ty.is_auth_status()));
|
||||
|
||||
// put everything together
|
||||
let mut dummy = format_ident!("_IMPL_RESOURCEMETHOD_FOR_{}", fun_ident);
|
||||
dummy.set_span(Span::call_site());
|
||||
Ok(quote! {
|
||||
struct #handler_ident;
|
||||
|
||||
impl #krate::ResourceMethod for #handler_ident {
|
||||
type Res = #ret;
|
||||
|
||||
#operation_id
|
||||
#wants_auth
|
||||
}
|
||||
|
||||
impl #krate::#trait_ident for #handler_ident
|
||||
where #where_clause
|
||||
{
|
||||
#(#generics)*
|
||||
|
||||
fn #method_ident(#(#args_def),*) -> std::pin::Pin<Box<dyn std::future::Future<Output = (#krate::State, #ret)> + Send>> {
|
||||
#[allow(unused_imports)]
|
||||
use #krate::{export::FutureExt, FromState};
|
||||
|
||||
#state_block
|
||||
|
||||
async move {
|
||||
let #res_ident = { #block };
|
||||
(#state_ident, #res_ident)
|
||||
}.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
route.#method_ident::<#handler_ident>();
|
||||
})
|
||||
}
|
||||
|
||||
pub fn expand_method(method: Method, mut attrs: AttributeArgs, fun: ItemFn) -> Result<TokenStream> {
|
||||
let krate = super::krate();
|
||||
|
||||
// parse attributes
|
||||
if attrs.len() < 1 {
|
||||
return Err(Error::new(
|
||||
Span::call_site(),
|
||||
"Missing Resource struct. Example: #[read_all(MyResource)]"
|
||||
));
|
||||
}
|
||||
let resource_path = match attrs.remove(0) {
|
||||
NestedMeta::Meta(Meta::Path(path)) => path,
|
||||
p => {
|
||||
return Err(Error::new(
|
||||
p.span(),
|
||||
"Expected name of the Resource struct this method belongs to"
|
||||
))
|
||||
},
|
||||
};
|
||||
let resource_name = resource_path
|
||||
.segments
|
||||
.last()
|
||||
.map(|s| s.ident.to_string())
|
||||
.ok_or_else(|| Error::new(resource_path.span(), "Resource name must not be empty"))?;
|
||||
|
||||
let fun_vis = &fun.vis;
|
||||
let setup_ident = method.setup_ident(&resource_name);
|
||||
let setup_body = match setup_body(&method, &fun, &attrs, &resource_name, &resource_path) {
|
||||
Ok(body) => body,
|
||||
Err(err) => err.to_compile_error()
|
||||
};
|
||||
|
||||
Ok(quote! {
|
||||
#fun
|
||||
|
||||
#[deny(dead_code)]
|
||||
#[doc(hidden)]
|
||||
/// `gotham_restful` implementation detail.
|
||||
#fun_vis fn #setup_ident<D : #krate::DrawResourceRoutes>(route : &mut D) {
|
||||
#setup_body
|
||||
}
|
||||
})
|
||||
}
|
|
@ -3,7 +3,8 @@ use proc_macro2::{Ident, TokenStream};
|
|||
use quote::quote;
|
||||
use syn::{
|
||||
parse_macro_input, spanned::Spanned, Attribute, AttributeArgs, Data, DataEnum, DataStruct, DeriveInput, Error, Field,
|
||||
Fields, GenericParam, Generics, Lit, LitStr, Meta, NestedMeta, Result, Variant
|
||||
Fields, GenericParam, Generics, Lit, LitStr, Meta, NestedMeta, Path, PathSegment, PredicateType, Result, TraitBound,
|
||||
TraitBoundModifier, Type, TypeParamBound, TypePath, Variant, WhereClause, WherePredicate
|
||||
};
|
||||
|
||||
pub fn expand_openapi_type(input: DeriveInput) -> Result<TokenStream> {
|
||||
|
@ -17,24 +18,46 @@ pub fn expand_openapi_type(input: DeriveInput) -> Result<TokenStream> {
|
|||
}
|
||||
}
|
||||
|
||||
fn expand_where(generics: &Generics) -> TokenStream {
|
||||
fn update_generics(generics: &Generics, where_clause: &mut Option<WhereClause>) {
|
||||
if generics.params.is_empty() {
|
||||
return quote!();
|
||||
return;
|
||||
}
|
||||
|
||||
let krate = super::krate();
|
||||
let idents = generics
|
||||
.params
|
||||
.iter()
|
||||
.map(|param| match param {
|
||||
GenericParam::Type(ty) => Some(ty.ident.clone()),
|
||||
_ => None
|
||||
})
|
||||
.filter(|param| param.is_some())
|
||||
.map(|param| param.unwrap());
|
||||
if where_clause.is_none() {
|
||||
*where_clause = Some(WhereClause {
|
||||
where_token: Default::default(),
|
||||
predicates: Default::default()
|
||||
});
|
||||
}
|
||||
let where_clause = where_clause.as_mut().unwrap();
|
||||
|
||||
quote! {
|
||||
where #(#idents : #krate::OpenapiType),*
|
||||
for param in &generics.params {
|
||||
if let GenericParam::Type(ty_param) = param {
|
||||
where_clause.predicates.push(WherePredicate::Type(PredicateType {
|
||||
lifetimes: None,
|
||||
bounded_ty: Type::Path(TypePath {
|
||||
qself: None,
|
||||
path: Path {
|
||||
leading_colon: None,
|
||||
segments: vec![PathSegment {
|
||||
ident: ty_param.ident.clone(),
|
||||
arguments: Default::default()
|
||||
}]
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
}),
|
||||
colon_token: Default::default(),
|
||||
bounds: vec![TypeParamBound::Trait(TraitBound {
|
||||
paren_token: None,
|
||||
modifier: TraitBoundModifier::None,
|
||||
lifetimes: None,
|
||||
path: syn::parse_str("::gotham_restful::OpenapiType").unwrap()
|
||||
})]
|
||||
.into_iter()
|
||||
.collect()
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -106,7 +129,9 @@ fn expand_variant(variant: &Variant) -> Result<TokenStream> {
|
|||
|
||||
fn expand_enum(ident: Ident, generics: Generics, attrs: Vec<Attribute>, input: DataEnum) -> Result<TokenStream> {
|
||||
let krate = super::krate();
|
||||
let where_clause = expand_where(&generics);
|
||||
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
|
||||
let mut where_clause = where_clause.cloned();
|
||||
update_generics(&generics, &mut where_clause);
|
||||
|
||||
let attrs = parse_attributes(&attrs)?;
|
||||
let nullable = attrs.nullable;
|
||||
|
@ -118,7 +143,7 @@ fn expand_enum(ident: Ident, generics: Generics, attrs: Vec<Attribute>, input: D
|
|||
let variants = input.variants.iter().map(expand_variant).collect_to_result()?;
|
||||
|
||||
Ok(quote! {
|
||||
impl #generics #krate::OpenapiType for #ident #generics
|
||||
impl #impl_generics #krate::OpenapiType for #ident #ty_generics
|
||||
#where_clause
|
||||
{
|
||||
fn schema() -> #krate::OpenapiSchema
|
||||
|
@ -208,7 +233,9 @@ fn expand_field(field: &Field) -> Result<TokenStream> {
|
|||
|
||||
fn expand_struct(ident: Ident, generics: Generics, attrs: Vec<Attribute>, input: DataStruct) -> Result<TokenStream> {
|
||||
let krate = super::krate();
|
||||
let where_clause = expand_where(&generics);
|
||||
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
|
||||
let mut where_clause = where_clause.cloned();
|
||||
update_generics(&generics, &mut where_clause);
|
||||
|
||||
let attrs = parse_attributes(&attrs)?;
|
||||
let nullable = attrs.nullable;
|
||||
|
@ -229,7 +256,7 @@ fn expand_struct(ident: Ident, generics: Generics, attrs: Vec<Attribute>, input:
|
|||
};
|
||||
|
||||
Ok(quote! {
|
||||
impl #generics #krate::OpenapiType for #ident #generics
|
||||
impl #impl_generics #krate::OpenapiType for #ident #ty_generics
|
||||
#where_clause
|
||||
{
|
||||
fn schema() -> #krate::OpenapiSchema
|
||||
|
|
165
derive/src/private_openapi_trait.rs
Normal file
165
derive/src/private_openapi_trait.rs
Normal file
|
@ -0,0 +1,165 @@
|
|||
use crate::util::{remove_parens, CollectToResult, PathEndsWith};
|
||||
use proc_macro2::{Span, TokenStream};
|
||||
use quote::{quote, ToTokens};
|
||||
use syn::{
|
||||
parse::Parse, spanned::Spanned, Attribute, AttributeArgs, Error, ItemTrait, LitStr, Meta, NestedMeta, PredicateType,
|
||||
Result, TraitItem, WherePredicate
|
||||
};
|
||||
|
||||
struct TraitItemAttrs {
|
||||
openapi_only: bool,
|
||||
openapi_bound: Vec<PredicateType>,
|
||||
non_openapi_bound: Vec<PredicateType>,
|
||||
other_attrs: Vec<Attribute>
|
||||
}
|
||||
|
||||
impl TraitItemAttrs {
|
||||
fn parse(attrs: Vec<Attribute>) -> Result<Self> {
|
||||
let mut openapi_only = false;
|
||||
let mut openapi_bound = Vec::new();
|
||||
let mut non_openapi_bound = Vec::new();
|
||||
let mut other = Vec::new();
|
||||
|
||||
for attr in attrs {
|
||||
if attr.path.ends_with("openapi_only") {
|
||||
openapi_only = true;
|
||||
} else if attr.path.ends_with("openapi_bound") {
|
||||
let attr_arg: LitStr = syn::parse2(remove_parens(attr.tokens))?;
|
||||
let predicate = attr_arg.parse_with(WherePredicate::parse)?;
|
||||
openapi_bound.push(match predicate {
|
||||
WherePredicate::Type(ty) => ty,
|
||||
_ => return Err(Error::new(predicate.span(), "Expected type bound"))
|
||||
});
|
||||
} else if attr.path.ends_with("non_openapi_bound") {
|
||||
let attr_arg: LitStr = syn::parse2(remove_parens(attr.tokens))?;
|
||||
let predicate = attr_arg.parse_with(WherePredicate::parse)?;
|
||||
non_openapi_bound.push(match predicate {
|
||||
WherePredicate::Type(ty) => ty,
|
||||
_ => return Err(Error::new(predicate.span(), "Expected type bound"))
|
||||
});
|
||||
} else {
|
||||
other.push(attr);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
openapi_only,
|
||||
openapi_bound,
|
||||
non_openapi_bound,
|
||||
other_attrs: other
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn expand_private_openapi_trait(mut attrs: AttributeArgs, tr8: ItemTrait) -> Result<TokenStream> {
|
||||
let tr8_attrs = &tr8.attrs;
|
||||
let vis = &tr8.vis;
|
||||
let ident = &tr8.ident;
|
||||
let generics = &tr8.generics;
|
||||
let colon_token = &tr8.colon_token;
|
||||
let supertraits = &tr8.supertraits;
|
||||
|
||||
if attrs.len() != 1 {
|
||||
return Err(Error::new(
|
||||
Span::call_site(),
|
||||
"Expected one argument. Example: #[_private_openapi_trait(OpenapiTraitName)]"
|
||||
));
|
||||
}
|
||||
let openapi_ident = match attrs.remove(0) {
|
||||
NestedMeta::Meta(Meta::Path(path)) => path,
|
||||
p => {
|
||||
return Err(Error::new(
|
||||
p.span(),
|
||||
"Expected name of the Resource struct this method belongs to"
|
||||
))
|
||||
},
|
||||
};
|
||||
|
||||
let orig_trait = {
|
||||
let items = tr8
|
||||
.items
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|item| {
|
||||
Ok(match item {
|
||||
TraitItem::Method(mut method) => {
|
||||
let attrs = TraitItemAttrs::parse(method.attrs)?;
|
||||
method.attrs = attrs.other_attrs;
|
||||
for bound in attrs.non_openapi_bound {
|
||||
method
|
||||
.sig
|
||||
.generics
|
||||
.type_params_mut()
|
||||
.filter(|param| param.ident.to_string() == bound.bounded_ty.to_token_stream().to_string())
|
||||
.for_each(|param| param.bounds.extend(bound.bounds.clone()));
|
||||
}
|
||||
if attrs.openapi_only {
|
||||
None
|
||||
} else {
|
||||
Some(TraitItem::Method(method))
|
||||
}
|
||||
},
|
||||
TraitItem::Type(mut ty) => {
|
||||
let attrs = TraitItemAttrs::parse(ty.attrs)?;
|
||||
ty.attrs = attrs.other_attrs;
|
||||
Some(TraitItem::Type(ty))
|
||||
},
|
||||
item => Some(item)
|
||||
})
|
||||
})
|
||||
.collect_to_result()?;
|
||||
quote! {
|
||||
#(#tr8_attrs)*
|
||||
#vis trait #ident #generics #colon_token #supertraits {
|
||||
#(#items)*
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let openapi_trait = if !cfg!(feature = "openapi") {
|
||||
None
|
||||
} else {
|
||||
let items = tr8
|
||||
.items
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|item| {
|
||||
Ok(match item {
|
||||
TraitItem::Method(mut method) => {
|
||||
let attrs = TraitItemAttrs::parse(method.attrs)?;
|
||||
method.attrs = attrs.other_attrs;
|
||||
for bound in attrs.openapi_bound {
|
||||
method
|
||||
.sig
|
||||
.generics
|
||||
.type_params_mut()
|
||||
.filter(|param| param.ident.to_string() == bound.bounded_ty.to_token_stream().to_string())
|
||||
.for_each(|param| param.bounds.extend(bound.bounds.clone()));
|
||||
}
|
||||
TraitItem::Method(method)
|
||||
},
|
||||
TraitItem::Type(mut ty) => {
|
||||
let attrs = TraitItemAttrs::parse(ty.attrs)?;
|
||||
ty.attrs = attrs.other_attrs;
|
||||
for bound in attrs.openapi_bound {
|
||||
ty.bounds.extend(bound.bounds.clone());
|
||||
}
|
||||
TraitItem::Type(ty)
|
||||
},
|
||||
item => item
|
||||
})
|
||||
})
|
||||
.collect_to_result()?;
|
||||
Some(quote! {
|
||||
#(#tr8_attrs)*
|
||||
#vis trait #openapi_ident #generics #colon_token #supertraits {
|
||||
#(#items)*
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
Ok(quote! {
|
||||
#orig_trait
|
||||
#openapi_trait
|
||||
})
|
||||
}
|
|
@ -1,12 +1,15 @@
|
|||
use crate::{method::Method, util::CollectToResult};
|
||||
use crate::{
|
||||
endpoint::endpoint_ident,
|
||||
util::{CollectToResult, PathEndsWith}
|
||||
};
|
||||
use proc_macro2::{Ident, TokenStream};
|
||||
use quote::quote;
|
||||
use std::{iter, str::FromStr};
|
||||
use std::iter;
|
||||
use syn::{
|
||||
parenthesized,
|
||||
parse::{Parse, ParseStream},
|
||||
punctuated::Punctuated,
|
||||
DeriveInput, Error, Result, Token
|
||||
DeriveInput, Result, Token
|
||||
};
|
||||
|
||||
struct MethodList(Punctuated<Ident, Token![,]>);
|
||||
|
@ -23,29 +26,22 @@ impl Parse for MethodList {
|
|||
pub fn expand_resource(input: DeriveInput) -> Result<TokenStream> {
|
||||
let krate = super::krate();
|
||||
let ident = input.ident;
|
||||
let name = ident.to_string();
|
||||
|
||||
let methods =
|
||||
input
|
||||
.attrs
|
||||
.into_iter()
|
||||
.filter(
|
||||
|attr| {
|
||||
attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("resource".to_string())
|
||||
} // TODO wtf
|
||||
)
|
||||
.map(|attr| syn::parse2(attr.tokens).map(|m: MethodList| m.0.into_iter()))
|
||||
.flat_map(|list| match list {
|
||||
Ok(iter) => Box::new(iter.map(|method| {
|
||||
let method = Method::from_str(&method.to_string()).map_err(|err| Error::new(method.span(), err))?;
|
||||
let ident = method.setup_ident(&name);
|
||||
Ok(quote!(#ident(&mut route);))
|
||||
})) as Box<dyn Iterator<Item = Result<TokenStream>>>,
|
||||
Err(err) => Box::new(iter::once(Err(err)))
|
||||
})
|
||||
.collect_to_result()?;
|
||||
let methods = input
|
||||
.attrs
|
||||
.into_iter()
|
||||
.filter(|attr| attr.path.ends_with("resource"))
|
||||
.map(|attr| syn::parse2(attr.tokens).map(|m: MethodList| m.0.into_iter()))
|
||||
.flat_map(|list| match list {
|
||||
Ok(iter) => Box::new(iter.map(|method| {
|
||||
let ident = endpoint_ident(&method);
|
||||
Ok(quote!(route.endpoint::<#ident>();))
|
||||
})) as Box<dyn Iterator<Item = Result<TokenStream>>>,
|
||||
Err(err) => Box::new(iter::once(Err(err)))
|
||||
})
|
||||
.collect_to_result()?;
|
||||
|
||||
Ok(quote! {
|
||||
let non_openapi_impl = quote! {
|
||||
impl #krate::Resource for #ident
|
||||
{
|
||||
fn setup<D : #krate::DrawResourceRoutes>(mut route : D)
|
||||
|
@ -53,5 +49,22 @@ pub fn expand_resource(input: DeriveInput) -> Result<TokenStream> {
|
|||
#(#methods)*
|
||||
}
|
||||
}
|
||||
};
|
||||
let openapi_impl = if !cfg!(feature = "openapi") {
|
||||
None
|
||||
} else {
|
||||
Some(quote! {
|
||||
impl #krate::ResourceWithSchema for #ident
|
||||
{
|
||||
fn setup<D : #krate::DrawResourceRoutesWithSchema>(mut route : D)
|
||||
{
|
||||
#(#methods)*
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
Ok(quote! {
|
||||
#non_openapi_impl
|
||||
#openapi_impl
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use proc_macro2::{Delimiter, TokenStream, TokenTree};
|
||||
use std::iter;
|
||||
use syn::Error;
|
||||
use syn::{Error, Path};
|
||||
|
||||
pub trait CollectToResult {
|
||||
type Item;
|
||||
|
@ -30,6 +30,16 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) trait PathEndsWith {
|
||||
fn ends_with(&self, s: &str) -> bool;
|
||||
}
|
||||
|
||||
impl PathEndsWith for Path {
|
||||
fn ends_with(&self, s: &str) -> bool {
|
||||
self.segments.last().map(|segment| segment.ident.to_string()).as_deref() == Some(s)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_parens(input: TokenStream) -> TokenStream {
|
||||
let iter = input.into_iter().flat_map(|tt| {
|
||||
if let TokenTree::Group(group) = &tt {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue