mirror of
https://gitlab.com/msrd0/gotham-restful.git
synced 2025-04-20 23:07:01 +00:00
merge workspace and main crate
This commit is contained in:
parent
52679ad29d
commit
5587ded60d
45 changed files with 58 additions and 67 deletions
31
derive/Cargo.toml
Normal file
31
derive/Cargo.toml
Normal file
|
@ -0,0 +1,31 @@
|
|||
# -*- eval: (cargo-minor-mode 1) -*-
|
||||
|
||||
[package]
|
||||
name = "gotham_restful_derive"
|
||||
version = "0.1.0-dev"
|
||||
authors = ["Dominic Meiser <git@msrd0.de>"]
|
||||
edition = "2018"
|
||||
description = "RESTful additions for the gotham web framework - Derive"
|
||||
keywords = ["gotham", "rest", "restful", "web", "http", "derive"]
|
||||
license = "EPL-2.0 OR Apache-2.0"
|
||||
repository = "https://gitlab.com/msrd0/gotham-restful"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[badges]
|
||||
gitlab = { repository = "msrd0/gotham-restful", branch = "master" }
|
||||
|
||||
[dependencies]
|
||||
heck = "0.3.1"
|
||||
lazy_static = "1.4.0"
|
||||
proc-macro2 = "1.0.12"
|
||||
quote = "1.0.4"
|
||||
regex = "1.3.7"
|
||||
syn = "1.0.18"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
auth = []
|
||||
database = []
|
||||
openapi = []
|
1
derive/LICENSE-Apache
Symbolic link
1
derive/LICENSE-Apache
Symbolic link
|
@ -0,0 +1 @@
|
|||
../LICENSE-Apache
|
1
derive/LICENSE-EPL
Symbolic link
1
derive/LICENSE-EPL
Symbolic link
|
@ -0,0 +1 @@
|
|||
../LICENSE-EPL
|
1
derive/LICENSE.md
Symbolic link
1
derive/LICENSE.md
Symbolic link
|
@ -0,0 +1 @@
|
|||
../LICENSE.md
|
132
derive/src/from_body.rs
Normal file
132
derive/src/from_body.rs
Normal file
|
@ -0,0 +1,132 @@
|
|||
use proc_macro2::TokenStream;
|
||||
use quote::{format_ident, quote};
|
||||
use std::cmp::min;
|
||||
use syn::{
|
||||
spanned::Spanned,
|
||||
Data,
|
||||
DeriveInput,
|
||||
Error,
|
||||
Field,
|
||||
Fields,
|
||||
Ident,
|
||||
Result,
|
||||
Type
|
||||
};
|
||||
|
||||
struct ParsedFields
|
||||
{
|
||||
fields : Vec<(Ident, Type)>,
|
||||
named : bool
|
||||
}
|
||||
|
||||
impl ParsedFields
|
||||
{
|
||||
fn from_named<I>(fields : I) -> Self
|
||||
where
|
||||
I : Iterator<Item = Field>
|
||||
{
|
||||
let fields = fields.map(|field| (field.ident.unwrap(), field.ty)).collect();
|
||||
Self { fields, named: true }
|
||||
}
|
||||
|
||||
fn from_unnamed<I>(fields : I) -> Self
|
||||
where
|
||||
I : Iterator<Item = Field>
|
||||
{
|
||||
let fields = fields.enumerate().map(|(i, field)| (format_ident!("arg{}", i), field.ty)).collect();
|
||||
Self { fields, named: false }
|
||||
}
|
||||
|
||||
fn from_unit() -> Self
|
||||
{
|
||||
Self { fields: Vec::new(), named: false }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expand_from_body(input : DeriveInput) -> Result<TokenStream>
|
||||
{
|
||||
let krate = super::krate();
|
||||
let ident = input.ident;
|
||||
let generics = input.generics;
|
||||
|
||||
let strukt = match input.data {
|
||||
Data::Enum(inum) => Err(inum.enum_token.span()),
|
||||
Data::Struct(strukt) => Ok(strukt),
|
||||
Data::Union(uni) => Err(uni.union_token.span())
|
||||
}.map_err(|span| Error::new(span, "#[derive(FromBody)] only works for structs"))?;
|
||||
|
||||
let fields = match strukt.fields {
|
||||
Fields::Named(named) => ParsedFields::from_named(named.named.into_iter()),
|
||||
Fields::Unnamed(unnamed) => ParsedFields::from_unnamed(unnamed.unnamed.into_iter()),
|
||||
Fields::Unit => ParsedFields::from_unit()
|
||||
};
|
||||
|
||||
let mut where_clause = quote!();
|
||||
let mut block = quote!();
|
||||
let mut body_ident = format_ident!("_body");
|
||||
let mut type_ident = format_ident!("_type");
|
||||
|
||||
if let Some(body_field) = fields.fields.get(0)
|
||||
{
|
||||
body_ident = body_field.0.clone();
|
||||
let body_ty = &body_field.1;
|
||||
where_clause = quote! {
|
||||
#where_clause
|
||||
#body_ty : for<'a> From<&'a [u8]>,
|
||||
};
|
||||
block = quote! {
|
||||
#block
|
||||
let #body_ident : &[u8] = &#body_ident;
|
||||
let #body_ident : #body_ty = #body_ident.into();
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(type_field) = fields.fields.get(1)
|
||||
{
|
||||
type_ident = type_field.0.clone();
|
||||
let type_ty = &type_field.1;
|
||||
where_clause = quote! {
|
||||
#where_clause
|
||||
#type_ty : From<#krate::Mime>,
|
||||
};
|
||||
block = quote! {
|
||||
#block
|
||||
let #type_ident : #type_ty = #type_ident.into();
|
||||
};
|
||||
}
|
||||
|
||||
for field in &fields.fields[min(2, fields.fields.len())..]
|
||||
{
|
||||
let field_ident = &field.0;
|
||||
let field_ty = &field.1;
|
||||
where_clause = quote! {
|
||||
#where_clause
|
||||
#field_ty : Default,
|
||||
};
|
||||
block = quote! {
|
||||
#block
|
||||
let #field_ident : #field_ty = Default::default();
|
||||
};
|
||||
}
|
||||
|
||||
let field_names : Vec<&Ident> = fields.fields.iter().map(|field| &field.0).collect();
|
||||
let ctor = if fields.named {
|
||||
quote!(Self { #(#field_names),* })
|
||||
} else {
|
||||
quote!(Self ( #(#field_names),* ))
|
||||
};
|
||||
|
||||
Ok(quote! {
|
||||
impl #generics #krate::FromBody for #ident #generics
|
||||
where #where_clause
|
||||
{
|
||||
type Err = ::std::convert::Infallible;
|
||||
|
||||
fn from_body(#body_ident : #krate::gotham::hyper::body::Bytes, #type_ident : #krate::Mime) -> Result<Self, ::std::convert::Infallible>
|
||||
{
|
||||
#block
|
||||
Ok(#ctor)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
134
derive/src/lib.rs
Normal file
134
derive/src/lib.rs
Normal file
|
@ -0,0 +1,134 @@
|
|||
use proc_macro::TokenStream;
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::quote;
|
||||
use syn::{parse_macro_input, parse_macro_input::ParseMacroInput, DeriveInput, Result};
|
||||
|
||||
mod util;
|
||||
|
||||
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;
|
||||
|
||||
#[inline]
|
||||
fn print_tokens(tokens : TokenStream2) -> TokenStream
|
||||
{
|
||||
//eprintln!("{}", tokens);
|
||||
tokens.into()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn expand_derive<F>(input : TokenStream, expand : F) -> TokenStream
|
||||
where
|
||||
F : FnOnce(DeriveInput) -> Result<TokenStream2>
|
||||
{
|
||||
print_tokens(expand(parse_macro_input!(input))
|
||||
.unwrap_or_else(|err| err.to_compile_error()))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn expand_macro<F, A, I>(attrs : TokenStream, item : TokenStream, expand : F) -> TokenStream
|
||||
where
|
||||
F : FnOnce(A, I) -> Result<TokenStream2>,
|
||||
A : ParseMacroInput,
|
||||
I : ParseMacroInput
|
||||
{
|
||||
print_tokens(expand(parse_macro_input!(attrs), parse_macro_input!(item))
|
||||
.unwrap_or_else(|err| err.to_compile_error()))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn krate() -> TokenStream2
|
||||
{
|
||||
quote!(::gotham_restful)
|
||||
}
|
||||
|
||||
#[proc_macro_derive(FromBody)]
|
||||
pub fn derive_from_body(input : TokenStream) -> TokenStream
|
||||
{
|
||||
expand_derive(input, expand_from_body)
|
||||
}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
#[proc_macro_derive(OpenapiType, attributes(openapi))]
|
||||
pub fn derive_openapi_type(input : TokenStream) -> TokenStream
|
||||
{
|
||||
expand_derive(input, expand_openapi_type)
|
||||
}
|
||||
|
||||
#[proc_macro_derive(RequestBody, attributes(supported_types))]
|
||||
pub fn derive_request_body(input : TokenStream) -> TokenStream
|
||||
{
|
||||
expand_derive(input, expand_request_body)
|
||||
}
|
||||
|
||||
#[proc_macro_derive(Resource, attributes(resource))]
|
||||
pub fn derive_resource(input : TokenStream) -> TokenStream
|
||||
{
|
||||
expand_derive(input, expand_resource)
|
||||
}
|
||||
|
||||
#[proc_macro_derive(ResourceError, attributes(display, from, status))]
|
||||
pub fn derive_resource_error(input : TokenStream) -> TokenStream
|
||||
{
|
||||
expand_derive(input, expand_resource_error)
|
||||
}
|
||||
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn read_all(attr : TokenStream, item : TokenStream) -> TokenStream
|
||||
{
|
||||
expand_macro(attr, item, |attr, item| expand_method(Method::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))
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn search(attr : TokenStream, item : TokenStream) -> TokenStream
|
||||
{
|
||||
expand_macro(attr, item, |attr, item| expand_method(Method::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))
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn change_all(attr : TokenStream, item : TokenStream) -> TokenStream
|
||||
{
|
||||
expand_macro(attr, item, |attr, item| expand_method(Method::ChangeAll, 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))
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn delete_all(attr : TokenStream, item : TokenStream) -> TokenStream
|
||||
{
|
||||
expand_macro(attr, item, |attr, item| expand_method(Method::RemoveAll, attr, item))
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn delete(attr : TokenStream, item : TokenStream) -> TokenStream
|
||||
{
|
||||
expand_macro(attr, item, |attr, item| expand_method(Method::Remove, attr, item))
|
||||
}
|
471
derive/src/method.rs
Normal file
471
derive/src/method.rs
Normal file
|
@ -0,0 +1,471 @@
|
|||
use crate::util::CollectToResult;
|
||||
use heck::{CamelCase, SnakeCase};
|
||||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
use quote::{format_ident, quote};
|
||||
use syn::{
|
||||
spanned::Spanned,
|
||||
Attribute,
|
||||
AttributeArgs,
|
||||
Error,
|
||||
FnArg,
|
||||
ItemFn,
|
||||
Lit,
|
||||
LitBool,
|
||||
Meta,
|
||||
NestedMeta,
|
||||
PatType,
|
||||
Result,
|
||||
ReturnType,
|
||||
Type
|
||||
};
|
||||
use std::str::FromStr;
|
||||
|
||||
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 mod_ident(&self, resource : &str) -> Ident
|
||||
{
|
||||
format_ident!("_gotham_restful_resource_{}_method_{}", resource.to_snake_case(), self.fn_ident())
|
||||
}
|
||||
|
||||
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!("{}_{}_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_state_ref(&self) -> bool
|
||||
{
|
||||
matches!(self, Self::StateRef | Self::StateMutRef)
|
||||
}
|
||||
|
||||
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 quote_ty(&self) -> Option<TokenStream>
|
||||
{
|
||||
match self {
|
||||
Self::MethodArg(ty) | Self::DatabaseConnection(ty) | Self::AuthStatus(ty) | Self::AuthStatusRef(ty) => Some(quote!(#ty)),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)]
|
||||
pub fn expand_method(method : Method, mut attrs : AttributeArgs, fun : ItemFn) -> Result<TokenStream>
|
||||
{
|
||||
let krate = super::krate();
|
||||
|
||||
// parse attributes
|
||||
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_ident = &fun.sig.ident;
|
||||
let fun_vis = &fun.vis;
|
||||
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 mod_ident = method.mod_ident(&resource_name);
|
||||
let handler_ident = method.handler_struct_ident(&resource_name);
|
||||
let setup_ident = method.setup_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| (*arg).ty.is_state_ref())
|
||||
{
|
||||
return Err(Error::new(arg.span(), "async fn must not take &State as an argument as State is not Sync"));
|
||||
}
|
||||
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
|
||||
Ok(quote! {
|
||||
#fun
|
||||
|
||||
#fun_vis mod #mod_ident
|
||||
{
|
||||
use super::*;
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
#[deny(dead_code)]
|
||||
pub fn #setup_ident<D : #krate::DrawResourceRoutes>(route : &mut D)
|
||||
{
|
||||
route.#method_ident::<#handler_ident>();
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
}
|
280
derive/src/openapi_type.rs
Normal file
280
derive/src/openapi_type.rs
Normal file
|
@ -0,0 +1,280 @@
|
|||
use crate::util::{CollectToResult, remove_parens};
|
||||
use proc_macro2::{Ident, TokenStream};
|
||||
use quote::quote;
|
||||
use syn::{
|
||||
parse_macro_input,
|
||||
spanned::Spanned,
|
||||
Attribute,
|
||||
AttributeArgs,
|
||||
Data,
|
||||
DataEnum,
|
||||
DataStruct,
|
||||
DeriveInput,
|
||||
Error,
|
||||
Field,
|
||||
Fields,
|
||||
Generics,
|
||||
GenericParam,
|
||||
Lit,
|
||||
Meta,
|
||||
NestedMeta,
|
||||
Result,
|
||||
Variant
|
||||
};
|
||||
|
||||
pub fn expand_openapi_type(input : DeriveInput) -> Result<TokenStream>
|
||||
{
|
||||
match (input.ident, input.generics, input.attrs, input.data) {
|
||||
(ident, generics, attrs, Data::Enum(inum)) => expand_enum(ident, generics, attrs, inum),
|
||||
(ident, generics, attrs, Data::Struct(strukt)) => expand_struct(ident, generics, attrs, strukt),
|
||||
(_, _, _, Data::Union(uni)) => Err(Error::new(uni.union_token.span(), "#[derive(OpenapiType)] only works for structs and enums"))
|
||||
}
|
||||
}
|
||||
|
||||
fn expand_where(generics : &Generics) -> TokenStream
|
||||
{
|
||||
if generics.params.is_empty()
|
||||
{
|
||||
return quote!();
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
quote! {
|
||||
where #(#idents : #krate::OpenapiType),*
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct Attrs
|
||||
{
|
||||
nullable : bool,
|
||||
rename : Option<String>
|
||||
}
|
||||
|
||||
fn to_string(lit : &Lit) -> Result<String>
|
||||
{
|
||||
match lit {
|
||||
Lit::Str(str) => Ok(str.value()),
|
||||
_ => Err(Error::new(lit.span(), "Expected string literal"))
|
||||
}
|
||||
}
|
||||
|
||||
fn to_bool(lit : &Lit) -> Result<bool>
|
||||
{
|
||||
match lit {
|
||||
Lit::Bool(bool) => Ok(bool.value),
|
||||
_ => Err(Error::new(lit.span(), "Expected bool"))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_attributes(input : &[Attribute]) -> Result<Attrs>
|
||||
{
|
||||
let mut parsed = Attrs::default();
|
||||
for attr in input
|
||||
{
|
||||
if attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("openapi".to_owned())
|
||||
{
|
||||
let tokens = remove_parens(attr.tokens.clone());
|
||||
// TODO this is not public api but syn currently doesn't offer another convenient way to parse AttributeArgs
|
||||
let nested = parse_macro_input::parse::<AttributeArgs>(tokens.into())?;
|
||||
for meta in nested
|
||||
{
|
||||
match &meta {
|
||||
NestedMeta::Meta(Meta::NameValue(kv)) => match kv.path.segments.last().map(|s| s.ident.to_string()) {
|
||||
Some(key) => match key.as_ref() {
|
||||
"nullable" => parsed.nullable = to_bool(&kv.lit)?,
|
||||
"rename" => parsed.rename = Some(to_string(&kv.lit)?),
|
||||
_ => return Err(Error::new(kv.path.span(), "Unknown key")),
|
||||
},
|
||||
_ => return Err(Error::new(meta.span(), "Unexpected token"))
|
||||
},
|
||||
_ => return Err(Error::new(meta.span(), "Unexpected token"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
fn expand_variant(variant : &Variant) -> Result<TokenStream>
|
||||
{
|
||||
if variant.fields != Fields::Unit
|
||||
{
|
||||
return Err(Error::new(variant.span(), "Enum Variants with Fields not supported"));
|
||||
}
|
||||
|
||||
let ident = &variant.ident;
|
||||
|
||||
let attrs = parse_attributes(&variant.attrs)?;
|
||||
let name = match attrs.rename {
|
||||
Some(rename) => rename,
|
||||
None => ident.to_string()
|
||||
};
|
||||
|
||||
Ok(quote! {
|
||||
enumeration.push(#name.to_string());
|
||||
})
|
||||
}
|
||||
|
||||
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 attrs = parse_attributes(&attrs)?;
|
||||
let nullable = attrs.nullable;
|
||||
let name = match attrs.rename {
|
||||
Some(rename) => rename,
|
||||
None => ident.to_string()
|
||||
};
|
||||
|
||||
let variants = input.variants.iter()
|
||||
.map(expand_variant)
|
||||
.collect_to_result()?;
|
||||
|
||||
Ok(quote! {
|
||||
impl #generics #krate::OpenapiType for #ident #generics
|
||||
#where_clause
|
||||
{
|
||||
fn schema() -> #krate::OpenapiSchema
|
||||
{
|
||||
use #krate::{export::openapi::*, OpenapiSchema};
|
||||
|
||||
let mut enumeration : Vec<String> = Vec::new();
|
||||
|
||||
#(#variants)*
|
||||
|
||||
let schema = SchemaKind::Type(Type::String(StringType {
|
||||
format: VariantOrUnknownOrEmpty::Empty,
|
||||
enumeration,
|
||||
..Default::default()
|
||||
}));
|
||||
|
||||
OpenapiSchema {
|
||||
name: Some(#name.to_string()),
|
||||
nullable: #nullable,
|
||||
schema,
|
||||
dependencies: Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn expand_field(field : &Field) -> Result<TokenStream>
|
||||
{
|
||||
let ident = match &field.ident {
|
||||
Some(ident) => ident,
|
||||
None => return Err(Error::new(field.span(), "Fields without ident are not supported"))
|
||||
};
|
||||
let ty = &field.ty;
|
||||
|
||||
let attrs = parse_attributes(&field.attrs)?;
|
||||
let nullable = attrs.nullable;
|
||||
let name = match attrs.rename {
|
||||
Some(rename) => rename,
|
||||
None => ident.to_string()
|
||||
};
|
||||
|
||||
Ok(quote! {{
|
||||
let mut schema = <#ty>::schema();
|
||||
|
||||
if schema.nullable
|
||||
{
|
||||
schema.nullable = false;
|
||||
}
|
||||
else if !#nullable
|
||||
{
|
||||
required.push(stringify!(#ident).to_string());
|
||||
}
|
||||
|
||||
let keys : Vec<String> = schema.dependencies.keys().map(|k| k.to_string()).collect();
|
||||
for dep in keys
|
||||
{
|
||||
let dep_schema = schema.dependencies.swap_remove(&dep);
|
||||
if let Some(dep_schema) = dep_schema
|
||||
{
|
||||
dependencies.insert(dep, dep_schema);
|
||||
}
|
||||
}
|
||||
|
||||
match schema.name.clone() {
|
||||
Some(schema_name) => {
|
||||
properties.insert(
|
||||
#name.to_string(),
|
||||
ReferenceOr::Reference { reference: format!("#/components/schemas/{}", schema_name) }
|
||||
);
|
||||
dependencies.insert(schema_name, schema);
|
||||
},
|
||||
None => {
|
||||
properties.insert(
|
||||
#name.to_string(),
|
||||
ReferenceOr::Item(Box::new(schema.into_schema()))
|
||||
);
|
||||
}
|
||||
}
|
||||
}})
|
||||
}
|
||||
|
||||
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 attrs = parse_attributes(&attrs)?;
|
||||
let nullable = attrs.nullable;
|
||||
let name = match attrs.rename {
|
||||
Some(rename) => rename,
|
||||
None => ident.to_string()
|
||||
};
|
||||
|
||||
let fields : Vec<TokenStream> = match input.fields {
|
||||
Fields::Named(named_fields) => {
|
||||
named_fields.named.iter()
|
||||
.map(expand_field)
|
||||
.collect_to_result()?
|
||||
},
|
||||
Fields::Unnamed(fields) => return Err(Error::new(fields.span(), "Unnamed fields are not supported")),
|
||||
Fields::Unit => Vec::new()
|
||||
};
|
||||
|
||||
Ok(quote!{
|
||||
impl #generics #krate::OpenapiType for #ident #generics
|
||||
#where_clause
|
||||
{
|
||||
fn schema() -> #krate::OpenapiSchema
|
||||
{
|
||||
use #krate::{export::{openapi::*, IndexMap}, OpenapiSchema};
|
||||
|
||||
let mut properties : IndexMap<String, ReferenceOr<Box<Schema>>> = IndexMap::new();
|
||||
let mut required : Vec<String> = Vec::new();
|
||||
let mut dependencies : IndexMap<String, OpenapiSchema> = IndexMap::new();
|
||||
|
||||
#(#fields)*
|
||||
|
||||
let schema = SchemaKind::Type(Type::Object(ObjectType {
|
||||
properties,
|
||||
required,
|
||||
additional_properties: None,
|
||||
min_properties: None,
|
||||
max_properties: None
|
||||
}));
|
||||
|
||||
OpenapiSchema {
|
||||
name: Some(#name.to_string()),
|
||||
nullable: #nullable,
|
||||
schema,
|
||||
dependencies
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
89
derive/src/request_body.rs
Normal file
89
derive/src/request_body.rs
Normal file
|
@ -0,0 +1,89 @@
|
|||
use crate::util::CollectToResult;
|
||||
use proc_macro2::{Ident, TokenStream};
|
||||
use quote::quote;
|
||||
use std::iter;
|
||||
use syn::{
|
||||
parenthesized,
|
||||
parse::{Parse, ParseStream},
|
||||
punctuated::Punctuated,
|
||||
DeriveInput,
|
||||
Generics,
|
||||
Path,
|
||||
Result,
|
||||
Token
|
||||
};
|
||||
|
||||
struct MimeList(Punctuated<Path, Token![,]>);
|
||||
|
||||
impl Parse for MimeList
|
||||
{
|
||||
fn parse(input: ParseStream) -> Result<Self>
|
||||
{
|
||||
let content;
|
||||
let _paren = parenthesized!(content in input);
|
||||
let list = Punctuated::parse_separated_nonempty(&content)?;
|
||||
Ok(Self(list))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "openapi"))]
|
||||
fn impl_openapi_type(_ident : &Ident, _generics : &Generics) -> TokenStream
|
||||
{
|
||||
quote!()
|
||||
}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
fn impl_openapi_type(ident : &Ident, generics : &Generics) -> TokenStream
|
||||
{
|
||||
let krate = super::krate();
|
||||
|
||||
quote! {
|
||||
impl #generics #krate::OpenapiType for #ident #generics
|
||||
{
|
||||
fn schema() -> #krate::OpenapiSchema
|
||||
{
|
||||
use #krate::{export::openapi::*, OpenapiSchema};
|
||||
|
||||
OpenapiSchema::new(SchemaKind::Type(Type::String(StringType {
|
||||
format: VariantOrUnknownOrEmpty::Item(StringFormat::Binary),
|
||||
..Default::default()
|
||||
})))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expand_request_body(input : DeriveInput) -> Result<TokenStream>
|
||||
{
|
||||
let krate = super::krate();
|
||||
let ident = input.ident;
|
||||
let generics = input.generics;
|
||||
|
||||
let types = input.attrs.into_iter()
|
||||
.filter(|attr| attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("supported_types".to_string()))
|
||||
.flat_map(|attr|
|
||||
syn::parse2::<MimeList>(attr.tokens)
|
||||
.map(|list| Box::new(list.0.into_iter().map(Ok)) as Box<dyn Iterator<Item = Result<Path>>>)
|
||||
.unwrap_or_else(|err| Box::new(iter::once(Err(err)))))
|
||||
.collect_to_result()?;
|
||||
|
||||
let types = match types {
|
||||
ref types if types.is_empty() => quote!(None),
|
||||
types => quote!(Some(vec![#(#types),*]))
|
||||
};
|
||||
|
||||
let impl_openapi_type = impl_openapi_type(&ident, &generics);
|
||||
|
||||
Ok(quote! {
|
||||
impl #generics #krate::RequestBody for #ident #generics
|
||||
where #ident #generics : #krate::FromBody
|
||||
{
|
||||
fn supported_types() -> Option<Vec<#krate::Mime>>
|
||||
{
|
||||
#types
|
||||
}
|
||||
}
|
||||
|
||||
#impl_openapi_type
|
||||
})
|
||||
}
|
62
derive/src/resource.rs
Normal file
62
derive/src/resource.rs
Normal file
|
@ -0,0 +1,62 @@
|
|||
use crate::{method::Method, util::CollectToResult};
|
||||
use proc_macro2::{Ident, TokenStream};
|
||||
use quote::quote;
|
||||
use syn::{
|
||||
parenthesized,
|
||||
parse::{Parse, ParseStream},
|
||||
punctuated::Punctuated,
|
||||
DeriveInput,
|
||||
Error,
|
||||
Result,
|
||||
Token
|
||||
};
|
||||
use std::{iter, str::FromStr};
|
||||
|
||||
struct MethodList(Punctuated<Ident, Token![,]>);
|
||||
|
||||
impl Parse for MethodList
|
||||
{
|
||||
fn parse(input: ParseStream) -> Result<Self>
|
||||
{
|
||||
let content;
|
||||
let _paren = parenthesized!(content in input);
|
||||
let list = Punctuated::parse_separated_nonempty(&content)?;
|
||||
Ok(Self(list))
|
||||
}
|
||||
}
|
||||
|
||||
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 mod_ident = method.mod_ident(&name);
|
||||
let ident = method.setup_ident(&name);
|
||||
Ok(quote!(#mod_ident::#ident(&mut route);))
|
||||
})) as Box<dyn Iterator<Item = Result<TokenStream>>>,
|
||||
Err(err) => Box::new(iter::once(Err(err)))
|
||||
}).collect_to_result()?;
|
||||
|
||||
Ok(quote! {
|
||||
impl #krate::Resource for #ident
|
||||
{
|
||||
fn name() -> String
|
||||
{
|
||||
stringify!(#ident).to_string()
|
||||
}
|
||||
|
||||
fn setup<D : #krate::DrawResourceRoutes>(mut route : D)
|
||||
{
|
||||
#(#methods)*
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
289
derive/src/resource_error.rs
Normal file
289
derive/src/resource_error.rs
Normal file
|
@ -0,0 +1,289 @@
|
|||
use crate::util::{CollectToResult, remove_parens};
|
||||
use lazy_static::lazy_static;
|
||||
use proc_macro2::{Ident, TokenStream};
|
||||
use quote::{format_ident, quote};
|
||||
use regex::Regex;
|
||||
use std::iter;
|
||||
use syn::{
|
||||
spanned::Spanned,
|
||||
Attribute,
|
||||
Data,
|
||||
DeriveInput,
|
||||
Error,
|
||||
Fields,
|
||||
GenericParam,
|
||||
LitStr,
|
||||
Path,
|
||||
PathSegment,
|
||||
Result,
|
||||
Type,
|
||||
Variant
|
||||
};
|
||||
|
||||
|
||||
struct ErrorVariantField
|
||||
{
|
||||
attrs : Vec<Attribute>,
|
||||
ident : Ident,
|
||||
ty : Type
|
||||
}
|
||||
|
||||
struct ErrorVariant
|
||||
{
|
||||
ident : Ident,
|
||||
status : Option<Path>,
|
||||
is_named : bool,
|
||||
fields : Vec<ErrorVariantField>,
|
||||
from_ty : Option<(usize, Type)>,
|
||||
display : Option<LitStr>
|
||||
}
|
||||
|
||||
fn process_variant(variant : Variant) -> Result<ErrorVariant>
|
||||
{
|
||||
let status = match variant.attrs.iter()
|
||||
.find(|attr| attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("status".to_string()))
|
||||
{
|
||||
Some(attr) => Some(syn::parse2(remove_parens(attr.tokens.clone()))?),
|
||||
None => None
|
||||
};
|
||||
|
||||
let mut is_named = false;
|
||||
let mut fields = Vec::new();
|
||||
match variant.fields {
|
||||
Fields::Named(named) => {
|
||||
is_named = true;
|
||||
for field in named.named
|
||||
{
|
||||
let span = field.span();
|
||||
fields.push(ErrorVariantField {
|
||||
attrs: field.attrs,
|
||||
ident: field.ident.ok_or_else(|| Error::new(span, "Missing ident for this enum variant field"))?,
|
||||
ty: field.ty
|
||||
});
|
||||
}
|
||||
},
|
||||
Fields::Unnamed(unnamed) => {
|
||||
for (i, field) in unnamed.unnamed.into_iter().enumerate()
|
||||
{
|
||||
fields.push(ErrorVariantField {
|
||||
attrs: field.attrs,
|
||||
ident: format_ident!("arg{}", i),
|
||||
ty: field.ty
|
||||
})
|
||||
}
|
||||
},
|
||||
Fields::Unit => {}
|
||||
}
|
||||
|
||||
let from_ty = fields.iter()
|
||||
.enumerate()
|
||||
.find(|(_, field)| field.attrs.iter().any(|attr| attr.path.segments.last().map(|segment| segment.ident.to_string()) == Some("from".to_string())))
|
||||
.map(|(i, field)| (i, field.ty.clone()));
|
||||
|
||||
let display = match variant.attrs.iter()
|
||||
.find(|attr| attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("display".to_string()))
|
||||
{
|
||||
Some(attr) => Some(syn::parse2(remove_parens(attr.tokens.clone()))?),
|
||||
None => None
|
||||
};
|
||||
|
||||
Ok(ErrorVariant {
|
||||
ident: variant.ident,
|
||||
status,
|
||||
is_named,
|
||||
fields,
|
||||
from_ty,
|
||||
display
|
||||
})
|
||||
}
|
||||
|
||||
fn path_segment(name : &str) -> PathSegment
|
||||
{
|
||||
PathSegment {
|
||||
ident: format_ident!("{}", name),
|
||||
arguments: Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
// TODO this is a really ugly regex that requires at least two characters between captures
|
||||
static ref DISPLAY_REGEX : Regex = Regex::new(r"(^|[^\{])\{(?P<param>[^\}]+)\}([^\}]|$)").unwrap();
|
||||
}
|
||||
|
||||
impl ErrorVariant
|
||||
{
|
||||
fn fields_pat(&self) -> TokenStream
|
||||
{
|
||||
let mut fields = self.fields.iter().map(|field| &field.ident).peekable();
|
||||
if fields.peek().is_none() {
|
||||
quote!()
|
||||
} else if self.is_named {
|
||||
quote!( { #( #fields ),* } )
|
||||
} else {
|
||||
quote!( ( #( #fields ),* ) )
|
||||
}
|
||||
}
|
||||
|
||||
fn to_display_match_arm(&self, formatter_ident : &Ident, enum_ident : &Ident) -> Result<TokenStream>
|
||||
{
|
||||
let ident = &self.ident;
|
||||
let display = self.display.as_ref().ok_or_else(|| Error::new(self.ident.span(), "Missing display string for this variant"))?;
|
||||
|
||||
// lets find all required format parameters
|
||||
let display_str = display.value();
|
||||
let params = DISPLAY_REGEX.captures_iter(&display_str)
|
||||
.map(|cap| format_ident!("{}{}", if self.is_named { "" } else { "arg" }, cap.name("param").unwrap().as_str()));
|
||||
|
||||
let fields_pat = self.fields_pat();
|
||||
Ok(quote! {
|
||||
#enum_ident::#ident #fields_pat => write!(#formatter_ident, #display #(, #params = #params)*)
|
||||
})
|
||||
}
|
||||
|
||||
fn into_match_arm(self, krate : &TokenStream, enum_ident : &Ident) -> TokenStream
|
||||
{
|
||||
let ident = &self.ident;
|
||||
let fields_pat = self.fields_pat();
|
||||
let status = self.status.map(|status| {
|
||||
// the status might be relative to StatusCode, so let's fix that
|
||||
if status.leading_colon.is_none() && status.segments.len() < 2
|
||||
{
|
||||
let status_ident = status.segments.first().cloned().unwrap_or_else(|| path_segment("OK"));
|
||||
Path {
|
||||
leading_colon: Some(Default::default()),
|
||||
segments: vec![path_segment("gotham_restful"), path_segment("gotham"), path_segment("hyper"), path_segment("StatusCode"), status_ident].into_iter().collect()
|
||||
}
|
||||
}
|
||||
else { status }
|
||||
});
|
||||
|
||||
// the response will come directly from the from_ty if present
|
||||
let res = match self.from_ty {
|
||||
Some((from_index, _)) => {
|
||||
let from_field = &self.fields[from_index].ident;
|
||||
quote!(#from_field.into_response_error())
|
||||
},
|
||||
None => quote!(Ok(#krate::Response {
|
||||
status: { #status }.into(),
|
||||
body: #krate::gotham::hyper::Body::empty(),
|
||||
mime: None
|
||||
}))
|
||||
};
|
||||
|
||||
quote! {
|
||||
#enum_ident::#ident #fields_pat => #res
|
||||
}
|
||||
}
|
||||
|
||||
fn were(&self) -> Option<TokenStream>
|
||||
{
|
||||
match self.from_ty.as_ref() {
|
||||
Some((_, ty)) => Some(quote!( #ty : ::std::error::Error )),
|
||||
None => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expand_resource_error(input : DeriveInput) -> Result<TokenStream>
|
||||
{
|
||||
let krate = super::krate();
|
||||
let ident = input.ident;
|
||||
let generics = input.generics;
|
||||
|
||||
let inum = match input.data {
|
||||
Data::Enum(inum) => Ok(inum),
|
||||
Data::Struct(strukt) => Err(strukt.struct_token.span()),
|
||||
Data::Union(uni) => Err(uni.union_token.span())
|
||||
}.map_err(|span| Error::new(span, "#[derive(ResourceError)] only works for enums"))?;
|
||||
let variants = inum.variants.into_iter()
|
||||
.map(process_variant)
|
||||
.collect_to_result()?;
|
||||
|
||||
let display_impl = if variants.iter().any(|v| v.display.is_none()) { None } else {
|
||||
let were = generics.params.iter().filter_map(|param| match param {
|
||||
GenericParam::Type(ty) => {
|
||||
let ident = &ty.ident;
|
||||
Some(quote!(#ident : ::std::fmt::Display))
|
||||
},
|
||||
_ => None
|
||||
});
|
||||
let formatter_ident = format_ident!("resource_error_display_formatter");
|
||||
let match_arms = variants.iter()
|
||||
.map(|v| v.to_display_match_arm(&formatter_ident, &ident))
|
||||
.collect_to_result()?;
|
||||
Some(quote! {
|
||||
impl #generics ::std::fmt::Display for #ident #generics
|
||||
where #( #were ),*
|
||||
{
|
||||
fn fmt(&self, #formatter_ident: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result
|
||||
{
|
||||
match self {
|
||||
#( #match_arms ),*
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let mut from_impls : Vec<TokenStream> = Vec::new();
|
||||
|
||||
for var in &variants
|
||||
{
|
||||
let var_ident = &var.ident;
|
||||
let (from_index, from_ty) = match var.from_ty.as_ref() {
|
||||
Some(f) => f,
|
||||
None => continue
|
||||
};
|
||||
let from_ident = &var.fields[*from_index].ident;
|
||||
|
||||
let fields_pat = var.fields_pat();
|
||||
let fields_where = var.fields.iter().enumerate()
|
||||
.filter(|(i, _)| i != from_index)
|
||||
.map(|(_, field)| {
|
||||
let ty = &field.ty;
|
||||
quote!( #ty : Default )
|
||||
})
|
||||
.chain(iter::once(quote!( #from_ty : ::std::error::Error )));
|
||||
let fields_let = var.fields.iter().enumerate()
|
||||
.filter(|(i, _)| i != from_index)
|
||||
.map(|(_, field)| {
|
||||
let id = &field.ident;
|
||||
let ty = &field.ty;
|
||||
quote!( let #id : #ty = Default::default(); )
|
||||
});
|
||||
|
||||
from_impls.push(quote! {
|
||||
impl #generics ::std::convert::From<#from_ty> for #ident #generics
|
||||
where #( #fields_where ),*
|
||||
{
|
||||
fn from(#from_ident : #from_ty) -> Self
|
||||
{
|
||||
#( #fields_let )*
|
||||
Self::#var_ident #fields_pat
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let were = variants.iter().filter_map(|variant| variant.were()).collect::<Vec<_>>();
|
||||
let variants = variants.into_iter().map(|variant| variant.into_match_arm(&krate, &ident));
|
||||
|
||||
Ok(quote! {
|
||||
#display_impl
|
||||
|
||||
impl #generics #krate::IntoResponseError for #ident #generics
|
||||
where #( #were ),*
|
||||
{
|
||||
type Err = #krate::export::serde_json::Error;
|
||||
|
||||
fn into_response_error(self) -> Result<#krate::Response, Self::Err>
|
||||
{
|
||||
match self {
|
||||
#( #variants ),*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#( #from_impls )*
|
||||
})
|
||||
}
|
45
derive/src/util.rs
Normal file
45
derive/src/util.rs
Normal file
|
@ -0,0 +1,45 @@
|
|||
use proc_macro2::{Delimiter, TokenStream, TokenTree};
|
||||
use std::iter;
|
||||
use syn::Error;
|
||||
|
||||
pub trait CollectToResult
|
||||
{
|
||||
type Item;
|
||||
|
||||
fn collect_to_result(self) -> Result<Vec<Self::Item>, Error>;
|
||||
}
|
||||
|
||||
impl<Item, I> CollectToResult for I
|
||||
where
|
||||
I : Iterator<Item = Result<Item, Error>>
|
||||
{
|
||||
type Item = Item;
|
||||
|
||||
fn collect_to_result(self) -> Result<Vec<Item>, Error>
|
||||
{
|
||||
self.fold(<Result<Vec<Item>, Error>>::Ok(Vec::new()), |res, code| {
|
||||
match (code, res) {
|
||||
(Ok(code), Ok(mut codes)) => { codes.push(code); Ok(codes) },
|
||||
(Ok(_), Err(errors)) => Err(errors),
|
||||
(Err(err), Ok(_)) => Err(err),
|
||||
(Err(err), Err(mut errors)) => { errors.combine(err); Err(errors) }
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn remove_parens(input : TokenStream) -> TokenStream
|
||||
{
|
||||
let iter = input.into_iter().flat_map(|tt| {
|
||||
if let TokenTree::Group(group) = &tt
|
||||
{
|
||||
if group.delimiter() == Delimiter::Parenthesis
|
||||
{
|
||||
return Box::new(group.stream().into_iter()) as Box<dyn Iterator<Item = TokenTree>>;
|
||||
}
|
||||
}
|
||||
Box::new(iter::once(tt))
|
||||
});
|
||||
iter.collect()
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue