1
0
Fork 0
mirror of https://gitlab.com/msrd0/gotham-restful.git synced 2025-02-23 04:52:28 +00:00

update to gotham 0.5 and start using rustfmt

This commit is contained in:
Dominic 2020-09-15 15:10:41 +02:00
parent 5317e50961
commit d55b0897e9
Signed by: msrd0
GPG key ID: DCC8C247452E98F9
39 changed files with 1798 additions and 2095 deletions

View file

@ -20,12 +20,12 @@ gitlab = { repository = "msrd0/gotham-restful", branch = "master" }
[dependencies] [dependencies]
base64 = { version = "0.12.1", optional = true } base64 = { version = "0.12.1", optional = true }
chrono = { version = "0.4.11", features = ["serde"], optional = true } chrono = { version = "0.4.11", features = ["serde"], optional = true }
cookie = { version = "0.13.3", optional = true } cookie = { version = "0.14", optional = true }
futures-core = "0.3.5" futures-core = "0.3.5"
futures-util = "0.3.5" futures-util = "0.3.5"
gotham = { version = "0.5.0-rc.1", default-features = false } gotham = { version = "0.5.0", default-features = false }
gotham_derive = "0.5.0-rc.1" gotham_derive = "0.5.0"
gotham_middleware_diesel = { version = "0.1.2", optional = true } gotham_middleware_diesel = { version = "0.2.0", optional = true }
gotham_restful_derive = { version = "0.1.0-rc0" } gotham_restful_derive = { version = "0.1.0-rc0" }
indexmap = { version = "1.3.2", optional = true } indexmap = { version = "1.3.2", optional = true }
itertools = "0.9.0" itertools = "0.9.0"
@ -40,7 +40,7 @@ uuid = { version = "0.8.1", optional = true }
[dev-dependencies] [dev-dependencies]
diesel = { version = "1.4.4", features = ["postgres"] } diesel = { version = "1.4.4", features = ["postgres"] }
futures-executor = "0.3.5" futures-executor = "0.3.5"
paste = "0.1.12" paste = "1.0"
thiserror = "1.0.18" thiserror = "1.0.18"
trybuild = "1.0.27" trybuild = "1.0.27"
@ -56,5 +56,6 @@ openapi = ["gotham_restful_derive/openapi", "indexmap", "openapiv3"]
all-features = true all-features = true
[patch.crates-io] [patch.crates-io]
gotham = { path = "../gotham/gotham" }
gotham_restful = { path = "." } gotham_restful = { path = "." }
gotham_restful_derive = { path = "./derive" } gotham_restful_derive = { path = "./derive" }

View file

@ -1,50 +1,42 @@
use proc_macro2::TokenStream; use proc_macro2::TokenStream;
use quote::{format_ident, quote}; use quote::{format_ident, quote};
use std::cmp::min; use std::cmp::min;
use syn::{ use syn::{spanned::Spanned, Data, DeriveInput, Error, Field, Fields, Ident, Result, Type};
spanned::Spanned,
Data,
DeriveInput,
Error,
Field,
Fields,
Ident,
Result,
Type
};
struct ParsedFields struct ParsedFields {
{ fields: Vec<(Ident, Type)>,
fields : Vec<(Ident, Type)>, named: bool
named : bool
} }
impl ParsedFields impl ParsedFields {
{ fn from_named<I>(fields: I) -> Self
fn from_named<I>(fields : I) -> Self
where where
I : Iterator<Item = Field> I: Iterator<Item = Field>
{ {
let fields = fields.map(|field| (field.ident.unwrap(), field.ty)).collect(); let fields = fields.map(|field| (field.ident.unwrap(), field.ty)).collect();
Self { fields, named: true } Self { fields, named: true }
} }
fn from_unnamed<I>(fields : I) -> Self fn from_unnamed<I>(fields: I) -> Self
where where
I : Iterator<Item = Field> I: Iterator<Item = Field>
{ {
let fields = fields.enumerate().map(|(i, field)| (format_ident!("arg{}", i), field.ty)).collect(); let fields = fields
.enumerate()
.map(|(i, field)| (format_ident!("arg{}", i), field.ty))
.collect();
Self { fields, named: false } Self { fields, named: false }
} }
fn from_unit() -> Self fn from_unit() -> Self {
{ Self {
Self { fields: Vec::new(), named: false } fields: Vec::new(),
named: false
}
} }
} }
pub fn expand_from_body(input : DeriveInput) -> Result<TokenStream> pub fn expand_from_body(input: DeriveInput) -> Result<TokenStream> {
{
let krate = super::krate(); let krate = super::krate();
let ident = input.ident; let ident = input.ident;
let generics = input.generics; let generics = input.generics;
@ -53,7 +45,8 @@ pub fn expand_from_body(input : DeriveInput) -> Result<TokenStream>
Data::Enum(inum) => Err(inum.enum_token.span()), Data::Enum(inum) => Err(inum.enum_token.span()),
Data::Struct(strukt) => Ok(strukt), Data::Struct(strukt) => Ok(strukt),
Data::Union(uni) => Err(uni.union_token.span()) Data::Union(uni) => Err(uni.union_token.span())
}.map_err(|span| Error::new(span, "#[derive(FromBody)] only works for structs"))?; }
.map_err(|span| Error::new(span, "#[derive(FromBody)] only works for structs"))?;
let fields = match strukt.fields { let fields = match strukt.fields {
Fields::Named(named) => ParsedFields::from_named(named.named.into_iter()), Fields::Named(named) => ParsedFields::from_named(named.named.into_iter()),
@ -66,8 +59,7 @@ pub fn expand_from_body(input : DeriveInput) -> Result<TokenStream>
let mut body_ident = format_ident!("_body"); let mut body_ident = format_ident!("_body");
let mut type_ident = format_ident!("_type"); let mut type_ident = format_ident!("_type");
if let Some(body_field) = fields.fields.get(0) if let Some(body_field) = fields.fields.get(0) {
{
body_ident = body_field.0.clone(); body_ident = body_field.0.clone();
let body_ty = &body_field.1; let body_ty = &body_field.1;
where_clause = quote! { where_clause = quote! {
@ -81,8 +73,7 @@ pub fn expand_from_body(input : DeriveInput) -> Result<TokenStream>
}; };
} }
if let Some(type_field) = fields.fields.get(1) if let Some(type_field) = fields.fields.get(1) {
{
type_ident = type_field.0.clone(); type_ident = type_field.0.clone();
let type_ty = &type_field.1; let type_ty = &type_field.1;
where_clause = quote! { where_clause = quote! {
@ -95,8 +86,7 @@ pub fn expand_from_body(input : DeriveInput) -> Result<TokenStream>
}; };
} }
for field in &fields.fields[min(2, fields.fields.len())..] for field in &fields.fields[min(2, fields.fields.len())..] {
{
let field_ident = &field.0; let field_ident = &field.0;
let field_ty = &field.1; let field_ty = &field.1;
where_clause = quote! { where_clause = quote! {
@ -109,7 +99,7 @@ pub fn expand_from_body(input : DeriveInput) -> Result<TokenStream>
}; };
} }
let field_names : Vec<&Ident> = fields.fields.iter().map(|field| &field.0).collect(); let field_names: Vec<&Ident> = fields.fields.iter().map(|field| &field.0).collect();
let ctor = if fields.named { let ctor = if fields.named {
quote!(Self { #(#field_names),* }) quote!(Self { #(#field_names),* })
} else { } else {

View file

@ -21,114 +21,96 @@ mod openapi_type;
use openapi_type::expand_openapi_type; use openapi_type::expand_openapi_type;
#[inline] #[inline]
fn print_tokens(tokens : TokenStream2) -> TokenStream fn print_tokens(tokens: TokenStream2) -> TokenStream {
{
//eprintln!("{}", tokens); //eprintln!("{}", tokens);
tokens.into() tokens.into()
} }
#[inline] #[inline]
fn expand_derive<F>(input : TokenStream, expand : F) -> TokenStream fn expand_derive<F>(input: TokenStream, expand: F) -> TokenStream
where where
F : FnOnce(DeriveInput) -> Result<TokenStream2> F: FnOnce(DeriveInput) -> Result<TokenStream2>
{ {
print_tokens(expand(parse_macro_input!(input)) print_tokens(expand(parse_macro_input!(input)).unwrap_or_else(|err| err.to_compile_error()))
.unwrap_or_else(|err| err.to_compile_error()))
} }
#[inline] #[inline]
fn expand_macro<F, A, I>(attrs : TokenStream, item : TokenStream, expand : F) -> TokenStream fn expand_macro<F, A, I>(attrs: TokenStream, item: TokenStream, expand: F) -> TokenStream
where where
F : FnOnce(A, I) -> Result<TokenStream2>, F: FnOnce(A, I) -> Result<TokenStream2>,
A : ParseMacroInput, A: ParseMacroInput,
I : ParseMacroInput I: ParseMacroInput
{ {
print_tokens(expand(parse_macro_input!(attrs), parse_macro_input!(item)) print_tokens(expand(parse_macro_input!(attrs), parse_macro_input!(item)).unwrap_or_else(|err| err.to_compile_error()))
.unwrap_or_else(|err| err.to_compile_error()))
} }
#[inline] #[inline]
fn krate() -> TokenStream2 fn krate() -> TokenStream2 {
{
quote!(::gotham_restful) quote!(::gotham_restful)
} }
#[proc_macro_derive(FromBody)] #[proc_macro_derive(FromBody)]
pub fn derive_from_body(input : TokenStream) -> TokenStream pub fn derive_from_body(input: TokenStream) -> TokenStream {
{
expand_derive(input, expand_from_body) expand_derive(input, expand_from_body)
} }
#[cfg(feature = "openapi")] #[cfg(feature = "openapi")]
#[proc_macro_derive(OpenapiType, attributes(openapi))] #[proc_macro_derive(OpenapiType, attributes(openapi))]
pub fn derive_openapi_type(input : TokenStream) -> TokenStream pub fn derive_openapi_type(input: TokenStream) -> TokenStream {
{
expand_derive(input, expand_openapi_type) expand_derive(input, expand_openapi_type)
} }
#[proc_macro_derive(RequestBody, attributes(supported_types))] #[proc_macro_derive(RequestBody, attributes(supported_types))]
pub fn derive_request_body(input : TokenStream) -> TokenStream pub fn derive_request_body(input: TokenStream) -> TokenStream {
{
expand_derive(input, expand_request_body) expand_derive(input, expand_request_body)
} }
#[proc_macro_derive(Resource, attributes(resource))] #[proc_macro_derive(Resource, attributes(resource))]
pub fn derive_resource(input : TokenStream) -> TokenStream pub fn derive_resource(input: TokenStream) -> TokenStream {
{
expand_derive(input, expand_resource) expand_derive(input, expand_resource)
} }
#[proc_macro_derive(ResourceError, attributes(display, from, status))] #[proc_macro_derive(ResourceError, attributes(display, from, status))]
pub fn derive_resource_error(input : TokenStream) -> TokenStream pub fn derive_resource_error(input: TokenStream) -> TokenStream {
{
expand_derive(input, expand_resource_error) expand_derive(input, expand_resource_error)
} }
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn read_all(attr : TokenStream, item : TokenStream) -> TokenStream 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_method(Method::ReadAll, attr, item))
} }
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn read(attr : TokenStream, item : TokenStream) -> TokenStream 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_method(Method::Read, attr, item))
} }
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn search(attr : TokenStream, item : TokenStream) -> TokenStream 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_method(Method::Search, attr, item))
} }
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn create(attr : TokenStream, item : TokenStream) -> TokenStream 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_method(Method::Create, attr, item))
} }
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn change_all(attr : TokenStream, item : TokenStream) -> TokenStream 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_method(Method::ChangeAll, attr, item))
} }
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn change(attr : TokenStream, item : TokenStream) -> TokenStream 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_method(Method::Change, attr, item))
} }
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn remove_all(attr : TokenStream, item : TokenStream) -> TokenStream 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_method(Method::RemoveAll, attr, item))
} }
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn remove(attr : TokenStream, item : TokenStream) -> TokenStream 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_method(Method::Remove, attr, item))
} }

View file

@ -2,26 +2,13 @@ use crate::util::CollectToResult;
use heck::{CamelCase, SnakeCase}; use heck::{CamelCase, SnakeCase};
use proc_macro2::{Ident, Span, TokenStream}; use proc_macro2::{Ident, Span, TokenStream};
use quote::{format_ident, quote}; 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; use std::str::FromStr;
use syn::{
spanned::Spanned, Attribute, AttributeArgs, Error, FnArg, ItemFn, Lit, LitBool, Meta, NestedMeta, PatType, Result,
ReturnType, Type
};
pub enum Method pub enum Method {
{
ReadAll, ReadAll,
Read, Read,
Search, Search,
@ -32,12 +19,10 @@ pub enum Method
Remove Remove
} }
impl FromStr for Method impl FromStr for Method {
{
type Err = Error; type Err = Error;
fn from_str(str : &str) -> Result<Self> fn from_str(str: &str) -> Result<Self> {
{
match str { match str {
"ReadAll" | "read_all" => Ok(Self::ReadAll), "ReadAll" | "read_all" => Ok(Self::ReadAll),
"Read" | "read" => Ok(Self::Read), "Read" | "read" => Ok(Self::Read),
@ -52,10 +37,8 @@ impl FromStr for Method
} }
} }
impl Method impl Method {
{ pub fn type_names(&self) -> Vec<&'static str> {
pub fn type_names(&self) -> Vec<&'static str>
{
use Method::*; use Method::*;
match self { match self {
@ -67,8 +50,7 @@ impl Method
} }
} }
pub fn trait_ident(&self) -> Ident pub fn trait_ident(&self) -> Ident {
{
use Method::*; use Method::*;
let name = match self { let name = match self {
@ -84,8 +66,7 @@ impl Method
format_ident!("Resource{}", name) format_ident!("Resource{}", name)
} }
pub fn fn_ident(&self) -> Ident pub fn fn_ident(&self) -> Ident {
{
use Method::*; use Method::*;
let name = match self { let name = match self {
@ -101,25 +82,25 @@ impl Method
format_ident!("{}", name) format_ident!("{}", name)
} }
pub fn mod_ident(&self, resource : &str) -> Ident pub fn mod_ident(&self, resource: &str) -> Ident {
{ format_ident!(
format_ident!("_gotham_restful_resource_{}_method_{}", resource.to_snake_case(), self.fn_ident()) "_gotham_restful_resource_{}_method_{}",
resource.to_snake_case(),
self.fn_ident()
)
} }
pub fn handler_struct_ident(&self, resource : &str) -> Ident pub fn handler_struct_ident(&self, resource: &str) -> Ident {
{
format_ident!("{}{}Handler", resource.to_camel_case(), self.trait_ident()) format_ident!("{}{}Handler", resource.to_camel_case(), self.trait_ident())
} }
pub fn setup_ident(&self, resource : &str) -> Ident pub fn setup_ident(&self, resource: &str) -> Ident {
{
format_ident!("{}_{}_setup_impl", resource.to_snake_case(), self.fn_ident()) format_ident!("{}_{}_setup_impl", resource.to_snake_case(), self.fn_ident())
} }
} }
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
enum MethodArgumentType enum MethodArgumentType {
{
StateRef, StateRef,
StateMutRef, StateMutRef,
MethodArg(Type), MethodArg(Type),
@ -128,81 +109,77 @@ enum MethodArgumentType
AuthStatusRef(Type) AuthStatusRef(Type)
} }
impl MethodArgumentType impl MethodArgumentType {
{ fn is_state_ref(&self) -> bool {
fn is_state_ref(&self) -> bool
{
matches!(self, Self::StateRef | Self::StateMutRef) matches!(self, Self::StateRef | Self::StateMutRef)
} }
fn is_method_arg(&self) -> bool fn is_method_arg(&self) -> bool {
{
matches!(self, Self::MethodArg(_)) matches!(self, Self::MethodArg(_))
} }
fn is_database_conn(&self) -> bool fn is_database_conn(&self) -> bool {
{
matches!(self, Self::DatabaseConnection(_)) matches!(self, Self::DatabaseConnection(_))
} }
fn is_auth_status(&self) -> bool fn is_auth_status(&self) -> bool {
{
matches!(self, Self::AuthStatus(_) | Self::AuthStatusRef(_)) matches!(self, Self::AuthStatus(_) | Self::AuthStatusRef(_))
} }
fn ty(&self) -> Option<&Type> fn ty(&self) -> Option<&Type> {
{
match self { match self {
Self::MethodArg(ty) | Self::DatabaseConnection(ty) | Self::AuthStatus(ty) | Self::AuthStatusRef(ty) => Some(ty), Self::MethodArg(ty) | Self::DatabaseConnection(ty) | Self::AuthStatus(ty) | Self::AuthStatusRef(ty) => Some(ty),
_ => None _ => None
} }
} }
fn quote_ty(&self) -> Option<TokenStream> fn quote_ty(&self) -> Option<TokenStream> {
{
self.ty().map(|ty| quote!(#ty)) self.ty().map(|ty| quote!(#ty))
} }
} }
struct MethodArgument struct MethodArgument {
{ ident: Ident,
ident : Ident, ident_span: Span,
ident_span : Span, ty: MethodArgumentType
ty : MethodArgumentType
} }
impl Spanned for MethodArgument impl Spanned for MethodArgument {
{ fn span(&self) -> Span {
fn span(&self) -> Span
{
self.ident_span self.ident_span
} }
} }
fn interpret_arg_ty(attrs : &[Attribute], name : &str, ty : Type) -> Result<MethodArgumentType> fn interpret_arg_ty(attrs: &[Attribute], name: &str, ty: Type) -> Result<MethodArgumentType> {
{ let attr = attrs
let attr = attrs.iter() .iter()
.find(|arg| arg.path.segments.iter().any(|path| &path.ident.to_string() == "rest_arg")) .find(|arg| arg.path.segments.iter().any(|path| &path.ident.to_string() == "rest_arg"))
.map(|arg| arg.tokens.to_string()); .map(|arg| arg.tokens.to_string());
// TODO issue a warning for _state usage once diagnostics become stable // TODO issue a warning for _state usage once diagnostics become stable
if attr.as_deref() == Some("state") || (attr.is_none() && (name == "state" || name == "_state")) if attr.as_deref() == Some("state") || (attr.is_none() && (name == "state" || name == "_state")) {
{
return match ty { return match ty {
Type::Reference(ty) => Ok(if ty.mutability.is_none() { MethodArgumentType::StateRef } else { MethodArgumentType::StateMutRef }), Type::Reference(ty) => Ok(if ty.mutability.is_none() {
_ => Err(Error::new(ty.span(), "The state parameter has to be a (mutable) reference to gotham_restful::State")) 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")) if cfg!(feature = "auth") && (attr.as_deref() == Some("auth") || (attr.is_none() && name == "auth")) {
{
return Ok(match ty { return Ok(match ty {
Type::Reference(ty) => MethodArgumentType::AuthStatusRef(*ty.elem), Type::Reference(ty) => MethodArgumentType::AuthStatusRef(*ty.elem),
ty => MethodArgumentType::AuthStatus(ty) ty => MethodArgumentType::AuthStatus(ty)
}); });
} }
if cfg!(feature = "database") && (attr.as_deref() == Some("connection") || attr.as_deref() == Some("conn") || (attr.is_none() && name == "conn")) 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 { return Ok(MethodArgumentType::DatabaseConnection(match ty {
Type::Reference(ty) => *ty.elem, Type::Reference(ty) => *ty.elem,
@ -213,26 +190,25 @@ fn interpret_arg_ty(attrs : &[Attribute], name : &str, ty : Type) -> Result<Meth
Ok(MethodArgumentType::MethodArg(ty)) Ok(MethodArgumentType::MethodArg(ty))
} }
fn interpret_arg(index : usize, arg : &PatType) -> Result<MethodArgument> fn interpret_arg(index: usize, arg: &PatType) -> Result<MethodArgument> {
{
let pat = &arg.pat; let pat = &arg.pat;
let ident = format_ident!("arg{}", index); let ident = format_ident!("arg{}", index);
let orig_name = quote!(#pat); let orig_name = quote!(#pat);
let ty = interpret_arg_ty(&arg.attrs, &orig_name.to_string(), *arg.ty.clone())?; let ty = interpret_arg_ty(&arg.attrs, &orig_name.to_string(), *arg.ty.clone())?;
Ok(MethodArgument { ident, ident_span: arg.pat.span(), ty }) Ok(MethodArgument {
ident,
ident_span: arg.pat.span(),
ty
})
} }
#[cfg(feature = "openapi")] #[cfg(feature = "openapi")]
fn expand_operation_id(attrs : &[NestedMeta]) -> TokenStream fn expand_operation_id(attrs: &[NestedMeta]) -> TokenStream {
{ let mut operation_id: Option<&Lit> = None;
let mut operation_id : Option<&Lit> = None; for meta in attrs {
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()) {
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) operation_id = Some(&kv.lit)
} }
} }
@ -250,21 +226,19 @@ fn expand_operation_id(attrs : &[NestedMeta]) -> TokenStream
} }
#[cfg(not(feature = "openapi"))] #[cfg(not(feature = "openapi"))]
fn expand_operation_id(_ : &[NestedMeta]) -> TokenStream fn expand_operation_id(_: &[NestedMeta]) -> TokenStream {
{
quote!() quote!()
} }
fn expand_wants_auth(attrs : &[NestedMeta], default : bool) -> TokenStream fn expand_wants_auth(attrs: &[NestedMeta], default: bool) -> TokenStream {
{ let default_lit = Lit::Bool(LitBool {
let default_lit = Lit::Bool(LitBool { value: default, span: Span::call_site() }); value: default,
span: Span::call_site()
});
let mut wants_auth = &default_lit; let mut wants_auth = &default_lit;
for meta in attrs for meta in attrs {
{ if let NestedMeta::Meta(Meta::NameValue(kv)) = meta {
if let NestedMeta::Meta(Meta::NameValue(kv)) = meta if kv.path.segments.last().map(|p| p.ident.to_string()) == Some("wants_auth".to_owned()) {
{
if kv.path.segments.last().map(|p| p.ident.to_string()) == Some("wants_auth".to_owned())
{
wants_auth = &kv.lit wants_auth = &kv.lit
} }
} }
@ -279,28 +253,36 @@ fn expand_wants_auth(attrs : &[NestedMeta], default : bool) -> TokenStream
} }
#[allow(clippy::comparison_chain)] #[allow(clippy::comparison_chain)]
pub fn expand_method(method : Method, mut attrs : AttributeArgs, fun : ItemFn) -> Result<TokenStream> pub fn expand_method(method: Method, mut attrs: AttributeArgs, fun: ItemFn) -> Result<TokenStream> {
{
let krate = super::krate(); let krate = super::krate();
// parse attributes // parse attributes
if attrs.len() < 1 if attrs.len() < 1 {
{ return Err(Error::new(
return Err(Error::new(Span::call_site(), "Missing Resource struct. Example: #[read_all(MyResource)]")); Span::call_site(),
"Missing Resource struct. Example: #[read_all(MyResource)]"
));
} }
let resource_path = match attrs.remove(0) { let resource_path = match attrs.remove(0) {
NestedMeta::Meta(Meta::Path(path)) => path, NestedMeta::Meta(Meta::Path(path)) => path,
p => return Err(Error::new(p.span(), "Expected name of the Resource struct this method belongs to")) 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()) let resource_name = resource_path
.ok_or_else(|| Error::new(resource_path.span(), "Resource name must not be empty"))?; .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_ident = &fun.sig.ident;
let fun_vis = &fun.vis; let fun_vis = &fun.vis;
let fun_is_async = fun.sig.asyncness.is_some(); let fun_is_async = fun.sig.asyncness.is_some();
if let Some(unsafety) = fun.sig.unsafety if let Some(unsafety) = fun.sig.unsafety {
{
return Err(Error::new(unsafety.span(), "Resource methods must not be unsafe")); return Err(Error::new(unsafety.span(), "Resource methods must not be unsafe"));
} }
@ -323,7 +305,10 @@ pub fn expand_method(method : Method, mut attrs : AttributeArgs, fun : ItemFn) -
let res_ident = format_ident!("res"); let res_ident = format_ident!("res");
// extract arguments into pattern, ident and type // extract arguments into pattern, ident and type
let args = fun.sig.inputs.iter() let args = fun
.sig
.inputs
.iter()
.enumerate() .enumerate()
.map(|(i, arg)| match arg { .map(|(i, arg)| match arg {
FnArg::Typed(arg) => interpret_arg(i, arg), FnArg::Typed(arg) => interpret_arg(i, arg),
@ -334,18 +319,14 @@ pub fn expand_method(method : Method, mut attrs : AttributeArgs, fun : ItemFn) -
// extract the generic parameters to use // extract the generic parameters to use
let ty_names = method.type_names(); let ty_names = method.type_names();
let ty_len = ty_names.len(); let ty_len = ty_names.len();
let generics_args : Vec<&MethodArgument> = args.iter() let generics_args: Vec<&MethodArgument> = args.iter().filter(|arg| (*arg).ty.is_method_arg()).collect();
.filter(|arg| (*arg).ty.is_method_arg()) if generics_args.len() > ty_len {
.collect();
if generics_args.len() > ty_len
{
return Err(Error::new(generics_args[ty_len].span(), "Too many arguments")); return Err(Error::new(generics_args[ty_len].span(), "Too many arguments"));
} } else if generics_args.len() < ty_len {
else if generics_args.len() < ty_len
{
return Err(Error::new(fun_ident.span(), "Too few arguments")); return Err(Error::new(fun_ident.span(), "Too few arguments"));
} }
let generics : Vec<TokenStream> = generics_args.iter() let generics: Vec<TokenStream> = generics_args
.iter()
.map(|arg| arg.ty.quote_ty().unwrap()) .map(|arg| arg.ty.quote_ty().unwrap())
.zip(ty_names) .zip(ty_names)
.map(|(arg, name)| { .map(|(arg, name)| {
@ -355,45 +336,51 @@ pub fn expand_method(method : Method, mut attrs : AttributeArgs, fun : ItemFn) -
.collect(); .collect();
// extract the definition of our method // extract the definition of our method
let mut args_def : Vec<TokenStream> = args.iter() let mut args_def: Vec<TokenStream> = args
.iter()
.filter(|arg| (*arg).ty.is_method_arg()) .filter(|arg| (*arg).ty.is_method_arg())
.map(|arg| { .map(|arg| {
let ident = &arg.ident; let ident = &arg.ident;
let ty = arg.ty.quote_ty(); let ty = arg.ty.quote_ty();
quote!(#ident : #ty) quote!(#ident : #ty)
}).collect(); })
.collect();
args_def.insert(0, quote!(mut #state_ident : #krate::State)); args_def.insert(0, quote!(mut #state_ident : #krate::State));
// extract the arguments to pass over to the supplied method // extract the arguments to pass over to the supplied method
let args_pass : Vec<TokenStream> = args.iter().map(|arg| match (&arg.ty, &arg.ident) { let args_pass: Vec<TokenStream> = args
(MethodArgumentType::StateRef, _) => quote!(&#state_ident), .iter()
(MethodArgumentType::StateMutRef, _) => quote!(&mut #state_ident), .map(|arg| match (&arg.ty, &arg.ident) {
(MethodArgumentType::MethodArg(_), ident) => quote!(#ident), (MethodArgumentType::StateRef, _) => quote!(&#state_ident),
(MethodArgumentType::DatabaseConnection(_), _) => quote!(&#conn_ident), (MethodArgumentType::StateMutRef, _) => quote!(&mut #state_ident),
(MethodArgumentType::AuthStatus(_), _) => quote!(#auth_ident), (MethodArgumentType::MethodArg(_), ident) => quote!(#ident),
(MethodArgumentType::AuthStatusRef(_), _) => quote!(&#auth_ident) (MethodArgumentType::DatabaseConnection(_), _) => quote!(&#conn_ident),
}).collect(); (MethodArgumentType::AuthStatus(_), _) => quote!(#auth_ident),
(MethodArgumentType::AuthStatusRef(_), _) => quote!(&#auth_ident)
})
.collect();
// prepare the method block // prepare the method block
let mut block = quote!(#fun_ident(#(#args_pass),*)); let mut block = quote!(#fun_ident(#(#args_pass),*));
let mut state_block = quote!(); let mut state_block = quote!();
if fun_is_async if fun_is_async {
{ if let Some(arg) = args.iter().find(|arg| (*arg).ty.is_state_ref()) {
if let Some(arg) = args.iter().find(|arg| (*arg).ty.is_state_ref()) return Err(Error::new(
{ arg.span(),
return Err(Error::new(arg.span(), "async fn must not take &State as an argument as State is not Sync, consider boxing")); "async fn must not take &State as an argument as State is not Sync, consider boxing"
));
} }
block = quote!(#block.await); block = quote!(#block.await);
} }
if is_no_content if is_no_content {
{
block = quote!(#block; Default::default()) block = quote!(#block; Default::default())
} }
if let Some(arg) = args.iter().find(|arg| (*arg).ty.is_database_conn()) if let Some(arg) = args.iter().find(|arg| (*arg).ty.is_database_conn()) {
{ if fun_is_async {
if fun_is_async return Err(Error::new(
{ arg.span(),
return Err(Error::new(arg.span(), "async fn is not supported when database support is required, consider boxing")); "async fn is not supported when database support is required, consider boxing"
));
} }
let conn_ty = arg.ty.quote_ty(); let conn_ty = arg.ty.quote_ty();
state_block = quote! { state_block = quote! {
@ -411,8 +398,7 @@ pub fn expand_method(method : Method, mut attrs : AttributeArgs, fun : ItemFn) -
} }
}; };
} }
if let Some(arg) = args.iter().find(|arg| (*arg).ty.is_auth_status()) if let Some(arg) = args.iter().find(|arg| (*arg).ty.is_auth_status()) {
{
let auth_ty = arg.ty.quote_ty(); let auth_ty = arg.ty.quote_ty();
state_block = quote! { state_block = quote! {
#state_block #state_block
@ -422,8 +408,7 @@ pub fn expand_method(method : Method, mut attrs : AttributeArgs, fun : ItemFn) -
// prepare the where clause // prepare the where clause
let mut where_clause = quote!(#resource_path : #krate::Resource,); let mut where_clause = quote!(#resource_path : #krate::Resource,);
for arg in args.iter().filter(|arg| (*arg).ty.is_auth_status()) for arg in args.iter().filter(|arg| (*arg).ty.is_auth_status()) {
{
let auth_ty = arg.ty.quote_ty(); let auth_ty = arg.ty.quote_ty();
where_clause = quote!(#where_clause #auth_ty : Clone,); where_clause = quote!(#where_clause #auth_ty : Clone,);
} }

View file

@ -1,46 +1,31 @@
use crate::util::{CollectToResult, remove_parens}; use crate::util::{remove_parens, CollectToResult};
use proc_macro2::{Ident, TokenStream}; use proc_macro2::{Ident, TokenStream};
use quote::quote; use quote::quote;
use syn::{ use syn::{
parse_macro_input, parse_macro_input, spanned::Spanned, Attribute, AttributeArgs, Data, DataEnum, DataStruct, DeriveInput, Error, Field,
spanned::Spanned, Fields, GenericParam, Generics, Lit, LitStr, Meta, NestedMeta, Result, Variant
Attribute,
AttributeArgs,
Data,
DataEnum,
DataStruct,
DeriveInput,
Error,
Field,
Fields,
Generics,
GenericParam,
Lit,
LitStr,
Meta,
NestedMeta,
Result,
Variant
}; };
pub fn expand_openapi_type(input : DeriveInput) -> Result<TokenStream> pub fn expand_openapi_type(input: DeriveInput) -> Result<TokenStream> {
{
match (input.ident, input.generics, input.attrs, input.data) { 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::Enum(inum)) => expand_enum(ident, generics, attrs, inum),
(ident, generics, attrs, Data::Struct(strukt)) => expand_struct(ident, generics, attrs, strukt), (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")) (_, _, _, Data::Union(uni)) => Err(Error::new(
uni.union_token.span(),
"#[derive(OpenapiType)] only works for structs and enums"
))
} }
} }
fn expand_where(generics : &Generics) -> TokenStream fn expand_where(generics: &Generics) -> TokenStream {
{ if generics.params.is_empty() {
if generics.params.is_empty()
{
return quote!(); return quote!();
} }
let krate = super::krate(); let krate = super::krate();
let idents = generics.params.iter() let idents = generics
.params
.iter()
.map(|param| match param { .map(|param| match param {
GenericParam::Type(ty) => Some(ty.ident.clone()), GenericParam::Type(ty) => Some(ty.ident.clone()),
_ => None _ => None
@ -54,46 +39,39 @@ fn expand_where(generics : &Generics) -> TokenStream
} }
#[derive(Debug, Default)] #[derive(Debug, Default)]
struct Attrs struct Attrs {
{ nullable: bool,
nullable : bool, rename: Option<String>
rename : Option<String>
} }
fn to_string(lit : &Lit) -> Result<String> fn to_string(lit: &Lit) -> Result<String> {
{
match lit { match lit {
Lit::Str(str) => Ok(str.value()), Lit::Str(str) => Ok(str.value()),
_ => Err(Error::new(lit.span(), "Expected string literal")) _ => Err(Error::new(lit.span(), "Expected string literal"))
} }
} }
fn to_bool(lit : &Lit) -> Result<bool> fn to_bool(lit: &Lit) -> Result<bool> {
{
match lit { match lit {
Lit::Bool(bool) => Ok(bool.value), Lit::Bool(bool) => Ok(bool.value),
_ => Err(Error::new(lit.span(), "Expected bool")) _ => Err(Error::new(lit.span(), "Expected bool"))
} }
} }
fn parse_attributes(input : &[Attribute]) -> Result<Attrs> fn parse_attributes(input: &[Attribute]) -> Result<Attrs> {
{
let mut parsed = Attrs::default(); let mut parsed = Attrs::default();
for attr in input for attr in input {
{ if attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("openapi".to_owned()) {
if attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("openapi".to_owned())
{
let tokens = remove_parens(attr.tokens.clone()); 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 // 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())?; let nested = parse_macro_input::parse::<AttributeArgs>(tokens.into())?;
for meta in nested for meta in nested {
{
match &meta { match &meta {
NestedMeta::Meta(Meta::NameValue(kv)) => match kv.path.segments.last().map(|s| s.ident.to_string()) { NestedMeta::Meta(Meta::NameValue(kv)) => match kv.path.segments.last().map(|s| s.ident.to_string()) {
Some(key) => match key.as_ref() { Some(key) => match key.as_ref() {
"nullable" => parsed.nullable = to_bool(&kv.lit)?, "nullable" => parsed.nullable = to_bool(&kv.lit)?,
"rename" => parsed.rename = Some(to_string(&kv.lit)?), "rename" => parsed.rename = Some(to_string(&kv.lit)?),
_ => return Err(Error::new(kv.path.span(), "Unknown key")), _ => 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"))
}, },
@ -105,11 +83,12 @@ fn parse_attributes(input : &[Attribute]) -> Result<Attrs>
Ok(parsed) Ok(parsed)
} }
fn expand_variant(variant : &Variant) -> Result<TokenStream> fn expand_variant(variant: &Variant) -> Result<TokenStream> {
{ if variant.fields != Fields::Unit {
if variant.fields != Fields::Unit return Err(Error::new(
{ variant.span(),
return Err(Error::new(variant.span(), "#[derive(OpenapiType)] does not support enum variants with fields")); "#[derive(OpenapiType)] does not support enum variants with fields"
));
} }
let ident = &variant.ident; let ident = &variant.ident;
@ -125,8 +104,7 @@ fn expand_variant(variant : &Variant) -> Result<TokenStream>
}) })
} }
fn expand_enum(ident : Ident, generics : Generics, attrs : Vec<Attribute>, input : DataEnum) -> Result<TokenStream> fn expand_enum(ident: Ident, generics: Generics, attrs: Vec<Attribute>, input: DataEnum) -> Result<TokenStream> {
{
let krate = super::krate(); let krate = super::krate();
let where_clause = expand_where(&generics); let where_clause = expand_where(&generics);
@ -137,9 +115,7 @@ fn expand_enum(ident : Ident, generics : Generics, attrs : Vec<Attribute>, input
None => ident.to_string() None => ident.to_string()
}; };
let variants = input.variants.iter() let variants = input.variants.iter().map(expand_variant).collect_to_result()?;
.map(expand_variant)
.collect_to_result()?;
Ok(quote! { Ok(quote! {
impl #generics #krate::OpenapiType for #ident #generics impl #generics #krate::OpenapiType for #ident #generics
@ -170,11 +146,15 @@ fn expand_enum(ident : Ident, generics : Generics, attrs : Vec<Attribute>, input
}) })
} }
fn expand_field(field : &Field) -> Result<TokenStream> fn expand_field(field: &Field) -> Result<TokenStream> {
{
let ident = match &field.ident { let ident = match &field.ident {
Some(ident) => ident, Some(ident) => ident,
None => return Err(Error::new(field.span(), "#[derive(OpenapiType)] does not support fields without an ident")) None => {
return Err(Error::new(
field.span(),
"#[derive(OpenapiType)] does not support fields without an ident"
))
},
}; };
let ident_str = LitStr::new(&ident.to_string(), ident.span()); let ident_str = LitStr::new(&ident.to_string(), ident.span());
let ty = &field.ty; let ty = &field.ty;
@ -226,8 +206,7 @@ fn expand_field(field : &Field) -> Result<TokenStream>
}}) }})
} }
fn expand_struct(ident : Ident, generics : Generics, attrs : Vec<Attribute>, input : DataStruct) -> Result<TokenStream> fn expand_struct(ident: Ident, generics: Generics, attrs: Vec<Attribute>, input: DataStruct) -> Result<TokenStream> {
{
let krate = super::krate(); let krate = super::krate();
let where_clause = expand_where(&generics); let where_clause = expand_where(&generics);
@ -238,17 +217,18 @@ fn expand_struct(ident : Ident, generics : Generics, attrs : Vec<Attribute>, inp
None => ident.to_string() None => ident.to_string()
}; };
let fields : Vec<TokenStream> = match input.fields { let fields: Vec<TokenStream> = match input.fields {
Fields::Named(named_fields) => { Fields::Named(named_fields) => named_fields.named.iter().map(expand_field).collect_to_result()?,
named_fields.named.iter() Fields::Unnamed(fields) => {
.map(expand_field) return Err(Error::new(
.collect_to_result()? fields.span(),
"#[derive(OpenapiType)] does not support unnamed fields"
))
}, },
Fields::Unnamed(fields) => return Err(Error::new(fields.span(), "#[derive(OpenapiType)] does not support unnamed fields")),
Fields::Unit => Vec::new() Fields::Unit => Vec::new()
}; };
Ok(quote!{ Ok(quote! {
impl #generics #krate::OpenapiType for #ident #generics impl #generics #krate::OpenapiType for #ident #generics
#where_clause #where_clause
{ {

View file

@ -6,34 +6,25 @@ use syn::{
parse::{Parse, ParseStream}, parse::{Parse, ParseStream},
punctuated::Punctuated, punctuated::Punctuated,
spanned::Spanned, spanned::Spanned,
DeriveInput, DeriveInput, Error, Generics, Path, Result, Token
Error,
Generics,
Path,
Result,
Token
}; };
struct MimeList(Punctuated<Path, Token![,]>); struct MimeList(Punctuated<Path, Token![,]>);
impl Parse for MimeList impl Parse for MimeList {
{ fn parse(input: ParseStream) -> Result<Self> {
fn parse(input: ParseStream) -> Result<Self>
{
let list = Punctuated::parse_separated_nonempty(&input)?; let list = Punctuated::parse_separated_nonempty(&input)?;
Ok(Self(list)) Ok(Self(list))
} }
} }
#[cfg(not(feature = "openapi"))] #[cfg(not(feature = "openapi"))]
fn impl_openapi_type(_ident : &Ident, _generics : &Generics) -> TokenStream fn impl_openapi_type(_ident: &Ident, _generics: &Generics) -> TokenStream {
{
quote!() quote!()
} }
#[cfg(feature = "openapi")] #[cfg(feature = "openapi")]
fn impl_openapi_type(ident : &Ident, generics : &Generics) -> TokenStream fn impl_openapi_type(ident: &Ident, generics: &Generics) -> TokenStream {
{
let krate = super::krate(); let krate = super::krate();
quote! { quote! {
@ -52,20 +43,26 @@ fn impl_openapi_type(ident : &Ident, generics : &Generics) -> TokenStream
} }
} }
pub fn expand_request_body(input : DeriveInput) -> Result<TokenStream> pub fn expand_request_body(input: DeriveInput) -> Result<TokenStream> {
{
let krate = super::krate(); let krate = super::krate();
let ident = input.ident; let ident = input.ident;
let generics = input.generics; let generics = input.generics;
let types = input.attrs.into_iter() let types = input
.filter(|attr| attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("supported_types".to_string())) .attrs
.into_iter()
.filter(|attr| {
attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("supported_types".to_string())
})
.flat_map(|attr| { .flat_map(|attr| {
let span = attr.span(); let span = attr.span();
attr.parse_args::<MimeList>() attr.parse_args::<MimeList>()
.map(|list| Box::new(list.0.into_iter().map(Ok)) as Box<dyn Iterator<Item = Result<Path>>>) .map(|list| Box::new(list.0.into_iter().map(Ok)) as Box<dyn Iterator<Item = Result<Path>>>)
.unwrap_or_else(|mut err| { .unwrap_or_else(|mut err| {
err.combine(Error::new(span, "Hint: Types list should look like #[supported_types(TEXT_PLAIN, APPLICATION_JSON)]")); err.combine(Error::new(
span,
"Hint: Types list should look like #[supported_types(TEXT_PLAIN, APPLICATION_JSON)]"
));
Box::new(iter::once(Err(err))) Box::new(iter::once(Err(err)))
}) })
}) })

View file

@ -1,23 +1,18 @@
use crate::{method::Method, util::CollectToResult}; use crate::{method::Method, util::CollectToResult};
use proc_macro2::{Ident, TokenStream}; use proc_macro2::{Ident, TokenStream};
use quote::quote; use quote::quote;
use std::{iter, str::FromStr};
use syn::{ use syn::{
parenthesized, parenthesized,
parse::{Parse, ParseStream}, parse::{Parse, ParseStream},
punctuated::Punctuated, punctuated::Punctuated,
DeriveInput, DeriveInput, Error, Result, Token
Error,
Result,
Token
}; };
use std::{iter, str::FromStr};
struct MethodList(Punctuated<Ident, Token![,]>); struct MethodList(Punctuated<Ident, Token![,]>);
impl Parse for MethodList impl Parse for MethodList {
{ fn parse(input: ParseStream) -> Result<Self> {
fn parse(input: ParseStream) -> Result<Self>
{
let content; let content;
let _paren = parenthesized!(content in input); let _paren = parenthesized!(content in input);
let list = Punctuated::parse_separated_nonempty(&content)?; let list = Punctuated::parse_separated_nonempty(&content)?;
@ -25,25 +20,31 @@ impl Parse for MethodList
} }
} }
pub fn expand_resource(input : DeriveInput) -> Result<TokenStream> pub fn expand_resource(input: DeriveInput) -> Result<TokenStream> {
{
let krate = super::krate(); let krate = super::krate();
let ident = input.ident; let ident = input.ident;
let name = ident.to_string(); let name = ident.to_string();
let methods = input.attrs.into_iter().filter(|attr| let methods =
attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("resource".to_string()) // TODO wtf input
).map(|attr| { .attrs
syn::parse2(attr.tokens).map(|m : MethodList| m.0.into_iter()) .into_iter()
}).flat_map(|list| match list { .filter(
Ok(iter) => Box::new(iter.map(|method| { |attr| {
let method = Method::from_str(&method.to_string()).map_err(|err| Error::new(method.span(), err))?; attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("resource".to_string())
let mod_ident = method.mod_ident(&name); } // TODO wtf
let ident = method.setup_ident(&name); )
Ok(quote!(#mod_ident::#ident(&mut route);)) .map(|attr| syn::parse2(attr.tokens).map(|m: MethodList| m.0.into_iter()))
})) as Box<dyn Iterator<Item = Result<TokenStream>>>, .flat_map(|list| match list {
Err(err) => Box::new(iter::once(Err(err))) Ok(iter) => Box::new(iter.map(|method| {
}).collect_to_result()?; 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! { Ok(quote! {
impl #krate::Resource for #ident impl #krate::Resource for #ident

View file

@ -1,68 +1,54 @@
use crate::util::{CollectToResult, remove_parens}; use crate::util::{remove_parens, CollectToResult};
use proc_macro2::{Ident, TokenStream}; use proc_macro2::{Ident, TokenStream};
use quote::{format_ident, quote}; use quote::{format_ident, quote};
use std::iter; use std::iter;
use syn::{ use syn::{
spanned::Spanned, spanned::Spanned, Attribute, Data, DeriveInput, Error, Fields, GenericParam, LitStr, Path, PathSegment, Result, Type,
Attribute,
Data,
DeriveInput,
Error,
Fields,
GenericParam,
LitStr,
Path,
PathSegment,
Result,
Type,
Variant Variant
}; };
struct ErrorVariantField {
struct ErrorVariantField attrs: Vec<Attribute>,
{ ident: Ident,
attrs : Vec<Attribute>, ty: Type
ident : Ident,
ty : Type
} }
struct ErrorVariant struct ErrorVariant {
{ ident: Ident,
ident : Ident, status: Option<Path>,
status : Option<Path>, is_named: bool,
is_named : bool, fields: Vec<ErrorVariantField>,
fields : Vec<ErrorVariantField>, from_ty: Option<(usize, Type)>,
from_ty : Option<(usize, Type)>, display: Option<LitStr>
display : Option<LitStr>
} }
fn process_variant(variant : Variant) -> Result<ErrorVariant> fn process_variant(variant: Variant) -> Result<ErrorVariant> {
{ let status =
let status = match variant.attrs.iter() match variant.attrs.iter().find(|attr| {
.find(|attr| attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("status".to_string())) 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()))?), Some(attr) => Some(syn::parse2(remove_parens(attr.tokens.clone()))?),
None => None None => None
}; };
let mut is_named = false; let mut is_named = false;
let mut fields = Vec::new(); let mut fields = Vec::new();
match variant.fields { match variant.fields {
Fields::Named(named) => { Fields::Named(named) => {
is_named = true; is_named = true;
for field in named.named for field in named.named {
{
let span = field.span(); let span = field.span();
fields.push(ErrorVariantField { fields.push(ErrorVariantField {
attrs: field.attrs, attrs: field.attrs,
ident: field.ident.ok_or_else(|| Error::new(span, "Missing ident for this enum variant field"))?, ident: field
.ident
.ok_or_else(|| Error::new(span, "Missing ident for this enum variant field"))?,
ty: field.ty ty: field.ty
}); });
} }
}, },
Fields::Unnamed(unnamed) => { Fields::Unnamed(unnamed) => {
for (i, field) in unnamed.unnamed.into_iter().enumerate() for (i, field) in unnamed.unnamed.into_iter().enumerate() {
{
fields.push(ErrorVariantField { fields.push(ErrorVariantField {
attrs: field.attrs, attrs: field.attrs,
ident: format_ident!("arg{}", i), ident: format_ident!("arg{}", i),
@ -73,14 +59,20 @@ fn process_variant(variant : Variant) -> Result<ErrorVariant>
Fields::Unit => {} Fields::Unit => {}
} }
let from_ty = fields.iter() let from_ty = fields
.iter()
.enumerate() .enumerate()
.find(|(_, field)| field.attrs.iter().any(|attr| attr.path.segments.last().map(|segment| segment.ident.to_string()) == Some("from".to_string()))) .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())); .map(|(i, field)| (i, field.ty.clone()));
let display = match variant.attrs.iter() let display = match variant.attrs.iter().find(|attr| {
.find(|attr| attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("display".to_string())) 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()))?), Some(attr) => Some(syn::parse2(remove_parens(attr.tokens.clone()))?),
None => None None => None
}; };
@ -95,18 +87,15 @@ fn process_variant(variant : Variant) -> Result<ErrorVariant>
}) })
} }
fn path_segment(name : &str) -> PathSegment fn path_segment(name: &str) -> PathSegment {
{
PathSegment { PathSegment {
ident: format_ident!("{}", name), ident: format_ident!("{}", name),
arguments: Default::default() arguments: Default::default()
} }
} }
impl ErrorVariant impl ErrorVariant {
{ fn fields_pat(&self) -> TokenStream {
fn fields_pat(&self) -> TokenStream
{
let mut fields = self.fields.iter().map(|field| &field.ident).peekable(); let mut fields = self.fields.iter().map(|field| &field.ident).peekable();
if fields.peek().is_none() { if fields.peek().is_none() {
quote!() quote!()
@ -117,49 +106,58 @@ impl ErrorVariant
} }
} }
fn to_display_match_arm(&self, formatter_ident : &Ident, enum_ident : &Ident) -> Result<TokenStream> fn to_display_match_arm(&self, formatter_ident: &Ident, enum_ident: &Ident) -> Result<TokenStream> {
{
let ident = &self.ident; let ident = &self.ident;
let display = self.display.as_ref().ok_or_else(|| Error::new(self.ident.span(), "Missing display string for this variant"))?; 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 // lets find all required format parameters
let display_str = display.value(); let display_str = display.value();
let mut params : Vec<&str> = Vec::new(); let mut params: Vec<&str> = Vec::new();
let len = display_str.len(); let len = display_str.len();
let mut start = len; let mut start = len;
let mut iter = display_str.chars().enumerate().peekable(); let mut iter = display_str.chars().enumerate().peekable();
while let Some((i, c)) = iter.next() while let Some((i, c)) = iter.next() {
{
// we found a new opening brace // we found a new opening brace
if start == len && c == '{' if start == len && c == '{' {
{
start = i + 1; start = i + 1;
} }
// we found a duplicate opening brace // we found a duplicate opening brace
else if start == i && c == '{' else if start == i && c == '{' {
{
start = len; start = len;
} }
// we found a closing brace // we found a closing brace
else if start < i && c == '}' else if start < i && c == '}' {
{
match iter.peek() { match iter.peek() {
Some((_, '}')) => return Err(Error::new(display.span(), "Error parsing format string: curly braces not allowed inside parameter name")), Some((_, '}')) => {
return Err(Error::new(
display.span(),
"Error parsing format string: curly braces not allowed inside parameter name"
))
},
_ => params.push(&display_str[start..i]) _ => params.push(&display_str[start..i])
}; };
start = len; start = len;
} }
// we found a closing brace without content // we found a closing brace without content
else if start == i && c == '}' else if start == i && c == '}' {
{ return Err(Error::new(
return Err(Error::new(display.span(), "Error parsing format string: parameter name must not be empty")) display.span(),
"Error parsing format string: parameter name must not be empty"
));
} }
} }
if start != len if start != len {
{ return Err(Error::new(
return Err(Error::new(display.span(), "Error parsing format string: Unmatched opening brace")); display.span(),
"Error parsing format string: Unmatched opening brace"
));
} }
let params = params.into_iter().map(|name| format_ident!("{}{}", if self.is_named { "" } else { "arg" }, name)); let params = params
.into_iter()
.map(|name| format_ident!("{}{}", if self.is_named { "" } else { "arg" }, name));
let fields_pat = self.fields_pat(); let fields_pat = self.fields_pat();
Ok(quote! { Ok(quote! {
@ -167,21 +165,28 @@ impl ErrorVariant
}) })
} }
fn into_match_arm(self, krate : &TokenStream, enum_ident : &Ident) -> Result<TokenStream> fn into_match_arm(self, krate: &TokenStream, enum_ident: &Ident) -> Result<TokenStream> {
{
let ident = &self.ident; let ident = &self.ident;
let fields_pat = self.fields_pat(); let fields_pat = self.fields_pat();
let status = self.status.map(|status| { let status = self.status.map(|status| {
// the status might be relative to StatusCode, so let's fix that // the status might be relative to StatusCode, so let's fix that
if status.leading_colon.is_none() && status.segments.len() < 2 if status.leading_colon.is_none() && status.segments.len() < 2 {
{
let status_ident = status.segments.first().cloned().unwrap_or_else(|| path_segment("OK")); let status_ident = status.segments.first().cloned().unwrap_or_else(|| path_segment("OK"));
Path { Path {
leading_colon: Some(Default::default()), 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() segments: vec![
path_segment("gotham_restful"),
path_segment("gotham"),
path_segment("hyper"),
path_segment("StatusCode"),
status_ident,
]
.into_iter()
.collect()
} }
} else {
status
} }
else { status }
}); });
// the response will come directly from the from_ty if present // the response will come directly from the from_ty if present
@ -204,8 +209,7 @@ impl ErrorVariant
}) })
} }
fn were(&self) -> Option<TokenStream> fn were(&self) -> Option<TokenStream> {
{
match self.from_ty.as_ref() { match self.from_ty.as_ref() {
Some((_, ty)) => Some(quote!( #ty : ::std::error::Error )), Some((_, ty)) => Some(quote!( #ty : ::std::error::Error )),
None => None None => None
@ -213,8 +217,7 @@ impl ErrorVariant
} }
} }
pub fn expand_resource_error(input : DeriveInput) -> Result<TokenStream> pub fn expand_resource_error(input: DeriveInput) -> Result<TokenStream> {
{
let krate = super::krate(); let krate = super::krate();
let ident = input.ident; let ident = input.ident;
let generics = input.generics; let generics = input.generics;
@ -223,12 +226,13 @@ pub fn expand_resource_error(input : DeriveInput) -> Result<TokenStream>
Data::Enum(inum) => Ok(inum), Data::Enum(inum) => Ok(inum),
Data::Struct(strukt) => Err(strukt.struct_token.span()), Data::Struct(strukt) => Err(strukt.struct_token.span()),
Data::Union(uni) => Err(uni.union_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_err(|span| Error::new(span, "#[derive(ResourceError)] only works for enums"))?;
.map(process_variant) let variants = inum.variants.into_iter().map(process_variant).collect_to_result()?;
.collect_to_result()?;
let display_impl = if variants.iter().any(|v| v.display.is_none()) { None } else { let display_impl = if variants.iter().any(|v| v.display.is_none()) {
None
} else {
let were = generics.params.iter().filter_map(|param| match param { let were = generics.params.iter().filter_map(|param| match param {
GenericParam::Type(ty) => { GenericParam::Type(ty) => {
let ident = &ty.ident; let ident = &ty.ident;
@ -237,7 +241,8 @@ pub fn expand_resource_error(input : DeriveInput) -> Result<TokenStream>
_ => None _ => None
}); });
let formatter_ident = format_ident!("resource_error_display_formatter"); let formatter_ident = format_ident!("resource_error_display_formatter");
let match_arms = variants.iter() let match_arms = variants
.iter()
.map(|v| v.to_display_match_arm(&formatter_ident, &ident)) .map(|v| v.to_display_match_arm(&formatter_ident, &ident))
.collect_to_result()?; .collect_to_result()?;
Some(quote! { Some(quote! {
@ -254,10 +259,9 @@ pub fn expand_resource_error(input : DeriveInput) -> Result<TokenStream>
}) })
}; };
let mut from_impls : Vec<TokenStream> = Vec::new(); let mut from_impls: Vec<TokenStream> = Vec::new();
for var in &variants for var in &variants {
{
let var_ident = &var.ident; let var_ident = &var.ident;
let (from_index, from_ty) = match var.from_ty.as_ref() { let (from_index, from_ty) = match var.from_ty.as_ref() {
Some(f) => f, Some(f) => f,
@ -266,14 +270,20 @@ pub fn expand_resource_error(input : DeriveInput) -> Result<TokenStream>
let from_ident = &var.fields[*from_index].ident; let from_ident = &var.fields[*from_index].ident;
let fields_pat = var.fields_pat(); let fields_pat = var.fields_pat();
let fields_where = var.fields.iter().enumerate() let fields_where = var
.fields
.iter()
.enumerate()
.filter(|(i, _)| i != from_index) .filter(|(i, _)| i != from_index)
.map(|(_, field)| { .map(|(_, field)| {
let ty = &field.ty; let ty = &field.ty;
quote!( #ty : Default ) quote!( #ty : Default )
}) })
.chain(iter::once(quote!( #from_ty : ::std::error::Error ))); .chain(iter::once(quote!( #from_ty : ::std::error::Error )));
let fields_let = var.fields.iter().enumerate() let fields_let = var
.fields
.iter()
.enumerate()
.filter(|(i, _)| i != from_index) .filter(|(i, _)| i != from_index)
.map(|(_, field)| { .map(|(_, field)| {
let id = &field.ident; let id = &field.ident;
@ -295,9 +305,10 @@ pub fn expand_resource_error(input : DeriveInput) -> Result<TokenStream>
} }
let were = variants.iter().filter_map(|variant| variant.were()).collect::<Vec<_>>(); let were = variants.iter().filter_map(|variant| variant.were()).collect::<Vec<_>>();
let variants = variants.into_iter() let variants = variants
.into_iter()
.map(|variant| variant.into_match_arm(&krate, &ident)) .map(|variant| variant.into_match_arm(&krate, &ident))
.collect_to_result()?; .collect_to_result()?;
Ok(quote! { Ok(quote! {
#display_impl #display_impl

View file

@ -2,8 +2,7 @@ use proc_macro2::{Delimiter, TokenStream, TokenTree};
use std::iter; use std::iter;
use syn::Error; use syn::Error;
pub trait CollectToResult pub trait CollectToResult {
{
type Item; type Item;
fn collect_to_result(self) -> Result<Vec<Self::Item>, Error>; fn collect_to_result(self) -> Result<Vec<Self::Item>, Error>;
@ -11,31 +10,30 @@ pub trait CollectToResult
impl<Item, I> CollectToResult for I impl<Item, I> CollectToResult for I
where where
I : Iterator<Item = Result<Item, Error>> I: Iterator<Item = Result<Item, Error>>
{ {
type Item = Item; type Item = Item;
fn collect_to_result(self) -> Result<Vec<Item>, Error> fn collect_to_result(self) -> Result<Vec<Item>, Error> {
{ self.fold(<Result<Vec<Item>, Error>>::Ok(Vec::new()), |res, code| match (code, res) {
self.fold(<Result<Vec<Item>, Error>>::Ok(Vec::new()), |res, code| { (Ok(code), Ok(mut codes)) => {
match (code, res) { codes.push(code);
(Ok(code), Ok(mut codes)) => { codes.push(code); Ok(codes) }, Ok(codes)
(Ok(_), Err(errors)) => Err(errors), },
(Err(err), Ok(_)) => Err(err), (Ok(_), Err(errors)) => Err(errors),
(Err(err), Err(mut errors)) => { errors.combine(err); Err(errors) } (Err(err), Ok(_)) => Err(err),
} (Err(err), Err(mut errors)) => {
errors.combine(err);
Err(errors)
}
}) })
} }
} }
pub fn remove_parens(input: TokenStream) -> TokenStream {
pub fn remove_parens(input : TokenStream) -> TokenStream
{
let iter = input.into_iter().flat_map(|tt| { let iter = input.into_iter().flat_map(|tt| {
if let TokenTree::Group(group) = &tt if let TokenTree::Group(group) = &tt {
{ if group.delimiter() == Delimiter::Parenthesis {
if group.delimiter() == Delimiter::Parenthesis
{
return Box::new(group.stream().into_iter()) as Box<dyn Iterator<Item = TokenTree>>; return Box::new(group.stream().into_iter()) as Box<dyn Iterator<Item = TokenTree>>;
} }
} }

View file

@ -1,5 +1,7 @@
#[macro_use] extern crate gotham_derive; #[macro_use]
#[macro_use] extern crate log; extern crate gotham_derive;
#[macro_use]
extern crate log;
use fake::{faker::internet::en::Username, Fake}; use fake::{faker::internet::en::Username, Fake};
use gotham::{ use gotham::{
@ -20,25 +22,19 @@ use serde::{Deserialize, Serialize};
#[derive(Resource)] #[derive(Resource)]
#[resource(read_all, read, search, create, change_all, change, remove, remove_all)] #[resource(read_all, read, search, create, change_all, change, remove, remove_all)]
struct Users struct Users {}
{
}
#[derive(Resource)] #[derive(Resource)]
#[resource(ReadAll)] #[resource(ReadAll)]
struct Auth struct Auth {}
{
}
#[derive(Deserialize, OpenapiType, Serialize, StateData, StaticResponseExtender)] #[derive(Deserialize, OpenapiType, Serialize, StateData, StaticResponseExtender)]
struct User struct User {
{ username: String
username : String
} }
#[read_all(Users)] #[read_all(Users)]
fn read_all() -> Success<Vec<Option<User>>> fn read_all() -> Success<Vec<Option<User>>> {
{
vec![Username().fake(), Username().fake()] vec![Username().fake(), Username().fake()]
.into_iter() .into_iter()
.map(|username| Some(User { username })) .map(|username| Some(User { username }))
@ -47,80 +43,72 @@ fn read_all() -> Success<Vec<Option<User>>>
} }
#[read(Users)] #[read(Users)]
fn read(id : u64) -> Success<User> fn read(id: u64) -> Success<User> {
{ let username: String = Username().fake();
let username : String = Username().fake(); User {
User { username: format!("{}{}", username, id) }.into() username: format!("{}{}", username, id)
}
.into()
} }
#[search(Users)] #[search(Users)]
fn search(query : User) -> Success<User> fn search(query: User) -> Success<User> {
{
query.into() query.into()
} }
#[create(Users)] #[create(Users)]
fn create(body : User) fn create(body: User) {
{
info!("Created User: {}", body.username); info!("Created User: {}", body.username);
} }
#[change_all(Users)] #[change_all(Users)]
fn update_all(body : Vec<User>) fn update_all(body: Vec<User>) {
{ info!(
info!("Changing all Users to {:?}", body.into_iter().map(|u| u.username).collect::<Vec<String>>()); "Changing all Users to {:?}",
body.into_iter().map(|u| u.username).collect::<Vec<String>>()
);
} }
#[change(Users)] #[change(Users)]
fn update(id : u64, body : User) fn update(id: u64, body: User) {
{
info!("Change User {} to {}", id, body.username); info!("Change User {} to {}", id, body.username);
} }
#[remove_all(Users)] #[remove_all(Users)]
fn remove_all() fn remove_all() {
{
info!("Delete all Users"); info!("Delete all Users");
} }
#[remove(Users)] #[remove(Users)]
fn remove(id : u64) fn remove(id: u64) {
{
info!("Delete User {}", id); info!("Delete User {}", id);
} }
#[read_all(Auth)] #[read_all(Auth)]
fn auth_read_all(auth : AuthStatus<()>) -> AuthSuccess<String> fn auth_read_all(auth: AuthStatus<()>) -> AuthSuccess<String> {
{
match auth { match auth {
AuthStatus::Authenticated(data) => Ok(format!("{:?}", data)), AuthStatus::Authenticated(data) => Ok(format!("{:?}", data)),
_ => Err(Forbidden) _ => Err(Forbidden)
} }
} }
const ADDR : &str = "127.0.0.1:18080"; const ADDR: &str = "127.0.0.1:18080";
#[derive(Clone, Default)] #[derive(Clone, Default)]
struct Handler; struct Handler;
impl<T> AuthHandler<T> for Handler impl<T> AuthHandler<T> for Handler {
{ fn jwt_secret<F: FnOnce() -> Option<T>>(&self, _state: &mut State, _decode_data: F) -> Option<Vec<u8>> {
fn jwt_secret<F : FnOnce() -> Option<T>>(&self, _state : &mut State, _decode_data : F) -> Option<Vec<u8>>
{
None None
} }
} }
fn main() fn main() {
{
let encoder = PatternEncoder::new("{d(%Y-%m-%d %H:%M:%S%.3f %Z)} [{l}] {M} - {m}\n"); let encoder = PatternEncoder::new("{d(%Y-%m-%d %H:%M:%S%.3f %Z)} [{l}] {M} - {m}\n");
let config = Config::builder() let config = Config::builder()
.appender( .appender(Appender::builder().build(
Appender::builder() "stdout",
.build("stdout", Box::new( Box::new(ConsoleAppender::builder().encoder(Box::new(encoder)).build())
ConsoleAppender::builder() ))
.encoder(Box::new(encoder))
.build()
)))
.build(Root::builder().appender("stdout").build(LevelFilter::Info)) .build(Root::builder().appender("stdout").build(LevelFilter::Info))
.unwrap(); .unwrap();
log4rs::init_config(config).unwrap(); log4rs::init_config(config).unwrap();
@ -134,26 +122,22 @@ fn main()
let auth = <AuthMiddleware<(), Handler>>::from_source(AuthSource::AuthorizationHeader); let auth = <AuthMiddleware<(), Handler>>::from_source(AuthSource::AuthorizationHeader);
let logging = RequestLogger::new(log::Level::Info); let logging = RequestLogger::new(log::Level::Info);
let (chain, pipelines) = single_pipeline( let (chain, pipelines) = single_pipeline(new_pipeline().add(auth).add(logging).add(cors).build());
new_pipeline()
.add(auth)
.add(logging)
.add(cors)
.build()
);
gotham::start(ADDR, build_router(chain, pipelines, |route| { gotham::start(
let info = OpenapiInfo { ADDR,
title: "Users Example".to_owned(), build_router(chain, pipelines, |route| {
version: "0.0.1".to_owned(), let info = OpenapiInfo {
urls: vec![format!("http://{}", ADDR)] title: "Users Example".to_owned(),
}; version: "0.0.1".to_owned(),
route.with_openapi(info, |mut route| { urls: vec![format!("http://{}", ADDR)]
route.resource::<Users>("users"); };
route.resource::<Auth>("auth"); route.with_openapi(info, |mut route| {
route.get_openapi("openapi"); route.resource::<Users>("users");
}); route.resource::<Auth>("auth");
})); route.get_openapi("openapi");
});
})
);
println!("Gotham started on {} for testing", ADDR); println!("Gotham started on {} for testing", ADDR);
} }

19
rustfmt.toml Normal file
View file

@ -0,0 +1,19 @@
edition = "2018"
max_width = 125
newline_style = "Unix"
unstable_features = true
# always use tabs.
hard_tabs = true
tab_spaces = 4
# commas inbetween but not after
match_block_trailing_comma = true
trailing_comma = "Never"
# misc
format_code_in_doc_comments = true
merge_imports = true
overflow_delimited_expr = true
use_field_init_shorthand = true
use_try_shorthand = true

View file

@ -1,29 +1,24 @@
use crate::{AuthError, Forbidden, HeaderName}; use crate::{AuthError, Forbidden, HeaderName};
use cookie::CookieJar; use cookie::CookieJar;
use futures_util::{future, future::{FutureExt, TryFutureExt}}; use futures_util::{
future,
future::{FutureExt, TryFutureExt}
};
use gotham::{ use gotham::{
handler::HandlerFuture, handler::HandlerFuture,
hyper::header::{AUTHORIZATION, HeaderMap}, hyper::header::{HeaderMap, AUTHORIZATION},
middleware::{Middleware, NewMiddleware}, middleware::{Middleware, NewMiddleware},
state::{FromState, State} state::{FromState, State}
}; };
use jsonwebtoken::{ use jsonwebtoken::{errors::ErrorKind, DecodingKey};
errors::ErrorKind,
DecodingKey
};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use std::{ use std::{marker::PhantomData, panic::RefUnwindSafe, pin::Pin};
marker::PhantomData,
panic::RefUnwindSafe,
pin::Pin
};
pub use jsonwebtoken::Validation as AuthValidation; pub use jsonwebtoken::Validation as AuthValidation;
/// The authentication status returned by the auth middleware for each request. /// The authentication status returned by the auth middleware for each request.
#[derive(Debug, StateData)] #[derive(Debug, StateData)]
pub enum AuthStatus<T : Send + 'static> pub enum AuthStatus<T: Send + 'static> {
{
/// The auth status is unknown. /// The auth status is unknown.
Unknown, Unknown,
/// The request has been performed without any kind of authentication. /// The request has been performed without any kind of authentication.
@ -38,10 +33,9 @@ pub enum AuthStatus<T : Send + 'static>
impl<T> Clone for AuthStatus<T> impl<T> Clone for AuthStatus<T>
where where
T : Clone + Send + 'static T: Clone + Send + 'static
{ {
fn clone(&self) -> Self fn clone(&self) -> Self {
{
match self { match self {
Self::Unknown => Self::Unknown, Self::Unknown => Self::Unknown,
Self::Unauthenticated => Self::Unauthenticated, Self::Unauthenticated => Self::Unauthenticated,
@ -52,16 +46,10 @@ where
} }
} }
impl<T> Copy for AuthStatus<T> impl<T> Copy for AuthStatus<T> where T: Copy + Send + 'static {}
where
T : Copy + Send + 'static
{
}
impl<T : Send + 'static> AuthStatus<T> impl<T: Send + 'static> AuthStatus<T> {
{ pub fn ok(self) -> Result<T, AuthError> {
pub fn ok(self) -> Result<T, AuthError>
{
match self { match self {
Self::Authenticated(data) => Ok(data), Self::Authenticated(data) => Ok(data),
_ => Err(Forbidden) _ => Err(Forbidden)
@ -71,8 +59,7 @@ impl<T : Send + 'static> AuthStatus<T>
/// The source of the authentication token in the request. /// The source of the authentication token in the request.
#[derive(Clone, Debug, StateData)] #[derive(Clone, Debug, StateData)]
pub enum AuthSource pub enum AuthSource {
{
/// Take the token from a cookie with the given name. /// Take the token from a cookie with the given name.
Cookie(String), Cookie(String),
/// Take the token from a header with the given name. /// Take the token from a header with the given name.
@ -100,36 +87,29 @@ impl<T> AuthHandler<T> for CustomAuthHandler {
} }
``` ```
*/ */
pub trait AuthHandler<Data> pub trait AuthHandler<Data> {
{
/// Return the SHA256-HMAC secret used to verify the JWT token. /// Return the SHA256-HMAC secret used to verify the JWT token.
fn jwt_secret<F : FnOnce() -> Option<Data>>(&self, state : &mut State, decode_data : F) -> Option<Vec<u8>>; fn jwt_secret<F: FnOnce() -> Option<Data>>(&self, state: &mut State, decode_data: F) -> Option<Vec<u8>>;
} }
/// An `AuthHandler` returning always the same secret. See `AuthMiddleware` for a usage example. /// An `AuthHandler` returning always the same secret. See `AuthMiddleware` for a usage example.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct StaticAuthHandler pub struct StaticAuthHandler {
{ secret: Vec<u8>
secret : Vec<u8>
} }
impl StaticAuthHandler impl StaticAuthHandler {
{ pub fn from_vec(secret: Vec<u8>) -> Self {
pub fn from_vec(secret : Vec<u8>) -> Self
{
Self { secret } Self { secret }
} }
pub fn from_array(secret : &[u8]) -> Self pub fn from_array(secret: &[u8]) -> Self {
{
Self::from_vec(secret.to_vec()) Self::from_vec(secret.to_vec())
} }
} }
impl<T> AuthHandler<T> for StaticAuthHandler impl<T> AuthHandler<T> for StaticAuthHandler {
{ fn jwt_secret<F: FnOnce() -> Option<T>>(&self, _state: &mut State, _decode_data: F) -> Option<Vec<u8>> {
fn jwt_secret<F : FnOnce() -> Option<T>>(&self, _state : &mut State, _decode_data : F) -> Option<Vec<u8>>
{
Some(self.secret.clone()) Some(self.secret.clone())
} }
} }
@ -173,19 +153,18 @@ fn main() {
``` ```
*/ */
#[derive(Debug)] #[derive(Debug)]
pub struct AuthMiddleware<Data, Handler> pub struct AuthMiddleware<Data, Handler> {
{ source: AuthSource,
source : AuthSource, validation: AuthValidation,
validation : AuthValidation, handler: Handler,
handler : Handler, _data: PhantomData<Data>
_data : PhantomData<Data>
} }
impl<Data, Handler> Clone for AuthMiddleware<Data, Handler> impl<Data, Handler> Clone for AuthMiddleware<Data, Handler>
where Handler : Clone where
Handler: Clone
{ {
fn clone(&self) -> Self fn clone(&self) -> Self {
{
Self { Self {
source: self.source.clone(), source: self.source.clone(),
validation: self.validation.clone(), validation: self.validation.clone(),
@ -197,11 +176,10 @@ where Handler : Clone
impl<Data, Handler> AuthMiddleware<Data, Handler> impl<Data, Handler> AuthMiddleware<Data, Handler>
where where
Data : DeserializeOwned + Send, Data: DeserializeOwned + Send,
Handler : AuthHandler<Data> + Default Handler: AuthHandler<Data> + Default
{ {
pub fn from_source(source : AuthSource) -> Self pub fn from_source(source: AuthSource) -> Self {
{
Self { Self {
source, source,
validation: Default::default(), validation: Default::default(),
@ -213,11 +191,10 @@ where
impl<Data, Handler> AuthMiddleware<Data, Handler> impl<Data, Handler> AuthMiddleware<Data, Handler>
where where
Data : DeserializeOwned + Send, Data: DeserializeOwned + Send,
Handler : AuthHandler<Data> Handler: AuthHandler<Data>
{ {
pub fn new(source : AuthSource, validation : AuthValidation, handler : Handler) -> Self pub fn new(source: AuthSource, validation: AuthValidation, handler: Handler) -> Self {
{
Self { Self {
source, source,
validation, validation,
@ -226,28 +203,21 @@ where
} }
} }
fn auth_status(&self, state : &mut State) -> AuthStatus<Data> fn auth_status(&self, state: &mut State) -> AuthStatus<Data> {
{
// extract the provided token, if any // extract the provided token, if any
let token = match &self.source { let token = match &self.source {
AuthSource::Cookie(name) => { AuthSource::Cookie(name) => CookieJar::try_borrow_from(&state)
CookieJar::try_borrow_from(&state) .and_then(|jar| jar.get(&name))
.and_then(|jar| jar.get(&name)) .map(|cookie| cookie.value().to_owned()),
.map(|cookie| cookie.value().to_owned()) AuthSource::Header(name) => HeaderMap::try_borrow_from(&state)
}, .and_then(|map| map.get(name))
AuthSource::Header(name) => { .and_then(|header| header.to_str().ok())
HeaderMap::try_borrow_from(&state) .map(|value| value.to_owned()),
.and_then(|map| map.get(name)) AuthSource::AuthorizationHeader => HeaderMap::try_borrow_from(&state)
.and_then(|header| header.to_str().ok()) .and_then(|map| map.get(AUTHORIZATION))
.map(|value| value.to_owned()) .and_then(|header| header.to_str().ok())
}, .and_then(|value| value.split_whitespace().nth(1))
AuthSource::AuthorizationHeader => { .map(|value| value.to_owned())
HeaderMap::try_borrow_from(&state)
.and_then(|map| map.get(AUTHORIZATION))
.and_then(|header| header.to_str().ok())
.and_then(|value| value.split_whitespace().nth(1))
.map(|value| value.to_owned())
}
}; };
// unauthed if no token // unauthed if no token
@ -270,7 +240,7 @@ where
}; };
// validate the token // validate the token
let data : Data = match jsonwebtoken::decode(&token, &DecodingKey::from_secret(&secret), &self.validation) { let data: Data = match jsonwebtoken::decode(&token, &DecodingKey::from_secret(&secret), &self.validation) {
Ok(data) => data.claims, Ok(data) => data.claims,
Err(e) => match dbg!(e.into_kind()) { Err(e) => match dbg!(e.into_kind()) {
ErrorKind::ExpiredSignature => return AuthStatus::Expired, ErrorKind::ExpiredSignature => return AuthStatus::Expired,
@ -285,12 +255,12 @@ where
impl<Data, Handler> Middleware for AuthMiddleware<Data, Handler> impl<Data, Handler> Middleware for AuthMiddleware<Data, Handler>
where where
Data : DeserializeOwned + Send + 'static, Data: DeserializeOwned + Send + 'static,
Handler : AuthHandler<Data> Handler: AuthHandler<Data>
{ {
fn call<Chain>(self, mut state : State, chain : Chain) -> Pin<Box<HandlerFuture>> fn call<Chain>(self, mut state: State, chain: Chain) -> Pin<Box<HandlerFuture>>
where where
Chain : FnOnce(State) -> Pin<Box<HandlerFuture>> Chain: FnOnce(State) -> Pin<Box<HandlerFuture>>
{ {
// put the source in our state, required for e.g. openapi // put the source in our state, required for e.g. openapi
state.put(self.source.clone()); state.put(self.source.clone());
@ -306,26 +276,24 @@ where
impl<Data, Handler> NewMiddleware for AuthMiddleware<Data, Handler> impl<Data, Handler> NewMiddleware for AuthMiddleware<Data, Handler>
where where
Self : Clone + Middleware + Sync + RefUnwindSafe Self: Clone + Middleware + Sync + RefUnwindSafe
{ {
type Instance = Self; type Instance = Self;
fn new_middleware(&self) -> Result<Self::Instance, std::io::Error> fn new_middleware(&self) -> Result<Self::Instance, std::io::Error> {
{ let c: Self = self.clone();
let c : Self = self.clone();
Ok(c) Ok(c)
} }
} }
#[cfg(test)] #[cfg(test)]
mod test mod test {
{
use super::*; use super::*;
use cookie::Cookie; use cookie::Cookie;
use std::fmt::Debug; use std::fmt::Debug;
// 256-bit random string // 256-bit random string
const JWT_SECRET : &'static [u8; 32] = b"Lyzsfnta0cdxyF0T9y6VGxp3jpgoMUuW"; const JWT_SECRET: &'static [u8; 32] = b"Lyzsfnta0cdxyF0T9y6VGxp3jpgoMUuW";
// some known tokens // some known tokens
const VALID_TOKEN : &'static str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJtc3JkMCIsInN1YiI6ImdvdGhhbS1yZXN0ZnVsIiwiaWF0IjoxNTc3ODM2ODAwLCJleHAiOjQxMDI0NDQ4MDB9.8h8Ax-nnykqEQ62t7CxmM3ja6NzUQ4L0MLOOzddjLKk"; const VALID_TOKEN : &'static str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJtc3JkMCIsInN1YiI6ImdvdGhhbS1yZXN0ZnVsIiwiaWF0IjoxNTc3ODM2ODAwLCJleHAiOjQxMDI0NDQ4MDB9.8h8Ax-nnykqEQ62t7CxmM3ja6NzUQ4L0MLOOzddjLKk";
@ -333,18 +301,15 @@ mod test
const INVALID_TOKEN : &'static str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJtc3JkMCIsInN1YiI6ImdvdGhhbS1yZXN0ZnVsIiwiaWF0IjoxNTc3ODM2ODAwLCJleHAiOjQxMDI0NDQ4MDB9"; const INVALID_TOKEN : &'static str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJtc3JkMCIsInN1YiI6ImdvdGhhbS1yZXN0ZnVsIiwiaWF0IjoxNTc3ODM2ODAwLCJleHAiOjQxMDI0NDQ4MDB9";
#[derive(Debug, Deserialize, PartialEq)] #[derive(Debug, Deserialize, PartialEq)]
struct TestData struct TestData {
{ iss: String,
iss : String, sub: String,
sub : String, iat: u64,
iat : u64, exp: u64
exp : u64
} }
impl Default for TestData impl Default for TestData {
{ fn default() -> Self {
fn default() -> Self
{
Self { Self {
iss: "msrd0".to_owned(), iss: "msrd0".to_owned(),
sub: "gotham-restful".to_owned(), sub: "gotham-restful".to_owned(),
@ -356,17 +321,14 @@ mod test
#[derive(Default)] #[derive(Default)]
struct NoneAuthHandler; struct NoneAuthHandler;
impl<T> AuthHandler<T> for NoneAuthHandler impl<T> AuthHandler<T> for NoneAuthHandler {
{ fn jwt_secret<F: FnOnce() -> Option<T>>(&self, _state: &mut State, _decode_data: F) -> Option<Vec<u8>> {
fn jwt_secret<F : FnOnce() -> Option<T>>(&self, _state : &mut State, _decode_data : F) -> Option<Vec<u8>>
{
None None
} }
} }
#[test] #[test]
fn test_auth_middleware_none_secret() fn test_auth_middleware_none_secret() {
{
let middleware = <AuthMiddleware<TestData, NoneAuthHandler>>::from_source(AuthSource::AuthorizationHeader); let middleware = <AuthMiddleware<TestData, NoneAuthHandler>>::from_source(AuthSource::AuthorizationHeader);
State::with_new(|mut state| { State::with_new(|mut state| {
let mut headers = HeaderMap::new(); let mut headers = HeaderMap::new();
@ -379,18 +341,17 @@ mod test
#[derive(Default)] #[derive(Default)]
struct TestAssertingHandler; struct TestAssertingHandler;
impl<T> AuthHandler<T> for TestAssertingHandler impl<T> AuthHandler<T> for TestAssertingHandler
where T : Debug + Default + PartialEq where
T: Debug + Default + PartialEq
{ {
fn jwt_secret<F : FnOnce() -> Option<T>>(&self, _state : &mut State, decode_data : F) -> Option<Vec<u8>> fn jwt_secret<F: FnOnce() -> Option<T>>(&self, _state: &mut State, decode_data: F) -> Option<Vec<u8>> {
{
assert_eq!(decode_data(), Some(T::default())); assert_eq!(decode_data(), Some(T::default()));
Some(JWT_SECRET.to_vec()) Some(JWT_SECRET.to_vec())
} }
} }
#[test] #[test]
fn test_auth_middleware_decode_data() fn test_auth_middleware_decode_data() {
{
let middleware = <AuthMiddleware<TestData, TestAssertingHandler>>::from_source(AuthSource::AuthorizationHeader); let middleware = <AuthMiddleware<TestData, TestAssertingHandler>>::from_source(AuthSource::AuthorizationHeader);
State::with_new(|mut state| { State::with_new(|mut state| {
let mut headers = HeaderMap::new(); let mut headers = HeaderMap::new();
@ -400,15 +361,15 @@ mod test
}); });
} }
fn new_middleware<T>(source : AuthSource) -> AuthMiddleware<T, StaticAuthHandler> fn new_middleware<T>(source: AuthSource) -> AuthMiddleware<T, StaticAuthHandler>
where T : DeserializeOwned + Send where
T: DeserializeOwned + Send
{ {
AuthMiddleware::new(source, Default::default(), StaticAuthHandler::from_array(JWT_SECRET)) AuthMiddleware::new(source, Default::default(), StaticAuthHandler::from_array(JWT_SECRET))
} }
#[test] #[test]
fn test_auth_middleware_no_token() fn test_auth_middleware_no_token() {
{
let middleware = new_middleware::<TestData>(AuthSource::AuthorizationHeader); let middleware = new_middleware::<TestData>(AuthSource::AuthorizationHeader);
State::with_new(|mut state| { State::with_new(|mut state| {
let status = middleware.auth_status(&mut state); let status = middleware.auth_status(&mut state);
@ -420,8 +381,7 @@ mod test
} }
#[test] #[test]
fn test_auth_middleware_expired_token() fn test_auth_middleware_expired_token() {
{
let middleware = new_middleware::<TestData>(AuthSource::AuthorizationHeader); let middleware = new_middleware::<TestData>(AuthSource::AuthorizationHeader);
State::with_new(|mut state| { State::with_new(|mut state| {
let mut headers = HeaderMap::new(); let mut headers = HeaderMap::new();
@ -436,8 +396,7 @@ mod test
} }
#[test] #[test]
fn test_auth_middleware_invalid_token() fn test_auth_middleware_invalid_token() {
{
let middleware = new_middleware::<TestData>(AuthSource::AuthorizationHeader); let middleware = new_middleware::<TestData>(AuthSource::AuthorizationHeader);
State::with_new(|mut state| { State::with_new(|mut state| {
let mut headers = HeaderMap::new(); let mut headers = HeaderMap::new();
@ -452,8 +411,7 @@ mod test
} }
#[test] #[test]
fn test_auth_middleware_auth_header_token() fn test_auth_middleware_auth_header_token() {
{
let middleware = new_middleware::<TestData>(AuthSource::AuthorizationHeader); let middleware = new_middleware::<TestData>(AuthSource::AuthorizationHeader);
State::with_new(|mut state| { State::with_new(|mut state| {
let mut headers = HeaderMap::new(); let mut headers = HeaderMap::new();
@ -468,8 +426,7 @@ mod test
} }
#[test] #[test]
fn test_auth_middleware_header_token() fn test_auth_middleware_header_token() {
{
let header_name = "x-znoiprwmvfexju"; let header_name = "x-znoiprwmvfexju";
let middleware = new_middleware::<TestData>(AuthSource::Header(HeaderName::from_static(header_name))); let middleware = new_middleware::<TestData>(AuthSource::Header(HeaderName::from_static(header_name)));
State::with_new(|mut state| { State::with_new(|mut state| {
@ -485,8 +442,7 @@ mod test
} }
#[test] #[test]
fn test_auth_middleware_cookie_token() fn test_auth_middleware_cookie_token() {
{
let cookie_name = "znoiprwmvfexju"; let cookie_name = "znoiprwmvfexju";
let middleware = new_middleware::<TestData>(AuthSource::Cookie(cookie_name.to_owned())); let middleware = new_middleware::<TestData>(AuthSource::Cookie(cookie_name.to_owned()));
State::with_new(|mut state| { State::with_new(|mut state| {

View file

@ -4,22 +4,19 @@ use gotham::{
helpers::http::response::create_empty_response, helpers::http::response::create_empty_response,
hyper::{ hyper::{
header::{ header::{
ACCESS_CONTROL_ALLOW_CREDENTIALS, ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_METHODS, HeaderMap, HeaderName, HeaderValue, ACCESS_CONTROL_ALLOW_CREDENTIALS, ACCESS_CONTROL_ALLOW_HEADERS,
ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_MAX_AGE, ACCESS_CONTROL_REQUEST_METHOD, ORIGIN, VARY, ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_MAX_AGE,
HeaderMap, HeaderName, HeaderValue ACCESS_CONTROL_REQUEST_METHOD, ORIGIN, VARY
}, },
Body, Method, Response, StatusCode Body, Method, Response, StatusCode
}, },
middleware::Middleware, middleware::Middleware,
pipeline::chain::PipelineHandleChain, pipeline::chain::PipelineHandleChain,
router::builder::*, router::builder::*,
state::{FromState, State}, state::{FromState, State}
}; };
use itertools::Itertools; use itertools::Itertools;
use std::{ use std::{panic::RefUnwindSafe, pin::Pin};
panic::RefUnwindSafe,
pin::Pin
};
/** /**
Specify the allowed origins of the request. It is up to the browser to check the validity of the Specify the allowed origins of the request. It is up to the browser to check the validity of the
@ -27,8 +24,7 @@ origin. This, when sent to the browser, will indicate whether or not the request
allowed to make the request. allowed to make the request.
*/ */
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum Origin pub enum Origin {
{
/// Do not send any `Access-Control-Allow-Origin` headers. /// Do not send any `Access-Control-Allow-Origin` headers.
None, None,
/// Send `Access-Control-Allow-Origin: *`. Note that browser will not send credentials. /// Send `Access-Control-Allow-Origin: *`. Note that browser will not send credentials.
@ -39,19 +35,15 @@ pub enum Origin
Copy Copy
} }
impl Default for Origin impl Default for Origin {
{ fn default() -> Self {
fn default() -> Self
{
Self::None Self::None
} }
} }
impl Origin impl Origin {
{
/// Get the header value for the `Access-Control-Allow-Origin` header. /// Get the header value for the `Access-Control-Allow-Origin` header.
fn header_value(&self, state : &State) -> Option<HeaderValue> fn header_value(&self, state: &State) -> Option<HeaderValue> {
{
match self { match self {
Self::None => None, Self::None => None,
Self::Star => Some("*".parse().unwrap()), Self::Star => Some("*".parse().unwrap()),
@ -126,23 +118,21 @@ gotham::start("127.0.0.1:8080", build_router((), pipeline_set, |route| {
[`State`]: ../gotham/state/struct.State.html [`State`]: ../gotham/state/struct.State.html
*/ */
#[derive(Clone, Debug, Default, NewMiddleware, StateData)] #[derive(Clone, Debug, Default, NewMiddleware, StateData)]
pub struct CorsConfig pub struct CorsConfig {
{
/// The allowed origins. /// The allowed origins.
pub origin : Origin, pub origin: Origin,
/// The allowed headers. /// The allowed headers.
pub headers : Vec<HeaderName>, pub headers: Vec<HeaderName>,
/// The amount of seconds that the preflight request can be cached. /// The amount of seconds that the preflight request can be cached.
pub max_age : u64, pub max_age: u64,
/// Whether or not the request may be made with supplying credentials. /// Whether or not the request may be made with supplying credentials.
pub credentials : bool pub credentials: bool
} }
impl Middleware for CorsConfig impl Middleware for CorsConfig {
{ fn call<Chain>(self, mut state: State, chain: Chain) -> Pin<Box<HandlerFuture>>
fn call<Chain>(self, mut state : State, chain : Chain) -> Pin<Box<HandlerFuture>>
where where
Chain : FnOnce(State) -> Pin<Box<HandlerFuture>> Chain: FnOnce(State) -> Pin<Box<HandlerFuture>>
{ {
state.put(self); state.put(self);
chain(state) chain(state)
@ -161,27 +151,23 @@ For further information on CORS, read https://developer.mozilla.org/en-US/docs/W
[`CorsConfig`]: ./struct.CorsConfig.html [`CorsConfig`]: ./struct.CorsConfig.html
*/ */
pub fn handle_cors(state : &State, res : &mut Response<Body>) pub fn handle_cors(state: &State, res: &mut Response<Body>) {
{
let config = CorsConfig::try_borrow_from(state); let config = CorsConfig::try_borrow_from(state);
let headers = res.headers_mut(); let headers = res.headers_mut();
// non-preflight requests require the Access-Control-Allow-Origin header // non-preflight requests require the Access-Control-Allow-Origin header
if let Some(header) = config.and_then(|cfg| cfg.origin.header_value(state)) if let Some(header) = config.and_then(|cfg| cfg.origin.header_value(state)) {
{
headers.insert(ACCESS_CONTROL_ALLOW_ORIGIN, header); headers.insert(ACCESS_CONTROL_ALLOW_ORIGIN, header);
} }
// if the origin is copied over, we should tell the browser by specifying the Vary header // if the origin is copied over, we should tell the browser by specifying the Vary header
if matches!(config.map(|cfg| &cfg.origin), Some(Origin::Copy)) if matches!(config.map(|cfg| &cfg.origin), Some(Origin::Copy)) {
{
let vary = headers.get(VARY).map(|vary| format!("{},Origin", vary.to_str().unwrap())); let vary = headers.get(VARY).map(|vary| format!("{},Origin", vary.to_str().unwrap()));
headers.insert(VARY, vary.as_deref().unwrap_or("Origin").parse().unwrap()); headers.insert(VARY, vary.as_deref().unwrap_or("Origin").parse().unwrap());
} }
// if we allow credentials, tell the browser // if we allow credentials, tell the browser
if config.map(|cfg| cfg.credentials).unwrap_or(false) if config.map(|cfg| cfg.credentials).unwrap_or(false) {
{
headers.insert(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true".parse().unwrap()); headers.insert(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true".parse().unwrap());
} }
} }
@ -206,16 +192,15 @@ pub fn handle_cors(state : &State, res : &mut Response<Body>)
/// ``` /// ```
pub trait CorsRoute<C, P> pub trait CorsRoute<C, P>
where where
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static, C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
P : RefUnwindSafe + Send + Sync + 'static P: RefUnwindSafe + Send + Sync + 'static
{ {
/// Handle a preflight request on `path` for `method`. To configure the behaviour, use /// Handle a preflight request on `path` for `method`. To configure the behaviour, use
/// [`CorsConfig`](struct.CorsConfig.html). /// [`CorsConfig`](struct.CorsConfig.html).
fn cors(&mut self, path : &str, method : Method); fn cors(&mut self, path: &str, method: Method);
} }
fn cors_preflight_handler(state : State) -> (State, Response<Body>) fn cors_preflight_handler(state: State) -> (State, Response<Body>) {
{
let config = CorsConfig::try_borrow_from(&state); let config = CorsConfig::try_borrow_from(&state);
// prepare the response // prepare the response
@ -223,22 +208,22 @@ fn cors_preflight_handler(state : State) -> (State, Response<Body>)
let headers = res.headers_mut(); let headers = res.headers_mut();
// copy the request method over to the response // copy the request method over to the response
let method = HeaderMap::borrow_from(&state).get(ACCESS_CONTROL_REQUEST_METHOD).unwrap().clone(); let method = HeaderMap::borrow_from(&state)
.get(ACCESS_CONTROL_REQUEST_METHOD)
.unwrap()
.clone();
headers.insert(ACCESS_CONTROL_ALLOW_METHODS, method); headers.insert(ACCESS_CONTROL_ALLOW_METHODS, method);
// if we allow any headers, put them in // if we allow any headers, put them in
if let Some(hdrs) = config.map(|cfg| &cfg.headers) if let Some(hdrs) = config.map(|cfg| &cfg.headers) {
{ if hdrs.len() > 0 {
if hdrs.len() > 0
{
// TODO do we want to return all headers or just those asked by the browser? // TODO do we want to return all headers or just those asked by the browser?
headers.insert(ACCESS_CONTROL_ALLOW_HEADERS, hdrs.iter().join(",").parse().unwrap()); headers.insert(ACCESS_CONTROL_ALLOW_HEADERS, hdrs.iter().join(",").parse().unwrap());
} }
} }
// set the max age for the preflight cache // set the max age for the preflight cache
if let Some(age) = config.map(|cfg| cfg.max_age) if let Some(age) = config.map(|cfg| cfg.max_age) {
{
headers.insert(ACCESS_CONTROL_MAX_AGE, age.into()); headers.insert(ACCESS_CONTROL_MAX_AGE, age.into());
} }
@ -251,15 +236,12 @@ fn cors_preflight_handler(state : State) -> (State, Response<Body>)
impl<D, C, P> CorsRoute<C, P> for D impl<D, C, P> CorsRoute<C, P> for D
where where
D : DrawRoutes<C, P>, D: DrawRoutes<C, P>,
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static, C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
P : RefUnwindSafe + Send + Sync + 'static P: RefUnwindSafe + Send + Sync + 'static
{ {
fn cors(&mut self, path : &str, method : Method) fn cors(&mut self, path: &str, method: Method) {
{
let matcher = AccessControlRequestMethodMatcher::new(method); let matcher = AccessControlRequestMethodMatcher::new(method);
self.options(path) self.options(path).extend_route_matcher(matcher).to(cors_preflight_handler);
.extend_route_matcher(matcher)
.to(cors_preflight_handler);
} }
} }

View file

@ -370,9 +370,12 @@ Licensed under your option of:
// weird proc macro issue // weird proc macro issue
extern crate self as gotham_restful; extern crate self as gotham_restful;
#[macro_use] extern crate gotham_derive; #[macro_use]
#[macro_use] extern crate log; extern crate gotham_derive;
#[macro_use] extern crate serde; #[macro_use]
extern crate log;
#[macro_use]
extern crate serde;
#[doc(no_inline)] #[doc(no_inline)]
pub use gotham; pub use gotham;
@ -388,8 +391,7 @@ pub use gotham_restful_derive::*;
/// Not public API /// Not public API
#[doc(hidden)] #[doc(hidden)]
pub mod export pub mod export {
{
pub use futures_util::future::FutureExt; pub use futures_util::future::FutureExt;
pub use serde_json; pub use serde_json;
@ -406,24 +408,12 @@ pub mod export
#[cfg(feature = "auth")] #[cfg(feature = "auth")]
mod auth; mod auth;
#[cfg(feature = "auth")] #[cfg(feature = "auth")]
pub use auth::{ pub use auth::{AuthHandler, AuthMiddleware, AuthSource, AuthStatus, AuthValidation, StaticAuthHandler};
AuthHandler,
AuthMiddleware,
AuthSource,
AuthStatus,
AuthValidation,
StaticAuthHandler
};
#[cfg(feature = "cors")] #[cfg(feature = "cors")]
mod cors; mod cors;
#[cfg(feature = "cors")] #[cfg(feature = "cors")]
pub use cors::{ pub use cors::{handle_cors, CorsConfig, CorsRoute, Origin};
handle_cors,
CorsConfig,
CorsRoute,
Origin
};
pub mod matcher; pub mod matcher;
@ -438,16 +428,8 @@ pub use openapi::{
mod resource; mod resource;
pub use resource::{ pub use resource::{
Resource, Resource, ResourceChange, ResourceChangeAll, ResourceCreate, ResourceMethod, ResourceRead, ResourceReadAll,
ResourceMethod, ResourceRemove, ResourceRemoveAll, ResourceSearch
ResourceReadAll,
ResourceRead,
ResourceSearch,
ResourceCreate,
ResourceChangeAll,
ResourceChange,
ResourceRemoveAll,
ResourceRemove
}; };
mod response; mod response;
@ -455,22 +437,14 @@ pub use response::Response;
mod result; mod result;
pub use result::{ pub use result::{
AuthError, AuthError, AuthError::Forbidden, AuthErrorOrOther, AuthResult, AuthSuccess, IntoResponseError, NoContent, Raw,
AuthError::Forbidden, ResourceResult, Success
AuthErrorOrOther,
AuthResult,
AuthSuccess,
IntoResponseError,
NoContent,
Raw,
ResourceResult,
Success
}; };
mod routing; mod routing;
pub use routing::{DrawResources, DrawResourceRoutes};
#[cfg(feature = "openapi")] #[cfg(feature = "openapi")]
pub use routing::WithOpenapi; pub use routing::WithOpenapi;
pub use routing::{DrawResourceRoutes, DrawResources};
mod types; mod types;
pub use types::*; pub use types::*;

View file

@ -1,5 +1,8 @@
use gotham::{ use gotham::{
hyper::{header::{ACCESS_CONTROL_REQUEST_METHOD, HeaderMap}, Method, StatusCode}, hyper::{
header::{HeaderMap, ACCESS_CONTROL_REQUEST_METHOD},
Method, StatusCode
},
router::{non_match::RouteNonMatch, route::matcher::RouteMatcher}, router::{non_match::RouteNonMatch, route::matcher::RouteMatcher},
state::{FromState, State} state::{FromState, State}
}; };
@ -18,41 +21,35 @@ use gotham::{
/// ///
/// # build_simple_router(|route| { /// # build_simple_router(|route| {
/// // use the matcher for your request /// // use the matcher for your request
/// route.options("/foo") /// route.options("/foo").extend_route_matcher(matcher).to(|state| {
/// .extend_route_matcher(matcher) /// // we know that this is a CORS preflight for a PUT request
/// .to(|state| { /// let mut res = create_empty_response(&state, StatusCode::NO_CONTENT);
/// // we know that this is a CORS preflight for a PUT request /// res.headers_mut().insert(ACCESS_CONTROL_ALLOW_METHODS, "PUT".parse().unwrap());
/// let mut res = create_empty_response(&state, StatusCode::NO_CONTENT); /// (state, res)
/// res.headers_mut().insert(ACCESS_CONTROL_ALLOW_METHODS, "PUT".parse().unwrap());
/// (state, res)
/// }); /// });
/// # }); /// # });
/// ``` /// ```
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct AccessControlRequestMethodMatcher pub struct AccessControlRequestMethodMatcher {
{ method: Method
method : Method
} }
impl AccessControlRequestMethodMatcher impl AccessControlRequestMethodMatcher {
{
/// Construct a new matcher that matches if the `Access-Control-Request-Method` header matches `method`. /// Construct a new matcher that matches if the `Access-Control-Request-Method` header matches `method`.
/// Note that during matching the method is normalized according to the fetch specification, that is, /// Note that during matching the method is normalized according to the fetch specification, that is,
/// byte-uppercased. This means that when using a custom `method` instead of a predefined one, make sure /// byte-uppercased. This means that when using a custom `method` instead of a predefined one, make sure
/// it is uppercased or this matcher will never succeed. /// it is uppercased or this matcher will never succeed.
pub fn new(method : Method) -> Self pub fn new(method: Method) -> Self {
{
Self { method } Self { method }
} }
} }
impl RouteMatcher for AccessControlRequestMethodMatcher impl RouteMatcher for AccessControlRequestMethodMatcher {
{ fn is_match(&self, state: &State) -> Result<(), RouteNonMatch> {
fn is_match(&self, state : &State) -> Result<(), RouteNonMatch>
{
// according to the fetch specification, methods should be normalized by byte-uppercase // according to the fetch specification, methods should be normalized by byte-uppercase
// https://fetch.spec.whatwg.org/#concept-method // https://fetch.spec.whatwg.org/#concept-method
match HeaderMap::borrow_from(state).get(ACCESS_CONTROL_REQUEST_METHOD) match HeaderMap::borrow_from(state)
.get(ACCESS_CONTROL_REQUEST_METHOD)
.and_then(|value| value.to_str().ok()) .and_then(|value| value.to_str().ok())
.and_then(|str| str.to_ascii_uppercase().parse::<Method>().ok()) .and_then(|str| str.to_ascii_uppercase().parse::<Method>().ok())
{ {
@ -62,19 +59,17 @@ impl RouteMatcher for AccessControlRequestMethodMatcher
} }
} }
#[cfg(test)] #[cfg(test)]
mod test mod test {
{
use super::*; use super::*;
fn with_state<F>(accept : Option<&str>, block : F) fn with_state<F>(accept: Option<&str>, block: F)
where F : FnOnce(&mut State) -> () where
F: FnOnce(&mut State) -> ()
{ {
State::with_new(|state| { State::with_new(|state| {
let mut headers = HeaderMap::new(); let mut headers = HeaderMap::new();
if let Some(acc) = accept if let Some(acc) = accept {
{
headers.insert(ACCESS_CONTROL_REQUEST_METHOD, acc.parse().unwrap()); headers.insert(ACCESS_CONTROL_REQUEST_METHOD, acc.parse().unwrap());
} }
state.put(headers); state.put(headers);
@ -83,23 +78,20 @@ mod test
} }
#[test] #[test]
fn no_acrm_header() fn no_acrm_header() {
{
let matcher = AccessControlRequestMethodMatcher::new(Method::PUT); let matcher = AccessControlRequestMethodMatcher::new(Method::PUT);
with_state(None, |state| assert!(matcher.is_match(&state).is_err())); with_state(None, |state| assert!(matcher.is_match(&state).is_err()));
} }
#[test] #[test]
fn correct_acrm_header() fn correct_acrm_header() {
{
let matcher = AccessControlRequestMethodMatcher::new(Method::PUT); let matcher = AccessControlRequestMethodMatcher::new(Method::PUT);
with_state(Some("PUT"), |state| assert!(matcher.is_match(&state).is_ok())); with_state(Some("PUT"), |state| assert!(matcher.is_match(&state).is_ok()));
with_state(Some("put"), |state| assert!(matcher.is_match(&state).is_ok())); with_state(Some("put"), |state| assert!(matcher.is_match(&state).is_ok()));
} }
#[test] #[test]
fn incorrect_acrm_header() fn incorrect_acrm_header() {
{
let matcher = AccessControlRequestMethodMatcher::new(Method::PUT); let matcher = AccessControlRequestMethodMatcher::new(Method::PUT);
with_state(Some("DELETE"), |state| assert!(matcher.is_match(&state).is_err())); with_state(Some("DELETE"), |state| assert!(matcher.is_match(&state).is_err()));
} }

View file

@ -2,4 +2,3 @@
mod access_control_request_method; mod access_control_request_method;
#[cfg(feature = "cors")] #[cfg(feature = "cors")]
pub use access_control_request_method::AccessControlRequestMethodMatcher; pub use access_control_request_method::AccessControlRequestMethodMatcher;

View file

@ -1,29 +1,26 @@
use crate::{OpenapiType, OpenapiSchema}; use crate::{OpenapiSchema, OpenapiType};
use indexmap::IndexMap; use indexmap::IndexMap;
use openapiv3::{ use openapiv3::{
Components, OpenAPI, PathItem, ReferenceOr, ReferenceOr::Item, ReferenceOr::Reference, Schema, Components, OpenAPI, PathItem, ReferenceOr,
Server ReferenceOr::{Item, Reference},
Schema, Server
}; };
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct OpenapiInfo pub struct OpenapiInfo {
{ pub title: String,
pub title : String, pub version: String,
pub version : String, pub urls: Vec<String>
pub urls : Vec<String>
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct OpenapiBuilder pub struct OpenapiBuilder {
{ pub openapi: Arc<RwLock<OpenAPI>>
pub openapi : Arc<RwLock<OpenAPI>>
} }
impl OpenapiBuilder impl OpenapiBuilder {
{ pub fn new(info: OpenapiInfo) -> Self {
pub fn new(info : OpenapiInfo) -> Self
{
Self { Self {
openapi: Arc::new(RwLock::new(OpenAPI { openapi: Arc::new(RwLock::new(OpenAPI {
openapi: "3.0.2".to_string(), openapi: "3.0.2".to_string(),
@ -32,8 +29,13 @@ impl OpenapiBuilder
version: info.version, version: info.version,
..Default::default() ..Default::default()
}, },
servers: info.urls.into_iter() servers: info
.map(|url| Server { url, ..Default::default() }) .urls
.into_iter()
.map(|url| Server {
url,
..Default::default()
})
.collect(), .collect(),
..Default::default() ..Default::default()
})) }))
@ -42,8 +44,7 @@ impl OpenapiBuilder
/// Remove path from the OpenAPI spec, or return an empty one if not included. This is handy if you need to /// Remove path from the OpenAPI spec, or return an empty one if not included. This is handy if you need to
/// modify the path and add it back after the modification /// modify the path and add it back after the modification
pub fn remove_path(&mut self, path : &str) -> PathItem pub fn remove_path(&mut self, path: &str) -> PathItem {
{
let mut openapi = self.openapi.write().unwrap(); let mut openapi = self.openapi.write().unwrap();
match openapi.paths.swap_remove(path) { match openapi.paths.swap_remove(path) {
Some(Item(item)) => item, Some(Item(item)) => item,
@ -51,14 +52,12 @@ impl OpenapiBuilder
} }
} }
pub fn add_path<Path : ToString>(&mut self, path : Path, item : PathItem) pub fn add_path<Path: ToString>(&mut self, path: Path, item: PathItem) {
{
let mut openapi = self.openapi.write().unwrap(); let mut openapi = self.openapi.write().unwrap();
openapi.paths.insert(path.to_string(), Item(item)); openapi.paths.insert(path.to_string(), Item(item));
} }
fn add_schema_impl(&mut self, name : String, mut schema : OpenapiSchema) fn add_schema_impl(&mut self, name: String, mut schema: OpenapiSchema) {
{
self.add_schema_dependencies(&mut schema.dependencies); self.add_schema_dependencies(&mut schema.dependencies);
let mut openapi = self.openapi.write().unwrap(); let mut openapi = self.openapi.write().unwrap();
@ -74,25 +73,23 @@ impl OpenapiBuilder
}; };
} }
fn add_schema_dependencies(&mut self, dependencies : &mut IndexMap<String, OpenapiSchema>) fn add_schema_dependencies(&mut self, dependencies: &mut IndexMap<String, OpenapiSchema>) {
{ let keys: Vec<String> = dependencies.keys().map(|k| k.to_string()).collect();
let keys : Vec<String> = dependencies.keys().map(|k| k.to_string()).collect(); for dep in keys {
for dep in keys
{
let dep_schema = dependencies.swap_remove(&dep); let dep_schema = dependencies.swap_remove(&dep);
if let Some(dep_schema) = dep_schema if let Some(dep_schema) = dep_schema {
{
self.add_schema_impl(dep, dep_schema); self.add_schema_impl(dep, dep_schema);
} }
} }
} }
pub fn add_schema<T : OpenapiType>(&mut self) -> ReferenceOr<Schema> pub fn add_schema<T: OpenapiType>(&mut self) -> ReferenceOr<Schema> {
{
let mut schema = T::schema(); let mut schema = T::schema();
match schema.name.clone() { match schema.name.clone() {
Some(name) => { Some(name) => {
let reference = Reference { reference: format!("#/components/schemas/{}", name) }; let reference = Reference {
reference: format!("#/components/schemas/{}", name)
};
self.add_schema_impl(name, schema); self.add_schema_impl(name, schema);
reference reference
}, },
@ -104,27 +101,22 @@ impl OpenapiBuilder
} }
} }
#[cfg(test)] #[cfg(test)]
#[allow(dead_code)] #[allow(dead_code)]
mod test mod test {
{
use super::*; use super::*;
#[derive(OpenapiType)] #[derive(OpenapiType)]
struct Message struct Message {
{ msg: String
msg : String
} }
#[derive(OpenapiType)] #[derive(OpenapiType)]
struct Messages struct Messages {
{ msgs: Vec<Message>
msgs : Vec<Message>
} }
fn info() -> OpenapiInfo fn info() -> OpenapiInfo {
{
OpenapiInfo { OpenapiInfo {
title: "TEST CASE".to_owned(), title: "TEST CASE".to_owned(),
version: "1.2.3".to_owned(), version: "1.2.3".to_owned(),
@ -132,14 +124,12 @@ mod test
} }
} }
fn openapi(builder : OpenapiBuilder) -> OpenAPI fn openapi(builder: OpenapiBuilder) -> OpenAPI {
{
Arc::try_unwrap(builder.openapi).unwrap().into_inner().unwrap() Arc::try_unwrap(builder.openapi).unwrap().into_inner().unwrap()
} }
#[test] #[test]
fn new_builder() fn new_builder() {
{
let info = info(); let info = info();
let builder = OpenapiBuilder::new(info.clone()); let builder = OpenapiBuilder::new(info.clone());
let openapi = openapi(builder); let openapi = openapi(builder);
@ -150,13 +140,18 @@ mod test
} }
#[test] #[test]
fn add_schema() fn add_schema() {
{
let mut builder = OpenapiBuilder::new(info()); let mut builder = OpenapiBuilder::new(info());
builder.add_schema::<Option<Messages>>(); builder.add_schema::<Option<Messages>>();
let openapi = openapi(builder); let openapi = openapi(builder);
assert_eq!(openapi.components.clone().unwrap_or_default().schemas["Message"] , ReferenceOr::Item(Message ::schema().into_schema())); assert_eq!(
assert_eq!(openapi.components.clone().unwrap_or_default().schemas["Messages"], ReferenceOr::Item(Messages::schema().into_schema())); openapi.components.clone().unwrap_or_default().schemas["Message"],
ReferenceOr::Item(Message::schema().into_schema())
);
assert_eq!(
openapi.components.clone().unwrap_or_default().schemas["Messages"],
ReferenceOr::Item(Messages::schema().into_schema())
);
} }
} }

View file

@ -15,32 +15,26 @@ use std::{
}; };
#[derive(Clone)] #[derive(Clone)]
pub struct OpenapiHandler pub struct OpenapiHandler {
{ openapi: Arc<RwLock<OpenAPI>>
openapi : Arc<RwLock<OpenAPI>>
} }
impl OpenapiHandler impl OpenapiHandler {
{ pub fn new(openapi: Arc<RwLock<OpenAPI>>) -> Self {
pub fn new(openapi : Arc<RwLock<OpenAPI>>) -> Self
{
Self { openapi } Self { openapi }
} }
} }
impl NewHandler for OpenapiHandler impl NewHandler for OpenapiHandler {
{
type Instance = Self; type Instance = Self;
fn new_handler(&self) -> Result<Self> fn new_handler(&self) -> Result<Self> {
{
Ok(self.clone()) Ok(self.clone())
} }
} }
#[cfg(feature = "auth")] #[cfg(feature = "auth")]
fn get_security(state : &mut State) -> IndexMap<String, ReferenceOr<SecurityScheme>> fn get_security(state: &mut State) -> IndexMap<String, ReferenceOr<SecurityScheme>> {
{
use crate::AuthSource; use crate::AuthSource;
use gotham::state::FromState; use gotham::state::FromState;
@ -64,28 +58,25 @@ fn get_security(state : &mut State) -> IndexMap<String, ReferenceOr<SecuritySche
} }
}; };
let mut security_schemes : IndexMap<String, ReferenceOr<SecurityScheme>> = Default::default(); let mut security_schemes: IndexMap<String, ReferenceOr<SecurityScheme>> = Default::default();
security_schemes.insert(SECURITY_NAME.to_owned(), ReferenceOr::Item(security_scheme)); security_schemes.insert(SECURITY_NAME.to_owned(), ReferenceOr::Item(security_scheme));
security_schemes security_schemes
} }
#[cfg(not(feature = "auth"))] #[cfg(not(feature = "auth"))]
fn get_security(state : &mut State) -> (Vec<SecurityRequirement>, IndexMap<String, ReferenceOr<SecurityScheme>>) fn get_security(state: &mut State) -> (Vec<SecurityRequirement>, IndexMap<String, ReferenceOr<SecurityScheme>>) {
{
Default::default() Default::default()
} }
impl Handler for OpenapiHandler impl Handler for OpenapiHandler {
{ fn handle(self, mut state: State) -> Pin<Box<HandlerFuture>> {
fn handle(self, mut state : State) -> Pin<Box<HandlerFuture>>
{
let openapi = match self.openapi.read() { let openapi = match self.openapi.read() {
Ok(openapi) => openapi, Ok(openapi) => openapi,
Err(e) => { Err(e) => {
error!("Unable to acquire read lock for the OpenAPI specification: {}", e); error!("Unable to acquire read lock for the OpenAPI specification: {}", e);
let res = create_response(&state, crate::StatusCode::INTERNAL_SERVER_ERROR, TEXT_PLAIN, ""); let res = create_response(&state, crate::StatusCode::INTERNAL_SERVER_ERROR, TEXT_PLAIN, "");
return future::ok((state, res)).boxed() return future::ok((state, res)).boxed();
} }
}; };

View file

@ -1,5 +1,4 @@
const SECURITY_NAME: &str = "authToken";
const SECURITY_NAME : &str = "authToken";
pub mod builder; pub mod builder;
pub mod handler; pub mod handler;

View file

@ -1,32 +1,21 @@
use crate::{
resource::*,
result::*,
OpenapiSchema,
RequestBody
};
use super::SECURITY_NAME; use super::SECURITY_NAME;
use crate::{resource::*, result::*, OpenapiSchema, RequestBody};
use indexmap::IndexMap; use indexmap::IndexMap;
use mime::Mime; use mime::Mime;
use openapiv3::{ use openapiv3::{
MediaType, Operation, Parameter, ParameterData, ParameterSchemaOrContent, ReferenceOr, MediaType, Operation, Parameter, ParameterData, ParameterSchemaOrContent, ReferenceOr, ReferenceOr::Item,
ReferenceOr::Item, RequestBody as OARequestBody, Response, Responses, Schema, SchemaKind, RequestBody as OARequestBody, Response, Responses, Schema, SchemaKind, StatusCode, Type
StatusCode, Type
}; };
#[derive(Default)] #[derive(Default)]
struct OperationParams<'a> struct OperationParams<'a> {
{ path_params: Vec<(&'a str, ReferenceOr<Schema>)>,
path_params : Vec<(&'a str, ReferenceOr<Schema>)>, query_params: Option<OpenapiSchema>
query_params : Option<OpenapiSchema>
} }
impl<'a> OperationParams<'a> impl<'a> OperationParams<'a> {
{ fn add_path_params(&self, params: &mut Vec<ReferenceOr<Parameter>>) {
fn add_path_params(&self, params : &mut Vec<ReferenceOr<Parameter>>) for param in &self.path_params {
{
for param in &self.path_params
{
params.push(Item(Parameter::Path { params.push(Item(Parameter::Path {
parameter_data: ParameterData { parameter_data: ParameterData {
name: (*param).0.to_string(), name: (*param).0.to_string(),
@ -37,13 +26,12 @@ impl<'a> OperationParams<'a>
example: None, example: None,
examples: IndexMap::new() examples: IndexMap::new()
}, },
style: Default::default(), style: Default::default()
})); }));
} }
} }
fn add_query_params(self, params : &mut Vec<ReferenceOr<Parameter>>) fn add_query_params(self, params: &mut Vec<ReferenceOr<Parameter>>) {
{
let query_params = match self.query_params { let query_params = match self.query_params {
Some(qp) => qp.schema, Some(qp) => qp.schema,
None => return None => return
@ -52,8 +40,7 @@ impl<'a> OperationParams<'a>
SchemaKind::Type(Type::Object(ty)) => ty, SchemaKind::Type(Type::Object(ty)) => ty,
_ => panic!("Query Parameters needs to be a plain struct") _ => panic!("Query Parameters needs to be a plain struct")
}; };
for (name, schema) in query_params.properties for (name, schema) in query_params.properties {
{
let required = query_params.required.contains(&name); let required = query_params.required.contains(&name);
params.push(Item(Parameter::Query { params.push(Item(Parameter::Query {
parameter_data: ParameterData { parameter_data: ParameterData {
@ -72,31 +59,27 @@ impl<'a> OperationParams<'a>
} }
} }
fn into_params(self) -> Vec<ReferenceOr<Parameter>> fn into_params(self) -> Vec<ReferenceOr<Parameter>> {
{ let mut params: Vec<ReferenceOr<Parameter>> = Vec::new();
let mut params : Vec<ReferenceOr<Parameter>> = Vec::new();
self.add_path_params(&mut params); self.add_path_params(&mut params);
self.add_query_params(&mut params); self.add_query_params(&mut params);
params params
} }
} }
pub struct OperationDescription<'a> pub struct OperationDescription<'a> {
{ operation_id: Option<String>,
operation_id : Option<String>, default_status: crate::StatusCode,
default_status : crate::StatusCode, accepted_types: Option<Vec<Mime>>,
accepted_types : Option<Vec<Mime>>, schema: ReferenceOr<Schema>,
schema : ReferenceOr<Schema>, params: OperationParams<'a>,
params : OperationParams<'a>, body_schema: Option<ReferenceOr<Schema>>,
body_schema : Option<ReferenceOr<Schema>>, supported_types: Option<Vec<Mime>>,
supported_types : Option<Vec<Mime>>, requires_auth: bool
requires_auth : bool
} }
impl<'a> OperationDescription<'a> impl<'a> OperationDescription<'a> {
{ pub fn new<Handler: ResourceMethod>(schema: ReferenceOr<Schema>) -> Self {
pub fn new<Handler : ResourceMethod>(schema : ReferenceOr<Schema>) -> Self
{
Self { Self {
operation_id: Handler::operation_id(), operation_id: Handler::operation_id(),
default_status: Handler::Res::default_status(), default_status: Handler::Res::default_status(),
@ -109,31 +92,25 @@ impl<'a> OperationDescription<'a>
} }
} }
pub fn add_path_param(mut self, name : &'a str, schema : ReferenceOr<Schema>) -> Self pub fn add_path_param(mut self, name: &'a str, schema: ReferenceOr<Schema>) -> Self {
{
self.params.path_params.push((name, schema)); self.params.path_params.push((name, schema));
self self
} }
pub fn with_query_params(mut self, params : OpenapiSchema) -> Self pub fn with_query_params(mut self, params: OpenapiSchema) -> Self {
{
self.params.query_params = Some(params); self.params.query_params = Some(params);
self self
} }
pub fn with_body<Body : RequestBody>(mut self, schema : ReferenceOr<Schema>) -> Self pub fn with_body<Body: RequestBody>(mut self, schema: ReferenceOr<Schema>) -> Self {
{
self.body_schema = Some(schema); self.body_schema = Some(schema);
self.supported_types = Body::supported_types(); self.supported_types = Body::supported_types();
self self
} }
fn schema_to_content(types: Vec<Mime>, schema: ReferenceOr<Schema>) -> IndexMap<String, MediaType> {
fn schema_to_content(types : Vec<Mime>, schema : ReferenceOr<Schema>) -> IndexMap<String, MediaType> let mut content: IndexMap<String, MediaType> = IndexMap::new();
{ for ty in types {
let mut content : IndexMap<String, MediaType> = IndexMap::new();
for ty in types
{
content.insert(ty.to_string(), MediaType { content.insert(ty.to_string(), MediaType {
schema: Some(schema.clone()), schema: Some(schema.clone()),
..Default::default() ..Default::default()
@ -142,30 +119,41 @@ impl<'a> OperationDescription<'a>
content content
} }
pub fn into_operation(self) -> Operation pub fn into_operation(self) -> Operation {
{
// this is unfortunately neccessary to prevent rust from complaining about partially moving self // this is unfortunately neccessary to prevent rust from complaining about partially moving self
let (operation_id, default_status, accepted_types, schema, params, body_schema, supported_types, requires_auth) = ( let (operation_id, default_status, accepted_types, schema, params, body_schema, supported_types, requires_auth) = (
self.operation_id, self.default_status, self.accepted_types, self.schema, self.params, self.body_schema, self.supported_types, self.requires_auth); self.operation_id,
self.default_status,
self.accepted_types,
self.schema,
self.params,
self.body_schema,
self.supported_types,
self.requires_auth
);
let content = Self::schema_to_content(accepted_types.or_all_types(), schema); let content = Self::schema_to_content(accepted_types.or_all_types(), schema);
let mut responses : IndexMap<StatusCode, ReferenceOr<Response>> = IndexMap::new(); let mut responses: IndexMap<StatusCode, ReferenceOr<Response>> = IndexMap::new();
responses.insert(StatusCode::Code(default_status.as_u16()), Item(Response { responses.insert(
description: default_status.canonical_reason().map(|d| d.to_string()).unwrap_or_default(), StatusCode::Code(default_status.as_u16()),
content, Item(Response {
..Default::default() description: default_status.canonical_reason().map(|d| d.to_string()).unwrap_or_default(),
})); content,
..Default::default()
})
);
let request_body = body_schema.map(|schema| Item(OARequestBody { let request_body = body_schema.map(|schema| {
description: None, Item(OARequestBody {
content: Self::schema_to_content(supported_types.or_all_types(), schema), description: None,
required: true content: Self::schema_to_content(supported_types.or_all_types(), schema),
})); required: true
})
});
let mut security = Vec::new(); let mut security = Vec::new();
if requires_auth if requires_auth {
{
let mut sec = IndexMap::new(); let mut sec = IndexMap::new();
sec.insert(SECURITY_NAME.to_owned(), Vec::new()); sec.insert(SECURITY_NAME.to_owned(), Vec::new());
security.push(sec); security.push(sec);
@ -187,16 +175,13 @@ impl<'a> OperationDescription<'a>
} }
} }
#[cfg(test)] #[cfg(test)]
mod test mod test {
{
use crate::{OpenapiType, ResourceResult};
use super::*; use super::*;
use crate::{OpenapiType, ResourceResult};
#[test] #[test]
fn no_content_schema_to_content() fn no_content_schema_to_content() {
{
let types = NoContent::accepted_types(); let types = NoContent::accepted_types();
let schema = <NoContent as OpenapiType>::schema(); let schema = <NoContent as OpenapiType>::schema();
let content = OperationDescription::schema_to_content(types.or_all_types(), Item(schema.into_schema())); let content = OperationDescription::schema_to_content(types.or_all_types(), Item(schema.into_schema()));
@ -204,8 +189,7 @@ mod test
} }
#[test] #[test]
fn raw_schema_to_content() fn raw_schema_to_content() {
{
let types = Raw::<&str>::accepted_types(); let types = Raw::<&str>::accepted_types();
let schema = <Raw<&str> as OpenapiType>::schema(); let schema = <Raw<&str> as OpenapiType>::schema();
let content = OperationDescription::schema_to_content(types.or_all_types(), Item(schema.into_schema())); let content = OperationDescription::schema_to_content(types.or_all_types(), Item(schema.into_schema()));

View file

@ -1,40 +1,30 @@
use crate::{
resource::*,
routing::*,
OpenapiType,
};
use super::{builder::OpenapiBuilder, handler::OpenapiHandler, operation::OperationDescription}; use super::{builder::OpenapiBuilder, handler::OpenapiHandler, operation::OperationDescription};
use gotham::{ use crate::{resource::*, routing::*, OpenapiType};
pipeline::chain::PipelineHandleChain, use gotham::{pipeline::chain::PipelineHandleChain, router::builder::*};
router::builder::*
};
use std::panic::RefUnwindSafe; use std::panic::RefUnwindSafe;
/// This trait adds the `get_openapi` method to an OpenAPI-aware router. /// This trait adds the `get_openapi` method to an OpenAPI-aware router.
pub trait GetOpenapi pub trait GetOpenapi {
{ fn get_openapi(&mut self, path: &str);
fn get_openapi(&mut self, path : &str);
} }
#[derive(Debug)] #[derive(Debug)]
pub struct OpenapiRouter<'a, D> pub struct OpenapiRouter<'a, D> {
{ pub(crate) router: &'a mut D,
pub(crate) router : &'a mut D, pub(crate) scope: Option<&'a str>,
pub(crate) scope : Option<&'a str>, pub(crate) openapi_builder: &'a mut OpenapiBuilder
pub(crate) openapi_builder : &'a mut OpenapiBuilder
} }
macro_rules! implOpenapiRouter { macro_rules! implOpenapiRouter {
($implType:ident) => { ($implType:ident) => {
impl<'a, 'b, C, P> OpenapiRouter<'a, $implType<'b, C, P>> impl<'a, 'b, C, P> OpenapiRouter<'a, $implType<'b, C, P>>
where where
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static, C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
P : RefUnwindSafe + Send + Sync + 'static P: RefUnwindSafe + Send + Sync + 'static
{ {
pub fn scope<F>(&mut self, path : &str, callback : F) pub fn scope<F>(&mut self, path: &str, callback: F)
where where
F : FnOnce(&mut OpenapiRouter<'_, ScopeBuilder<'_, C, P>>) F: FnOnce(&mut OpenapiRouter<'_, ScopeBuilder<'_, C, P>>)
{ {
let mut openapi_builder = self.openapi_builder.clone(); let mut openapi_builder = self.openapi_builder.clone();
let new_scope = self.scope.map(|scope| format!("{}/{}", scope, path).replace("//", "/")); let new_scope = self.scope.map(|scope| format!("{}/{}", scope, path).replace("//", "/"));
@ -51,33 +41,32 @@ macro_rules! implOpenapiRouter {
impl<'a, 'b, C, P> GetOpenapi for OpenapiRouter<'a, $implType<'b, C, P>> impl<'a, 'b, C, P> GetOpenapi for OpenapiRouter<'a, $implType<'b, C, P>>
where where
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static, C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
P : RefUnwindSafe + Send + Sync + 'static P: RefUnwindSafe + Send + Sync + 'static
{ {
fn get_openapi(&mut self, path : &str) fn get_openapi(&mut self, path: &str) {
{ self.router
self.router.get(path).to_new_handler(OpenapiHandler::new(self.openapi_builder.openapi.clone())); .get(path)
.to_new_handler(OpenapiHandler::new(self.openapi_builder.openapi.clone()));
} }
} }
impl<'a, 'b, C, P> DrawResources for OpenapiRouter<'a, $implType<'b, C, P>> impl<'a, 'b, C, P> DrawResources for OpenapiRouter<'a, $implType<'b, C, P>>
where where
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static, C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
P : RefUnwindSafe + Send + Sync + 'static P: RefUnwindSafe + Send + Sync + 'static
{ {
fn resource<R : Resource>(&mut self, path : &str) fn resource<R: Resource>(&mut self, path: &str) {
{
R::setup((self, path)); R::setup((self, path));
} }
} }
impl<'a, 'b, C, P> DrawResourceRoutes for (&mut OpenapiRouter<'a, $implType<'b, C, P>>, &str) impl<'a, 'b, C, P> DrawResourceRoutes for (&mut OpenapiRouter<'a, $implType<'b, C, P>>, &str)
where where
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static, C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
P : RefUnwindSafe + Send + Sync + 'static P: RefUnwindSafe + Send + Sync + 'static
{ {
fn read_all<Handler : ResourceReadAll>(&mut self) fn read_all<Handler: ResourceReadAll>(&mut self) {
{
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>(); let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1); let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1);
@ -88,67 +77,81 @@ macro_rules! implOpenapiRouter {
(&mut *(self.0).router, self.1).read_all::<Handler>() (&mut *(self.0).router, self.1).read_all::<Handler>()
} }
fn read<Handler : ResourceRead>(&mut self) fn read<Handler: ResourceRead>(&mut self) {
{
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>(); let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
let id_schema = (self.0).openapi_builder.add_schema::<Handler::ID>(); let id_schema = (self.0).openapi_builder.add_schema::<Handler::ID>();
let path = format!("{}/{}/{{id}}", self.0.scope.unwrap_or_default(), self.1); let path = format!("{}/{}/{{id}}", self.0.scope.unwrap_or_default(), self.1);
let mut item = (self.0).openapi_builder.remove_path(&path); let mut item = (self.0).openapi_builder.remove_path(&path);
item.get = Some(OperationDescription::new::<Handler>(schema).add_path_param("id", id_schema).into_operation()); item.get = Some(
OperationDescription::new::<Handler>(schema)
.add_path_param("id", id_schema)
.into_operation()
);
(self.0).openapi_builder.add_path(path, item); (self.0).openapi_builder.add_path(path, item);
(&mut *(self.0).router, self.1).read::<Handler>() (&mut *(self.0).router, self.1).read::<Handler>()
} }
fn search<Handler : ResourceSearch>(&mut self) fn search<Handler: ResourceSearch>(&mut self) {
{
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>(); let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
let path = format!("{}/{}/search", self.0.scope.unwrap_or_default(), self.1); let path = format!("{}/{}/search", self.0.scope.unwrap_or_default(), self.1);
let mut item = (self.0).openapi_builder.remove_path(&path); let mut item = (self.0).openapi_builder.remove_path(&path);
item.get = Some(OperationDescription::new::<Handler>(schema).with_query_params(Handler::Query::schema()).into_operation()); item.get = Some(
OperationDescription::new::<Handler>(schema)
.with_query_params(Handler::Query::schema())
.into_operation()
);
(self.0).openapi_builder.add_path(path, item); (self.0).openapi_builder.add_path(path, item);
(&mut *(self.0).router, self.1).search::<Handler>() (&mut *(self.0).router, self.1).search::<Handler>()
} }
fn create<Handler : ResourceCreate>(&mut self) fn create<Handler: ResourceCreate>(&mut self)
where where
Handler::Res : 'static, Handler::Res: 'static,
Handler::Body : 'static Handler::Body: 'static
{ {
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>(); let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
let body_schema = (self.0).openapi_builder.add_schema::<Handler::Body>(); let body_schema = (self.0).openapi_builder.add_schema::<Handler::Body>();
let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1); let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1);
let mut item = (self.0).openapi_builder.remove_path(&path); let mut item = (self.0).openapi_builder.remove_path(&path);
item.post = Some(OperationDescription::new::<Handler>(schema).with_body::<Handler::Body>(body_schema).into_operation()); item.post = Some(
OperationDescription::new::<Handler>(schema)
.with_body::<Handler::Body>(body_schema)
.into_operation()
);
(self.0).openapi_builder.add_path(path, item); (self.0).openapi_builder.add_path(path, item);
(&mut *(self.0).router, self.1).create::<Handler>() (&mut *(self.0).router, self.1).create::<Handler>()
} }
fn change_all<Handler : ResourceChangeAll>(&mut self) fn change_all<Handler: ResourceChangeAll>(&mut self)
where where
Handler::Res : 'static, Handler::Res: 'static,
Handler::Body : 'static Handler::Body: 'static
{ {
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>(); let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
let body_schema = (self.0).openapi_builder.add_schema::<Handler::Body>(); let body_schema = (self.0).openapi_builder.add_schema::<Handler::Body>();
let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1); let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1);
let mut item = (self.0).openapi_builder.remove_path(&path); let mut item = (self.0).openapi_builder.remove_path(&path);
item.put = Some(OperationDescription::new::<Handler>(schema).with_body::<Handler::Body>(body_schema).into_operation()); item.put = Some(
OperationDescription::new::<Handler>(schema)
.with_body::<Handler::Body>(body_schema)
.into_operation()
);
(self.0).openapi_builder.add_path(path, item); (self.0).openapi_builder.add_path(path, item);
(&mut *(self.0).router, self.1).change_all::<Handler>() (&mut *(self.0).router, self.1).change_all::<Handler>()
} }
fn change<Handler : ResourceChange>(&mut self) fn change<Handler: ResourceChange>(&mut self)
where where
Handler::Res : 'static, Handler::Res: 'static,
Handler::Body : 'static Handler::Body: 'static
{ {
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>(); let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
let id_schema = (self.0).openapi_builder.add_schema::<Handler::ID>(); let id_schema = (self.0).openapi_builder.add_schema::<Handler::ID>();
@ -156,14 +159,18 @@ macro_rules! implOpenapiRouter {
let path = format!("{}/{}/{{id}}", self.0.scope.unwrap_or_default(), self.1); let path = format!("{}/{}/{{id}}", self.0.scope.unwrap_or_default(), self.1);
let mut item = (self.0).openapi_builder.remove_path(&path); let mut item = (self.0).openapi_builder.remove_path(&path);
item.put = Some(OperationDescription::new::<Handler>(schema).add_path_param("id", id_schema).with_body::<Handler::Body>(body_schema).into_operation()); item.put = Some(
OperationDescription::new::<Handler>(schema)
.add_path_param("id", id_schema)
.with_body::<Handler::Body>(body_schema)
.into_operation()
);
(self.0).openapi_builder.add_path(path, item); (self.0).openapi_builder.add_path(path, item);
(&mut *(self.0).router, self.1).change::<Handler>() (&mut *(self.0).router, self.1).change::<Handler>()
} }
fn remove_all<Handler : ResourceRemoveAll>(&mut self) fn remove_all<Handler: ResourceRemoveAll>(&mut self) {
{
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>(); let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1); let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1);
@ -174,21 +181,23 @@ macro_rules! implOpenapiRouter {
(&mut *(self.0).router, self.1).remove_all::<Handler>() (&mut *(self.0).router, self.1).remove_all::<Handler>()
} }
fn remove<Handler : ResourceRemove>(&mut self) fn remove<Handler: ResourceRemove>(&mut self) {
{
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>(); let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
let id_schema = (self.0).openapi_builder.add_schema::<Handler::ID>(); let id_schema = (self.0).openapi_builder.add_schema::<Handler::ID>();
let path = format!("{}/{}/{{id}}", self.0.scope.unwrap_or_default(), self.1); let path = format!("{}/{}/{{id}}", self.0.scope.unwrap_or_default(), self.1);
let mut item = (self.0).openapi_builder.remove_path(&path); let mut item = (self.0).openapi_builder.remove_path(&path);
item.delete = Some(OperationDescription::new::<Handler>(schema).add_path_param("id", id_schema).into_operation()); item.delete = Some(
OperationDescription::new::<Handler>(schema)
.add_path_param("id", id_schema)
.into_operation()
);
(self.0).openapi_builder.add_path(path, item); (self.0).openapi_builder.add_path(path, item);
(&mut *(self.0).router, self.1).remove::<Handler>() (&mut *(self.0).router, self.1).remove::<Handler>()
} }
} }
};
}
} }
implOpenapiRouter!(RouterBuilder); implOpenapiRouter!(RouterBuilder);

View file

@ -1,18 +1,17 @@
#[cfg(feature = "chrono")] #[cfg(feature = "chrono")]
use chrono::{ use chrono::{Date, DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, Utc};
Date, DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, Utc
};
use indexmap::IndexMap; use indexmap::IndexMap;
use openapiv3::{ use openapiv3::{
AdditionalProperties, ArrayType, IntegerType, NumberFormat, NumberType, ObjectType, ReferenceOr::Item, AdditionalProperties, ArrayType, IntegerType, NumberFormat, NumberType, ObjectType,
ReferenceOr::Reference, Schema, SchemaData, SchemaKind, StringType, Type, VariantOrUnknownOrEmpty ReferenceOr::{Item, Reference},
Schema, SchemaData, SchemaKind, StringType, Type, VariantOrUnknownOrEmpty
}; };
#[cfg(feature = "uuid")]
use uuid::Uuid;
use std::{ use std::{
collections::{BTreeSet, HashMap, HashSet}, collections::{BTreeSet, HashMap, HashSet},
hash::BuildHasher hash::BuildHasher
}; };
#[cfg(feature = "uuid")]
use uuid::Uuid;
/** /**
This struct needs to be available for every type that can be part of an OpenAPI Spec. It is This struct needs to be available for every type that can be part of an OpenAPI Spec. It is
@ -22,26 +21,23 @@ for your type, simply derive from [`OpenapiType`].
[`OpenapiType`]: trait.OpenapiType.html [`OpenapiType`]: trait.OpenapiType.html
*/ */
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct OpenapiSchema pub struct OpenapiSchema {
{
/// The name of this schema. If it is None, the schema will be inlined. /// The name of this schema. If it is None, the schema will be inlined.
pub name : Option<String>, pub name: Option<String>,
/// Whether this particular schema is nullable. Note that there is no guarantee that this will /// Whether this particular schema is nullable. Note that there is no guarantee that this will
/// make it into the final specification, it might just be interpreted as a hint to make it /// make it into the final specification, it might just be interpreted as a hint to make it
/// an optional parameter. /// an optional parameter.
pub nullable : bool, pub nullable: bool,
/// The actual OpenAPI schema. /// The actual OpenAPI schema.
pub schema : SchemaKind, pub schema: SchemaKind,
/// Other schemas that this schema depends on. They will be included in the final OpenAPI Spec /// Other schemas that this schema depends on. They will be included in the final OpenAPI Spec
/// along with this schema. /// along with this schema.
pub dependencies : IndexMap<String, OpenapiSchema> pub dependencies: IndexMap<String, OpenapiSchema>
} }
impl OpenapiSchema impl OpenapiSchema {
{
/// Create a new schema that has no name. /// Create a new schema that has no name.
pub fn new(schema : SchemaKind) -> Self pub fn new(schema: SchemaKind) -> Self {
{
Self { Self {
name: None, name: None,
nullable: false, nullable: false,
@ -51,8 +47,7 @@ impl OpenapiSchema
} }
/// Convert this schema to an `openapiv3::Schema` that can be serialized to the OpenAPI Spec. /// Convert this schema to an `openapiv3::Schema` that can be serialized to the OpenAPI Spec.
pub fn into_schema(self) -> Schema pub fn into_schema(self) -> Schema {
{
Schema { Schema {
schema_data: SchemaData { schema_data: SchemaData {
nullable: self.nullable, nullable: self.nullable,
@ -80,15 +75,12 @@ struct MyResponse {
[`OpenapiSchema`]: struct.OpenapiSchema.html [`OpenapiSchema`]: struct.OpenapiSchema.html
*/ */
pub trait OpenapiType pub trait OpenapiType {
{
fn schema() -> OpenapiSchema; fn schema() -> OpenapiSchema;
} }
impl OpenapiType for () impl OpenapiType for () {
{ fn schema() -> OpenapiSchema {
fn schema() -> OpenapiSchema
{
OpenapiSchema::new(SchemaKind::Type(Type::Object(ObjectType { OpenapiSchema::new(SchemaKind::Type(Type::Object(ObjectType {
additional_properties: Some(AdditionalProperties::Any(false)), additional_properties: Some(AdditionalProperties::Any(false)),
..Default::default() ..Default::default()
@ -96,11 +88,9 @@ impl OpenapiType for ()
} }
} }
impl OpenapiType for bool impl OpenapiType for bool {
{ fn schema() -> OpenapiSchema {
fn schema() -> OpenapiSchema OpenapiSchema::new(SchemaKind::Type(Type::Boolean {}))
{
OpenapiSchema::new(SchemaKind::Type(Type::Boolean{}))
} }
} }
@ -231,20 +221,26 @@ str_types!(String, &str);
#[cfg(feature = "chrono")] #[cfg(feature = "chrono")]
str_types!(format = Date, Date<FixedOffset>, Date<Local>, Date<Utc>, NaiveDate); str_types!(format = Date, Date<FixedOffset>, Date<Local>, Date<Utc>, NaiveDate);
#[cfg(feature = "chrono")] #[cfg(feature = "chrono")]
str_types!(format = DateTime, DateTime<FixedOffset>, DateTime<Local>, DateTime<Utc>, NaiveDateTime); str_types!(
format = DateTime,
DateTime<FixedOffset>,
DateTime<Local>,
DateTime<Utc>,
NaiveDateTime
);
#[cfg(feature = "uuid")] #[cfg(feature = "uuid")]
str_types!(format_str = "uuid", Uuid); str_types!(format_str = "uuid", Uuid);
impl<T : OpenapiType> OpenapiType for Option<T> impl<T: OpenapiType> OpenapiType for Option<T> {
{ fn schema() -> OpenapiSchema {
fn schema() -> OpenapiSchema
{
let schema = T::schema(); let schema = T::schema();
let mut dependencies = schema.dependencies.clone(); let mut dependencies = schema.dependencies.clone();
let schema = match schema.name.clone() { let schema = match schema.name.clone() {
Some(name) => { Some(name) => {
let reference = Reference { reference: format!("#/components/schemas/{}", name) }; let reference = Reference {
reference: format!("#/components/schemas/{}", name)
};
dependencies.insert(name, schema); dependencies.insert(name, schema);
SchemaKind::AllOf { all_of: vec![reference] } SchemaKind::AllOf { all_of: vec![reference] }
}, },
@ -260,16 +256,16 @@ impl<T : OpenapiType> OpenapiType for Option<T>
} }
} }
impl<T : OpenapiType> OpenapiType for Vec<T> impl<T: OpenapiType> OpenapiType for Vec<T> {
{ fn schema() -> OpenapiSchema {
fn schema() -> OpenapiSchema
{
let schema = T::schema(); let schema = T::schema();
let mut dependencies = schema.dependencies.clone(); let mut dependencies = schema.dependencies.clone();
let items = match schema.name.clone() { let items = match schema.name.clone() {
Some(name) => { Some(name) => {
let reference = Reference { reference: format!("#/components/schemas/{}", name) }; let reference = Reference {
reference: format!("#/components/schemas/{}", name)
};
dependencies.insert(name, schema); dependencies.insert(name, schema);
reference reference
}, },
@ -290,32 +286,28 @@ impl<T : OpenapiType> OpenapiType for Vec<T>
} }
} }
impl<T : OpenapiType> OpenapiType for BTreeSet<T> impl<T: OpenapiType> OpenapiType for BTreeSet<T> {
{ fn schema() -> OpenapiSchema {
fn schema() -> OpenapiSchema
{
<Vec<T> as OpenapiType>::schema() <Vec<T> as OpenapiType>::schema()
} }
} }
impl<T : OpenapiType, S : BuildHasher> OpenapiType for HashSet<T, S> impl<T: OpenapiType, S: BuildHasher> OpenapiType for HashSet<T, S> {
{ fn schema() -> OpenapiSchema {
fn schema() -> OpenapiSchema
{
<Vec<T> as OpenapiType>::schema() <Vec<T> as OpenapiType>::schema()
} }
} }
impl<K, T : OpenapiType, S : BuildHasher> OpenapiType for HashMap<K, T, S> impl<K, T: OpenapiType, S: BuildHasher> OpenapiType for HashMap<K, T, S> {
{ fn schema() -> OpenapiSchema {
fn schema() -> OpenapiSchema
{
let schema = T::schema(); let schema = T::schema();
let mut dependencies = schema.dependencies.clone(); let mut dependencies = schema.dependencies.clone();
let items = Box::new(match schema.name.clone() { let items = Box::new(match schema.name.clone() {
Some(name) => { Some(name) => {
let reference = Reference { reference: format!("#/components/schemas/{}", name) }; let reference = Reference {
reference: format!("#/components/schemas/{}", name)
};
dependencies.insert(name, schema); dependencies.insert(name, schema);
reference reference
}, },
@ -334,10 +326,8 @@ impl<K, T : OpenapiType, S : BuildHasher> OpenapiType for HashMap<K, T, S>
} }
} }
impl OpenapiType for serde_json::Value impl OpenapiType for serde_json::Value {
{ fn schema() -> OpenapiSchema {
fn schema() -> OpenapiSchema
{
OpenapiSchema { OpenapiSchema {
nullable: true, nullable: true,
name: None, name: None,
@ -347,10 +337,8 @@ impl OpenapiType for serde_json::Value
} }
} }
#[cfg(test)] #[cfg(test)]
mod test mod test {
{
use super::*; use super::*;
use serde_json::Value; use serde_json::Value;

View file

@ -1,22 +1,14 @@
use crate::{DrawResourceRoutes, RequestBody, ResourceID, ResourceResult, ResourceType}; use crate::{DrawResourceRoutes, RequestBody, ResourceID, ResourceResult, ResourceType};
use gotham::{ use gotham::{extractor::QueryStringExtractor, hyper::Body, state::State};
extractor::QueryStringExtractor, use std::{future::Future, pin::Pin};
hyper::Body,
state::State
};
use std::{
future::Future,
pin::Pin
};
/// This trait must be implemented for every resource. It allows you to register the different /// This trait must be implemented for every resource. It allows you to register the different
/// methods that can be handled by this resource to be registered with the underlying router. /// methods that can be handled by this resource to be registered with the underlying router.
/// ///
/// It is not recommended to implement this yourself, rather just use `#[derive(Resource)]`. /// It is not recommended to implement this yourself, rather just use `#[derive(Resource)]`.
pub trait Resource pub trait Resource {
{
/// Register all methods handled by this resource with the underlying router. /// Register all methods handled by this resource with the underlying router.
fn setup<D : DrawResourceRoutes>(route : D); fn setup<D: DrawResourceRoutes>(route: D);
} }
/// A common trait for every resource method. It defines the return type as well as some general /// A common trait for every resource method. It defines the return type as well as some general
@ -25,94 +17,83 @@ pub trait Resource
/// It is not recommended to implement this yourself. Rather, just write your handler method and /// It is not recommended to implement this yourself. Rather, just write your handler method and
/// annotate it with `#[<method>(YourResource)]`, where `<method>` is one of the supported /// annotate it with `#[<method>(YourResource)]`, where `<method>` is one of the supported
/// resource methods. /// resource methods.
pub trait ResourceMethod pub trait ResourceMethod {
{ type Res: ResourceResult + Send + 'static;
type Res : ResourceResult + Send + 'static;
#[cfg(feature = "openapi")] #[cfg(feature = "openapi")]
fn operation_id() -> Option<String> fn operation_id() -> Option<String> {
{
None None
} }
fn wants_auth() -> bool fn wants_auth() -> bool {
{
false false
} }
} }
/// The read_all [`ResourceMethod`](trait.ResourceMethod.html). /// The read_all [`ResourceMethod`](trait.ResourceMethod.html).
pub trait ResourceReadAll : ResourceMethod pub trait ResourceReadAll: ResourceMethod {
{
/// Handle a GET request on the Resource root. /// Handle a GET request on the Resource root.
fn read_all(state : State) -> Pin<Box<dyn Future<Output = (State, Self::Res)> + Send>>; fn read_all(state: State) -> Pin<Box<dyn Future<Output = (State, Self::Res)> + Send>>;
} }
/// The read [`ResourceMethod`](trait.ResourceMethod.html). /// The read [`ResourceMethod`](trait.ResourceMethod.html).
pub trait ResourceRead : ResourceMethod pub trait ResourceRead: ResourceMethod {
{
/// The ID type to be parsed from the request path. /// The ID type to be parsed from the request path.
type ID : ResourceID + 'static; type ID: ResourceID + 'static;
/// Handle a GET request on the Resource with an id. /// Handle a GET request on the Resource with an id.
fn read(state : State, id : Self::ID) -> Pin<Box<dyn Future<Output = (State, Self::Res)> + Send>>; fn read(state: State, id: Self::ID) -> Pin<Box<dyn Future<Output = (State, Self::Res)> + Send>>;
} }
/// The search [`ResourceMethod`](trait.ResourceMethod.html). /// The search [`ResourceMethod`](trait.ResourceMethod.html).
pub trait ResourceSearch : ResourceMethod pub trait ResourceSearch: ResourceMethod {
{
/// The Query type to be parsed from the request parameters. /// The Query type to be parsed from the request parameters.
type Query : ResourceType + QueryStringExtractor<Body> + Sync; type Query: ResourceType + QueryStringExtractor<Body> + Sync;
/// Handle a GET request on the Resource with additional search parameters. /// Handle a GET request on the Resource with additional search parameters.
fn search(state : State, query : Self::Query) -> Pin<Box<dyn Future<Output = (State, Self::Res)> + Send>>; fn search(state: State, query: Self::Query) -> Pin<Box<dyn Future<Output = (State, Self::Res)> + Send>>;
} }
/// The create [`ResourceMethod`](trait.ResourceMethod.html). /// The create [`ResourceMethod`](trait.ResourceMethod.html).
pub trait ResourceCreate : ResourceMethod pub trait ResourceCreate: ResourceMethod {
{
/// The Body type to be parsed from the request body. /// The Body type to be parsed from the request body.
type Body : RequestBody; type Body: RequestBody;
/// Handle a POST request on the Resource root. /// Handle a POST request on the Resource root.
fn create(state : State, body : Self::Body) -> Pin<Box<dyn Future<Output = (State, Self::Res)> + Send>>; fn create(state: State, body: Self::Body) -> Pin<Box<dyn Future<Output = (State, Self::Res)> + Send>>;
} }
/// The change_all [`ResourceMethod`](trait.ResourceMethod.html). /// The change_all [`ResourceMethod`](trait.ResourceMethod.html).
pub trait ResourceChangeAll : ResourceMethod pub trait ResourceChangeAll: ResourceMethod {
{
/// The Body type to be parsed from the request body. /// The Body type to be parsed from the request body.
type Body : RequestBody; type Body: RequestBody;
/// Handle a PUT request on the Resource root. /// Handle a PUT request on the Resource root.
fn change_all(state : State, body : Self::Body) -> Pin<Box<dyn Future<Output = (State, Self::Res)> + Send>>; fn change_all(state: State, body: Self::Body) -> Pin<Box<dyn Future<Output = (State, Self::Res)> + Send>>;
} }
/// The change [`ResourceMethod`](trait.ResourceMethod.html). /// The change [`ResourceMethod`](trait.ResourceMethod.html).
pub trait ResourceChange : ResourceMethod pub trait ResourceChange: ResourceMethod {
{
/// The Body type to be parsed from the request body. /// The Body type to be parsed from the request body.
type Body : RequestBody; type Body: RequestBody;
/// The ID type to be parsed from the request path. /// The ID type to be parsed from the request path.
type ID : ResourceID + 'static; type ID: ResourceID + 'static;
/// Handle a PUT request on the Resource with an id. /// Handle a PUT request on the Resource with an id.
fn change(state : State, id : Self::ID, body : Self::Body) -> Pin<Box<dyn Future<Output = (State, Self::Res)> + Send>>; fn change(state: State, id: Self::ID, body: Self::Body) -> Pin<Box<dyn Future<Output = (State, Self::Res)> + Send>>;
} }
/// The remove_all [`ResourceMethod`](trait.ResourceMethod.html). /// The remove_all [`ResourceMethod`](trait.ResourceMethod.html).
pub trait ResourceRemoveAll : ResourceMethod pub trait ResourceRemoveAll: ResourceMethod {
{
/// Handle a DELETE request on the Resource root. /// Handle a DELETE request on the Resource root.
fn remove_all(state : State) -> Pin<Box<dyn Future<Output = (State, Self::Res)> + Send>>; fn remove_all(state: State) -> Pin<Box<dyn Future<Output = (State, Self::Res)> + Send>>;
} }
/// The remove [`ResourceMethod`](trait.ResourceMethod.html). /// The remove [`ResourceMethod`](trait.ResourceMethod.html).
pub trait ResourceRemove : ResourceMethod pub trait ResourceRemove: ResourceMethod {
{
/// The ID type to be parsed from the request path. /// The ID type to be parsed from the request path.
type ID : ResourceID + 'static; type ID: ResourceID + 'static;
/// Handle a DELETE request on the Resource with an id. /// Handle a DELETE request on the Resource with an id.
fn remove(state : State, id : Self::ID) -> Pin<Box<dyn Future<Output = (State, Self::Res)> + Send>>; fn remove(state: State, id: Self::ID) -> Pin<Box<dyn Future<Output = (State, Self::Res)> + Send>>;
} }

View file

@ -3,18 +3,15 @@ use mime::{Mime, APPLICATION_JSON};
/// A response, used to create the final gotham response from. /// A response, used to create the final gotham response from.
#[derive(Debug)] #[derive(Debug)]
pub struct Response pub struct Response {
{ pub status: StatusCode,
pub status : StatusCode, pub body: Body,
pub body : Body, pub mime: Option<Mime>
pub mime : Option<Mime>
} }
impl Response impl Response {
{
/// Create a new `Response` from raw data. /// Create a new `Response` from raw data.
pub fn new<B : Into<Body>>(status : StatusCode, body : B, mime : Option<Mime>) -> Self pub fn new<B: Into<Body>>(status: StatusCode, body: B, mime: Option<Mime>) -> Self {
{
Self { Self {
status, status,
body: body.into(), body: body.into(),
@ -23,8 +20,7 @@ impl Response
} }
/// Create a `Response` with mime type json from already serialized data. /// Create a `Response` with mime type json from already serialized data.
pub fn json<B : Into<Body>>(status : StatusCode, body : B) -> Self pub fn json<B: Into<Body>>(status: StatusCode, body: B) -> Self {
{
Self { Self {
status, status,
body: body.into(), body: body.into(),
@ -33,8 +29,7 @@ impl Response
} }
/// Create a _204 No Content_ `Response`. /// Create a _204 No Content_ `Response`.
pub fn no_content() -> Self pub fn no_content() -> Self {
{
Self { Self {
status: StatusCode::NO_CONTENT, status: StatusCode::NO_CONTENT,
body: Body::empty(), body: Body::empty(),
@ -43,8 +38,7 @@ impl Response
} }
/// Create an empty _403 Forbidden_ `Response`. /// Create an empty _403 Forbidden_ `Response`.
pub fn forbidden() -> Self pub fn forbidden() -> Self {
{
Self { Self {
status: StatusCode::FORBIDDEN, status: StatusCode::FORBIDDEN,
body: Body::empty(), body: Body::empty(),
@ -53,12 +47,11 @@ impl Response
} }
#[cfg(test)] #[cfg(test)]
pub(crate) fn full_body(mut self) -> Result<Vec<u8>, <Body as gotham::hyper::body::HttpBody>::Error> pub(crate) fn full_body(mut self) -> Result<Vec<u8>, <Body as gotham::hyper::body::HttpBody>::Error> {
{
use futures_executor::block_on; use futures_executor::block_on;
use gotham::hyper::body::to_bytes; use gotham::hyper::body::to_bytes;
let bytes : &[u8] = &block_on(to_bytes(&mut self.body))?; let bytes: &[u8] = &block_on(to_bytes(&mut self.body))?;
Ok(bytes.to_vec()) Ok(bytes.to_vec())
} }
} }

View file

@ -1,6 +1,5 @@
use gotham_restful_derive::ResourceError; use gotham_restful_derive::ResourceError;
/** /**
This is an error type that always yields a _403 Forbidden_ response. This type is best used in This is an error type that always yields a _403 Forbidden_ response. This type is best used in
combination with [`AuthSuccess`] or [`AuthResult`]. combination with [`AuthSuccess`] or [`AuthResult`].
@ -9,8 +8,7 @@ combination with [`AuthSuccess`] or [`AuthResult`].
[`AuthResult`]: type.AuthResult.html [`AuthResult`]: type.AuthResult.html
*/ */
#[derive(Debug, Clone, Copy, ResourceError)] #[derive(Debug, Clone, Copy, ResourceError)]
pub enum AuthError pub enum AuthError {
{
#[status(FORBIDDEN)] #[status(FORBIDDEN)]
#[display("Forbidden")] #[display("Forbidden")]
Forbidden Forbidden
@ -57,8 +55,7 @@ error, or delegates to another error type. This type is best used with [`AuthRes
[`AuthResult`]: type.AuthResult.html [`AuthResult`]: type.AuthResult.html
*/ */
#[derive(Debug, ResourceError)] #[derive(Debug, ResourceError)]
pub enum AuthErrorOrOther<E> pub enum AuthErrorOrOther<E> {
{
#[status(FORBIDDEN)] #[status(FORBIDDEN)]
#[display("Forbidden")] #[display("Forbidden")]
Forbidden, Forbidden,
@ -67,10 +64,8 @@ pub enum AuthErrorOrOther<E>
Other(E) Other(E)
} }
impl<E> From<AuthError> for AuthErrorOrOther<E> impl<E> From<AuthError> for AuthErrorOrOther<E> {
{ fn from(err: AuthError) -> Self {
fn from(err : AuthError) -> Self
{
match err { match err {
AuthError::Forbidden => Self::Forbidden AuthError::Forbidden => Self::Forbidden
} }
@ -80,10 +75,9 @@ impl<E> From<AuthError> for AuthErrorOrOther<E>
impl<E, F> From<F> for AuthErrorOrOther<E> impl<E, F> From<F> for AuthErrorOrOther<E>
where where
// TODO https://gitlab.com/msrd0/gotham-restful/-/issues/20 // TODO https://gitlab.com/msrd0/gotham-restful/-/issues/20
F : std::error::Error + Into<E> F: std::error::Error + Into<E>
{ {
fn from(err : F) -> Self fn from(err: F) -> Self {
{
Self::Other(err.into()) Self::Other(err.into())
} }
} }

View file

@ -1,13 +1,13 @@
use crate::Response;
#[cfg(feature = "openapi")] #[cfg(feature = "openapi")]
use crate::OpenapiSchema; use crate::OpenapiSchema;
use crate::Response;
use futures_util::future::FutureExt; use futures_util::future::FutureExt;
use mime::{Mime, STAR_STAR}; use mime::{Mime, STAR_STAR};
use serde::Serialize; use serde::Serialize;
use std::{ use std::{
error::Error, error::Error,
future::Future,
fmt::{Debug, Display}, fmt::{Debug, Display},
future::Future,
pin::Pin pin::Pin
}; };
@ -27,33 +27,26 @@ pub use result::IntoResponseError;
mod success; mod success;
pub use success::Success; pub use success::Success;
pub(crate) trait OrAllTypes {
pub(crate) trait OrAllTypes
{
fn or_all_types(self) -> Vec<Mime>; fn or_all_types(self) -> Vec<Mime>;
} }
impl OrAllTypes for Option<Vec<Mime>> impl OrAllTypes for Option<Vec<Mime>> {
{ fn or_all_types(self) -> Vec<Mime> {
fn or_all_types(self) -> Vec<Mime>
{
self.unwrap_or_else(|| vec![STAR_STAR]) self.unwrap_or_else(|| vec![STAR_STAR])
} }
} }
/// A trait provided to convert a resource's result to json. /// A trait provided to convert a resource's result to json.
pub trait ResourceResult pub trait ResourceResult {
{ type Err: Error + Send + Sync + 'static;
type Err : Error + Send + 'static;
/// Turn this into a response that can be returned to the browser. This api will likely /// Turn this into a response that can be returned to the browser. This api will likely
/// change in the future. /// change in the future.
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, Self::Err>> + Send>>; fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, Self::Err>> + Send>>;
/// Return a list of supported mime types. /// Return a list of supported mime types.
fn accepted_types() -> Option<Vec<Mime>> fn accepted_types() -> Option<Vec<Mime>> {
{
None None
} }
@ -61,33 +54,27 @@ pub trait ResourceResult
fn schema() -> OpenapiSchema; fn schema() -> OpenapiSchema;
#[cfg(feature = "openapi")] #[cfg(feature = "openapi")]
fn default_status() -> crate::StatusCode fn default_status() -> crate::StatusCode {
{
crate::StatusCode::OK crate::StatusCode::OK
} }
} }
#[cfg(feature = "openapi")] #[cfg(feature = "openapi")]
impl<Res : ResourceResult> crate::OpenapiType for Res impl<Res: ResourceResult> crate::OpenapiType for Res {
{ fn schema() -> OpenapiSchema {
fn schema() -> OpenapiSchema
{
Self::schema() Self::schema()
} }
} }
/// The default json returned on an 500 Internal Server Error. /// The default json returned on an 500 Internal Server Error.
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
pub(crate) struct ResourceError pub(crate) struct ResourceError {
{ error: bool,
error : bool, message: String
message : String
} }
impl<T : ToString> From<T> for ResourceError impl<T: ToString> From<T> for ResourceError {
{ fn from(message: T) -> Self {
fn from(message : T) -> Self
{
Self { Self {
error: true, error: true,
message: message.to_string() message: message.to_string()
@ -95,27 +82,26 @@ impl<T : ToString> From<T> for ResourceError
} }
} }
fn into_response_helper<Err, F>(create_response : F) -> Pin<Box<dyn Future<Output = Result<Response, Err>> + Send>> fn into_response_helper<Err, F>(create_response: F) -> Pin<Box<dyn Future<Output = Result<Response, Err>> + Send>>
where where
Err : Send + 'static, Err: Send + 'static,
F : FnOnce() -> Result<Response, Err> F: FnOnce() -> Result<Response, Err>
{ {
let res = create_response(); let res = create_response();
async move { res }.boxed() async move { res }.boxed()
} }
#[cfg(feature = "errorlog")] #[cfg(feature = "errorlog")]
fn errorlog<E : Display>(e : E) fn errorlog<E: Display>(e: E) {
{
error!("The handler encountered an error: {}", e); error!("The handler encountered an error: {}", e);
} }
#[cfg(not(feature = "errorlog"))] #[cfg(not(feature = "errorlog"))]
fn errorlog<E>(_e : E) {} fn errorlog<E>(_e: E) {}
fn handle_error<E>(e : E) -> Pin<Box<dyn Future<Output = Result<Response, E::Err>> + Send>> fn handle_error<E>(e: E) -> Pin<Box<dyn Future<Output = Result<Response, E::Err>> + Send>>
where where
E : Display + IntoResponseError E: Display + IntoResponseError
{ {
into_response_helper(|| { into_response_helper(|| {
errorlog(&e); errorlog(&e);
@ -123,52 +109,41 @@ where
}) })
} }
impl<Res> ResourceResult for Pin<Box<dyn Future<Output = Res> + Send>> impl<Res> ResourceResult for Pin<Box<dyn Future<Output = Res> + Send>>
where where
Res : ResourceResult + 'static Res: ResourceResult + 'static
{ {
type Err = Res::Err; type Err = Res::Err;
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, Self::Err>> + Send>> fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, Self::Err>> + Send>> {
{ self.then(|result| result.into_response()).boxed()
self.then(|result| {
result.into_response()
}).boxed()
} }
fn accepted_types() -> Option<Vec<Mime>> fn accepted_types() -> Option<Vec<Mime>> {
{
Res::accepted_types() Res::accepted_types()
} }
#[cfg(feature = "openapi")] #[cfg(feature = "openapi")]
fn schema() -> OpenapiSchema fn schema() -> OpenapiSchema {
{
Res::schema() Res::schema()
} }
#[cfg(feature = "openapi")] #[cfg(feature = "openapi")]
fn default_status() -> crate::StatusCode fn default_status() -> crate::StatusCode {
{
Res::default_status() Res::default_status()
} }
} }
#[cfg(test)] #[cfg(test)]
mod test mod test {
{
use super::*; use super::*;
use futures_executor::block_on; use futures_executor::block_on;
use thiserror::Error; use thiserror::Error;
#[derive(Debug, Default, Deserialize, Serialize)] #[derive(Debug, Default, Deserialize, Serialize)]
#[cfg_attr(feature = "openapi", derive(crate::OpenapiType))] #[cfg_attr(feature = "openapi", derive(crate::OpenapiType))]
struct Msg struct Msg {
{ msg: String
msg : String
} }
#[derive(Debug, Default, Error)] #[derive(Debug, Default, Error)]
@ -176,8 +151,7 @@ mod test
struct MsgError; struct MsgError;
#[test] #[test]
fn result_from_future() fn result_from_future() {
{
let nc = NoContent::default(); let nc = NoContent::default();
let res = block_on(nc.into_response()).unwrap(); let res = block_on(nc.into_response()).unwrap();

View file

@ -1,14 +1,10 @@
use super::{ResourceResult, handle_error}; use super::{handle_error, ResourceResult};
use crate::{IntoResponseError, Response}; use crate::{IntoResponseError, Response};
#[cfg(feature = "openapi")] #[cfg(feature = "openapi")]
use crate::{OpenapiSchema, OpenapiType}; use crate::{OpenapiSchema, OpenapiType};
use futures_util::{future, future::FutureExt}; use futures_util::{future, future::FutureExt};
use mime::Mime; use mime::Mime;
use std::{ use std::{fmt::Display, future::Future, pin::Pin};
fmt::Display,
future::Future,
pin::Pin
};
/** /**
This is the return type of a resource that doesn't actually return something. It will result This is the return type of a resource that doesn't actually return something. It will result
@ -35,81 +31,68 @@ fn read_all(_state: &mut State) {
#[derive(Clone, Copy, Debug, Default)] #[derive(Clone, Copy, Debug, Default)]
pub struct NoContent; pub struct NoContent;
impl From<()> for NoContent impl From<()> for NoContent {
{ fn from(_: ()) -> Self {
fn from(_ : ()) -> Self
{
Self {} Self {}
} }
} }
impl ResourceResult for NoContent impl ResourceResult for NoContent {
{
// TODO this shouldn't be a serde_json::Error // TODO this shouldn't be a serde_json::Error
type Err = serde_json::Error; // just for easier handling of `Result<NoContent, E>` type Err = serde_json::Error; // just for easier handling of `Result<NoContent, E>`
/// This will always be a _204 No Content_ together with an empty string. /// This will always be a _204 No Content_ together with an empty string.
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, Self::Err>> + Send>> fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, Self::Err>> + Send>> {
{
future::ok(Response::no_content()).boxed() future::ok(Response::no_content()).boxed()
} }
fn accepted_types() -> Option<Vec<Mime>> fn accepted_types() -> Option<Vec<Mime>> {
{
Some(Vec::new()) Some(Vec::new())
} }
/// Returns the schema of the `()` type. /// Returns the schema of the `()` type.
#[cfg(feature = "openapi")] #[cfg(feature = "openapi")]
fn schema() -> OpenapiSchema fn schema() -> OpenapiSchema {
{
<()>::schema() <()>::schema()
} }
/// This will always be a _204 No Content_ /// This will always be a _204 No Content_
#[cfg(feature = "openapi")] #[cfg(feature = "openapi")]
fn default_status() -> crate::StatusCode fn default_status() -> crate::StatusCode {
{
crate::StatusCode::NO_CONTENT crate::StatusCode::NO_CONTENT
} }
} }
impl<E> ResourceResult for Result<NoContent, E> impl<E> ResourceResult for Result<NoContent, E>
where where
E : Display + IntoResponseError<Err = serde_json::Error> E: Display + IntoResponseError<Err = serde_json::Error>
{ {
type Err = serde_json::Error; type Err = serde_json::Error;
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, serde_json::Error>> + Send>> fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, serde_json::Error>> + Send>> {
{
match self { match self {
Ok(nc) => nc.into_response(), Ok(nc) => nc.into_response(),
Err(e) => handle_error(e) Err(e) => handle_error(e)
} }
} }
fn accepted_types() -> Option<Vec<Mime>> fn accepted_types() -> Option<Vec<Mime>> {
{
NoContent::accepted_types() NoContent::accepted_types()
} }
#[cfg(feature = "openapi")] #[cfg(feature = "openapi")]
fn schema() -> OpenapiSchema fn schema() -> OpenapiSchema {
{
<NoContent as ResourceResult>::schema() <NoContent as ResourceResult>::schema()
} }
#[cfg(feature = "openapi")] #[cfg(feature = "openapi")]
fn default_status() -> crate::StatusCode fn default_status() -> crate::StatusCode {
{
NoContent::default_status() NoContent::default_status()
} }
} }
#[cfg(test)] #[cfg(test)]
mod test mod test {
{
use super::*; use super::*;
use futures_executor::block_on; use futures_executor::block_on;
use gotham::hyper::StatusCode; use gotham::hyper::StatusCode;
@ -120,8 +103,7 @@ mod test
struct MsgError; struct MsgError;
#[test] #[test]
fn no_content_has_empty_response() fn no_content_has_empty_response() {
{
let no_content = NoContent::default(); let no_content = NoContent::default();
let res = block_on(no_content.into_response()).expect("didn't expect error response"); let res = block_on(no_content.into_response()).expect("didn't expect error response");
assert_eq!(res.status, StatusCode::NO_CONTENT); assert_eq!(res.status, StatusCode::NO_CONTENT);
@ -130,9 +112,8 @@ mod test
} }
#[test] #[test]
fn no_content_result() fn no_content_result() {
{ let no_content: Result<NoContent, MsgError> = Ok(NoContent::default());
let no_content : Result<NoContent, MsgError> = Ok(NoContent::default());
let res = block_on(no_content.into_response()).expect("didn't expect error response"); let res = block_on(no_content.into_response()).expect("didn't expect error response");
assert_eq!(res.status, StatusCode::NO_CONTENT); assert_eq!(res.status, StatusCode::NO_CONTENT);
assert_eq!(res.mime, None); assert_eq!(res.mime, None);

View file

@ -1,7 +1,7 @@
use super::{IntoResponseError, ResourceResult, handle_error}; use super::{handle_error, IntoResponseError, ResourceResult};
use crate::{FromBody, RequestBody, ResourceType, Response, StatusCode};
#[cfg(feature = "openapi")] #[cfg(feature = "openapi")]
use crate::OpenapiSchema; use crate::OpenapiSchema;
use crate::{FromBody, RequestBody, ResourceType, Response, StatusCode};
use futures_core::future::Future; use futures_core::future::Future;
use futures_util::{future, future::FutureExt}; use futures_util::{future, future::FutureExt};
use gotham::hyper::body::{Body, Bytes}; use gotham::hyper::body::{Body, Bytes};
@ -9,11 +9,7 @@ use mime::Mime;
#[cfg(feature = "openapi")] #[cfg(feature = "openapi")]
use openapiv3::{SchemaKind, StringFormat, StringType, Type, VariantOrUnknownOrEmpty}; use openapiv3::{SchemaKind, StringFormat, StringType, Type, VariantOrUnknownOrEmpty};
use serde_json::error::Error as SerdeJsonError; use serde_json::error::Error as SerdeJsonError;
use std::{ use std::{convert::Infallible, fmt::Display, pin::Pin};
convert::Infallible,
fmt::Display,
pin::Pin
};
/** /**
This type can be used both as a raw request body, as well as as a raw response. However, all types This type can be used both as a raw request body, as well as as a raw response. However, all types
@ -43,44 +39,37 @@ fn create(body : Raw<Vec<u8>>) -> Raw<Vec<u8>> {
[`OpenapiType`]: trait.OpenapiType.html [`OpenapiType`]: trait.OpenapiType.html
*/ */
#[derive(Debug)] #[derive(Debug)]
pub struct Raw<T> pub struct Raw<T> {
{ pub raw: T,
pub raw : T, pub mime: Mime
pub mime : Mime
} }
impl<T> Raw<T> impl<T> Raw<T> {
{ pub fn new(raw: T, mime: Mime) -> Self {
pub fn new(raw : T, mime : Mime) -> Self
{
Self { raw, mime } Self { raw, mime }
} }
} }
impl<T, U> AsMut<U> for Raw<T> impl<T, U> AsMut<U> for Raw<T>
where where
T : AsMut<U> T: AsMut<U>
{ {
fn as_mut(&mut self) -> &mut U fn as_mut(&mut self) -> &mut U {
{
self.raw.as_mut() self.raw.as_mut()
} }
} }
impl<T, U> AsRef<U> for Raw<T> impl<T, U> AsRef<U> for Raw<T>
where where
T : AsRef<U> T: AsRef<U>
{ {
fn as_ref(&self) -> &U fn as_ref(&self) -> &U {
{
self.raw.as_ref() self.raw.as_ref()
} }
} }
impl<T : Clone> Clone for Raw<T> impl<T: Clone> Clone for Raw<T> {
{ fn clone(&self) -> Self {
fn clone(&self) -> Self
{
Self { Self {
raw: self.raw.clone(), raw: self.raw.clone(),
mime: self.mime.clone() mime: self.mime.clone()
@ -88,36 +77,28 @@ impl<T : Clone> Clone for Raw<T>
} }
} }
impl<T : for<'a> From<&'a [u8]>> FromBody for Raw<T> impl<T: for<'a> From<&'a [u8]>> FromBody for Raw<T> {
{
type Err = Infallible; type Err = Infallible;
fn from_body(body : Bytes, mime : Mime) -> Result<Self, Self::Err> fn from_body(body: Bytes, mime: Mime) -> Result<Self, Self::Err> {
{
Ok(Self::new(body.as_ref().into(), mime)) Ok(Self::new(body.as_ref().into(), mime))
} }
} }
impl<T> RequestBody for Raw<T> impl<T> RequestBody for Raw<T> where Raw<T>: FromBody + ResourceType {}
where
Raw<T> : FromBody + ResourceType
{
}
impl<T : Into<Body>> ResourceResult for Raw<T> impl<T: Into<Body>> ResourceResult for Raw<T>
where where
Self : Send Self: Send
{ {
type Err = SerdeJsonError; // just for easier handling of `Result<Raw<T>, E>` type Err = SerdeJsonError; // just for easier handling of `Result<Raw<T>, E>`
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, SerdeJsonError>> + Send>> fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, SerdeJsonError>> + Send>> {
{
future::ok(Response::new(StatusCode::OK, self.raw, Some(self.mime.clone()))).boxed() future::ok(Response::new(StatusCode::OK, self.raw, Some(self.mime.clone()))).boxed()
} }
#[cfg(feature = "openapi")] #[cfg(feature = "openapi")]
fn schema() -> OpenapiSchema fn schema() -> OpenapiSchema {
{
OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { OpenapiSchema::new(SchemaKind::Type(Type::String(StringType {
format: VariantOrUnknownOrEmpty::Item(StringFormat::Binary), format: VariantOrUnknownOrEmpty::Item(StringFormat::Binary),
..Default::default() ..Default::default()
@ -127,13 +108,12 @@ where
impl<T, E> ResourceResult for Result<Raw<T>, E> impl<T, E> ResourceResult for Result<Raw<T>, E>
where where
Raw<T> : ResourceResult, Raw<T>: ResourceResult,
E : Display + IntoResponseError<Err = <Raw<T> as ResourceResult>::Err> E: Display + IntoResponseError<Err = <Raw<T> as ResourceResult>::Err>
{ {
type Err = E::Err; type Err = E::Err;
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, E::Err>> + Send>> fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, E::Err>> + Send>> {
{
match self { match self {
Ok(raw) => raw.into_response(), Ok(raw) => raw.into_response(),
Err(e) => handle_error(e) Err(e) => handle_error(e)
@ -141,23 +121,19 @@ where
} }
#[cfg(feature = "openapi")] #[cfg(feature = "openapi")]
fn schema() -> OpenapiSchema fn schema() -> OpenapiSchema {
{
<Raw<T> as ResourceResult>::schema() <Raw<T> as ResourceResult>::schema()
} }
} }
#[cfg(test)] #[cfg(test)]
mod test mod test {
{
use super::*; use super::*;
use futures_executor::block_on; use futures_executor::block_on;
use mime::TEXT_PLAIN; use mime::TEXT_PLAIN;
#[test] #[test]
fn raw_response() fn raw_response() {
{
let msg = "Test"; let msg = "Test";
let raw = Raw::new(msg, TEXT_PLAIN); let raw = Raw::new(msg, TEXT_PLAIN);
let res = block_on(raw.into_response()).expect("didn't expect error response"); let res = block_on(raw.into_response()).expect("didn't expect error response");

View file

@ -1,67 +1,55 @@
use super::{ResourceResult, handle_error, into_response_helper}; use super::{handle_error, into_response_helper, ResourceResult};
use crate::{
result::ResourceError,
Response, ResponseBody, StatusCode
};
#[cfg(feature = "openapi")] #[cfg(feature = "openapi")]
use crate::OpenapiSchema; use crate::OpenapiSchema;
use crate::{result::ResourceError, Response, ResponseBody, StatusCode};
use futures_core::future::Future; use futures_core::future::Future;
use mime::{Mime, APPLICATION_JSON}; use mime::{Mime, APPLICATION_JSON};
use std::{ use std::{error::Error, fmt::Display, pin::Pin};
error::Error,
fmt::Display,
pin::Pin
};
pub trait IntoResponseError pub trait IntoResponseError {
{ type Err: Error + Send + 'static;
type Err : Error + Send + 'static;
fn into_response_error(self) -> Result<Response, Self::Err>; fn into_response_error(self) -> Result<Response, Self::Err>;
} }
impl<E : Error> IntoResponseError for E impl<E: Error> IntoResponseError for E {
{
type Err = serde_json::Error; type Err = serde_json::Error;
fn into_response_error(self) -> Result<Response, Self::Err> fn into_response_error(self) -> Result<Response, Self::Err> {
{ let err: ResourceError = self.into();
let err : ResourceError = self.into(); Ok(Response::json(
Ok(Response::json(StatusCode::INTERNAL_SERVER_ERROR, serde_json::to_string(&err)?)) StatusCode::INTERNAL_SERVER_ERROR,
serde_json::to_string(&err)?
))
} }
} }
impl<R, E> ResourceResult for Result<R, E> impl<R, E> ResourceResult for Result<R, E>
where where
R : ResponseBody, R: ResponseBody,
E : Display + IntoResponseError<Err = serde_json::Error> E: Display + IntoResponseError<Err = serde_json::Error>
{ {
type Err = E::Err; type Err = E::Err;
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, E::Err>> + Send>> fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, E::Err>> + Send>> {
{
match self { match self {
Ok(r) => into_response_helper(|| Ok(Response::json(StatusCode::OK, serde_json::to_string(&r)?))), Ok(r) => into_response_helper(|| Ok(Response::json(StatusCode::OK, serde_json::to_string(&r)?))),
Err(e) => handle_error(e) Err(e) => handle_error(e)
} }
} }
fn accepted_types() -> Option<Vec<Mime>> fn accepted_types() -> Option<Vec<Mime>> {
{
Some(vec![APPLICATION_JSON]) Some(vec![APPLICATION_JSON])
} }
#[cfg(feature = "openapi")] #[cfg(feature = "openapi")]
fn schema() -> OpenapiSchema fn schema() -> OpenapiSchema {
{
R::schema() R::schema()
} }
} }
#[cfg(test)] #[cfg(test)]
mod test mod test {
{
use super::*; use super::*;
use crate::result::OrAllTypes; use crate::result::OrAllTypes;
use futures_executor::block_on; use futures_executor::block_on;
@ -69,9 +57,8 @@ mod test
#[derive(Debug, Default, Deserialize, Serialize)] #[derive(Debug, Default, Deserialize, Serialize)]
#[cfg_attr(feature = "openapi", derive(crate::OpenapiType))] #[cfg_attr(feature = "openapi", derive(crate::OpenapiType))]
struct Msg struct Msg {
{ msg: String
msg : String
} }
#[derive(Debug, Default, Error)] #[derive(Debug, Default, Error)]
@ -79,9 +66,8 @@ mod test
struct MsgError; struct MsgError;
#[test] #[test]
fn result_ok() fn result_ok() {
{ let ok: Result<Msg, MsgError> = Ok(Msg::default());
let ok : Result<Msg, MsgError> = Ok(Msg::default());
let res = block_on(ok.into_response()).expect("didn't expect error response"); let res = block_on(ok.into_response()).expect("didn't expect error response");
assert_eq!(res.status, StatusCode::OK); assert_eq!(res.status, StatusCode::OK);
assert_eq!(res.mime, Some(APPLICATION_JSON)); assert_eq!(res.mime, Some(APPLICATION_JSON));
@ -89,18 +75,21 @@ mod test
} }
#[test] #[test]
fn result_err() fn result_err() {
{ let err: Result<Msg, MsgError> = Err(MsgError::default());
let err : Result<Msg, MsgError> = Err(MsgError::default());
let res = block_on(err.into_response()).expect("didn't expect error response"); let res = block_on(err.into_response()).expect("didn't expect error response");
assert_eq!(res.status, StatusCode::INTERNAL_SERVER_ERROR); assert_eq!(res.status, StatusCode::INTERNAL_SERVER_ERROR);
assert_eq!(res.mime, Some(APPLICATION_JSON)); assert_eq!(res.mime, Some(APPLICATION_JSON));
assert_eq!(res.full_body().unwrap(), format!(r#"{{"error":true,"message":"{}"}}"#, MsgError::default()).as_bytes()); assert_eq!(
res.full_body().unwrap(),
format!(r#"{{"error":true,"message":"{}"}}"#, MsgError::default()).as_bytes()
);
} }
#[test] #[test]
fn success_accepts_json() fn success_accepts_json() {
{ assert!(<Result<Msg, MsgError>>::accepted_types()
assert!(<Result<Msg, MsgError>>::accepted_types().or_all_types().contains(&APPLICATION_JSON)) .or_all_types()
.contains(&APPLICATION_JSON))
} }
} }

View file

@ -1,14 +1,14 @@
use super::{ResourceResult, into_response_helper}; use super::{into_response_helper, ResourceResult};
use crate::{Response, ResponseBody};
#[cfg(feature = "openapi")] #[cfg(feature = "openapi")]
use crate::OpenapiSchema; use crate::OpenapiSchema;
use crate::{Response, ResponseBody};
use gotham::hyper::StatusCode; use gotham::hyper::StatusCode;
use mime::{Mime, APPLICATION_JSON}; use mime::{Mime, APPLICATION_JSON};
use std::{ use std::{
fmt::Debug, fmt::Debug,
future::Future, future::Future,
pin::Pin, ops::{Deref, DerefMut},
ops::{Deref, DerefMut} pin::Pin
}; };
/** /**
@ -45,110 +45,87 @@ fn read_all(_state: &mut State) -> Success<MyResponse> {
#[derive(Debug)] #[derive(Debug)]
pub struct Success<T>(T); pub struct Success<T>(T);
impl<T> AsMut<T> for Success<T> impl<T> AsMut<T> for Success<T> {
{ fn as_mut(&mut self) -> &mut T {
fn as_mut(&mut self) -> &mut T
{
&mut self.0 &mut self.0
} }
} }
impl<T> AsRef<T> for Success<T> impl<T> AsRef<T> for Success<T> {
{ fn as_ref(&self) -> &T {
fn as_ref(&self) -> &T
{
&self.0 &self.0
} }
} }
impl<T> Deref for Success<T> impl<T> Deref for Success<T> {
{
type Target = T; type Target = T;
fn deref(&self) -> &T fn deref(&self) -> &T {
{
&self.0 &self.0
} }
} }
impl<T> DerefMut for Success<T> impl<T> DerefMut for Success<T> {
{ fn deref_mut(&mut self) -> &mut T {
fn deref_mut(&mut self) -> &mut T
{
&mut self.0 &mut self.0
} }
} }
impl<T> From<T> for Success<T> impl<T> From<T> for Success<T> {
{ fn from(t: T) -> Self {
fn from(t : T) -> Self
{
Self(t) Self(t)
} }
} }
impl<T : Clone> Clone for Success<T> impl<T: Clone> Clone for Success<T> {
{ fn clone(&self) -> Self {
fn clone(&self) -> Self
{
Self(self.0.clone()) Self(self.0.clone())
} }
} }
impl<T : Copy> Copy for Success<T> impl<T: Copy> Copy for Success<T> {}
{
}
impl<T : Default> Default for Success<T> impl<T: Default> Default for Success<T> {
{ fn default() -> Self {
fn default() -> Self
{
Self(T::default()) Self(T::default())
} }
} }
impl<T : ResponseBody> ResourceResult for Success<T> impl<T: ResponseBody> ResourceResult for Success<T>
where where
Self : Send Self: Send
{ {
type Err = serde_json::Error; type Err = serde_json::Error;
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, Self::Err>> + Send>> fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, Self::Err>> + Send>> {
{
into_response_helper(|| Ok(Response::json(StatusCode::OK, serde_json::to_string(self.as_ref())?))) into_response_helper(|| Ok(Response::json(StatusCode::OK, serde_json::to_string(self.as_ref())?)))
} }
fn accepted_types() -> Option<Vec<Mime>> fn accepted_types() -> Option<Vec<Mime>> {
{
Some(vec![APPLICATION_JSON]) Some(vec![APPLICATION_JSON])
} }
#[cfg(feature = "openapi")] #[cfg(feature = "openapi")]
fn schema() -> OpenapiSchema fn schema() -> OpenapiSchema {
{
T::schema() T::schema()
} }
} }
#[cfg(test)] #[cfg(test)]
mod test mod test {
{
use super::*; use super::*;
use crate::result::OrAllTypes; use crate::result::OrAllTypes;
use futures_executor::block_on; use futures_executor::block_on;
#[derive(Debug, Default, Serialize)] #[derive(Debug, Default, Serialize)]
#[cfg_attr(feature = "openapi", derive(crate::OpenapiType))] #[cfg_attr(feature = "openapi", derive(crate::OpenapiType))]
struct Msg struct Msg {
{ msg: String
msg : String
} }
#[test] #[test]
fn success_always_successfull() fn success_always_successfull() {
{ let success: Success<Msg> = Msg::default().into();
let success : Success<Msg> = Msg::default().into();
let res = block_on(success.into_response()).expect("didn't expect error response"); let res = block_on(success.into_response()).expect("didn't expect error response");
assert_eq!(res.status, StatusCode::OK); assert_eq!(res.status, StatusCode::OK);
assert_eq!(res.mime, Some(APPLICATION_JSON)); assert_eq!(res.mime, Some(APPLICATION_JSON));
@ -156,8 +133,7 @@ mod test
} }
#[test] #[test]
fn success_accepts_json() fn success_accepts_json() {
{
assert!(<Success<Msg>>::accepted_types().or_all_types().contains(&APPLICATION_JSON)) assert!(<Success<Msg>>::accepted_types().or_all_types().contains(&APPLICATION_JSON))
} }
} }

View file

@ -1,22 +1,21 @@
use crate::{
resource::*,
result::{ResourceError, ResourceResult},
RequestBody,
Response,
StatusCode
};
#[cfg(feature = "cors")]
use crate::CorsRoute;
#[cfg(feature = "openapi")] #[cfg(feature = "openapi")]
use crate::openapi::{ use crate::openapi::{
builder::{OpenapiBuilder, OpenapiInfo}, builder::{OpenapiBuilder, OpenapiInfo},
router::OpenapiRouter router::OpenapiRouter
}; };
#[cfg(feature = "cors")]
use crate::CorsRoute;
use crate::{
resource::*,
result::{ResourceError, ResourceResult},
RequestBody, Response, StatusCode
};
use futures_util::{future, future::FutureExt}; use futures_util::{future, future::FutureExt};
use gotham::{ use gotham::{
handler::{HandlerError, HandlerFuture, IntoHandlerError}, handler::{HandlerError, HandlerFuture},
helpers::http::response::{create_empty_response, create_response}, helpers::http::response::{create_empty_response, create_response},
hyper::{body::to_bytes, header::CONTENT_TYPE, Body, HeaderMap, Method},
pipeline::chain::PipelineHandleChain, pipeline::chain::PipelineHandleChain,
router::{ router::{
builder::*, builder::*,
@ -25,86 +24,68 @@ use gotham::{
}, },
state::{FromState, State} state::{FromState, State}
}; };
use gotham::hyper::{
body::to_bytes,
header::CONTENT_TYPE,
Body,
HeaderMap,
Method
};
use mime::{Mime, APPLICATION_JSON}; use mime::{Mime, APPLICATION_JSON};
use std::{ use std::{future::Future, panic::RefUnwindSafe, pin::Pin};
future::Future,
panic::RefUnwindSafe,
pin::Pin
};
/// Allow us to extract an id from a path. /// Allow us to extract an id from a path.
#[derive(Deserialize, StateData, StaticResponseExtender)] #[derive(Deserialize, StateData, StaticResponseExtender)]
struct PathExtractor<ID : RefUnwindSafe + Send + 'static> struct PathExtractor<ID: RefUnwindSafe + Send + 'static> {
{ id: ID
id : ID
} }
/// This trait adds the `with_openapi` method to gotham's routing. It turns the default /// This trait adds the `with_openapi` method to gotham's routing. It turns the default
/// router into one that will only allow RESTful resources, but record them and generate /// router into one that will only allow RESTful resources, but record them and generate
/// an OpenAPI specification on request. /// an OpenAPI specification on request.
#[cfg(feature = "openapi")] #[cfg(feature = "openapi")]
pub trait WithOpenapi<D> pub trait WithOpenapi<D> {
{ fn with_openapi<F>(&mut self, info: OpenapiInfo, block: F)
fn with_openapi<F>(&mut self, info : OpenapiInfo, block : F)
where where
F : FnOnce(OpenapiRouter<'_, D>); F: FnOnce(OpenapiRouter<'_, D>);
} }
/// This trait adds the `resource` method to gotham's routing. It allows you to register /// This trait adds the `resource` method to gotham's routing. It allows you to register
/// any RESTful `Resource` with a path. /// any RESTful `Resource` with a path.
pub trait DrawResources pub trait DrawResources {
{ fn resource<R: Resource>(&mut self, path: &str);
fn resource<R : Resource>(&mut self, path : &str);
} }
/// This trait allows to draw routes within an resource. Use this only inside the /// This trait allows to draw routes within an resource. Use this only inside the
/// `Resource::setup` method. /// `Resource::setup` method.
pub trait DrawResourceRoutes pub trait DrawResourceRoutes {
{ fn read_all<Handler: ResourceReadAll>(&mut self);
fn read_all<Handler : ResourceReadAll>(&mut self);
fn read<Handler : ResourceRead>(&mut self); fn read<Handler: ResourceRead>(&mut self);
fn search<Handler : ResourceSearch>(&mut self); fn search<Handler: ResourceSearch>(&mut self);
fn create<Handler : ResourceCreate>(&mut self) fn create<Handler: ResourceCreate>(&mut self)
where where
Handler::Res : 'static, Handler::Res: 'static,
Handler::Body : 'static; Handler::Body: 'static;
fn change_all<Handler : ResourceChangeAll>(&mut self) fn change_all<Handler: ResourceChangeAll>(&mut self)
where where
Handler::Res : 'static, Handler::Res: 'static,
Handler::Body : 'static; Handler::Body: 'static;
fn change<Handler : ResourceChange>(&mut self) fn change<Handler: ResourceChange>(&mut self)
where where
Handler::Res : 'static, Handler::Res: 'static,
Handler::Body : 'static; Handler::Body: 'static;
fn remove_all<Handler : ResourceRemoveAll>(&mut self); fn remove_all<Handler: ResourceRemoveAll>(&mut self);
fn remove<Handler : ResourceRemove>(&mut self); fn remove<Handler: ResourceRemove>(&mut self);
} }
fn response_from(res : Response, state : &State) -> gotham::hyper::Response<Body> fn response_from(res: Response, state: &State) -> gotham::hyper::Response<Body> {
{
let mut r = create_empty_response(state, res.status); let mut r = create_empty_response(state, res.status);
if let Some(mime) = res.mime if let Some(mime) = res.mime {
{
r.headers_mut().insert(CONTENT_TYPE, mime.as_ref().parse().unwrap()); r.headers_mut().insert(CONTENT_TYPE, mime.as_ref().parse().unwrap());
} }
let method = Method::borrow_from(state); let method = Method::borrow_from(state);
if method != Method::HEAD if method != Method::HEAD {
{
*r.body_mut() = res.body; *r.body_mut() = res.body;
} }
@ -114,10 +95,13 @@ fn response_from(res : Response, state : &State) -> gotham::hyper::Response<Body
r r
} }
async fn to_handler_future<F, R>(state : State, get_result : F) -> Result<(State, gotham::hyper::Response<Body>), (State, HandlerError)> async fn to_handler_future<F, R>(
state: State,
get_result: F
) -> Result<(State, gotham::hyper::Response<Body>), (State, HandlerError)>
where where
F : FnOnce(State) -> Pin<Box<dyn Future<Output = (State, R)> + Send>>, F: FnOnce(State) -> Pin<Box<dyn Future<Output = (State, R)> + Send>>,
R : ResourceResult R: ResourceResult
{ {
let (state, res) = get_result(state).await; let (state, res) = get_result(state).await;
let res = res.into_response().await; let res = res.into_response().await;
@ -126,28 +110,31 @@ where
let r = response_from(res, &state); let r = response_from(res, &state);
Ok((state, r)) Ok((state, r))
}, },
Err(e) => Err((state, e.into_handler_error())) Err(e) => Err((state, e.into()))
} }
} }
async fn body_to_res<B, F, R>(mut state : State, get_result : F) -> (State, Result<gotham::hyper::Response<Body>, HandlerError>) async fn body_to_res<B, F, R>(
mut state: State,
get_result: F
) -> (State, Result<gotham::hyper::Response<Body>, HandlerError>)
where where
B : RequestBody, B: RequestBody,
F : FnOnce(State, B) -> Pin<Box<dyn Future<Output = (State, R)> + Send>>, F: FnOnce(State, B) -> Pin<Box<dyn Future<Output = (State, R)> + Send>>,
R : ResourceResult R: ResourceResult
{ {
let body = to_bytes(Body::take_from(&mut state)).await; let body = to_bytes(Body::take_from(&mut state)).await;
let body = match body { let body = match body {
Ok(body) => body, Ok(body) => body,
Err(e) => return (state, Err(e.into_handler_error())) Err(e) => return (state, Err(e.into()))
}; };
let content_type : Mime = match HeaderMap::borrow_from(&state).get(CONTENT_TYPE) { let content_type: Mime = match HeaderMap::borrow_from(&state).get(CONTENT_TYPE) {
Some(content_type) => content_type.to_str().unwrap().parse().unwrap(), Some(content_type) => content_type.to_str().unwrap().parse().unwrap(),
None => { None => {
let res = create_empty_response(&state, StatusCode::UNSUPPORTED_MEDIA_TYPE); let res = create_empty_response(&state, StatusCode::UNSUPPORTED_MEDIA_TYPE);
return (state, Ok(res)) return (state, Ok(res));
} }
}; };
@ -155,15 +142,15 @@ where
let body = match B::from_body(body, content_type) { let body = match B::from_body(body, content_type) {
Ok(body) => body, Ok(body) => body,
Err(e) => { Err(e) => {
let error : ResourceError = e.into(); let error: ResourceError = e.into();
let res = match serde_json::to_string(&error) { let res = match serde_json::to_string(&error) {
Ok(json) => { Ok(json) => {
let res = create_response(&state, StatusCode::BAD_REQUEST, APPLICATION_JSON, json); let res = create_response(&state, StatusCode::BAD_REQUEST, APPLICATION_JSON, json);
Ok(res) Ok(res)
}, },
Err(e) => Err(e.into_handler_error()) Err(e) => Err(e.into())
}; };
return (state, res) return (state, res);
} }
}; };
get_result(state, body) get_result(state, body)
@ -177,16 +164,16 @@ where
let r = response_from(res, &state); let r = response_from(res, &state);
Ok(r) Ok(r)
}, },
Err(e) => Err(e.into_handler_error()) Err(e) => Err(e.into())
}; };
(state, res) (state, res)
} }
fn handle_with_body<B, F, R>(state : State, get_result : F) -> Pin<Box<HandlerFuture>> fn handle_with_body<B, F, R>(state: State, get_result: F) -> Pin<Box<HandlerFuture>>
where where
B : RequestBody + 'static, B: RequestBody + 'static,
F : FnOnce(State, B) -> Pin<Box<dyn Future<Output = (State, R)> + Send>> + Send + 'static, F: FnOnce(State, B) -> Pin<Box<dyn Future<Output = (State, R)> + Send>> + Send + 'static,
R : ResourceResult + Send + 'static R: ResourceResult + Send + 'static
{ {
body_to_res(state, get_result) body_to_res(state, get_result)
.then(|(state, res)| match res { .then(|(state, res)| match res {
@ -196,78 +183,70 @@ where
.boxed() .boxed()
} }
fn read_all_handler<Handler : ResourceReadAll>(state : State) -> Pin<Box<HandlerFuture>> fn read_all_handler<Handler: ResourceReadAll>(state: State) -> Pin<Box<HandlerFuture>> {
{
to_handler_future(state, |state| Handler::read_all(state)).boxed() to_handler_future(state, |state| Handler::read_all(state)).boxed()
} }
fn read_handler<Handler : ResourceRead>(state : State) -> Pin<Box<HandlerFuture>> fn read_handler<Handler: ResourceRead>(state: State) -> Pin<Box<HandlerFuture>> {
{
let id = { let id = {
let path : &PathExtractor<Handler::ID> = PathExtractor::borrow_from(&state); let path: &PathExtractor<Handler::ID> = PathExtractor::borrow_from(&state);
path.id.clone() path.id.clone()
}; };
to_handler_future(state, |state| Handler::read(state, id)).boxed() to_handler_future(state, |state| Handler::read(state, id)).boxed()
} }
fn search_handler<Handler : ResourceSearch>(mut state : State) -> Pin<Box<HandlerFuture>> fn search_handler<Handler: ResourceSearch>(mut state: State) -> Pin<Box<HandlerFuture>> {
{
let query = Handler::Query::take_from(&mut state); let query = Handler::Query::take_from(&mut state);
to_handler_future(state, |state| Handler::search(state, query)).boxed() to_handler_future(state, |state| Handler::search(state, query)).boxed()
} }
fn create_handler<Handler : ResourceCreate>(state : State) -> Pin<Box<HandlerFuture>> fn create_handler<Handler: ResourceCreate>(state: State) -> Pin<Box<HandlerFuture>>
where where
Handler::Res : 'static, Handler::Res: 'static,
Handler::Body : 'static Handler::Body: 'static
{ {
handle_with_body::<Handler::Body, _, _>(state, |state, body| Handler::create(state, body)) handle_with_body::<Handler::Body, _, _>(state, |state, body| Handler::create(state, body))
} }
fn change_all_handler<Handler : ResourceChangeAll>(state : State) -> Pin<Box<HandlerFuture>> fn change_all_handler<Handler: ResourceChangeAll>(state: State) -> Pin<Box<HandlerFuture>>
where where
Handler::Res : 'static, Handler::Res: 'static,
Handler::Body : 'static Handler::Body: 'static
{ {
handle_with_body::<Handler::Body, _, _>(state, |state, body| Handler::change_all(state, body)) handle_with_body::<Handler::Body, _, _>(state, |state, body| Handler::change_all(state, body))
} }
fn change_handler<Handler : ResourceChange>(state : State) -> Pin<Box<HandlerFuture>> fn change_handler<Handler: ResourceChange>(state: State) -> Pin<Box<HandlerFuture>>
where where
Handler::Res : 'static, Handler::Res: 'static,
Handler::Body : 'static Handler::Body: 'static
{ {
let id = { let id = {
let path : &PathExtractor<Handler::ID> = PathExtractor::borrow_from(&state); let path: &PathExtractor<Handler::ID> = PathExtractor::borrow_from(&state);
path.id.clone() path.id.clone()
}; };
handle_with_body::<Handler::Body, _, _>(state, |state, body| Handler::change(state, id, body)) handle_with_body::<Handler::Body, _, _>(state, |state, body| Handler::change(state, id, body))
} }
fn remove_all_handler<Handler : ResourceRemoveAll>(state : State) -> Pin<Box<HandlerFuture>> fn remove_all_handler<Handler: ResourceRemoveAll>(state: State) -> Pin<Box<HandlerFuture>> {
{
to_handler_future(state, |state| Handler::remove_all(state)).boxed() to_handler_future(state, |state| Handler::remove_all(state)).boxed()
} }
fn remove_handler<Handler : ResourceRemove>(state : State) -> Pin<Box<HandlerFuture>> fn remove_handler<Handler: ResourceRemove>(state: State) -> Pin<Box<HandlerFuture>> {
{
let id = { let id = {
let path : &PathExtractor<Handler::ID> = PathExtractor::borrow_from(&state); let path: &PathExtractor<Handler::ID> = PathExtractor::borrow_from(&state);
path.id.clone() path.id.clone()
}; };
to_handler_future(state, |state| Handler::remove(state, id)).boxed() to_handler_future(state, |state| Handler::remove(state, id)).boxed()
} }
#[derive(Clone)] #[derive(Clone)]
struct MaybeMatchAcceptHeader struct MaybeMatchAcceptHeader {
{ matcher: Option<AcceptHeaderRouteMatcher>
matcher : Option<AcceptHeaderRouteMatcher>
} }
impl RouteMatcher for MaybeMatchAcceptHeader impl RouteMatcher for MaybeMatchAcceptHeader {
{ fn is_match(&self, state: &State) -> Result<(), RouteNonMatch> {
fn is_match(&self, state : &State) -> Result<(), RouteNonMatch>
{
match &self.matcher { match &self.matcher {
Some(matcher) => matcher.is_match(state), Some(matcher) => matcher.is_match(state),
None => Ok(()) None => Ok(())
@ -275,10 +254,8 @@ impl RouteMatcher for MaybeMatchAcceptHeader
} }
} }
impl From<Option<Vec<Mime>>> for MaybeMatchAcceptHeader impl From<Option<Vec<Mime>>> for MaybeMatchAcceptHeader {
{ fn from(types: Option<Vec<Mime>>) -> Self {
fn from(types : Option<Vec<Mime>>) -> Self
{
let types = match types { let types = match types {
Some(types) if types.is_empty() => None, Some(types) if types.is_empty() => None,
types => types types => types
@ -290,15 +267,12 @@ impl From<Option<Vec<Mime>>> for MaybeMatchAcceptHeader
} }
#[derive(Clone)] #[derive(Clone)]
struct MaybeMatchContentTypeHeader struct MaybeMatchContentTypeHeader {
{ matcher: Option<ContentTypeHeaderRouteMatcher>
matcher : Option<ContentTypeHeaderRouteMatcher>
} }
impl RouteMatcher for MaybeMatchContentTypeHeader impl RouteMatcher for MaybeMatchContentTypeHeader {
{ fn is_match(&self, state: &State) -> Result<(), RouteNonMatch> {
fn is_match(&self, state : &State) -> Result<(), RouteNonMatch>
{
match &self.matcher { match &self.matcher {
Some(matcher) => matcher.is_match(state), Some(matcher) => matcher.is_match(state),
None => Ok(()) None => Ok(())
@ -306,10 +280,8 @@ impl RouteMatcher for MaybeMatchContentTypeHeader
} }
} }
impl From<Option<Vec<Mime>>> for MaybeMatchContentTypeHeader impl From<Option<Vec<Mime>>> for MaybeMatchContentTypeHeader {
{ fn from(types: Option<Vec<Mime>>) -> Self {
fn from(types : Option<Vec<Mime>>) -> Self
{
Self { Self {
matcher: types.map(|types| ContentTypeHeaderRouteMatcher::new(types).allow_no_type()) matcher: types.map(|types| ContentTypeHeaderRouteMatcher::new(types).allow_no_type())
} }
@ -318,16 +290,15 @@ impl From<Option<Vec<Mime>>> for MaybeMatchContentTypeHeader
macro_rules! implDrawResourceRoutes { macro_rules! implDrawResourceRoutes {
($implType:ident) => { ($implType:ident) => {
#[cfg(feature = "openapi")] #[cfg(feature = "openapi")]
impl<'a, C, P> WithOpenapi<Self> for $implType<'a, C, P> impl<'a, C, P> WithOpenapi<Self> for $implType<'a, C, P>
where where
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static, C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
P : RefUnwindSafe + Send + Sync + 'static P: RefUnwindSafe + Send + Sync + 'static
{ {
fn with_openapi<F>(&mut self, info : OpenapiInfo, block : F) fn with_openapi<F>(&mut self, info: OpenapiInfo, block: F)
where where
F : FnOnce(OpenapiRouter<'_, $implType<'a, C, P>>) F: FnOnce(OpenapiRouter<'_, $implType<'a, C, P>>)
{ {
let router = OpenapiRouter { let router = OpenapiRouter {
router: self, router: self,
@ -340,11 +311,10 @@ macro_rules! implDrawResourceRoutes {
impl<'a, C, P> DrawResources for $implType<'a, C, P> impl<'a, C, P> DrawResources for $implType<'a, C, P>
where where
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static, C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
P : RefUnwindSafe + Send + Sync + 'static P: RefUnwindSafe + Send + Sync + 'static
{ {
fn resource<R : Resource>(&mut self, path : &str) fn resource<R: Resource>(&mut self, path: &str) {
{
R::setup((self, path)); R::setup((self, path));
} }
} }
@ -352,43 +322,44 @@ macro_rules! implDrawResourceRoutes {
#[allow(clippy::redundant_closure)] // doesn't work because of type parameters #[allow(clippy::redundant_closure)] // doesn't work because of type parameters
impl<'a, C, P> DrawResourceRoutes for (&mut $implType<'a, C, P>, &str) impl<'a, C, P> DrawResourceRoutes for (&mut $implType<'a, C, P>, &str)
where where
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static, C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
P : RefUnwindSafe + Send + Sync + 'static P: RefUnwindSafe + Send + Sync + 'static
{ {
fn read_all<Handler : ResourceReadAll>(&mut self) fn read_all<Handler: ResourceReadAll>(&mut self) {
{ let matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); self.0
self.0.get(&self.1) .get(&self.1)
.extend_route_matcher(matcher) .extend_route_matcher(matcher)
.to(|state| read_all_handler::<Handler>(state)); .to(|state| read_all_handler::<Handler>(state));
} }
fn read<Handler : ResourceRead>(&mut self) fn read<Handler: ResourceRead>(&mut self) {
{ let matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); self.0
self.0.get(&format!("{}/:id", self.1)) .get(&format!("{}/:id", self.1))
.extend_route_matcher(matcher) .extend_route_matcher(matcher)
.with_path_extractor::<PathExtractor<Handler::ID>>() .with_path_extractor::<PathExtractor<Handler::ID>>()
.to(|state| read_handler::<Handler>(state)); .to(|state| read_handler::<Handler>(state));
} }
fn search<Handler : ResourceSearch>(&mut self) fn search<Handler: ResourceSearch>(&mut self) {
{ let matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); self.0
self.0.get(&format!("{}/search", self.1)) .get(&format!("{}/search", self.1))
.extend_route_matcher(matcher) .extend_route_matcher(matcher)
.with_query_string_extractor::<Handler::Query>() .with_query_string_extractor::<Handler::Query>()
.to(|state| search_handler::<Handler>(state)); .to(|state| search_handler::<Handler>(state));
} }
fn create<Handler : ResourceCreate>(&mut self) fn create<Handler: ResourceCreate>(&mut self)
where where
Handler::Res : Send + 'static, Handler::Res: Send + 'static,
Handler::Body : 'static Handler::Body: 'static
{ {
let accept_matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); let accept_matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
let content_matcher : MaybeMatchContentTypeHeader = Handler::Body::supported_types().into(); let content_matcher: MaybeMatchContentTypeHeader = Handler::Body::supported_types().into();
self.0.post(&self.1) self.0
.post(&self.1)
.extend_route_matcher(accept_matcher) .extend_route_matcher(accept_matcher)
.extend_route_matcher(content_matcher) .extend_route_matcher(content_matcher)
.to(|state| create_handler::<Handler>(state)); .to(|state| create_handler::<Handler>(state));
@ -396,14 +367,15 @@ macro_rules! implDrawResourceRoutes {
self.0.cors(&self.1, Method::POST); self.0.cors(&self.1, Method::POST);
} }
fn change_all<Handler : ResourceChangeAll>(&mut self) fn change_all<Handler: ResourceChangeAll>(&mut self)
where where
Handler::Res : Send + 'static, Handler::Res: Send + 'static,
Handler::Body : 'static Handler::Body: 'static
{ {
let accept_matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); let accept_matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
let content_matcher : MaybeMatchContentTypeHeader = Handler::Body::supported_types().into(); let content_matcher: MaybeMatchContentTypeHeader = Handler::Body::supported_types().into();
self.0.put(&self.1) self.0
.put(&self.1)
.extend_route_matcher(accept_matcher) .extend_route_matcher(accept_matcher)
.extend_route_matcher(content_matcher) .extend_route_matcher(content_matcher)
.to(|state| change_all_handler::<Handler>(state)); .to(|state| change_all_handler::<Handler>(state));
@ -411,15 +383,16 @@ macro_rules! implDrawResourceRoutes {
self.0.cors(&self.1, Method::PUT); self.0.cors(&self.1, Method::PUT);
} }
fn change<Handler : ResourceChange>(&mut self) fn change<Handler: ResourceChange>(&mut self)
where where
Handler::Res : Send + 'static, Handler::Res: Send + 'static,
Handler::Body : 'static Handler::Body: 'static
{ {
let accept_matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); let accept_matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
let content_matcher : MaybeMatchContentTypeHeader = Handler::Body::supported_types().into(); let content_matcher: MaybeMatchContentTypeHeader = Handler::Body::supported_types().into();
let path = format!("{}/:id", self.1); let path = format!("{}/:id", self.1);
self.0.put(&path) self.0
.put(&path)
.extend_route_matcher(accept_matcher) .extend_route_matcher(accept_matcher)
.extend_route_matcher(content_matcher) .extend_route_matcher(content_matcher)
.with_path_extractor::<PathExtractor<Handler::ID>>() .with_path_extractor::<PathExtractor<Handler::ID>>()
@ -428,21 +401,21 @@ macro_rules! implDrawResourceRoutes {
self.0.cors(&path, Method::PUT); self.0.cors(&path, Method::PUT);
} }
fn remove_all<Handler : ResourceRemoveAll>(&mut self) fn remove_all<Handler: ResourceRemoveAll>(&mut self) {
{ let matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); self.0
self.0.delete(&self.1) .delete(&self.1)
.extend_route_matcher(matcher) .extend_route_matcher(matcher)
.to(|state| remove_all_handler::<Handler>(state)); .to(|state| remove_all_handler::<Handler>(state));
#[cfg(feature = "cors")] #[cfg(feature = "cors")]
self.0.cors(&self.1, Method::DELETE); self.0.cors(&self.1, Method::DELETE);
} }
fn remove<Handler : ResourceRemove>(&mut self) fn remove<Handler: ResourceRemove>(&mut self) {
{ let matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
let path = format!("{}/:id", self.1); let path = format!("{}/:id", self.1);
self.0.delete(&path) self.0
.delete(&path)
.extend_route_matcher(matcher) .extend_route_matcher(matcher)
.with_path_extractor::<PathExtractor<Handler::ID>>() .with_path_extractor::<PathExtractor<Handler::ID>>()
.to(|state| remove_handler::<Handler>(state)); .to(|state| remove_handler::<Handler>(state));
@ -450,7 +423,7 @@ macro_rules! implDrawResourceRoutes {
self.0.cors(&path, Method::POST); self.0.cors(&path, Method::POST);
} }
} }
} };
} }
implDrawResourceRoutes!(RouterBuilder); implDrawResourceRoutes!(RouterBuilder);

View file

@ -4,43 +4,26 @@ use crate::OpenapiType;
use gotham::hyper::body::Bytes; use gotham::hyper::body::Bytes;
use mime::{Mime, APPLICATION_JSON}; use mime::{Mime, APPLICATION_JSON};
use serde::{de::DeserializeOwned, Serialize}; use serde::{de::DeserializeOwned, Serialize};
use std::{ use std::{error::Error, panic::RefUnwindSafe};
error::Error,
panic::RefUnwindSafe
};
#[cfg(not(feature = "openapi"))] #[cfg(not(feature = "openapi"))]
pub trait ResourceType pub trait ResourceType {}
{
}
#[cfg(not(feature = "openapi"))] #[cfg(not(feature = "openapi"))]
impl<T> ResourceType for T impl<T> ResourceType for T {}
{
}
#[cfg(feature = "openapi")] #[cfg(feature = "openapi")]
pub trait ResourceType : OpenapiType pub trait ResourceType: OpenapiType {}
{
}
#[cfg(feature = "openapi")] #[cfg(feature = "openapi")]
impl<T : OpenapiType> ResourceType for T impl<T: OpenapiType> ResourceType for T {}
{
}
/// A type that can be used inside a response body. Implemented for every type that is /// A type that can be used inside a response body. Implemented for every type that is
/// serializable with serde. If the `openapi` feature is used, it must also be of type /// serializable with serde. If the `openapi` feature is used, it must also be of type
/// `OpenapiType`. /// `OpenapiType`.
pub trait ResponseBody : ResourceType + Serialize pub trait ResponseBody: ResourceType + Serialize {}
{
}
impl<T : ResourceType + Serialize> ResponseBody for T
{
}
impl<T: ResourceType + Serialize> ResponseBody for T {}
/** /**
This trait should be implemented for every type that can be built from an HTTP request body This trait should be implemented for every type that can be built from an HTTP request body
@ -64,28 +47,24 @@ struct RawImage {
[`Bytes`]: ../bytes/struct.Bytes.html [`Bytes`]: ../bytes/struct.Bytes.html
[`Mime`]: ../mime/struct.Mime.html [`Mime`]: ../mime/struct.Mime.html
*/ */
pub trait FromBody : Sized pub trait FromBody: Sized {
{
/// The error type returned by the conversion if it was unsuccessfull. When using the derive /// The error type returned by the conversion if it was unsuccessfull. When using the derive
/// macro, there is no way to trigger an error, so `Infallible` is used here. However, this /// macro, there is no way to trigger an error, so `Infallible` is used here. However, this
/// might change in the future. /// might change in the future.
type Err : Error; type Err: Error;
/// Perform the conversion. /// Perform the conversion.
fn from_body(body : Bytes, content_type : Mime) -> Result<Self, Self::Err>; fn from_body(body: Bytes, content_type: Mime) -> Result<Self, Self::Err>;
} }
impl<T : DeserializeOwned> FromBody for T impl<T: DeserializeOwned> FromBody for T {
{
type Err = serde_json::Error; type Err = serde_json::Error;
fn from_body(body : Bytes, _content_type : Mime) -> Result<Self, Self::Err> fn from_body(body: Bytes, _content_type: Mime) -> Result<Self, Self::Err> {
{
serde_json::from_slice(&body) serde_json::from_slice(&body)
} }
} }
/** /**
A type that can be used inside a request body. Implemented for every type that is deserializable A type that can be used inside a request body. Implemented for every type that is deserializable
with serde. If the `openapi` feature is used, it must also be of type [`OpenapiType`]. with serde. If the `openapi` feature is used, it must also be of type [`OpenapiType`].
@ -108,19 +87,15 @@ struct RawImage {
[`FromBody`]: trait.FromBody.html [`FromBody`]: trait.FromBody.html
[`OpenapiType`]: trait.OpenapiType.html [`OpenapiType`]: trait.OpenapiType.html
*/ */
pub trait RequestBody : ResourceType + FromBody pub trait RequestBody: ResourceType + FromBody {
{
/// Return all types that are supported as content types. Use `None` if all types are supported. /// Return all types that are supported as content types. Use `None` if all types are supported.
fn supported_types() -> Option<Vec<Mime>> fn supported_types() -> Option<Vec<Mime>> {
{
None None
} }
} }
impl<T : ResourceType + DeserializeOwned> RequestBody for T impl<T: ResourceType + DeserializeOwned> RequestBody for T {
{ fn supported_types() -> Option<Vec<Mime>> {
fn supported_types() -> Option<Vec<Mime>>
{
Some(vec![APPLICATION_JSON]) Some(vec![APPLICATION_JSON])
} }
} }
@ -128,10 +103,6 @@ impl<T : ResourceType + DeserializeOwned> RequestBody for T
/// A type than can be used as a parameter to a resource method. Implemented for every type /// A type than can be used as a parameter to a resource method. Implemented for every type
/// that is deserialize and thread-safe. If the `openapi` feature is used, it must also be of /// that is deserialize and thread-safe. If the `openapi` feature is used, it must also be of
/// type `OpenapiType`. /// type `OpenapiType`.
pub trait ResourceID : ResourceType + DeserializeOwned + Clone + RefUnwindSafe + Send + Sync pub trait ResourceID: ResourceType + DeserializeOwned + Clone + RefUnwindSafe + Send + Sync {}
{
}
impl<T : ResourceType + DeserializeOwned + Clone + RefUnwindSafe + Send + Sync> ResourceID for T impl<T: ResourceType + DeserializeOwned + Clone + RefUnwindSafe + Send + Sync> ResourceID for T {}
{
}

View file

@ -1,16 +1,15 @@
#[macro_use] extern crate gotham_derive; #[macro_use]
extern crate gotham_derive;
use gotham::{ use gotham::{router::builder::*, test::TestServer};
router::builder::*,
test::TestServer
};
use gotham_restful::*; use gotham_restful::*;
use mime::{APPLICATION_JSON, TEXT_PLAIN}; use mime::{APPLICATION_JSON, TEXT_PLAIN};
use serde::Deserialize; use serde::Deserialize;
mod util { include!("util/mod.rs"); } mod util {
use util::{test_get_response, test_post_response, test_put_response, test_delete_response}; include!("util/mod.rs");
}
use util::{test_delete_response, test_get_response, test_post_response, test_put_response};
#[derive(Resource)] #[derive(Resource)]
#[resource(read_all, read, search, create, change_all, change, remove_all, remove)] #[resource(read_all, read, search, create, change_all, change, remove_all, remove)]
@ -19,88 +18,96 @@ struct FooResource;
#[derive(Deserialize)] #[derive(Deserialize)]
#[cfg_attr(feature = "openapi", derive(OpenapiType))] #[cfg_attr(feature = "openapi", derive(OpenapiType))]
#[allow(dead_code)] #[allow(dead_code)]
struct FooBody struct FooBody {
{ data: String
data : String
} }
#[derive(Deserialize, StateData, StaticResponseExtender)] #[derive(Deserialize, StateData, StaticResponseExtender)]
#[cfg_attr(feature = "openapi", derive(OpenapiType))] #[cfg_attr(feature = "openapi", derive(OpenapiType))]
#[allow(dead_code)] #[allow(dead_code)]
struct FooSearch struct FooSearch {
{ query: String
query : String
} }
const READ_ALL_RESPONSE : &[u8] = b"1ARwwSPVyOKpJKrYwqGgECPVWDl1BqajAAj7g7WJ3e"; const READ_ALL_RESPONSE: &[u8] = b"1ARwwSPVyOKpJKrYwqGgECPVWDl1BqajAAj7g7WJ3e";
#[read_all(FooResource)] #[read_all(FooResource)]
async fn read_all() -> Raw<&'static [u8]> async fn read_all() -> Raw<&'static [u8]> {
{
Raw::new(READ_ALL_RESPONSE, TEXT_PLAIN) Raw::new(READ_ALL_RESPONSE, TEXT_PLAIN)
} }
const READ_RESPONSE : &[u8] = b"FEReHoeBKU17X2bBpVAd1iUvktFL43CDu0cFYHdaP9"; const READ_RESPONSE: &[u8] = b"FEReHoeBKU17X2bBpVAd1iUvktFL43CDu0cFYHdaP9";
#[read(FooResource)] #[read(FooResource)]
async fn read(_id : u64) -> Raw<&'static [u8]> async fn read(_id: u64) -> Raw<&'static [u8]> {
{
Raw::new(READ_RESPONSE, TEXT_PLAIN) Raw::new(READ_RESPONSE, TEXT_PLAIN)
} }
const SEARCH_RESPONSE : &[u8] = b"AWqcQUdBRHXKh3at4u79mdupOAfEbnTcx71ogCVF0E"; const SEARCH_RESPONSE: &[u8] = b"AWqcQUdBRHXKh3at4u79mdupOAfEbnTcx71ogCVF0E";
#[search(FooResource)] #[search(FooResource)]
async fn search(_body : FooSearch) -> Raw<&'static [u8]> async fn search(_body: FooSearch) -> Raw<&'static [u8]> {
{
Raw::new(SEARCH_RESPONSE, TEXT_PLAIN) Raw::new(SEARCH_RESPONSE, TEXT_PLAIN)
} }
const CREATE_RESPONSE : &[u8] = b"y6POY7wOMAB0jBRBw0FJT7DOpUNbhmT8KdpQPLkI83"; const CREATE_RESPONSE: &[u8] = b"y6POY7wOMAB0jBRBw0FJT7DOpUNbhmT8KdpQPLkI83";
#[create(FooResource)] #[create(FooResource)]
async fn create(_body : FooBody) -> Raw<&'static [u8]> async fn create(_body: FooBody) -> Raw<&'static [u8]> {
{
Raw::new(CREATE_RESPONSE, TEXT_PLAIN) Raw::new(CREATE_RESPONSE, TEXT_PLAIN)
} }
const CHANGE_ALL_RESPONSE : &[u8] = b"QlbYg8gHE9OQvvk3yKjXJLTSXlIrg9mcqhfMXJmQkv"; const CHANGE_ALL_RESPONSE: &[u8] = b"QlbYg8gHE9OQvvk3yKjXJLTSXlIrg9mcqhfMXJmQkv";
#[change_all(FooResource)] #[change_all(FooResource)]
async fn change_all(_body : FooBody) -> Raw<&'static [u8]> async fn change_all(_body: FooBody) -> Raw<&'static [u8]> {
{
Raw::new(CHANGE_ALL_RESPONSE, TEXT_PLAIN) Raw::new(CHANGE_ALL_RESPONSE, TEXT_PLAIN)
} }
const CHANGE_RESPONSE : &[u8] = b"qGod55RUXkT1lgPO8h0uVM6l368O2S0GrwENZFFuRu"; const CHANGE_RESPONSE: &[u8] = b"qGod55RUXkT1lgPO8h0uVM6l368O2S0GrwENZFFuRu";
#[change(FooResource)] #[change(FooResource)]
async fn change(_id : u64, _body : FooBody) -> Raw<&'static [u8]> async fn change(_id: u64, _body: FooBody) -> Raw<&'static [u8]> {
{
Raw::new(CHANGE_RESPONSE, TEXT_PLAIN) Raw::new(CHANGE_RESPONSE, TEXT_PLAIN)
} }
const REMOVE_ALL_RESPONSE : &[u8] = b"Y36kZ749MRk2Nem4BedJABOZiZWPLOtiwLfJlGTwm5"; const REMOVE_ALL_RESPONSE: &[u8] = b"Y36kZ749MRk2Nem4BedJABOZiZWPLOtiwLfJlGTwm5";
#[remove_all(FooResource)] #[remove_all(FooResource)]
async fn remove_all() -> Raw<&'static [u8]> async fn remove_all() -> Raw<&'static [u8]> {
{
Raw::new(REMOVE_ALL_RESPONSE, TEXT_PLAIN) Raw::new(REMOVE_ALL_RESPONSE, TEXT_PLAIN)
} }
const REMOVE_RESPONSE : &[u8] = b"CwRzBrKErsVZ1N7yeNfjZuUn1MacvgBqk4uPOFfDDq"; const REMOVE_RESPONSE: &[u8] = b"CwRzBrKErsVZ1N7yeNfjZuUn1MacvgBqk4uPOFfDDq";
#[remove(FooResource)] #[remove(FooResource)]
async fn remove(_id : u64) -> Raw<&'static [u8]> async fn remove(_id: u64) -> Raw<&'static [u8]> {
{
Raw::new(REMOVE_RESPONSE, TEXT_PLAIN) Raw::new(REMOVE_RESPONSE, TEXT_PLAIN)
} }
#[test] #[test]
fn async_methods() fn async_methods() {
{
let server = TestServer::new(build_simple_router(|router| { let server = TestServer::new(build_simple_router(|router| {
router.resource::<FooResource>("foo"); router.resource::<FooResource>("foo");
})).unwrap(); }))
.unwrap();
test_get_response(&server, "http://localhost/foo", READ_ALL_RESPONSE); test_get_response(&server, "http://localhost/foo", READ_ALL_RESPONSE);
test_get_response(&server, "http://localhost/foo/1", READ_RESPONSE); test_get_response(&server, "http://localhost/foo/1", READ_RESPONSE);
test_get_response(&server, "http://localhost/foo/search?query=hello+world", SEARCH_RESPONSE); test_get_response(&server, "http://localhost/foo/search?query=hello+world", SEARCH_RESPONSE);
test_post_response(&server, "http://localhost/foo", r#"{"data":"hello world"}"#, APPLICATION_JSON, CREATE_RESPONSE); test_post_response(
test_put_response(&server, "http://localhost/foo", r#"{"data":"hello world"}"#, APPLICATION_JSON, CHANGE_ALL_RESPONSE); &server,
test_put_response(&server, "http://localhost/foo/1", r#"{"data":"hello world"}"#, APPLICATION_JSON, CHANGE_RESPONSE); "http://localhost/foo",
r#"{"data":"hello world"}"#,
APPLICATION_JSON,
CREATE_RESPONSE
);
test_put_response(
&server,
"http://localhost/foo",
r#"{"data":"hello world"}"#,
APPLICATION_JSON,
CHANGE_ALL_RESPONSE
);
test_put_response(
&server,
"http://localhost/foo/1",
r#"{"data":"hello world"}"#,
APPLICATION_JSON,
CHANGE_RESPONSE
);
test_delete_response(&server, "http://localhost/foo", REMOVE_ALL_RESPONSE); test_delete_response(&server, "http://localhost/foo", REMOVE_ALL_RESPONSE);
test_delete_response(&server, "http://localhost/foo/1", REMOVE_RESPONSE); test_delete_response(&server, "http://localhost/foo/1", REMOVE_RESPONSE);
} }

View file

@ -5,7 +5,7 @@ use gotham::{
router::builder::*, router::builder::*,
test::{Server, TestRequest, TestServer} test::{Server, TestRequest, TestServer}
}; };
use gotham_restful::{CorsConfig, DrawResources, Origin, Raw, Resource, change_all, read_all}; use gotham_restful::{change_all, read_all, CorsConfig, DrawResources, Origin, Raw, Resource};
use itertools::Itertools; use itertools::Itertools;
use mime::TEXT_PLAIN; use mime::TEXT_PLAIN;
@ -14,70 +14,108 @@ use mime::TEXT_PLAIN;
struct FooResource; struct FooResource;
#[read_all(FooResource)] #[read_all(FooResource)]
fn read_all() fn read_all() {}
{
}
#[change_all(FooResource)] #[change_all(FooResource)]
fn change_all(_body : Raw<Vec<u8>>) fn change_all(_body: Raw<Vec<u8>>) {}
{
}
fn test_server(cfg : CorsConfig) -> TestServer fn test_server(cfg: CorsConfig) -> TestServer {
{
let (chain, pipeline) = single_pipeline(new_pipeline().add(cfg).build()); let (chain, pipeline) = single_pipeline(new_pipeline().add(cfg).build());
TestServer::new(build_router(chain, pipeline, |router| { TestServer::new(build_router(chain, pipeline, |router| router.resource::<FooResource>("/foo"))).unwrap()
router.resource::<FooResource>("/foo")
})).unwrap()
} }
fn test_response<TS, C>(req : TestRequest<TS, C>, origin : Option<&str>, vary : Option<&str>, credentials : bool) fn test_response<TS, C>(req: TestRequest<TS, C>, origin: Option<&str>, vary: Option<&str>, credentials: bool)
where where
TS : Server + 'static, TS: Server + 'static,
C : Connect + Clone + Send + Sync + 'static C: Connect + Clone + Send + Sync + 'static
{ {
let res = req.with_header(ORIGIN, "http://example.org".parse().unwrap()).perform().unwrap(); let res = req
.with_header(ORIGIN, "http://example.org".parse().unwrap())
.perform()
.unwrap();
assert_eq!(res.status(), StatusCode::NO_CONTENT); assert_eq!(res.status(), StatusCode::NO_CONTENT);
let headers = res.headers(); let headers = res.headers();
println!("{}", headers.keys().join(",")); println!("{}", headers.keys().join(","));
assert_eq!(headers.get(ACCESS_CONTROL_ALLOW_ORIGIN).and_then(|value| value.to_str().ok()).as_deref(), origin); assert_eq!(
headers
.get(ACCESS_CONTROL_ALLOW_ORIGIN)
.and_then(|value| value.to_str().ok())
.as_deref(),
origin
);
assert_eq!(headers.get(VARY).and_then(|value| value.to_str().ok()).as_deref(), vary); assert_eq!(headers.get(VARY).and_then(|value| value.to_str().ok()).as_deref(), vary);
assert_eq!(headers.get(ACCESS_CONTROL_ALLOW_CREDENTIALS).and_then(|value| value.to_str().ok()).map(|value| value == "true").unwrap_or(false), credentials); assert_eq!(
headers
.get(ACCESS_CONTROL_ALLOW_CREDENTIALS)
.and_then(|value| value.to_str().ok())
.map(|value| value == "true")
.unwrap_or(false),
credentials
);
assert!(headers.get(ACCESS_CONTROL_MAX_AGE).is_none()); assert!(headers.get(ACCESS_CONTROL_MAX_AGE).is_none());
} }
fn test_preflight(server : &TestServer, method : &str, origin : Option<&str>, vary : &str, credentials : bool, max_age : u64) fn test_preflight(server: &TestServer, method: &str, origin: Option<&str>, vary: &str, credentials: bool, max_age: u64) {
{ let res = server
let res = server.client().options("http://example.org/foo") .client()
.options("http://example.org/foo")
.with_header(ACCESS_CONTROL_REQUEST_METHOD, method.parse().unwrap()) .with_header(ACCESS_CONTROL_REQUEST_METHOD, method.parse().unwrap())
.with_header(ORIGIN, "http://example.org".parse().unwrap()) .with_header(ORIGIN, "http://example.org".parse().unwrap())
.perform().unwrap(); .perform()
.unwrap();
assert_eq!(res.status(), StatusCode::NO_CONTENT); assert_eq!(res.status(), StatusCode::NO_CONTENT);
let headers = res.headers(); let headers = res.headers();
println!("{}", headers.keys().join(",")); println!("{}", headers.keys().join(","));
assert_eq!(headers.get(ACCESS_CONTROL_ALLOW_METHODS).and_then(|value| value.to_str().ok()).as_deref(), Some(method)); assert_eq!(
assert_eq!(headers.get(ACCESS_CONTROL_ALLOW_ORIGIN).and_then(|value| value.to_str().ok()).as_deref(), origin); headers
.get(ACCESS_CONTROL_ALLOW_METHODS)
.and_then(|value| value.to_str().ok())
.as_deref(),
Some(method)
);
assert_eq!(
headers
.get(ACCESS_CONTROL_ALLOW_ORIGIN)
.and_then(|value| value.to_str().ok())
.as_deref(),
origin
);
assert_eq!(headers.get(VARY).and_then(|value| value.to_str().ok()).as_deref(), Some(vary)); assert_eq!(headers.get(VARY).and_then(|value| value.to_str().ok()).as_deref(), Some(vary));
assert_eq!(headers.get(ACCESS_CONTROL_ALLOW_CREDENTIALS).and_then(|value| value.to_str().ok()).map(|value| value == "true").unwrap_or(false), credentials); assert_eq!(
assert_eq!(headers.get(ACCESS_CONTROL_MAX_AGE).and_then(|value| value.to_str().ok()).and_then(|value| value.parse().ok()), Some(max_age)); headers
.get(ACCESS_CONTROL_ALLOW_CREDENTIALS)
.and_then(|value| value.to_str().ok())
.map(|value| value == "true")
.unwrap_or(false),
credentials
);
assert_eq!(
headers
.get(ACCESS_CONTROL_MAX_AGE)
.and_then(|value| value.to_str().ok())
.and_then(|value| value.parse().ok()),
Some(max_age)
);
} }
#[test] #[test]
fn cors_origin_none() fn cors_origin_none() {
{
let cfg = Default::default(); let cfg = Default::default();
let server = test_server(cfg); let server = test_server(cfg);
test_preflight(&server, "PUT", None, "Access-Control-Request-Method", false, 0); test_preflight(&server, "PUT", None, "Access-Control-Request-Method", false, 0);
test_response(server.client().get("http://example.org/foo"), None, None, false); test_response(server.client().get("http://example.org/foo"), None, None, false);
test_response(server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN), None, None, false); test_response(
server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN),
None,
None,
false
);
} }
#[test] #[test]
fn cors_origin_star() fn cors_origin_star() {
{
let cfg = CorsConfig { let cfg = CorsConfig {
origin: Origin::Star, origin: Origin::Star,
..Default::default() ..Default::default()
@ -87,42 +125,78 @@ fn cors_origin_star()
test_preflight(&server, "PUT", Some("*"), "Access-Control-Request-Method", false, 0); test_preflight(&server, "PUT", Some("*"), "Access-Control-Request-Method", false, 0);
test_response(server.client().get("http://example.org/foo"), Some("*"), None, false); test_response(server.client().get("http://example.org/foo"), Some("*"), None, false);
test_response(server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN), Some("*"), None, false); test_response(
server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN),
Some("*"),
None,
false
);
} }
#[test] #[test]
fn cors_origin_single() fn cors_origin_single() {
{
let cfg = CorsConfig { let cfg = CorsConfig {
origin: Origin::Single("https://foo.com".to_owned()), origin: Origin::Single("https://foo.com".to_owned()),
..Default::default() ..Default::default()
}; };
let server = test_server(cfg); let server = test_server(cfg);
test_preflight(&server, "PUT", Some("https://foo.com"), "Access-Control-Request-Method", false, 0); test_preflight(
&server,
"PUT",
Some("https://foo.com"),
"Access-Control-Request-Method",
false,
0
);
test_response(server.client().get("http://example.org/foo"), Some("https://foo.com"), None, false); test_response(
test_response(server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN), Some("https://foo.com"), None, false); server.client().get("http://example.org/foo"),
Some("https://foo.com"),
None,
false
);
test_response(
server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN),
Some("https://foo.com"),
None,
false
);
} }
#[test] #[test]
fn cors_origin_copy() fn cors_origin_copy() {
{
let cfg = CorsConfig { let cfg = CorsConfig {
origin: Origin::Copy, origin: Origin::Copy,
..Default::default() ..Default::default()
}; };
let server = test_server(cfg); let server = test_server(cfg);
test_preflight(&server, "PUT", Some("http://example.org"), "Access-Control-Request-Method,Origin", false, 0); test_preflight(
&server,
"PUT",
Some("http://example.org"),
"Access-Control-Request-Method,Origin",
false,
0
);
test_response(server.client().get("http://example.org/foo"), Some("http://example.org"), Some("Origin"), false); test_response(
test_response(server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN), Some("http://example.org"), Some("Origin"), false); server.client().get("http://example.org/foo"),
Some("http://example.org"),
Some("Origin"),
false
);
test_response(
server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN),
Some("http://example.org"),
Some("Origin"),
false
);
} }
#[test] #[test]
fn cors_credentials() fn cors_credentials() {
{
let cfg = CorsConfig { let cfg = CorsConfig {
origin: Origin::None, origin: Origin::None,
credentials: true, credentials: true,
@ -133,12 +207,16 @@ fn cors_credentials()
test_preflight(&server, "PUT", None, "Access-Control-Request-Method", true, 0); test_preflight(&server, "PUT", None, "Access-Control-Request-Method", true, 0);
test_response(server.client().get("http://example.org/foo"), None, None, true); test_response(server.client().get("http://example.org/foo"), None, None, true);
test_response(server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN), None, None, true); test_response(
server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN),
None,
None,
true
);
} }
#[test] #[test]
fn cors_max_age() fn cors_max_age() {
{
let cfg = CorsConfig { let cfg = CorsConfig {
origin: Origin::None, origin: Origin::None,
max_age: 31536000, max_age: 31536000,
@ -149,5 +227,10 @@ fn cors_max_age()
test_preflight(&server, "PUT", None, "Access-Control-Request-Method", false, 31536000); test_preflight(&server, "PUT", None, "Access-Control-Request-Method", false, 31536000);
test_response(server.client().get("http://example.org/foo"), None, None, false); test_response(server.client().get("http://example.org/foo"), None, None, false);
test_response(server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN), None, None, false); test_response(
server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN),
None,
None,
false
);
} }

View file

@ -1,13 +1,8 @@
use gotham::{ use gotham::{hyper::header::CONTENT_TYPE, router::builder::*, test::TestServer};
hyper::header::CONTENT_TYPE,
router::builder::*,
test::TestServer
};
use gotham_restful::*; use gotham_restful::*;
use mime::TEXT_PLAIN; use mime::TEXT_PLAIN;
const RESPONSE: &[u8] = b"This is the only valid response.";
const RESPONSE : &[u8] = b"This is the only valid response.";
#[derive(Resource)] #[derive(Resource)]
#[resource(create)] #[resource(create)]
@ -21,23 +16,24 @@ struct Foo {
} }
#[create(FooResource)] #[create(FooResource)]
fn create(body : Foo) -> Raw<Vec<u8>> { fn create(body: Foo) -> Raw<Vec<u8>> {
Raw::new(body.content, body.content_type) Raw::new(body.content, body.content_type)
} }
#[test] #[test]
fn custom_request_body() fn custom_request_body() {
{
let server = TestServer::new(build_simple_router(|router| { let server = TestServer::new(build_simple_router(|router| {
router.resource::<FooResource>("foo"); router.resource::<FooResource>("foo");
})).unwrap(); }))
.unwrap();
let res = server.client() let res = server
.client()
.post("http://localhost/foo", RESPONSE, TEXT_PLAIN) .post("http://localhost/foo", RESPONSE, TEXT_PLAIN)
.perform().unwrap(); .perform()
.unwrap();
assert_eq!(res.headers().get(CONTENT_TYPE).unwrap().to_str().unwrap(), "text/plain"); assert_eq!(res.headers().get(CONTENT_TYPE).unwrap().to_str().unwrap(), "text/plain");
let res = res.read_body().unwrap(); let res = res.read_body().unwrap();
let body : &[u8] = res.as_ref(); let body: &[u8] = res.as_ref();
assert_eq!(body, RESPONSE); assert_eq!(body, RESPONSE);
} }

View file

@ -1,6 +1,7 @@
#![cfg(all(feature = "auth", feature = "chrono", feature = "openapi"))] #![cfg(all(feature = "auth", feature = "chrono", feature = "openapi"))]
#[macro_use] extern crate gotham_derive; #[macro_use]
extern crate gotham_derive;
use chrono::{NaiveDate, NaiveDateTime}; use chrono::{NaiveDate, NaiveDateTime};
use gotham::{ use gotham::{
@ -13,10 +14,11 @@ use mime::IMAGE_PNG;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[allow(dead_code)] #[allow(dead_code)]
mod util { include!("util/mod.rs"); } mod util {
include!("util/mod.rs");
}
use util::{test_get_response, test_openapi_response}; use util::{test_get_response, test_openapi_response};
const IMAGE_RESPONSE : &[u8] = b"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUA/wA0XsCoAAAAAXRSTlN/gFy0ywAAAApJREFUeJxjYgAAAAYAAzY3fKgAAAAASUVORK5CYII="; const IMAGE_RESPONSE : &[u8] = b"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUA/wA0XsCoAAAAAXRSTlN/gFy0ywAAAApJREFUeJxjYgAAAAYAAzY3fKgAAAAASUVORK5CYII=";
#[derive(Resource)] #[derive(Resource)]
@ -28,71 +30,59 @@ struct ImageResource;
struct Image(Vec<u8>); struct Image(Vec<u8>);
#[read(ImageResource, operation_id = "getImage")] #[read(ImageResource, operation_id = "getImage")]
fn get_image(_id : u64) -> Raw<&'static [u8]> fn get_image(_id: u64) -> Raw<&'static [u8]> {
{
Raw::new(IMAGE_RESPONSE, "image/png;base64".parse().unwrap()) Raw::new(IMAGE_RESPONSE, "image/png;base64".parse().unwrap())
} }
#[change(ImageResource, operation_id = "setImage")] #[change(ImageResource, operation_id = "setImage")]
fn set_image(_id : u64, _image : Image) fn set_image(_id: u64, _image: Image) {}
{
}
#[derive(Resource)] #[derive(Resource)]
#[resource(read, search)] #[resource(read, search)]
struct SecretResource; struct SecretResource;
#[derive(Deserialize, Clone)] #[derive(Deserialize, Clone)]
struct AuthData struct AuthData {
{ sub: String,
sub : String, iat: u64,
iat : u64, exp: u64
exp : u64
} }
type AuthStatus = gotham_restful::AuthStatus<AuthData>; type AuthStatus = gotham_restful::AuthStatus<AuthData>;
#[derive(OpenapiType, Serialize)] #[derive(OpenapiType, Serialize)]
struct Secret struct Secret {
{ code: f32
code : f32
} }
#[derive(OpenapiType, Serialize)] #[derive(OpenapiType, Serialize)]
struct Secrets struct Secrets {
{ secrets: Vec<Secret>
secrets : Vec<Secret>
} }
#[derive(Deserialize, OpenapiType, StateData, StaticResponseExtender)] #[derive(Deserialize, OpenapiType, StateData, StaticResponseExtender)]
struct SecretQuery struct SecretQuery {
{ date: NaiveDate,
date : NaiveDate, hour: Option<u16>,
hour : Option<u16>, minute: Option<u16>
minute : Option<u16>
} }
#[read(SecretResource)] #[read(SecretResource)]
fn read_secret(auth : AuthStatus, _id : NaiveDateTime) -> AuthSuccess<Secret> fn read_secret(auth: AuthStatus, _id: NaiveDateTime) -> AuthSuccess<Secret> {
{
auth.ok()?; auth.ok()?;
Ok(Secret { code: 4.2 }) Ok(Secret { code: 4.2 })
} }
#[search(SecretResource)] #[search(SecretResource)]
fn search_secret(auth : AuthStatus, _query : SecretQuery) -> AuthSuccess<Secrets> fn search_secret(auth: AuthStatus, _query: SecretQuery) -> AuthSuccess<Secrets> {
{
auth.ok()?; auth.ok()?;
Ok(Secrets { Ok(Secrets {
secrets: vec![Secret { code: 4.2 }, Secret { code: 3.14 }] secrets: vec![Secret { code: 4.2 }, Secret { code: 3.14 }]
}) })
} }
#[test] #[test]
fn openapi_supports_scope() fn openapi_supports_scope() {
{
let info = OpenapiInfo { let info = OpenapiInfo {
title: "This is just a test".to_owned(), title: "This is just a test".to_owned(),
version: "1.2.3".to_owned(), version: "1.2.3".to_owned(),
@ -110,7 +100,8 @@ fn openapi_supports_scope()
router.get_openapi("openapi"); router.get_openapi("openapi");
router.resource::<SecretResource>("secret"); router.resource::<SecretResource>("secret");
}); });
})).unwrap(); }))
.unwrap();
test_openapi_response(&server, "http://localhost/openapi", "tests/openapi_specification.json"); test_openapi_response(&server, "http://localhost/openapi", "tests/openapi_specification.json");
} }

View file

@ -1,32 +1,27 @@
#![cfg(feature = "openapi")] #![cfg(feature = "openapi")]
use gotham::{ use gotham::{router::builder::*, test::TestServer};
router::builder::*,
test::TestServer
};
use gotham_restful::*; use gotham_restful::*;
use mime::TEXT_PLAIN; use mime::TEXT_PLAIN;
#[allow(dead_code)] #[allow(dead_code)]
mod util { include!("util/mod.rs"); } mod util {
include!("util/mod.rs");
}
use util::{test_get_response, test_openapi_response}; use util::{test_get_response, test_openapi_response};
const RESPONSE: &[u8] = b"This is the only valid response.";
const RESPONSE : &[u8] = b"This is the only valid response.";
#[derive(Resource)] #[derive(Resource)]
#[resource(read_all)] #[resource(read_all)]
struct FooResource; struct FooResource;
#[read_all(FooResource)] #[read_all(FooResource)]
fn read_all() -> Raw<&'static [u8]> fn read_all() -> Raw<&'static [u8]> {
{
Raw::new(RESPONSE, TEXT_PLAIN) Raw::new(RESPONSE, TEXT_PLAIN)
} }
#[test] #[test]
fn openapi_supports_scope() fn openapi_supports_scope() {
{
let info = OpenapiInfo { let info = OpenapiInfo {
title: "Test".to_owned(), title: "Test".to_owned(),
version: "1.2.3".to_owned(), version: "1.2.3".to_owned(),
@ -44,7 +39,8 @@ fn openapi_supports_scope()
}); });
router.resource::<FooResource>("foo4"); router.resource::<FooResource>("foo4");
}); });
})).unwrap(); }))
.unwrap();
test_get_response(&server, "http://localhost/foo1", RESPONSE); test_get_response(&server, "http://localhost/foo1", RESPONSE);
test_get_response(&server, "http://localhost/bar/foo2", RESPONSE); test_get_response(&server, "http://localhost/bar/foo2", RESPONSE);

View file

@ -1,16 +1,15 @@
#[macro_use] extern crate gotham_derive; #[macro_use]
extern crate gotham_derive;
use gotham::{ use gotham::{router::builder::*, test::TestServer};
router::builder::*,
test::TestServer
};
use gotham_restful::*; use gotham_restful::*;
use mime::{APPLICATION_JSON, TEXT_PLAIN}; use mime::{APPLICATION_JSON, TEXT_PLAIN};
use serde::Deserialize; use serde::Deserialize;
mod util { include!("util/mod.rs"); } mod util {
use util::{test_get_response, test_post_response, test_put_response, test_delete_response}; include!("util/mod.rs");
}
use util::{test_delete_response, test_get_response, test_post_response, test_put_response};
#[derive(Resource)] #[derive(Resource)]
#[resource(read_all, read, search, create, change_all, change, remove_all, remove)] #[resource(read_all, read, search, create, change_all, change, remove_all, remove)]
@ -19,88 +18,96 @@ struct FooResource;
#[derive(Deserialize)] #[derive(Deserialize)]
#[cfg_attr(feature = "openapi", derive(OpenapiType))] #[cfg_attr(feature = "openapi", derive(OpenapiType))]
#[allow(dead_code)] #[allow(dead_code)]
struct FooBody struct FooBody {
{ data: String
data : String
} }
#[derive(Deserialize, StateData, StaticResponseExtender)] #[derive(Deserialize, StateData, StaticResponseExtender)]
#[cfg_attr(feature = "openapi", derive(OpenapiType))] #[cfg_attr(feature = "openapi", derive(OpenapiType))]
#[allow(dead_code)] #[allow(dead_code)]
struct FooSearch struct FooSearch {
{ query: String
query : String
} }
const READ_ALL_RESPONSE : &[u8] = b"1ARwwSPVyOKpJKrYwqGgECPVWDl1BqajAAj7g7WJ3e"; const READ_ALL_RESPONSE: &[u8] = b"1ARwwSPVyOKpJKrYwqGgECPVWDl1BqajAAj7g7WJ3e";
#[read_all(FooResource)] #[read_all(FooResource)]
fn read_all() -> Raw<&'static [u8]> fn read_all() -> Raw<&'static [u8]> {
{
Raw::new(READ_ALL_RESPONSE, TEXT_PLAIN) Raw::new(READ_ALL_RESPONSE, TEXT_PLAIN)
} }
const READ_RESPONSE : &[u8] = b"FEReHoeBKU17X2bBpVAd1iUvktFL43CDu0cFYHdaP9"; const READ_RESPONSE: &[u8] = b"FEReHoeBKU17X2bBpVAd1iUvktFL43CDu0cFYHdaP9";
#[read(FooResource)] #[read(FooResource)]
fn read(_id : u64) -> Raw<&'static [u8]> fn read(_id: u64) -> Raw<&'static [u8]> {
{
Raw::new(READ_RESPONSE, TEXT_PLAIN) Raw::new(READ_RESPONSE, TEXT_PLAIN)
} }
const SEARCH_RESPONSE : &[u8] = b"AWqcQUdBRHXKh3at4u79mdupOAfEbnTcx71ogCVF0E"; const SEARCH_RESPONSE: &[u8] = b"AWqcQUdBRHXKh3at4u79mdupOAfEbnTcx71ogCVF0E";
#[search(FooResource)] #[search(FooResource)]
fn search(_body : FooSearch) -> Raw<&'static [u8]> fn search(_body: FooSearch) -> Raw<&'static [u8]> {
{
Raw::new(SEARCH_RESPONSE, TEXT_PLAIN) Raw::new(SEARCH_RESPONSE, TEXT_PLAIN)
} }
const CREATE_RESPONSE : &[u8] = b"y6POY7wOMAB0jBRBw0FJT7DOpUNbhmT8KdpQPLkI83"; const CREATE_RESPONSE: &[u8] = b"y6POY7wOMAB0jBRBw0FJT7DOpUNbhmT8KdpQPLkI83";
#[create(FooResource)] #[create(FooResource)]
fn create(_body : FooBody) -> Raw<&'static [u8]> fn create(_body: FooBody) -> Raw<&'static [u8]> {
{
Raw::new(CREATE_RESPONSE, TEXT_PLAIN) Raw::new(CREATE_RESPONSE, TEXT_PLAIN)
} }
const CHANGE_ALL_RESPONSE : &[u8] = b"QlbYg8gHE9OQvvk3yKjXJLTSXlIrg9mcqhfMXJmQkv"; const CHANGE_ALL_RESPONSE: &[u8] = b"QlbYg8gHE9OQvvk3yKjXJLTSXlIrg9mcqhfMXJmQkv";
#[change_all(FooResource)] #[change_all(FooResource)]
fn change_all(_body : FooBody) -> Raw<&'static [u8]> fn change_all(_body: FooBody) -> Raw<&'static [u8]> {
{
Raw::new(CHANGE_ALL_RESPONSE, TEXT_PLAIN) Raw::new(CHANGE_ALL_RESPONSE, TEXT_PLAIN)
} }
const CHANGE_RESPONSE : &[u8] = b"qGod55RUXkT1lgPO8h0uVM6l368O2S0GrwENZFFuRu"; const CHANGE_RESPONSE: &[u8] = b"qGod55RUXkT1lgPO8h0uVM6l368O2S0GrwENZFFuRu";
#[change(FooResource)] #[change(FooResource)]
fn change(_id : u64, _body : FooBody) -> Raw<&'static [u8]> fn change(_id: u64, _body: FooBody) -> Raw<&'static [u8]> {
{
Raw::new(CHANGE_RESPONSE, TEXT_PLAIN) Raw::new(CHANGE_RESPONSE, TEXT_PLAIN)
} }
const REMOVE_ALL_RESPONSE : &[u8] = b"Y36kZ749MRk2Nem4BedJABOZiZWPLOtiwLfJlGTwm5"; const REMOVE_ALL_RESPONSE: &[u8] = b"Y36kZ749MRk2Nem4BedJABOZiZWPLOtiwLfJlGTwm5";
#[remove_all(FooResource)] #[remove_all(FooResource)]
fn remove_all() -> Raw<&'static [u8]> fn remove_all() -> Raw<&'static [u8]> {
{
Raw::new(REMOVE_ALL_RESPONSE, TEXT_PLAIN) Raw::new(REMOVE_ALL_RESPONSE, TEXT_PLAIN)
} }
const REMOVE_RESPONSE : &[u8] = b"CwRzBrKErsVZ1N7yeNfjZuUn1MacvgBqk4uPOFfDDq"; const REMOVE_RESPONSE: &[u8] = b"CwRzBrKErsVZ1N7yeNfjZuUn1MacvgBqk4uPOFfDDq";
#[remove(FooResource)] #[remove(FooResource)]
fn remove(_id : u64) -> Raw<&'static [u8]> fn remove(_id: u64) -> Raw<&'static [u8]> {
{
Raw::new(REMOVE_RESPONSE, TEXT_PLAIN) Raw::new(REMOVE_RESPONSE, TEXT_PLAIN)
} }
#[test] #[test]
fn sync_methods() fn sync_methods() {
{
let server = TestServer::new(build_simple_router(|router| { let server = TestServer::new(build_simple_router(|router| {
router.resource::<FooResource>("foo"); router.resource::<FooResource>("foo");
})).unwrap(); }))
.unwrap();
test_get_response(&server, "http://localhost/foo", READ_ALL_RESPONSE); test_get_response(&server, "http://localhost/foo", READ_ALL_RESPONSE);
test_get_response(&server, "http://localhost/foo/1", READ_RESPONSE); test_get_response(&server, "http://localhost/foo/1", READ_RESPONSE);
test_get_response(&server, "http://localhost/foo/search?query=hello+world", SEARCH_RESPONSE); test_get_response(&server, "http://localhost/foo/search?query=hello+world", SEARCH_RESPONSE);
test_post_response(&server, "http://localhost/foo", r#"{"data":"hello world"}"#, APPLICATION_JSON, CREATE_RESPONSE); test_post_response(
test_put_response(&server, "http://localhost/foo", r#"{"data":"hello world"}"#, APPLICATION_JSON, CHANGE_ALL_RESPONSE); &server,
test_put_response(&server, "http://localhost/foo/1", r#"{"data":"hello world"}"#, APPLICATION_JSON, CHANGE_RESPONSE); "http://localhost/foo",
r#"{"data":"hello world"}"#,
APPLICATION_JSON,
CREATE_RESPONSE
);
test_put_response(
&server,
"http://localhost/foo",
r#"{"data":"hello world"}"#,
APPLICATION_JSON,
CHANGE_ALL_RESPONSE
);
test_put_response(
&server,
"http://localhost/foo/1",
r#"{"data":"hello world"}"#,
APPLICATION_JSON,
CHANGE_RESPONSE
);
test_delete_response(&server, "http://localhost/foo", REMOVE_ALL_RESPONSE); test_delete_response(&server, "http://localhost/foo", REMOVE_ALL_RESPONSE);
test_delete_response(&server, "http://localhost/foo/1", REMOVE_RESPONSE); test_delete_response(&server, "http://localhost/foo/1", REMOVE_RESPONSE);
} }

View file

@ -2,8 +2,7 @@ use trybuild::TestCases;
#[test] #[test]
#[ignore] #[ignore]
fn trybuild_ui() fn trybuild_ui() {
{
let t = TestCases::new(); let t = TestCases::new();
// always enabled // always enabled
@ -18,8 +17,7 @@ fn trybuild_ui()
t.compile_fail("tests/ui/resource_unknown_method.rs"); t.compile_fail("tests/ui/resource_unknown_method.rs");
// require the openapi feature // require the openapi feature
if cfg!(feature = "openapi") if cfg!(feature = "openapi") {
{
t.compile_fail("tests/ui/openapi_type_enum_with_fields.rs"); t.compile_fail("tests/ui/openapi_type_enum_with_fields.rs");
t.compile_fail("tests/ui/openapi_type_nullable_non_bool.rs"); t.compile_fail("tests/ui/openapi_type_nullable_non_bool.rs");
t.compile_fail("tests/ui/openapi_type_rename_non_string.rs"); t.compile_fail("tests/ui/openapi_type_rename_non_string.rs");