From f425f21ff3114592f444bbcfb6240755b320d34a Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 14 Apr 2020 17:44:07 +0200 Subject: [PATCH 001/170] update jsonwebtoken, futures, and hyper and co --- Cargo.toml | 2 +- example/Cargo.toml | 5 +-- gotham_restful/Cargo.toml | 19 +++++---- gotham_restful/src/auth.rs | 18 +++++---- gotham_restful/src/lib.rs | 8 ++-- gotham_restful/src/openapi/router.rs | 13 +++--- gotham_restful/src/result.rs | 23 ++++++----- gotham_restful/src/routing.rs | 56 +++++++++++++------------- gotham_restful/src/types.rs | 6 +-- gotham_restful_derive/src/from_body.rs | 2 +- gotham_restful_derive/src/method.rs | 2 +- 11 files changed, 83 insertions(+), 71 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ce5a3ac..8efb671 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,4 +10,4 @@ members = [ [patch.crates-io] gotham_restful = { path = "./gotham_restful" } gotham_restful_derive = { path = "./gotham_restful_derive" } -openapiv3 = { git = "https://github.com/glademiller/openapiv3", rev = "4c3bd95c966a3f9d59bb494c3d8e30c5c3068bdb" } \ No newline at end of file +openapiv3 = { git = "https://github.com/glademiller/openapiv3", rev = "4c3bd95c966a3f9d59bb494c3d8e30c5c3068bdb" } diff --git a/example/Cargo.toml b/example/Cargo.toml index a1ff5df..425f161 100644 --- a/example/Cargo.toml +++ b/example/Cargo.toml @@ -15,10 +15,9 @@ gitlab = { repository = "msrd0/gotham-restful", branch = "master" } [dependencies] fake = "2.2" -gotham = "0.4" -gotham_derive = "0.4" +gotham = { git = "https://github.com/gotham-rs/gotham", version = "0.5.0-dev", default-features = false } +gotham_derive = { git = "https://github.com/gotham-rs/gotham", version = "0.5.0-dev", default-features = false } gotham_restful = { version = "0.0.3", features = ["auth", "openapi"] } -hyper = "0.12" log = "0.4" log4rs = { version = "0.8", features = ["console_appender"], default-features = false } serde = "1" diff --git a/gotham_restful/Cargo.toml b/gotham_restful/Cargo.toml index 4aad918..a7eddab 100644 --- a/gotham_restful/Cargo.toml +++ b/gotham_restful/Cargo.toml @@ -15,17 +15,19 @@ repository = "https://gitlab.com/msrd0/gotham-restful" gitlab = { repository = "msrd0/gotham-restful", branch = "master" } [dependencies] -base64 = { version = ">=0.10.1, <0.12", optional = true } +base64 = { version = "0.12.0", optional = true } chrono = { version = "0.4.10", optional = true } -cookie = { version = "0.12", optional = true } -futures = "0.1.29" -gotham = "0.4" -gotham_derive = "0.4" -gotham_middleware_diesel = { version = "0.1", optional = true } +cookie = { version = "0.13.3", optional = true } +futures = "0.3.4" +futures-core = "0.3.4" +futures-util = "0.3.4" +gotham = { git = "https://github.com/gotham-rs/gotham", version = "0.5.0-dev", default-features = false } +gotham_derive = { git = "https://github.com/gotham-rs/gotham", version = "0.5.0-dev" } +gotham_middleware_diesel = { git = "https://github.com/gotham-rs/gotham", version = "0.1.0", optional = true } gotham_restful_derive = { version = "0.0.2" } -hyper = "0.12.35" +hyper = "0.13.4" indexmap = { version = "1.3.0", optional = true } -jsonwebtoken = { version = "6.0.1", optional = true } +jsonwebtoken = { version = "7.1.0", optional = true } log = { version = "0.4.8", optional = true } mime = "0.3.16" openapiv3 = { version = "0.3", optional = true } @@ -34,6 +36,7 @@ serde_json = "1.0.45" uuid = { version = ">= 0.1, < 0.9", optional = true } [dev-dependencies] +futures-executor = "0.3.4" paste = "0.1.10" thiserror = "1" diff --git a/gotham_restful/src/auth.rs b/gotham_restful/src/auth.rs index 54c6a89..7ee6880 100644 --- a/gotham_restful/src/auth.rs +++ b/gotham_restful/src/auth.rs @@ -1,17 +1,21 @@ use crate::HeaderName; use cookie::CookieJar; -use futures::{future, future::Future}; +use futures_util::{future, future::{FutureExt, TryFutureExt}}; use gotham::{ handler::HandlerFuture, middleware::{Middleware, NewMiddleware}, state::{FromState, State} }; use hyper::header::{AUTHORIZATION, HeaderMap}; -use jsonwebtoken::errors::ErrorKind; +use jsonwebtoken::{ + errors::ErrorKind, + DecodingKey +}; use serde::de::DeserializeOwned; use std::{ marker::PhantomData, - panic::RefUnwindSafe + panic::RefUnwindSafe, + pin::Pin }; pub use jsonwebtoken::Validation as AuthValidation; @@ -248,7 +252,7 @@ where }; // validate the token - let data : Data = match jsonwebtoken::decode(&token, &secret, &self.validation) { + let data : Data = match jsonwebtoken::decode(&token, &DecodingKey::from_secret(&secret), &self.validation) { Ok(data) => data.claims, Err(e) => match dbg!(e.into_kind()) { ErrorKind::ExpiredSignature => return AuthStatus::Expired, @@ -266,9 +270,9 @@ where Data : DeserializeOwned + Send + 'static, Handler : AuthHandler { - fn call(self, mut state : State, chain : Chain) -> Box + fn call(self, mut state : State, chain : Chain) -> Pin> where - Chain : FnOnce(State) -> Box + Chain : FnOnce(State) -> Pin> { // put the source in our state, required for e.g. openapi state.put(self.source.clone()); @@ -278,7 +282,7 @@ where state.put(status); // call the rest of the chain - Box::new(chain(state).and_then(|(state, res)| future::ok((state, res)))) + chain(state).and_then(|(state, res)| future::ok((state, res))).boxed() } } diff --git a/gotham_restful/src/lib.rs b/gotham_restful/src/lib.rs index a30ba32..7b4006a 100644 --- a/gotham_restful/src/lib.rs +++ b/gotham_restful/src/lib.rs @@ -105,7 +105,7 @@ extern crate self as gotham_restful; #[macro_use] extern crate serde; #[doc(no_inline)] -pub use hyper::{header::HeaderName, Chunk, StatusCode}; +pub use gotham::hyper::{header::HeaderName, StatusCode}; #[doc(no_inline)] pub use mime::Mime; @@ -115,8 +115,10 @@ pub use gotham_restful_derive::*; #[doc(hidden)] pub mod export { - pub use futures::future::Future; - pub use gotham::state::{FromState, State}; + pub use gotham::{ + hyper::body::Bytes, + state::{FromState, State} + }; #[cfg(feature = "database")] pub use gotham_middleware_diesel::Repo; diff --git a/gotham_restful/src/openapi/router.rs b/gotham_restful/src/openapi/router.rs index 1789c80..2bf93ef 100644 --- a/gotham_restful/src/openapi/router.rs +++ b/gotham_restful/src/openapi/router.rs @@ -6,7 +6,7 @@ use crate::{ OpenapiType, RequestBody }; -use futures::future::ok; +use futures_util::{future, future::FutureExt}; use gotham::{ handler::{Handler, HandlerFuture, NewHandler}, helpers::http::response::create_response, @@ -22,7 +22,10 @@ use openapiv3::{ ReferenceOr, ReferenceOr::Item, ReferenceOr::Reference, RequestBody as OARequestBody, Response, Responses, Schema, SchemaKind, SecurityScheme, Server, StatusCode, Type }; -use std::panic::RefUnwindSafe; +use std::{ + panic::RefUnwindSafe, + pin::Pin +}; /** This type is required to build routes while adding them to the generated OpenAPI Spec at the @@ -175,7 +178,7 @@ fn get_security(state : &mut State) -> (Vec, IndexMap Box + fn handle(self, mut state : State) -> Pin> { let mut openapi = self.0; let security_schemes = get_security(&mut state); @@ -186,12 +189,12 @@ impl Handler for OpenapiHandler match serde_json::to_string(&openapi) { Ok(body) => { let res = create_response(&state, hyper::StatusCode::OK, APPLICATION_JSON, body); - Box::new(ok((state, res))) + future::ok((state, res)).boxed() }, Err(e) => { error!("Unable to handle OpenAPI request due to error: {}", e); let res = create_response(&state, hyper::StatusCode::INTERNAL_SERVER_ERROR, TEXT_PLAIN, ""); - Box::new(ok((state, res))) + future::ok((state, res)).boxed() } } } diff --git a/gotham_restful/src/result.rs b/gotham_restful/src/result.rs index 55f4c3c..d6ff755 100644 --- a/gotham_restful/src/result.rs +++ b/gotham_restful/src/result.rs @@ -1,7 +1,7 @@ use crate::{ResponseBody, StatusCode}; #[cfg(feature = "openapi")] use crate::{OpenapiSchema, OpenapiType}; -use hyper::Body; +use gotham::hyper::Body; #[cfg(feature = "errorlog")] use log::error; use mime::{Mime, APPLICATION_JSON, STAR_STAR}; @@ -65,12 +65,13 @@ impl Response } #[cfg(test)] - fn full_body(self) -> Vec + fn full_body(mut self) -> Result, ::Error> { - use futures::{future::Future, stream::Stream}; + use futures_executor::block_on; + use gotham::hyper::body::to_bytes; - let bytes : &[u8] = &self.body.concat2().wait().unwrap().into_bytes(); - bytes.to_vec() + let bytes : &[u8] = &block_on(to_bytes(&mut self.body))?; + Ok(bytes.to_vec()) } } @@ -532,7 +533,7 @@ mod test let res = ok.into_response().expect("didn't expect error response"); assert_eq!(res.status, StatusCode::OK); assert_eq!(res.mime, Some(APPLICATION_JSON)); - assert_eq!(res.full_body(), r#"{"msg":""}"#.as_bytes()); + assert_eq!(res.full_body().unwrap(), r#"{"msg":""}"#.as_bytes()); } #[test] @@ -542,7 +543,7 @@ mod test let res = err.into_response().expect("didn't expect error response"); assert_eq!(res.status, StatusCode::INTERNAL_SERVER_ERROR); assert_eq!(res.mime, Some(APPLICATION_JSON)); - assert_eq!(res.full_body(), 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] @@ -552,7 +553,7 @@ mod test let res = success.into_response().expect("didn't expect error response"); assert_eq!(res.status, StatusCode::OK); assert_eq!(res.mime, Some(APPLICATION_JSON)); - assert_eq!(res.full_body(), r#"{"msg":""}"#.as_bytes()); + assert_eq!(res.full_body().unwrap(), r#"{"msg":""}"#.as_bytes()); } #[test] @@ -562,7 +563,7 @@ mod test let res = no_content.into_response().expect("didn't expect error response"); assert_eq!(res.status, StatusCode::NO_CONTENT); assert_eq!(res.mime, None); - assert_eq!(res.full_body(), &[] as &[u8]); + assert_eq!(res.full_body().unwrap(), &[] as &[u8]); } #[test] @@ -572,7 +573,7 @@ mod test let res = no_content.into_response().expect("didn't expect error response"); assert_eq!(res.status, StatusCode::NO_CONTENT); assert_eq!(res.mime, None); - assert_eq!(res.full_body(), &[] as &[u8]); + assert_eq!(res.full_body().unwrap(), &[] as &[u8]); } #[test] @@ -583,6 +584,6 @@ mod test let res = raw.into_response().expect("didn't expect error response"); assert_eq!(res.status, StatusCode::OK); assert_eq!(res.mime, Some(TEXT_PLAIN)); - assert_eq!(res.full_body(), msg.as_bytes()); + assert_eq!(res.full_body().unwrap(), msg.as_bytes()); } } diff --git a/gotham_restful/src/routing.rs b/gotham_restful/src/routing.rs index c5a866c..6385300 100644 --- a/gotham_restful/src/routing.rs +++ b/gotham_restful/src/routing.rs @@ -7,12 +7,9 @@ use crate::{ #[cfg(feature = "openapi")] use crate::OpenapiRouter; -use futures::{ - future::{Future, err, ok}, - stream::Stream -}; +use futures_util::{future, future::FutureExt}; use gotham::{ - handler::{HandlerFuture, IntoHandlerError}, + handler::{HandlerFuture, IntoHandlerError, IntoHandlerFuture}, helpers::http::response::{create_empty_response, create_response}, pipeline::chain::PipelineHandleChain, router::{ @@ -26,14 +23,18 @@ use gotham::{ }, state::{FromState, State} }; -use hyper::{ +use gotham::hyper::{ + body::to_bytes, header::CONTENT_TYPE, Body, HeaderMap, Method }; use mime::{Mime, APPLICATION_JSON}; -use std::panic::RefUnwindSafe; +use std::{ + panic::RefUnwindSafe, + pin::Pin +}; /// Allow us to extract an id from a path. #[derive(Deserialize, StateData, StaticResponseExtender)] @@ -88,7 +89,7 @@ fn response_from(res : Response, state : &State) -> hyper::Response r } -fn to_handler_future(mut state : State, get_result : F) -> Box +fn to_handler_future(mut state : State, get_result : F) -> Pin> where F : FnOnce(&mut State) -> R, R : ResourceResult @@ -97,32 +98,31 @@ where match res { Ok(res) => { let r = response_from(res, &state); - Box::new(ok((state, r))) + (state, r).into_handler_future() }, - Err(e) => Box::new(err((state, e.into_handler_error()))) + Err(e) => future::err((state, e.into_handler_error())).boxed() } } -fn handle_with_body(mut state : State, get_result : F) -> Box +fn handle_with_body(mut state : State, get_result : F) -> Pin> where Body : RequestBody, F : FnOnce(&mut State, Body) -> R + Send + 'static, R : ResourceResult { - let f = hyper::Body::take_from(&mut state) - .concat2() + let f = to_bytes(gotham::hyper::Body::take_from(&mut state)) .then(|body| { let body = match body { Ok(body) => body, - Err(e) => return err((state, e.into_handler_error())) + Err(e) => return future::err((state, e.into_handler_error())) }; let content_type : Mime = match HeaderMap::borrow_from(&state).get(CONTENT_TYPE) { Some(content_type) => content_type.to_str().unwrap().parse().unwrap(), None => { let res = create_empty_response(&state, StatusCode::UNSUPPORTED_MEDIA_TYPE); - return ok((state, res)) + return future::ok((state, res)) } }; @@ -133,9 +133,9 @@ where match serde_json::to_string(&error) { Ok(json) => { let res = create_response(&state, StatusCode::BAD_REQUEST, APPLICATION_JSON, json); - ok((state, res)) + future::ok((state, res)) }, - Err(e) => err((state, e.into_handler_error())) + Err(e) => future::err((state, e.into_handler_error())) } } }; @@ -144,22 +144,22 @@ where match res { Ok(res) => { let r = response_from(res, &state); - ok((state, r)) + future::ok((state, r)) }, - Err(e) => err((state, e.into_handler_error())) + Err(e) => future::err((state, e.into_handler_error())) } }); - Box::new(f) + f.boxed() } -fn read_all_handler(state : State) -> Box +fn read_all_handler(state : State) -> Pin> { to_handler_future(state, |state| Handler::read_all(state)) } -fn read_handler(state : State) -> Box +fn read_handler(state : State) -> Pin> { let id = { let path : &PathExtractor = PathExtractor::borrow_from(&state); @@ -168,23 +168,23 @@ fn read_handler(state : State) -> Box to_handler_future(state, |state| Handler::read(state, id)) } -fn search_handler(mut state : State) -> Box +fn search_handler(mut state : State) -> Pin> { let query = Handler::Query::take_from(&mut state); to_handler_future(state, |state| Handler::search(state, query)) } -fn create_handler(state : State) -> Box +fn create_handler(state : State) -> Pin> { handle_with_body::(state, |state, body| Handler::create(state, body)) } -fn update_all_handler(state : State) -> Box +fn update_all_handler(state : State) -> Pin> { handle_with_body::(state, |state, body| Handler::update_all(state, body)) } -fn update_handler(state : State) -> Box +fn update_handler(state : State) -> Pin> { let id = { let path : &PathExtractor = PathExtractor::borrow_from(&state); @@ -193,12 +193,12 @@ fn update_handler(state : State) -> Box handle_with_body::(state, |state, body| Handler::update(state, id, body)) } -fn delete_all_handler(state : State) -> Box +fn delete_all_handler(state : State) -> Pin> { to_handler_future(state, |state| Handler::delete_all(state)) } -fn delete_handler(state : State) -> Box +fn delete_handler(state : State) -> Pin> { let id = { let path : &PathExtractor = PathExtractor::borrow_from(&state); diff --git a/gotham_restful/src/types.rs b/gotham_restful/src/types.rs index 6764595..d3a8e76 100644 --- a/gotham_restful/src/types.rs +++ b/gotham_restful/src/types.rs @@ -2,7 +2,7 @@ use crate::OpenapiType; use crate::result::ResourceError; -use hyper::Chunk; +use gotham::hyper::body::Bytes; use mime::{Mime, APPLICATION_JSON}; use serde::{de::DeserializeOwned, Serialize}; @@ -46,14 +46,14 @@ pub trait FromBody : Sized type Err : Into; /// Create the request body from a raw body and the content type. - fn from_body(body : Chunk, content_type : Mime) -> Result; + fn from_body(body : Bytes, content_type : Mime) -> Result; } impl FromBody for T { type Err = serde_json::Error; - fn from_body(body : Chunk, _content_type : Mime) -> Result + fn from_body(body : Bytes, _content_type : Mime) -> Result { serde_json::from_slice(&body) } diff --git a/gotham_restful_derive/src/from_body.rs b/gotham_restful_derive/src/from_body.rs index 78ac7d0..4884659 100644 --- a/gotham_restful_derive/src/from_body.rs +++ b/gotham_restful_derive/src/from_body.rs @@ -58,7 +58,7 @@ fn expand(tokens : TokenStream) -> Result { type Err = String; - fn from_body(body : #krate::Chunk, _content_type : #krate::Mime) -> Result + fn from_body(body : #krate::export::Bytes, _content_type : #krate::Mime) -> Result { let body : &[u8] = &body; Ok(#body) diff --git a/gotham_restful_derive/src/method.rs b/gotham_restful_derive/src/method.rs index 7e50f13..a4c3fb8 100644 --- a/gotham_restful_derive/src/method.rs +++ b/gotham_restful_derive/src/method.rs @@ -435,7 +435,7 @@ fn expand(method : Method, attrs : TokenStream, item : TokenStream) -> Result #ret { #[allow(unused_imports)] - use #krate::export::{Future, FromState}; + use #krate::export::FromState; #block } From ede0d751615fe494a95b7c464c0a1f81ff9f6d92 Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 14 Apr 2020 21:17:12 +0200 Subject: [PATCH 002/170] start moving shit over to async --- gotham_restful/src/result.rs | 128 +++++++++++++++------ gotham_restful/src/routing.rs | 204 +++++++++++++++++++++++----------- 2 files changed, 239 insertions(+), 93 deletions(-) diff --git a/gotham_restful/src/result.rs b/gotham_restful/src/result.rs index d6ff755..3108749 100644 --- a/gotham_restful/src/result.rs +++ b/gotham_restful/src/result.rs @@ -1,6 +1,8 @@ use crate::{ResponseBody, StatusCode}; #[cfg(feature = "openapi")] use crate::{OpenapiSchema, OpenapiType}; +use futures_core::future::Future; +use futures_util::{future, future::FutureExt}; use gotham::hyper::Body; #[cfg(feature = "errorlog")] use log::error; @@ -11,7 +13,8 @@ use serde::Serialize; use serde_json::error::Error as SerdeJsonError; use std::{ error::Error, - fmt::Debug + fmt::Debug, + pin::Pin }; /// A response, used to create the final gotham response from. @@ -75,12 +78,15 @@ impl Response } } + /// A trait provided to convert a resource's result to json. pub trait ResourceResult { + type Err : Error + Send + 'static; + /// Turn this into a response that can be returned to the browser. This api will likely /// change in the future. - fn into_response(self) -> Result; + fn into_response(self) -> Pin> + Send>>; /// Return a list of supported mime types. fn accepted_types() -> Option> @@ -126,6 +132,15 @@ impl From for ResourceError } } +fn into_response_helper(create_response : F) -> Pin> + Send>> +where + Err : Send + 'static, + F : FnOnce() -> Result +{ + let res = create_response(); + async move { res }.boxed() +} + #[cfg(feature = "errorlog")] fn errorlog(e : E) { @@ -137,15 +152,19 @@ fn errorlog(_e : E) {} impl ResourceResult for Result { - fn into_response(self) -> Result + type Err = SerdeJsonError; + + fn into_response(self) -> Pin> + Send>> { - Ok(match self { - Ok(r) => Response::json(StatusCode::OK, serde_json::to_string(&r)?), - Err(e) => { - errorlog(&e); - let err : ResourceError = e.into(); - Response::json(StatusCode::INTERNAL_SERVER_ERROR, serde_json::to_string(&err)?) - } + into_response_helper(|| { + Ok(match self { + Ok(r) => Response::json(StatusCode::OK, serde_json::to_string(&r)?), + Err(e) => { + errorlog(&e); + let err : ResourceError = e.into(); + Response::json(StatusCode::INTERNAL_SERVER_ERROR, serde_json::to_string(&err)?) + } + }) }) } @@ -161,6 +180,40 @@ impl ResourceResult for Result } } + +impl ResourceResult for Pin + Send>> +where + Res : ResourceResult + 'static, + dyn Future> : Send +{ + type Err = Res::Err; + + fn into_response(self) -> Pin> + Send>> + { + self.then(|result| { + result.into_response() + }).boxed() + } + + fn accepted_types() -> Option> + { + Res::accepted_types() + } + + #[cfg(feature = "openapi")] + fn schema() -> OpenapiSchema + { + Res::schema() + } + + #[cfg(feature = "openapi")] + fn default_status() -> StatusCode + { + Res::default_status() + } +} + + /** This can be returned from a resource when there is no cause of an error. For example: @@ -215,9 +268,11 @@ impl Debug for Success impl ResourceResult for Success { - fn into_response(self) -> Result + type Err = SerdeJsonError; + + fn into_response(self) -> Pin> + Send>> { - Ok(Response::json(StatusCode::OK, serde_json::to_string(&self.0)?)) + into_response_helper(|| Ok(Response::json(StatusCode::OK, serde_json::to_string(&self.0)?))) } fn accepted_types() -> Option> @@ -318,12 +373,14 @@ impl Debug for AuthResult impl ResourceResult for AuthResult { - fn into_response(self) -> Result + type Err = T::Err; + + fn into_response(self) -> Pin> + Send>> { match self { Self::Ok(res) => res.into_response(), - Self::AuthErr => Ok(Response::forbidden()) + Self::AuthErr => future::ok(Response::forbidden()).boxed() } } @@ -379,10 +436,12 @@ impl From<()> for NoContent impl ResourceResult for NoContent { + type Err = SerdeJsonError; // just for easier handling of `Result` + /// This will always be a _204 No Content_ together with an empty string. - fn into_response(self) -> Result + fn into_response(self) -> Pin> + Send>> { - Ok(Response::no_content()) + future::ok(Response::no_content()).boxed() } /// Returns the schema of the `()` type. @@ -402,14 +461,16 @@ impl ResourceResult for NoContent impl ResourceResult for Result { - fn into_response(self) -> Result + type Err = SerdeJsonError; + + fn into_response(self) -> Pin> + Send>> { match self { Ok(nc) => nc.into_response(), - Err(e) => { + Err(e) => into_response_helper(|| { let err : ResourceError = e.into(); Ok(Response::json(StatusCode::INTERNAL_SERVER_ERROR, serde_json::to_string(&err)?)) - } + }) } } @@ -460,9 +521,11 @@ impl Debug for Raw impl> ResourceResult for Raw { - fn into_response(self) -> Result + type Err = SerdeJsonError; // just for easier handling of `Result, E>` + + fn into_response(self) -> Pin> + Send>> { - Ok(Response::new(StatusCode::OK, self.raw, Some(self.mime.clone()))) + future::ok(Response::new(StatusCode::OK, self.raw, Some(self.mime.clone()))).boxed() } fn accepted_types() -> Option> @@ -482,16 +545,18 @@ impl> ResourceResult for Raw impl ResourceResult for Result, E> where - Raw : ResourceResult + Raw : ResourceResult { - fn into_response(self) -> Result + type Err = SerdeJsonError; + + fn into_response(self) -> Pin> + Send>> { match self { Ok(raw) => raw.into_response(), - Err(e) => { + Err(e) => into_response_helper(|| { let err : ResourceError = e.into(); Ok(Response::json(StatusCode::INTERNAL_SERVER_ERROR, serde_json::to_string(&err)?)) - } + }) } } @@ -512,6 +577,7 @@ where mod test { use super::*; + use futures_executor::block_on; use mime::TEXT_PLAIN; use thiserror::Error; @@ -530,7 +596,7 @@ mod test fn resource_result_ok() { let ok : Result = Ok(Msg::default()); - let res = 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.mime, Some(APPLICATION_JSON)); assert_eq!(res.full_body().unwrap(), r#"{"msg":""}"#.as_bytes()); @@ -540,7 +606,7 @@ mod test fn resource_result_err() { let err : Result = Err(MsgError::default()); - let res = 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.mime, Some(APPLICATION_JSON)); assert_eq!(res.full_body().unwrap(), format!(r#"{{"error":true,"message":"{}"}}"#, MsgError::default()).as_bytes()); @@ -550,7 +616,7 @@ mod test fn success_always_successfull() { let success : Success = Msg::default().into(); - let res = 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.mime, Some(APPLICATION_JSON)); assert_eq!(res.full_body().unwrap(), r#"{"msg":""}"#.as_bytes()); @@ -560,7 +626,7 @@ mod test fn no_content_has_empty_response() { let no_content = NoContent::default(); - let res = 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.mime, None); assert_eq!(res.full_body().unwrap(), &[] as &[u8]); @@ -570,7 +636,7 @@ mod test fn no_content_result() { let no_content : Result = Ok(NoContent::default()); - let res = 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.mime, None); assert_eq!(res.full_body().unwrap(), &[] as &[u8]); @@ -581,7 +647,7 @@ mod test { let msg = "Test"; let raw = Raw::new(msg, TEXT_PLAIN); - let res = raw.into_response().expect("didn't expect error response"); + let res = block_on(raw.into_response()).expect("didn't expect error response"); assert_eq!(res.status, StatusCode::OK); assert_eq!(res.mime, Some(TEXT_PLAIN)); assert_eq!(res.full_body().unwrap(), msg.as_bytes()); diff --git a/gotham_restful/src/routing.rs b/gotham_restful/src/routing.rs index 6385300..8aac5ea 100644 --- a/gotham_restful/src/routing.rs +++ b/gotham_restful/src/routing.rs @@ -7,9 +7,10 @@ use crate::{ #[cfg(feature = "openapi")] use crate::OpenapiRouter; +use futures_core::future::Future; use futures_util::{future, future::FutureExt}; use gotham::{ - handler::{HandlerFuture, IntoHandlerError, IntoHandlerFuture}, + handler::{HandlerError, HandlerFuture, IntoHandlerError, IntoHandlerFuture}, helpers::http::response::{create_empty_response, create_response}, pipeline::chain::PipelineHandleChain, router::{ @@ -65,14 +66,40 @@ pub trait DrawResources /// `Resource::setup` method. pub trait DrawResourceRoutes { - fn read_all(&mut self); - fn read(&mut self); - fn search(&mut self); - fn create(&mut self); - fn update_all(&mut self); - fn update(&mut self); - fn delete_all(&mut self); - fn delete(&mut self); + fn read_all(&mut self) + where + dyn Future::Err>> : Send; + + fn read(&mut self) + where + dyn Future::Err>> : Send; + + fn search(&mut self) + where + dyn Future::Err>> : Send; + + fn create(&mut self) + where + Handler::Res : Send + 'static, + Handler::Body : 'static; + + fn update_all(&mut self) + where + Handler::Res : Send + 'static, + Handler::Body : 'static; + + fn update(&mut self) + where + Handler::Res : Send + 'static, + Handler::Body : 'static; + + fn delete_all(&mut self) + where + dyn Future::Err>> : Send; + + fn delete(&mut self) + where + dyn Future::Err>> : Send; } fn response_from(res : Response, state : &State) -> hyper::Response @@ -92,74 +119,93 @@ fn response_from(res : Response, state : &State) -> hyper::Response fn to_handler_future(mut state : State, get_result : F) -> Pin> where F : FnOnce(&mut State) -> R, - R : ResourceResult + R : ResourceResult, + dyn Future> : Send { - let res = get_result(&mut state).into_response(); - match res { - Ok(res) => { - let r = response_from(res, &state); - (state, r).into_handler_future() - }, - Err(e) => future::err((state, e.into_handler_error())).boxed() - } -} - -fn handle_with_body(mut state : State, get_result : F) -> Pin> -where - Body : RequestBody, - F : FnOnce(&mut State, Body) -> R + Send + 'static, - R : ResourceResult -{ - let f = to_bytes(gotham::hyper::Body::take_from(&mut state)) - .then(|body| { - - let body = match body { - Ok(body) => body, - Err(e) => return future::err((state, e.into_handler_error())) - }; - - let content_type : Mime = match HeaderMap::borrow_from(&state).get(CONTENT_TYPE) { - Some(content_type) => content_type.to_str().unwrap().parse().unwrap(), - None => { - let res = create_empty_response(&state, StatusCode::UNSUPPORTED_MEDIA_TYPE); - return future::ok((state, res)) - } - }; - - let body = match Body::from_body(body, content_type) { - Ok(body) => body, - Err(e) => return { - let error : ResourceError = e.into(); - match serde_json::to_string(&error) { - Ok(json) => { - let res = create_response(&state, StatusCode::BAD_REQUEST, APPLICATION_JSON, json); - future::ok((state, res)) - }, - Err(e) => future::err((state, e.into_handler_error())) - } - } - }; - - let res = get_result(&mut state, body).into_response(); + get_result(&mut state).into_response() + .then(|res| match res { Ok(res) => { let r = response_from(res, &state); - future::ok((state, r)) + (state, r).into_handler_future() }, - Err(e) => future::err((state, e.into_handler_error())) + Err(e) => future::err((state, e.into_handler_error())).boxed() } - - }); + ).boxed() +} - f.boxed() +async fn body_to_res(state : &mut State, get_result : F) -> Result, HandlerError> +where + B : RequestBody, + F : FnOnce(&mut State, B) -> R, + R : ResourceResult +{ + let body = to_bytes(Body::take_from(&mut state)).await; + + let body = match body { + Ok(body) => body, + Err(e) => return Err(e.into_handler_error()) + }; + + let content_type : Mime = match HeaderMap::borrow_from(&state).get(CONTENT_TYPE) { + Some(content_type) => content_type.to_str().unwrap().parse().unwrap(), + None => { + let res = create_empty_response(&state, StatusCode::UNSUPPORTED_MEDIA_TYPE); + return Ok(res) + } + }; + + let res = { + let body = match B::from_body(body, content_type) { + Ok(body) => body, + Err(e) => return { + let error : ResourceError = e.into(); + match serde_json::to_string(&error) { + Ok(json) => { + let res = create_response(&state, StatusCode::BAD_REQUEST, APPLICATION_JSON, json); + Ok(res) + }, + Err(e) => Err(e.into_handler_error()) + } + } + }; + get_result(&mut state, body) + }; + + let res = res.into_response().await; + match res { + Ok(res) => { + let r = response_from(res, &state); + Ok(r) + }, + Err(e) => Err(e.into_handler_error()) + } +} + +fn handle_with_body(mut state : State, get_result : F) -> Pin> +where + B : RequestBody + 'static, + F : FnOnce(&mut State, B) -> R + Send + 'static, + R : ResourceResult + Send + 'static +{ + body_to_res(&mut state, get_result) + .then(|res| match res { + Ok(ok) => future::ok((state, ok)), + Err(err) => future::err((state, err)) + }) + .boxed() } fn read_all_handler(state : State) -> Pin> +where + dyn Future::Err>> : Send { to_handler_future(state, |state| Handler::read_all(state)) } fn read_handler(state : State) -> Pin> +where + dyn Future::Err>> : Send { let id = { let path : &PathExtractor = PathExtractor::borrow_from(&state); @@ -169,22 +215,33 @@ fn read_handler(state : State) -> Pin } fn search_handler(mut state : State) -> Pin> +where + dyn Future::Err>> : Send { let query = Handler::Query::take_from(&mut state); to_handler_future(state, |state| Handler::search(state, query)) } fn create_handler(state : State) -> Pin> +where + Handler::Res : Send + 'static, + Handler::Body : 'static { handle_with_body::(state, |state, body| Handler::create(state, body)) } fn update_all_handler(state : State) -> Pin> +where + Handler::Res : Send + 'static, + Handler::Body : 'static { handle_with_body::(state, |state, body| Handler::update_all(state, body)) } fn update_handler(state : State) -> Pin> +where + Handler::Res : Send + 'static, + Handler::Body : 'static { let id = { let path : &PathExtractor = PathExtractor::borrow_from(&state); @@ -194,11 +251,15 @@ fn update_handler(state : State) -> Pin(state : State) -> Pin> +where + dyn Future::Err>> : Send { to_handler_future(state, |state| Handler::delete_all(state)) } fn delete_handler(state : State) -> Pin> +where + dyn Future::Err>> : Send { let id = { let path : &PathExtractor = PathExtractor::borrow_from(&state); @@ -297,6 +358,8 @@ macro_rules! implDrawResourceRoutes { P : RefUnwindSafe + Send + Sync + 'static { fn read_all(&mut self) + where + dyn Future::Err>> : Send { let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); self.0.get(&self.1) @@ -305,6 +368,8 @@ macro_rules! implDrawResourceRoutes { } fn read(&mut self) + where + dyn Future::Err>> : Send { let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); self.0.get(&format!("{}/:id", self.1)) @@ -314,6 +379,8 @@ macro_rules! implDrawResourceRoutes { } fn search(&mut self) + where + dyn Future::Err>> : Send { let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); self.0.get(&format!("{}/search", self.1)) @@ -323,6 +390,9 @@ macro_rules! implDrawResourceRoutes { } fn create(&mut self) + where + Handler::Res : Send + 'static, + Handler::Body : 'static { let accept_matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); let content_matcher : MaybeMatchContentTypeHeader = Handler::Body::supported_types().into(); @@ -333,6 +403,9 @@ macro_rules! implDrawResourceRoutes { } fn update_all(&mut self) + where + Handler::Res : Send + 'static, + Handler::Body : 'static { let accept_matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); let content_matcher : MaybeMatchContentTypeHeader = Handler::Body::supported_types().into(); @@ -343,6 +416,9 @@ macro_rules! implDrawResourceRoutes { } fn update(&mut self) + where + Handler::Res : Send + 'static, + Handler::Body : 'static { let accept_matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); let content_matcher : MaybeMatchContentTypeHeader = Handler::Body::supported_types().into(); @@ -354,6 +430,8 @@ macro_rules! implDrawResourceRoutes { } fn delete_all(&mut self) + where + dyn Future::Err>> : Send { let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); self.0.delete(&self.1) @@ -362,6 +440,8 @@ macro_rules! implDrawResourceRoutes { } fn delete(&mut self) + where + dyn Future::Err>> : Send { let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); self.0.delete(&format!("{}/:id", self.1)) From a8ae9390190962fe20f8c7252a0ba8939aaf33aa Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 14 Apr 2020 22:40:27 +0200 Subject: [PATCH 003/170] clean up async shit --- gotham_restful/src/routing.rs | 69 ++++++++++------------------------- 1 file changed, 19 insertions(+), 50 deletions(-) diff --git a/gotham_restful/src/routing.rs b/gotham_restful/src/routing.rs index 8aac5ea..ee7cf31 100644 --- a/gotham_restful/src/routing.rs +++ b/gotham_restful/src/routing.rs @@ -7,7 +7,6 @@ use crate::{ #[cfg(feature = "openapi")] use crate::OpenapiRouter; -use futures_core::future::Future; use futures_util::{future, future::FutureExt}; use gotham::{ handler::{HandlerError, HandlerFuture, IntoHandlerError, IntoHandlerFuture}, @@ -66,17 +65,11 @@ pub trait DrawResources /// `Resource::setup` method. pub trait DrawResourceRoutes { - fn read_all(&mut self) - where - dyn Future::Err>> : Send; + fn read_all(&mut self); - fn read(&mut self) - where - dyn Future::Err>> : Send; + fn read(&mut self); - fn search(&mut self) - where - dyn Future::Err>> : Send; + fn search(&mut self); fn create(&mut self) where @@ -93,13 +86,9 @@ pub trait DrawResourceRoutes Handler::Res : Send + 'static, Handler::Body : 'static; - fn delete_all(&mut self) - where - dyn Future::Err>> : Send; + fn delete_all(&mut self); - fn delete(&mut self) - where - dyn Future::Err>> : Send; + fn delete(&mut self); } fn response_from(res : Response, state : &State) -> hyper::Response @@ -119,8 +108,7 @@ fn response_from(res : Response, state : &State) -> hyper::Response fn to_handler_future(mut state : State, get_result : F) -> Pin> where F : FnOnce(&mut State) -> R, - R : ResourceResult, - dyn Future> : Send + R : ResourceResult { get_result(&mut state).into_response() .then(|res| @@ -134,7 +122,7 @@ where ).boxed() } -async fn body_to_res(state : &mut State, get_result : F) -> Result, HandlerError> +async fn body_to_res(mut state : State, get_result : F) -> (State, Result, HandlerError>) where B : RequestBody, F : FnOnce(&mut State, B) -> R, @@ -144,52 +132,53 @@ where let body = match body { Ok(body) => body, - Err(e) => return Err(e.into_handler_error()) + Err(e) => return (state, Err(e.into_handler_error())) }; let content_type : Mime = match HeaderMap::borrow_from(&state).get(CONTENT_TYPE) { Some(content_type) => content_type.to_str().unwrap().parse().unwrap(), None => { let res = create_empty_response(&state, StatusCode::UNSUPPORTED_MEDIA_TYPE); - return Ok(res) + return (state, Ok(res)) } }; let res = { let body = match B::from_body(body, content_type) { Ok(body) => body, - Err(e) => return { + Err(e) => { let error : ResourceError = e.into(); - match serde_json::to_string(&error) { + let res = match serde_json::to_string(&error) { Ok(json) => { let res = create_response(&state, StatusCode::BAD_REQUEST, APPLICATION_JSON, json); Ok(res) }, Err(e) => Err(e.into_handler_error()) - } + }; + return (state, res) } }; get_result(&mut state, body) }; - let res = res.into_response().await; - match res { + let res = match res.into_response().await { Ok(res) => { let r = response_from(res, &state); Ok(r) }, Err(e) => Err(e.into_handler_error()) - } + }; + (state, res) } -fn handle_with_body(mut state : State, get_result : F) -> Pin> +fn handle_with_body(state : State, get_result : F) -> Pin> where B : RequestBody + 'static, F : FnOnce(&mut State, B) -> R + Send + 'static, R : ResourceResult + Send + 'static { - body_to_res(&mut state, get_result) - .then(|res| match res { + body_to_res(state, get_result) + .then(|(state, res)| match res { Ok(ok) => future::ok((state, ok)), Err(err) => future::err((state, err)) }) @@ -197,15 +186,11 @@ where } fn read_all_handler(state : State) -> Pin> -where - dyn Future::Err>> : Send { to_handler_future(state, |state| Handler::read_all(state)) } fn read_handler(state : State) -> Pin> -where - dyn Future::Err>> : Send { let id = { let path : &PathExtractor = PathExtractor::borrow_from(&state); @@ -215,8 +200,6 @@ where } fn search_handler(mut state : State) -> Pin> -where - dyn Future::Err>> : Send { let query = Handler::Query::take_from(&mut state); to_handler_future(state, |state| Handler::search(state, query)) @@ -251,15 +234,11 @@ where } fn delete_all_handler(state : State) -> Pin> -where - dyn Future::Err>> : Send { to_handler_future(state, |state| Handler::delete_all(state)) } fn delete_handler(state : State) -> Pin> -where - dyn Future::Err>> : Send { let id = { let path : &PathExtractor = PathExtractor::borrow_from(&state); @@ -358,8 +337,6 @@ macro_rules! implDrawResourceRoutes { P : RefUnwindSafe + Send + Sync + 'static { fn read_all(&mut self) - where - dyn Future::Err>> : Send { let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); self.0.get(&self.1) @@ -368,8 +345,6 @@ macro_rules! implDrawResourceRoutes { } fn read(&mut self) - where - dyn Future::Err>> : Send { let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); self.0.get(&format!("{}/:id", self.1)) @@ -379,8 +354,6 @@ macro_rules! implDrawResourceRoutes { } fn search(&mut self) - where - dyn Future::Err>> : Send { let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); self.0.get(&format!("{}/search", self.1)) @@ -430,8 +403,6 @@ macro_rules! implDrawResourceRoutes { } fn delete_all(&mut self) - where - dyn Future::Err>> : Send { let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); self.0.delete(&self.1) @@ -440,8 +411,6 @@ macro_rules! implDrawResourceRoutes { } fn delete(&mut self) - where - dyn Future::Err>> : Send { let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); self.0.delete(&format!("{}/:id", self.1)) From 06e6c93a46666a1674870a17498d5ced369ebc3f Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 14 Apr 2020 22:41:20 +0200 Subject: [PATCH 004/170] fix openapi routing errors --- gotham_restful/src/openapi/router.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/gotham_restful/src/openapi/router.rs b/gotham_restful/src/openapi/router.rs index 2bf93ef..e2113d5 100644 --- a/gotham_restful/src/openapi/router.rs +++ b/gotham_restful/src/openapi/router.rs @@ -418,6 +418,9 @@ macro_rules! implOpenapiRouter { } fn create(&mut self) + where + Handler::Res : Send + 'static, + Handler::Body : 'static { let schema = (self.0).1.add_schema::(); let body_schema = (self.0).1.add_schema::(); @@ -431,6 +434,9 @@ macro_rules! implOpenapiRouter { } fn update_all(&mut self) + where + Handler::Res : Send + 'static, + Handler::Body : 'static { let schema = (self.0).1.add_schema::(); let body_schema = (self.0).1.add_schema::(); @@ -444,6 +450,9 @@ macro_rules! implOpenapiRouter { } fn update(&mut self) + where + Handler::Res : Send + 'static, + Handler::Body : 'static { let schema = (self.0).1.add_schema::(); let body_schema = (self.0).1.add_schema::(); From d7282786b13421c65acd9e3bd8e391d8dab47ee6 Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 14 Apr 2020 22:44:43 +0200 Subject: [PATCH 005/170] require all resource results to be sendable --- gotham_restful/src/openapi/router.rs | 6 +++--- gotham_restful/src/result.rs | 11 ++++++++++- gotham_restful/src/routing.rs | 14 +++++++------- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/gotham_restful/src/openapi/router.rs b/gotham_restful/src/openapi/router.rs index e2113d5..ab4992b 100644 --- a/gotham_restful/src/openapi/router.rs +++ b/gotham_restful/src/openapi/router.rs @@ -419,7 +419,7 @@ macro_rules! implOpenapiRouter { fn create(&mut self) where - Handler::Res : Send + 'static, + Handler::Res : 'static, Handler::Body : 'static { let schema = (self.0).1.add_schema::(); @@ -435,7 +435,7 @@ macro_rules! implOpenapiRouter { fn update_all(&mut self) where - Handler::Res : Send + 'static, + Handler::Res : 'static, Handler::Body : 'static { let schema = (self.0).1.add_schema::(); @@ -451,7 +451,7 @@ macro_rules! implOpenapiRouter { fn update(&mut self) where - Handler::Res : Send + 'static, + Handler::Res : 'static, Handler::Body : 'static { let schema = (self.0).1.add_schema::(); diff --git a/gotham_restful/src/result.rs b/gotham_restful/src/result.rs index 3108749..cb96636 100644 --- a/gotham_restful/src/result.rs +++ b/gotham_restful/src/result.rs @@ -80,7 +80,7 @@ impl Response /// A trait provided to convert a resource's result to json. -pub trait ResourceResult +pub trait ResourceResult : Send { type Err : Error + Send + 'static; @@ -151,6 +151,8 @@ fn errorlog(e : E) fn errorlog(_e : E) {} impl ResourceResult for Result +where + Self : Send { type Err = SerdeJsonError; @@ -267,6 +269,8 @@ impl Debug for Success } impl ResourceResult for Success +where + Self : Send { type Err = SerdeJsonError; @@ -460,6 +464,8 @@ impl ResourceResult for NoContent } impl ResourceResult for Result +where + Self : Send { type Err = SerdeJsonError; @@ -520,6 +526,8 @@ impl Debug for Raw } impl> ResourceResult for Raw +where + Self : Send { type Err = SerdeJsonError; // just for easier handling of `Result, E>` @@ -545,6 +553,7 @@ impl> ResourceResult for Raw impl ResourceResult for Result, E> where + Self : Send, Raw : ResourceResult { type Err = SerdeJsonError; diff --git a/gotham_restful/src/routing.rs b/gotham_restful/src/routing.rs index ee7cf31..22af7fb 100644 --- a/gotham_restful/src/routing.rs +++ b/gotham_restful/src/routing.rs @@ -73,17 +73,17 @@ pub trait DrawResourceRoutes fn create(&mut self) where - Handler::Res : Send + 'static, + Handler::Res : 'static, Handler::Body : 'static; fn update_all(&mut self) where - Handler::Res : Send + 'static, + Handler::Res : 'static, Handler::Body : 'static; fn update(&mut self) where - Handler::Res : Send + 'static, + Handler::Res : 'static, Handler::Body : 'static; fn delete_all(&mut self); @@ -175,7 +175,7 @@ fn handle_with_body(state : State, get_result : F) -> Pin R + Send + 'static, - R : ResourceResult + Send + 'static + R : ResourceResult + 'static { body_to_res(state, get_result) .then(|(state, res)| match res { @@ -207,7 +207,7 @@ fn search_handler(mut state : State) -> Pin(state : State) -> Pin> where - Handler::Res : Send + 'static, + Handler::Res : 'static, Handler::Body : 'static { handle_with_body::(state, |state, body| Handler::create(state, body)) @@ -215,7 +215,7 @@ where fn update_all_handler(state : State) -> Pin> where - Handler::Res : Send + 'static, + Handler::Res : 'static, Handler::Body : 'static { handle_with_body::(state, |state, body| Handler::update_all(state, body)) @@ -223,7 +223,7 @@ where fn update_handler(state : State) -> Pin> where - Handler::Res : Send + 'static, + Handler::Res : 'static, Handler::Body : 'static { let id = { From 427c836f52810021879967fe82a9f1bd1896f034 Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 15 Apr 2020 20:55:25 +0200 Subject: [PATCH 006/170] expose async to handlers --- gotham_restful/src/lib.rs | 1 + gotham_restful/src/resource.rs | 24 ++++++++------ gotham_restful/src/result.rs | 2 +- gotham_restful/src/routing.rs | 49 +++++++++++++++-------------- gotham_restful_derive/src/method.rs | 36 ++++++++++++++++----- 5 files changed, 71 insertions(+), 41 deletions(-) diff --git a/gotham_restful/src/lib.rs b/gotham_restful/src/lib.rs index 7b4006a..9253f3e 100644 --- a/gotham_restful/src/lib.rs +++ b/gotham_restful/src/lib.rs @@ -115,6 +115,7 @@ pub use gotham_restful_derive::*; #[doc(hidden)] pub mod export { + pub use futures_util::future::FutureExt; pub use gotham::{ hyper::body::Bytes, state::{FromState, State} diff --git a/gotham_restful/src/resource.rs b/gotham_restful/src/resource.rs index fd1c7e6..79b0de9 100644 --- a/gotham_restful/src/resource.rs +++ b/gotham_restful/src/resource.rs @@ -5,7 +5,11 @@ use gotham::{ }; use hyper::Body; use serde::de::DeserializeOwned; -use std::panic::RefUnwindSafe; +use std::{ + future::Future, + panic::RefUnwindSafe, + pin::Pin +}; /// This trait must be implemented by every RESTful Resource. It will /// allow you to register the different methods for this Resource. @@ -21,7 +25,7 @@ pub trait Resource pub trait ResourceMethod { - type Res : ResourceResult; + type Res : ResourceResult + Send + 'static; #[cfg(feature = "openapi")] fn operation_id() -> Option @@ -38,7 +42,7 @@ pub trait ResourceMethod /// Handle a GET request on the Resource root. pub trait ResourceReadAll : ResourceMethod { - fn read_all(state : &mut State) -> Self::Res; + fn read_all(state : &mut State) -> Pin + Send>>; } /// Handle a GET request on the Resource with an id. @@ -46,7 +50,7 @@ pub trait ResourceRead : ResourceMethod { type ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static; - fn read(state : &mut State, id : Self::ID) -> Self::Res; + fn read(state : &mut State, id : Self::ID) -> Pin + Send>>; } /// Handle a GET request on the Resource with additional search parameters. @@ -54,7 +58,7 @@ pub trait ResourceSearch : ResourceMethod { type Query : ResourceType + QueryStringExtractor + Sync; - fn search(state : &mut State, query : Self::Query) -> Self::Res; + fn search(state : &mut State, query : Self::Query) -> Pin + Send>>; } /// Handle a POST request on the Resource root. @@ -62,7 +66,7 @@ pub trait ResourceCreate : ResourceMethod { type Body : RequestBody; - fn create(state : &mut State, body : Self::Body) -> Self::Res; + fn create(state : &mut State, body : Self::Body) -> Pin + Send>>; } /// Handle a PUT request on the Resource root. @@ -70,7 +74,7 @@ pub trait ResourceUpdateAll : ResourceMethod { type Body : RequestBody; - fn update_all(state : &mut State, body : Self::Body) -> Self::Res; + fn update_all(state : &mut State, body : Self::Body) -> Pin + Send>>; } /// Handle a PUT request on the Resource with an id. @@ -79,13 +83,13 @@ pub trait ResourceUpdate : ResourceMethod type Body : RequestBody; type ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static; - fn update(state : &mut State, id : Self::ID, body : Self::Body) -> Self::Res; + fn update(state : &mut State, id : Self::ID, body : Self::Body) -> Pin + Send>>; } /// Handle a DELETE request on the Resource root. pub trait ResourceDeleteAll : ResourceMethod { - fn delete_all(state : &mut State) -> Self::Res; + fn delete_all(state : &mut State) -> Pin + Send>>; } /// Handle a DELETE request on the Resource with an id. @@ -93,5 +97,5 @@ pub trait ResourceDelete : ResourceMethod { type ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static; - fn delete(state : &mut State, id : Self::ID) -> Self::Res; + fn delete(state : &mut State, id : Self::ID) -> Pin + Send>>; } diff --git a/gotham_restful/src/result.rs b/gotham_restful/src/result.rs index cb96636..a41b4c2 100644 --- a/gotham_restful/src/result.rs +++ b/gotham_restful/src/result.rs @@ -80,7 +80,7 @@ impl Response /// A trait provided to convert a resource's result to json. -pub trait ResourceResult : Send +pub trait ResourceResult { type Err : Error + Send + 'static; diff --git a/gotham_restful/src/routing.rs b/gotham_restful/src/routing.rs index 22af7fb..7fd66f0 100644 --- a/gotham_restful/src/routing.rs +++ b/gotham_restful/src/routing.rs @@ -9,7 +9,7 @@ use crate::OpenapiRouter; use futures_util::{future, future::FutureExt}; use gotham::{ - handler::{HandlerError, HandlerFuture, IntoHandlerError, IntoHandlerFuture}, + handler::{HandlerError, HandlerFuture, IntoHandlerError}, helpers::http::response::{create_empty_response, create_response}, pipeline::chain::PipelineHandleChain, router::{ @@ -32,6 +32,7 @@ use gotham::hyper::{ }; use mime::{Mime, APPLICATION_JSON}; use std::{ + future::Future, panic::RefUnwindSafe, pin::Pin }; @@ -105,27 +106,26 @@ fn response_from(res : Response, state : &State) -> hyper::Response r } -fn to_handler_future(mut state : State, get_result : F) -> Pin> +async fn to_handler_future(mut state : State, get_result : F) -> Result<(State, gotham::hyper::Response), (State, HandlerError)> where - F : FnOnce(&mut State) -> R, + F : FnOnce(&mut State) -> Pin + Send>>, R : ResourceResult { - get_result(&mut state).into_response() - .then(|res| - match res { - Ok(res) => { - let r = response_from(res, &state); - (state, r).into_handler_future() - }, - Err(e) => future::err((state, e.into_handler_error())).boxed() - } - ).boxed() + let res = get_result(&mut state).await; + let res = res.into_response().await; + match res { + Ok(res) => { + let r = response_from(res, &state); + Ok((state, r)) + }, + Err(e) => Err((state, e.into_handler_error())) + } } async fn body_to_res(mut state : State, get_result : F) -> (State, Result, HandlerError>) where B : RequestBody, - F : FnOnce(&mut State, B) -> R, + F : FnOnce(&mut State, B) -> Pin + Send>>, R : ResourceResult { let body = to_bytes(Body::take_from(&mut state)).await; @@ -160,8 +160,11 @@ where }; get_result(&mut state, body) }; - - let res = match res.into_response().await { + + let res = res.await; + let res = res.into_response().await; + + let res = match res { Ok(res) => { let r = response_from(res, &state); Ok(r) @@ -174,8 +177,8 @@ where fn handle_with_body(state : State, get_result : F) -> Pin> where B : RequestBody + 'static, - F : FnOnce(&mut State, B) -> R + Send + 'static, - R : ResourceResult + 'static + F : FnOnce(&mut State, B) -> Pin + Send>> + Send + 'static, + R : ResourceResult + Send + 'static { body_to_res(state, get_result) .then(|(state, res)| match res { @@ -187,7 +190,7 @@ where fn read_all_handler(state : State) -> Pin> { - to_handler_future(state, |state| Handler::read_all(state)) + to_handler_future(state, |state| Handler::read_all(state)).boxed() } fn read_handler(state : State) -> Pin> @@ -196,13 +199,13 @@ fn read_handler(state : State) -> Pin let path : &PathExtractor = PathExtractor::borrow_from(&state); path.id.clone() }; - to_handler_future(state, |state| Handler::read(state, id)) + to_handler_future(state, |state| Handler::read(state, id)).boxed() } fn search_handler(mut state : State) -> Pin> { let query = Handler::Query::take_from(&mut state); - to_handler_future(state, |state| Handler::search(state, query)) + to_handler_future(state, |state| Handler::search(state, query)).boxed() } fn create_handler(state : State) -> Pin> @@ -235,7 +238,7 @@ where fn delete_all_handler(state : State) -> Pin> { - to_handler_future(state, |state| Handler::delete_all(state)) + to_handler_future(state, |state| Handler::delete_all(state)).boxed() } fn delete_handler(state : State) -> Pin> @@ -244,7 +247,7 @@ fn delete_handler(state : State) -> Pin = PathExtractor::borrow_from(&state); path.id.clone() }; - to_handler_future(state, |state| Handler::delete(state, id)) + to_handler_future(state, |state| Handler::delete(state, id)).boxed() } #[derive(Clone)] diff --git a/gotham_restful_derive/src/method.rs b/gotham_restful_derive/src/method.rs index a4c3fb8..9ca87c6 100644 --- a/gotham_restful_derive/src/method.rs +++ b/gotham_restful_derive/src/method.rs @@ -302,6 +302,12 @@ fn expand(method : Method, attrs : TokenStream, item : TokenStream) -> Result(item)?; let fun_ident = &fun.sig.ident; let fun_vis = &fun.vis; + let fun_is_async = fun.sig.asyncness.is_some(); + + if let Some(unsafety) = fun.sig.unsafety + { + return Err(Error::new(unsafety.span(), "Resource methods must not be unsafe")); + } let trait_ident = method.trait_ident(); let method_ident = method.fn_ident(); @@ -374,6 +380,11 @@ fn expand(method : Method, attrs : TokenStream, item : TokenStream) -> Result Result>::borrow_from(&#state_ident).clone(); + }; + block = quote! { #repo_ident.run::<_, #ret, ()>(move |#conn_ident| { Ok({#block}) - }).wait().unwrap() + }).await.unwrap() }; } if let Some(arg) = args.iter().filter(|arg| (*arg).ty.is_auth_status()).nth(0) { let auth_ty = arg.ty.quote_ty(); - block = quote! { + state_block = quote! { + #state_block let #auth_ident : #auth_ty = <#auth_ty>::borrow_from(#state_ident).clone(); - #block }; } @@ -416,6 +430,10 @@ fn expand(method : Method, attrs : TokenStream, item : TokenStream) -> Result Result #ret + fn #method_ident(#(#args_def),*) -> Pin + Send>> { #[allow(unused_imports)] - use #krate::export::FromState; + use #krate::export::{FromState, FutureExt}; - #block + #state_block + + async move { + #block + }.boxed() } } From 89f6494b51fb1ed5831769a261f214e841ed4969 Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 15 Apr 2020 21:07:33 +0200 Subject: [PATCH 007/170] asyncify method proc macro --- gotham_restful/src/resource.rs | 16 ++++++++-------- gotham_restful/src/routing.rs | 14 +++++++------- gotham_restful_derive/src/method.rs | 18 ++++++++---------- 3 files changed, 23 insertions(+), 25 deletions(-) diff --git a/gotham_restful/src/resource.rs b/gotham_restful/src/resource.rs index 79b0de9..39bd964 100644 --- a/gotham_restful/src/resource.rs +++ b/gotham_restful/src/resource.rs @@ -42,7 +42,7 @@ pub trait ResourceMethod /// Handle a GET request on the Resource root. pub trait ResourceReadAll : ResourceMethod { - fn read_all(state : &mut State) -> Pin + Send>>; + fn read_all(state : State) -> Pin + Send>>; } /// Handle a GET request on the Resource with an id. @@ -50,7 +50,7 @@ pub trait ResourceRead : ResourceMethod { type ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static; - fn read(state : &mut State, id : Self::ID) -> Pin + Send>>; + fn read(state : State, id : Self::ID) -> Pin + Send>>; } /// Handle a GET request on the Resource with additional search parameters. @@ -58,7 +58,7 @@ pub trait ResourceSearch : ResourceMethod { type Query : ResourceType + QueryStringExtractor + Sync; - fn search(state : &mut State, query : Self::Query) -> Pin + Send>>; + fn search(state : State, query : Self::Query) -> Pin + Send>>; } /// Handle a POST request on the Resource root. @@ -66,7 +66,7 @@ pub trait ResourceCreate : ResourceMethod { type Body : RequestBody; - fn create(state : &mut State, body : Self::Body) -> Pin + Send>>; + fn create(state : State, body : Self::Body) -> Pin + Send>>; } /// Handle a PUT request on the Resource root. @@ -74,7 +74,7 @@ pub trait ResourceUpdateAll : ResourceMethod { type Body : RequestBody; - fn update_all(state : &mut State, body : Self::Body) -> Pin + Send>>; + fn update_all(state : State, body : Self::Body) -> Pin + Send>>; } /// Handle a PUT request on the Resource with an id. @@ -83,13 +83,13 @@ pub trait ResourceUpdate : ResourceMethod type Body : RequestBody; type ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static; - fn update(state : &mut State, id : Self::ID, body : Self::Body) -> Pin + Send>>; + fn update(state : State, id : Self::ID, body : Self::Body) -> Pin + Send>>; } /// Handle a DELETE request on the Resource root. pub trait ResourceDeleteAll : ResourceMethod { - fn delete_all(state : &mut State) -> Pin + Send>>; + fn delete_all(state : State) -> Pin + Send>>; } /// Handle a DELETE request on the Resource with an id. @@ -97,5 +97,5 @@ pub trait ResourceDelete : ResourceMethod { type ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static; - fn delete(state : &mut State, id : Self::ID) -> Pin + Send>>; + fn delete(state : State, id : Self::ID) -> Pin + Send>>; } diff --git a/gotham_restful/src/routing.rs b/gotham_restful/src/routing.rs index 7fd66f0..13672c2 100644 --- a/gotham_restful/src/routing.rs +++ b/gotham_restful/src/routing.rs @@ -106,12 +106,12 @@ fn response_from(res : Response, state : &State) -> hyper::Response r } -async fn to_handler_future(mut state : State, get_result : F) -> Result<(State, gotham::hyper::Response), (State, HandlerError)> +async fn to_handler_future(state : State, get_result : F) -> Result<(State, gotham::hyper::Response), (State, HandlerError)> where - F : FnOnce(&mut State) -> Pin + Send>>, + F : FnOnce(State) -> Pin + Send>>, R : ResourceResult { - let res = get_result(&mut state).await; + let (state, res) = get_result(state).await; let res = res.into_response().await; match res { Ok(res) => { @@ -125,7 +125,7 @@ where async fn body_to_res(mut state : State, get_result : F) -> (State, Result, HandlerError>) where B : RequestBody, - F : FnOnce(&mut State, B) -> Pin + Send>>, + F : FnOnce(State, B) -> Pin + Send>>, R : ResourceResult { let body = to_bytes(Body::take_from(&mut state)).await; @@ -158,10 +158,10 @@ where return (state, res) } }; - get_result(&mut state, body) + get_result(state, body) }; - let res = res.await; + let (state, res) = res.await; let res = res.into_response().await; let res = match res { @@ -177,7 +177,7 @@ where fn handle_with_body(state : State, get_result : F) -> Pin> where B : RequestBody + 'static, - F : FnOnce(&mut State, B) -> Pin + Send>> + Send + 'static, + F : FnOnce(State, B) -> Pin + Send>> + Send + 'static, R : ResourceResult + Send + 'static { body_to_res(state, get_result) diff --git a/gotham_restful_derive/src/method.rs b/gotham_restful_derive/src/method.rs index 9ca87c6..fb89b45 100644 --- a/gotham_restful_derive/src/method.rs +++ b/gotham_restful_derive/src/method.rs @@ -325,6 +325,7 @@ fn expand(method : Method, attrs : TokenStream, item : TokenStream) -> Result Result = args.iter().map(|arg| match (&arg.ty, &arg.ident) { - (MethodArgumentType::StateRef, _) => quote!(#state_ident), - (MethodArgumentType::StateMutRef, _) => quote!(#state_ident), + (MethodArgumentType::StateRef, _) => quote!(&#state_ident), + (MethodArgumentType::StateMutRef, _) => quote!(&mut #state_ident), (MethodArgumentType::MethodArg(_), ident) => quote!(#ident), (MethodArgumentType::DatabaseConnection(_), _) => quote!(&#conn_ident), (MethodArgumentType::AuthStatus(_), _) => quote!(#auth_ident), @@ -407,7 +408,7 @@ fn expand(method : Method, attrs : TokenStream, item : TokenStream) -> Result::borrow_from(#state_ident).clone(); + let #auth_ident : #auth_ty = <#auth_ty>::borrow_from(&#state_ident).clone(); }; } @@ -430,10 +431,6 @@ fn expand(method : Method, attrs : TokenStream, item : TokenStream) -> Result Result Pin + Send>> + fn #method_ident(#(#args_def),*) -> std::pin::Pin + Send>> { #[allow(unused_imports)] use #krate::export::{FromState, FutureExt}; @@ -458,7 +455,8 @@ fn expand(method : Method, attrs : TokenStream, item : TokenStream) -> Result Date: Wed, 15 Apr 2020 23:01:21 +0200 Subject: [PATCH 008/170] make clear that this tracks gotham master --- README.md | 3 +++ example/Cargo.toml | 2 +- gotham_restful/Cargo.toml | 4 ++-- gotham_restful/src/lib.rs | 3 +++ gotham_restful_derive/Cargo.toml | 2 +- 5 files changed, 10 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7af32f1..8ebf802 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,9 @@ bodies, relying on [`serde`][serde] and [`serde_json`][serde_json] for (de)seria enable the `openapi` feature, you can also generate an OpenAPI Specification from your RESTful resources. +**Note:** The master branch currently tracks gotham's master branch and the next release will use +gotham 0.5.0 and be compatible with the new future / async stuff. + ## Usage A basic server with only one resource, handling a simple `GET` request, could look like this: diff --git a/example/Cargo.toml b/example/Cargo.toml index b3e08cd..022a647 100644 --- a/example/Cargo.toml +++ b/example/Cargo.toml @@ -17,7 +17,7 @@ gitlab = { repository = "msrd0/gotham-restful", branch = "master" } fake = "2.2" gotham = { git = "https://github.com/gotham-rs/gotham", version = "0.5.0-dev", default-features = false } gotham_derive = { git = "https://github.com/gotham-rs/gotham", version = "0.5.0-dev", default-features = false } -gotham_restful = { version = "0.0.4", features = ["auth", "openapi"] } +gotham_restful = { version = "0.0.5-dev", features = ["auth", "openapi"] } log = "0.4" log4rs = { version = "0.8", features = ["console_appender"], default-features = false } serde = "1" diff --git a/gotham_restful/Cargo.toml b/gotham_restful/Cargo.toml index dcea947..6b41c39 100644 --- a/gotham_restful/Cargo.toml +++ b/gotham_restful/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "gotham_restful" -version = "0.0.4" +version = "0.0.5-dev" authors = ["Dominic Meiser "] edition = "2018" description = "RESTful additions for Gotham" @@ -24,7 +24,7 @@ futures-util = "0.3.4" gotham = { git = "https://github.com/gotham-rs/gotham", version = "0.5.0-dev", default-features = false } gotham_derive = { git = "https://github.com/gotham-rs/gotham", version = "0.5.0-dev" } gotham_middleware_diesel = { git = "https://github.com/gotham-rs/gotham", version = "0.1.0", optional = true } -gotham_restful_derive = { version = "0.0.3" } +gotham_restful_derive = { version = "0.0.4-dev" } hyper = "0.13.4" indexmap = { version = "1.3.0", optional = true } jsonwebtoken = { version = "7.1.0", optional = true } diff --git a/gotham_restful/src/lib.rs b/gotham_restful/src/lib.rs index 9253f3e..cf1ef9c 100644 --- a/gotham_restful/src/lib.rs +++ b/gotham_restful/src/lib.rs @@ -6,6 +6,9 @@ bodies, relying on [`serde`][serde] and [`serde_json`][serde_json] for (de)seria enable the `openapi` feature, you can also generate an OpenAPI Specification from your RESTful resources. +**Note:** The master branch currently tracks gotham's master branch and the next release will use +gotham 0.5.0 and be compatible with the new future / async stuff. + # Usage A basic server with only one resource, handling a simple `GET` request, could look like this: diff --git a/gotham_restful_derive/Cargo.toml b/gotham_restful_derive/Cargo.toml index c1f460e..0b65218 100644 --- a/gotham_restful_derive/Cargo.toml +++ b/gotham_restful_derive/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "gotham_restful_derive" -version = "0.0.3" +version = "0.0.4-dev" authors = ["Dominic Meiser "] edition = "2018" description = "RESTful additions for Gotham - Derive" From a493071ff8a18095b7e27c9536933aa361f233aa Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 15 Apr 2020 23:15:13 +0200 Subject: [PATCH 009/170] dependency management --- example/Cargo.toml | 11 +++-------- gotham_restful/Cargo.toml | 10 ++++------ gotham_restful/src/auth.rs | 2 +- gotham_restful/src/openapi/router.rs | 6 +++--- gotham_restful/src/resource.rs | 2 +- gotham_restful/src/routing.rs | 2 +- gotham_restful_derive/Cargo.toml | 6 +++--- 7 files changed, 16 insertions(+), 23 deletions(-) diff --git a/example/Cargo.toml b/example/Cargo.toml index 022a647..af55bfa 100644 --- a/example/Cargo.toml +++ b/example/Cargo.toml @@ -18,11 +18,6 @@ fake = "2.2" gotham = { git = "https://github.com/gotham-rs/gotham", version = "0.5.0-dev", default-features = false } gotham_derive = { git = "https://github.com/gotham-rs/gotham", version = "0.5.0-dev", default-features = false } gotham_restful = { version = "0.0.5-dev", features = ["auth", "openapi"] } -log = "0.4" -log4rs = { version = "0.8", features = ["console_appender"], default-features = false } -serde = "1" - -[dev-dependencies] -fake = "2.2" -log = "0.4" -log4rs = { version = "0.8", features = ["console_appender"], default-features = false } +log = "0.4.8" +log4rs = { version = "0.11", features = ["console_appender"], default-features = false } +serde = "1.0.106" diff --git a/gotham_restful/Cargo.toml b/gotham_restful/Cargo.toml index 6b41c39..bb9bd45 100644 --- a/gotham_restful/Cargo.toml +++ b/gotham_restful/Cargo.toml @@ -16,23 +16,21 @@ gitlab = { repository = "msrd0/gotham-restful", branch = "master" } [dependencies] base64 = { version = "0.12.0", optional = true } -chrono = { version = "0.4.10", optional = true } +chrono = { version = "0.4.11", optional = true } cookie = { version = "0.13.3", optional = true } -futures = "0.3.4" futures-core = "0.3.4" futures-util = "0.3.4" gotham = { git = "https://github.com/gotham-rs/gotham", version = "0.5.0-dev", default-features = false } gotham_derive = { git = "https://github.com/gotham-rs/gotham", version = "0.5.0-dev" } gotham_middleware_diesel = { git = "https://github.com/gotham-rs/gotham", version = "0.1.0", optional = true } gotham_restful_derive = { version = "0.0.4-dev" } -hyper = "0.13.4" -indexmap = { version = "1.3.0", optional = true } +indexmap = { version = "1.3.2", optional = true } jsonwebtoken = { version = "7.1.0", optional = true } log = { version = "0.4.8", optional = true } mime = "0.3.16" openapiv3 = { version = "0.3", optional = true } -serde = { version = "1.0.104", features = ["derive"] } -serde_json = "1.0.45" +serde = { version = "1.0.106", features = ["derive"] } +serde_json = "1.0.51" uuid = { version = ">= 0.1, < 0.9", optional = true } [dev-dependencies] diff --git a/gotham_restful/src/auth.rs b/gotham_restful/src/auth.rs index 7ee6880..137e9b4 100644 --- a/gotham_restful/src/auth.rs +++ b/gotham_restful/src/auth.rs @@ -3,10 +3,10 @@ use cookie::CookieJar; use futures_util::{future, future::{FutureExt, TryFutureExt}}; use gotham::{ handler::HandlerFuture, + hyper::header::{AUTHORIZATION, HeaderMap}, middleware::{Middleware, NewMiddleware}, state::{FromState, State} }; -use hyper::header::{AUTHORIZATION, HeaderMap}; use jsonwebtoken::{ errors::ErrorKind, DecodingKey diff --git a/gotham_restful/src/openapi/router.rs b/gotham_restful/src/openapi/router.rs index ab4992b..a379585 100644 --- a/gotham_restful/src/openapi/router.rs +++ b/gotham_restful/src/openapi/router.rs @@ -188,12 +188,12 @@ impl Handler for OpenapiHandler match serde_json::to_string(&openapi) { Ok(body) => { - let res = create_response(&state, hyper::StatusCode::OK, APPLICATION_JSON, body); + let res = create_response(&state, crate::StatusCode::OK, APPLICATION_JSON, body); future::ok((state, res)).boxed() }, Err(e) => { error!("Unable to handle OpenAPI request due to error: {}", e); - let res = create_response(&state, hyper::StatusCode::INTERNAL_SERVER_ERROR, TEXT_PLAIN, ""); + let res = create_response(&state, crate::StatusCode::INTERNAL_SERVER_ERROR, TEXT_PLAIN, ""); future::ok((state, res)).boxed() } } @@ -303,7 +303,7 @@ impl<'a> OperationParams<'a> fn new_operation( operation_id : Option, - default_status : hyper::StatusCode, + default_status : crate::StatusCode, accepted_types : Option>, schema : ReferenceOr, params : OperationParams, diff --git a/gotham_restful/src/resource.rs b/gotham_restful/src/resource.rs index 39bd964..cd903b7 100644 --- a/gotham_restful/src/resource.rs +++ b/gotham_restful/src/resource.rs @@ -1,9 +1,9 @@ use crate::{DrawResourceRoutes, RequestBody, ResourceResult, ResourceType}; use gotham::{ extractor::QueryStringExtractor, + hyper::Body, state::State }; -use hyper::Body; use serde::de::DeserializeOwned; use std::{ future::Future, diff --git a/gotham_restful/src/routing.rs b/gotham_restful/src/routing.rs index 13672c2..b8d8c56 100644 --- a/gotham_restful/src/routing.rs +++ b/gotham_restful/src/routing.rs @@ -92,7 +92,7 @@ pub trait DrawResourceRoutes fn delete(&mut self); } -fn response_from(res : Response, state : &State) -> hyper::Response +fn response_from(res : Response, state : &State) -> gotham::hyper::Response { let mut r = create_empty_response(state, res.status); if let Some(mime) = res.mime diff --git a/gotham_restful_derive/Cargo.toml b/gotham_restful_derive/Cargo.toml index 0b65218..4166067 100644 --- a/gotham_restful_derive/Cargo.toml +++ b/gotham_restful_derive/Cargo.toml @@ -18,9 +18,9 @@ gitlab = { repository = "msrd0/gotham-restful", branch = "master" } [dependencies] heck = "0.3.1" -proc-macro2 = "1.0.8" -quote = "1.0.2" -syn = { version = "1.0.14", features = ["extra-traits", "full"] } +proc-macro2 = "1.0.10" +quote = "1.0.3" +syn = { version = "1.0.17", features = ["extra-traits", "full"] } [features] default = [] From 63e6eb9b32f9308e87073e4a21bad009ba49b4bf Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 15 Apr 2020 23:20:41 +0200 Subject: [PATCH 010/170] re-export gotham --- gotham_restful/src/lib.rs | 11 ++++++----- gotham_restful_derive/src/from_body.rs | 2 +- gotham_restful_derive/src/method.rs | 6 +++--- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/gotham_restful/src/lib.rs b/gotham_restful/src/lib.rs index cf1ef9c..e8ee7bc 100644 --- a/gotham_restful/src/lib.rs +++ b/gotham_restful/src/lib.rs @@ -108,7 +108,12 @@ extern crate self as gotham_restful; #[macro_use] extern crate serde; #[doc(no_inline)] -pub use gotham::hyper::{header::HeaderName, StatusCode}; +pub use gotham; +#[doc(no_inline)] +pub use gotham::{ + hyper::{header::HeaderName, StatusCode}, + state::{FromState, State} +}; #[doc(no_inline)] pub use mime::Mime; @@ -119,10 +124,6 @@ pub use gotham_restful_derive::*; pub mod export { pub use futures_util::future::FutureExt; - pub use gotham::{ - hyper::body::Bytes, - state::{FromState, State} - }; #[cfg(feature = "database")] pub use gotham_middleware_diesel::Repo; diff --git a/gotham_restful_derive/src/from_body.rs b/gotham_restful_derive/src/from_body.rs index 4884659..4119845 100644 --- a/gotham_restful_derive/src/from_body.rs +++ b/gotham_restful_derive/src/from_body.rs @@ -58,7 +58,7 @@ fn expand(tokens : TokenStream) -> Result { type Err = String; - fn from_body(body : #krate::export::Bytes, _content_type : #krate::Mime) -> Result + fn from_body(body : #krate::gotham::hyper::body::Bytes, _content_type : #krate::Mime) -> Result { let body : &[u8] = &body; Ok(#body) diff --git a/gotham_restful_derive/src/method.rs b/gotham_restful_derive/src/method.rs index a8603b2..2d44ed4 100644 --- a/gotham_restful_derive/src/method.rs +++ b/gotham_restful_derive/src/method.rs @@ -367,7 +367,7 @@ fn expand(method : Method, attrs : TokenStream, item : TokenStream) -> Result = args.iter().map(|arg| match (&arg.ty, &arg.ident) { @@ -447,10 +447,10 @@ fn expand(method : Method, attrs : TokenStream, item : TokenStream) -> Result std::pin::Pin + Send>> + fn #method_ident(#(#args_def),*) -> std::pin::Pin + Send>> { #[allow(unused_imports)] - use #krate::export::{FromState, FutureExt}; + use #krate::{export::FutureExt, FromState}; #state_block From 310d7f79d521b10a9d8e92f14bb6bfd3af584657 Mon Sep 17 00:00:00 2001 From: Dominic Date: Thu, 16 Apr 2020 23:48:54 +0200 Subject: [PATCH 011/170] fix state ownership issue when using the database feature --- gotham_restful_derive/src/method.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/gotham_restful_derive/src/method.rs b/gotham_restful_derive/src/method.rs index 2d44ed4..39c31a5 100644 --- a/gotham_restful_derive/src/method.rs +++ b/gotham_restful_derive/src/method.rs @@ -398,9 +398,14 @@ fn expand(method : Method, attrs : TokenStream, item : TokenStream) -> Result>::borrow_from(&#state_ident).clone(); }; block = quote! { - #repo_ident.run::<_, #ret, ()>(move |#conn_ident| { - Ok({#block}) - }).await.unwrap() + { + let #res_ident = #repo_ident.run::<_, (#krate::State, #ret), ()>(move |#conn_ident| { + let #res_ident = { #block }; + Ok((#state_ident, #res_ident)) + }).await.unwrap(); + #state_ident = #res_ident.0; + #res_ident.1 + } }; } if let Some(arg) = args.iter().filter(|arg| (*arg).ty.is_auth_status()).nth(0) From 523d01d4437de5c81cc45948506c8589be77b6a9 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sat, 18 Apr 2020 15:48:00 +0200 Subject: [PATCH 012/170] async fn and conn are not compatible atm since diesel is completely sync --- gotham_restful_derive/src/method.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gotham_restful_derive/src/method.rs b/gotham_restful_derive/src/method.rs index 39c31a5..098c4c7 100644 --- a/gotham_restful_derive/src/method.rs +++ b/gotham_restful_derive/src/method.rs @@ -392,6 +392,10 @@ fn expand(method : Method, attrs : TokenStream, item : TokenStream) -> Result Date: Sat, 18 Apr 2020 16:18:02 +0200 Subject: [PATCH 013/170] add ResourceResult impl for Result, E> --- gotham_restful/src/result.rs | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/gotham_restful/src/result.rs b/gotham_restful/src/result.rs index a41b4c2..06d5fa2 100644 --- a/gotham_restful/src/result.rs +++ b/gotham_restful/src/result.rs @@ -406,6 +406,42 @@ impl ResourceResult for AuthResult } } +impl, E : Error> ResourceResult for Result, E> +{ + type Err = T::Err; + + fn into_response(self) -> Pin> + Send>> + { + match self { + Ok(r) => r.into_response(), + Err(e) => { + into_response_helper(|| { + errorlog(&e); + let err : ResourceError = e.into(); + Ok(Response::json(StatusCode::INTERNAL_SERVER_ERROR, serde_json::to_string(&err)?)) + }) + } + } + } + + fn accepted_types() -> Option> + { + T::accepted_types() + } + + #[cfg(feature = "openapi")] + fn schema() -> OpenapiSchema + { + T::schema() + } + + #[cfg(feature = "openapi")] + fn default_status() -> StatusCode + { + T::default_status() + } +} + /** This is the return type of a resource that doesn't actually return something. It will result in a _204 No Content_ answer by default. You don't need to use this type directly if using From fdc34fc296ec468442ae14ad17e7d43800d05c23 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 19 Apr 2020 20:49:47 +0200 Subject: [PATCH 014/170] remove weird useless constraint --- gotham_restful/src/result.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gotham_restful/src/result.rs b/gotham_restful/src/result.rs index 06d5fa2..7e5bdd9 100644 --- a/gotham_restful/src/result.rs +++ b/gotham_restful/src/result.rs @@ -185,8 +185,7 @@ where impl ResourceResult for Pin + Send>> where - Res : ResourceResult + 'static, - dyn Future> : Send + Res : ResourceResult + 'static { type Err = Res::Err; From 1e607bbcc9c87ce815de2e3b77df91af34320a02 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 19 Apr 2020 22:26:29 +0200 Subject: [PATCH 015/170] more generous FromBody implementation --- gotham_restful/src/lib.rs | 9 +- gotham_restful_derive/src/from_body.rs | 129 ++++++++++++++++++------- 2 files changed, 102 insertions(+), 36 deletions(-) diff --git a/gotham_restful/src/lib.rs b/gotham_restful/src/lib.rs index e8ee7bc..0295a4b 100644 --- a/gotham_restful/src/lib.rs +++ b/gotham_restful/src/lib.rs @@ -55,7 +55,7 @@ Uploads and Downloads can also be handled, but you need to specify the mime type ```rust,no_run # #[macro_use] extern crate gotham_restful_derive; # use gotham::{router::builder::*, state::State}; -# use gotham_restful::{DrawResources, Raw, Resource, Success}; +# use gotham_restful::{DrawResources, Mime, Raw, Resource, Success}; # use serde::{Deserialize, Serialize}; #[derive(Resource)] #[rest_resource(create)] @@ -63,11 +63,14 @@ struct ImageResource; #[derive(FromBody, RequestBody)] #[supported_types(mime::IMAGE_GIF, mime::IMAGE_JPEG, mime::IMAGE_PNG)] -struct RawImage(Vec); +struct RawImage { + content: Vec, + content_type: Mime +} #[rest_create(ImageResource)] fn create(_state : &mut State, body : RawImage) -> Raw> { - Raw::new(body.0, mime::APPLICATION_OCTET_STREAM) + Raw::new(body.content, body.content_type) } # fn main() { # gotham::start("127.0.0.1:8080", build_simple_router(|route| { diff --git a/gotham_restful_derive/src/from_body.rs b/gotham_restful_derive/src/from_body.rs index 4119845..b3368ad 100644 --- a/gotham_restful_derive/src/from_body.rs +++ b/gotham_restful_derive/src/from_body.rs @@ -1,11 +1,15 @@ use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; -use quote::quote; +use quote::{format_ident, quote}; use syn::{ - spanned::Spanned, + punctuated::Punctuated, + token::Comma, Error, + Field, Fields, + Ident, ItemStruct, + Type, parse_macro_input }; @@ -16,6 +20,32 @@ pub fn expand_from_body(tokens : TokenStream) -> TokenStream .into() } +struct ParsedFields +{ + fields : Vec<(Ident, Type)>, + named : bool +} + +impl ParsedFields +{ + fn from_named(fields : Punctuated) -> Result + { + let fields = fields.into_iter().map(|field| (field.ident.unwrap(), field.ty)).collect(); + Ok(Self { fields, named: true }) + } + + fn from_unnamed(fields : Punctuated) -> Result + { + let fields = fields.into_iter().enumerate().map(|(i, field)| (format_ident!("arg{}", i), field.ty)).collect(); + Ok(Self { fields, named: false }) + } + + fn from_unit() -> Result + { + Ok(Self { fields: Vec::new(), named: false }) + } +} + fn expand(tokens : TokenStream) -> Result { let krate = super::krate(); @@ -23,45 +53,78 @@ fn expand(tokens : TokenStream) -> Result let ident = input.ident; let generics = input.generics; - let (were, body) = match input.fields { - Fields::Named(named) => { - let fields = named.named; - match fields.len() { - 0 => (quote!(), quote!(Self{})), - 1 => { - let field = fields.first().unwrap(); - let field_ident = field.ident.as_ref().unwrap(); - let field_ty = &field.ty; - (quote!(where #field_ty : for<'a> From<&'a [u8]>), quote!(Self { #field_ident: body.into() })) - }, - _ => return Err(Error::new(fields.into_iter().nth(1).unwrap().span(), "FromBody can only be derived for structs with at most one field")) - } - }, - Fields::Unnamed(unnamed) => { - let fields = unnamed.unnamed; - match fields.len() { - 0 => (quote!(), quote!(Self{})), - 1 => { - let field = fields.first().unwrap(); - let field_ty = &field.ty; - (quote!(where #field_ty : for<'a> From<&'a [u8]>), quote!(Self(body.into()))) - }, - _ => return Err(Error::new(fields.into_iter().nth(1).unwrap().span(), "FromBody can only be derived for structs with at most one field")) - } - }, - Fields::Unit => (quote!(), quote!(Self{})) + let fields = match input.fields { + Fields::Named(named) => ParsedFields::from_named(named.named)?, + Fields::Unnamed(unnamed) => ParsedFields::from_unnamed(unnamed.unnamed)?, + Fields::Unit => ParsedFields::from_unit()? }; + let mut where_clause = quote!(); + let mut block = quote!(); + let mut body_ident = format_ident!("_body"); + let mut type_ident = format_ident!("_type"); + + if let Some(body_field) = fields.fields.get(0) + { + body_ident = body_field.0.clone(); + let body_ty = &body_field.1; + where_clause = quote! { + #where_clause + #body_ty : for<'a> From<&'a [u8]>, + }; + block = quote! { + #block + let #body_ident : &[u8] = &#body_ident; + let #body_ident : #body_ty = #body_ident.into(); + }; + } + + if let Some(type_field) = fields.fields.get(1) + { + type_ident = type_field.0.clone(); + let type_ty = &type_field.1; + where_clause = quote! { + #where_clause + #type_ty : From<#krate::Mime>, + }; + block = quote! { + #block + let #type_ident : #type_ty = #type_ident.into(); + }; + } + + for field in &fields.fields[2..] + { + let field_ident = &field.0; + let field_ty = &field.1; + where_clause = quote! { + #where_clause + #field_ty : Default, + }; + block = quote! { + #block + let #field_ident : #field_ty = Default::default(); + }; + } + + let field_names : Vec<&Ident> = fields.fields.iter().map(|field| &field.0).collect(); + let ctor = if fields.named { + quote!(Self { #(#field_names),* }) + } else { + quote!(Self ( #(#field_names),* )) + }; + + // TODO: Replace the Err type with something more appropriate that implements Display Ok(quote! { impl #generics #krate::FromBody for #ident #generics - #were + where #where_clause { type Err = String; - fn from_body(body : #krate::gotham::hyper::body::Bytes, _content_type : #krate::Mime) -> Result + fn from_body(#body_ident : #krate::gotham::hyper::body::Bytes, #type_ident : #krate::Mime) -> Result { - let body : &[u8] = &body; - Ok(#body) + #block + Ok(#ctor) } } }) From 8834f3f64b389545ff1dddefaabf6ea693033960 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 19 Apr 2020 22:27:34 +0200 Subject: [PATCH 016/170] fix import error --- gotham_restful/src/auth.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gotham_restful/src/auth.rs b/gotham_restful/src/auth.rs index 137e9b4..8324622 100644 --- a/gotham_restful/src/auth.rs +++ b/gotham_restful/src/auth.rs @@ -71,7 +71,7 @@ This trait will help the auth middleware to determine the validity of an authent A very basic implementation could look like this: ``` -# use gotham_restful::{export::State, AuthHandler}; +# use gotham_restful::{AuthHandler, State}; # const SECRET : &'static [u8; 32] = b"zlBsA2QXnkmpe0QTh8uCvtAEa4j33YAc"; From b6006797f4d7e26ff8b21481367323b7c40b1be2 Mon Sep 17 00:00:00 2001 From: Dominic Date: Mon, 20 Apr 2020 22:34:39 +0200 Subject: [PATCH 017/170] update readme --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8ebf802..e0aeb60 100644 --- a/README.md +++ b/README.md @@ -78,11 +78,14 @@ struct ImageResource; #[derive(FromBody, RequestBody)] #[supported_types(mime::IMAGE_GIF, mime::IMAGE_JPEG, mime::IMAGE_PNG)] -struct RawImage(Vec); +struct RawImage { + content: Vec, + content_type: Mime +} #[rest_create(ImageResource)] fn create(_state : &mut State, body : RawImage) -> Raw> { - Raw::new(body.0, mime::APPLICATION_OCTET_STREAM) + Raw::new(body.content, body.content_type) } ``` From f70865d246ba746f25e1701b0bfb5e5745e4d64c Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 22 Apr 2020 11:18:10 +0200 Subject: [PATCH 018/170] fix no schema having content --- gotham_restful/src/openapi/router.rs | 7 +++---- gotham_restful/src/result.rs | 10 ++++++++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/gotham_restful/src/openapi/router.rs b/gotham_restful/src/openapi/router.rs index a379585..a5ea5eb 100644 --- a/gotham_restful/src/openapi/router.rs +++ b/gotham_restful/src/openapi/router.rs @@ -317,9 +317,8 @@ fn new_operation( let mut responses : IndexMap> = IndexMap::new(); responses.insert(StatusCode::Code(default_status.as_u16()), Item(Response { description: default_status.canonical_reason().map(|d| d.to_string()).unwrap_or_default(), - headers: IndexMap::new(), content, - links: IndexMap::new() + ..Default::default() })); let request_body = body_schema.map(|schema| Item(OARequestBody { @@ -552,7 +551,7 @@ mod test { let types = NoContent::accepted_types(); let schema = ::schema(); - let content = schema_to_content(types.unwrap_or_default(), Item(schema.into_schema())); + let content = schema_to_content(types.unwrap_or_else(|| vec![STAR_STAR]), Item(schema.into_schema())); assert!(content.is_empty()); } @@ -561,7 +560,7 @@ mod test { let types = Raw::<&str>::accepted_types(); let schema = as OpenapiType>::schema(); - let content = schema_to_content(types.unwrap_or_default(), Item(schema.into_schema())); + let content = schema_to_content(types.unwrap_or_else(|| vec![STAR_STAR]), Item(schema.into_schema())); assert_eq!(content.len(), 1); let json = serde_json::to_string(&content.values().nth(0).unwrap()).unwrap(); assert_eq!(json, r#"{"schema":{"type":"string","format":"binary"}}"#); diff --git a/gotham_restful/src/result.rs b/gotham_restful/src/result.rs index 7e5bdd9..a8d49ed 100644 --- a/gotham_restful/src/result.rs +++ b/gotham_restful/src/result.rs @@ -483,6 +483,11 @@ impl ResourceResult for NoContent future::ok(Response::no_content()).boxed() } + fn accepted_types() -> Option> + { + Some(Vec::new()) + } + /// Returns the schema of the `()` type. #[cfg(feature = "openapi")] fn schema() -> OpenapiSchema @@ -515,6 +520,11 @@ where } } + fn accepted_types() -> Option> + { + NoContent::accepted_types() + } + #[cfg(feature = "openapi")] fn schema() -> OpenapiSchema { From 876f44cefff9acf95b686637466449f5805b0997 Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 22 Apr 2020 11:31:45 +0200 Subject: [PATCH 019/170] advertise the stable branch --- README.md | 5 +++-- gotham_restful/src/lib.rs | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e0aeb60..6815044 100644 --- a/README.md +++ b/README.md @@ -30,8 +30,9 @@ bodies, relying on [`serde`][serde] and [`serde_json`][serde_json] for (de)seria enable the `openapi` feature, you can also generate an OpenAPI Specification from your RESTful resources. -**Note:** The master branch currently tracks gotham's master branch and the next release will use -gotham 0.5.0 and be compatible with the new future / async stuff. +**Note:** The `stable` branch contains some bugfixes against the last release. The `master` +branch currently tracks gotham's master branch and the next release will use gotham 0.5.0 and be +compatible with the new future / async stuff. ## Usage diff --git a/gotham_restful/src/lib.rs b/gotham_restful/src/lib.rs index 0295a4b..9bc4674 100644 --- a/gotham_restful/src/lib.rs +++ b/gotham_restful/src/lib.rs @@ -6,8 +6,9 @@ bodies, relying on [`serde`][serde] and [`serde_json`][serde_json] for (de)seria enable the `openapi` feature, you can also generate an OpenAPI Specification from your RESTful resources. -**Note:** The master branch currently tracks gotham's master branch and the next release will use -gotham 0.5.0 and be compatible with the new future / async stuff. +**Note:** The `stable` branch contains some bugfixes against the last release. The `master` +branch currently tracks gotham's master branch and the next release will use gotham 0.5.0 and be +compatible with the new future / async stuff. # Usage From ad6e3dd00d5271e061616dc676fc63497183528c Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 22 Apr 2020 11:46:15 +0200 Subject: [PATCH 020/170] make sure that 204-responses can be accepted --- gotham_restful/src/routing.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gotham_restful/src/routing.rs b/gotham_restful/src/routing.rs index b8d8c56..617e59c 100644 --- a/gotham_restful/src/routing.rs +++ b/gotham_restful/src/routing.rs @@ -271,6 +271,10 @@ impl From>> for MaybeMatchAcceptHeader { fn from(types : Option>) -> Self { + let types = match types { + Some(types) if types.is_empty() => None, + types => types + }; Self { matcher: types.map(AcceptHeaderRouteMatcher::new) } From 147ea980bf6e1f9639623a3b45d1d48f3f1375c0 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sat, 25 Apr 2020 16:47:33 +0200 Subject: [PATCH 021/170] move to Rust 1.42 features --- README.md | 4 ++-- README.tpl | 4 ++-- gotham_restful_derive/src/lib.rs | 2 -- gotham_restful_derive/src/method.rs | 15 +++------------ 4 files changed, 7 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 6815044..51700af 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,8 @@ Build with Rust - - Minimum Rust Version + + Minimum Rust Version
diff --git a/README.tpl b/README.tpl index a148bef..5be3b7f 100644 --- a/README.tpl +++ b/README.tpl @@ -17,8 +17,8 @@ Build with Rust - - Minimum Rust Version + + Minimum Rust Version
diff --git a/gotham_restful_derive/src/lib.rs b/gotham_restful_derive/src/lib.rs index 71017d9..4a98a9d 100644 --- a/gotham_restful_derive/src/lib.rs +++ b/gotham_restful_derive/src/lib.rs @@ -1,5 +1,3 @@ -extern crate proc_macro; - use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use quote::quote; diff --git a/gotham_restful_derive/src/method.rs b/gotham_restful_derive/src/method.rs index 098c4c7..d83f6ab 100644 --- a/gotham_restful_derive/src/method.rs +++ b/gotham_restful_derive/src/method.rs @@ -134,26 +134,17 @@ impl MethodArgumentType { fn is_method_arg(&self) -> bool { - match self { - Self::MethodArg(_) => true, - _ => false, - } + matches!(self, Self::MethodArg(_)) } fn is_database_conn(&self) -> bool { - match self { - Self::DatabaseConnection(_) => true, - _ => false - } + matches!(self, Self::DatabaseConnection(_)) } fn is_auth_status(&self) -> bool { - match self { - Self::AuthStatus(_) | Self::AuthStatusRef(_) => true, - _ => false - } + matches!(self, Self::AuthStatus(_) | Self::AuthStatusRef(_)) } fn quote_ty(&self) -> Option From d08d9bea8c6c0a1f22030c9d4b58a76c61362f58 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sat, 25 Apr 2020 17:01:16 +0200 Subject: [PATCH 022/170] fix some clippy warnings --- gotham_restful/src/auth.rs | 4 +- gotham_restful/src/lib.rs | 1 + gotham_restful/src/openapi/router.rs | 4 +- gotham_restful/src/openapi/types.rs | 7 +++- gotham_restful_derive/src/method.rs | 45 +++++++++++------------ gotham_restful_derive/src/openapi_type.rs | 31 +++++++--------- gotham_restful_derive/src/request_body.rs | 2 +- 7 files changed, 46 insertions(+), 48 deletions(-) diff --git a/gotham_restful/src/auth.rs b/gotham_restful/src/auth.rs index 8324622..94c95fd 100644 --- a/gotham_restful/src/auth.rs +++ b/gotham_restful/src/auth.rs @@ -240,7 +240,7 @@ where // get the secret from the handler, possibly decoding claims ourselves let secret = self.handler.jwt_secret(state, || { - let b64 = token.split(".").nth(1)?; + let b64 = token.split('.').nth(1)?; let raw = base64::decode_config(b64, base64::URL_SAFE_NO_PAD).ok()?; serde_json::from_slice(&raw).ok()? }); @@ -261,7 +261,7 @@ where }; // we found a valid token - return AuthStatus::Authenticated(data); + AuthStatus::Authenticated(data) } } diff --git a/gotham_restful/src/lib.rs b/gotham_restful/src/lib.rs index 9bc4674..a31247c 100644 --- a/gotham_restful/src/lib.rs +++ b/gotham_restful/src/lib.rs @@ -1,3 +1,4 @@ +#![allow(clippy::tabs_in_doc_comments)] /*! This crate is an extension to the popular [gotham web framework][gotham] for Rust. The idea is to have several RESTful resources that can be added to the gotham router. This crate will take care diff --git a/gotham_restful/src/openapi/router.rs b/gotham_restful/src/openapi/router.rs index a5ea5eb..d69c111 100644 --- a/gotham_restful/src/openapi/router.rs +++ b/gotham_restful/src/openapi/router.rs @@ -136,7 +136,7 @@ impl NewHandler for OpenapiHandler } #[cfg(feature = "auth")] -const SECURITY_NAME : &'static str = "authToken"; +const SECURITY_NAME : &str = "authToken"; #[cfg(feature = "auth")] fn get_security(state : &mut State) -> IndexMap> @@ -249,7 +249,7 @@ impl<'a> OperationParams<'a> { params.push(Item(Parameter::Path { parameter_data: ParameterData { - name: param.to_string(), + name: (*param).to_string(), description: None, required: true, deprecated: None, diff --git a/gotham_restful/src/openapi/types.rs b/gotham_restful/src/openapi/types.rs index 5cbacce..49e5e57 100644 --- a/gotham_restful/src/openapi/types.rs +++ b/gotham_restful/src/openapi/types.rs @@ -9,7 +9,10 @@ use openapiv3::{ }; #[cfg(feature = "uuid")] use uuid::Uuid; -use std::collections::{BTreeSet, HashSet}; +use std::{ + collections::{BTreeSet, HashSet}, + hash::BuildHasher +}; /** This struct needs to be available for every type that can be part of an OpenAPI Spec. It is @@ -296,7 +299,7 @@ impl OpenapiType for BTreeSet } } -impl OpenapiType for HashSet +impl OpenapiType for HashSet { fn schema() -> OpenapiSchema { diff --git a/gotham_restful_derive/src/method.rs b/gotham_restful_derive/src/method.rs index d83f6ab..514bb4a 100644 --- a/gotham_restful_derive/src/method.rs +++ b/gotham_restful_derive/src/method.rs @@ -120,6 +120,7 @@ impl Method } } +#[allow(clippy::large_enum_variant)] enum MethodArgumentType { StateRef, @@ -176,9 +177,8 @@ impl Spanned for MethodArgument fn interpret_arg_ty(index : usize, attrs : &[Attribute], name : &str, ty : Type) -> Result { - let attr = attrs.into_iter() - .filter(|arg| arg.path.segments.iter().filter(|path| &path.ident.to_string() == "rest_arg").nth(0).is_some()) - .nth(0) + let attr = attrs.iter() + .find(|arg| arg.path.segments.iter().any(|path| &path.ident.to_string() == "rest_arg")) .map(|arg| arg.tokens.to_string()); if cfg!(feature = "auth") && (attr.as_deref() == Some("auth") || (attr.is_none() && name == "auth")) @@ -219,19 +219,17 @@ fn interpret_arg(index : usize, arg : &PatType) -> Result } #[cfg(feature = "openapi")] -fn expand_operation_id(attrs : &AttributeArgs) -> TokenStream2 +fn expand_operation_id(attrs : &[NestedMeta]) -> TokenStream2 { let mut operation_id : Option<&Lit> = None; for meta in attrs { - match meta { - NestedMeta::Meta(Meta::NameValue(kv)) => { - if kv.path.segments.last().map(|p| p.ident.to_string()) == Some("operation_id".to_owned()) - { - operation_id = Some(&kv.lit) - } - }, - _ => {} + 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) + } } } @@ -247,25 +245,23 @@ fn expand_operation_id(attrs : &AttributeArgs) -> TokenStream2 } #[cfg(not(feature = "openapi"))] -fn expand_operation_id(_ : &AttributeArgs) -> TokenStream2 +fn expand_operation_id(_ : &[NestedMeta]) -> TokenStream2 { quote!() } -fn expand_wants_auth(attrs : &AttributeArgs, default : bool) -> TokenStream2 +fn expand_wants_auth(attrs : &[NestedMeta], default : bool) -> TokenStream2 { let default_lit = Lit::Bool(LitBool { value: default, span: Span::call_site() }); let mut wants_auth = &default_lit; for meta in attrs { - match meta { - NestedMeta::Meta(Meta::NameValue(kv)) => { - if kv.path.segments.last().map(|p| p.ident.to_string()) == Some("wants_auth".to_owned()) - { - wants_auth = &kv.lit - } - }, - _ => {} + if let NestedMeta::Meta(Meta::NameValue(kv)) = meta + { + if kv.path.segments.last().map(|p| p.ident.to_string()) == Some("wants_auth".to_owned()) + { + wants_auth = &kv.lit + } } } @@ -277,6 +273,7 @@ fn expand_wants_auth(attrs : &AttributeArgs, default : bool) -> TokenStream2 } } +#[allow(clippy::comparison_chain)] fn expand(method : Method, attrs : TokenStream, item : TokenStream) -> Result { let krate = super::krate(); @@ -381,7 +378,7 @@ fn expand(method : Method, attrs : TokenStream, item : TokenStream) -> Result Result TokenStream2 { if generics.params.is_empty() { - quote!() + return quote!(); } - else - { - let krate = super::krate(); - let idents = generics.params.iter() - .map(|param| match param { - GenericParam::Type(ty) => Some(ty.ident.clone()), - _ => None - }) - .filter(|param| param.is_some()) - .map(|param| param.unwrap()); - - quote! { - where #(#idents : #krate::OpenapiType),* - } + + let krate = super::krate(); + let idents = generics.params.iter() + .map(|param| match param { + GenericParam::Type(ty) => Some(ty.ident.clone()), + _ => None + }) + .filter(|param| param.is_some()) + .map(|param| param.unwrap()); + + quote! { + where #(#idents : #krate::OpenapiType),* } } @@ -98,8 +96,7 @@ fn remove_parens(input : TokenStream2) -> TokenStream2 } Box::new(iter::once(tt)) }); - let output = TokenStream2::from_iter(iter); - output + TokenStream2::from_iter(iter) } fn parse_attributes(input : &[Attribute]) -> Result diff --git a/gotham_restful_derive/src/request_body.rs b/gotham_restful_derive/src/request_body.rs index 6c6f473..7bc5d9d 100644 --- a/gotham_restful_derive/src/request_body.rs +++ b/gotham_restful_derive/src/request_body.rs @@ -67,7 +67,7 @@ fn expand(tokens : TokenStream) -> Result .filter(|attr| attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("supported_types".to_string())) .flat_map(|attr| syn::parse2::(attr.tokens) - .map(|list| Box::new(list.0.into_iter().map(|mime| Ok(mime))) as Box>>) + .map(|list| Box::new(list.0.into_iter().map(Ok)) as Box>>) .unwrap_or_else(|err| Box::new(iter::once(Err(err))))) .collect_to_result()?; From 4ce53bc361f38d0ef39cf817f75af57bfa1d0e1b Mon Sep 17 00:00:00 2001 From: msrd0 <1182023-msrd0@users.noreply.gitlab.com> Date: Sat, 25 Apr 2020 18:31:57 +0000 Subject: [PATCH 023/170] Add path matchers that are more capable than gotham's stock ones --- gotham_restful/Cargo.toml | 3 +- gotham_restful/src/lib.rs | 2 + gotham_restful/src/matcher/accept.rs | 214 +++++++++++++++++++++ gotham_restful/src/matcher/content_type.rs | 173 +++++++++++++++++ gotham_restful/src/matcher/mod.rs | 37 ++++ gotham_restful/src/result.rs | 7 +- gotham_restful/src/routing.rs | 15 +- 7 files changed, 435 insertions(+), 16 deletions(-) create mode 100644 gotham_restful/src/matcher/accept.rs create mode 100644 gotham_restful/src/matcher/content_type.rs create mode 100644 gotham_restful/src/matcher/mod.rs diff --git a/gotham_restful/Cargo.toml b/gotham_restful/Cargo.toml index bb9bd45..512cbb1 100644 --- a/gotham_restful/Cargo.toml +++ b/gotham_restful/Cargo.toml @@ -25,18 +25,19 @@ gotham_derive = { git = "https://github.com/gotham-rs/gotham", version = "0.5.0- gotham_middleware_diesel = { git = "https://github.com/gotham-rs/gotham", version = "0.1.0", optional = true } gotham_restful_derive = { version = "0.0.4-dev" } indexmap = { version = "1.3.2", optional = true } +itertools = "0.9.0" jsonwebtoken = { version = "7.1.0", optional = true } log = { version = "0.4.8", optional = true } mime = "0.3.16" openapiv3 = { version = "0.3", optional = true } serde = { version = "1.0.106", features = ["derive"] } serde_json = "1.0.51" +thiserror = "1.0.15" uuid = { version = ">= 0.1, < 0.9", optional = true } [dev-dependencies] futures-executor = "0.3.4" paste = "0.1.10" -thiserror = "1" [features] default = [] diff --git a/gotham_restful/src/lib.rs b/gotham_restful/src/lib.rs index a31247c..ad19175 100644 --- a/gotham_restful/src/lib.rs +++ b/gotham_restful/src/lib.rs @@ -151,6 +151,8 @@ pub use auth::{ StaticAuthHandler }; +pub mod matcher; + #[cfg(feature = "openapi")] mod openapi; #[cfg(feature = "openapi")] diff --git a/gotham_restful/src/matcher/accept.rs b/gotham_restful/src/matcher/accept.rs new file mode 100644 index 0000000..82e0fed --- /dev/null +++ b/gotham_restful/src/matcher/accept.rs @@ -0,0 +1,214 @@ +use super::{LookupTable, LookupTableFromTypes}; +use gotham::{ + hyper::{ + header::{HeaderMap, ACCEPT}, + StatusCode + }, + router::{non_match::RouteNonMatch, route::matcher::RouteMatcher}, + state::{FromState, State} +}; +use mime::Mime; +use std::{ + num::ParseFloatError, + str::FromStr +}; +use thiserror::Error; + + +/// A mime type that is optionally weighted with a quality. +#[derive(Debug)] +struct QMime +{ + mime : Mime, + weight : Option +} + +impl QMime +{ + fn new(mime : Mime, weight : Option) -> Self + { + Self { mime, weight } + } +} + +#[derive(Debug, Error)] +enum QMimeError +{ + #[error("Unable to parse mime type: {0}")] + MimeError(#[from] mime::FromStrError), + #[error("Unable to parse mime quality: {0}")] + NumError(#[from] ParseFloatError) +} + +impl FromStr for QMime +{ + type Err = QMimeError; + + fn from_str(str : &str) -> Result + { + match str.find(";q=") { + None => Ok(Self::new(str.parse()?, None)), + Some(index) => { + let mime = str[..index].parse()?; + let weight = str[index+3..].parse()?; + Ok(Self::new(mime, Some(weight))) + } + } + } +} + + +/** +A route matcher that checks for the presence of a supported content type. + +Usage: + +``` +# use gotham::{helpers::http::response::create_response, hyper::StatusCode, router::builder::*}; +# use gotham_restful::matcher::ContentTypeMatcher; +# +let types = vec![mime::TEXT_HTML, mime::TEXT_PLAIN]; +let matcher = ContentTypeMatcher::new(types) + // optionally accept requests with no content type + .allow_no_type(); + +# build_simple_router(|route| { +// use the matcher for your request +route.post("/foo") + .extend_route_matcher(matcher) + .to(|state| { + let res = create_response(&state, StatusCode::OK, mime::TEXT_PLAIN, "Correct Content Type!"); + (state, res) + }); +# }); +``` +*/ +#[derive(Clone)] +pub struct AcceptHeaderMatcher +{ + types : Vec, + lookup_table : LookupTable +} + +impl AcceptHeaderMatcher +{ + /// Create a new `AcceptHeaderMatcher` with the given types that can be produced by the route. + pub fn new(types : Vec) -> Self + { + let lookup_table = LookupTable::from_types(types.iter(), true); + Self { types, lookup_table } + } +} + +#[inline] +fn err() -> RouteNonMatch +{ + RouteNonMatch::new(StatusCode::NOT_ACCEPTABLE) +} + +impl RouteMatcher for AcceptHeaderMatcher +{ + fn is_match(&self, state : &State) -> Result<(), RouteNonMatch> + { + HeaderMap::borrow_from(state).get(ACCEPT) + .map(|header| { + // parse mime types from the accept header + let acceptable = header.to_str() + .map_err(|_| err())? + .split(',') + .map(|str| str.trim().parse()) + .collect::, _>>() + .map_err(|_| err())?; + + for qmime in acceptable + { + // get mime type candidates from the lookup table + let essence = qmime.mime.essence_str(); + let candidates = match self.lookup_table.get(essence) { + Some(candidates) => candidates, + None => continue + }; + for i in candidates + { + let candidate = &self.types[*i]; + + // check that the candidates have the same suffix - this is not included in the + // essence string + if candidate.suffix() != qmime.mime.suffix() + { + continue + } + + // this candidate matches - params don't play a role in accept header matching + return Ok(()) + } + } + + // no candidates found + Err(err()) + }).unwrap_or_else(|| { + // no accept header - assume all types are acceptable + Ok(()) + }) + } +} + + +#[cfg(test)] +mod test +{ + use super::*; + + fn with_state(accept : Option<&str>, block : F) + where F : FnOnce(&mut State) -> () + { + State::with_new(|state| { + let mut headers = HeaderMap::new(); + if let Some(acc) = accept + { + headers.insert(ACCEPT, acc.parse().unwrap()); + } + state.put(headers); + block(state); + }); + } + + #[test] + fn no_accept_header() + { + let matcher = AcceptHeaderMatcher::new(vec!(mime::TEXT_PLAIN)); + with_state(None, |state| assert!(matcher.is_match(&state).is_ok())); + } + + #[test] + fn single_mime_type() + { + let matcher = AcceptHeaderMatcher::new(vec!(mime::TEXT_PLAIN, mime::IMAGE_PNG)); + with_state(Some("text/plain"), |state| assert!(matcher.is_match(&state).is_ok())); + with_state(Some("text/html"), |state| assert!(matcher.is_match(&state).is_err())); + with_state(Some("image/png"), |state| assert!(matcher.is_match(&state).is_ok())); + with_state(Some("image/webp"), |state| assert!(matcher.is_match(&state).is_err())); + } + + #[test] + fn star_star() + { + let matcher = AcceptHeaderMatcher::new(vec!(mime::IMAGE_PNG)); + with_state(Some("*/*"), |state| assert!(matcher.is_match(&state).is_ok())); + } + + #[test] + fn image_star() + { + let matcher = AcceptHeaderMatcher::new(vec!(mime::IMAGE_PNG)); + with_state(Some("image/*"), |state| assert!(matcher.is_match(&state).is_ok())); + } + + #[test] + fn complex_header() + { + let matcher = AcceptHeaderMatcher::new(vec!(mime::IMAGE_PNG)); + with_state(Some("text/html,image/webp;q=0.8"), |state| assert!(matcher.is_match(&state).is_err())); + with_state(Some("text/html,image/webp;q=0.8,*/*;q=0.1"), |state| assert!(matcher.is_match(&state).is_ok())); + } +} diff --git a/gotham_restful/src/matcher/content_type.rs b/gotham_restful/src/matcher/content_type.rs new file mode 100644 index 0000000..cb55571 --- /dev/null +++ b/gotham_restful/src/matcher/content_type.rs @@ -0,0 +1,173 @@ +use super::{LookupTable, LookupTableFromTypes}; +use gotham::{ + hyper::{ + header::{HeaderMap, CONTENT_TYPE}, + StatusCode + }, + router::{non_match::RouteNonMatch, route::matcher::RouteMatcher}, + state::{FromState, State} +}; +use mime::Mime; + +/** +A route matcher that checks for the presence of a supported content type. + +Usage: + +``` +# use gotham::{helpers::http::response::create_response, hyper::StatusCode, router::builder::*}; +# use gotham_restful::matcher::ContentTypeMatcher; +# +let types = vec![mime::TEXT_HTML, mime::TEXT_PLAIN]; +let matcher = ContentTypeMatcher::new(types) + // optionally accept requests with no content type + .allow_no_type(); + +# build_simple_router(|route| { +// use the matcher for your request +route.post("/foo") + .extend_route_matcher(matcher) + .to(|state| { + let res = create_response(&state, StatusCode::OK, mime::TEXT_PLAIN, "Correct Content Type!"); + (state, res) + }); +# }); +``` +*/ +#[derive(Clone)] +pub struct ContentTypeMatcher +{ + types : Vec, + lookup_table : LookupTable, + allow_no_type : bool +} + +impl ContentTypeMatcher +{ + /// Create a new `ContentTypeMatcher` with the given supported types that does not allow requests + /// that don't include a content-type header. + pub fn new(types : Vec) -> Self + { + let lookup_table = LookupTable::from_types(types.iter(), false); + Self { types, lookup_table, allow_no_type: false } + } + + /// Modify this matcher to allow requests that don't include a content-type header. + pub fn allow_no_type(mut self) -> Self + { + self.allow_no_type = true; + self + } +} + +#[inline] +fn err() -> RouteNonMatch +{ + RouteNonMatch::new(StatusCode::UNSUPPORTED_MEDIA_TYPE) +} + +impl RouteMatcher for ContentTypeMatcher +{ + fn is_match(&self, state : &State) -> Result<(), RouteNonMatch> + { + HeaderMap::borrow_from(state).get(CONTENT_TYPE) + .map(|ty| { + // parse mime type from the content type header + let mime : Mime = ty.to_str() + .map_err(|_| err())? + .parse() + .map_err(|_| err())?; + + // get mime type candidates from the lookup table + let essence = mime.essence_str(); + let candidates = self.lookup_table.get(essence).ok_or_else(err)?; + for i in candidates + { + let candidate = &self.types[*i]; + + // check that the candidates have the same suffix - this is not included in the + // essence string + if candidate.suffix() != mime.suffix() + { + continue + } + + // check that this candidate has at least the parameters that the content type + // has and that their values are equal + if candidate.params().any(|(key, value)| mime.get_param(key) != Some(value)) + { + continue + } + + // this candidate matches + return Ok(()) + } + + // no candidates found + Err(err()) + }).unwrap_or_else(|| { + // no type present + if self.allow_no_type { Ok(()) } else { Err(err()) } + }) + } +} + + +#[cfg(test)] +mod test +{ + use super::*; + + fn with_state(content_type : Option<&str>, block : F) + where F : FnOnce(&mut State) -> () + { + State::with_new(|state| { + let mut headers = HeaderMap::new(); + if let Some(ty) = content_type + { + headers.insert(CONTENT_TYPE, ty.parse().unwrap()); + } + state.put(headers); + block(state); + }); + } + + #[test] + fn empty_type_list() + { + let matcher = ContentTypeMatcher::new(Vec::new()); + with_state(None, |state| assert!(matcher.is_match(&state).is_err())); + with_state(Some("text/plain"), |state| assert!(matcher.is_match(&state).is_err())); + + let matcher = matcher.allow_no_type(); + with_state(None, |state| assert!(matcher.is_match(&state).is_ok())); + } + + #[test] + fn simple_type() + { + let matcher = ContentTypeMatcher::new(vec![mime::TEXT_PLAIN]); + with_state(None, |state| assert!(matcher.is_match(&state).is_err())); + with_state(Some("text/plain"), |state| assert!(matcher.is_match(&state).is_ok())); + with_state(Some("text/plain; charset=utf-8"), |state| assert!(matcher.is_match(&state).is_ok())); + } + + #[test] + fn complex_type() + { + let matcher = ContentTypeMatcher::new(vec!["image/svg+xml; charset=utf-8".parse().unwrap()]); + with_state(Some("image/svg"), |state| assert!(matcher.is_match(&state).is_err())); + with_state(Some("image/svg+xml"), |state| assert!(matcher.is_match(&state).is_err())); + with_state(Some("image/svg+xml; charset=utf-8"), |state| assert!(matcher.is_match(&state).is_ok())); + with_state(Some("image/svg+xml; charset=utf-8; eol=lf"), |state| assert!(matcher.is_match(&state).is_ok())); + with_state(Some("image/svg+xml; charset=us-ascii"), |state| assert!(matcher.is_match(&state).is_err())); + with_state(Some("image/svg+json; charset=utf-8"), |state| assert!(matcher.is_match(&state).is_err())); + } + + #[test] + fn type_mismatch() + { + let matcher = ContentTypeMatcher::new(vec![mime::TEXT_HTML]); + with_state(Some("text/plain"), |state| assert!(matcher.is_match(&state).is_err())); + } +} diff --git a/gotham_restful/src/matcher/mod.rs b/gotham_restful/src/matcher/mod.rs new file mode 100644 index 0000000..e1029e3 --- /dev/null +++ b/gotham_restful/src/matcher/mod.rs @@ -0,0 +1,37 @@ +use itertools::Itertools; +use mime::Mime; +use std::collections::HashMap; + +mod accept; +pub use accept::AcceptHeaderMatcher; + +mod content_type; +pub use content_type::ContentTypeMatcher; + +type LookupTable = HashMap>; + +trait LookupTableFromTypes +{ + fn from_types<'a, I : Iterator>(types : I, include_stars : bool) -> Self; +} + +impl LookupTableFromTypes for LookupTable +{ + fn from_types<'a, I : Iterator>(types : I, include_stars : bool) -> Self + { + if include_stars + { + types + .enumerate() + .flat_map(|(i, mime)| vec![("*/*".to_owned(), i), (format!("{}/*", mime.type_()), i), (mime.essence_str().to_owned(), i)].into_iter()) + .into_group_map() + } + else + { + types + .enumerate() + .map(|(i, mime)| (mime.essence_str().to_owned(), i)) + .into_group_map() + } + } +} diff --git a/gotham_restful/src/result.rs b/gotham_restful/src/result.rs index a8d49ed..792298f 100644 --- a/gotham_restful/src/result.rs +++ b/gotham_restful/src/result.rs @@ -6,7 +6,7 @@ use futures_util::{future, future::FutureExt}; use gotham::hyper::Body; #[cfg(feature = "errorlog")] use log::error; -use mime::{Mime, APPLICATION_JSON, STAR_STAR}; +use mime::{Mime, APPLICATION_JSON}; #[cfg(feature = "openapi")] use openapiv3::{SchemaKind, StringFormat, StringType, Type, VariantOrUnknownOrEmpty}; use serde::Serialize; @@ -581,11 +581,6 @@ where future::ok(Response::new(StatusCode::OK, self.raw, Some(self.mime.clone()))).boxed() } - fn accepted_types() -> Option> - { - Some(vec![STAR_STAR]) - } - #[cfg(feature = "openapi")] fn schema() -> OpenapiSchema { diff --git a/gotham_restful/src/routing.rs b/gotham_restful/src/routing.rs index 617e59c..7ff6ce1 100644 --- a/gotham_restful/src/routing.rs +++ b/gotham_restful/src/routing.rs @@ -1,4 +1,5 @@ use crate::{ + matcher::{AcceptHeaderMatcher, ContentTypeMatcher}, resource::*, result::{ResourceError, ResourceResult, Response}, RequestBody, @@ -15,11 +16,7 @@ use gotham::{ router::{ builder::*, non_match::RouteNonMatch, - route::matcher::{ - content_type::ContentTypeHeaderRouteMatcher, - AcceptHeaderRouteMatcher, - RouteMatcher - } + route::matcher::RouteMatcher }, state::{FromState, State} }; @@ -253,7 +250,7 @@ fn delete_handler(state : State) -> Pin + matcher : Option } impl RouteMatcher for MaybeMatchAcceptHeader @@ -276,7 +273,7 @@ impl From>> for MaybeMatchAcceptHeader types => types }; Self { - matcher: types.map(AcceptHeaderRouteMatcher::new) + matcher: types.map(AcceptHeaderMatcher::new) } } } @@ -284,7 +281,7 @@ impl From>> for MaybeMatchAcceptHeader #[derive(Clone)] struct MaybeMatchContentTypeHeader { - matcher : Option + matcher : Option } impl RouteMatcher for MaybeMatchContentTypeHeader @@ -303,7 +300,7 @@ impl From>> for MaybeMatchContentTypeHeader fn from(types : Option>) -> Self { Self { - matcher: types.map(ContentTypeHeaderRouteMatcher::new) + matcher: types.map(ContentTypeMatcher::new).map(ContentTypeMatcher::allow_no_type) } } } From 96317cdfb7c1f70c06888cef6411f721ab4da955 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sat, 25 Apr 2020 20:45:39 +0200 Subject: [PATCH 024/170] fix documentation for accept header matcher --- gotham_restful/src/matcher/accept.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/gotham_restful/src/matcher/accept.rs b/gotham_restful/src/matcher/accept.rs index 82e0fed..5c1ee4e 100644 --- a/gotham_restful/src/matcher/accept.rs +++ b/gotham_restful/src/matcher/accept.rs @@ -59,25 +59,28 @@ impl FromStr for QMime /** -A route matcher that checks for the presence of a supported content type. +A route matcher that checks whether the supported types match the accept header of the request. Usage: ``` # use gotham::{helpers::http::response::create_response, hyper::StatusCode, router::builder::*}; -# use gotham_restful::matcher::ContentTypeMatcher; +# use gotham_restful::matcher::AcceptHeaderMatcher; # -let types = vec![mime::TEXT_HTML, mime::TEXT_PLAIN]; -let matcher = ContentTypeMatcher::new(types) - // optionally accept requests with no content type - .allow_no_type(); +# const img_content : &[u8] = b"This is the content of a webp image"; +# +# let IMAGE_WEBP : mime::Mime = "image/webp".parse().unwrap(); +let types = vec![IMAGE_WEBP]; +let matcher = AcceptHeaderMatcher::new(types); # build_simple_router(|route| { // use the matcher for your request route.post("/foo") .extend_route_matcher(matcher) .to(|state| { - let res = create_response(&state, StatusCode::OK, mime::TEXT_PLAIN, "Correct Content Type!"); + // we know that the client is a modern browser and can handle webp images +# let IMAGE_WEBP : mime::Mime = "image/webp".parse().unwrap(); + let res = create_response(&state, StatusCode::OK, IMAGE_WEBP, img_content); (state, res) }); # }); From b4eaeca01c47a19d5c3f08564b027099f5ed96c7 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 26 Apr 2020 22:20:07 +0200 Subject: [PATCH 025/170] don't require the get_openapi call be the last one to contain the full spec --- gotham_restful/src/lib.rs | 2 +- gotham_restful/src/openapi/router.rs | 75 ++++++++++++++++++---------- gotham_restful/src/routing.rs | 8 +-- 3 files changed, 53 insertions(+), 32 deletions(-) diff --git a/gotham_restful/src/lib.rs b/gotham_restful/src/lib.rs index ad19175..6c1a5fd 100644 --- a/gotham_restful/src/lib.rs +++ b/gotham_restful/src/lib.rs @@ -157,7 +157,7 @@ pub mod matcher; mod openapi; #[cfg(feature = "openapi")] pub use openapi::{ - router::{GetOpenapi, OpenapiRouter}, + router::GetOpenapi, types::{OpenapiSchema, OpenapiType} }; diff --git a/gotham_restful/src/openapi/router.rs b/gotham_restful/src/openapi/router.rs index d69c111..3cf3b19 100644 --- a/gotham_restful/src/openapi/router.rs +++ b/gotham_restful/src/openapi/router.rs @@ -24,7 +24,8 @@ use openapiv3::{ }; use std::{ panic::RefUnwindSafe, - pin::Pin + pin::Pin, + sync::{Arc, RwLock} }; /** @@ -33,31 +34,37 @@ same time. There is no need to use this type directly. See [`WithOpenapi`] on ho [`WithOpenapi`]: trait.WithOpenapi.html */ -pub struct OpenapiRouter(OpenAPI); +pub struct OpenapiBuilder +{ + openapi : Arc> +} -impl OpenapiRouter +impl OpenapiBuilder { pub fn new(title : String, version : String, url : String) -> Self { - Self(OpenAPI { - openapi: "3.0.2".to_string(), - info: openapiv3::Info { - title, version, + Self { + openapi: Arc::new(RwLock::new(OpenAPI { + openapi: "3.0.2".to_string(), + info: openapiv3::Info { + title, version, + ..Default::default() + }, + servers: vec![Server { + url, + ..Default::default() + }], ..Default::default() - }, - servers: vec![Server { - url, - ..Default::default() - }], - ..Default::default() - }) + })) + } } - + /// 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 fn remove_path(&mut self, path : &str) -> PathItem { - match self.0.paths.swap_remove(path) { + let mut openapi = self.openapi.write().unwrap(); + match openapi.paths.swap_remove(path) { Some(Item(item)) => item, _ => PathItem::default() } @@ -65,21 +72,23 @@ impl OpenapiRouter fn add_path(&mut self, path : Path, item : PathItem) { - self.0.paths.insert(path.to_string(), Item(item)); + let mut openapi = self.openapi.write().unwrap(); + openapi.paths.insert(path.to_string(), Item(item)); } fn add_schema_impl(&mut self, name : String, mut schema : OpenapiSchema) { self.add_schema_dependencies(&mut schema.dependencies); - match &mut self.0.components { + let mut openapi = self.openapi.write().unwrap(); + match &mut openapi.components { Some(comp) => { comp.schemas.insert(name, Item(schema.into_schema())); }, None => { let mut comp = Components::default(); comp.schemas.insert(name, Item(schema.into_schema())); - self.0.components = Some(comp); + openapi.components = Some(comp); } }; } @@ -115,13 +124,16 @@ impl OpenapiRouter } #[derive(Clone)] -struct OpenapiHandler(OpenAPI); +struct OpenapiHandler +{ + openapi : Arc> +} impl OpenapiHandler { - fn new(openapi : &OpenapiRouter) -> Self + fn new(openapi : Arc>) -> Self { - Self(openapi.0.clone()) + Self { openapi } } } @@ -180,7 +192,16 @@ impl Handler for OpenapiHandler { fn handle(self, mut state : State) -> Pin> { - let mut openapi = self.0; + let openapi = match self.openapi.read() { + Ok(openapi) => openapi, + Err(e) => { + error!("Unable to acquire read lock for the OpenAPI specification: {}", e); + let res = create_response(&state, crate::StatusCode::INTERNAL_SERVER_ERROR, TEXT_PLAIN, ""); + return future::ok((state, res)).boxed() + } + }; + + let mut openapi = openapi.clone(); let security_schemes = get_security(&mut state); let mut components = openapi.components.unwrap_or_default(); components.security_schemes = security_schemes; @@ -353,18 +374,18 @@ fn new_operation( macro_rules! implOpenapiRouter { ($implType:ident) => { - impl<'a, C, P> GetOpenapi for (&mut $implType<'a, C, P>, &mut OpenapiRouter) + impl<'a, C, P> GetOpenapi for (&mut $implType<'a, C, P>, &mut OpenapiBuilder) where C : PipelineHandleChain

+ Copy + Send + Sync + 'static, P : RefUnwindSafe + Send + Sync + 'static { fn get_openapi(&mut self, path : &str) { - self.0.get(path).to_new_handler(OpenapiHandler::new(&self.1)); + self.0.get(path).to_new_handler(OpenapiHandler::new(self.1.openapi.clone())); } } - impl<'a, C, P> DrawResources for (&mut $implType<'a, C, P>, &mut OpenapiRouter) + impl<'a, C, P> DrawResources for (&mut $implType<'a, C, P>, &mut OpenapiBuilder) where C : PipelineHandleChain

+ Copy + Send + Sync + 'static, P : RefUnwindSafe + Send + Sync + 'static @@ -375,7 +396,7 @@ macro_rules! implOpenapiRouter { } } - impl<'a, C, P> DrawResourceRoutes for (&mut (&mut $implType<'a, C, P>, &mut OpenapiRouter), &str) + impl<'a, C, P> DrawResourceRoutes for (&mut (&mut $implType<'a, C, P>, &mut OpenapiBuilder), &str) where C : PipelineHandleChain

+ Copy + Send + Sync + 'static, P : RefUnwindSafe + Send + Sync + 'static diff --git a/gotham_restful/src/routing.rs b/gotham_restful/src/routing.rs index 7ff6ce1..70eafc7 100644 --- a/gotham_restful/src/routing.rs +++ b/gotham_restful/src/routing.rs @@ -6,7 +6,7 @@ use crate::{ StatusCode }; #[cfg(feature = "openapi")] -use crate::OpenapiRouter; +use crate::openapi::router::OpenapiBuilder; use futures_util::{future, future::FutureExt}; use gotham::{ @@ -49,7 +49,7 @@ pub trait WithOpenapi { fn with_openapi(&mut self, title : String, version : String, server_url : String, block : F) where - F : FnOnce((&mut D, &mut OpenapiRouter)); + F : FnOnce((&mut D, &mut OpenapiBuilder)); } /// This trait adds the `resource` method to gotham's routing. It allows you to register @@ -316,9 +316,9 @@ macro_rules! implDrawResourceRoutes { { fn with_openapi(&mut self, title : String, version : String, server_url : String, block : F) where - F : FnOnce((&mut Self, &mut OpenapiRouter)) + F : FnOnce((&mut Self, &mut OpenapiBuilder)) { - let mut router = OpenapiRouter::new(title, version, server_url); + let mut router = OpenapiBuilder::new(title, version, server_url); block((self, &mut router)); } } From 01f818e268a2a533a84efcce66f977ef41274036 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 26 Apr 2020 22:34:22 +0200 Subject: [PATCH 026/170] split the openapi code into several files --- gotham_restful/Cargo.toml | 4 +- gotham_restful/src/lib.rs | 1 + gotham_restful/src/openapi/builder.rs | 96 ++++++++++++ gotham_restful/src/openapi/handler.rs | 110 +++++++++++++ gotham_restful/src/openapi/mod.rs | 4 + gotham_restful/src/openapi/router.rs | 215 +------------------------- gotham_restful/src/routing.rs | 2 +- 7 files changed, 221 insertions(+), 211 deletions(-) create mode 100644 gotham_restful/src/openapi/builder.rs create mode 100644 gotham_restful/src/openapi/handler.rs diff --git a/gotham_restful/Cargo.toml b/gotham_restful/Cargo.toml index 512cbb1..78359fe 100644 --- a/gotham_restful/Cargo.toml +++ b/gotham_restful/Cargo.toml @@ -27,7 +27,7 @@ gotham_restful_derive = { version = "0.0.4-dev" } indexmap = { version = "1.3.2", optional = true } itertools = "0.9.0" jsonwebtoken = { version = "7.1.0", optional = true } -log = { version = "0.4.8", optional = true } +log = "0.4.8" mime = "0.3.16" openapiv3 = { version = "0.3", optional = true } serde = { version = "1.0.106", features = ["derive"] } @@ -44,7 +44,7 @@ default = [] auth = ["gotham_restful_derive/auth", "base64", "cookie", "jsonwebtoken"] errorlog = [] database = ["gotham_restful_derive/database", "gotham_middleware_diesel"] -openapi = ["gotham_restful_derive/openapi", "indexmap", "log", "openapiv3"] +openapi = ["gotham_restful_derive/openapi", "indexmap", "openapiv3"] [package.metadata.docs.rs] all-features = true diff --git a/gotham_restful/src/lib.rs b/gotham_restful/src/lib.rs index 6c1a5fd..c62ce0b 100644 --- a/gotham_restful/src/lib.rs +++ b/gotham_restful/src/lib.rs @@ -110,6 +110,7 @@ Licensed under your option of: extern crate self as gotham_restful; #[macro_use] extern crate gotham_derive; +#[macro_use] extern crate log; #[macro_use] extern crate serde; #[doc(no_inline)] diff --git a/gotham_restful/src/openapi/builder.rs b/gotham_restful/src/openapi/builder.rs new file mode 100644 index 0000000..88c0ba4 --- /dev/null +++ b/gotham_restful/src/openapi/builder.rs @@ -0,0 +1,96 @@ +use crate::{OpenapiType, OpenapiSchema}; +use indexmap::IndexMap; +use openapiv3::{ + Components, OpenAPI, PathItem, ReferenceOr, ReferenceOr::Item, ReferenceOr::Reference, Schema, + Server +}; +use std::sync::{Arc, RwLock}; + +pub struct OpenapiBuilder +{ + pub openapi : Arc> +} + +impl OpenapiBuilder +{ + pub fn new(title : String, version : String, url : String) -> Self + { + Self { + openapi: Arc::new(RwLock::new(OpenAPI { + openapi: "3.0.2".to_string(), + info: openapiv3::Info { + title, version, + ..Default::default() + }, + servers: vec![Server { + url, + ..Default::default() + }], + ..Default::default() + })) + } + } + + /// 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 + pub fn remove_path(&mut self, path : &str) -> PathItem + { + let mut openapi = self.openapi.write().unwrap(); + match openapi.paths.swap_remove(path) { + Some(Item(item)) => item, + _ => PathItem::default() + } + } + + pub fn add_path(&mut self, path : Path, item : PathItem) + { + let mut openapi = self.openapi.write().unwrap(); + openapi.paths.insert(path.to_string(), Item(item)); + } + + fn add_schema_impl(&mut self, name : String, mut schema : OpenapiSchema) + { + self.add_schema_dependencies(&mut schema.dependencies); + + let mut openapi = self.openapi.write().unwrap(); + match &mut openapi.components { + Some(comp) => { + comp.schemas.insert(name, Item(schema.into_schema())); + }, + None => { + let mut comp = Components::default(); + comp.schemas.insert(name, Item(schema.into_schema())); + openapi.components = Some(comp); + } + }; + } + + fn add_schema_dependencies(&mut self, dependencies : &mut IndexMap) + { + let keys : Vec = dependencies.keys().map(|k| k.to_string()).collect(); + for dep in keys + { + let dep_schema = dependencies.swap_remove(&dep); + if let Some(dep_schema) = dep_schema + { + self.add_schema_impl(dep, dep_schema); + } + } + } + + pub fn add_schema(&mut self) -> ReferenceOr + { + let mut schema = T::schema(); + match schema.name.clone() { + Some(name) => { + let reference = Reference { reference: format!("#/components/schemas/{}", name) }; + self.add_schema_impl(name, schema); + reference + }, + None => { + self.add_schema_dependencies(&mut schema.dependencies); + Item(schema.into_schema()) + } + } + } +} diff --git a/gotham_restful/src/openapi/handler.rs b/gotham_restful/src/openapi/handler.rs new file mode 100644 index 0000000..2054b0d --- /dev/null +++ b/gotham_restful/src/openapi/handler.rs @@ -0,0 +1,110 @@ +use super::SECURITY_NAME; +use futures_util::{future, future::FutureExt}; +use gotham::{ + error::Result, + handler::{Handler, HandlerFuture, NewHandler}, + helpers::http::response::create_response, + state::State +}; +use indexmap::IndexMap; +use mime::{APPLICATION_JSON, TEXT_PLAIN}; +use openapiv3::{APIKeyLocation, OpenAPI, ReferenceOr, SecurityScheme}; +use std::{ + pin::Pin, + sync::{Arc, RwLock} +}; + +#[derive(Clone)] +pub struct OpenapiHandler +{ + openapi : Arc> +} + +impl OpenapiHandler +{ + pub fn new(openapi : Arc>) -> Self + { + Self { openapi } + } +} + +impl NewHandler for OpenapiHandler +{ + type Instance = Self; + + fn new_handler(&self) -> Result + { + Ok(self.clone()) + } +} + +#[cfg(feature = "auth")] +fn get_security(state : &mut State) -> IndexMap> +{ + use crate::AuthSource; + use gotham::state::FromState; + + let source = match AuthSource::try_borrow_from(state) { + Some(source) => source, + None => return Default::default() + }; + + let security_scheme = match source { + AuthSource::Cookie(name) => SecurityScheme::APIKey { + location: APIKeyLocation::Cookie, + name: name.to_string() + }, + AuthSource::Header(name) => SecurityScheme::APIKey { + location: APIKeyLocation::Header, + name: name.to_string() + }, + AuthSource::AuthorizationHeader => SecurityScheme::HTTP { + scheme: "bearer".to_owned(), + bearer_format: Some("JWT".to_owned()) + } + }; + + let mut security_schemes : IndexMap> = Default::default(); + security_schemes.insert(SECURITY_NAME.to_owned(), ReferenceOr::Item(security_scheme)); + + security_schemes +} + +#[cfg(not(feature = "auth"))] +fn get_security(state : &mut State) -> (Vec, IndexMap>) +{ + Default::default() +} + +impl Handler for OpenapiHandler +{ + fn handle(self, mut state : State) -> Pin> + { + let openapi = match self.openapi.read() { + Ok(openapi) => openapi, + Err(e) => { + error!("Unable to acquire read lock for the OpenAPI specification: {}", e); + let res = create_response(&state, crate::StatusCode::INTERNAL_SERVER_ERROR, TEXT_PLAIN, ""); + return future::ok((state, res)).boxed() + } + }; + + let mut openapi = openapi.clone(); + let security_schemes = get_security(&mut state); + let mut components = openapi.components.unwrap_or_default(); + components.security_schemes = security_schemes; + openapi.components = Some(components); + + match serde_json::to_string(&openapi) { + Ok(body) => { + let res = create_response(&state, crate::StatusCode::OK, APPLICATION_JSON, body); + future::ok((state, res)).boxed() + }, + Err(e) => { + error!("Unable to handle OpenAPI request due to error: {}", e); + let res = create_response(&state, crate::StatusCode::INTERNAL_SERVER_ERROR, TEXT_PLAIN, ""); + future::ok((state, res)).boxed() + } + } + } +} diff --git a/gotham_restful/src/openapi/mod.rs b/gotham_restful/src/openapi/mod.rs index 5c19494..aff6b1e 100644 --- a/gotham_restful/src/openapi/mod.rs +++ b/gotham_restful/src/openapi/mod.rs @@ -1,3 +1,7 @@ +const SECURITY_NAME : &str = "authToken"; + +pub mod builder; +pub mod handler; pub mod router; pub mod types; diff --git a/gotham_restful/src/openapi/router.rs b/gotham_restful/src/openapi/router.rs index 3cf3b19..0298656 100644 --- a/gotham_restful/src/openapi/router.rs +++ b/gotham_restful/src/openapi/router.rs @@ -6,220 +6,19 @@ use crate::{ OpenapiType, RequestBody }; -use futures_util::{future, future::FutureExt}; +use super::{builder::OpenapiBuilder, handler::OpenapiHandler, SECURITY_NAME}; use gotham::{ - handler::{Handler, HandlerFuture, NewHandler}, - helpers::http::response::create_response, pipeline::chain::PipelineHandleChain, - router::builder::*, - state::State + router::builder::* }; use indexmap::IndexMap; -use log::error; -use mime::{Mime, APPLICATION_JSON, STAR_STAR, TEXT_PLAIN}; +use mime::{Mime, STAR_STAR}; use openapiv3::{ - APIKeyLocation, Components, MediaType, OpenAPI, Operation, Parameter, ParameterData, ParameterSchemaOrContent, PathItem, - ReferenceOr, ReferenceOr::Item, ReferenceOr::Reference, RequestBody as OARequestBody, Response, Responses, Schema, - SchemaKind, SecurityScheme, Server, StatusCode, Type + MediaType, Operation, Parameter, ParameterData, ParameterSchemaOrContent, ReferenceOr, + ReferenceOr::Item, RequestBody as OARequestBody, Response, Responses, Schema, SchemaKind, + StatusCode, Type }; -use std::{ - panic::RefUnwindSafe, - pin::Pin, - sync::{Arc, RwLock} -}; - -/** -This type is required to build routes while adding them to the generated OpenAPI Spec at the -same time. There is no need to use this type directly. See [`WithOpenapi`] on how to do this. - -[`WithOpenapi`]: trait.WithOpenapi.html -*/ -pub struct OpenapiBuilder -{ - openapi : Arc> -} - -impl OpenapiBuilder -{ - pub fn new(title : String, version : String, url : String) -> Self - { - Self { - openapi: Arc::new(RwLock::new(OpenAPI { - openapi: "3.0.2".to_string(), - info: openapiv3::Info { - title, version, - ..Default::default() - }, - servers: vec![Server { - url, - ..Default::default() - }], - ..Default::default() - })) - } - } - - /// 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 - fn remove_path(&mut self, path : &str) -> PathItem - { - let mut openapi = self.openapi.write().unwrap(); - match openapi.paths.swap_remove(path) { - Some(Item(item)) => item, - _ => PathItem::default() - } - } - - fn add_path(&mut self, path : Path, item : PathItem) - { - let mut openapi = self.openapi.write().unwrap(); - openapi.paths.insert(path.to_string(), Item(item)); - } - - fn add_schema_impl(&mut self, name : String, mut schema : OpenapiSchema) - { - self.add_schema_dependencies(&mut schema.dependencies); - - let mut openapi = self.openapi.write().unwrap(); - match &mut openapi.components { - Some(comp) => { - comp.schemas.insert(name, Item(schema.into_schema())); - }, - None => { - let mut comp = Components::default(); - comp.schemas.insert(name, Item(schema.into_schema())); - openapi.components = Some(comp); - } - }; - } - - fn add_schema_dependencies(&mut self, dependencies : &mut IndexMap) - { - let keys : Vec = dependencies.keys().map(|k| k.to_string()).collect(); - for dep in keys - { - let dep_schema = dependencies.swap_remove(&dep); - if let Some(dep_schema) = dep_schema - { - self.add_schema_impl(dep, dep_schema); - } - } - } - - fn add_schema(&mut self) -> ReferenceOr - { - let mut schema = T::schema(); - match schema.name.clone() { - Some(name) => { - let reference = Reference { reference: format!("#/components/schemas/{}", name) }; - self.add_schema_impl(name, schema); - reference - }, - None => { - self.add_schema_dependencies(&mut schema.dependencies); - Item(schema.into_schema()) - } - } - } -} - -#[derive(Clone)] -struct OpenapiHandler -{ - openapi : Arc> -} - -impl OpenapiHandler -{ - fn new(openapi : Arc>) -> Self - { - Self { openapi } - } -} - -impl NewHandler for OpenapiHandler -{ - type Instance = Self; - - fn new_handler(&self) -> gotham::error::Result - { - Ok(self.clone()) - } -} - -#[cfg(feature = "auth")] -const SECURITY_NAME : &str = "authToken"; - -#[cfg(feature = "auth")] -fn get_security(state : &mut State) -> IndexMap> -{ - use crate::AuthSource; - use gotham::state::FromState; - - let source = match AuthSource::try_borrow_from(state) { - Some(source) => source, - None => return Default::default() - }; - - let security_scheme = match source { - AuthSource::Cookie(name) => SecurityScheme::APIKey { - location: APIKeyLocation::Cookie, - name: name.to_string() - }, - AuthSource::Header(name) => SecurityScheme::APIKey { - location: APIKeyLocation::Header, - name: name.to_string() - }, - AuthSource::AuthorizationHeader => SecurityScheme::HTTP { - scheme: "bearer".to_owned(), - bearer_format: Some("JWT".to_owned()) - } - }; - - let mut security_schemes : IndexMap> = Default::default(); - security_schemes.insert(SECURITY_NAME.to_owned(), ReferenceOr::Item(security_scheme)); - - security_schemes -} - -#[cfg(not(feature = "auth"))] -fn get_security(state : &mut State) -> (Vec, IndexMap>) -{ - Default::default() -} - -impl Handler for OpenapiHandler -{ - fn handle(self, mut state : State) -> Pin> - { - let openapi = match self.openapi.read() { - Ok(openapi) => openapi, - Err(e) => { - error!("Unable to acquire read lock for the OpenAPI specification: {}", e); - let res = create_response(&state, crate::StatusCode::INTERNAL_SERVER_ERROR, TEXT_PLAIN, ""); - return future::ok((state, res)).boxed() - } - }; - - let mut openapi = openapi.clone(); - let security_schemes = get_security(&mut state); - let mut components = openapi.components.unwrap_or_default(); - components.security_schemes = security_schemes; - openapi.components = Some(components); - - match serde_json::to_string(&openapi) { - Ok(body) => { - let res = create_response(&state, crate::StatusCode::OK, APPLICATION_JSON, body); - future::ok((state, res)).boxed() - }, - Err(e) => { - error!("Unable to handle OpenAPI request due to error: {}", e); - let res = create_response(&state, crate::StatusCode::INTERNAL_SERVER_ERROR, TEXT_PLAIN, ""); - future::ok((state, res)).boxed() - } - } - } -} +use std::panic::RefUnwindSafe; /// This trait adds the `get_openapi` method to an OpenAPI-aware router. pub trait GetOpenapi diff --git a/gotham_restful/src/routing.rs b/gotham_restful/src/routing.rs index 70eafc7..b87089b 100644 --- a/gotham_restful/src/routing.rs +++ b/gotham_restful/src/routing.rs @@ -6,7 +6,7 @@ use crate::{ StatusCode }; #[cfg(feature = "openapi")] -use crate::openapi::router::OpenapiBuilder; +use crate::openapi::builder::OpenapiBuilder; use futures_util::{future, future::FutureExt}; use gotham::{ From 9fd0bceaf4a4c6dbb3b7df2d6da4a6d77f3d1bdf Mon Sep 17 00:00:00 2001 From: Dominic Date: Mon, 27 Apr 2020 02:12:51 +0200 Subject: [PATCH 027/170] move openapi operation extraction code into its own mod --- gotham_restful/src/openapi/mod.rs | 1 + gotham_restful/src/openapi/operation.rs | 217 +++++++++++++++++++++ gotham_restful/src/openapi/router.rs | 244 +----------------------- 3 files changed, 227 insertions(+), 235 deletions(-) create mode 100644 gotham_restful/src/openapi/operation.rs diff --git a/gotham_restful/src/openapi/mod.rs b/gotham_restful/src/openapi/mod.rs index aff6b1e..141ea22 100644 --- a/gotham_restful/src/openapi/mod.rs +++ b/gotham_restful/src/openapi/mod.rs @@ -3,5 +3,6 @@ const SECURITY_NAME : &str = "authToken"; pub mod builder; pub mod handler; +pub mod operation; pub mod router; pub mod types; diff --git a/gotham_restful/src/openapi/operation.rs b/gotham_restful/src/openapi/operation.rs new file mode 100644 index 0000000..77d0698 --- /dev/null +++ b/gotham_restful/src/openapi/operation.rs @@ -0,0 +1,217 @@ +use crate::{ + resource::*, + result::*, + OpenapiSchema, + OpenapiType, + RequestBody +}; +use super::SECURITY_NAME; +use indexmap::IndexMap; +use mime::{Mime, STAR_STAR}; +use openapiv3::{ + MediaType, Operation, Parameter, ParameterData, ParameterSchemaOrContent, ReferenceOr, + ReferenceOr::Item, RequestBody as OARequestBody, Response, Responses, Schema, SchemaKind, + StatusCode, Type +}; + + +#[derive(Default)] +struct OperationParams<'a> +{ + path_params : Vec<&'a str>, + query_params : Option +} + +impl<'a> OperationParams<'a> +{ + fn add_path_params(&self, params : &mut Vec>) + { + for param in &self.path_params + { + params.push(Item(Parameter::Path { + parameter_data: ParameterData { + name: (*param).to_string(), + description: None, + required: true, + deprecated: None, + format: ParameterSchemaOrContent::Schema(Item(String::schema().into_schema())), + example: None, + examples: IndexMap::new() + }, + style: Default::default(), + })); + } + } + + fn add_query_params(self, params : &mut Vec>) + { + let query_params = match self.query_params { + Some(qp) => qp.schema, + None => return + }; + let query_params = match query_params { + SchemaKind::Type(Type::Object(ty)) => ty, + _ => panic!("Query Parameters needs to be a plain struct") + }; + for (name, schema) in query_params.properties + { + let required = query_params.required.contains(&name); + params.push(Item(Parameter::Query { + parameter_data: ParameterData { + name, + description: None, + required, + deprecated: None, + format: ParameterSchemaOrContent::Schema(schema.unbox()), + example: None, + examples: IndexMap::new() + }, + allow_reserved: false, + style: Default::default(), + allow_empty_value: None + })) + } + } + + fn into_params(self) -> Vec> + { + let mut params : Vec> = Vec::new(); + self.add_path_params(&mut params); + self.add_query_params(&mut params); + params + } +} + +pub struct OperationDescription<'a> +{ + operation_id : Option, + default_status : crate::StatusCode, + accepted_types : Option>, + schema : ReferenceOr, + params : OperationParams<'a>, + body_schema : Option>, + supported_types : Option>, + requires_auth : bool +} + +impl<'a> OperationDescription<'a> +{ + pub fn new(schema : ReferenceOr) -> Self + { + Self { + operation_id: Handler::operation_id(), + default_status: Handler::Res::default_status(), + accepted_types: Handler::Res::accepted_types(), + schema, + params: Default::default(), + body_schema: None, + supported_types: None, + requires_auth: Handler::wants_auth() + } + } + + pub fn with_path_params(mut self, params : Vec<&'a str>) -> Self + { + self.params.path_params = params; + self + } + + pub fn with_query_params(mut self, params : OpenapiSchema) -> Self + { + self.params.query_params = Some(params); + self + } + + pub fn with_body(mut self, schema : ReferenceOr) -> Self + { + self.body_schema = Some(schema); + self.supported_types = Body::supported_types(); + self + } + + + fn schema_to_content(types : Vec, schema : ReferenceOr) -> IndexMap + { + let mut content : IndexMap = IndexMap::new(); + for ty in types + { + content.insert(ty.to_string(), MediaType { + schema: Some(schema.clone()), + ..Default::default() + }); + } + content + } + + pub fn into_operation(self) -> Operation + { + // 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) = ( + 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.unwrap_or_else(|| vec![STAR_STAR]), schema); + + let mut responses : IndexMap> = IndexMap::new(); + responses.insert(StatusCode::Code(default_status.as_u16()), Item(Response { + 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 { + description: None, + content: Self::schema_to_content(supported_types.unwrap_or_else(|| vec![STAR_STAR]), schema), + required: true + })); + + let mut security = Vec::new(); + if requires_auth + { + let mut sec = IndexMap::new(); + sec.insert(SECURITY_NAME.to_owned(), Vec::new()); + security.push(sec); + } + + Operation { + tags: Vec::new(), + operation_id, + parameters: params.into_params(), + request_body, + responses: Responses { + default: None, + responses + }, + deprecated: false, + security, + ..Default::default() + } + } +} + + +#[cfg(test)] +mod test +{ + use crate::ResourceResult; + use super::*; + + #[test] + fn no_content_schema_to_content() + { + let types = NoContent::accepted_types(); + let schema = ::schema(); + let content = OperationDescription::schema_to_content(types.unwrap_or_else(|| vec![STAR_STAR]), Item(schema.into_schema())); + assert!(content.is_empty()); + } + + #[test] + fn raw_schema_to_content() + { + let types = Raw::<&str>::accepted_types(); + let schema = as OpenapiType>::schema(); + let content = OperationDescription::schema_to_content(types.unwrap_or_else(|| vec![STAR_STAR]), Item(schema.into_schema())); + assert_eq!(content.len(), 1); + let json = serde_json::to_string(&content.values().nth(0).unwrap()).unwrap(); + assert_eq!(json, r#"{"schema":{"type":"string","format":"binary"}}"#); + } +} diff --git a/gotham_restful/src/openapi/router.rs b/gotham_restful/src/openapi/router.rs index 0298656..36783d3 100644 --- a/gotham_restful/src/openapi/router.rs +++ b/gotham_restful/src/openapi/router.rs @@ -1,23 +1,13 @@ use crate::{ resource::*, - result::*, routing::*, - OpenapiSchema, OpenapiType, - RequestBody }; -use super::{builder::OpenapiBuilder, handler::OpenapiHandler, SECURITY_NAME}; +use super::{builder::OpenapiBuilder, handler::OpenapiHandler, operation::OperationDescription}; use gotham::{ pipeline::chain::PipelineHandleChain, router::builder::* }; -use indexmap::IndexMap; -use mime::{Mime, STAR_STAR}; -use openapiv3::{ - MediaType, Operation, Parameter, ParameterData, ParameterSchemaOrContent, ReferenceOr, - ReferenceOr::Item, RequestBody as OARequestBody, Response, Responses, Schema, SchemaKind, - StatusCode, Type -}; use std::panic::RefUnwindSafe; /// This trait adds the `get_openapi` method to an OpenAPI-aware router. @@ -26,150 +16,6 @@ pub trait GetOpenapi fn get_openapi(&mut self, path : &str); } -fn schema_to_content(types : Vec, schema : ReferenceOr) -> IndexMap -{ - let mut content : IndexMap = IndexMap::new(); - for ty in types - { - content.insert(ty.to_string(), MediaType { - schema: Some(schema.clone()), - ..Default::default() - }); - } - content -} - -#[derive(Default)] -struct OperationParams<'a> -{ - path_params : Vec<&'a str>, - query_params : Option -} - -impl<'a> OperationParams<'a> -{ - fn new(path_params : Vec<&'a str>, query_params : Option) -> Self - { - Self { path_params, query_params } - } - - fn from_path_params(path_params : Vec<&'a str>) -> Self - { - Self::new(path_params, None) - } - - fn from_query_params(query_params : OpenapiSchema) -> Self - { - Self::new(Vec::new(), Some(query_params)) - } - - fn add_path_params(&self, params : &mut Vec>) - { - for param in &self.path_params - { - params.push(Item(Parameter::Path { - parameter_data: ParameterData { - name: (*param).to_string(), - description: None, - required: true, - deprecated: None, - format: ParameterSchemaOrContent::Schema(Item(String::schema().into_schema())), - example: None, - examples: IndexMap::new() - }, - style: Default::default(), - })); - } - } - - fn add_query_params(self, params : &mut Vec>) - { - let query_params = match self.query_params { - Some(qp) => qp.schema, - None => return - }; - let query_params = match query_params { - SchemaKind::Type(Type::Object(ty)) => ty, - _ => panic!("Query Parameters needs to be a plain struct") - }; - for (name, schema) in query_params.properties - { - let required = query_params.required.contains(&name); - params.push(Item(Parameter::Query { - parameter_data: ParameterData { - name, - description: None, - required, - deprecated: None, - format: ParameterSchemaOrContent::Schema(schema.unbox()), - example: None, - examples: IndexMap::new() - }, - allow_reserved: false, - style: Default::default(), - allow_empty_value: None - })) - } - } - - fn into_params(self) -> Vec> - { - let mut params : Vec> = Vec::new(); - self.add_path_params(&mut params); - self.add_query_params(&mut params); - params - } -} - -fn new_operation( - operation_id : Option, - default_status : crate::StatusCode, - accepted_types : Option>, - schema : ReferenceOr, - params : OperationParams, - body_schema : Option>, - supported_types : Option>, - requires_auth : bool -) -> Operation -{ - let content = schema_to_content(accepted_types.unwrap_or_else(|| vec![STAR_STAR]), schema); - - let mut responses : IndexMap> = IndexMap::new(); - responses.insert(StatusCode::Code(default_status.as_u16()), Item(Response { - 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 { - description: None, - content: schema_to_content(supported_types.unwrap_or_else(|| vec![STAR_STAR]), schema), - required: true - })); - - let mut security = Vec::new(); - if requires_auth - { - let mut sec = IndexMap::new(); - sec.insert(SECURITY_NAME.to_owned(), Vec::new()); - security.push(sec); - } - - Operation { - tags: Vec::new(), - operation_id, - parameters: params.into_params(), - request_body, - responses: Responses { - default: None, - responses - }, - deprecated: false, - security, - ..Default::default() - } -} - macro_rules! implOpenapiRouter { ($implType:ident) => { @@ -206,7 +52,7 @@ macro_rules! implOpenapiRouter { let path = format!("/{}", &self.1); let mut item = (self.0).1.remove_path(&path); - item.get = Some(new_operation(Handler::operation_id(), Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::default(), None, None, Handler::wants_auth())); + item.get = Some(OperationDescription::new::(schema).into_operation()); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1).read_all::() @@ -218,7 +64,7 @@ macro_rules! implOpenapiRouter { let path = format!("/{}/{{id}}", &self.1); let mut item = (self.0).1.remove_path(&path); - item.get = Some(new_operation(Handler::operation_id(), Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::from_path_params(vec!["id"]), None, None, Handler::wants_auth())); + item.get = Some(OperationDescription::new::(schema).with_path_params(vec!["id"]).into_operation()); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1).read::() @@ -230,7 +76,7 @@ macro_rules! implOpenapiRouter { let path = format!("/{}/search", &self.1); let mut item = (self.0).1.remove_path(&self.1); - item.get = Some(new_operation(Handler::operation_id(), Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::from_query_params(Handler::Query::schema()), None, None, Handler::wants_auth())); + item.get = Some(OperationDescription::new::(schema).with_query_params(Handler::Query::schema()).into_operation()); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1).search::() @@ -246,7 +92,7 @@ macro_rules! implOpenapiRouter { let path = format!("/{}", &self.1); let mut item = (self.0).1.remove_path(&path); - item.post = Some(new_operation(Handler::operation_id(), Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::default(), Some(body_schema), Handler::Body::supported_types(), Handler::wants_auth())); + item.post = Some(OperationDescription::new::(schema).with_body::(body_schema).into_operation()); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1).create::() @@ -262,7 +108,7 @@ macro_rules! implOpenapiRouter { let path = format!("/{}", &self.1); let mut item = (self.0).1.remove_path(&path); - item.put = Some(new_operation(Handler::operation_id(), Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::default(), Some(body_schema), Handler::Body::supported_types(), Handler::wants_auth())); + item.put = Some(OperationDescription::new::(schema).with_body::(body_schema).into_operation()); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1).update_all::() @@ -278,7 +124,7 @@ macro_rules! implOpenapiRouter { let path = format!("/{}/{{id}}", &self.1); let mut item = (self.0).1.remove_path(&path); - item.put = Some(new_operation(Handler::operation_id(), Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::from_path_params(vec!["id"]), Some(body_schema), Handler::Body::supported_types(), Handler::wants_auth())); + item.put = Some(OperationDescription::new::(schema).with_path_params(vec!["id"]).with_body::(body_schema).into_operation()); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1).update::() @@ -290,7 +136,7 @@ macro_rules! implOpenapiRouter { let path = format!("/{}", &self.1); let mut item = (self.0).1.remove_path(&path); - item.delete = Some(new_operation(Handler::operation_id(), Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::default(), None, None, Handler::wants_auth())); + item.delete = Some(OperationDescription::new::(schema).into_operation()); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1).delete_all::() @@ -302,7 +148,7 @@ macro_rules! implOpenapiRouter { let path = format!("/{}/{{id}}", &self.1); let mut item = (self.0).1.remove_path(&path); - item.delete = Some(new_operation(Handler::operation_id(), Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::from_path_params(vec!["id"]), None, None, Handler::wants_auth())); + item.delete = Some(OperationDescription::new::(schema).with_path_params(vec!["id"]).into_operation()); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1).delete::() @@ -314,75 +160,3 @@ macro_rules! implOpenapiRouter { implOpenapiRouter!(RouterBuilder); implOpenapiRouter!(ScopeBuilder); - - -#[cfg(test)] -mod test -{ - use crate::ResourceResult; - use super::*; - - #[derive(OpenapiType)] - #[allow(dead_code)] - struct QueryParams - { - id : isize - } - - #[test] - fn params_empty() - { - let op_params = OperationParams::default(); - let params = op_params.into_params(); - assert!(params.is_empty()); - } - - #[test] - fn params_from_path_params() - { - let name = "id"; - let op_params = OperationParams::from_path_params(vec![name]); - let params = op_params.into_params(); - let json = serde_json::to_string(¶ms).unwrap(); - assert_eq!(json, format!(r#"[{{"in":"path","name":"{}","required":true,"schema":{{"type":"string"}},"style":"simple"}}]"#, name)); - } - - #[test] - fn params_from_query_params() - { - let op_params = OperationParams::from_query_params(QueryParams::schema()); - let params = op_params.into_params(); - let json = serde_json::to_string(¶ms).unwrap(); - assert_eq!(json, r#"[{"in":"query","name":"id","required":true,"schema":{"type":"integer"},"style":"form"}]"#); - } - - #[test] - fn params_both() - { - let name = "id"; - let op_params = OperationParams::new(vec![name], Some(QueryParams::schema())); - let params = op_params.into_params(); - let json = serde_json::to_string(¶ms).unwrap(); - assert_eq!(json, format!(r#"[{{"in":"path","name":"{}","required":true,"schema":{{"type":"string"}},"style":"simple"}},{{"in":"query","name":"id","required":true,"schema":{{"type":"integer"}},"style":"form"}}]"#, name)); - } - - #[test] - fn no_content_schema_to_content() - { - let types = NoContent::accepted_types(); - let schema = ::schema(); - let content = schema_to_content(types.unwrap_or_else(|| vec![STAR_STAR]), Item(schema.into_schema())); - assert!(content.is_empty()); - } - - #[test] - fn raw_schema_to_content() - { - let types = Raw::<&str>::accepted_types(); - let schema = as OpenapiType>::schema(); - let content = schema_to_content(types.unwrap_or_else(|| vec![STAR_STAR]), Item(schema.into_schema())); - assert_eq!(content.len(), 1); - let json = serde_json::to_string(&content.values().nth(0).unwrap()).unwrap(); - assert_eq!(json, r#"{"schema":{"type":"string","format":"binary"}}"#); - } -} From 45eac2172648bf5d33c11e4444f18ee72730ac2d Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 29 Apr 2020 19:10:11 +0200 Subject: [PATCH 028/170] Proper OpenAPI type for path parameters (Fixes #18) --- gotham_restful/src/openapi/operation.rs | 13 ++++++------- gotham_restful/src/openapi/router.rs | 9 ++++++--- gotham_restful/src/resource.rs | 10 ++++------ gotham_restful/src/types.rs | 12 ++++++++++++ 4 files changed, 28 insertions(+), 16 deletions(-) diff --git a/gotham_restful/src/openapi/operation.rs b/gotham_restful/src/openapi/operation.rs index 77d0698..e949301 100644 --- a/gotham_restful/src/openapi/operation.rs +++ b/gotham_restful/src/openapi/operation.rs @@ -2,7 +2,6 @@ use crate::{ resource::*, result::*, OpenapiSchema, - OpenapiType, RequestBody }; use super::SECURITY_NAME; @@ -18,7 +17,7 @@ use openapiv3::{ #[derive(Default)] struct OperationParams<'a> { - path_params : Vec<&'a str>, + path_params : Vec<(&'a str, ReferenceOr)>, query_params : Option } @@ -30,11 +29,11 @@ impl<'a> OperationParams<'a> { params.push(Item(Parameter::Path { parameter_data: ParameterData { - name: (*param).to_string(), + name: (*param).0.to_string(), description: None, required: true, deprecated: None, - format: ParameterSchemaOrContent::Schema(Item(String::schema().into_schema())), + format: ParameterSchemaOrContent::Schema((*param).1.clone()), example: None, examples: IndexMap::new() }, @@ -110,9 +109,9 @@ impl<'a> OperationDescription<'a> } } - pub fn with_path_params(mut self, params : Vec<&'a str>) -> Self + pub fn add_path_param(mut self, name : &'a str, schema : ReferenceOr) -> Self { - self.params.path_params = params; + self.params.path_params.push((name, schema)); self } @@ -192,7 +191,7 @@ impl<'a> OperationDescription<'a> #[cfg(test)] mod test { - use crate::ResourceResult; + use crate::{OpenapiType, ResourceResult}; use super::*; #[test] diff --git a/gotham_restful/src/openapi/router.rs b/gotham_restful/src/openapi/router.rs index 36783d3..24ead43 100644 --- a/gotham_restful/src/openapi/router.rs +++ b/gotham_restful/src/openapi/router.rs @@ -61,10 +61,11 @@ macro_rules! implOpenapiRouter { fn read(&mut self) { let schema = (self.0).1.add_schema::(); + let id_schema = (self.0).1.add_schema::(); let path = format!("/{}/{{id}}", &self.1); let mut item = (self.0).1.remove_path(&path); - item.get = Some(OperationDescription::new::(schema).with_path_params(vec!["id"]).into_operation()); + item.get = Some(OperationDescription::new::(schema).add_path_param("id", id_schema).into_operation()); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1).read::() @@ -120,11 +121,12 @@ macro_rules! implOpenapiRouter { Handler::Body : 'static { let schema = (self.0).1.add_schema::(); + let id_schema = (self.0).1.add_schema::(); let body_schema = (self.0).1.add_schema::(); let path = format!("/{}/{{id}}", &self.1); let mut item = (self.0).1.remove_path(&path); - item.put = Some(OperationDescription::new::(schema).with_path_params(vec!["id"]).with_body::(body_schema).into_operation()); + item.put = Some(OperationDescription::new::(schema).add_path_param("id", id_schema).with_body::(body_schema).into_operation()); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1).update::() @@ -145,10 +147,11 @@ macro_rules! implOpenapiRouter { fn delete(&mut self) { let schema = (self.0).1.add_schema::(); + let id_schema = (self.0).1.add_schema::(); let path = format!("/{}/{{id}}", &self.1); let mut item = (self.0).1.remove_path(&path); - item.delete = Some(OperationDescription::new::(schema).with_path_params(vec!["id"]).into_operation()); + item.delete = Some(OperationDescription::new::(schema).add_path_param("id", id_schema).into_operation()); (self.0).1.add_path(path, item); (&mut *(self.0).0, self.1).delete::() diff --git a/gotham_restful/src/resource.rs b/gotham_restful/src/resource.rs index cd903b7..360f2a1 100644 --- a/gotham_restful/src/resource.rs +++ b/gotham_restful/src/resource.rs @@ -1,13 +1,11 @@ -use crate::{DrawResourceRoutes, RequestBody, ResourceResult, ResourceType}; +use crate::{DrawResourceRoutes, RequestBody, ResourceID, ResourceResult, ResourceType}; use gotham::{ extractor::QueryStringExtractor, hyper::Body, state::State }; -use serde::de::DeserializeOwned; use std::{ future::Future, - panic::RefUnwindSafe, pin::Pin }; @@ -48,7 +46,7 @@ pub trait ResourceReadAll : ResourceMethod /// Handle a GET request on the Resource with an id. pub trait ResourceRead : ResourceMethod { - type ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static; + type ID : ResourceID + 'static; fn read(state : State, id : Self::ID) -> Pin + Send>>; } @@ -81,7 +79,7 @@ pub trait ResourceUpdateAll : ResourceMethod pub trait ResourceUpdate : ResourceMethod { type Body : RequestBody; - type ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static; + type ID : ResourceID + 'static; fn update(state : State, id : Self::ID, body : Self::Body) -> Pin + Send>>; } @@ -95,7 +93,7 @@ pub trait ResourceDeleteAll : ResourceMethod /// Handle a DELETE request on the Resource with an id. pub trait ResourceDelete : ResourceMethod { - type ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static; + type ID : ResourceID + 'static; fn delete(state : State, id : Self::ID) -> Pin + Send>>; } diff --git a/gotham_restful/src/types.rs b/gotham_restful/src/types.rs index d3a8e76..e0c769b 100644 --- a/gotham_restful/src/types.rs +++ b/gotham_restful/src/types.rs @@ -5,6 +5,7 @@ use crate::result::ResourceError; use gotham::hyper::body::Bytes; use mime::{Mime, APPLICATION_JSON}; use serde::{de::DeserializeOwned, Serialize}; +use std::panic::RefUnwindSafe; #[cfg(not(feature = "openapi"))] pub trait ResourceType @@ -78,3 +79,14 @@ impl RequestBody for T Some(vec![APPLICATION_JSON]) } } + +/// 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 +/// type `OpenapiType`. +pub trait ResourceID : ResourceType + DeserializeOwned + Clone + RefUnwindSafe + Send + Sync +{ +} + +impl ResourceID for T +{ +} From e013af8e185846217652c479cfe92b549244e955 Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 29 Apr 2020 19:22:32 +0200 Subject: [PATCH 029/170] remove some of the &mut &mut types (#6) --- gotham_restful/src/openapi/router.rs | 90 +++++++++++++++------------- gotham_restful/src/routing.rs | 12 ++-- 2 files changed, 56 insertions(+), 46 deletions(-) diff --git a/gotham_restful/src/openapi/router.rs b/gotham_restful/src/openapi/router.rs index 24ead43..33f4cba 100644 --- a/gotham_restful/src/openapi/router.rs +++ b/gotham_restful/src/openapi/router.rs @@ -16,21 +16,27 @@ pub trait GetOpenapi fn get_openapi(&mut self, path : &str); } +pub struct OpenapiRouter<'a, D> +{ + pub router : &'a mut D, + pub openapi_builder : &'a mut OpenapiBuilder +} + macro_rules! implOpenapiRouter { ($implType:ident) => { - impl<'a, C, P> GetOpenapi for (&mut $implType<'a, C, P>, &mut OpenapiBuilder) + impl<'a, 'b, C, P> GetOpenapi for OpenapiRouter<'a, $implType<'b, C, P>> where C : PipelineHandleChain

+ Copy + Send + Sync + 'static, P : RefUnwindSafe + Send + Sync + 'static { fn get_openapi(&mut self, path : &str) { - self.0.get(path).to_new_handler(OpenapiHandler::new(self.1.openapi.clone())); + self.router.get(path).to_new_handler(OpenapiHandler::new(self.openapi_builder.openapi.clone())); } } - impl<'a, C, P> DrawResources for (&mut $implType<'a, C, P>, &mut OpenapiBuilder) + impl<'a, 'b, C, P> DrawResources for OpenapiRouter<'a, $implType<'b, C, P>> where C : PipelineHandleChain

+ Copy + Send + Sync + 'static, P : RefUnwindSafe + Send + Sync + 'static @@ -41,46 +47,46 @@ macro_rules! implOpenapiRouter { } } - impl<'a, C, P> DrawResourceRoutes for (&mut (&mut $implType<'a, C, P>, &mut OpenapiBuilder), &str) + impl<'a, 'b, C, P> DrawResourceRoutes for (&mut OpenapiRouter<'a, $implType<'b, C, P>>, &str) where C : PipelineHandleChain

+ Copy + Send + Sync + 'static, P : RefUnwindSafe + Send + Sync + 'static { fn read_all(&mut self) { - let schema = (self.0).1.add_schema::(); + let schema = (self.0).openapi_builder.add_schema::(); let path = format!("/{}", &self.1); - let mut item = (self.0).1.remove_path(&path); + let mut item = (self.0).openapi_builder.remove_path(&path); item.get = Some(OperationDescription::new::(schema).into_operation()); - (self.0).1.add_path(path, item); + (self.0).openapi_builder.add_path(path, item); - (&mut *(self.0).0, self.1).read_all::() + (&mut *(self.0).router, self.1).read_all::() } fn read(&mut self) { - let schema = (self.0).1.add_schema::(); - let id_schema = (self.0).1.add_schema::(); + let schema = (self.0).openapi_builder.add_schema::(); + let id_schema = (self.0).openapi_builder.add_schema::(); let path = format!("/{}/{{id}}", &self.1); - let mut item = (self.0).1.remove_path(&path); + let mut item = (self.0).openapi_builder.remove_path(&path); item.get = Some(OperationDescription::new::(schema).add_path_param("id", id_schema).into_operation()); - (self.0).1.add_path(path, item); + (self.0).openapi_builder.add_path(path, item); - (&mut *(self.0).0, self.1).read::() + (&mut *(self.0).router, self.1).read::() } fn search(&mut self) { - let schema = (self.0).1.add_schema::(); + let schema = (self.0).openapi_builder.add_schema::(); let path = format!("/{}/search", &self.1); - let mut item = (self.0).1.remove_path(&self.1); + let mut item = (self.0).openapi_builder.remove_path(&self.1); item.get = Some(OperationDescription::new::(schema).with_query_params(Handler::Query::schema()).into_operation()); - (self.0).1.add_path(path, item); + (self.0).openapi_builder.add_path(path, item); - (&mut *(self.0).0, self.1).search::() + (&mut *(self.0).router, self.1).search::() } fn create(&mut self) @@ -88,15 +94,15 @@ macro_rules! implOpenapiRouter { Handler::Res : 'static, Handler::Body : 'static { - let schema = (self.0).1.add_schema::(); - let body_schema = (self.0).1.add_schema::(); + let schema = (self.0).openapi_builder.add_schema::(); + let body_schema = (self.0).openapi_builder.add_schema::(); let path = format!("/{}", &self.1); - let mut item = (self.0).1.remove_path(&path); + let mut item = (self.0).openapi_builder.remove_path(&path); item.post = Some(OperationDescription::new::(schema).with_body::(body_schema).into_operation()); - (self.0).1.add_path(path, item); + (self.0).openapi_builder.add_path(path, item); - (&mut *(self.0).0, self.1).create::() + (&mut *(self.0).router, self.1).create::() } fn update_all(&mut self) @@ -104,15 +110,15 @@ macro_rules! implOpenapiRouter { Handler::Res : 'static, Handler::Body : 'static { - let schema = (self.0).1.add_schema::(); - let body_schema = (self.0).1.add_schema::(); + let schema = (self.0).openapi_builder.add_schema::(); + let body_schema = (self.0).openapi_builder.add_schema::(); let path = format!("/{}", &self.1); - let mut item = (self.0).1.remove_path(&path); + let mut item = (self.0).openapi_builder.remove_path(&path); item.put = Some(OperationDescription::new::(schema).with_body::(body_schema).into_operation()); - (self.0).1.add_path(path, item); + (self.0).openapi_builder.add_path(path, item); - (&mut *(self.0).0, self.1).update_all::() + (&mut *(self.0).router, self.1).update_all::() } fn update(&mut self) @@ -120,41 +126,41 @@ macro_rules! implOpenapiRouter { Handler::Res : 'static, Handler::Body : 'static { - let schema = (self.0).1.add_schema::(); - let id_schema = (self.0).1.add_schema::(); - let body_schema = (self.0).1.add_schema::(); + let schema = (self.0).openapi_builder.add_schema::(); + let id_schema = (self.0).openapi_builder.add_schema::(); + let body_schema = (self.0).openapi_builder.add_schema::(); let path = format!("/{}/{{id}}", &self.1); - let mut item = (self.0).1.remove_path(&path); + let mut item = (self.0).openapi_builder.remove_path(&path); item.put = Some(OperationDescription::new::(schema).add_path_param("id", id_schema).with_body::(body_schema).into_operation()); - (self.0).1.add_path(path, item); + (self.0).openapi_builder.add_path(path, item); - (&mut *(self.0).0, self.1).update::() + (&mut *(self.0).router, self.1).update::() } fn delete_all(&mut self) { - let schema = (self.0).1.add_schema::(); + let schema = (self.0).openapi_builder.add_schema::(); let path = format!("/{}", &self.1); - let mut item = (self.0).1.remove_path(&path); + let mut item = (self.0).openapi_builder.remove_path(&path); item.delete = Some(OperationDescription::new::(schema).into_operation()); - (self.0).1.add_path(path, item); + (self.0).openapi_builder.add_path(path, item); - (&mut *(self.0).0, self.1).delete_all::() + (&mut *(self.0).router, self.1).delete_all::() } fn delete(&mut self) { - let schema = (self.0).1.add_schema::(); - let id_schema = (self.0).1.add_schema::(); + let schema = (self.0).openapi_builder.add_schema::(); + let id_schema = (self.0).openapi_builder.add_schema::(); let path = format!("/{}/{{id}}", &self.1); - let mut item = (self.0).1.remove_path(&path); + let mut item = (self.0).openapi_builder.remove_path(&path); item.delete = Some(OperationDescription::new::(schema).add_path_param("id", id_schema).into_operation()); - (self.0).1.add_path(path, item); + (self.0).openapi_builder.add_path(path, item); - (&mut *(self.0).0, self.1).delete::() + (&mut *(self.0).router, self.1).delete::() } } diff --git a/gotham_restful/src/routing.rs b/gotham_restful/src/routing.rs index b87089b..f213248 100644 --- a/gotham_restful/src/routing.rs +++ b/gotham_restful/src/routing.rs @@ -1,5 +1,6 @@ use crate::{ matcher::{AcceptHeaderMatcher, ContentTypeMatcher}, + openapi::router::OpenapiRouter, resource::*, result::{ResourceError, ResourceResult, Response}, RequestBody, @@ -49,7 +50,7 @@ pub trait WithOpenapi { fn with_openapi(&mut self, title : String, version : String, server_url : String, block : F) where - F : FnOnce((&mut D, &mut OpenapiBuilder)); + F : FnOnce(OpenapiRouter); } /// This trait adds the `resource` method to gotham's routing. It allows you to register @@ -316,10 +317,13 @@ macro_rules! implDrawResourceRoutes { { fn with_openapi(&mut self, title : String, version : String, server_url : String, block : F) where - F : FnOnce((&mut Self, &mut OpenapiBuilder)) + F : FnOnce(OpenapiRouter<$implType<'a, C, P>>) { - let mut router = OpenapiBuilder::new(title, version, server_url); - block((self, &mut router)); + let router = OpenapiRouter { + router: self, + openapi_builder: &mut OpenapiBuilder::new(title, version, server_url) + }; + block(router); } } From cd7cf073184ff05c829deeee28a0b291d81318c2 Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 29 Apr 2020 21:00:06 +0200 Subject: [PATCH 030/170] rust can't think for itself --- gotham_restful_derive/src/from_body.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gotham_restful_derive/src/from_body.rs b/gotham_restful_derive/src/from_body.rs index b3368ad..dd2069e 100644 --- a/gotham_restful_derive/src/from_body.rs +++ b/gotham_restful_derive/src/from_body.rs @@ -1,6 +1,7 @@ use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use quote::{format_ident, quote}; +use std::cmp::min; use syn::{ punctuated::Punctuated, token::Comma, @@ -93,7 +94,7 @@ fn expand(tokens : TokenStream) -> Result }; } - for field in &fields.fields[2..] + for field in &fields.fields[min(2, fields.fields.len())..] { let field_ident = &field.0; let field_ty = &field.1; From a36993f6151a6871db083e49b3d3a235e5ee3e39 Mon Sep 17 00:00:00 2001 From: Dominic Date: Thu, 30 Apr 2020 00:37:24 +0200 Subject: [PATCH 031/170] there is no need to force people to take &State arg this highly improves async compatibility --- gotham_restful_derive/src/method.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/gotham_restful_derive/src/method.rs b/gotham_restful_derive/src/method.rs index 514bb4a..95e9221 100644 --- a/gotham_restful_derive/src/method.rs +++ b/gotham_restful_derive/src/method.rs @@ -175,12 +175,20 @@ impl Spanned for MethodArgument } } -fn interpret_arg_ty(index : usize, attrs : &[Attribute], name : &str, ty : Type) -> Result +fn interpret_arg_ty(attrs : &[Attribute], name : &str, ty : Type) -> Result { let attr = attrs.iter() .find(|arg| arg.path.segments.iter().any(|path| &path.ident.to_string() == "rest_arg")) .map(|arg| arg.tokens.to_string()); + if attr.as_deref() == Some("state") || (attr.is_none() && name == "state") + { + return match ty { + Type::Reference(ty) => Ok(if ty.mutability.is_none() { MethodArgumentType::StateRef } else { MethodArgumentType::StateMutRef }), + _ => Err(Error::new(ty.span(), "The state parameter has to be a (mutable) reference to gotham_restful::State")) + }; + } + if cfg!(feature = "auth") && (attr.as_deref() == Some("auth") || (attr.is_none() && name == "auth")) { return Ok(match ty { @@ -197,14 +205,6 @@ fn interpret_arg_ty(index : usize, attrs : &[Attribute], name : &str, ty : Type) })); } - if index == 0 - { - return match ty { - Type::Reference(ty) => Ok(if ty.mutability.is_none() { MethodArgumentType::StateRef } else { MethodArgumentType::StateMutRef }), - _ => Err(Error::new(ty.span(), "The first argument, unless some feature is used, has to be a (mutable) reference to gotham::state::State")) - }; - } - Ok(MethodArgumentType::MethodArg(ty)) } @@ -213,7 +213,7 @@ fn interpret_arg(index : usize, arg : &PatType) -> Result let pat = &arg.pat; let ident = format_ident!("arg{}", index); let orig_name = quote!(#pat); - let ty = interpret_arg_ty(index, &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 }) } From 8593e133b73d2c911f216a21996d05033cf81e7b Mon Sep 17 00:00:00 2001 From: Dominic Date: Thu, 30 Apr 2020 16:48:50 +0200 Subject: [PATCH 032/170] also detect _state as state argument --- gotham_restful_derive/src/method.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gotham_restful_derive/src/method.rs b/gotham_restful_derive/src/method.rs index 95e9221..db0b8b8 100644 --- a/gotham_restful_derive/src/method.rs +++ b/gotham_restful_derive/src/method.rs @@ -181,7 +181,8 @@ fn interpret_arg_ty(attrs : &[Attribute], name : &str, ty : Type) -> Result Ok(if ty.mutability.is_none() { MethodArgumentType::StateRef } else { MethodArgumentType::StateMutRef }), From d754d6044de4e2dd34031ad1d3e8a6bf246a27ec Mon Sep 17 00:00:00 2001 From: msrd0 <1182023-msrd0@users.noreply.gitlab.com> Date: Fri, 1 May 2020 14:48:11 +0000 Subject: [PATCH 033/170] Allow custom error types through a macro and allow them to be used with Result --- README.md | 2 +- example/src/main.rs | 27 +- gotham_restful/src/lib.rs | 14 +- gotham_restful/src/response.rs | 63 ++ gotham_restful/src/result.rs | 704 -------------------- gotham_restful/src/result/auth_result.rs | 107 +++ gotham_restful/src/result/mod.rs | 224 +++++++ gotham_restful/src/result/no_content.rs | 106 +++ gotham_restful/src/result/raw.rs | 90 +++ gotham_restful/src/result/result.rs | 59 ++ gotham_restful/src/result/success.rs | 136 ++++ gotham_restful/src/routing.rs | 9 +- gotham_restful/src/types.rs | 17 +- gotham_restful_derive/Cargo.toml | 2 + gotham_restful_derive/src/from_body.rs | 2 +- gotham_restful_derive/src/lib.rs | 9 + gotham_restful_derive/src/openapi_type.rs | 24 +- gotham_restful_derive/src/resource_error.rs | 299 +++++++++ gotham_restful_derive/src/util.rs | 22 + 19 files changed, 1165 insertions(+), 751 deletions(-) create mode 100644 gotham_restful/src/response.rs delete mode 100644 gotham_restful/src/result.rs create mode 100644 gotham_restful/src/result/auth_result.rs create mode 100644 gotham_restful/src/result/mod.rs create mode 100644 gotham_restful/src/result/no_content.rs create mode 100644 gotham_restful/src/result/raw.rs create mode 100644 gotham_restful/src/result/result.rs create mode 100644 gotham_restful/src/result/success.rs create mode 100644 gotham_restful_derive/src/resource_error.rs diff --git a/README.md b/README.md index 51700af..1a7e323 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ fn main() { } ``` -Uploads and Downloads can also be handled, but you need to specify the mime type manually: +Uploads and Downloads can also be handled: ```rust #[derive(Resource)] diff --git a/example/src/main.rs b/example/src/main.rs index 3d9a249..f1d822b 100644 --- a/example/src/main.rs +++ b/example/src/main.rs @@ -36,7 +36,7 @@ struct User } #[rest_read_all(Users)] -fn read_all(_state : &mut State) -> Success>> +fn read_all() -> Success>> { vec![Username().fake(), Username().fake()] .into_iter() @@ -46,56 +46,55 @@ fn read_all(_state : &mut State) -> Success>> } #[rest_read(Users)] -fn read(_state : &mut State, id : u64) -> Success +fn read(id : u64) -> Success { let username : String = Username().fake(); User { username: format!("{}{}", username, id) }.into() } #[rest_search(Users)] -fn search(_state : &mut State, query : User) -> Success +fn search(query : User) -> Success { query.into() } #[rest_create(Users)] -fn create(_state : &mut State, body : User) +fn create(body : User) { info!("Created User: {}", body.username); } #[rest_update_all(Users)] -fn update_all(_state : &mut State, body : Vec) +fn update_all(body : Vec) { info!("Changing all Users to {:?}", body.into_iter().map(|u| u.username).collect::>()); } #[rest_update(Users)] -fn update(_state : &mut State, id : u64, body : User) +fn update(id : u64, body : User) { info!("Change User {} to {}", id, body.username); } #[rest_delete_all(Users)] -fn delete_all(_state : &mut State) +fn delete_all() { info!("Delete all Users"); } #[rest_delete(Users)] -fn delete(_state : &mut State, id : u64) +fn delete(id : u64) { info!("Delete User {}", id); } #[rest_read_all(Auth)] -fn auth_read_all(auth : AuthStatus<()>) -> AuthResult> +fn auth_read_all(auth : AuthStatus<()>) -> AuthSuccess { - let str : Success = match auth { - AuthStatus::Authenticated(data) => format!("{:?}", data).into(), - _ => return AuthErr - }; - str.into() + match auth { + AuthStatus::Authenticated(data) => Ok(format!("{:?}", data).into()), + _ => Err(Forbidden) + } } const ADDR : &str = "127.0.0.1:18080"; diff --git a/gotham_restful/src/lib.rs b/gotham_restful/src/lib.rs index c62ce0b..1142f29 100644 --- a/gotham_restful/src/lib.rs +++ b/gotham_restful/src/lib.rs @@ -52,7 +52,7 @@ fn main() { } ``` -Uploads and Downloads can also be handled, but you need to specify the mime type manually: +Uploads and Downloads can also be handled: ```rust,no_run # #[macro_use] extern crate gotham_restful_derive; @@ -131,6 +131,8 @@ pub mod export { pub use futures_util::future::FutureExt; + pub use serde_json; + #[cfg(feature = "database")] pub use gotham_middleware_diesel::Repo; @@ -176,14 +178,20 @@ pub use resource::{ ResourceDelete }; +mod response; +pub use response::Response; + mod result; pub use result::{ + AuthError, + AuthError::Forbidden, + AuthErrorOrOther, AuthResult, - AuthResult::AuthErr, + AuthSuccess, + IntoResponseError, NoContent, Raw, ResourceResult, - Response, Success }; diff --git a/gotham_restful/src/response.rs b/gotham_restful/src/response.rs new file mode 100644 index 0000000..ee4e1f3 --- /dev/null +++ b/gotham_restful/src/response.rs @@ -0,0 +1,63 @@ +use gotham::hyper::{Body, StatusCode}; +use mime::{Mime, APPLICATION_JSON}; + +/// A response, used to create the final gotham response from. +pub struct Response +{ + pub status : StatusCode, + pub body : Body, + pub mime : Option +} + +impl Response +{ + /// Create a new `Response` from raw data. + pub fn new>(status : StatusCode, body : B, mime : Option) -> Self + { + Self { + status, + body: body.into(), + mime + } + } + + /// Create a `Response` with mime type json from already serialized data. + pub fn json>(status : StatusCode, body : B) -> Self + { + Self { + status, + body: body.into(), + mime: Some(APPLICATION_JSON) + } + } + + /// Create a _204 No Content_ `Response`. + pub fn no_content() -> Self + { + Self { + status: StatusCode::NO_CONTENT, + body: Body::empty(), + mime: None + } + } + + /// Create an empty _403 Forbidden_ `Response`. + pub fn forbidden() -> Self + { + Self { + status: StatusCode::FORBIDDEN, + body: Body::empty(), + mime: None + } + } + + #[cfg(test)] + pub(crate) fn full_body(mut self) -> Result, ::Error> + { + use futures_executor::block_on; + use gotham::hyper::body::to_bytes; + + let bytes : &[u8] = &block_on(to_bytes(&mut self.body))?; + Ok(bytes.to_vec()) + } +} \ No newline at end of file diff --git a/gotham_restful/src/result.rs b/gotham_restful/src/result.rs deleted file mode 100644 index 792298f..0000000 --- a/gotham_restful/src/result.rs +++ /dev/null @@ -1,704 +0,0 @@ -use crate::{ResponseBody, StatusCode}; -#[cfg(feature = "openapi")] -use crate::{OpenapiSchema, OpenapiType}; -use futures_core::future::Future; -use futures_util::{future, future::FutureExt}; -use gotham::hyper::Body; -#[cfg(feature = "errorlog")] -use log::error; -use mime::{Mime, APPLICATION_JSON}; -#[cfg(feature = "openapi")] -use openapiv3::{SchemaKind, StringFormat, StringType, Type, VariantOrUnknownOrEmpty}; -use serde::Serialize; -use serde_json::error::Error as SerdeJsonError; -use std::{ - error::Error, - fmt::Debug, - pin::Pin -}; - -/// A response, used to create the final gotham response from. -pub struct Response -{ - pub status : StatusCode, - pub body : Body, - pub mime : Option -} - -impl Response -{ - /// Create a new `Response` from raw data. - pub fn new>(status : StatusCode, body : B, mime : Option) -> Self - { - Self { - status, - body: body.into(), - mime - } - } - - /// Create a `Response` with mime type json from already serialized data. - pub fn json>(status : StatusCode, body : B) -> Self - { - Self { - status, - body: body.into(), - mime: Some(APPLICATION_JSON) - } - } - - /// Create a _204 No Content_ `Response`. - pub fn no_content() -> Self - { - Self { - status: StatusCode::NO_CONTENT, - body: Body::empty(), - mime: None - } - } - - /// Create an empty _403 Forbidden_ `Response`. - pub fn forbidden() -> Self - { - Self { - status: StatusCode::FORBIDDEN, - body: Body::empty(), - mime: None - } - } - - #[cfg(test)] - fn full_body(mut self) -> Result, ::Error> - { - use futures_executor::block_on; - use gotham::hyper::body::to_bytes; - - let bytes : &[u8] = &block_on(to_bytes(&mut self.body))?; - Ok(bytes.to_vec()) - } -} - - -/// A trait provided to convert a resource's result to json. -pub trait ResourceResult -{ - type Err : Error + Send + 'static; - - /// Turn this into a response that can be returned to the browser. This api will likely - /// change in the future. - fn into_response(self) -> Pin> + Send>>; - - /// Return a list of supported mime types. - fn accepted_types() -> Option> - { - None - } - - #[cfg(feature = "openapi")] - fn schema() -> OpenapiSchema; - - #[cfg(feature = "openapi")] - fn default_status() -> StatusCode - { - StatusCode::OK - } -} - -#[cfg(feature = "openapi")] -impl crate::OpenapiType for Res -{ - fn schema() -> OpenapiSchema - { - Self::schema() - } -} - -/// The default json returned on an 500 Internal Server Error. -#[derive(Debug, Serialize)] -pub struct ResourceError -{ - error : bool, - message : String -} - -impl From for ResourceError -{ - fn from(message : T) -> Self - { - Self { - error: true, - message: message.to_string() - } - } -} - -fn into_response_helper(create_response : F) -> Pin> + Send>> -where - Err : Send + 'static, - F : FnOnce() -> Result -{ - let res = create_response(); - async move { res }.boxed() -} - -#[cfg(feature = "errorlog")] -fn errorlog(e : E) -{ - error!("The handler encountered an error: {}", e); -} - -#[cfg(not(feature = "errorlog"))] -fn errorlog(_e : E) {} - -impl ResourceResult for Result -where - Self : Send -{ - type Err = SerdeJsonError; - - fn into_response(self) -> Pin> + Send>> - { - into_response_helper(|| { - Ok(match self { - Ok(r) => Response::json(StatusCode::OK, serde_json::to_string(&r)?), - Err(e) => { - errorlog(&e); - let err : ResourceError = e.into(); - Response::json(StatusCode::INTERNAL_SERVER_ERROR, serde_json::to_string(&err)?) - } - }) - }) - } - - fn accepted_types() -> Option> - { - Some(vec![APPLICATION_JSON]) - } - - #[cfg(feature = "openapi")] - fn schema() -> OpenapiSchema - { - R::schema() - } -} - - -impl ResourceResult for Pin + Send>> -where - Res : ResourceResult + 'static -{ - type Err = Res::Err; - - fn into_response(self) -> Pin> + Send>> - { - self.then(|result| { - result.into_response() - }).boxed() - } - - fn accepted_types() -> Option> - { - Res::accepted_types() - } - - #[cfg(feature = "openapi")] - fn schema() -> OpenapiSchema - { - Res::schema() - } - - #[cfg(feature = "openapi")] - fn default_status() -> StatusCode - { - Res::default_status() - } -} - - -/** -This can be returned from a resource when there is no cause of an error. For example: - -``` -# #[macro_use] extern crate gotham_restful_derive; -# mod doc_tests_are_broken { -# use gotham::state::State; -# use gotham_restful::*; -# use serde::{Deserialize, Serialize}; -# -# #[derive(Resource)] -# struct MyResource; -# -#[derive(Deserialize, Serialize)] -# #[derive(OpenapiType)] -struct MyResponse { - message: String -} - -#[rest_read_all(MyResource)] -fn read_all(_state: &mut State) -> Success { - let res = MyResponse { message: "I'm always happy".to_string() }; - res.into() -} -# } -``` -*/ -pub struct Success(T); - -impl From for Success -{ - fn from(t : T) -> Self - { - Self(t) - } -} - -impl Clone for Success -{ - fn clone(&self) -> Self - { - Self(self.0.clone()) - } -} - -impl Debug for Success -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Success({:?})", self.0) - } -} - -impl ResourceResult for Success -where - Self : Send -{ - type Err = SerdeJsonError; - - fn into_response(self) -> Pin> + Send>> - { - into_response_helper(|| Ok(Response::json(StatusCode::OK, serde_json::to_string(&self.0)?))) - } - - fn accepted_types() -> Option> - { - Some(vec![APPLICATION_JSON]) - } - - #[cfg(feature = "openapi")] - fn schema() -> OpenapiSchema - { - T::schema() - } -} - -/** -This return type can be used to map another `ResourceResult` that can only be returned if the -client is authenticated. Otherwise, an empty _403 Forbidden_ response will be issued. Use can -look something like this (assuming the `auth` feature is enabled): - -``` -# #[macro_use] extern crate gotham_restful_derive; -# mod doc_tests_are_broken { -# use gotham::state::State; -# use gotham_restful::*; -# use serde::Deserialize; -# -# #[derive(Resource)] -# struct MyResource; -# -# #[derive(Clone, Deserialize)] -# struct MyAuthData { exp : u64 } -# -#[rest_read_all(MyResource)] -fn read_all(auth : AuthStatus) -> AuthResult { - let auth_data = match auth { - AuthStatus::Authenticated(data) => data, - _ => return AuthErr - }; - // do something - NoContent::default().into() -} -# } -``` -*/ -pub enum AuthResult -{ - Ok(T), - AuthErr -} - -impl AuthResult -{ - pub fn is_ok(&self) -> bool - { - match self { - Self::Ok(_) => true, - _ => false - } - } - - pub fn unwrap(self) -> T - { - match self { - Self::Ok(data) => data, - _ => panic!() - } - } -} - -impl From for AuthResult -{ - fn from(t : T) -> Self - { - Self::Ok(t) - } -} - -impl Clone for AuthResult -{ - fn clone(&self) -> Self - { - match self { - Self::Ok(t) => Self::Ok(t.clone()), - Self::AuthErr => Self::AuthErr - } - } -} - -impl Debug for AuthResult -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Ok(t) => write!(f, "Ok({:?})", t), - Self::AuthErr => write!(f, "AuthErr") - } - } -} - -impl ResourceResult for AuthResult -{ - type Err = T::Err; - - fn into_response(self) -> Pin> + Send>> - { - match self - { - Self::Ok(res) => res.into_response(), - Self::AuthErr => future::ok(Response::forbidden()).boxed() - } - } - - fn accepted_types() -> Option> - { - T::accepted_types() - } - - #[cfg(feature = "openapi")] - fn schema() -> OpenapiSchema - { - T::schema() - } - - #[cfg(feature = "openapi")] - fn default_status() -> StatusCode - { - T::default_status() - } -} - -impl, E : Error> ResourceResult for Result, E> -{ - type Err = T::Err; - - fn into_response(self) -> Pin> + Send>> - { - match self { - Ok(r) => r.into_response(), - Err(e) => { - into_response_helper(|| { - errorlog(&e); - let err : ResourceError = e.into(); - Ok(Response::json(StatusCode::INTERNAL_SERVER_ERROR, serde_json::to_string(&err)?)) - }) - } - } - } - - fn accepted_types() -> Option> - { - T::accepted_types() - } - - #[cfg(feature = "openapi")] - fn schema() -> OpenapiSchema - { - T::schema() - } - - #[cfg(feature = "openapi")] - fn default_status() -> StatusCode - { - T::default_status() - } -} - -/** -This is the return type of a resource that doesn't actually return something. It will result -in a _204 No Content_ answer by default. You don't need to use this type directly if using -the function attributes: - -``` -# #[macro_use] extern crate gotham_restful_derive; -# mod doc_tests_are_broken { -# use gotham::state::State; -# use gotham_restful::*; -# -# #[derive(Resource)] -# struct MyResource; -# -#[rest_read_all(MyResource)] -fn read_all(_state: &mut State) { - // do something -} -# } -``` -*/ -#[derive(Default)] -pub struct NoContent; - -impl From<()> for NoContent -{ - fn from(_ : ()) -> Self - { - Self {} - } -} - -impl ResourceResult for NoContent -{ - type Err = SerdeJsonError; // just for easier handling of `Result` - - /// This will always be a _204 No Content_ together with an empty string. - fn into_response(self) -> Pin> + Send>> - { - future::ok(Response::no_content()).boxed() - } - - fn accepted_types() -> Option> - { - Some(Vec::new()) - } - - /// Returns the schema of the `()` type. - #[cfg(feature = "openapi")] - fn schema() -> OpenapiSchema - { - <()>::schema() - } - - /// This will always be a _204 No Content_ - #[cfg(feature = "openapi")] - fn default_status() -> StatusCode - { - StatusCode::NO_CONTENT - } -} - -impl ResourceResult for Result -where - Self : Send -{ - type Err = SerdeJsonError; - - fn into_response(self) -> Pin> + Send>> - { - match self { - Ok(nc) => nc.into_response(), - Err(e) => into_response_helper(|| { - let err : ResourceError = e.into(); - Ok(Response::json(StatusCode::INTERNAL_SERVER_ERROR, serde_json::to_string(&err)?)) - }) - } - } - - fn accepted_types() -> Option> - { - NoContent::accepted_types() - } - - #[cfg(feature = "openapi")] - fn schema() -> OpenapiSchema - { - ::schema() - } - - #[cfg(feature = "openapi")] - fn default_status() -> StatusCode - { - NoContent::default_status() - } -} - -pub struct Raw -{ - pub raw : T, - pub mime : Mime -} - -impl Raw -{ - pub fn new(raw : T, mime : Mime) -> Self - { - Self { raw, mime } - } -} - -impl Clone for Raw -{ - fn clone(&self) -> Self - { - Self { - raw: self.raw.clone(), - mime: self.mime.clone() - } - } -} - -impl Debug for Raw -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Raw({:?}, {:?})", self.raw, self.mime) - } -} - -impl> ResourceResult for Raw -where - Self : Send -{ - type Err = SerdeJsonError; // just for easier handling of `Result, E>` - - fn into_response(self) -> Pin> + Send>> - { - future::ok(Response::new(StatusCode::OK, self.raw, Some(self.mime.clone()))).boxed() - } - - #[cfg(feature = "openapi")] - fn schema() -> OpenapiSchema - { - OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { - format: VariantOrUnknownOrEmpty::Item(StringFormat::Binary), - ..Default::default() - }))) - } -} - -impl ResourceResult for Result, E> -where - Self : Send, - Raw : ResourceResult -{ - type Err = SerdeJsonError; - - fn into_response(self) -> Pin> + Send>> - { - match self { - Ok(raw) => raw.into_response(), - Err(e) => into_response_helper(|| { - let err : ResourceError = e.into(); - Ok(Response::json(StatusCode::INTERNAL_SERVER_ERROR, serde_json::to_string(&err)?)) - }) - } - } - - fn accepted_types() -> Option> - { - as ResourceResult>::accepted_types() - } - - #[cfg(feature = "openapi")] - fn schema() -> OpenapiSchema - { - as ResourceResult>::schema() - } -} - - -#[cfg(test)] -mod test -{ - use super::*; - use futures_executor::block_on; - use mime::TEXT_PLAIN; - use thiserror::Error; - - #[derive(Debug, Default, Deserialize, Serialize)] - #[cfg_attr(feature = "openapi", derive(OpenapiType))] - struct Msg - { - msg : String - } - - #[derive(Debug, Default, Error)] - #[error("An Error")] - struct MsgError; - - #[test] - fn resource_result_ok() - { - let ok : Result = Ok(Msg::default()); - let res = block_on(ok.into_response()).expect("didn't expect error response"); - assert_eq!(res.status, StatusCode::OK); - assert_eq!(res.mime, Some(APPLICATION_JSON)); - assert_eq!(res.full_body().unwrap(), r#"{"msg":""}"#.as_bytes()); - } - - #[test] - fn resource_result_err() - { - let err : Result = Err(MsgError::default()); - let res = block_on(err.into_response()).expect("didn't expect error response"); - assert_eq!(res.status, StatusCode::INTERNAL_SERVER_ERROR); - assert_eq!(res.mime, Some(APPLICATION_JSON)); - assert_eq!(res.full_body().unwrap(), format!(r#"{{"error":true,"message":"{}"}}"#, MsgError::default()).as_bytes()); - } - - #[test] - fn success_always_successfull() - { - let success : Success = Msg::default().into(); - let res = block_on(success.into_response()).expect("didn't expect error response"); - assert_eq!(res.status, StatusCode::OK); - assert_eq!(res.mime, Some(APPLICATION_JSON)); - assert_eq!(res.full_body().unwrap(), r#"{"msg":""}"#.as_bytes()); - } - - #[test] - fn no_content_has_empty_response() - { - let no_content = NoContent::default(); - let res = block_on(no_content.into_response()).expect("didn't expect error response"); - assert_eq!(res.status, StatusCode::NO_CONTENT); - assert_eq!(res.mime, None); - assert_eq!(res.full_body().unwrap(), &[] as &[u8]); - } - - #[test] - fn no_content_result() - { - let no_content : Result = Ok(NoContent::default()); - let res = block_on(no_content.into_response()).expect("didn't expect error response"); - assert_eq!(res.status, StatusCode::NO_CONTENT); - assert_eq!(res.mime, None); - assert_eq!(res.full_body().unwrap(), &[] as &[u8]); - } - - #[test] - fn raw_response() - { - let msg = "Test"; - let raw = Raw::new(msg, TEXT_PLAIN); - let res = block_on(raw.into_response()).expect("didn't expect error response"); - assert_eq!(res.status, StatusCode::OK); - assert_eq!(res.mime, Some(TEXT_PLAIN)); - assert_eq!(res.full_body().unwrap(), msg.as_bytes()); - } -} diff --git a/gotham_restful/src/result/auth_result.rs b/gotham_restful/src/result/auth_result.rs new file mode 100644 index 0000000..86db9d3 --- /dev/null +++ b/gotham_restful/src/result/auth_result.rs @@ -0,0 +1,107 @@ +use gotham_restful_derive::ResourceError; + + +/** +This is an error type that always yields a _403 Forbidden_ response. This type is best used in +combination with [`AuthSuccess`] or [`AuthResult`]. + + [`AuthSuccess`]: type.AuthSuccess.html + [`AuthResult`]: type.AuthResult.html +*/ +#[derive(ResourceError)] +pub enum AuthError +{ + #[status(FORBIDDEN)] + #[display("Forbidden")] + Forbidden +} + +/** +This return type can be used to map another `ResourceResult` that can only be returned if the +client is authenticated. Otherwise, an empty _403 Forbidden_ response will be issued. Use can +look something like this (assuming the `auth` feature is enabled): + +``` +# #[macro_use] extern crate gotham_restful_derive; +# mod doc_tests_are_broken { +# use gotham::state::State; +# use gotham_restful::*; +# use serde::Deserialize; +# +# #[derive(Resource)] +# struct MyResource; +# +# #[derive(Clone, Deserialize)] +# struct MyAuthData { exp : u64 } +# +#[rest_read_all(MyResource)] +fn read_all(auth : AuthStatus) -> AuthSuccess { + let auth_data = match auth { + AuthStatus::Authenticated(data) => data, + _ => return Err(Forbidden) + }; + // do something + Ok(NoContent::default()) +} +# } +``` +*/ +pub type AuthSuccess = Result; + +/** +This is an error type that either yields a _403 Forbidden_ respone if produced from an authentication +error, or delegates to another error type. This type is best used with [`AuthResult`]. + + [`AuthResult`]: type.AuthResult.html +*/ +#[derive(ResourceError)] +pub enum AuthErrorOrOther +{ + #[status(UNAUTHORIZED)] + #[display("Forbidden")] + Forbidden, + #[display("{0}")] + Other(#[from] E) +} + +impl From for AuthErrorOrOther +{ + fn from(err : AuthError) -> Self + { + match err { + AuthError::Forbidden => Self::Forbidden + } + } +} + +/** +This return type can be used to map another `ResourceResult` that can only be returned if the +client is authenticated. Otherwise, an empty _403 Forbidden_ response will be issued. Use can +look something like this (assuming the `auth` feature is enabled): + +``` +# #[macro_use] extern crate gotham_restful_derive; +# mod doc_tests_are_broken { +# use gotham::state::State; +# use gotham_restful::*; +# use serde::Deserialize; +# use std::io; +# +# #[derive(Resource)] +# struct MyResource; +# +# #[derive(Clone, Deserialize)] +# struct MyAuthData { exp : u64 } +# +#[rest_read_all(MyResource)] +fn read_all(auth : AuthStatus) -> AuthResult { + let auth_data = match auth { + AuthStatus::Authenticated(data) => data, + _ => Err(Forbidden)? + }; + // do something + Ok(NoContent::default().into()) +} +# } +*/ +pub type AuthResult = Result>; diff --git a/gotham_restful/src/result/mod.rs b/gotham_restful/src/result/mod.rs new file mode 100644 index 0000000..8399c7c --- /dev/null +++ b/gotham_restful/src/result/mod.rs @@ -0,0 +1,224 @@ +use crate::Response; +#[cfg(feature = "openapi")] +use crate::OpenapiSchema; +use futures_util::future::FutureExt; +use mime::Mime; +use serde::Serialize; +use std::{ + error::Error, + future::Future, + fmt::{Debug, Display}, + pin::Pin +}; + +mod auth_result; +pub use auth_result::{AuthError, AuthErrorOrOther, AuthResult, AuthSuccess}; + +mod no_content; +pub use no_content::NoContent; + +mod raw; +pub use raw::Raw; + +mod result; +pub use result::IntoResponseError; + +mod success; +pub use success::Success; + +/// A trait provided to convert a resource's result to json. +pub trait ResourceResult +{ + type Err : Error + Send + 'static; + + /// Turn this into a response that can be returned to the browser. This api will likely + /// change in the future. + fn into_response(self) -> Pin> + Send>>; + + /// Return a list of supported mime types. + fn accepted_types() -> Option> + { + None + } + + #[cfg(feature = "openapi")] + fn schema() -> OpenapiSchema; + + #[cfg(feature = "openapi")] + fn default_status() -> crate::StatusCode + { + crate::StatusCode::OK + } +} + +#[cfg(feature = "openapi")] +impl crate::OpenapiType for Res +{ + fn schema() -> OpenapiSchema + { + Self::schema() + } +} + +/// The default json returned on an 500 Internal Server Error. +#[derive(Debug, Serialize)] +pub(crate) struct ResourceError +{ + error : bool, + message : String +} + +impl From for ResourceError +{ + fn from(message : T) -> Self + { + Self { + error: true, + message: message.to_string() + } + } +} + +fn into_response_helper(create_response : F) -> Pin> + Send>> +where + Err : Send + 'static, + F : FnOnce() -> Result +{ + let res = create_response(); + async move { res }.boxed() +} + +#[cfg(feature = "errorlog")] +fn errorlog(e : E) +{ + error!("The handler encountered an error: {}", e); +} + +#[cfg(not(feature = "errorlog"))] +fn errorlog(_e : E) {} + +fn handle_error(e : E) -> Pin> + Send>> +where + E : Display + IntoResponseError +{ + into_response_helper(|| { + errorlog(&e); + e.into_response_error() + }) +} + + +impl ResourceResult for Pin + Send>> +where + Res : ResourceResult + 'static +{ + type Err = Res::Err; + + fn into_response(self) -> Pin> + Send>> + { + self.then(|result| { + result.into_response() + }).boxed() + } + + fn accepted_types() -> Option> + { + Res::accepted_types() + } + + #[cfg(feature = "openapi")] + fn schema() -> OpenapiSchema + { + Res::schema() + } + + #[cfg(feature = "openapi")] + fn default_status() -> crate::StatusCode + { + Res::default_status() + } +} + + + +#[cfg(test)] +mod test +{ + use super::*; + use crate::{OpenapiType, StatusCode}; + use futures_executor::block_on; + use mime::{APPLICATION_JSON, TEXT_PLAIN}; + use thiserror::Error; + + #[derive(Debug, Default, Deserialize, Serialize)] + #[cfg_attr(feature = "openapi", derive(OpenapiType))] + struct Msg + { + msg : String + } + + #[derive(Debug, Default, Error)] + #[error("An Error")] + struct MsgError; + + #[test] + fn resource_result_ok() + { + let ok : Result = Ok(Msg::default()); + let res = block_on(ok.into_response()).expect("didn't expect error response"); + assert_eq!(res.status, StatusCode::OK); + assert_eq!(res.mime, Some(APPLICATION_JSON)); + assert_eq!(res.full_body().unwrap(), r#"{"msg":""}"#.as_bytes()); + } + + #[test] + fn resource_result_err() + { + let err : Result = Err(MsgError::default()); + let res = block_on(err.into_response()).expect("didn't expect error response"); + assert_eq!(res.status, StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!(res.mime, Some(APPLICATION_JSON)); + assert_eq!(res.full_body().unwrap(), format!(r#"{{"error":true,"message":"{}"}}"#, MsgError::default()).as_bytes()); + } + + #[test] + fn success_always_successfull() + { + let success : Success = Msg::default().into(); + let res = block_on(success.into_response()).expect("didn't expect error response"); + assert_eq!(res.status, StatusCode::OK); + assert_eq!(res.mime, Some(APPLICATION_JSON)); + assert_eq!(res.full_body().unwrap(), r#"{"msg":""}"#.as_bytes()); + } + + #[test] + fn no_content_has_empty_response() + { + let no_content = NoContent::default(); + let res = block_on(no_content.into_response()).expect("didn't expect error response"); + assert_eq!(res.status, StatusCode::NO_CONTENT); + assert_eq!(res.mime, None); + assert_eq!(res.full_body().unwrap(), &[] as &[u8]); + } + + #[test] + fn no_content_result() + { + let no_content : Result = Ok(NoContent::default()); + let res = block_on(no_content.into_response()).expect("didn't expect error response"); + assert_eq!(res.status, StatusCode::NO_CONTENT); + assert_eq!(res.mime, None); + assert_eq!(res.full_body().unwrap(), &[] as &[u8]); + } + + #[test] + fn raw_response() + { + let msg = "Test"; + let raw = Raw::new(msg, TEXT_PLAIN); + let res = block_on(raw.into_response()).expect("didn't expect error response"); + assert_eq!(res.status, StatusCode::OK); + assert_eq!(res.mime, Some(TEXT_PLAIN)); + assert_eq!(res.full_body().unwrap(), msg.as_bytes()); + } +} diff --git a/gotham_restful/src/result/no_content.rs b/gotham_restful/src/result/no_content.rs new file mode 100644 index 0000000..c04ea8d --- /dev/null +++ b/gotham_restful/src/result/no_content.rs @@ -0,0 +1,106 @@ +use super::{ResourceResult, handle_error}; +use crate::{IntoResponseError, Response}; +#[cfg(feature = "openapi")] +use crate::{OpenapiSchema, OpenapiType}; +use futures_util::{future, future::FutureExt}; +use mime::Mime; +use std::{ + fmt::Display, + future::Future, + pin::Pin +}; + +/** +This is the return type of a resource that doesn't actually return something. It will result +in a _204 No Content_ answer by default. You don't need to use this type directly if using +the function attributes: + +``` +# #[macro_use] extern crate gotham_restful_derive; +# mod doc_tests_are_broken { +# use gotham::state::State; +# use gotham_restful::*; +# +# #[derive(Resource)] +# struct MyResource; +# +#[rest_read_all(MyResource)] +fn read_all(_state: &mut State) { + // do something +} +# } +``` +*/ +#[derive(Clone, Copy, Default)] +pub struct NoContent; + +impl From<()> for NoContent +{ + fn from(_ : ()) -> Self + { + Self {} + } +} + +impl ResourceResult for NoContent +{ + // TODO this shouldn't be a serde_json::Error + type Err = serde_json::Error; // just for easier handling of `Result` + + /// This will always be a _204 No Content_ together with an empty string. + fn into_response(self) -> Pin> + Send>> + { + future::ok(Response::no_content()).boxed() + } + + fn accepted_types() -> Option> + { + Some(Vec::new()) + } + + /// Returns the schema of the `()` type. + #[cfg(feature = "openapi")] + fn schema() -> OpenapiSchema + { + <()>::schema() + } + + /// This will always be a _204 No Content_ + #[cfg(feature = "openapi")] + fn default_status() -> crate::StatusCode + { + crate::StatusCode::NO_CONTENT + } +} + +impl ResourceResult for Result +where + E : Display + IntoResponseError +{ + type Err = serde_json::Error; + + fn into_response(self) -> Pin> + Send>> + { + match self { + Ok(nc) => nc.into_response(), + Err(e) => handle_error(e) + } + } + + fn accepted_types() -> Option> + { + NoContent::accepted_types() + } + + #[cfg(feature = "openapi")] + fn schema() -> OpenapiSchema + { + ::schema() + } + + #[cfg(feature = "openapi")] + fn default_status() -> crate::StatusCode + { + NoContent::default_status() + } +} \ No newline at end of file diff --git a/gotham_restful/src/result/raw.rs b/gotham_restful/src/result/raw.rs new file mode 100644 index 0000000..95fafdf --- /dev/null +++ b/gotham_restful/src/result/raw.rs @@ -0,0 +1,90 @@ +use super::{IntoResponseError, ResourceResult, handle_error}; +use crate::{Response, StatusCode}; +#[cfg(feature = "openapi")] +use crate::OpenapiSchema; +use futures_core::future::Future; +use futures_util::{future, future::FutureExt}; +use gotham::hyper::Body; +use mime::Mime; +#[cfg(feature = "openapi")] +use openapiv3::{SchemaKind, StringFormat, StringType, Type, VariantOrUnknownOrEmpty}; +use serde_json::error::Error as SerdeJsonError; +use std::{ + fmt::{Debug, Display}, + pin::Pin +}; + +pub struct Raw +{ + pub raw : T, + pub mime : Mime +} + +impl Raw +{ + pub fn new(raw : T, mime : Mime) -> Self + { + Self { raw, mime } + } +} + +impl Clone for Raw +{ + fn clone(&self) -> Self + { + Self { + raw: self.raw.clone(), + mime: self.mime.clone() + } + } +} + +impl Debug for Raw +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Raw({:?}, {:?})", self.raw, self.mime) + } +} + +impl> ResourceResult for Raw +where + Self : Send +{ + type Err = SerdeJsonError; // just for easier handling of `Result, E>` + + fn into_response(self) -> Pin> + Send>> + { + future::ok(Response::new(StatusCode::OK, self.raw, Some(self.mime.clone()))).boxed() + } + + #[cfg(feature = "openapi")] + fn schema() -> OpenapiSchema + { + OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { + format: VariantOrUnknownOrEmpty::Item(StringFormat::Binary), + ..Default::default() + }))) + } +} + +impl ResourceResult for Result, E> +where + Raw : ResourceResult, + E : Display + IntoResponseError as ResourceResult>::Err> +{ + type Err = E::Err; + + fn into_response(self) -> Pin> + Send>> + { + match self { + Ok(raw) => raw.into_response(), + Err(e) => handle_error(e) + } + } + + #[cfg(feature = "openapi")] + fn schema() -> OpenapiSchema + { + as ResourceResult>::schema() + } +} \ No newline at end of file diff --git a/gotham_restful/src/result/result.rs b/gotham_restful/src/result/result.rs new file mode 100644 index 0000000..5857812 --- /dev/null +++ b/gotham_restful/src/result/result.rs @@ -0,0 +1,59 @@ +use super::{ResourceResult, handle_error, into_response_helper}; +use crate::{ + result::ResourceError, + Response, ResponseBody, StatusCode +}; +#[cfg(feature = "openapi")] +use crate::OpenapiSchema; +use futures_core::future::Future; +use mime::{Mime, APPLICATION_JSON}; +use std::{ + error::Error, + fmt::Display, + pin::Pin +}; + +pub trait IntoResponseError +{ + type Err : Error + Send + 'static; + + fn into_response_error(self) -> Result; +} + +impl IntoResponseError for E +{ + type Err = serde_json::Error; + + fn into_response_error(self) -> Result + { + let err : ResourceError = self.into(); + Ok(Response::json(StatusCode::INTERNAL_SERVER_ERROR, serde_json::to_string(&err)?)) + } +} + +impl ResourceResult for Result +where + R : ResponseBody, + E : Display + IntoResponseError +{ + type Err = E::Err; + + fn into_response(self) -> Pin> + Send>> + { + match self { + Ok(r) => into_response_helper(|| Ok(Response::json(StatusCode::OK, serde_json::to_string(&r)?))), + Err(e) => handle_error(e) + } + } + + fn accepted_types() -> Option> + { + Some(vec![APPLICATION_JSON]) + } + + #[cfg(feature = "openapi")] + fn schema() -> OpenapiSchema + { + R::schema() + } +} \ No newline at end of file diff --git a/gotham_restful/src/result/success.rs b/gotham_restful/src/result/success.rs new file mode 100644 index 0000000..9931384 --- /dev/null +++ b/gotham_restful/src/result/success.rs @@ -0,0 +1,136 @@ +use super::{ResourceResult, into_response_helper}; +use crate::{Response, ResponseBody}; +#[cfg(feature = "openapi")] +use crate::OpenapiSchema; +use gotham::hyper::StatusCode; +use mime::{Mime, APPLICATION_JSON}; +use std::{ + fmt::Debug, + future::Future, + pin::Pin, + ops::{Deref, DerefMut} +}; + +/** +This can be returned from a resource when there is no cause of an error. It behaves similar to a +smart pointer like box, it that it implements `AsRef`, `Deref` and the likes. + +Usage example: + +``` +# #[macro_use] extern crate gotham_restful_derive; +# mod doc_tests_are_broken { +# use gotham::state::State; +# use gotham_restful::*; +# use serde::{Deserialize, Serialize}; +# +# #[derive(Resource)] +# struct MyResource; +# +#[derive(Deserialize, Serialize)] +# #[derive(OpenapiType)] +struct MyResponse { + message: &'static str +} + +#[rest_read_all(MyResource)] +fn read_all(_state: &mut State) -> Success { + let res = MyResponse { message: "I'm always happy" }; + res.into() +} +# } +``` +*/ +pub struct Success(T); + +impl AsMut for Success +{ + fn as_mut(&mut self) -> &mut T + { + &mut self.0 + } +} + +impl AsRef for Success +{ + fn as_ref(&self) -> &T + { + &self.0 + } +} + +impl Deref for Success +{ + type Target = T; + + fn deref(&self) -> &T + { + &self.0 + } +} + +impl DerefMut for Success +{ + fn deref_mut(&mut self) -> &mut T + { + &mut self.0 + } +} + +impl From for Success +{ + fn from(t : T) -> Self + { + Self(t) + } +} + +impl Clone for Success +{ + fn clone(&self) -> Self + { + Self(self.0.clone()) + } +} + +impl Copy for Success +{ +} + +impl Debug for Success +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Success({:?})", self.0) + } +} + +impl Default for Success +{ + fn default() -> Self + { + Self(T::default()) + } +} + +impl ResourceResult for Success +where + Self : Send +{ + type Err = serde_json::Error; + + fn into_response(self) -> Pin> + Send>> + { + into_response_helper(|| Ok(Response::json(StatusCode::OK, serde_json::to_string(self.as_ref())?))) + } + + fn accepted_types() -> Option> + { + Some(vec![APPLICATION_JSON]) + } + + #[cfg(feature = "openapi")] + fn schema() -> OpenapiSchema + { + T::schema() + } +} diff --git a/gotham_restful/src/routing.rs b/gotham_restful/src/routing.rs index f213248..ffc9e58 100644 --- a/gotham_restful/src/routing.rs +++ b/gotham_restful/src/routing.rs @@ -1,13 +1,16 @@ use crate::{ matcher::{AcceptHeaderMatcher, ContentTypeMatcher}, - openapi::router::OpenapiRouter, resource::*, - result::{ResourceError, ResourceResult, Response}, + result::{ResourceError, ResourceResult}, RequestBody, + Response, StatusCode }; #[cfg(feature = "openapi")] -use crate::openapi::builder::OpenapiBuilder; +use crate::openapi::{ + builder::OpenapiBuilder, + router::OpenapiRouter +}; use futures_util::{future, future::FutureExt}; use gotham::{ diff --git a/gotham_restful/src/types.rs b/gotham_restful/src/types.rs index e0c769b..504cf94 100644 --- a/gotham_restful/src/types.rs +++ b/gotham_restful/src/types.rs @@ -1,11 +1,14 @@ #[cfg(feature = "openapi")] use crate::OpenapiType; -use crate::result::ResourceError; use gotham::hyper::body::Bytes; use mime::{Mime, APPLICATION_JSON}; use serde::{de::DeserializeOwned, Serialize}; -use std::panic::RefUnwindSafe; +use std::{ + error::Error, + panic::RefUnwindSafe +}; +use thiserror::Error; #[cfg(not(feature = "openapi"))] pub trait ResourceType @@ -44,7 +47,7 @@ impl ResponseBody for T /// to create the type from a hyper body chunk and it's content type. pub trait FromBody : Sized { - type Err : Into; + type Err : Error; /// Create the request body from a raw body and the content type. fn from_body(body : Bytes, content_type : Mime) -> Result; @@ -60,6 +63,14 @@ impl FromBody for T } } +/// This error type can be used by `FromBody` implementations when there is no need to return any +/// errors. + +#[derive(Clone, Copy, Debug, Error)] +#[error("No Error")] +pub struct FromBodyNoError; + + /// 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`. diff --git a/gotham_restful_derive/Cargo.toml b/gotham_restful_derive/Cargo.toml index 4166067..1fb4f53 100644 --- a/gotham_restful_derive/Cargo.toml +++ b/gotham_restful_derive/Cargo.toml @@ -18,8 +18,10 @@ gitlab = { repository = "msrd0/gotham-restful", branch = "master" } [dependencies] heck = "0.3.1" +lazy_static = "1.4.0" proc-macro2 = "1.0.10" quote = "1.0.3" +regex = "1.3.7" syn = { version = "1.0.17", features = ["extra-traits", "full"] } [features] diff --git a/gotham_restful_derive/src/from_body.rs b/gotham_restful_derive/src/from_body.rs index dd2069e..a027c55 100644 --- a/gotham_restful_derive/src/from_body.rs +++ b/gotham_restful_derive/src/from_body.rs @@ -120,7 +120,7 @@ fn expand(tokens : TokenStream) -> Result impl #generics #krate::FromBody for #ident #generics where #where_clause { - type Err = String; + type Err = #krate::FromBodyNoError; fn from_body(#body_ident : #krate::gotham::hyper::body::Bytes, #type_ident : #krate::Mime) -> Result { diff --git a/gotham_restful_derive/src/lib.rs b/gotham_restful_derive/src/lib.rs index 4a98a9d..78b8796 100644 --- a/gotham_restful_derive/src/lib.rs +++ b/gotham_restful_derive/src/lib.rs @@ -12,6 +12,8 @@ mod request_body; use request_body::expand_request_body; mod resource; use resource::expand_resource; +mod resource_error; +use resource_error::expand_resource_error; #[cfg(feature = "openapi")] mod openapi_type; @@ -52,6 +54,13 @@ pub fn derive_resource(tokens : TokenStream) -> TokenStream print_tokens(expand_resource(tokens)) } +#[proc_macro_derive(ResourceError, attributes(display, from, status))] +pub fn derive_resource_error(tokens : TokenStream) -> TokenStream +{ + print_tokens(expand_resource_error(tokens)) +} + + #[proc_macro_attribute] pub fn rest_read_all(attr : TokenStream, item : TokenStream) -> TokenStream { diff --git a/gotham_restful_derive/src/openapi_type.rs b/gotham_restful_derive/src/openapi_type.rs index 4f39983..319972c 100644 --- a/gotham_restful_derive/src/openapi_type.rs +++ b/gotham_restful_derive/src/openapi_type.rs @@ -1,12 +1,7 @@ -use crate::util::CollectToResult; +use crate::util::{CollectToResult, remove_parens}; use proc_macro::TokenStream; -use proc_macro2::{ - Delimiter, - TokenStream as TokenStream2, - TokenTree -}; +use proc_macro2::TokenStream as TokenStream2; use quote::quote; -use std::{iter, iter::FromIterator}; use syn::{ spanned::Spanned, Attribute, @@ -84,21 +79,6 @@ fn to_bool(lit : &Lit) -> Result } } -fn remove_parens(input : TokenStream2) -> TokenStream2 -{ - let iter = input.into_iter().flat_map(|tt| { - if let TokenTree::Group(group) = &tt - { - if group.delimiter() == Delimiter::Parenthesis - { - return Box::new(group.stream().into_iter()) as Box>; - } - } - Box::new(iter::once(tt)) - }); - TokenStream2::from_iter(iter) -} - fn parse_attributes(input : &[Attribute]) -> Result { let mut parsed = Attrs::default(); diff --git a/gotham_restful_derive/src/resource_error.rs b/gotham_restful_derive/src/resource_error.rs new file mode 100644 index 0000000..4958123 --- /dev/null +++ b/gotham_restful_derive/src/resource_error.rs @@ -0,0 +1,299 @@ +use crate::util::{CollectToResult, remove_parens}; +use lazy_static::lazy_static; +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::{format_ident, quote}; +use regex::Regex; +use std::iter; +use syn::{ + parse_macro_input, + spanned::Spanned, + Attribute, + Data, + DeriveInput, + Error, + Fields, + GenericParam, + Ident, + LitStr, + Path, + PathSegment, + Type, + Variant +}; + + +struct ErrorVariantField +{ + attrs : Vec, + ident : Ident, + ty : Type +} + +struct ErrorVariant +{ + ident : Ident, + status : Option, + is_named : bool, + fields : Vec, + from_ty : Option<(usize, Type)>, + display : Option +} + +fn process_variant(variant : Variant) -> Result +{ + let status = match variant.attrs.iter() + .find(|attr| attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("status".to_string())) + { + Some(attr) => Some(parse_macro_input::parse::(remove_parens(attr.tokens.clone()).into())?), + None => None + }; + + let mut is_named = false; + let mut fields = Vec::new(); + match variant.fields { + Fields::Named(named) => { + is_named = true; + for field in named.named + { + let span = field.span(); + fields.push(ErrorVariantField { + attrs: field.attrs, + ident: field.ident.ok_or_else(|| Error::new(span, "Missing ident for this enum variant field"))?, + ty: field.ty + }); + } + }, + Fields::Unnamed(unnamed) => { + for (i, field) in unnamed.unnamed.into_iter().enumerate() + { + fields.push(ErrorVariantField { + attrs: field.attrs, + ident: format_ident!("arg{}", i), + ty: field.ty + }) + } + }, + Fields::Unit => {} + } + + let from_ty = fields.iter() + .enumerate() + .find(|(_, field)| field.attrs.iter().any(|attr| attr.path.segments.last().map(|segment| segment.ident.to_string()) == Some("from".to_string()))) + .map(|(i, field)| (i, field.ty.clone())); + + let display = match variant.attrs.iter() + .find(|attr| attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("display".to_string())) + { + Some(attr) => Some(parse_macro_input::parse::(remove_parens(attr.tokens.clone()).into())?), + None => None + }; + + Ok(ErrorVariant { + ident: variant.ident, + status, + is_named, + fields, + from_ty, + display + }) +} + +fn path_segment(name : &str) -> PathSegment +{ + PathSegment { + ident: format_ident!("{}", name), + arguments: Default::default() + } +} + +lazy_static! { + // TODO this is a really ugly regex that requires at least two characters between captures + static ref DISPLAY_REGEX : Regex = Regex::new(r"(^|[^\{])\{(?P[^\}]+)\}([^\}]|$)").unwrap(); +} + +impl ErrorVariant +{ + fn fields_pat(&self) -> TokenStream2 + { + let mut fields = self.fields.iter().map(|field| &field.ident).peekable(); + if fields.peek().is_none() { + quote!() + } else if self.is_named { + quote!( { #( #fields ),* } ) + } else { + quote!( ( #( #fields ),* ) ) + } + } + + fn to_display_match_arm(&self, formatter_ident : &Ident, enum_ident : &Ident) -> Result + { + let ident = &self.ident; + let display = self.display.as_ref().ok_or_else(|| Error::new(self.ident.span(), "Missing display string for this variant"))?; + + // lets find all required format parameters + let display_str = display.value(); + let params = DISPLAY_REGEX.captures_iter(&display_str) + .map(|cap| format_ident!("{}{}", if self.is_named { "" } else { "arg" }, cap.name("param").unwrap().as_str())); + + let fields_pat = self.fields_pat(); + Ok(quote! { + #enum_ident::#ident #fields_pat => write!(#formatter_ident, #display #(, #params = #params)*) + }) + } + + fn into_match_arm(self, krate : &TokenStream2, enum_ident : &Ident) -> TokenStream2 + { + let ident = &self.ident; + let fields_pat = self.fields_pat(); + let status = self.status.map(|status| { + // the status might be relative to StatusCode, so let's fix that + if status.leading_colon.is_none() && status.segments.len() < 2 + { + let status_ident = status.segments.first().map(|path| path.clone()).unwrap_or_else(|| path_segment("OK")); + Path { + leading_colon: Some(Default::default()), + segments: vec![path_segment("gotham_restful"), path_segment("gotham"), path_segment("hyper"), path_segment("StatusCode"), status_ident].into_iter().collect() + } + } + else { status } + }); + + // the response will come directly from the from_ty if present + let res = match self.from_ty { + Some((from_index, _)) => { + let from_field = &self.fields[from_index].ident; + quote!(#from_field.into_response_error()) + }, + None => quote!(Ok(#krate::Response { + status: { #status }.into(), + body: #krate::gotham::hyper::Body::empty(), + mime: None + })) + }; + + quote! { + #enum_ident::#ident #fields_pat => #res + } + } + + fn were(&self) -> Option + { + match self.from_ty.as_ref() { + Some((_, ty)) => Some(quote!( #ty : ::std::error::Error )), + None => None + } + } +} + +fn expand(tokens : TokenStream) -> Result +{ + let krate = super::krate(); + let input = parse_macro_input::parse::(tokens)?; + let ident = input.ident; + let generics = input.generics; + + let inum = match input.data { + Data::Enum(inum) => Ok(inum), + Data::Struct(strukt) => Err(strukt.struct_token.span()), + Data::Union(uni) => Err(uni.union_token.span()) + }.map_err(|span| Error::new(span, "#[derive(ResourceError)] only works for enums"))?; + let variants = inum.variants.into_iter() + .map(|variant| process_variant(variant)) + .collect_to_result()?; + + let display_impl = if variants.iter().any(|v| v.display.is_none()) { None } else { + let were = generics.params.iter().filter_map(|param| match param { + GenericParam::Type(ty) => { + let ident = &ty.ident; + Some(quote!(#ident : ::std::fmt::Display)) + }, + _ => None + }); + let formatter_ident = format_ident!("resource_error_display_formatter"); + let match_arms = variants.iter() + .map(|v| v.to_display_match_arm(&formatter_ident, &ident)) + .collect_to_result()?; + Some(quote! { + impl #generics ::std::fmt::Display for #ident #generics + where #( #were ),* + { + fn fmt(&self, #formatter_ident: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result + { + match self { + #( #match_arms ),* + } + } + } + }) + }; + + let mut from_impls : Vec = Vec::new(); + + for var in &variants + { + let var_ident = &var.ident; + let (from_index, from_ty) = match var.from_ty.as_ref() { + Some(f) => f, + None => continue + }; + let from_ident = &var.fields[*from_index].ident; + + let fields_pat = var.fields_pat(); + let fields_where = var.fields.iter().enumerate() + .filter(|(i, _)| i != from_index) + .map(|(_, field)| { + let ty = &field.ty; + quote!( #ty : Default ) + }) + .chain(iter::once(quote!( #from_ty : ::std::error::Error ))); + let fields_let = var.fields.iter().enumerate() + .filter(|(i, _)| i != from_index) + .map(|(_, field)| { + let id = &field.ident; + let ty = &field.ty; + quote!( let #id : #ty = Default::default(); ) + }); + + from_impls.push(quote! { + impl #generics ::std::convert::From<#from_ty> for #ident #generics + where #( #fields_where ),* + { + fn from(#from_ident : #from_ty) -> Self + { + #( #fields_let )* + Self::#var_ident #fields_pat + } + } + }); + } + + let were = variants.iter().filter_map(|variant| variant.were()).collect::>(); + let variants = variants.into_iter().map(|variant| variant.into_match_arm(&krate, &ident)); + + Ok(quote! { + #display_impl + + impl #generics #krate::IntoResponseError for #ident #generics + where #( #were ),* + { + type Err = #krate::export::serde_json::Error; + + fn into_response_error(self) -> Result<#krate::Response, Self::Err> + { + match self { + #( #variants ),* + } + } + } + + #( #from_impls )* + }) +} + +pub fn expand_resource_error(tokens : TokenStream) -> TokenStream +{ + expand(tokens) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} diff --git a/gotham_restful_derive/src/util.rs b/gotham_restful_derive/src/util.rs index 319830a..12a4951 100644 --- a/gotham_restful_derive/src/util.rs +++ b/gotham_restful_derive/src/util.rs @@ -1,3 +1,9 @@ +use proc_macro2::{ + Delimiter, + TokenStream as TokenStream2, + TokenTree +}; +use std::iter; use syn::Error; pub trait CollectToResult @@ -25,3 +31,19 @@ where }) } } + + +pub fn remove_parens(input : TokenStream2) -> TokenStream2 +{ + let iter = input.into_iter().flat_map(|tt| { + if let TokenTree::Group(group) = &tt + { + if group.delimiter() == Delimiter::Parenthesis + { + return Box::new(group.stream().into_iter()) as Box>; + } + } + Box::new(iter::once(tt)) + }); + iter.collect() +} From 101e94b90007da2ddf02c66b808c4ff353c5f578 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 3 May 2020 18:48:55 +0200 Subject: [PATCH 034/170] improve test coverage for the result types --- gotham_restful/src/openapi/operation.rs | 10 +-- gotham_restful/src/result/mod.rs | 88 ++++++++----------------- gotham_restful/src/result/no_content.rs | 36 +++++++++- gotham_restful/src/result/raw.rs | 32 ++++++--- gotham_restful/src/result/result.rs | 49 +++++++++++++- gotham_restful/src/result/success.rs | 40 +++++++++-- 6 files changed, 171 insertions(+), 84 deletions(-) diff --git a/gotham_restful/src/openapi/operation.rs b/gotham_restful/src/openapi/operation.rs index e949301..fc06e43 100644 --- a/gotham_restful/src/openapi/operation.rs +++ b/gotham_restful/src/openapi/operation.rs @@ -6,7 +6,7 @@ use crate::{ }; use super::SECURITY_NAME; use indexmap::IndexMap; -use mime::{Mime, STAR_STAR}; +use mime::Mime; use openapiv3::{ MediaType, Operation, Parameter, ParameterData, ParameterSchemaOrContent, ReferenceOr, ReferenceOr::Item, RequestBody as OARequestBody, Response, Responses, Schema, SchemaKind, @@ -148,7 +148,7 @@ impl<'a> OperationDescription<'a> 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); - let content = Self::schema_to_content(accepted_types.unwrap_or_else(|| vec![STAR_STAR]), schema); + let content = Self::schema_to_content(accepted_types.or_all_types(), schema); let mut responses : IndexMap> = IndexMap::new(); responses.insert(StatusCode::Code(default_status.as_u16()), Item(Response { @@ -159,7 +159,7 @@ impl<'a> OperationDescription<'a> let request_body = body_schema.map(|schema| Item(OARequestBody { description: None, - content: Self::schema_to_content(supported_types.unwrap_or_else(|| vec![STAR_STAR]), schema), + content: Self::schema_to_content(supported_types.or_all_types(), schema), required: true })); @@ -199,7 +199,7 @@ mod test { let types = NoContent::accepted_types(); let schema = ::schema(); - let content = OperationDescription::schema_to_content(types.unwrap_or_else(|| vec![STAR_STAR]), Item(schema.into_schema())); + let content = OperationDescription::schema_to_content(types.or_all_types(), Item(schema.into_schema())); assert!(content.is_empty()); } @@ -208,7 +208,7 @@ mod test { let types = Raw::<&str>::accepted_types(); let schema = as OpenapiType>::schema(); - let content = OperationDescription::schema_to_content(types.unwrap_or_else(|| vec![STAR_STAR]), Item(schema.into_schema())); + let content = OperationDescription::schema_to_content(types.or_all_types(), Item(schema.into_schema())); assert_eq!(content.len(), 1); let json = serde_json::to_string(&content.values().nth(0).unwrap()).unwrap(); assert_eq!(json, r#"{"schema":{"type":"string","format":"binary"}}"#); diff --git a/gotham_restful/src/result/mod.rs b/gotham_restful/src/result/mod.rs index 8399c7c..834bf46 100644 --- a/gotham_restful/src/result/mod.rs +++ b/gotham_restful/src/result/mod.rs @@ -2,7 +2,7 @@ use crate::Response; #[cfg(feature = "openapi")] use crate::OpenapiSchema; use futures_util::future::FutureExt; -use mime::Mime; +use mime::{Mime, STAR_STAR}; use serde::Serialize; use std::{ error::Error, @@ -26,6 +26,21 @@ pub use result::IntoResponseError; mod success; pub use success::Success; + +pub(crate) trait OrAllTypes +{ + fn or_all_types(self) -> Vec; +} + +impl OrAllTypes for Option> +{ + fn or_all_types(self) -> Vec + { + self.unwrap_or_else(|| vec![STAR_STAR]) + } +} + + /// A trait provided to convert a resource's result to json. pub trait ResourceResult { @@ -145,13 +160,11 @@ where mod test { use super::*; - use crate::{OpenapiType, StatusCode}; use futures_executor::block_on; - use mime::{APPLICATION_JSON, TEXT_PLAIN}; use thiserror::Error; #[derive(Debug, Default, Deserialize, Serialize)] - #[cfg_attr(feature = "openapi", derive(OpenapiType))] + #[cfg_attr(feature = "openapi", derive(crate::OpenapiType))] struct Msg { msg : String @@ -162,63 +175,16 @@ mod test struct MsgError; #[test] - fn resource_result_ok() + fn result_from_future() { - let ok : Result = Ok(Msg::default()); - let res = block_on(ok.into_response()).expect("didn't expect error response"); - assert_eq!(res.status, StatusCode::OK); - assert_eq!(res.mime, Some(APPLICATION_JSON)); - assert_eq!(res.full_body().unwrap(), r#"{"msg":""}"#.as_bytes()); - } - - #[test] - fn resource_result_err() - { - let err : Result = Err(MsgError::default()); - let res = block_on(err.into_response()).expect("didn't expect error response"); - assert_eq!(res.status, StatusCode::INTERNAL_SERVER_ERROR); - assert_eq!(res.mime, Some(APPLICATION_JSON)); - assert_eq!(res.full_body().unwrap(), format!(r#"{{"error":true,"message":"{}"}}"#, MsgError::default()).as_bytes()); - } - - #[test] - fn success_always_successfull() - { - let success : Success = Msg::default().into(); - let res = block_on(success.into_response()).expect("didn't expect error response"); - assert_eq!(res.status, StatusCode::OK); - assert_eq!(res.mime, Some(APPLICATION_JSON)); - assert_eq!(res.full_body().unwrap(), r#"{"msg":""}"#.as_bytes()); - } - - #[test] - fn no_content_has_empty_response() - { - let no_content = NoContent::default(); - let res = block_on(no_content.into_response()).expect("didn't expect error response"); - assert_eq!(res.status, StatusCode::NO_CONTENT); - assert_eq!(res.mime, None); - assert_eq!(res.full_body().unwrap(), &[] as &[u8]); - } - - #[test] - fn no_content_result() - { - let no_content : Result = Ok(NoContent::default()); - let res = block_on(no_content.into_response()).expect("didn't expect error response"); - assert_eq!(res.status, StatusCode::NO_CONTENT); - assert_eq!(res.mime, None); - assert_eq!(res.full_body().unwrap(), &[] as &[u8]); - } - - #[test] - fn raw_response() - { - let msg = "Test"; - let raw = Raw::new(msg, TEXT_PLAIN); - let res = block_on(raw.into_response()).expect("didn't expect error response"); - assert_eq!(res.status, StatusCode::OK); - assert_eq!(res.mime, Some(TEXT_PLAIN)); - assert_eq!(res.full_body().unwrap(), msg.as_bytes()); + let nc = NoContent::default(); + let res = block_on(nc.into_response()).unwrap(); + + let fut_nc = async move { NoContent::default() }.boxed(); + let fut_res = block_on(fut_nc.into_response()).unwrap(); + + assert_eq!(res.status, fut_res.status); + assert_eq!(res.mime, fut_res.mime); + assert_eq!(res.full_body().unwrap(), fut_res.full_body().unwrap()); } } diff --git a/gotham_restful/src/result/no_content.rs b/gotham_restful/src/result/no_content.rs index c04ea8d..f733118 100644 --- a/gotham_restful/src/result/no_content.rs +++ b/gotham_restful/src/result/no_content.rs @@ -103,4 +103,38 @@ where { NoContent::default_status() } -} \ No newline at end of file +} + + +#[cfg(test)] +mod test +{ + use super::*; + use futures_executor::block_on; + use gotham::hyper::StatusCode; + use thiserror::Error; + + #[derive(Debug, Default, Error)] + #[error("An Error")] + struct MsgError; + + #[test] + fn no_content_has_empty_response() + { + let no_content = NoContent::default(); + let res = block_on(no_content.into_response()).expect("didn't expect error response"); + assert_eq!(res.status, StatusCode::NO_CONTENT); + assert_eq!(res.mime, None); + assert_eq!(res.full_body().unwrap(), &[] as &[u8]); + } + + #[test] + fn no_content_result() + { + let no_content : Result = Ok(NoContent::default()); + let res = block_on(no_content.into_response()).expect("didn't expect error response"); + assert_eq!(res.status, StatusCode::NO_CONTENT); + assert_eq!(res.mime, None); + assert_eq!(res.full_body().unwrap(), &[] as &[u8]); + } +} diff --git a/gotham_restful/src/result/raw.rs b/gotham_restful/src/result/raw.rs index 95fafdf..f41ffe6 100644 --- a/gotham_restful/src/result/raw.rs +++ b/gotham_restful/src/result/raw.rs @@ -10,10 +10,11 @@ use mime::Mime; use openapiv3::{SchemaKind, StringFormat, StringType, Type, VariantOrUnknownOrEmpty}; use serde_json::error::Error as SerdeJsonError; use std::{ - fmt::{Debug, Display}, + fmt::Display, pin::Pin }; +#[derive(Debug)] pub struct Raw { pub raw : T, @@ -39,13 +40,6 @@ impl Clone for Raw } } -impl Debug for Raw -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Raw({:?}, {:?})", self.raw, self.mime) - } -} - impl> ResourceResult for Raw where Self : Send @@ -87,4 +81,24 @@ where { as ResourceResult>::schema() } -} \ No newline at end of file +} + + +#[cfg(test)] +mod test +{ + use super::*; + use futures_executor::block_on; + use mime::TEXT_PLAIN; + + #[test] + fn raw_response() + { + let msg = "Test"; + let raw = Raw::new(msg, TEXT_PLAIN); + let res = block_on(raw.into_response()).expect("didn't expect error response"); + assert_eq!(res.status, StatusCode::OK); + assert_eq!(res.mime, Some(TEXT_PLAIN)); + assert_eq!(res.full_body().unwrap(), msg.as_bytes()); + } +} diff --git a/gotham_restful/src/result/result.rs b/gotham_restful/src/result/result.rs index 5857812..5de2e44 100644 --- a/gotham_restful/src/result/result.rs +++ b/gotham_restful/src/result/result.rs @@ -56,4 +56,51 @@ where { R::schema() } -} \ No newline at end of file +} + + +#[cfg(test)] +mod test +{ + use super::*; + use crate::result::OrAllTypes; + use futures_executor::block_on; + use thiserror::Error; + + #[derive(Debug, Default, Deserialize, Serialize)] + #[cfg_attr(feature = "openapi", derive(crate::OpenapiType))] + struct Msg + { + msg : String + } + + #[derive(Debug, Default, Error)] + #[error("An Error")] + struct MsgError; + + #[test] + fn result_ok() + { + let ok : Result = Ok(Msg::default()); + let res = block_on(ok.into_response()).expect("didn't expect error response"); + assert_eq!(res.status, StatusCode::OK); + assert_eq!(res.mime, Some(APPLICATION_JSON)); + assert_eq!(res.full_body().unwrap(), r#"{"msg":""}"#.as_bytes()); + } + + #[test] + fn result_err() + { + let err : Result = Err(MsgError::default()); + let res = block_on(err.into_response()).expect("didn't expect error response"); + assert_eq!(res.status, StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!(res.mime, Some(APPLICATION_JSON)); + assert_eq!(res.full_body().unwrap(), format!(r#"{{"error":true,"message":"{}"}}"#, MsgError::default()).as_bytes()); + } + + #[test] + fn success_accepts_json() + { + assert!(>::accepted_types().or_all_types().contains(&APPLICATION_JSON)) + } +} diff --git a/gotham_restful/src/result/success.rs b/gotham_restful/src/result/success.rs index 9931384..11b2f2b 100644 --- a/gotham_restful/src/result/success.rs +++ b/gotham_restful/src/result/success.rs @@ -41,6 +41,7 @@ fn read_all(_state: &mut State) -> Success { # } ``` */ +#[derive(Debug)] pub struct Success(T); impl AsMut for Success @@ -97,13 +98,6 @@ impl Copy for Success { } -impl Debug for Success -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Success({:?})", self.0) - } -} - impl Default for Success { fn default() -> Self @@ -134,3 +128,35 @@ where T::schema() } } + + +#[cfg(test)] +mod test +{ + use super::*; + use crate::result::OrAllTypes; + use futures_executor::block_on; + + #[derive(Debug, Default, Serialize)] + #[cfg_attr(feature = "openapi", derive(crate::OpenapiType))] + struct Msg + { + msg : String + } + + #[test] + fn success_always_successfull() + { + let success : Success = Msg::default().into(); + let res = block_on(success.into_response()).expect("didn't expect error response"); + assert_eq!(res.status, StatusCode::OK); + assert_eq!(res.mime, Some(APPLICATION_JSON)); + assert_eq!(res.full_body().unwrap(), r#"{"msg":""}"#.as_bytes()); + } + + #[test] + fn success_accepts_json() + { + assert!(>::accepted_types().or_all_types().contains(&APPLICATION_JSON)) + } +} From f7157dcf629e5b0f8c61b026ca75ccb0482da959 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 3 May 2020 19:17:55 +0200 Subject: [PATCH 035/170] add some tests for OpenapiBuilder --- example/src/main.rs | 7 ++- gotham_restful/src/lib.rs | 1 + gotham_restful/src/openapi/builder.rs | 77 ++++++++++++++++++++++++--- gotham_restful/src/routing.rs | 8 +-- 4 files changed, 82 insertions(+), 11 deletions(-) diff --git a/example/src/main.rs b/example/src/main.rs index f1d822b..73d892c 100644 --- a/example/src/main.rs +++ b/example/src/main.rs @@ -134,7 +134,12 @@ fn main() ); gotham::start(ADDR, build_router(chain, pipelines, |route| { - route.with_openapi("Users Example".to_owned(), "0.0.1".to_owned(), format!("http://{}", ADDR), |mut route| { + let info = OpenapiInfo { + title: "Users Example".to_owned(), + version: "0.0.1".to_owned(), + urls: vec![format!("http://{}", ADDR)] + }; + route.with_openapi(info, |mut route| { route.resource::("users"); route.resource::("auth"); route.get_openapi("openapi"); diff --git a/gotham_restful/src/lib.rs b/gotham_restful/src/lib.rs index 1142f29..6591105 100644 --- a/gotham_restful/src/lib.rs +++ b/gotham_restful/src/lib.rs @@ -160,6 +160,7 @@ pub mod matcher; mod openapi; #[cfg(feature = "openapi")] pub use openapi::{ + builder::OpenapiInfo, router::GetOpenapi, types::{OpenapiSchema, OpenapiType} }; diff --git a/gotham_restful/src/openapi/builder.rs b/gotham_restful/src/openapi/builder.rs index 88c0ba4..6c3d12b 100644 --- a/gotham_restful/src/openapi/builder.rs +++ b/gotham_restful/src/openapi/builder.rs @@ -6,6 +6,14 @@ use openapiv3::{ }; use std::sync::{Arc, RwLock}; +#[derive(Clone, Debug)] +pub struct OpenapiInfo +{ + pub title : String, + pub version : String, + pub urls : Vec +} + pub struct OpenapiBuilder { pub openapi : Arc> @@ -13,19 +21,19 @@ pub struct OpenapiBuilder impl OpenapiBuilder { - pub fn new(title : String, version : String, url : String) -> Self + pub fn new(info : OpenapiInfo) -> Self { Self { openapi: Arc::new(RwLock::new(OpenAPI { openapi: "3.0.2".to_string(), info: openapiv3::Info { - title, version, + title: info.title, + version: info.version, ..Default::default() }, - servers: vec![Server { - url, - ..Default::default() - }], + servers: info.urls.into_iter() + .map(|url| Server { url, ..Default::default() }) + .collect(), ..Default::default() })) } @@ -94,3 +102,60 @@ impl OpenapiBuilder } } } + + +#[cfg(test)] +#[allow(dead_code)] +mod test +{ + use super::*; + + #[derive(OpenapiType)] + struct Message + { + msg : String + } + + #[derive(OpenapiType)] + struct Messages + { + msgs : Vec + } + + fn info() -> OpenapiInfo + { + OpenapiInfo { + title: "TEST CASE".to_owned(), + version: "1.2.3".to_owned(), + urls: vec!["http://localhost:1234".to_owned(), "https://example.org".to_owned()] + } + } + + fn openapi(builder : OpenapiBuilder) -> OpenAPI + { + Arc::try_unwrap(builder.openapi).unwrap().into_inner().unwrap() + } + + #[test] + fn new_builder() + { + let info = info(); + let builder = OpenapiBuilder::new(info.clone()); + let openapi = openapi(builder); + + assert_eq!(info.title, openapi.info.title); + assert_eq!(info.version, openapi.info.version); + assert_eq!(info.urls.len(), openapi.servers.len()); + } + + #[test] + fn add_schema() + { + let mut builder = OpenapiBuilder::new(info()); + builder.add_schema::>(); + let openapi = openapi(builder); + + assert_eq!(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())); + } +} diff --git a/gotham_restful/src/routing.rs b/gotham_restful/src/routing.rs index ffc9e58..cd8ea13 100644 --- a/gotham_restful/src/routing.rs +++ b/gotham_restful/src/routing.rs @@ -8,7 +8,7 @@ use crate::{ }; #[cfg(feature = "openapi")] use crate::openapi::{ - builder::OpenapiBuilder, + builder::{OpenapiBuilder, OpenapiInfo}, router::OpenapiRouter }; @@ -51,7 +51,7 @@ struct PathExtractor #[cfg(feature = "openapi")] pub trait WithOpenapi { - fn with_openapi(&mut self, title : String, version : String, server_url : String, block : F) + fn with_openapi(&mut self, info : OpenapiInfo, block : F) where F : FnOnce(OpenapiRouter); } @@ -318,13 +318,13 @@ macro_rules! implDrawResourceRoutes { C : PipelineHandleChain

+ Copy + Send + Sync + 'static, P : RefUnwindSafe + Send + Sync + 'static { - fn with_openapi(&mut self, title : String, version : String, server_url : String, block : F) + fn with_openapi(&mut self, info : OpenapiInfo, block : F) where F : FnOnce(OpenapiRouter<$implType<'a, C, P>>) { let router = OpenapiRouter { router: self, - openapi_builder: &mut OpenapiBuilder::new(title, version, server_url) + openapi_builder: &mut OpenapiBuilder::new(info) }; block(router); } From da30f34d972961eb4c3e6fdde8f410095fad480c Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 3 May 2020 23:10:19 +0200 Subject: [PATCH 036/170] use DeriveInput for input into derive macros from syn --- gotham_restful_derive/Cargo.toml | 2 +- gotham_restful_derive/src/from_body.rs | 17 +++++++++---- gotham_restful_derive/src/openapi_type.rs | 31 +++++++++++------------ gotham_restful_derive/src/request_body.rs | 4 +-- gotham_restful_derive/src/resource.rs | 4 +-- 5 files changed, 32 insertions(+), 26 deletions(-) diff --git a/gotham_restful_derive/Cargo.toml b/gotham_restful_derive/Cargo.toml index 1fb4f53..bd7f368 100644 --- a/gotham_restful_derive/Cargo.toml +++ b/gotham_restful_derive/Cargo.toml @@ -22,7 +22,7 @@ lazy_static = "1.4.0" proc-macro2 = "1.0.10" quote = "1.0.3" regex = "1.3.7" -syn = { version = "1.0.17", features = ["extra-traits", "full"] } +syn = "1.0.18" [features] default = [] diff --git a/gotham_restful_derive/src/from_body.rs b/gotham_restful_derive/src/from_body.rs index a027c55..5aa962a 100644 --- a/gotham_restful_derive/src/from_body.rs +++ b/gotham_restful_derive/src/from_body.rs @@ -4,12 +4,14 @@ use quote::{format_ident, quote}; use std::cmp::min; use syn::{ punctuated::Punctuated, + spanned::Spanned, token::Comma, + Data, + DeriveInput, Error, Field, Fields, Ident, - ItemStruct, Type, parse_macro_input }; @@ -50,11 +52,17 @@ impl ParsedFields fn expand(tokens : TokenStream) -> Result { let krate = super::krate(); - let input = parse_macro_input::parse::(tokens)?; + let input = parse_macro_input::parse::(tokens)?; let ident = input.ident; let generics = input.generics; - let fields = match input.fields { + let strukt = match input.data { + Data::Enum(inum) => Err(inum.enum_token.span()), + Data::Struct(strukt) => Ok(strukt), + Data::Union(uni) => Err(uni.union_token.span()) + }.map_err(|span| Error::new(span, "#[derive(FromBody)] only works for enums"))?; + + let fields = match strukt.fields { Fields::Named(named) => ParsedFields::from_named(named.named)?, Fields::Unnamed(unnamed) => ParsedFields::from_unnamed(unnamed.unnamed)?, Fields::Unit => ParsedFields::from_unit()? @@ -115,14 +123,13 @@ fn expand(tokens : TokenStream) -> Result quote!(Self ( #(#field_names),* )) }; - // TODO: Replace the Err type with something more appropriate that implements Display Ok(quote! { impl #generics #krate::FromBody for #ident #generics where #where_clause { type Err = #krate::FromBodyNoError; - fn from_body(#body_ident : #krate::gotham::hyper::body::Bytes, #type_ident : #krate::Mime) -> Result + fn from_body(#body_ident : #krate::gotham::hyper::body::Bytes, #type_ident : #krate::Mime) -> Result { #block Ok(#ctor) diff --git a/gotham_restful_derive/src/openapi_type.rs b/gotham_restful_derive/src/openapi_type.rs index 319972c..355312f 100644 --- a/gotham_restful_derive/src/openapi_type.rs +++ b/gotham_restful_derive/src/openapi_type.rs @@ -6,14 +6,16 @@ use syn::{ spanned::Spanned, Attribute, AttributeArgs, + Data, + DataEnum, + DataStruct, + DeriveInput, Error, Field, Fields, Generics, GenericParam, - Item, - ItemEnum, - ItemStruct, + Ident, Lit, Meta, NestedMeta, @@ -23,13 +25,14 @@ use syn::{ pub fn expand(tokens : TokenStream) -> TokenStream { - let input = parse_macro_input!(tokens as Item); + let input = parse_macro_input!(tokens as DeriveInput); - let output = match input { - Item::Enum(item) => expand_enum(item), - Item::Struct(item) => expand_struct(item), - _ => Err(Error::new(input.span(), "derive(OpenapiType) not supported for this context")) + let output = match (input.ident, input.generics, input.attrs, input.data) { + (ident, generics, attrs, Data::Enum(inum)) => expand_enum(ident, generics, attrs, inum), + (ident, generics, attrs, Data::Struct(strukt)) => expand_struct(ident, generics, attrs, strukt), + (_, _, _, Data::Union(uni)) => Err(Error::new(uni.union_token.span(), "#[derive(OpenapiType)] only works for structs and enums")) }; + output .unwrap_or_else(|err| err.to_compile_error()) .into() @@ -127,14 +130,12 @@ fn expand_variant(variant : &Variant) -> Result }) } -fn expand_enum(input : ItemEnum) -> Result +fn expand_enum(ident : Ident, generics : Generics, attrs : Vec, input : DataEnum) -> Result { let krate = super::krate(); - let ident = input.ident; - let generics = input.generics; let where_clause = expand_where(&generics); - let attrs = parse_attributes(&input.attrs)?; + let attrs = parse_attributes(&attrs)?; let nullable = attrs.nullable; let name = match attrs.rename { Some(rename) => rename, @@ -229,14 +230,12 @@ fn expand_field(field : &Field) -> Result }}) } -pub fn expand_struct(input : ItemStruct) -> Result +pub fn expand_struct(ident : Ident, generics : Generics, attrs : Vec, input : DataStruct) -> Result { let krate = super::krate(); - let ident = input.ident; - let generics = input.generics; let where_clause = expand_where(&generics); - let attrs = parse_attributes(&input.attrs)?; + let attrs = parse_attributes(&attrs)?; let nullable = attrs.nullable; let name = match attrs.rename { Some(rename) => rename, diff --git a/gotham_restful_derive/src/request_body.rs b/gotham_restful_derive/src/request_body.rs index 7bc5d9d..f561a3a 100644 --- a/gotham_restful_derive/src/request_body.rs +++ b/gotham_restful_derive/src/request_body.rs @@ -7,10 +7,10 @@ use syn::{ parse::{Parse, ParseStream, Result as SynResult}, punctuated::Punctuated, token::Comma, + DeriveInput, Error, Generics, Ident, - ItemStruct, Path, parenthesized, parse_macro_input @@ -59,7 +59,7 @@ fn impl_openapi_type(ident : &Ident, generics : &Generics) -> TokenStream2 fn expand(tokens : TokenStream) -> Result { let krate = super::krate(); - let input = parse_macro_input::parse::(tokens)?; + let input = parse_macro_input::parse::(tokens)?; let ident = input.ident; let generics = input.generics; diff --git a/gotham_restful_derive/src/resource.rs b/gotham_restful_derive/src/resource.rs index fe2f47d..e618ebb 100644 --- a/gotham_restful_derive/src/resource.rs +++ b/gotham_restful_derive/src/resource.rs @@ -9,9 +9,9 @@ use syn::{ parse::{Parse, ParseStream}, punctuated::Punctuated, token::Comma, + DeriveInput, Error, Ident, - ItemStruct, parenthesized, parse_macro_input }; @@ -33,7 +33,7 @@ impl Parse for MethodList fn expand(tokens : TokenStream) -> Result { let krate = super::krate(); - let input = parse_macro_input::parse::(tokens)?; + let input = parse_macro_input::parse::(tokens)?; let ident = input.ident; let name = ident.to_string(); From 5e5e3aaf9d1d91c8266edd13fbdee79d2592c4be Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 3 May 2020 23:25:48 +0200 Subject: [PATCH 037/170] don't use syn::token module --- gotham_restful_derive/src/from_body.rs | 20 +++++++++++--------- gotham_restful_derive/src/method.rs | 4 ++-- gotham_restful_derive/src/openapi_type.rs | 4 ++-- gotham_restful_derive/src/request_body.rs | 10 +++++----- gotham_restful_derive/src/resource.rs | 10 +++++----- 5 files changed, 25 insertions(+), 23 deletions(-) diff --git a/gotham_restful_derive/src/from_body.rs b/gotham_restful_derive/src/from_body.rs index 5aa962a..c4fab76 100644 --- a/gotham_restful_derive/src/from_body.rs +++ b/gotham_restful_derive/src/from_body.rs @@ -3,17 +3,15 @@ use proc_macro2::TokenStream as TokenStream2; use quote::{format_ident, quote}; use std::cmp::min; use syn::{ - punctuated::Punctuated, + parse_macro_input, spanned::Spanned, - token::Comma, Data, DeriveInput, Error, Field, Fields, Ident, - Type, - parse_macro_input + Type }; pub fn expand_from_body(tokens : TokenStream) -> TokenStream @@ -31,13 +29,17 @@ struct ParsedFields impl ParsedFields { - fn from_named(fields : Punctuated) -> Result + fn from_named(fields : I) -> Result + where + I : Iterator { - let fields = fields.into_iter().map(|field| (field.ident.unwrap(), field.ty)).collect(); + let fields = fields.map(|field| (field.ident.unwrap(), field.ty)).collect(); Ok(Self { fields, named: true }) } - fn from_unnamed(fields : Punctuated) -> Result + fn from_unnamed(fields : I) -> Result + where + I : Iterator { let fields = fields.into_iter().enumerate().map(|(i, field)| (format_ident!("arg{}", i), field.ty)).collect(); Ok(Self { fields, named: false }) @@ -63,8 +65,8 @@ fn expand(tokens : TokenStream) -> Result }.map_err(|span| Error::new(span, "#[derive(FromBody)] only works for enums"))?; let fields = match strukt.fields { - Fields::Named(named) => ParsedFields::from_named(named.named)?, - Fields::Unnamed(unnamed) => ParsedFields::from_unnamed(unnamed.unnamed)?, + Fields::Named(named) => ParsedFields::from_named(named.named.into_iter())?, + Fields::Unnamed(unnamed) => ParsedFields::from_unnamed(unnamed.unnamed.into_iter())?, Fields::Unit => ParsedFields::from_unit()? }; diff --git a/gotham_restful_derive/src/method.rs b/gotham_restful_derive/src/method.rs index db0b8b8..580ea75 100644 --- a/gotham_restful_derive/src/method.rs +++ b/gotham_restful_derive/src/method.rs @@ -4,6 +4,7 @@ use proc_macro::TokenStream; use proc_macro2::{Ident, Span, TokenStream as TokenStream2}; use quote::{format_ident, quote}; use syn::{ + parse_macro_input, spanned::Spanned, Attribute, AttributeArgs, @@ -16,8 +17,7 @@ use syn::{ NestedMeta, PatType, ReturnType, - Type, - parse_macro_input + Type }; use std::str::FromStr; diff --git a/gotham_restful_derive/src/openapi_type.rs b/gotham_restful_derive/src/openapi_type.rs index 355312f..a65e880 100644 --- a/gotham_restful_derive/src/openapi_type.rs +++ b/gotham_restful_derive/src/openapi_type.rs @@ -3,6 +3,7 @@ use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use quote::quote; use syn::{ + parse_macro_input, spanned::Spanned, Attribute, AttributeArgs, @@ -19,8 +20,7 @@ use syn::{ Lit, Meta, NestedMeta, - Variant, - parse_macro_input + Variant }; pub fn expand(tokens : TokenStream) -> TokenStream diff --git a/gotham_restful_derive/src/request_body.rs b/gotham_restful_derive/src/request_body.rs index f561a3a..ea6ae4d 100644 --- a/gotham_restful_derive/src/request_body.rs +++ b/gotham_restful_derive/src/request_body.rs @@ -4,19 +4,19 @@ use proc_macro2::TokenStream as TokenStream2; use quote::quote; use std::iter; use syn::{ + parenthesized, + parse_macro_input, parse::{Parse, ParseStream, Result as SynResult}, punctuated::Punctuated, - token::Comma, DeriveInput, Error, Generics, Ident, Path, - parenthesized, - parse_macro_input + Token }; -struct MimeList(Punctuated); +struct MimeList(Punctuated); impl Parse for MimeList { @@ -24,7 +24,7 @@ impl Parse for MimeList { let content; let _paren = parenthesized!(content in input); - let list : Punctuated = Punctuated::parse_separated_nonempty(&content)?; + let list = Punctuated::parse_separated_nonempty(&content)?; Ok(Self(list)) } } diff --git a/gotham_restful_derive/src/resource.rs b/gotham_restful_derive/src/resource.rs index e618ebb..eade96e 100644 --- a/gotham_restful_derive/src/resource.rs +++ b/gotham_restful_derive/src/resource.rs @@ -6,18 +6,18 @@ use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use quote::quote; use syn::{ + parenthesized, + parse_macro_input, parse::{Parse, ParseStream}, punctuated::Punctuated, - token::Comma, DeriveInput, Error, Ident, - parenthesized, - parse_macro_input + Token }; use std::{iter, str::FromStr}; -struct MethodList(Punctuated); +struct MethodList(Punctuated); impl Parse for MethodList { @@ -25,7 +25,7 @@ impl Parse for MethodList { let content; let _paren = parenthesized!(content in input); - let list : Punctuated = Punctuated::parse_separated_nonempty(&content)?; + let list = Punctuated::parse_separated_nonempty(&content)?; Ok(Self(list)) } } From 992d9be195cddf054c717413ec8b4658f8f9e04b Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 3 May 2020 23:43:42 +0200 Subject: [PATCH 038/170] use less non-public syn api --- gotham_restful_derive/src/from_body.rs | 3 +-- gotham_restful_derive/src/method.rs | 10 ++++------ gotham_restful_derive/src/openapi_type.rs | 1 + gotham_restful_derive/src/request_body.rs | 3 +-- gotham_restful_derive/src/resource.rs | 3 +-- gotham_restful_derive/src/resource_error.rs | 4 ++-- 6 files changed, 10 insertions(+), 14 deletions(-) diff --git a/gotham_restful_derive/src/from_body.rs b/gotham_restful_derive/src/from_body.rs index c4fab76..9b7aa38 100644 --- a/gotham_restful_derive/src/from_body.rs +++ b/gotham_restful_derive/src/from_body.rs @@ -3,7 +3,6 @@ use proc_macro2::TokenStream as TokenStream2; use quote::{format_ident, quote}; use std::cmp::min; use syn::{ - parse_macro_input, spanned::Spanned, Data, DeriveInput, @@ -54,7 +53,7 @@ impl ParsedFields fn expand(tokens : TokenStream) -> Result { let krate = super::krate(); - let input = parse_macro_input::parse::(tokens)?; + let input : DeriveInput = syn::parse(tokens)?; let ident = input.ident; let generics = input.generics; diff --git a/gotham_restful_derive/src/method.rs b/gotham_restful_derive/src/method.rs index 580ea75..53fac96 100644 --- a/gotham_restful_derive/src/method.rs +++ b/gotham_restful_derive/src/method.rs @@ -151,10 +151,7 @@ impl MethodArgumentType fn quote_ty(&self) -> Option { match self { - Self::MethodArg(ty) => Some(quote!(#ty)), - Self::DatabaseConnection(ty) => Some(quote!(#ty)), - Self::AuthStatus(ty) => Some(quote!(#ty)), - Self::AuthStatusRef(ty) => Some(quote!(#ty)), + Self::MethodArg(ty) | Self::DatabaseConnection(ty) | Self::AuthStatus(ty) | Self::AuthStatusRef(ty) => Some(quote!(#ty)), _ => None } } @@ -280,7 +277,8 @@ fn expand(method : Method, attrs : TokenStream, item : TokenStream) -> Result(attrs)?; + // TODO this is not public api but syn currently doesn't offer another convenient way to parse AttributeArgs + let mut method_attrs : AttributeArgs = parse_macro_input::parse(attrs)?; let resource_path = match method_attrs.remove(0) { NestedMeta::Meta(Meta::Path(path)) => path, p => return Err(Error::new(p.span(), "Expected name of the Resource struct this method belongs to")) @@ -288,7 +286,7 @@ fn expand(method : Method, attrs : TokenStream, item : TokenStream) -> Result(item)?; + let fun : ItemFn = syn::parse(item)?; let fun_ident = &fun.sig.ident; let fun_vis = &fun.vis; let fun_is_async = fun.sig.asyncness.is_some(); diff --git a/gotham_restful_derive/src/openapi_type.rs b/gotham_restful_derive/src/openapi_type.rs index a65e880..62e51ca 100644 --- a/gotham_restful_derive/src/openapi_type.rs +++ b/gotham_restful_derive/src/openapi_type.rs @@ -90,6 +90,7 @@ fn parse_attributes(input : &[Attribute]) -> Result if attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("openapi".to_owned()) { let tokens = remove_parens(attr.tokens.clone()); + // TODO this is not public api but syn currently doesn't offer another convenient way to parse AttributeArgs let nested = parse_macro_input::parse::(tokens.into())?; for meta in nested { diff --git a/gotham_restful_derive/src/request_body.rs b/gotham_restful_derive/src/request_body.rs index ea6ae4d..57909b3 100644 --- a/gotham_restful_derive/src/request_body.rs +++ b/gotham_restful_derive/src/request_body.rs @@ -5,7 +5,6 @@ use quote::quote; use std::iter; use syn::{ parenthesized, - parse_macro_input, parse::{Parse, ParseStream, Result as SynResult}, punctuated::Punctuated, DeriveInput, @@ -59,7 +58,7 @@ fn impl_openapi_type(ident : &Ident, generics : &Generics) -> TokenStream2 fn expand(tokens : TokenStream) -> Result { let krate = super::krate(); - let input = parse_macro_input::parse::(tokens)?; + let input : DeriveInput = syn::parse(tokens)?; let ident = input.ident; let generics = input.generics; diff --git a/gotham_restful_derive/src/resource.rs b/gotham_restful_derive/src/resource.rs index eade96e..23f0171 100644 --- a/gotham_restful_derive/src/resource.rs +++ b/gotham_restful_derive/src/resource.rs @@ -7,7 +7,6 @@ use proc_macro2::TokenStream as TokenStream2; use quote::quote; use syn::{ parenthesized, - parse_macro_input, parse::{Parse, ParseStream}, punctuated::Punctuated, DeriveInput, @@ -33,7 +32,7 @@ impl Parse for MethodList fn expand(tokens : TokenStream) -> Result { let krate = super::krate(); - let input = parse_macro_input::parse::(tokens)?; + let input : DeriveInput = syn::parse(tokens)?; let ident = input.ident; let name = ident.to_string(); diff --git a/gotham_restful_derive/src/resource_error.rs b/gotham_restful_derive/src/resource_error.rs index 4958123..2d4c341 100644 --- a/gotham_restful_derive/src/resource_error.rs +++ b/gotham_restful_derive/src/resource_error.rs @@ -45,7 +45,7 @@ fn process_variant(variant : Variant) -> Result let status = match variant.attrs.iter() .find(|attr| attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("status".to_string())) { - Some(attr) => Some(parse_macro_input::parse::(remove_parens(attr.tokens.clone()).into())?), + Some(attr) => Some(syn::parse2(remove_parens(attr.tokens.clone()))?), None => None }; @@ -85,7 +85,7 @@ fn process_variant(variant : Variant) -> Result let display = match variant.attrs.iter() .find(|attr| attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("display".to_string())) { - Some(attr) => Some(parse_macro_input::parse::(remove_parens(attr.tokens.clone()).into())?), + Some(attr) => Some(syn::parse2(remove_parens(attr.tokens.clone()))?), None => None }; From 328ebf821e18f415a07c8fe67993ac4a2eb2a513 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 3 May 2020 18:21:50 +0200 Subject: [PATCH 039/170] some minor improvements --- .gitlab-ci.yml | 2 +- example/src/main.rs | 2 +- gotham_restful/src/auth.rs | 9 ++++++++- gotham_restful/src/lib.rs | 2 ++ gotham_restful/src/matcher/accept.rs | 2 +- gotham_restful/src/matcher/content_type.rs | 2 +- gotham_restful/src/matcher/mod.rs | 16 +++++++--------- gotham_restful/src/openapi/router.rs | 1 + gotham_restful/src/response.rs | 1 + gotham_restful/src/result/auth_result.rs | 4 ++-- gotham_restful/src/result/mod.rs | 1 + gotham_restful/src/result/no_content.rs | 2 +- gotham_restful/src/routing.rs | 4 ++-- 13 files changed, 29 insertions(+), 19 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1d4bf0e..18413f8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -26,7 +26,7 @@ test-all: - cargo -V script: - cargo test --workspace --all-features --doc - - cargo tarpaulin --all --all-features --exclude-files 'cargo/*' --exclude-files 'gotham_restful_derive/*' --exclude-files 'example/*' --ignore-panics --ignore-tests --out Html -v + - cargo tarpaulin --target-dir target/tarpaulin --all --all-features --exclude-files 'cargo/*' --exclude-files 'gotham_restful_derive/*' --exclude-files 'example/*' --ignore-panics --ignore-tests --out Html -v artifacts: paths: - tarpaulin-report.html diff --git a/example/src/main.rs b/example/src/main.rs index 73d892c..d062710 100644 --- a/example/src/main.rs +++ b/example/src/main.rs @@ -92,7 +92,7 @@ fn delete(id : u64) fn auth_read_all(auth : AuthStatus<()>) -> AuthSuccess { match auth { - AuthStatus::Authenticated(data) => Ok(format!("{:?}", data).into()), + AuthStatus::Authenticated(data) => Ok(format!("{:?}", data)), _ => Err(Forbidden) } } diff --git a/gotham_restful/src/auth.rs b/gotham_restful/src/auth.rs index 94c95fd..c109f07 100644 --- a/gotham_restful/src/auth.rs +++ b/gotham_restful/src/auth.rs @@ -52,8 +52,14 @@ where } } +impl Copy for AuthStatus +where + T : Copy + Send + 'static +{ +} + /// The source of the authentication token in the request. -#[derive(Clone, StateData)] +#[derive(Clone, Debug, StateData)] pub enum AuthSource { /// Take the token from a cookie with the given name. @@ -155,6 +161,7 @@ fn main() { } ``` */ +#[derive(Debug)] pub struct AuthMiddleware { source : AuthSource, diff --git a/gotham_restful/src/lib.rs b/gotham_restful/src/lib.rs index 6591105..c96e905 100644 --- a/gotham_restful/src/lib.rs +++ b/gotham_restful/src/lib.rs @@ -1,4 +1,6 @@ #![allow(clippy::tabs_in_doc_comments)] +#![warn(missing_debug_implementations, rust_2018_idioms)] +#![deny(intra_doc_link_resolution_failure)] /*! This crate is an extension to the popular [gotham web framework][gotham] for Rust. The idea is to have several RESTful resources that can be added to the gotham router. This crate will take care diff --git a/gotham_restful/src/matcher/accept.rs b/gotham_restful/src/matcher/accept.rs index 5c1ee4e..2e89ac2 100644 --- a/gotham_restful/src/matcher/accept.rs +++ b/gotham_restful/src/matcher/accept.rs @@ -86,7 +86,7 @@ route.post("/foo") # }); ``` */ -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct AcceptHeaderMatcher { types : Vec, diff --git a/gotham_restful/src/matcher/content_type.rs b/gotham_restful/src/matcher/content_type.rs index cb55571..d33ed1a 100644 --- a/gotham_restful/src/matcher/content_type.rs +++ b/gotham_restful/src/matcher/content_type.rs @@ -34,7 +34,7 @@ route.post("/foo") # }); ``` */ -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct ContentTypeMatcher { types : Vec, diff --git a/gotham_restful/src/matcher/mod.rs b/gotham_restful/src/matcher/mod.rs index e1029e3..4d5268e 100644 --- a/gotham_restful/src/matcher/mod.rs +++ b/gotham_restful/src/matcher/mod.rs @@ -21,17 +21,15 @@ impl LookupTableFromTypes for LookupTable { if include_stars { - types + return types .enumerate() .flat_map(|(i, mime)| vec![("*/*".to_owned(), i), (format!("{}/*", mime.type_()), i), (mime.essence_str().to_owned(), i)].into_iter()) - .into_group_map() - } - else - { - types - .enumerate() - .map(|(i, mime)| (mime.essence_str().to_owned(), i)) - .into_group_map() + .into_group_map(); } + + types + .enumerate() + .map(|(i, mime)| (mime.essence_str().to_owned(), i)) + .into_group_map() } } diff --git a/gotham_restful/src/openapi/router.rs b/gotham_restful/src/openapi/router.rs index 33f4cba..1fa3ccf 100644 --- a/gotham_restful/src/openapi/router.rs +++ b/gotham_restful/src/openapi/router.rs @@ -16,6 +16,7 @@ pub trait GetOpenapi fn get_openapi(&mut self, path : &str); } +#[derive(Debug)] pub struct OpenapiRouter<'a, D> { pub router : &'a mut D, diff --git a/gotham_restful/src/response.rs b/gotham_restful/src/response.rs index ee4e1f3..dbbf8c7 100644 --- a/gotham_restful/src/response.rs +++ b/gotham_restful/src/response.rs @@ -2,6 +2,7 @@ use gotham::hyper::{Body, StatusCode}; use mime::{Mime, APPLICATION_JSON}; /// A response, used to create the final gotham response from. +#[derive(Debug)] pub struct Response { pub status : StatusCode, diff --git a/gotham_restful/src/result/auth_result.rs b/gotham_restful/src/result/auth_result.rs index 86db9d3..b2ebbe1 100644 --- a/gotham_restful/src/result/auth_result.rs +++ b/gotham_restful/src/result/auth_result.rs @@ -8,7 +8,7 @@ combination with [`AuthSuccess`] or [`AuthResult`]. [`AuthSuccess`]: type.AuthSuccess.html [`AuthResult`]: type.AuthResult.html */ -#[derive(ResourceError)] +#[derive(Debug, Clone, Copy, ResourceError)] pub enum AuthError { #[status(FORBIDDEN)] @@ -54,7 +54,7 @@ error, or delegates to another error type. This type is best used with [`AuthRes [`AuthResult`]: type.AuthResult.html */ -#[derive(ResourceError)] +#[derive(Debug, ResourceError)] pub enum AuthErrorOrOther { #[status(UNAUTHORIZED)] diff --git a/gotham_restful/src/result/mod.rs b/gotham_restful/src/result/mod.rs index 834bf46..547ae42 100644 --- a/gotham_restful/src/result/mod.rs +++ b/gotham_restful/src/result/mod.rs @@ -20,6 +20,7 @@ pub use no_content::NoContent; mod raw; pub use raw::Raw; +//#[allow(clippy::module_inception)] mod result; pub use result::IntoResponseError; diff --git a/gotham_restful/src/result/no_content.rs b/gotham_restful/src/result/no_content.rs index f733118..0011a67 100644 --- a/gotham_restful/src/result/no_content.rs +++ b/gotham_restful/src/result/no_content.rs @@ -31,7 +31,7 @@ fn read_all(_state: &mut State) { # } ``` */ -#[derive(Clone, Copy, Default)] +#[derive(Clone, Copy, Debug, Default)] pub struct NoContent; impl From<()> for NoContent diff --git a/gotham_restful/src/routing.rs b/gotham_restful/src/routing.rs index cd8ea13..3e67e09 100644 --- a/gotham_restful/src/routing.rs +++ b/gotham_restful/src/routing.rs @@ -53,7 +53,7 @@ pub trait WithOpenapi { fn with_openapi(&mut self, info : OpenapiInfo, block : F) where - F : FnOnce(OpenapiRouter); + F : FnOnce(OpenapiRouter<'_, D>); } /// This trait adds the `resource` method to gotham's routing. It allows you to register @@ -320,7 +320,7 @@ macro_rules! implDrawResourceRoutes { { fn with_openapi(&mut self, info : OpenapiInfo, block : F) where - F : FnOnce(OpenapiRouter<$implType<'a, C, P>>) + F : FnOnce(OpenapiRouter<'_, $implType<'a, C, P>>) { let router = OpenapiRouter { router: self, From 7ef964b0a0e01c1967ed27de8f1b3584ce9ef6fb Mon Sep 17 00:00:00 2001 From: Dominic Date: Mon, 4 May 2020 00:27:14 +0200 Subject: [PATCH 040/170] fix --- gotham_restful/src/openapi/builder.rs | 1 + gotham_restful/src/result/mod.rs | 2 +- gotham_restful_derive/src/from_body.rs | 2 +- gotham_restful_derive/src/resource_error.rs | 4 ++-- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/gotham_restful/src/openapi/builder.rs b/gotham_restful/src/openapi/builder.rs index 6c3d12b..0ca4cb1 100644 --- a/gotham_restful/src/openapi/builder.rs +++ b/gotham_restful/src/openapi/builder.rs @@ -14,6 +14,7 @@ pub struct OpenapiInfo pub urls : Vec } +#[derive(Debug)] pub struct OpenapiBuilder { pub openapi : Arc> diff --git a/gotham_restful/src/result/mod.rs b/gotham_restful/src/result/mod.rs index 547ae42..314b9ac 100644 --- a/gotham_restful/src/result/mod.rs +++ b/gotham_restful/src/result/mod.rs @@ -20,7 +20,7 @@ pub use no_content::NoContent; mod raw; pub use raw::Raw; -//#[allow(clippy::module_inception)] +#[allow(clippy::module_inception)] mod result; pub use result::IntoResponseError; diff --git a/gotham_restful_derive/src/from_body.rs b/gotham_restful_derive/src/from_body.rs index 9b7aa38..b3247e7 100644 --- a/gotham_restful_derive/src/from_body.rs +++ b/gotham_restful_derive/src/from_body.rs @@ -40,7 +40,7 @@ impl ParsedFields where I : Iterator { - let fields = fields.into_iter().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(); Ok(Self { fields, named: false }) } diff --git a/gotham_restful_derive/src/resource_error.rs b/gotham_restful_derive/src/resource_error.rs index 2d4c341..7ebc738 100644 --- a/gotham_restful_derive/src/resource_error.rs +++ b/gotham_restful_derive/src/resource_error.rs @@ -150,7 +150,7 @@ impl ErrorVariant // the status might be relative to StatusCode, so let's fix that if status.leading_colon.is_none() && status.segments.len() < 2 { - let status_ident = status.segments.first().map(|path| path.clone()).unwrap_or_else(|| path_segment("OK")); + let status_ident = status.segments.first().cloned().unwrap_or_else(|| path_segment("OK")); Path { leading_colon: Some(Default::default()), segments: vec![path_segment("gotham_restful"), path_segment("gotham"), path_segment("hyper"), path_segment("StatusCode"), status_ident].into_iter().collect() @@ -199,7 +199,7 @@ fn expand(tokens : TokenStream) -> Result 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(|variant| process_variant(variant)) + .map(process_variant) .collect_to_result()?; let display_impl = if variants.iter().any(|v| v.display.is_none()) { None } else { From 110ef2be7aac19292439f625058ab956a624adc1 Mon Sep 17 00:00:00 2001 From: Dominic Date: Mon, 4 May 2020 19:08:22 +0200 Subject: [PATCH 041/170] simplify derive/macro code --- gotham_restful_derive/src/from_body.rs | 20 ++----- gotham_restful_derive/src/lib.rs | 64 ++++++++++++++------- gotham_restful_derive/src/method.rs | 48 ++++++---------- gotham_restful_derive/src/openapi_type.rs | 35 +++++------ gotham_restful_derive/src/request_body.rs | 26 +++------ gotham_restful_derive/src/resource.rs | 24 ++------ gotham_restful_derive/src/resource_error.rs | 28 +++------ gotham_restful_derive/src/util.rs | 8 +-- 8 files changed, 108 insertions(+), 145 deletions(-) diff --git a/gotham_restful_derive/src/from_body.rs b/gotham_restful_derive/src/from_body.rs index b3247e7..e57c14c 100644 --- a/gotham_restful_derive/src/from_body.rs +++ b/gotham_restful_derive/src/from_body.rs @@ -1,5 +1,4 @@ -use proc_macro::TokenStream; -use proc_macro2::TokenStream as TokenStream2; +use proc_macro2::TokenStream; use quote::{format_ident, quote}; use std::cmp::min; use syn::{ @@ -10,16 +9,10 @@ use syn::{ Field, Fields, Ident, + Result, Type }; -pub fn expand_from_body(tokens : TokenStream) -> TokenStream -{ - expand(tokens) - .unwrap_or_else(|err| err.to_compile_error()) - .into() -} - struct ParsedFields { fields : Vec<(Ident, Type)>, @@ -28,7 +21,7 @@ struct ParsedFields impl ParsedFields { - fn from_named(fields : I) -> Result + fn from_named(fields : I) -> Result where I : Iterator { @@ -36,7 +29,7 @@ impl ParsedFields Ok(Self { fields, named: true }) } - fn from_unnamed(fields : I) -> Result + fn from_unnamed(fields : I) -> Result where I : Iterator { @@ -44,16 +37,15 @@ impl ParsedFields Ok(Self { fields, named: false }) } - fn from_unit() -> Result + fn from_unit() -> Result { Ok(Self { fields: Vec::new(), named: false }) } } -fn expand(tokens : TokenStream) -> Result +pub fn expand_from_body(input : DeriveInput) -> Result { let krate = super::krate(); - let input : DeriveInput = syn::parse(tokens)?; let ident = input.ident; let generics = input.generics; diff --git a/gotham_restful_derive/src/lib.rs b/gotham_restful_derive/src/lib.rs index 78b8796..66cd764 100644 --- a/gotham_restful_derive/src/lib.rs +++ b/gotham_restful_derive/src/lib.rs @@ -1,6 +1,7 @@ use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use quote::quote; +use syn::{parse_macro_input, parse_macro_input::ParseMacroInput, DeriveInput, Result}; mod util; @@ -16,95 +17,118 @@ mod resource_error; use resource_error::expand_resource_error; #[cfg(feature = "openapi")] mod openapi_type; +#[cfg(feature = "openapi")] +use openapi_type::expand_openapi_type; #[inline] -fn print_tokens(tokens : TokenStream) -> TokenStream +fn print_tokens(tokens : TokenStream2) -> TokenStream { //eprintln!("{}", tokens); - tokens + tokens.into() } +#[inline] +fn expand_derive(input : TokenStream, expand : F) -> TokenStream +where + F : FnOnce(DeriveInput) -> Result +{ + print_tokens(expand(parse_macro_input!(input)) + .unwrap_or_else(|err| err.to_compile_error())) +} + +#[inline] +fn expand_macro(attrs : TokenStream, item : TokenStream, expand : F) -> TokenStream +where + F : FnOnce(A, I) -> Result, + A : ParseMacroInput, + I : ParseMacroInput +{ + print_tokens(expand(parse_macro_input!(attrs), parse_macro_input!(item)) + .unwrap_or_else(|err| err.to_compile_error())) +} + +#[inline] fn krate() -> TokenStream2 { quote!(::gotham_restful) } #[proc_macro_derive(FromBody)] -pub fn derive_from_body(tokens : TokenStream) -> TokenStream +pub fn derive_from_body(input : TokenStream) -> TokenStream { - print_tokens(expand_from_body(tokens)) + expand_derive(input, expand_from_body) } #[cfg(feature = "openapi")] #[proc_macro_derive(OpenapiType, attributes(openapi))] -pub fn derive_openapi_type(tokens : TokenStream) -> TokenStream +pub fn derive_openapi_type(input : TokenStream) -> TokenStream { - print_tokens(openapi_type::expand(tokens)) + expand_derive(input, expand_openapi_type) } #[proc_macro_derive(RequestBody, attributes(supported_types))] -pub fn derive_request_body(tokens : TokenStream) -> TokenStream +pub fn derive_request_body(input : TokenStream) -> TokenStream { - print_tokens(expand_request_body(tokens)) + expand_derive(input, expand_request_body) } #[proc_macro_derive(Resource, attributes(rest_resource))] -pub fn derive_resource(tokens : TokenStream) -> TokenStream +pub fn derive_resource(input : TokenStream) -> TokenStream { - print_tokens(expand_resource(tokens)) + expand_derive(input, expand_resource) } #[proc_macro_derive(ResourceError, attributes(display, from, status))] -pub fn derive_resource_error(tokens : TokenStream) -> TokenStream +pub fn derive_resource_error(input : TokenStream) -> TokenStream { - print_tokens(expand_resource_error(tokens)) + expand_derive(input, expand_resource_error) } #[proc_macro_attribute] pub fn rest_read_all(attr : TokenStream, item : TokenStream) -> TokenStream { - print_tokens(expand_method(Method::ReadAll, attr, item)) + expand_macro(attr, item, |attr, item| expand_method(Method::ReadAll, attr, item)) } #[proc_macro_attribute] pub fn rest_read(attr : TokenStream, item : TokenStream) -> TokenStream { - print_tokens(expand_method(Method::Read, attr, item)) + expand_macro(attr, item, |attr, item| expand_method(Method::Read, attr, item)) } #[proc_macro_attribute] pub fn rest_search(attr : TokenStream, item : TokenStream) -> TokenStream { - print_tokens(expand_method(Method::Search, attr, item)) + expand_macro(attr, item, |attr, item| expand_method(Method::Search, attr, item)) } #[proc_macro_attribute] pub fn rest_create(attr : TokenStream, item : TokenStream) -> TokenStream { - print_tokens(expand_method(Method::Create, attr, item)) + expand_macro(attr, item, |attr, item| expand_method(Method::Create, attr, item)) } #[proc_macro_attribute] pub fn rest_update_all(attr : TokenStream, item : TokenStream) -> TokenStream { - print_tokens(expand_method(Method::UpdateAll, attr, item)) + expand_macro(attr, item, |attr, item| expand_method(Method::UpdateAll, attr, item)) } #[proc_macro_attribute] pub fn rest_update(attr : TokenStream, item : TokenStream) -> TokenStream { - print_tokens(expand_method(Method::Update, attr, item)) + expand_macro(attr, item, |attr, item| expand_method(Method::Update, attr, item)) } #[proc_macro_attribute] pub fn rest_delete_all(attr : TokenStream, item : TokenStream) -> TokenStream { - print_tokens(expand_method(Method::DeleteAll, attr, item)) + expand_macro(attr, item, |attr, item| expand_method(Method::DeleteAll, attr, item)) } #[proc_macro_attribute] pub fn rest_delete(attr : TokenStream, item : TokenStream) -> TokenStream { - print_tokens(expand_method(Method::Delete, attr, item)) + expand_macro(attr, item, |attr, item| expand_method(Method::Delete, attr, item)) } diff --git a/gotham_restful_derive/src/method.rs b/gotham_restful_derive/src/method.rs index 53fac96..d93c28e 100644 --- a/gotham_restful_derive/src/method.rs +++ b/gotham_restful_derive/src/method.rs @@ -1,10 +1,8 @@ use crate::util::CollectToResult; use heck::{CamelCase, SnakeCase}; -use proc_macro::TokenStream; -use proc_macro2::{Ident, Span, TokenStream as TokenStream2}; +use proc_macro2::{Ident, Span, TokenStream}; use quote::{format_ident, quote}; use syn::{ - parse_macro_input, spanned::Spanned, Attribute, AttributeArgs, @@ -16,6 +14,7 @@ use syn::{ Meta, NestedMeta, PatType, + Result, ReturnType, Type }; @@ -35,8 +34,9 @@ pub enum Method impl FromStr for Method { - type Err = String; - fn from_str(str : &str) -> Result + type Err = Error; + + fn from_str(str : &str) -> Result { match str { "ReadAll" | "read_all" => Ok(Self::ReadAll), @@ -47,7 +47,7 @@ impl FromStr for Method "Update" | "update" => Ok(Self::Update), "DeleteAll" | "delete_all" => Ok(Self::DeleteAll), "Delete" | "delete" => Ok(Self::Delete), - _ => Err(format!("Unknown method: `{}'", str)) + _ => Err(Error::new(Span::call_site(), format!("Unknown method: `{}'", str))) } } } @@ -148,7 +148,7 @@ impl MethodArgumentType matches!(self, Self::AuthStatus(_) | Self::AuthStatusRef(_)) } - fn quote_ty(&self) -> Option + fn quote_ty(&self) -> Option { match self { Self::MethodArg(ty) | Self::DatabaseConnection(ty) | Self::AuthStatus(ty) | Self::AuthStatusRef(ty) => Some(quote!(#ty)), @@ -172,7 +172,7 @@ impl Spanned for MethodArgument } } -fn interpret_arg_ty(attrs : &[Attribute], name : &str, ty : Type) -> Result +fn interpret_arg_ty(attrs : &[Attribute], name : &str, ty : Type) -> Result { let attr = attrs.iter() .find(|arg| arg.path.segments.iter().any(|path| &path.ident.to_string() == "rest_arg")) @@ -206,7 +206,7 @@ fn interpret_arg_ty(attrs : &[Attribute], name : &str, ty : Type) -> Result Result +fn interpret_arg(index : usize, arg : &PatType) -> Result { let pat = &arg.pat; let ident = format_ident!("arg{}", index); @@ -217,7 +217,7 @@ fn interpret_arg(index : usize, arg : &PatType) -> Result } #[cfg(feature = "openapi")] -fn expand_operation_id(attrs : &[NestedMeta]) -> TokenStream2 +fn expand_operation_id(attrs : &[NestedMeta]) -> TokenStream { let mut operation_id : Option<&Lit> = None; for meta in attrs @@ -243,12 +243,12 @@ fn expand_operation_id(attrs : &[NestedMeta]) -> TokenStream2 } #[cfg(not(feature = "openapi"))] -fn expand_operation_id(_ : &[NestedMeta]) -> TokenStream2 +fn expand_operation_id(_ : &[NestedMeta]) -> TokenStream { quote!() } -fn expand_wants_auth(attrs : &[NestedMeta], default : bool) -> TokenStream2 +fn expand_wants_auth(attrs : &[NestedMeta], default : bool) -> TokenStream { let default_lit = Lit::Bool(LitBool { value: default, span: Span::call_site() }); let mut wants_auth = &default_lit; @@ -272,21 +272,18 @@ fn expand_wants_auth(attrs : &[NestedMeta], default : bool) -> TokenStream2 } #[allow(clippy::comparison_chain)] -fn expand(method : Method, attrs : TokenStream, item : TokenStream) -> Result +pub fn expand_method(method : Method, mut attrs : AttributeArgs, fun : ItemFn) -> Result { let krate = super::krate(); // parse attributes - // TODO this is not public api but syn currently doesn't offer another convenient way to parse AttributeArgs - let mut method_attrs : AttributeArgs = parse_macro_input::parse(attrs)?; - let resource_path = match method_attrs.remove(0) { + let resource_path = match attrs.remove(0) { NestedMeta::Meta(Meta::Path(path)) => path, p => return Err(Error::new(p.span(), "Expected name of the Resource struct this method belongs to")) }; let resource_name = resource_path.segments.last().map(|s| s.ident.to_string()) .ok_or_else(|| Error::new(resource_path.span(), "Resource name must not be empty"))?; - let fun : ItemFn = syn::parse(item)?; let fun_ident = &fun.sig.ident; let fun_vis = &fun.vis; let fun_is_async = fun.sig.asyncness.is_some(); @@ -337,7 +334,7 @@ fn expand(method : Method, attrs : TokenStream, item : TokenStream) -> Result = generics_args.iter() + let generics : Vec = generics_args.iter() .map(|arg| arg.ty.quote_ty().unwrap()) .zip(ty_names) .map(|(arg, name)| { @@ -347,7 +344,7 @@ fn expand(method : Method, attrs : TokenStream, item : TokenStream) -> Result = args.iter() + let mut args_def : Vec = args.iter() .filter(|arg| (*arg).ty.is_method_arg()) .map(|arg| { let ident = &arg.ident; @@ -357,7 +354,7 @@ fn expand(method : Method, attrs : TokenStream, item : TokenStream) -> Result = args.iter().map(|arg| match (&arg.ty, &arg.ident) { + let args_pass : Vec = args.iter().map(|arg| match (&arg.ty, &arg.ident) { (MethodArgumentType::StateRef, _) => quote!(&#state_ident), (MethodArgumentType::StateMutRef, _) => quote!(&mut #state_ident), (MethodArgumentType::MethodArg(_), ident) => quote!(#ident), @@ -417,8 +414,8 @@ fn expand(method : Method, attrs : TokenStream, item : TokenStream) -> Result Result TokenStream -{ - expand(method, attrs, item) - .unwrap_or_else(|err| err.to_compile_error()) - .into() -} diff --git a/gotham_restful_derive/src/openapi_type.rs b/gotham_restful_derive/src/openapi_type.rs index 62e51ca..d73507e 100644 --- a/gotham_restful_derive/src/openapi_type.rs +++ b/gotham_restful_derive/src/openapi_type.rs @@ -1,6 +1,5 @@ use crate::util::{CollectToResult, remove_parens}; -use proc_macro::TokenStream; -use proc_macro2::TokenStream as TokenStream2; +use proc_macro2::{Ident, TokenStream}; use quote::quote; use syn::{ parse_macro_input, @@ -16,29 +15,23 @@ use syn::{ Fields, Generics, GenericParam, - Ident, Lit, Meta, NestedMeta, + Result, Variant }; -pub fn expand(tokens : TokenStream) -> TokenStream +pub fn expand_openapi_type(input : DeriveInput) -> Result { - let input = parse_macro_input!(tokens as DeriveInput); - - let output = 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::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")) - }; - - output - .unwrap_or_else(|err| err.to_compile_error()) - .into() + } } -fn expand_where(generics : &Generics) -> TokenStream2 +fn expand_where(generics : &Generics) -> TokenStream { if generics.params.is_empty() { @@ -66,7 +59,7 @@ struct Attrs rename : Option } -fn to_string(lit : &Lit) -> Result +fn to_string(lit : &Lit) -> Result { match lit { Lit::Str(str) => Ok(str.value()), @@ -74,7 +67,7 @@ fn to_string(lit : &Lit) -> Result } } -fn to_bool(lit : &Lit) -> Result +fn to_bool(lit : &Lit) -> Result { match lit { Lit::Bool(bool) => Ok(bool.value), @@ -82,7 +75,7 @@ fn to_bool(lit : &Lit) -> Result } } -fn parse_attributes(input : &[Attribute]) -> Result +fn parse_attributes(input : &[Attribute]) -> Result { let mut parsed = Attrs::default(); for attr in input @@ -111,7 +104,7 @@ fn parse_attributes(input : &[Attribute]) -> Result Ok(parsed) } -fn expand_variant(variant : &Variant) -> Result +fn expand_variant(variant : &Variant) -> Result { if variant.fields != Fields::Unit { @@ -131,7 +124,7 @@ fn expand_variant(variant : &Variant) -> Result }) } -fn expand_enum(ident : Ident, generics : Generics, attrs : Vec, input : DataEnum) -> Result +fn expand_enum(ident : Ident, generics : Generics, attrs : Vec, input : DataEnum) -> Result { let krate = super::krate(); let where_clause = expand_where(&generics); @@ -176,7 +169,7 @@ fn expand_enum(ident : Ident, generics : Generics, attrs : Vec, input }) } -fn expand_field(field : &Field) -> Result +fn expand_field(field : &Field) -> Result { let ident = match &field.ident { Some(ident) => ident, @@ -231,7 +224,7 @@ fn expand_field(field : &Field) -> Result }}) } -pub fn expand_struct(ident : Ident, generics : Generics, attrs : Vec, input : DataStruct) -> Result +fn expand_struct(ident : Ident, generics : Generics, attrs : Vec, input : DataStruct) -> Result { let krate = super::krate(); let where_clause = expand_where(&generics); @@ -243,7 +236,7 @@ pub fn expand_struct(ident : Ident, generics : Generics, attrs : Vec, None => ident.to_string() }; - let fields : Vec = match input.fields { + let fields : Vec = match input.fields { Fields::Named(named_fields) => { named_fields.named.iter() .map(expand_field) diff --git a/gotham_restful_derive/src/request_body.rs b/gotham_restful_derive/src/request_body.rs index 57909b3..ddcc7e8 100644 --- a/gotham_restful_derive/src/request_body.rs +++ b/gotham_restful_derive/src/request_body.rs @@ -1,17 +1,15 @@ use crate::util::CollectToResult; -use proc_macro::TokenStream; -use proc_macro2::TokenStream as TokenStream2; +use proc_macro2::{Ident, TokenStream}; use quote::quote; use std::iter; use syn::{ parenthesized, - parse::{Parse, ParseStream, Result as SynResult}, + parse::{Parse, ParseStream}, punctuated::Punctuated, DeriveInput, - Error, Generics, - Ident, Path, + Result, Token }; @@ -19,7 +17,7 @@ struct MimeList(Punctuated); impl Parse for MimeList { - fn parse(input: ParseStream) -> SynResult + fn parse(input: ParseStream) -> Result { let content; let _paren = parenthesized!(content in input); @@ -29,13 +27,13 @@ impl Parse for MimeList } #[cfg(not(feature = "openapi"))] -fn impl_openapi_type(_ident : &Ident, _generics : &Generics) -> TokenStream2 +fn impl_openapi_type(_ident : &Ident, _generics : &Generics) -> TokenStream { quote!() } #[cfg(feature = "openapi")] -fn impl_openapi_type(ident : &Ident, generics : &Generics) -> TokenStream2 +fn impl_openapi_type(ident : &Ident, generics : &Generics) -> TokenStream { let krate = super::krate(); @@ -55,10 +53,9 @@ fn impl_openapi_type(ident : &Ident, generics : &Generics) -> TokenStream2 } } -fn expand(tokens : TokenStream) -> Result +pub fn expand_request_body(input : DeriveInput) -> Result { let krate = super::krate(); - let input : DeriveInput = syn::parse(tokens)?; let ident = input.ident; let generics = input.generics; @@ -66,7 +63,7 @@ fn expand(tokens : TokenStream) -> Result .filter(|attr| attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("supported_types".to_string())) .flat_map(|attr| syn::parse2::(attr.tokens) - .map(|list| Box::new(list.0.into_iter().map(Ok)) as Box>>) + .map(|list| Box::new(list.0.into_iter().map(Ok)) as Box>>) .unwrap_or_else(|err| Box::new(iter::once(Err(err))))) .collect_to_result()?; @@ -90,10 +87,3 @@ fn expand(tokens : TokenStream) -> Result #impl_openapi_type }) } - -pub fn expand_request_body(tokens : TokenStream) -> TokenStream -{ - expand(tokens) - .unwrap_or_else(|err| err.to_compile_error()) - .into() -} diff --git a/gotham_restful_derive/src/resource.rs b/gotham_restful_derive/src/resource.rs index 23f0171..173a8d4 100644 --- a/gotham_restful_derive/src/resource.rs +++ b/gotham_restful_derive/src/resource.rs @@ -1,9 +1,5 @@ -use crate::{ - method::Method, - util::CollectToResult -}; -use proc_macro::TokenStream; -use proc_macro2::TokenStream as TokenStream2; +use crate::{method::Method, util::CollectToResult}; +use proc_macro2::{Ident, TokenStream}; use quote::quote; use syn::{ parenthesized, @@ -11,7 +7,7 @@ use syn::{ punctuated::Punctuated, DeriveInput, Error, - Ident, + Result, Token }; use std::{iter, str::FromStr}; @@ -20,7 +16,7 @@ struct MethodList(Punctuated); impl Parse for MethodList { - fn parse(input: ParseStream) -> Result + fn parse(input: ParseStream) -> Result { let content; let _paren = parenthesized!(content in input); @@ -29,10 +25,9 @@ impl Parse for MethodList } } -fn expand(tokens : TokenStream) -> Result +pub fn expand_resource(input : DeriveInput) -> Result { let krate = super::krate(); - let input : DeriveInput = syn::parse(tokens)?; let ident = input.ident; let name = ident.to_string(); @@ -46,7 +41,7 @@ fn expand(tokens : TokenStream) -> Result let mod_ident = method.mod_ident(&name); let ident = method.setup_ident(&name); Ok(quote!(#mod_ident::#ident(&mut route);)) - })) as Box>>, + })) as Box>>, Err(err) => Box::new(iter::once(Err(err))) }).collect_to_result()?; @@ -65,10 +60,3 @@ fn expand(tokens : TokenStream) -> Result } }) } - -pub fn expand_resource(tokens : TokenStream) -> TokenStream -{ - expand(tokens) - .unwrap_or_else(|err| err.to_compile_error()) - .into() -} diff --git a/gotham_restful_derive/src/resource_error.rs b/gotham_restful_derive/src/resource_error.rs index 7ebc738..60ed605 100644 --- a/gotham_restful_derive/src/resource_error.rs +++ b/gotham_restful_derive/src/resource_error.rs @@ -1,12 +1,10 @@ use crate::util::{CollectToResult, remove_parens}; use lazy_static::lazy_static; -use proc_macro::TokenStream; -use proc_macro2::TokenStream as TokenStream2; +use proc_macro2::{Ident, TokenStream}; use quote::{format_ident, quote}; use regex::Regex; use std::iter; use syn::{ - parse_macro_input, spanned::Spanned, Attribute, Data, @@ -14,10 +12,10 @@ use syn::{ Error, Fields, GenericParam, - Ident, LitStr, Path, PathSegment, + Result, Type, Variant }; @@ -40,7 +38,7 @@ struct ErrorVariant display : Option } -fn process_variant(variant : Variant) -> Result +fn process_variant(variant : Variant) -> Result { let status = match variant.attrs.iter() .find(|attr| attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("status".to_string())) @@ -114,7 +112,7 @@ lazy_static! { impl ErrorVariant { - fn fields_pat(&self) -> TokenStream2 + fn fields_pat(&self) -> TokenStream { let mut fields = self.fields.iter().map(|field| &field.ident).peekable(); if fields.peek().is_none() { @@ -126,7 +124,7 @@ impl ErrorVariant } } - fn to_display_match_arm(&self, formatter_ident : &Ident, enum_ident : &Ident) -> Result + fn to_display_match_arm(&self, formatter_ident : &Ident, enum_ident : &Ident) -> Result { let ident = &self.ident; let display = self.display.as_ref().ok_or_else(|| Error::new(self.ident.span(), "Missing display string for this variant"))?; @@ -142,7 +140,7 @@ impl ErrorVariant }) } - fn into_match_arm(self, krate : &TokenStream2, enum_ident : &Ident) -> TokenStream2 + fn into_match_arm(self, krate : &TokenStream, enum_ident : &Ident) -> TokenStream { let ident = &self.ident; let fields_pat = self.fields_pat(); @@ -177,7 +175,7 @@ impl ErrorVariant } } - fn were(&self) -> Option + fn were(&self) -> Option { match self.from_ty.as_ref() { Some((_, ty)) => Some(quote!( #ty : ::std::error::Error )), @@ -186,10 +184,9 @@ impl ErrorVariant } } -fn expand(tokens : TokenStream) -> Result +pub fn expand_resource_error(input : DeriveInput) -> Result { let krate = super::krate(); - let input = parse_macro_input::parse::(tokens)?; let ident = input.ident; let generics = input.generics; @@ -228,7 +225,7 @@ fn expand(tokens : TokenStream) -> Result }) }; - let mut from_impls : Vec = Vec::new(); + let mut from_impls : Vec = Vec::new(); for var in &variants { @@ -290,10 +287,3 @@ fn expand(tokens : TokenStream) -> Result #( #from_impls )* }) } - -pub fn expand_resource_error(tokens : TokenStream) -> TokenStream -{ - expand(tokens) - .unwrap_or_else(|err| err.to_compile_error()) - .into() -} diff --git a/gotham_restful_derive/src/util.rs b/gotham_restful_derive/src/util.rs index 12a4951..d82dc31 100644 --- a/gotham_restful_derive/src/util.rs +++ b/gotham_restful_derive/src/util.rs @@ -1,8 +1,4 @@ -use proc_macro2::{ - Delimiter, - TokenStream as TokenStream2, - TokenTree -}; +use proc_macro2::{Delimiter, TokenStream, TokenTree}; use std::iter; use syn::Error; @@ -33,7 +29,7 @@ where } -pub fn remove_parens(input : TokenStream2) -> TokenStream2 +pub fn remove_parens(input : TokenStream) -> TokenStream { let iter = input.into_iter().flat_map(|tt| { if let TokenTree::Group(group) = &tt From 3de130e104055f52c220416920ff4ee1638adacf Mon Sep 17 00:00:00 2001 From: Dominic Date: Mon, 4 May 2020 20:30:15 +0200 Subject: [PATCH 042/170] emit proper error message for `async fn read_all(state: &State)` --- gotham_restful_derive/src/method.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/gotham_restful_derive/src/method.rs b/gotham_restful_derive/src/method.rs index d93c28e..d232df5 100644 --- a/gotham_restful_derive/src/method.rs +++ b/gotham_restful_derive/src/method.rs @@ -133,6 +133,11 @@ enum MethodArgumentType impl MethodArgumentType { + fn is_state_ref(&self) -> bool + { + matches!(self, Self::StateRef | Self::StateMutRef) + } + fn is_method_arg(&self) -> bool { matches!(self, Self::MethodArg(_)) @@ -368,6 +373,10 @@ pub fn expand_method(method : Method, mut attrs : AttributeArgs, fun : ItemFn) - let mut state_block = quote!(); if fun_is_async { + if let Some(arg) = args.iter().find(|arg| (*arg).ty.is_state_ref()) + { + return Err(Error::new(arg.span(), "async fn must not take &State as an argument as State is not Sync")); + } block = quote!(#block.await); } if is_no_content From cc86d3396c1437e30c31eda862c6c0396bdb0bf7 Mon Sep 17 00:00:00 2001 From: Dominic Date: Mon, 4 May 2020 20:45:46 +0200 Subject: [PATCH 043/170] rename update to change, delete to remove, and remove rest_ prefix from macros --- example/src/main.rs | 22 +++++++------- gotham_restful/src/lib.rs | 8 ++--- gotham_restful/src/openapi/router.rs | 16 +++++----- gotham_restful/src/resource.rs | 16 +++++----- gotham_restful/src/routing.rs | 40 ++++++++++++------------- gotham_restful_derive/src/lib.rs | 26 ++++++++-------- gotham_restful_derive/src/method.rs | 43 +++++++++++++-------------- gotham_restful_derive/src/resource.rs | 2 +- 8 files changed, 85 insertions(+), 88 deletions(-) diff --git a/example/src/main.rs b/example/src/main.rs index d062710..d300bd8 100644 --- a/example/src/main.rs +++ b/example/src/main.rs @@ -18,13 +18,13 @@ use log4rs::{ use serde::{Deserialize, Serialize}; #[derive(Resource)] -#[rest_resource(ReadAll, Read, Search, Create, DeleteAll, Delete, Update, UpdateAll)] +#[resource(read_all, read, search, create, change_all, change, remove, remove_all)] struct Users { } #[derive(Resource)] -#[rest_resource(ReadAll)] +#[resource(ReadAll)] struct Auth { } @@ -35,7 +35,7 @@ struct User username : String } -#[rest_read_all(Users)] +#[read_all(Users)] fn read_all() -> Success>> { vec![Username().fake(), Username().fake()] @@ -45,50 +45,50 @@ fn read_all() -> Success>> .into() } -#[rest_read(Users)] +#[read(Users)] fn read(id : u64) -> Success { let username : String = Username().fake(); User { username: format!("{}{}", username, id) }.into() } -#[rest_search(Users)] +#[search(Users)] fn search(query : User) -> Success { query.into() } -#[rest_create(Users)] +#[create(Users)] fn create(body : User) { info!("Created User: {}", body.username); } -#[rest_update_all(Users)] +#[change_all(Users)] fn update_all(body : Vec) { info!("Changing all Users to {:?}", body.into_iter().map(|u| u.username).collect::>()); } -#[rest_update(Users)] +#[change(Users)] fn update(id : u64, body : User) { info!("Change User {} to {}", id, body.username); } -#[rest_delete_all(Users)] +#[delete_all(Users)] fn delete_all() { info!("Delete all Users"); } -#[rest_delete(Users)] +#[delete(Users)] fn delete(id : u64) { info!("Delete User {}", id); } -#[rest_read_all(Auth)] +#[read_all(Auth)] fn auth_read_all(auth : AuthStatus<()>) -> AuthSuccess { match auth { diff --git a/gotham_restful/src/lib.rs b/gotham_restful/src/lib.rs index c96e905..058f273 100644 --- a/gotham_restful/src/lib.rs +++ b/gotham_restful/src/lib.rs @@ -175,10 +175,10 @@ pub use resource::{ ResourceRead, ResourceSearch, ResourceCreate, - ResourceUpdateAll, - ResourceUpdate, - ResourceDeleteAll, - ResourceDelete + ResourceChangeAll, + ResourceChange, + ResourceRemoveAll, + ResourceRemove }; mod response; diff --git a/gotham_restful/src/openapi/router.rs b/gotham_restful/src/openapi/router.rs index 1fa3ccf..86d92cb 100644 --- a/gotham_restful/src/openapi/router.rs +++ b/gotham_restful/src/openapi/router.rs @@ -106,7 +106,7 @@ macro_rules! implOpenapiRouter { (&mut *(self.0).router, self.1).create::() } - fn update_all(&mut self) + fn change_all(&mut self) where Handler::Res : 'static, Handler::Body : 'static @@ -119,10 +119,10 @@ macro_rules! implOpenapiRouter { item.put = Some(OperationDescription::new::(schema).with_body::(body_schema).into_operation()); (self.0).openapi_builder.add_path(path, item); - (&mut *(self.0).router, self.1).update_all::() + (&mut *(self.0).router, self.1).change_all::() } - fn update(&mut self) + fn change(&mut self) where Handler::Res : 'static, Handler::Body : 'static @@ -136,10 +136,10 @@ macro_rules! implOpenapiRouter { item.put = Some(OperationDescription::new::(schema).add_path_param("id", id_schema).with_body::(body_schema).into_operation()); (self.0).openapi_builder.add_path(path, item); - (&mut *(self.0).router, self.1).update::() + (&mut *(self.0).router, self.1).change::() } - fn delete_all(&mut self) + fn remove_all(&mut self) { let schema = (self.0).openapi_builder.add_schema::(); @@ -148,10 +148,10 @@ macro_rules! implOpenapiRouter { item.delete = Some(OperationDescription::new::(schema).into_operation()); (self.0).openapi_builder.add_path(path, item); - (&mut *(self.0).router, self.1).delete_all::() + (&mut *(self.0).router, self.1).remove_all::() } - fn delete(&mut self) + fn remove(&mut self) { let schema = (self.0).openapi_builder.add_schema::(); let id_schema = (self.0).openapi_builder.add_schema::(); @@ -161,7 +161,7 @@ macro_rules! implOpenapiRouter { item.delete = Some(OperationDescription::new::(schema).add_path_param("id", id_schema).into_operation()); (self.0).openapi_builder.add_path(path, item); - (&mut *(self.0).router, self.1).delete::() + (&mut *(self.0).router, self.1).remove::() } } diff --git a/gotham_restful/src/resource.rs b/gotham_restful/src/resource.rs index 360f2a1..6e2d5cd 100644 --- a/gotham_restful/src/resource.rs +++ b/gotham_restful/src/resource.rs @@ -68,32 +68,32 @@ pub trait ResourceCreate : ResourceMethod } /// Handle a PUT request on the Resource root. -pub trait ResourceUpdateAll : ResourceMethod +pub trait ResourceChangeAll : ResourceMethod { type Body : RequestBody; - fn update_all(state : State, body : Self::Body) -> Pin + Send>>; + fn change_all(state : State, body : Self::Body) -> Pin + Send>>; } /// Handle a PUT request on the Resource with an id. -pub trait ResourceUpdate : ResourceMethod +pub trait ResourceChange : ResourceMethod { type Body : RequestBody; type ID : ResourceID + 'static; - fn update(state : State, id : Self::ID, body : Self::Body) -> Pin + Send>>; + fn change(state : State, id : Self::ID, body : Self::Body) -> Pin + Send>>; } /// Handle a DELETE request on the Resource root. -pub trait ResourceDeleteAll : ResourceMethod +pub trait ResourceRemoveAll : ResourceMethod { - fn delete_all(state : State) -> Pin + Send>>; + fn remove_all(state : State) -> Pin + Send>>; } /// Handle a DELETE request on the Resource with an id. -pub trait ResourceDelete : ResourceMethod +pub trait ResourceRemove : ResourceMethod { type ID : ResourceID + 'static; - fn delete(state : State, id : Self::ID) -> Pin + Send>>; + fn remove(state : State, id : Self::ID) -> Pin + Send>>; } diff --git a/gotham_restful/src/routing.rs b/gotham_restful/src/routing.rs index 3e67e09..1714a32 100644 --- a/gotham_restful/src/routing.rs +++ b/gotham_restful/src/routing.rs @@ -78,19 +78,19 @@ pub trait DrawResourceRoutes Handler::Res : 'static, Handler::Body : 'static; - fn update_all(&mut self) + fn change_all(&mut self) where Handler::Res : 'static, Handler::Body : 'static; - fn update(&mut self) + fn change(&mut self) where Handler::Res : 'static, Handler::Body : 'static; - fn delete_all(&mut self); + fn remove_all(&mut self); - fn delete(&mut self); + fn remove(&mut self); } fn response_from(res : Response, state : &State) -> gotham::hyper::Response @@ -217,15 +217,15 @@ where handle_with_body::(state, |state, body| Handler::create(state, body)) } -fn update_all_handler(state : State) -> Pin> +fn change_all_handler(state : State) -> Pin> where Handler::Res : 'static, Handler::Body : 'static { - handle_with_body::(state, |state, body| Handler::update_all(state, body)) + handle_with_body::(state, |state, body| Handler::change_all(state, body)) } -fn update_handler(state : State) -> Pin> +fn change_handler(state : State) -> Pin> where Handler::Res : 'static, Handler::Body : 'static @@ -234,21 +234,21 @@ where let path : &PathExtractor = PathExtractor::borrow_from(&state); path.id.clone() }; - handle_with_body::(state, |state, body| Handler::update(state, id, body)) + handle_with_body::(state, |state, body| Handler::change(state, id, body)) } -fn delete_all_handler(state : State) -> Pin> +fn remove_all_handler(state : State) -> Pin> { - to_handler_future(state, |state| Handler::delete_all(state)).boxed() + to_handler_future(state, |state| Handler::remove_all(state)).boxed() } -fn delete_handler(state : State) -> Pin> +fn remove_handler(state : State) -> Pin> { let id = { let path : &PathExtractor = PathExtractor::borrow_from(&state); path.id.clone() }; - to_handler_future(state, |state| Handler::delete(state, id)).boxed() + to_handler_future(state, |state| Handler::remove(state, id)).boxed() } #[derive(Clone)] @@ -386,7 +386,7 @@ macro_rules! implDrawResourceRoutes { .to(|state| create_handler::(state)); } - fn update_all(&mut self) + fn change_all(&mut self) where Handler::Res : Send + 'static, Handler::Body : 'static @@ -396,10 +396,10 @@ macro_rules! implDrawResourceRoutes { self.0.put(&self.1) .extend_route_matcher(accept_matcher) .extend_route_matcher(content_matcher) - .to(|state| update_all_handler::(state)); + .to(|state| change_all_handler::(state)); } - fn update(&mut self) + fn change(&mut self) where Handler::Res : Send + 'static, Handler::Body : 'static @@ -410,24 +410,24 @@ macro_rules! implDrawResourceRoutes { .extend_route_matcher(accept_matcher) .extend_route_matcher(content_matcher) .with_path_extractor::>() - .to(|state| update_handler::(state)); + .to(|state| change_handler::(state)); } - fn delete_all(&mut self) + fn remove_all(&mut self) { let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); self.0.delete(&self.1) .extend_route_matcher(matcher) - .to(|state| delete_all_handler::(state)); + .to(|state| remove_all_handler::(state)); } - fn delete(&mut self) + fn remove(&mut self) { let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); self.0.delete(&format!("{}/:id", self.1)) .extend_route_matcher(matcher) .with_path_extractor::>() - .to(|state| delete_handler::(state)); + .to(|state| remove_handler::(state)); } } } diff --git a/gotham_restful_derive/src/lib.rs b/gotham_restful_derive/src/lib.rs index 66cd764..7645df9 100644 --- a/gotham_restful_derive/src/lib.rs +++ b/gotham_restful_derive/src/lib.rs @@ -72,7 +72,7 @@ pub fn derive_request_body(input : TokenStream) -> TokenStream expand_derive(input, expand_request_body) } -#[proc_macro_derive(Resource, attributes(rest_resource))] +#[proc_macro_derive(Resource, attributes(resource))] pub fn derive_resource(input : TokenStream) -> TokenStream { expand_derive(input, expand_resource) @@ -86,49 +86,49 @@ pub fn derive_resource_error(input : TokenStream) -> TokenStream #[proc_macro_attribute] -pub fn rest_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)) } #[proc_macro_attribute] -pub fn rest_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)) } #[proc_macro_attribute] -pub fn rest_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)) } #[proc_macro_attribute] -pub fn rest_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)) } #[proc_macro_attribute] -pub fn rest_update_all(attr : TokenStream, item : TokenStream) -> TokenStream +pub fn change_all(attr : TokenStream, item : TokenStream) -> TokenStream { - expand_macro(attr, item, |attr, item| expand_method(Method::UpdateAll, attr, item)) + expand_macro(attr, item, |attr, item| expand_method(Method::ChangeAll, attr, item)) } #[proc_macro_attribute] -pub fn rest_update(attr : TokenStream, item : TokenStream) -> TokenStream +pub fn change(attr : TokenStream, item : TokenStream) -> TokenStream { - expand_macro(attr, item, |attr, item| expand_method(Method::Update, attr, item)) + expand_macro(attr, item, |attr, item| expand_method(Method::Change, attr, item)) } #[proc_macro_attribute] -pub fn rest_delete_all(attr : TokenStream, item : TokenStream) -> TokenStream +pub fn delete_all(attr : TokenStream, item : TokenStream) -> TokenStream { - expand_macro(attr, item, |attr, item| expand_method(Method::DeleteAll, attr, item)) + expand_macro(attr, item, |attr, item| expand_method(Method::RemoveAll, attr, item)) } #[proc_macro_attribute] -pub fn rest_delete(attr : TokenStream, item : TokenStream) -> TokenStream +pub fn delete(attr : TokenStream, item : TokenStream) -> TokenStream { - expand_macro(attr, item, |attr, item| expand_method(Method::Delete, attr, item)) + expand_macro(attr, item, |attr, item| expand_method(Method::Remove, attr, item)) } diff --git a/gotham_restful_derive/src/method.rs b/gotham_restful_derive/src/method.rs index d232df5..789c50d 100644 --- a/gotham_restful_derive/src/method.rs +++ b/gotham_restful_derive/src/method.rs @@ -26,10 +26,10 @@ pub enum Method Read, Search, Create, - UpdateAll, - Update, - DeleteAll, - Delete + ChangeAll, + Change, + RemoveAll, + Remove } impl FromStr for Method @@ -43,10 +43,10 @@ impl FromStr for Method "Read" | "read" => Ok(Self::Read), "Search" | "search" => Ok(Self::Search), "Create" | "create" => Ok(Self::Create), - "UpdateAll" | "update_all" => Ok(Self::UpdateAll), - "Update" | "update" => Ok(Self::Update), - "DeleteAll" | "delete_all" => Ok(Self::DeleteAll), - "Delete" | "delete" => Ok(Self::Delete), + "ChangeAll" | "change_all" => Ok(Self::ChangeAll), + "Change" | "change" => Ok(Self::Change), + "RemoveAll" | "remove_all" => Ok(Self::RemoveAll), + "Remove" | "remove" => Ok(Self::Remove), _ => Err(Error::new(Span::call_site(), format!("Unknown method: `{}'", str))) } } @@ -59,14 +59,11 @@ impl Method use Method::*; match self { - ReadAll => vec![], - Read => vec!["ID"], + ReadAll | RemoveAll => vec![], + Read | Remove => vec!["ID"], Search => vec!["Query"], - Create => vec!["Body"], - UpdateAll => vec!["Body"], - Update => vec!["ID", "Body"], - DeleteAll => vec![], - Delete => vec!["ID"] + Create | ChangeAll => vec!["Body"], + Change => vec!["ID", "Body"] } } @@ -79,10 +76,10 @@ impl Method Read => "Read", Search => "Search", Create => "Create", - UpdateAll => "UpdateAll", - Update => "Update", - DeleteAll => "DeleteAll", - Delete => "Delete" + ChangeAll => "ChangeAll", + Change => "Change", + RemoveAll => "RemoveAll", + Remove => "Remove" }; format_ident!("Resource{}", name) } @@ -96,10 +93,10 @@ impl Method Read => "read", Search => "search", Create => "create", - UpdateAll => "update_all", - Update => "update", - DeleteAll => "delete_all", - Delete => "delete" + ChangeAll => "change_all", + Change => "change", + RemoveAll => "remove_all", + Remove => "remove" }; format_ident!("{}", name) } diff --git a/gotham_restful_derive/src/resource.rs b/gotham_restful_derive/src/resource.rs index 173a8d4..a51ecbb 100644 --- a/gotham_restful_derive/src/resource.rs +++ b/gotham_restful_derive/src/resource.rs @@ -32,7 +32,7 @@ pub fn expand_resource(input : DeriveInput) -> Result let name = ident.to_string(); let methods = input.attrs.into_iter().filter(|attr| - attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("rest_resource".to_string()) // TODO wtf + attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("resource".to_string()) // TODO wtf ).map(|attr| { syn::parse2(attr.tokens).map(|m : MethodList| m.0.into_iter()) }).flat_map(|list| match list { From a1acc06f6d1538071f30fd2a96401ba2ba939524 Mon Sep 17 00:00:00 2001 From: Dominic Date: Mon, 4 May 2020 21:34:20 +0200 Subject: [PATCH 044/170] update doc --- .gitlab-ci.yml | 2 +- README.md | 196 ++++++++++++++----- gotham_restful/Cargo.toml | 3 +- gotham_restful/src/auth.rs | 17 +- gotham_restful/src/lib.rs | 229 +++++++++++++++++------ gotham_restful/src/result/auth_result.rs | 6 +- gotham_restful/src/result/no_content.rs | 3 +- gotham_restful/src/result/success.rs | 3 +- 8 files changed, 348 insertions(+), 111 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 18413f8..6ba7d54 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -13,7 +13,6 @@ test-default: - cargo -V script: - cargo test --workspace --lib - - cargo test --workspace --doc cache: paths: - cargo/ @@ -23,6 +22,7 @@ test-all: stage: test image: msrd0/rust:alpine-tarpaulin before_script: + - apk add --no-cache postgresql-dev - cargo -V script: - cargo test --workspace --all-features --doc diff --git a/README.md b/README.md index 1a7e323..2e6c996 100644 --- a/README.md +++ b/README.md @@ -23,58 +23,71 @@
-This crate is an extension to the popular [gotham web framework][gotham] for Rust. The idea is to -have several RESTful resources that can be added to the gotham router. This crate will take care -of everything else, like parsing path/query parameters, request bodies, and writing response -bodies, relying on [`serde`][serde] and [`serde_json`][serde_json] for (de)serializing. If you -enable the `openapi` feature, you can also generate an OpenAPI Specification from your RESTful -resources. - **Note:** The `stable` branch contains some bugfixes against the last release. The `master` branch currently tracks gotham's master branch and the next release will use gotham 0.5.0 and be compatible with the new future / async stuff. -## Usage +This crate is an extension to the popular [gotham web framework][gotham] for Rust. It allows you to +create resources with assigned methods that aim to be a more convenient way of creating handlers +for requests. Assuming you assign `/foobar` to your resource, you can implement the following +methods: -A basic server with only one resource, handling a simple `GET` request, could look like this: +| Method Name | Required Arguments | HTTP Verb | HTTP Path | +| ----------- | ------------------ | --------- | ----------- | +| read_all | | GET | /foobar | +| read | id | GET | /foobar/:id | +| search | query | GET | /foobar/search | +| create | body | POST | /foobar | +| change_all | body | PUT | /foobar | +| change | id, body | PUT | /foobar/:id | +| remove_all | | DELETE | /foobar | +| remove | id | DELETE | /foobar/:id | + +Each of those methods has a macro that creates the neccessary boilerplate for the Resource. A +simple example could look like this: ```rust -/// Our RESTful Resource. +/// Our RESTful resource. #[derive(Resource)] -#[rest_resource(read_all)] -struct UsersResource; +#[resource(read)] +struct FooResource; -/// Our return type. -#[derive(Deserialize, Serialize)] -struct User { - id: i64, - username: String, - email: String +/// The return type of the foo read method. +#[derive(Serialize)] +struct Foo { + id: u64 } -/// Our handler method. -#[rest_read_all(UsersResource)] -fn read_all(_state: &mut State) -> Success> { - vec![User { - id: 1, - username: "h4ck3r".to_string(), - email: "h4ck3r@example.org".to_string() - }].into() -} - -/// Our main method. -fn main() { - gotham::start("127.0.0.1:8080", build_simple_router(|route| { - route.resource::("users"); - })); +/// The foo read method handler. +#[read(FooResource)] +fn read(id: u64) -> Success { + Foo { id }.into() } ``` -Uploads and Downloads can also be handled: +## Arguments + +Some methods require arguments. Those should be + * **id** Should be a deserializable json-primitive like `i64` or `String`. + * **body** Should be any deserializable object, or any type implementing [`RequestBody`]. + * **query** Should be any deserializable object whose variables are json-primitives. It will + however not be parsed from json, but from HTTP GET parameters like in `search?id=1`. The + type needs to implement [`QueryStringExtractor`]. + +Additionally, non-async handlers may take a reference to gotham's [`State`]. If you need to +have an async handler (that is, the function that the method macro is invoked on is declared +as `async fn`), consider returning the boxed future instead. Since [`State`] does not implement +`Sync` there is unfortunately no more convenient way. + +## Uploads and Downloads + +By default, every request body is parsed from json, and every respone is converted to json using +[serde_json]. However, you may also use raw bodies. This is an example where the request body +is simply returned as the response again, no json parsing involved: ```rust #[derive(Resource)] -#[rest_resource(create)] +#[resource(create)] struct ImageResource; #[derive(FromBody, RequestBody)] @@ -84,23 +97,105 @@ struct RawImage { content_type: Mime } -#[rest_create(ImageResource)] -fn create(_state : &mut State, body : RawImage) -> Raw> { +#[create(ImageResource)] +fn create(body : RawImage) -> Raw> { Raw::new(body.content, body.content_type) } ``` -Look at the [example] for more methods and usage with the `openapi` feature. +## Features -## Known Issues +To make life easier for common use-cases, this create offers a few features that might be helpful +when you implement your web server. -These are currently known major issues. For a complete list please see -[the issue tracker](https://gitlab.com/msrd0/gotham-restful/issues). -If you encounter any issues that aren't yet reported, please report them -[here](https://gitlab.com/msrd0/gotham-restful/issues/new). +### Authentication Feature - - Enabling the `openapi` feature might break code ([#4](https://gitlab.com/msrd0/gotham-restful/issues/4)) - - For `chrono`'s `DateTime` types, the format is `date-time` instead of `datetime` ([openapiv3#14](https://github.com/glademiller/openapiv3/pull/14)) +In order to enable authentication support, enable the `auth` feature gate. This allows you to +register a middleware that can automatically check for the existence of an JWT authentication +token. Besides being supported by the method macros, it supports to lookup the required JWT secret +with the JWT data, hence you can use several JWT secrets and decide on the fly which secret to use. +None of this is currently supported by gotham's own JWT middleware. + +A simple example that uses only a single secret could look like this: + +```rust +#[derive(Resource)] +#[resource(read)] +struct SecretResource; + +#[derive(Serialize)] +struct Secret { + id: u64, + intended_for: String +} + +#[derive(Deserialize, Clone)] +struct AuthData { + sub: String, + exp: u64 +} + +#[read(SecretResource)] +fn read(auth: AuthStatus, id: u64) -> AuthSuccess { + let intended_for = auth.ok()?.sub; + Ok(Secret { id, intended_for }) +} + +fn main() { + let auth: AuthMiddleware = AuthMiddleware::new( + AuthSource::AuthorizationHeader, + AuthValidation::default(), + StaticAuthHandler::from_array(b"zlBsA2QXnkmpe0QTh8uCvtAEa4j33YAc") + ); + let (chain, pipelines) = single_pipeline(new_pipeline().add(auth).build()); + gotham::start("127.0.0.1:8080", build_router(chain, pipelines, |route| { + route.resource::("secret"); + })); +} +``` + +### Database Feature + +The database feature allows an easy integration of [diesel] into your handler functions. Please +note however that due to the way gotham's diesel middleware implementation, it is not possible +to run async code while holding a database connection. If you need to combine async and database, +you'll need to borrow the connection from the [`State`] yourself and return a boxed future. + +A simple non-async example could look like this: + +```rust +#[derive(Resource)] +#[resource(read_all)] +struct FooResource; + +#[derive(Queryable, Serialize)] +struct Foo { + id: i64, + value: String +} + +#[read_all(FooResource)] +fn read_all(conn: &PgConnection) -> QueryResult> { + foo::table.load(conn) +} + +type Repo = gotham_middleware_diesel::Repo; + +fn main() { + let repo = Repo::new(&env::var("DATABASE_URL").unwrap()); + let diesel = DieselMiddleware::new(repo); + + let (chain, pipelines) = single_pipeline(new_pipeline().add(diesel).build()); + gotham::start("127.0.0.1:8080", build_router(chain, pipelines, |route| { + route.resource::("foo"); + })); +} +``` + +## Examples + +There is a lack of good examples, but there is currently a collection of code in the [example] +directory, that might help you. Any help writing more examples is highly appreciated. ## License @@ -109,7 +204,10 @@ Licensed under your option of: - [Eclipse Public License Version 2.0](https://gitlab.com/msrd0/gotham-restful/blob/master/LICENSE-EPL) -[example]: https://gitlab.com/msrd0/gotham-restful/tree/master/example -[gotham]: https://gotham.rs/ -[serde]: https://github.com/serde-rs/serde#serde----- -[serde_json]: https://github.com/serde-rs/json#serde-json---- + [diesel]: https://diesel.rs/ + [example]: https://gitlab.com/msrd0/gotham-restful/tree/master/example + [gotham]: https://gotham.rs/ + [serde_json]: https://github.com/serde-rs/json#serde-json---- + [`QueryStringExtractor`]: ../gotham/extractor/trait.QueryStringExtractor.html + [`RequestBody`]: trait.RequestBody.html + [`State`]: ../gotham/state/struct.State.html diff --git a/gotham_restful/Cargo.toml b/gotham_restful/Cargo.toml index 78359fe..2e65978 100644 --- a/gotham_restful/Cargo.toml +++ b/gotham_restful/Cargo.toml @@ -36,11 +36,12 @@ thiserror = "1.0.15" uuid = { version = ">= 0.1, < 0.9", optional = true } [dev-dependencies] +diesel = { version = "1.4.4", features = ["postgres"] } futures-executor = "0.3.4" paste = "0.1.10" [features] -default = [] +default = ["errorlog"] auth = ["gotham_restful_derive/auth", "base64", "cookie", "jsonwebtoken"] errorlog = [] database = ["gotham_restful_derive/database", "gotham_middleware_diesel"] diff --git a/gotham_restful/src/auth.rs b/gotham_restful/src/auth.rs index c109f07..0888ac3 100644 --- a/gotham_restful/src/auth.rs +++ b/gotham_restful/src/auth.rs @@ -1,4 +1,4 @@ -use crate::HeaderName; +use crate::{AuthError, Forbidden, HeaderName}; use cookie::CookieJar; use futures_util::{future, future::{FutureExt, TryFutureExt}}; use gotham::{ @@ -58,6 +58,17 @@ where { } +impl AuthStatus +{ + pub fn ok(self) -> Result + { + match self { + Self::Authenticated(data) => Ok(data), + _ => Err(Forbidden) + } + } +} + /// The source of the authentication token in the request. #[derive(Clone, Debug, StateData)] pub enum AuthSource @@ -134,7 +145,7 @@ simply add it to your pipeline and request it inside your handler: # use serde::{Deserialize, Serialize}; # #[derive(Resource)] -#[rest_resource(read_all)] +#[resource(read_all)] struct AuthResource; #[derive(Debug, Deserialize, Clone)] @@ -143,7 +154,7 @@ struct AuthData { exp: u64 } -#[rest_read_all(AuthResource)] +#[read_all(AuthResource)] fn read_all(auth : &AuthStatus) -> Success { format!("{:?}", auth).into() } diff --git a/gotham_restful/src/lib.rs b/gotham_restful/src/lib.rs index 058f273..1e01f08 100644 --- a/gotham_restful/src/lib.rs +++ b/gotham_restful/src/lib.rs @@ -2,67 +2,85 @@ #![warn(missing_debug_implementations, rust_2018_idioms)] #![deny(intra_doc_link_resolution_failure)] /*! -This crate is an extension to the popular [gotham web framework][gotham] for Rust. The idea is to -have several RESTful resources that can be added to the gotham router. This crate will take care -of everything else, like parsing path/query parameters, request bodies, and writing response -bodies, relying on [`serde`][serde] and [`serde_json`][serde_json] for (de)serializing. If you -enable the `openapi` feature, you can also generate an OpenAPI Specification from your RESTful -resources. - **Note:** The `stable` branch contains some bugfixes against the last release. The `master` branch currently tracks gotham's master branch and the next release will use gotham 0.5.0 and be compatible with the new future / async stuff. -# Usage +This crate is an extension to the popular [gotham web framework][gotham] for Rust. It allows you to +create resources with assigned methods that aim to be a more convenient way of creating handlers +for requests. Assuming you assign `/foobar` to your resource, you can implement the following +methods: -A basic server with only one resource, handling a simple `GET` request, could look like this: +| Method Name | Required Arguments | HTTP Verb | HTTP Path | +| ----------- | ------------------ | --------- | ----------- | +| read_all | | GET | /foobar | +| read | id | GET | /foobar/:id | +| search | query | GET | /foobar/search | +| create | body | POST | /foobar | +| change_all | body | PUT | /foobar | +| change | id, body | PUT | /foobar/:id | +| remove_all | | DELETE | /foobar | +| remove | id | DELETE | /foobar/:id | + +Each of those methods has a macro that creates the neccessary boilerplate for the Resource. A +simple example could look like this: ```rust,no_run # #[macro_use] extern crate gotham_restful_derive; -# use gotham::{router::builder::*, state::State}; -# use gotham_restful::{DrawResources, Resource, Success}; +# use gotham::router::builder::*; +# use gotham_restful::*; # use serde::{Deserialize, Serialize}; -/// Our RESTful Resource. +/// Our RESTful resource. #[derive(Resource)] -#[rest_resource(read_all)] -struct UsersResource; +#[resource(read)] +struct FooResource; -/// Our return type. -#[derive(Deserialize, Serialize)] +/// The return type of the foo read method. +#[derive(Serialize)] # #[derive(OpenapiType)] -struct User { - id: i64, - username: String, - email: String +struct Foo { + id: u64 } -/// Our handler method. -#[rest_read_all(UsersResource)] -fn read_all(_state: &mut State) -> Success> { - vec![User { - id: 1, - username: "h4ck3r".to_string(), - email: "h4ck3r@example.org".to_string() - }].into() -} - -/// Our main method. -fn main() { - gotham::start("127.0.0.1:8080", build_simple_router(|route| { - route.resource::("users"); - })); +/// The foo read method handler. +#[read(FooResource)] +fn read(id: u64) -> Success { + Foo { id }.into() } +# fn main() { +# gotham::start("127.0.0.1:8080", build_simple_router(|route| { +# route.resource::("foo"); +# })); +# } ``` -Uploads and Downloads can also be handled: +# Arguments + +Some methods require arguments. Those should be + * **id** Should be a deserializable json-primitive like `i64` or `String`. + * **body** Should be any deserializable object, or any type implementing [`RequestBody`]. + * **query** Should be any deserializable object whose variables are json-primitives. It will + however not be parsed from json, but from HTTP GET parameters like in `search?id=1`. The + type needs to implement [`QueryStringExtractor`]. + +Additionally, non-async handlers may take a reference to gotham's [`State`]. If you need to +have an async handler (that is, the function that the method macro is invoked on is declared +as `async fn`), consider returning the boxed future instead. Since [`State`] does not implement +`Sync` there is unfortunately no more convenient way. + +# Uploads and Downloads + +By default, every request body is parsed from json, and every respone is converted to json using +[serde_json]. However, you may also use raw bodies. This is an example where the request body +is simply returned as the response again, no json parsing involved: ```rust,no_run # #[macro_use] extern crate gotham_restful_derive; -# use gotham::{router::builder::*, state::State}; -# use gotham_restful::{DrawResources, Mime, Raw, Resource, Success}; +# use gotham::router::builder::*; +# use gotham_restful::*; # use serde::{Deserialize, Serialize}; #[derive(Resource)] -#[rest_resource(create)] +#[resource(create)] struct ImageResource; #[derive(FromBody, RequestBody)] @@ -72,8 +90,8 @@ struct RawImage { content_type: Mime } -#[rest_create(ImageResource)] -fn create(_state : &mut State, body : RawImage) -> Raw> { +#[create(ImageResource)] +fn create(body : RawImage) -> Raw> { Raw::new(body.content, body.content_type) } # fn main() { @@ -83,17 +101,119 @@ fn create(_state : &mut State, body : RawImage) -> Raw> { # } ``` -Look at the [example] for more methods and usage with the `openapi` feature. +# Features -# Known Issues +To make life easier for common use-cases, this create offers a few features that might be helpful +when you implement your web server. -These are currently known major issues. For a complete list please see -[the issue tracker](https://gitlab.com/msrd0/gotham-restful/issues). -If you encounter any issues that aren't yet reported, please report them -[here](https://gitlab.com/msrd0/gotham-restful/issues/new). +## Authentication Feature - - Enabling the `openapi` feature might break code ([#4](https://gitlab.com/msrd0/gotham-restful/issues/4)) - - For `chrono`'s `DateTime` types, the format is `date-time` instead of `datetime` ([openapiv3#14](https://github.com/glademiller/openapiv3/pull/14)) +In order to enable authentication support, enable the `auth` feature gate. This allows you to +register a middleware that can automatically check for the existence of an JWT authentication +token. Besides being supported by the method macros, it supports to lookup the required JWT secret +with the JWT data, hence you can use several JWT secrets and decide on the fly which secret to use. +None of this is currently supported by gotham's own JWT middleware. + +A simple example that uses only a single secret could look like this: + +```rust,no_run +# #[macro_use] extern crate gotham_restful_derive; +# use gotham::{router::builder::*, pipeline::{new_pipeline, single::single_pipeline}, state::State}; +# use gotham_restful::*; +# use serde::{Deserialize, Serialize}; +#[derive(Resource)] +#[resource(read)] +struct SecretResource; + +#[derive(Serialize)] +# #[derive(OpenapiType)] +struct Secret { + id: u64, + intended_for: String +} + +#[derive(Deserialize, Clone)] +struct AuthData { + sub: String, + exp: u64 +} + +#[read(SecretResource)] +fn read(auth: AuthStatus, id: u64) -> AuthSuccess { + let intended_for = auth.ok()?.sub; + Ok(Secret { id, intended_for }) +} + +fn main() { + let auth: AuthMiddleware = AuthMiddleware::new( + AuthSource::AuthorizationHeader, + AuthValidation::default(), + StaticAuthHandler::from_array(b"zlBsA2QXnkmpe0QTh8uCvtAEa4j33YAc") + ); + let (chain, pipelines) = single_pipeline(new_pipeline().add(auth).build()); + gotham::start("127.0.0.1:8080", build_router(chain, pipelines, |route| { + route.resource::("secret"); + })); +} +``` + +## Database Feature + +The database feature allows an easy integration of [diesel] into your handler functions. Please +note however that due to the way gotham's diesel middleware implementation, it is not possible +to run async code while holding a database connection. If you need to combine async and database, +you'll need to borrow the connection from the [`State`] yourself and return a boxed future. + +A simple non-async example could look like this: + +```rust,no_run +# #[macro_use] extern crate diesel; +# #[macro_use] extern crate gotham_restful_derive; +# use diesel::{table, PgConnection, QueryResult, RunQueryDsl}; +# use gotham::{router::builder::*, pipeline::{new_pipeline, single::single_pipeline}, state::State}; +# use gotham_middleware_diesel::DieselMiddleware; +# use gotham_restful::*; +# use serde::{Deserialize, Serialize}; +# use std::env; +# table! { +# foo (id) { +# id -> Int8, +# value -> Text, +# } +# } +#[derive(Resource)] +#[resource(read_all)] +struct FooResource; + +#[derive(Queryable, Serialize)] +# #[derive(OpenapiType)] +struct Foo { + id: i64, + value: String +} + +#[read_all(FooResource)] +fn read_all(conn: &PgConnection) -> QueryResult> { + foo::table.load(conn) +} + +type Repo = gotham_middleware_diesel::Repo; + +fn main() { + let repo = Repo::new(&env::var("DATABASE_URL").unwrap()); + let diesel = DieselMiddleware::new(repo); + + let (chain, pipelines) = single_pipeline(new_pipeline().add(diesel).build()); + gotham::start("127.0.0.1:8080", build_router(chain, pipelines, |route| { + route.resource::("foo"); + })); +} +``` + +# Examples + +There is a lack of good examples, but there is currently a collection of code in the [example] +directory, that might help you. Any help writing more examples is highly appreciated. # License @@ -102,10 +222,13 @@ Licensed under your option of: - [Eclipse Public License Version 2.0](https://gitlab.com/msrd0/gotham-restful/blob/master/LICENSE-EPL) -[example]: https://gitlab.com/msrd0/gotham-restful/tree/master/example -[gotham]: https://gotham.rs/ -[serde]: https://github.com/serde-rs/serde#serde----- -[serde_json]: https://github.com/serde-rs/json#serde-json---- + [diesel]: https://diesel.rs/ + [example]: https://gitlab.com/msrd0/gotham-restful/tree/master/example + [gotham]: https://gotham.rs/ + [serde_json]: https://github.com/serde-rs/json#serde-json---- + [`QueryStringExtractor`]: ../gotham/extractor/trait.QueryStringExtractor.html + [`RequestBody`]: trait.RequestBody.html + [`State`]: ../gotham/state/struct.State.html */ // weird proc macro issue diff --git a/gotham_restful/src/result/auth_result.rs b/gotham_restful/src/result/auth_result.rs index b2ebbe1..bed279f 100644 --- a/gotham_restful/src/result/auth_result.rs +++ b/gotham_restful/src/result/auth_result.rs @@ -29,12 +29,13 @@ look something like this (assuming the `auth` feature is enabled): # use serde::Deserialize; # # #[derive(Resource)] +# #[resource(read_all)] # struct MyResource; # # #[derive(Clone, Deserialize)] # struct MyAuthData { exp : u64 } # -#[rest_read_all(MyResource)] +#[read_all(MyResource)] fn read_all(auth : AuthStatus) -> AuthSuccess { let auth_data = match auth { AuthStatus::Authenticated(data) => data, @@ -88,12 +89,13 @@ look something like this (assuming the `auth` feature is enabled): # use std::io; # # #[derive(Resource)] +# #[resource(read_all)] # struct MyResource; # # #[derive(Clone, Deserialize)] # struct MyAuthData { exp : u64 } # -#[rest_read_all(MyResource)] +#[read_all(MyResource)] fn read_all(auth : AuthStatus) -> AuthResult { let auth_data = match auth { AuthStatus::Authenticated(data) => data, diff --git a/gotham_restful/src/result/no_content.rs b/gotham_restful/src/result/no_content.rs index 0011a67..3377b66 100644 --- a/gotham_restful/src/result/no_content.rs +++ b/gotham_restful/src/result/no_content.rs @@ -22,9 +22,10 @@ the function attributes: # use gotham_restful::*; # # #[derive(Resource)] +# #[resource(read_all)] # struct MyResource; # -#[rest_read_all(MyResource)] +#[read_all(MyResource)] fn read_all(_state: &mut State) { // do something } diff --git a/gotham_restful/src/result/success.rs b/gotham_restful/src/result/success.rs index 11b2f2b..f622f12 100644 --- a/gotham_restful/src/result/success.rs +++ b/gotham_restful/src/result/success.rs @@ -25,6 +25,7 @@ Usage example: # use serde::{Deserialize, Serialize}; # # #[derive(Resource)] +# #[resource(read_all)] # struct MyResource; # #[derive(Deserialize, Serialize)] @@ -33,7 +34,7 @@ struct MyResponse { message: &'static str } -#[rest_read_all(MyResource)] +#[read_all(MyResource)] fn read_all(_state: &mut State) -> Success { let res = MyResponse { message: "I'm always happy" }; res.into() From 022edede62197b27463374c5feb7086ac5f46b47 Mon Sep 17 00:00:00 2001 From: Dominic Date: Mon, 4 May 2020 23:52:09 +0200 Subject: [PATCH 045/170] the next release will be 0.1.0, not 0.0.5 --- example/Cargo.toml | 2 +- gotham_restful/Cargo.toml | 12 ++++++------ gotham_restful_derive/Cargo.toml | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/example/Cargo.toml b/example/Cargo.toml index af55bfa..f765a82 100644 --- a/example/Cargo.toml +++ b/example/Cargo.toml @@ -17,7 +17,7 @@ gitlab = { repository = "msrd0/gotham-restful", branch = "master" } fake = "2.2" gotham = { git = "https://github.com/gotham-rs/gotham", version = "0.5.0-dev", default-features = false } gotham_derive = { git = "https://github.com/gotham-rs/gotham", version = "0.5.0-dev", default-features = false } -gotham_restful = { version = "0.0.5-dev", features = ["auth", "openapi"] } +gotham_restful = { version = "0.1.0-dev", features = ["auth", "openapi"] } log = "0.4.8" log4rs = { version = "0.11", features = ["console_appender"], default-features = false } serde = "1.0.106" diff --git a/gotham_restful/Cargo.toml b/gotham_restful/Cargo.toml index 2e65978..836cd19 100644 --- a/gotham_restful/Cargo.toml +++ b/gotham_restful/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "gotham_restful" -version = "0.0.5-dev" +version = "0.1.0-dev" authors = ["Dominic Meiser "] edition = "2018" description = "RESTful additions for Gotham" @@ -23,7 +23,7 @@ futures-util = "0.3.4" gotham = { git = "https://github.com/gotham-rs/gotham", version = "0.5.0-dev", default-features = false } gotham_derive = { git = "https://github.com/gotham-rs/gotham", version = "0.5.0-dev" } gotham_middleware_diesel = { git = "https://github.com/gotham-rs/gotham", version = "0.1.0", optional = true } -gotham_restful_derive = { version = "0.0.4-dev" } +gotham_restful_derive = { version = "0.1.0-dev" } indexmap = { version = "1.3.2", optional = true } itertools = "0.9.0" jsonwebtoken = { version = "7.1.0", optional = true } @@ -31,14 +31,14 @@ log = "0.4.8" mime = "0.3.16" openapiv3 = { version = "0.3", optional = true } serde = { version = "1.0.106", features = ["derive"] } -serde_json = "1.0.51" -thiserror = "1.0.15" -uuid = { version = ">= 0.1, < 0.9", optional = true } +serde_json = "1.0.52" +thiserror = "1.0.16" +uuid = { version = "0.8.1", optional = true } [dev-dependencies] diesel = { version = "1.4.4", features = ["postgres"] } futures-executor = "0.3.4" -paste = "0.1.10" +paste = "0.1.12" [features] default = ["errorlog"] diff --git a/gotham_restful_derive/Cargo.toml b/gotham_restful_derive/Cargo.toml index bd7f368..275b490 100644 --- a/gotham_restful_derive/Cargo.toml +++ b/gotham_restful_derive/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "gotham_restful_derive" -version = "0.0.4-dev" +version = "0.1.0-dev" authors = ["Dominic Meiser "] edition = "2018" description = "RESTful additions for Gotham - Derive" @@ -19,8 +19,8 @@ gitlab = { repository = "msrd0/gotham-restful", branch = "master" } [dependencies] heck = "0.3.1" lazy_static = "1.4.0" -proc-macro2 = "1.0.10" -quote = "1.0.3" +proc-macro2 = "1.0.12" +quote = "1.0.4" regex = "1.3.7" syn = "1.0.18" From e7e55514a24ddf8d792df4b51e35d23360baaafe Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 5 May 2020 00:34:19 +0200 Subject: [PATCH 046/170] support scopes inside openapi router (implements #5) --- .gitlab-ci.yml | 2 +- gotham_restful/src/openapi/builder.rs | 2 +- gotham_restful/src/openapi/router.rs | 47 ++++++++++---- gotham_restful/src/routing.rs | 1 + .../tests/openapi_supports_scope.rs | 61 +++++++++++++++++++ 5 files changed, 99 insertions(+), 14 deletions(-) create mode 100644 gotham_restful/tests/openapi_supports_scope.rs diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6ba7d54..5ec18d8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -12,7 +12,7 @@ test-default: before_script: - cargo -V script: - - cargo test --workspace --lib + - cargo test --workspace --tests cache: paths: - cargo/ diff --git a/gotham_restful/src/openapi/builder.rs b/gotham_restful/src/openapi/builder.rs index 0ca4cb1..bf81caa 100644 --- a/gotham_restful/src/openapi/builder.rs +++ b/gotham_restful/src/openapi/builder.rs @@ -14,7 +14,7 @@ pub struct OpenapiInfo pub urls : Vec } -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct OpenapiBuilder { pub openapi : Arc> diff --git a/gotham_restful/src/openapi/router.rs b/gotham_restful/src/openapi/router.rs index 86d92cb..6dd7a13 100644 --- a/gotham_restful/src/openapi/router.rs +++ b/gotham_restful/src/openapi/router.rs @@ -19,13 +19,36 @@ pub trait GetOpenapi #[derive(Debug)] pub struct OpenapiRouter<'a, D> { - pub router : &'a mut D, - pub openapi_builder : &'a mut OpenapiBuilder + pub(crate) router : &'a mut D, + pub(crate) scope : Option<&'a str>, + pub(crate) openapi_builder : &'a mut OpenapiBuilder } macro_rules! implOpenapiRouter { ($implType:ident) => { - + + impl<'a, 'b, C, P> OpenapiRouter<'a, $implType<'b, C, P>> + where + C : PipelineHandleChain

+ Copy + Send + Sync + 'static, + P : RefUnwindSafe + Send + Sync + 'static + { + pub fn scope(&mut self, path : &str, callback : F) + where + F : FnOnce(&mut OpenapiRouter<'_, ScopeBuilder<'_, C, P>>) + { + let mut openapi_builder = self.openapi_builder.clone(); + let new_scope = self.scope.map(|scope| format!("{}/{}", scope, path).replace("//", "/")); + self.router.scope(path, |router| { + let mut router = OpenapiRouter { + router, + scope: Some(new_scope.as_ref().map(String::as_ref).unwrap_or(path)), + openapi_builder: &mut openapi_builder + }; + callback(&mut router); + }); + } + } + impl<'a, 'b, C, P> GetOpenapi for OpenapiRouter<'a, $implType<'b, C, P>> where C : PipelineHandleChain

+ Copy + Send + Sync + 'static, @@ -57,7 +80,7 @@ macro_rules! implOpenapiRouter { { let schema = (self.0).openapi_builder.add_schema::(); - let path = format!("/{}", &self.1); + let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1); let mut item = (self.0).openapi_builder.remove_path(&path); item.get = Some(OperationDescription::new::(schema).into_operation()); (self.0).openapi_builder.add_path(path, item); @@ -70,7 +93,7 @@ macro_rules! implOpenapiRouter { let schema = (self.0).openapi_builder.add_schema::(); let id_schema = (self.0).openapi_builder.add_schema::(); - let path = format!("/{}/{{id}}", &self.1); + let path = format!("{}/{}/{{id}}", self.0.scope.unwrap_or_default(), self.1); let mut item = (self.0).openapi_builder.remove_path(&path); item.get = Some(OperationDescription::new::(schema).add_path_param("id", id_schema).into_operation()); (self.0).openapi_builder.add_path(path, item); @@ -82,8 +105,8 @@ macro_rules! implOpenapiRouter { { let schema = (self.0).openapi_builder.add_schema::(); - let path = format!("/{}/search", &self.1); - let mut item = (self.0).openapi_builder.remove_path(&self.1); + let path = format!("{}/{}/search", self.0.scope.unwrap_or_default(), self.1); + let mut item = (self.0).openapi_builder.remove_path(&path); item.get = Some(OperationDescription::new::(schema).with_query_params(Handler::Query::schema()).into_operation()); (self.0).openapi_builder.add_path(path, item); @@ -98,7 +121,7 @@ macro_rules! implOpenapiRouter { let schema = (self.0).openapi_builder.add_schema::(); let body_schema = (self.0).openapi_builder.add_schema::(); - let path = format!("/{}", &self.1); + let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1); let mut item = (self.0).openapi_builder.remove_path(&path); item.post = Some(OperationDescription::new::(schema).with_body::(body_schema).into_operation()); (self.0).openapi_builder.add_path(path, item); @@ -114,7 +137,7 @@ macro_rules! implOpenapiRouter { let schema = (self.0).openapi_builder.add_schema::(); let body_schema = (self.0).openapi_builder.add_schema::(); - let path = format!("/{}", &self.1); + let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1); let mut item = (self.0).openapi_builder.remove_path(&path); item.put = Some(OperationDescription::new::(schema).with_body::(body_schema).into_operation()); (self.0).openapi_builder.add_path(path, item); @@ -131,7 +154,7 @@ macro_rules! implOpenapiRouter { let id_schema = (self.0).openapi_builder.add_schema::(); let body_schema = (self.0).openapi_builder.add_schema::(); - let path = format!("/{}/{{id}}", &self.1); + let path = format!("{}/{}/{{id}}", self.0.scope.unwrap_or_default(), self.1); let mut item = (self.0).openapi_builder.remove_path(&path); item.put = Some(OperationDescription::new::(schema).add_path_param("id", id_schema).with_body::(body_schema).into_operation()); (self.0).openapi_builder.add_path(path, item); @@ -143,7 +166,7 @@ macro_rules! implOpenapiRouter { { let schema = (self.0).openapi_builder.add_schema::(); - let path = format!("/{}", &self.1); + let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1); let mut item = (self.0).openapi_builder.remove_path(&path); item.delete = Some(OperationDescription::new::(schema).into_operation()); (self.0).openapi_builder.add_path(path, item); @@ -156,7 +179,7 @@ macro_rules! implOpenapiRouter { let schema = (self.0).openapi_builder.add_schema::(); let id_schema = (self.0).openapi_builder.add_schema::(); - let path = format!("/{}/{{id}}", &self.1); + let path = format!("{}/{}/{{id}}", self.0.scope.unwrap_or_default(), self.1); let mut item = (self.0).openapi_builder.remove_path(&path); item.delete = Some(OperationDescription::new::(schema).add_path_param("id", id_schema).into_operation()); (self.0).openapi_builder.add_path(path, item); diff --git a/gotham_restful/src/routing.rs b/gotham_restful/src/routing.rs index 1714a32..1b0aa46 100644 --- a/gotham_restful/src/routing.rs +++ b/gotham_restful/src/routing.rs @@ -324,6 +324,7 @@ macro_rules! implDrawResourceRoutes { { let router = OpenapiRouter { router: self, + scope: None, openapi_builder: &mut OpenapiBuilder::new(info) }; block(router); diff --git a/gotham_restful/tests/openapi_supports_scope.rs b/gotham_restful/tests/openapi_supports_scope.rs new file mode 100644 index 0000000..02ba509 --- /dev/null +++ b/gotham_restful/tests/openapi_supports_scope.rs @@ -0,0 +1,61 @@ +#[cfg(feature = "openapi")] +mod openapi_supports_scope +{ + + +use gotham::{ + router::builder::*, + test::TestServer +}; +use gotham_restful::*; +use mime::TEXT_PLAIN; + +const RESPONSE : &[u8] = b"This is the only valid response."; + +#[derive(Resource)] +#[resource(read_all)] +struct FooResource; + +#[read_all(FooResource)] +fn read_all() -> Raw<&'static [u8]> +{ + Raw::new(RESPONSE, TEXT_PLAIN) +} + + +fn test_response(server : &TestServer, path : &str) +{ + let res = server.client().get(path).perform().unwrap().read_body().unwrap(); + let body : &[u8] = res.as_ref(); + assert_eq!(body, RESPONSE); +} + +#[test] +fn test() +{ + let info = OpenapiInfo { + title: "Test".to_owned(), + version: "1.2.3".to_owned(), + urls: Vec::new() + }; + let server = TestServer::new(build_simple_router(|router| { + router.with_openapi(info, |mut router| { + router.resource::("foo1"); + router.scope("/bar", |router| { + router.resource::("foo2"); + router.scope("/baz", |router| { + router.resource::("foo3"); + }) + }); + router.resource::("foo4"); + }); + })).unwrap(); + + test_response(&server, "http://localhost/foo1"); + test_response(&server, "http://localhost/bar/foo2"); + test_response(&server, "http://localhost/bar/baz/foo3"); + test_response(&server, "http://localhost/foo4"); +} + + +} // mod test From aa9fa0f4574f8bad7a8f4259c97af08c33fd4936 Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 5 May 2020 19:31:02 +0200 Subject: [PATCH 047/170] doc & ui-test for FromBody --- gotham_restful/Cargo.toml | 1 + gotham_restful/src/types.rs | 34 +++++++++++++++---- gotham_restful/tests/ui.rs | 10 ++++++ gotham_restful/tests/ui/from_body_enum.rs | 12 +++++++ gotham_restful/tests/ui/from_body_enum.stderr | 5 +++ gotham_restful_derive/src/from_body.rs | 20 +++++------ 6 files changed, 66 insertions(+), 16 deletions(-) create mode 100644 gotham_restful/tests/ui.rs create mode 100644 gotham_restful/tests/ui/from_body_enum.rs create mode 100644 gotham_restful/tests/ui/from_body_enum.stderr diff --git a/gotham_restful/Cargo.toml b/gotham_restful/Cargo.toml index 836cd19..a571a2d 100644 --- a/gotham_restful/Cargo.toml +++ b/gotham_restful/Cargo.toml @@ -39,6 +39,7 @@ uuid = { version = "0.8.1", optional = true } diesel = { version = "1.4.4", features = ["postgres"] } futures-executor = "0.3.4" paste = "0.1.12" +trybuild = "1.0.26" [features] default = ["errorlog"] diff --git a/gotham_restful/src/types.rs b/gotham_restful/src/types.rs index 504cf94..6c3d635 100644 --- a/gotham_restful/src/types.rs +++ b/gotham_restful/src/types.rs @@ -43,13 +43,36 @@ impl ResponseBody for T } -/// This trait must be implemented by every type that can be used as a request body. It allows -/// to create the type from a hyper body chunk and it's content type. +/** +This trait should be implemented for every type that can be built from an HTTP request body +plus its media type. For most use cases it is sufficient to derive this trait, you usually +don't need to manually implement this. Therefore, make sure that the first variable of +your struct can be built from [`Bytes`], and the second one can be build from [`Mime`]. +If you have any additional variables, they need to be `Default`. This is an example of +such a struct: + +```rust +# #[macro_use] extern crate gotham_restful; +# use gotham_restful::*; +#[derive(FromBody, RequestBody)] +#[supported_types(mime::IMAGE_GIF, mime::IMAGE_JPEG, mime::IMAGE_PNG)] +struct RawImage { + content: Vec, + content_type: Mime +} +``` + + [`Bytes`]: ../bytes/struct.Bytes.html + [`Mime`]: ../mime/struct.Mime.html +*/ pub trait FromBody : Sized { + /// 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 + /// [`FromBodyNoError`](struct.FromBodyNoError.html) is used here. type Err : Error; - /// Create the request body from a raw body and the content type. + /// Perform the conversion. fn from_body(body : Bytes, content_type : Mime) -> Result; } @@ -63,9 +86,8 @@ impl FromBody for T } } -/// This error type can be used by `FromBody` implementations when there is no need to return any -/// errors. - +/// This error type can be used by [`FromBody`](trait.FromBody.html) implementations when there +/// is no need to return any errors. #[derive(Clone, Copy, Debug, Error)] #[error("No Error")] pub struct FromBodyNoError; diff --git a/gotham_restful/tests/ui.rs b/gotham_restful/tests/ui.rs new file mode 100644 index 0000000..04e6311 --- /dev/null +++ b/gotham_restful/tests/ui.rs @@ -0,0 +1,10 @@ +use trybuild::TestCases; + +#[test] +fn ui() +{ + let t = TestCases::new(); + + // always enabled + t.compile_fail("tests/ui/from_body_enum.rs"); +} \ No newline at end of file diff --git a/gotham_restful/tests/ui/from_body_enum.rs b/gotham_restful/tests/ui/from_body_enum.rs new file mode 100644 index 0000000..24eb9db --- /dev/null +++ b/gotham_restful/tests/ui/from_body_enum.rs @@ -0,0 +1,12 @@ +#[macro_use] extern crate gotham_restful; + +#[derive(FromBody)] +enum FromBodyEnum +{ + SomeVariant(Vec), + OtherVariant(String) +} + +fn main() +{ +} diff --git a/gotham_restful/tests/ui/from_body_enum.stderr b/gotham_restful/tests/ui/from_body_enum.stderr new file mode 100644 index 0000000..26cab8b --- /dev/null +++ b/gotham_restful/tests/ui/from_body_enum.stderr @@ -0,0 +1,5 @@ +error: #[derive(FromBody)] only works for structs + --> $DIR/from_body_enum.rs:4:1 + | +4 | enum FromBodyEnum + | ^^^^ diff --git a/gotham_restful_derive/src/from_body.rs b/gotham_restful_derive/src/from_body.rs index e57c14c..6d48201 100644 --- a/gotham_restful_derive/src/from_body.rs +++ b/gotham_restful_derive/src/from_body.rs @@ -21,25 +21,25 @@ struct ParsedFields impl ParsedFields { - fn from_named(fields : I) -> Result + fn from_named(fields : I) -> Self where I : Iterator { let fields = fields.map(|field| (field.ident.unwrap(), field.ty)).collect(); - Ok(Self { fields, named: true }) + Self { fields, named: true } } - fn from_unnamed(fields : I) -> Result + fn from_unnamed(fields : I) -> Self where I : Iterator { let fields = fields.enumerate().map(|(i, field)| (format_ident!("arg{}", i), field.ty)).collect(); - Ok(Self { fields, named: false }) + Self { fields, named: false } } - fn from_unit() -> Result + fn from_unit() -> Self { - Ok(Self { fields: Vec::new(), named: false }) + Self { fields: Vec::new(), named: false } } } @@ -53,12 +53,12 @@ pub fn expand_from_body(input : DeriveInput) -> Result Data::Enum(inum) => Err(inum.enum_token.span()), Data::Struct(strukt) => Ok(strukt), Data::Union(uni) => Err(uni.union_token.span()) - }.map_err(|span| Error::new(span, "#[derive(FromBody)] only works for enums"))?; + }.map_err(|span| Error::new(span, "#[derive(FromBody)] only works for structs"))?; let fields = match strukt.fields { - Fields::Named(named) => ParsedFields::from_named(named.named.into_iter())?, - Fields::Unnamed(unnamed) => ParsedFields::from_unnamed(unnamed.unnamed.into_iter())?, - Fields::Unit => ParsedFields::from_unit()? + Fields::Named(named) => ParsedFields::from_named(named.named.into_iter()), + Fields::Unnamed(unnamed) => ParsedFields::from_unnamed(unnamed.unnamed.into_iter()), + Fields::Unit => ParsedFields::from_unit() }; let mut where_clause = quote!(); From e5f13792c66a0fe1852a894e7ba0b849c908201e Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 5 May 2020 19:50:23 +0200 Subject: [PATCH 048/170] doc & test for RequestBody --- gotham_restful/src/types.rs | 27 ++++++++++-- gotham_restful/tests/custom_request_body.rs | 49 +++++++++++++++++++++ 2 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 gotham_restful/tests/custom_request_body.rs diff --git a/gotham_restful/src/types.rs b/gotham_restful/src/types.rs index 6c3d635..c858d3b 100644 --- a/gotham_restful/src/types.rs +++ b/gotham_restful/src/types.rs @@ -93,12 +93,31 @@ impl FromBody for T pub struct FromBodyNoError; -/// 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`. +/** +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`]. + +If you want a non-deserializable type to be used as a request body, e.g. because you'd like to +get the raw data, you can derive it for your own type. All you need is to have a type implementing +[`FromBody`] and optionally a list of supported media types: + +```rust +# #[macro_use] extern crate gotham_restful; +# use gotham_restful::*; +#[derive(FromBody, RequestBody)] +#[supported_types(mime::IMAGE_GIF, mime::IMAGE_JPEG, mime::IMAGE_PNG)] +struct RawImage { + content: Vec, + content_type: Mime +} +``` + + [`FromBody`]: trait.FromBody.html + [`OpenapiType`]: trait.OpenapiType.html +*/ pub trait RequestBody : ResourceType + FromBody { - /// Return all types that are supported as content types. + /// Return all types that are supported as content types. Use `None` if all types are supported. fn supported_types() -> Option> { None diff --git a/gotham_restful/tests/custom_request_body.rs b/gotham_restful/tests/custom_request_body.rs new file mode 100644 index 0000000..5b108c7 --- /dev/null +++ b/gotham_restful/tests/custom_request_body.rs @@ -0,0 +1,49 @@ +mod custom_request_body +{ + + +use gotham::{ + hyper::header::CONTENT_TYPE, + router::builder::*, + test::TestServer +}; +use gotham_restful::*; +use mime::TEXT_PLAIN; + +const RESPONSE : &[u8] = b"This is the only valid response."; + +#[derive(Resource)] +#[resource(create)] +struct FooResource; + +#[derive(FromBody, RequestBody)] +#[supported_types(TEXT_PLAIN)] +struct Foo { + content: Vec, + content_type: Mime +} + +#[create(FooResource)] +fn create(body : Foo) -> Raw> { + Raw::new(body.content, body.content_type) +} + + +#[test] +fn test() +{ + let server = TestServer::new(build_simple_router(|router| { + router.resource::("foo"); + })).unwrap(); + + let res = server.client() + .post("http://localhost/foo", RESPONSE, TEXT_PLAIN) + .perform().unwrap(); + assert_eq!(res.headers().get(CONTENT_TYPE).unwrap().to_str().unwrap(), "text/plain"); + let res = res.read_body().unwrap(); + let body : &[u8] = res.as_ref(); + assert_eq!(body, RESPONSE); +} + + +} From bccefa8248099a0ad3659f23ea92d8c82cf54592 Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 5 May 2020 19:57:13 +0200 Subject: [PATCH 049/170] make tarpaulin not timeout --- .gitlab-ci.yml | 3 ++- gotham_restful/tests/{ui.rs => trybuild_ui.rs} | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) rename gotham_restful/tests/{ui.rs => trybuild_ui.rs} (81%) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5ec18d8..7f9f002 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -12,7 +12,7 @@ test-default: before_script: - cargo -V script: - - cargo test --workspace --tests + - cargo test --workspace --tests -- --ignored cache: paths: - cargo/ @@ -26,6 +26,7 @@ test-all: - cargo -V script: - cargo test --workspace --all-features --doc + - cargo test trybuild_ui -- --ignored - cargo tarpaulin --target-dir target/tarpaulin --all --all-features --exclude-files 'cargo/*' --exclude-files 'gotham_restful_derive/*' --exclude-files 'example/*' --ignore-panics --ignore-tests --out Html -v artifacts: paths: diff --git a/gotham_restful/tests/ui.rs b/gotham_restful/tests/trybuild_ui.rs similarity index 81% rename from gotham_restful/tests/ui.rs rename to gotham_restful/tests/trybuild_ui.rs index 04e6311..ac7adff 100644 --- a/gotham_restful/tests/ui.rs +++ b/gotham_restful/tests/trybuild_ui.rs @@ -1,10 +1,11 @@ use trybuild::TestCases; #[test] -fn ui() +#[ignore] +fn trybuild_ui() { let t = TestCases::new(); // always enabled t.compile_fail("tests/ui/from_body_enum.rs"); -} \ No newline at end of file +} From 4cd2474d9042f42c481f8b28286117b733e41b7c Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 5 May 2020 23:05:17 +0200 Subject: [PATCH 050/170] add documentation and some traits for Raw --- gotham_restful/src/result/raw.rs | 67 +++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/gotham_restful/src/result/raw.rs b/gotham_restful/src/result/raw.rs index f41ffe6..ec3c848 100644 --- a/gotham_restful/src/result/raw.rs +++ b/gotham_restful/src/result/raw.rs @@ -1,10 +1,10 @@ use super::{IntoResponseError, ResourceResult, handle_error}; -use crate::{Response, StatusCode}; +use crate::{FromBody, FromBodyNoError, RequestBody, ResourceType, Response, StatusCode}; #[cfg(feature = "openapi")] use crate::OpenapiSchema; use futures_core::future::Future; use futures_util::{future, future::FutureExt}; -use gotham::hyper::Body; +use gotham::hyper::body::{Body, Bytes}; use mime::Mime; #[cfg(feature = "openapi")] use openapiv3::{SchemaKind, StringFormat, StringType, Type, VariantOrUnknownOrEmpty}; @@ -14,6 +14,33 @@ use std::{ pin::Pin }; +/** +This type can be used both as a raw request body, as well as as a raw response. However, all types +of request bodies are accepted by this type. It is therefore recommended to derive your own type +from [`RequestBody`] and only use this when you need to return a raw response. This is a usage +example that simply returns its body: + +```rust,no_run +# #[macro_use] extern crate gotham_restful_derive; +# use gotham::router::builder::*; +# use gotham_restful::*; +#[derive(Resource)] +#[resource(create)] +struct ImageResource; + +#[create(ImageResource)] +fn create(body : Raw>) -> Raw> { + body +} +# fn main() { +# gotham::start("127.0.0.1:8080", build_simple_router(|route| { +# route.resource::("img"); +# })); +# } +``` + + [`OpenapiType`]: trait.OpenapiType.html +*/ #[derive(Debug)] pub struct Raw { @@ -29,6 +56,26 @@ impl Raw } } +impl AsMut for Raw +where + T : AsMut +{ + fn as_mut(&mut self) -> &mut U + { + self.raw.as_mut() + } +} + +impl AsRef for Raw +where + T : AsRef +{ + fn as_ref(&self) -> &U + { + self.raw.as_ref() + } +} + impl Clone for Raw { fn clone(&self) -> Self @@ -40,6 +87,22 @@ impl Clone for Raw } } +impl From<&'a [u8]>> FromBody for Raw +{ + type Err = FromBodyNoError; + + fn from_body(body : Bytes, mime : Mime) -> Result + { + Ok(Self::new(body.as_ref().into(), mime)) + } +} + +impl RequestBody for Raw +where + Raw : FromBody + ResourceType +{ +} + impl> ResourceResult for Raw where Self : Send From 52679ad29db63e5e1bf51a44aa431ebf5d228ec4 Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 5 May 2020 23:08:57 +0200 Subject: [PATCH 051/170] remove FromBodyNoError and replace with std::convert::Infallible --- gotham_restful/src/result/raw.rs | 5 +++-- gotham_restful/src/types.rs | 11 ++--------- gotham_restful_derive/src/from_body.rs | 4 ++-- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/gotham_restful/src/result/raw.rs b/gotham_restful/src/result/raw.rs index ec3c848..a44e15a 100644 --- a/gotham_restful/src/result/raw.rs +++ b/gotham_restful/src/result/raw.rs @@ -1,5 +1,5 @@ use super::{IntoResponseError, ResourceResult, handle_error}; -use crate::{FromBody, FromBodyNoError, RequestBody, ResourceType, Response, StatusCode}; +use crate::{FromBody, RequestBody, ResourceType, Response, StatusCode}; #[cfg(feature = "openapi")] use crate::OpenapiSchema; use futures_core::future::Future; @@ -10,6 +10,7 @@ use mime::Mime; use openapiv3::{SchemaKind, StringFormat, StringType, Type, VariantOrUnknownOrEmpty}; use serde_json::error::Error as SerdeJsonError; use std::{ + convert::Infallible, fmt::Display, pin::Pin }; @@ -89,7 +90,7 @@ impl Clone for Raw impl From<&'a [u8]>> FromBody for Raw { - type Err = FromBodyNoError; + type Err = Infallible; fn from_body(body : Bytes, mime : Mime) -> Result { diff --git a/gotham_restful/src/types.rs b/gotham_restful/src/types.rs index c858d3b..576f7e9 100644 --- a/gotham_restful/src/types.rs +++ b/gotham_restful/src/types.rs @@ -8,7 +8,6 @@ use std::{ error::Error, panic::RefUnwindSafe }; -use thiserror::Error; #[cfg(not(feature = "openapi"))] pub trait ResourceType @@ -68,8 +67,8 @@ struct RawImage { pub trait FromBody : Sized { /// 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 - /// [`FromBodyNoError`](struct.FromBodyNoError.html) is used here. + /// macro, there is no way to trigger an error, so `Infallible` is used here. However, this + /// might change in the future. type Err : Error; /// Perform the conversion. @@ -86,12 +85,6 @@ impl FromBody for T } } -/// This error type can be used by [`FromBody`](trait.FromBody.html) implementations when there -/// is no need to return any errors. -#[derive(Clone, Copy, Debug, Error)] -#[error("No Error")] -pub struct FromBodyNoError; - /** A type that can be used inside a request body. Implemented for every type that is deserializable diff --git a/gotham_restful_derive/src/from_body.rs b/gotham_restful_derive/src/from_body.rs index 6d48201..f7c04ad 100644 --- a/gotham_restful_derive/src/from_body.rs +++ b/gotham_restful_derive/src/from_body.rs @@ -120,9 +120,9 @@ pub fn expand_from_body(input : DeriveInput) -> Result impl #generics #krate::FromBody for #ident #generics where #where_clause { - type Err = #krate::FromBodyNoError; + type Err = ::std::convert::Infallible; - fn from_body(#body_ident : #krate::gotham::hyper::body::Bytes, #type_ident : #krate::Mime) -> Result + fn from_body(#body_ident : #krate::gotham::hyper::body::Bytes, #type_ident : #krate::Mime) -> Result { #block Ok(#ctor) From 5587ded60dd1211c47204b42fe3a4d2e3d765267 Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 5 May 2020 23:18:05 +0200 Subject: [PATCH 052/170] merge workspace and main crate --- .gitlab-ci.yml | 4 +- Cargo.toml | 61 ++++++++++++++++--- {gotham_restful_derive => derive}/Cargo.toml | 4 +- {gotham_restful => derive}/LICENSE-Apache | 0 {gotham_restful => derive}/LICENSE-EPL | 0 {gotham_restful => derive}/LICENSE.md | 0 .../src/from_body.rs | 0 {gotham_restful_derive => derive}/src/lib.rs | 0 .../src/method.rs | 0 .../src/openapi_type.rs | 0 .../src/request_body.rs | 0 .../src/resource.rs | 0 .../src/resource_error.rs | 0 {gotham_restful_derive => derive}/src/util.rs | 0 gotham_restful/Cargo.toml | 52 ---------------- gotham_restful/README.md | 1 - gotham_restful_derive/LICENSE-Apache | 1 - gotham_restful_derive/LICENSE-EPL | 1 - gotham_restful_derive/LICENSE.md | 1 - {gotham_restful/src => src}/auth.rs | 0 {gotham_restful/src => src}/lib.rs | 0 {gotham_restful/src => src}/matcher/accept.rs | 0 .../src => src}/matcher/content_type.rs | 0 {gotham_restful/src => src}/matcher/mod.rs | 0 .../src => src}/openapi/builder.rs | 0 .../src => src}/openapi/handler.rs | 0 {gotham_restful/src => src}/openapi/mod.rs | 0 .../src => src}/openapi/operation.rs | 0 {gotham_restful/src => src}/openapi/router.rs | 0 {gotham_restful/src => src}/openapi/types.rs | 0 {gotham_restful/src => src}/resource.rs | 0 {gotham_restful/src => src}/response.rs | 0 .../src => src}/result/auth_result.rs | 0 {gotham_restful/src => src}/result/mod.rs | 0 .../src => src}/result/no_content.rs | 0 {gotham_restful/src => src}/result/raw.rs | 0 {gotham_restful/src => src}/result/result.rs | 0 {gotham_restful/src => src}/result/success.rs | 0 {gotham_restful/src => src}/routing.rs | 0 {gotham_restful/src => src}/types.rs | 0 .../tests => tests}/custom_request_body.rs | 0 .../tests => tests}/openapi_supports_scope.rs | 0 .../tests => tests}/trybuild_ui.rs | 0 .../tests => tests}/ui/from_body_enum.rs | 0 .../tests => tests}/ui/from_body_enum.stderr | 0 45 files changed, 58 insertions(+), 67 deletions(-) rename {gotham_restful_derive => derive}/Cargo.toml (79%) rename {gotham_restful => derive}/LICENSE-Apache (100%) rename {gotham_restful => derive}/LICENSE-EPL (100%) rename {gotham_restful => derive}/LICENSE.md (100%) rename {gotham_restful_derive => derive}/src/from_body.rs (100%) rename {gotham_restful_derive => derive}/src/lib.rs (100%) rename {gotham_restful_derive => derive}/src/method.rs (100%) rename {gotham_restful_derive => derive}/src/openapi_type.rs (100%) rename {gotham_restful_derive => derive}/src/request_body.rs (100%) rename {gotham_restful_derive => derive}/src/resource.rs (100%) rename {gotham_restful_derive => derive}/src/resource_error.rs (100%) rename {gotham_restful_derive => derive}/src/util.rs (100%) delete mode 100644 gotham_restful/Cargo.toml delete mode 120000 gotham_restful/README.md delete mode 120000 gotham_restful_derive/LICENSE-Apache delete mode 120000 gotham_restful_derive/LICENSE-EPL delete mode 120000 gotham_restful_derive/LICENSE.md rename {gotham_restful/src => src}/auth.rs (100%) rename {gotham_restful/src => src}/lib.rs (100%) rename {gotham_restful/src => src}/matcher/accept.rs (100%) rename {gotham_restful/src => src}/matcher/content_type.rs (100%) rename {gotham_restful/src => src}/matcher/mod.rs (100%) rename {gotham_restful/src => src}/openapi/builder.rs (100%) rename {gotham_restful/src => src}/openapi/handler.rs (100%) rename {gotham_restful/src => src}/openapi/mod.rs (100%) rename {gotham_restful/src => src}/openapi/operation.rs (100%) rename {gotham_restful/src => src}/openapi/router.rs (100%) rename {gotham_restful/src => src}/openapi/types.rs (100%) rename {gotham_restful/src => src}/resource.rs (100%) rename {gotham_restful/src => src}/response.rs (100%) rename {gotham_restful/src => src}/result/auth_result.rs (100%) rename {gotham_restful/src => src}/result/mod.rs (100%) rename {gotham_restful/src => src}/result/no_content.rs (100%) rename {gotham_restful/src => src}/result/raw.rs (100%) rename {gotham_restful/src => src}/result/result.rs (100%) rename {gotham_restful/src => src}/result/success.rs (100%) rename {gotham_restful/src => src}/routing.rs (100%) rename {gotham_restful/src => src}/types.rs (100%) rename {gotham_restful/tests => tests}/custom_request_body.rs (100%) rename {gotham_restful/tests => tests}/openapi_supports_scope.rs (100%) rename {gotham_restful/tests => tests}/trybuild_ui.rs (100%) rename {gotham_restful/tests => tests}/ui/from_body_enum.rs (100%) rename {gotham_restful/tests => tests}/ui/from_body_enum.stderr (100%) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7f9f002..c50b7f5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -27,7 +27,7 @@ test-all: script: - cargo test --workspace --all-features --doc - cargo test trybuild_ui -- --ignored - - cargo tarpaulin --target-dir target/tarpaulin --all --all-features --exclude-files 'cargo/*' --exclude-files 'gotham_restful_derive/*' --exclude-files 'example/*' --ignore-panics --ignore-tests --out Html -v + - cargo tarpaulin --target-dir target/tarpaulin --all --all-features --exclude-files 'cargo/*' --exclude-files 'derive/*' --exclude-files 'example/*' --ignore-panics --ignore-tests --out Html -v artifacts: paths: - tarpaulin-report.html @@ -40,7 +40,7 @@ readme: stage: test image: msrd0/cargo-readme script: - - cargo readme -r gotham_restful -t ../README.tpl >README.md.new + - cargo readme -t README.tpl >README.md.new - diff README.md README.md.new publish: diff --git a/Cargo.toml b/Cargo.toml index 8efb671..59519a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,60 @@ # -*- eval: (cargo-minor-mode 1) -*- [workspace] -members = [ - "gotham_restful", - "gotham_restful_derive", - "example" -] +members = ["derive", "example"] + +[package] +name = "gotham_restful" +version = "0.1.0-dev" +authors = ["Dominic Meiser "] +edition = "2018" +description = "RESTful additions for the gotham web framework" +keywords = ["gotham", "rest", "restful", "web", "http"] +license = "EPL-2.0 OR Apache-2.0" +readme = "README.md" +repository = "https://gitlab.com/msrd0/gotham-restful" + +[badges] +gitlab = { repository = "msrd0/gotham-restful", branch = "master" } + +[dependencies] +base64 = { version = "0.12.0", optional = true } +chrono = { version = "0.4.11", optional = true } +cookie = { version = "0.13.3", optional = true } +futures-core = "0.3.4" +futures-util = "0.3.4" +gotham = { git = "https://github.com/gotham-rs/gotham", version = "0.5.0-dev", default-features = false } +gotham_derive = { git = "https://github.com/gotham-rs/gotham", version = "0.5.0-dev" } +gotham_middleware_diesel = { git = "https://github.com/gotham-rs/gotham", version = "0.1.0", optional = true } +gotham_restful_derive = { version = "0.1.0-dev" } +indexmap = { version = "1.3.2", optional = true } +itertools = "0.9.0" +jsonwebtoken = { version = "7.1.0", optional = true } +log = "0.4.8" +mime = "0.3.16" +openapiv3 = { version = "0.3", optional = true } +serde = { version = "1.0.106", features = ["derive"] } +serde_json = "1.0.52" +thiserror = "1.0.16" +uuid = { version = "0.8.1", optional = true } + +[dev-dependencies] +diesel = { version = "1.4.4", features = ["postgres"] } +futures-executor = "0.3.4" +paste = "0.1.12" +trybuild = "1.0.26" + +[features] +default = ["errorlog"] +auth = ["gotham_restful_derive/auth", "base64", "cookie", "jsonwebtoken"] +errorlog = [] +database = ["gotham_restful_derive/database", "gotham_middleware_diesel"] +openapi = ["gotham_restful_derive/openapi", "indexmap", "openapiv3"] + +[package.metadata.docs.rs] +all-features = true [patch.crates-io] -gotham_restful = { path = "./gotham_restful" } -gotham_restful_derive = { path = "./gotham_restful_derive" } +gotham_restful = { path = "." } +gotham_restful_derive = { path = "./derive" } openapiv3 = { git = "https://github.com/glademiller/openapiv3", rev = "4c3bd95c966a3f9d59bb494c3d8e30c5c3068bdb" } diff --git a/gotham_restful_derive/Cargo.toml b/derive/Cargo.toml similarity index 79% rename from gotham_restful_derive/Cargo.toml rename to derive/Cargo.toml index 275b490..62678f0 100644 --- a/gotham_restful_derive/Cargo.toml +++ b/derive/Cargo.toml @@ -5,8 +5,8 @@ name = "gotham_restful_derive" version = "0.1.0-dev" authors = ["Dominic Meiser "] edition = "2018" -description = "RESTful additions for Gotham - Derive" -keywords = ["gotham", "rest", "restful", "derive"] +description = "RESTful additions for the gotham web framework - Derive" +keywords = ["gotham", "rest", "restful", "web", "http", "derive"] license = "EPL-2.0 OR Apache-2.0" repository = "https://gitlab.com/msrd0/gotham-restful" diff --git a/gotham_restful/LICENSE-Apache b/derive/LICENSE-Apache similarity index 100% rename from gotham_restful/LICENSE-Apache rename to derive/LICENSE-Apache diff --git a/gotham_restful/LICENSE-EPL b/derive/LICENSE-EPL similarity index 100% rename from gotham_restful/LICENSE-EPL rename to derive/LICENSE-EPL diff --git a/gotham_restful/LICENSE.md b/derive/LICENSE.md similarity index 100% rename from gotham_restful/LICENSE.md rename to derive/LICENSE.md diff --git a/gotham_restful_derive/src/from_body.rs b/derive/src/from_body.rs similarity index 100% rename from gotham_restful_derive/src/from_body.rs rename to derive/src/from_body.rs diff --git a/gotham_restful_derive/src/lib.rs b/derive/src/lib.rs similarity index 100% rename from gotham_restful_derive/src/lib.rs rename to derive/src/lib.rs diff --git a/gotham_restful_derive/src/method.rs b/derive/src/method.rs similarity index 100% rename from gotham_restful_derive/src/method.rs rename to derive/src/method.rs diff --git a/gotham_restful_derive/src/openapi_type.rs b/derive/src/openapi_type.rs similarity index 100% rename from gotham_restful_derive/src/openapi_type.rs rename to derive/src/openapi_type.rs diff --git a/gotham_restful_derive/src/request_body.rs b/derive/src/request_body.rs similarity index 100% rename from gotham_restful_derive/src/request_body.rs rename to derive/src/request_body.rs diff --git a/gotham_restful_derive/src/resource.rs b/derive/src/resource.rs similarity index 100% rename from gotham_restful_derive/src/resource.rs rename to derive/src/resource.rs diff --git a/gotham_restful_derive/src/resource_error.rs b/derive/src/resource_error.rs similarity index 100% rename from gotham_restful_derive/src/resource_error.rs rename to derive/src/resource_error.rs diff --git a/gotham_restful_derive/src/util.rs b/derive/src/util.rs similarity index 100% rename from gotham_restful_derive/src/util.rs rename to derive/src/util.rs diff --git a/gotham_restful/Cargo.toml b/gotham_restful/Cargo.toml deleted file mode 100644 index a571a2d..0000000 --- a/gotham_restful/Cargo.toml +++ /dev/null @@ -1,52 +0,0 @@ -# -*- eval: (cargo-minor-mode 1) -*- - -[package] -name = "gotham_restful" -version = "0.1.0-dev" -authors = ["Dominic Meiser "] -edition = "2018" -description = "RESTful additions for Gotham" -keywords = ["gotham", "rest", "restful"] -license = "EPL-2.0 OR Apache-2.0" -readme = "README.md" -repository = "https://gitlab.com/msrd0/gotham-restful" - -[badges] -gitlab = { repository = "msrd0/gotham-restful", branch = "master" } - -[dependencies] -base64 = { version = "0.12.0", optional = true } -chrono = { version = "0.4.11", optional = true } -cookie = { version = "0.13.3", optional = true } -futures-core = "0.3.4" -futures-util = "0.3.4" -gotham = { git = "https://github.com/gotham-rs/gotham", version = "0.5.0-dev", default-features = false } -gotham_derive = { git = "https://github.com/gotham-rs/gotham", version = "0.5.0-dev" } -gotham_middleware_diesel = { git = "https://github.com/gotham-rs/gotham", version = "0.1.0", optional = true } -gotham_restful_derive = { version = "0.1.0-dev" } -indexmap = { version = "1.3.2", optional = true } -itertools = "0.9.0" -jsonwebtoken = { version = "7.1.0", optional = true } -log = "0.4.8" -mime = "0.3.16" -openapiv3 = { version = "0.3", optional = true } -serde = { version = "1.0.106", features = ["derive"] } -serde_json = "1.0.52" -thiserror = "1.0.16" -uuid = { version = "0.8.1", optional = true } - -[dev-dependencies] -diesel = { version = "1.4.4", features = ["postgres"] } -futures-executor = "0.3.4" -paste = "0.1.12" -trybuild = "1.0.26" - -[features] -default = ["errorlog"] -auth = ["gotham_restful_derive/auth", "base64", "cookie", "jsonwebtoken"] -errorlog = [] -database = ["gotham_restful_derive/database", "gotham_middleware_diesel"] -openapi = ["gotham_restful_derive/openapi", "indexmap", "openapiv3"] - -[package.metadata.docs.rs] -all-features = true diff --git a/gotham_restful/README.md b/gotham_restful/README.md deleted file mode 120000 index 32d46ee..0000000 --- a/gotham_restful/README.md +++ /dev/null @@ -1 +0,0 @@ -../README.md \ No newline at end of file diff --git a/gotham_restful_derive/LICENSE-Apache b/gotham_restful_derive/LICENSE-Apache deleted file mode 120000 index 0cd69a3..0000000 --- a/gotham_restful_derive/LICENSE-Apache +++ /dev/null @@ -1 +0,0 @@ -../LICENSE-Apache \ No newline at end of file diff --git a/gotham_restful_derive/LICENSE-EPL b/gotham_restful_derive/LICENSE-EPL deleted file mode 120000 index 2004d06..0000000 --- a/gotham_restful_derive/LICENSE-EPL +++ /dev/null @@ -1 +0,0 @@ -../LICENSE-EPL \ No newline at end of file diff --git a/gotham_restful_derive/LICENSE.md b/gotham_restful_derive/LICENSE.md deleted file mode 120000 index 7eabdb1..0000000 --- a/gotham_restful_derive/LICENSE.md +++ /dev/null @@ -1 +0,0 @@ -../LICENSE.md \ No newline at end of file diff --git a/gotham_restful/src/auth.rs b/src/auth.rs similarity index 100% rename from gotham_restful/src/auth.rs rename to src/auth.rs diff --git a/gotham_restful/src/lib.rs b/src/lib.rs similarity index 100% rename from gotham_restful/src/lib.rs rename to src/lib.rs diff --git a/gotham_restful/src/matcher/accept.rs b/src/matcher/accept.rs similarity index 100% rename from gotham_restful/src/matcher/accept.rs rename to src/matcher/accept.rs diff --git a/gotham_restful/src/matcher/content_type.rs b/src/matcher/content_type.rs similarity index 100% rename from gotham_restful/src/matcher/content_type.rs rename to src/matcher/content_type.rs diff --git a/gotham_restful/src/matcher/mod.rs b/src/matcher/mod.rs similarity index 100% rename from gotham_restful/src/matcher/mod.rs rename to src/matcher/mod.rs diff --git a/gotham_restful/src/openapi/builder.rs b/src/openapi/builder.rs similarity index 100% rename from gotham_restful/src/openapi/builder.rs rename to src/openapi/builder.rs diff --git a/gotham_restful/src/openapi/handler.rs b/src/openapi/handler.rs similarity index 100% rename from gotham_restful/src/openapi/handler.rs rename to src/openapi/handler.rs diff --git a/gotham_restful/src/openapi/mod.rs b/src/openapi/mod.rs similarity index 100% rename from gotham_restful/src/openapi/mod.rs rename to src/openapi/mod.rs diff --git a/gotham_restful/src/openapi/operation.rs b/src/openapi/operation.rs similarity index 100% rename from gotham_restful/src/openapi/operation.rs rename to src/openapi/operation.rs diff --git a/gotham_restful/src/openapi/router.rs b/src/openapi/router.rs similarity index 100% rename from gotham_restful/src/openapi/router.rs rename to src/openapi/router.rs diff --git a/gotham_restful/src/openapi/types.rs b/src/openapi/types.rs similarity index 100% rename from gotham_restful/src/openapi/types.rs rename to src/openapi/types.rs diff --git a/gotham_restful/src/resource.rs b/src/resource.rs similarity index 100% rename from gotham_restful/src/resource.rs rename to src/resource.rs diff --git a/gotham_restful/src/response.rs b/src/response.rs similarity index 100% rename from gotham_restful/src/response.rs rename to src/response.rs diff --git a/gotham_restful/src/result/auth_result.rs b/src/result/auth_result.rs similarity index 100% rename from gotham_restful/src/result/auth_result.rs rename to src/result/auth_result.rs diff --git a/gotham_restful/src/result/mod.rs b/src/result/mod.rs similarity index 100% rename from gotham_restful/src/result/mod.rs rename to src/result/mod.rs diff --git a/gotham_restful/src/result/no_content.rs b/src/result/no_content.rs similarity index 100% rename from gotham_restful/src/result/no_content.rs rename to src/result/no_content.rs diff --git a/gotham_restful/src/result/raw.rs b/src/result/raw.rs similarity index 100% rename from gotham_restful/src/result/raw.rs rename to src/result/raw.rs diff --git a/gotham_restful/src/result/result.rs b/src/result/result.rs similarity index 100% rename from gotham_restful/src/result/result.rs rename to src/result/result.rs diff --git a/gotham_restful/src/result/success.rs b/src/result/success.rs similarity index 100% rename from gotham_restful/src/result/success.rs rename to src/result/success.rs diff --git a/gotham_restful/src/routing.rs b/src/routing.rs similarity index 100% rename from gotham_restful/src/routing.rs rename to src/routing.rs diff --git a/gotham_restful/src/types.rs b/src/types.rs similarity index 100% rename from gotham_restful/src/types.rs rename to src/types.rs diff --git a/gotham_restful/tests/custom_request_body.rs b/tests/custom_request_body.rs similarity index 100% rename from gotham_restful/tests/custom_request_body.rs rename to tests/custom_request_body.rs diff --git a/gotham_restful/tests/openapi_supports_scope.rs b/tests/openapi_supports_scope.rs similarity index 100% rename from gotham_restful/tests/openapi_supports_scope.rs rename to tests/openapi_supports_scope.rs diff --git a/gotham_restful/tests/trybuild_ui.rs b/tests/trybuild_ui.rs similarity index 100% rename from gotham_restful/tests/trybuild_ui.rs rename to tests/trybuild_ui.rs diff --git a/gotham_restful/tests/ui/from_body_enum.rs b/tests/ui/from_body_enum.rs similarity index 100% rename from gotham_restful/tests/ui/from_body_enum.rs rename to tests/ui/from_body_enum.rs diff --git a/gotham_restful/tests/ui/from_body_enum.stderr b/tests/ui/from_body_enum.stderr similarity index 100% rename from gotham_restful/tests/ui/from_body_enum.stderr rename to tests/ui/from_body_enum.stderr From b1801f2486a79a36f829ec7d7d5d9f0bcce78a15 Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 6 May 2020 13:46:22 +0200 Subject: [PATCH 053/170] update log4rs --- example/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/Cargo.toml b/example/Cargo.toml index f765a82..d594b89 100644 --- a/example/Cargo.toml +++ b/example/Cargo.toml @@ -19,5 +19,5 @@ gotham = { git = "https://github.com/gotham-rs/gotham", version = "0.5.0-dev", d gotham_derive = { git = "https://github.com/gotham-rs/gotham", version = "0.5.0-dev", default-features = false } gotham_restful = { version = "0.1.0-dev", features = ["auth", "openapi"] } log = "0.4.8" -log4rs = { version = "0.11", features = ["console_appender"], default-features = false } +log4rs = { version = "0.12", features = ["console_appender"], default-features = false } serde = "1.0.106" From e470f060e3a23a1bab842e5328354b4727dfcfe8 Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 6 May 2020 15:58:11 +0200 Subject: [PATCH 054/170] deploy documentation as gitlab pages --- .gitlab-ci.yml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c50b7f5..128171b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,6 +1,7 @@ stages: - test + - build - publish variables: @@ -43,6 +44,36 @@ readme: - cargo readme -t README.tpl >README.md.new - diff README.md README.md.new +doc: + stage: build + image: msrd0/rust:alpine + before_script: + - cargo -V + script: + - cargo doc --all-features + - echo 'The documentation is located here' >target/doc/index.html + artifacts: + paths: + - target/doc/ + cache: + paths: + - cargo/ + - target/ + only: + - master + +pages: + stage: publish + image: busybox + script: + - mv target/doc public + - mv tarpaulin-report.html public/coverage.html + artifacts: + paths: + - public + only: + - master + publish: stage: publish image: msrd0/rust:alpine From f8181bcb7ee53256e32701319f8782777d9343cf Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 6 May 2020 17:15:51 +0200 Subject: [PATCH 055/170] cargo test is stupid --- .gitlab-ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 128171b..8d233ce 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -13,6 +13,7 @@ test-default: before_script: - cargo -V script: + - cargo test --workspace --tests - cargo test --workspace --tests -- --ignored cache: paths: @@ -27,7 +28,7 @@ test-all: - cargo -V script: - cargo test --workspace --all-features --doc - - cargo test trybuild_ui -- --ignored + - cargo test --workspace --tests -- --ignored - cargo tarpaulin --target-dir target/tarpaulin --all --all-features --exclude-files 'cargo/*' --exclude-files 'derive/*' --exclude-files 'example/*' --ignore-panics --ignore-tests --out Html -v artifacts: paths: From ea80689db22832ad738bb9001a04477b272cd876 Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 6 May 2020 17:28:36 +0200 Subject: [PATCH 056/170] update badges --- README.md | 15 +++++++++------ README.tpl | 15 +++++++++------ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 2e6c996..abfb943 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,10 @@

gotham-restful


diff --git a/README.tpl b/README.tpl index 5be3b7f..2dfc072 100644 --- a/README.tpl +++ b/README.tpl @@ -2,10 +2,10 @@

gotham-restful


From 4bf0bd7b09557b1e1a1fafb897b70e6dad9e5622 Mon Sep 17 00:00:00 2001 From: Dominic Date: Fri, 8 May 2020 15:10:37 +0200 Subject: [PATCH 057/170] add design goals to readme --- README.md | 14 ++++++++++++-- README.tpl | 4 ++++ src/lib.rs | 18 ++++++++++++------ 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index abfb943..ae7da3d 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,18 @@ compatible with the new future / async stuff. This crate is an extension to the popular [gotham web framework][gotham] for Rust. It allows you to create resources with assigned methods that aim to be a more convenient way of creating handlers -for requests. Assuming you assign `/foobar` to your resource, you can implement the following -methods: +for requests. + +## Design Goals + +This is an opinionated framework on top of [gotham]. Unless your web server handles mostly JSON as +request/response bodies and does that in a RESTful way, this framework is probably a bad fit for +your application. The ultimate goal of gotham-restful is to provide a way to write a RESTful +web server in Rust as convenient as possible with the least amount of boilerplate neccessary. + +## Methods + +Assuming you assign `/foobar` to your resource, you can implement the following methods: | Method Name | Required Arguments | HTTP Verb | HTTP Path | | ----------- | ------------------ | --------- | ----------- | diff --git a/README.tpl b/README.tpl index 2dfc072..c87a454 100644 --- a/README.tpl +++ b/README.tpl @@ -26,4 +26,8 @@
+**Note:** The `stable` branch contains some bugfixes against the last release. The `master` +branch currently tracks gotham's master branch and the next release will use gotham 0.5.0 and be +compatible with the new future / async stuff. + {{readme}} diff --git a/src/lib.rs b/src/lib.rs index 1e01f08..1fa19fd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,14 +2,20 @@ #![warn(missing_debug_implementations, rust_2018_idioms)] #![deny(intra_doc_link_resolution_failure)] /*! -**Note:** The `stable` branch contains some bugfixes against the last release. The `master` -branch currently tracks gotham's master branch and the next release will use gotham 0.5.0 and be -compatible with the new future / async stuff. - This crate is an extension to the popular [gotham web framework][gotham] for Rust. It allows you to create resources with assigned methods that aim to be a more convenient way of creating handlers -for requests. Assuming you assign `/foobar` to your resource, you can implement the following -methods: +for requests. + +# Design Goals + +This is an opinionated framework on top of [gotham]. Unless your web server handles mostly JSON as +request/response bodies and does that in a RESTful way, this framework is probably a bad fit for +your application. The ultimate goal of gotham-restful is to provide a way to write a RESTful +web server in Rust as convenient as possible with the least amount of boilerplate neccessary. + +# Methods + +Assuming you assign `/foobar` to your resource, you can implement the following methods: | Method Name | Required Arguments | HTTP Verb | HTTP Path | | ----------- | ------------------ | --------- | ----------- | From e05f9bb9639658c159c8097316d8b8d6a631c986 Mon Sep 17 00:00:00 2001 From: Dominic Date: Fri, 8 May 2020 18:39:11 +0200 Subject: [PATCH 058/170] a whole bunch of tests for the method macros --- derive/src/lib.rs | 4 +- derive/src/method.rs | 15 ++- example/src/main.rs | 8 +- tests/async_methods.rs | 106 ++++++++++++++++++++ tests/custom_request_body.rs | 10 +- tests/openapi_supports_scope.rs | 20 ++-- tests/sync_methods.rs | 106 ++++++++++++++++++++ tests/trybuild_ui.rs | 7 ++ tests/ui/method_async_state.rs | 15 +++ tests/ui/method_async_state.stderr | 21 ++++ tests/ui/method_for_unknown_resource.rs | 10 ++ tests/ui/method_for_unknown_resource.stderr | 5 + tests/ui/method_no_resource.rs | 14 +++ tests/ui/method_no_resource.stderr | 15 +++ tests/ui/method_self.rs | 14 +++ tests/ui/method_self.stderr | 13 +++ tests/ui/method_too_few_args.rs | 14 +++ tests/ui/method_too_few_args.stderr | 13 +++ tests/ui/method_too_many_args.rs | 14 +++ tests/ui/method_too_many_args.stderr | 13 +++ tests/ui/method_unsafe.rs | 14 +++ tests/ui/method_unsafe.stderr | 13 +++ tests/util/mod.rs | 37 +++++++ 23 files changed, 473 insertions(+), 28 deletions(-) create mode 100644 tests/async_methods.rs create mode 100644 tests/sync_methods.rs create mode 100644 tests/ui/method_async_state.rs create mode 100644 tests/ui/method_async_state.stderr create mode 100644 tests/ui/method_for_unknown_resource.rs create mode 100644 tests/ui/method_for_unknown_resource.stderr create mode 100644 tests/ui/method_no_resource.rs create mode 100644 tests/ui/method_no_resource.stderr create mode 100644 tests/ui/method_self.rs create mode 100644 tests/ui/method_self.stderr create mode 100644 tests/ui/method_too_few_args.rs create mode 100644 tests/ui/method_too_few_args.stderr create mode 100644 tests/ui/method_too_many_args.rs create mode 100644 tests/ui/method_too_many_args.stderr create mode 100644 tests/ui/method_unsafe.rs create mode 100644 tests/ui/method_unsafe.stderr create mode 100644 tests/util/mod.rs diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 7645df9..7e0dc00 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -122,13 +122,13 @@ pub fn change(attr : TokenStream, item : TokenStream) -> TokenStream } #[proc_macro_attribute] -pub fn delete_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)) } #[proc_macro_attribute] -pub fn delete(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)) } diff --git a/derive/src/method.rs b/derive/src/method.rs index 789c50d..be19e0b 100644 --- a/derive/src/method.rs +++ b/derive/src/method.rs @@ -150,13 +150,18 @@ impl MethodArgumentType matches!(self, Self::AuthStatus(_) | Self::AuthStatusRef(_)) } - fn quote_ty(&self) -> Option + fn ty(&self) -> Option<&Type> { match self { - Self::MethodArg(ty) | Self::DatabaseConnection(ty) | Self::AuthStatus(ty) | Self::AuthStatusRef(ty) => Some(quote!(#ty)), + Self::MethodArg(ty) | Self::DatabaseConnection(ty) | Self::AuthStatus(ty) | Self::AuthStatusRef(ty) => Some(ty), _ => None } } + + fn quote_ty(&self) -> Option + { + self.ty().map(|ty| quote!(#ty)) + } } struct MethodArgument @@ -279,6 +284,10 @@ pub fn expand_method(method : Method, mut attrs : AttributeArgs, fun : ItemFn) - let krate = super::krate(); // parse attributes + if attrs.len() < 1 + { + return Err(Error::new(Span::call_site(), "Missing Resource struct. Example: #[read_all(MyResource)]")); + } let resource_path = match attrs.remove(0) { NestedMeta::Meta(Meta::Path(path)) => path, p => return Err(Error::new(p.span(), "Expected name of the Resource struct this method belongs to")) @@ -372,7 +381,7 @@ pub fn expand_method(method : Method, mut attrs : AttributeArgs, fun : ItemFn) - { if let Some(arg) = args.iter().find(|arg| (*arg).ty.is_state_ref()) { - return Err(Error::new(arg.span(), "async fn must not take &State as an argument as State is not Sync")); + return Err(Error::new(arg.span(), "async fn must not take &State as an argument as State is not Sync, consider boxing")); } block = quote!(#block.await); } diff --git a/example/src/main.rs b/example/src/main.rs index d300bd8..acb56d3 100644 --- a/example/src/main.rs +++ b/example/src/main.rs @@ -76,14 +76,14 @@ fn update(id : u64, body : User) info!("Change User {} to {}", id, body.username); } -#[delete_all(Users)] -fn delete_all() +#[remove_all(Users)] +fn remove_all() { info!("Delete all Users"); } -#[delete(Users)] -fn delete(id : u64) +#[remove(Users)] +fn remove(id : u64) { info!("Delete User {}", id); } diff --git a/tests/async_methods.rs b/tests/async_methods.rs new file mode 100644 index 0000000..42cbc25 --- /dev/null +++ b/tests/async_methods.rs @@ -0,0 +1,106 @@ +#[macro_use] extern crate gotham_derive; + +use gotham::{ + router::builder::*, + test::TestServer +}; +use gotham_restful::*; +use mime::{APPLICATION_JSON, TEXT_PLAIN}; +use serde::Deserialize; + +mod util { include!("util/mod.rs"); } +use util::{test_get_response, test_post_response, test_put_response, test_delete_response}; + + +#[derive(Resource)] +#[resource(read_all, read, search, create, change_all, change, remove_all, remove)] +struct FooResource; + +#[derive(Deserialize)] +#[cfg_attr(feature = "openapi", derive(OpenapiType))] +#[allow(dead_code)] +struct FooBody +{ + data : String +} + +#[derive(Deserialize, StateData, StaticResponseExtender)] +#[cfg_attr(feature = "openapi", derive(OpenapiType))] +#[allow(dead_code)] +struct FooSearch +{ + query : String +} + +const READ_ALL_RESPONSE : &[u8] = b"1ARwwSPVyOKpJKrYwqGgECPVWDl1BqajAAj7g7WJ3e"; +#[read_all(FooResource)] +async fn read_all() -> Raw<&'static [u8]> +{ + Raw::new(READ_ALL_RESPONSE, TEXT_PLAIN) +} + +const READ_RESPONSE : &[u8] = b"FEReHoeBKU17X2bBpVAd1iUvktFL43CDu0cFYHdaP9"; +#[read(FooResource)] +async fn read(_id : u64) -> Raw<&'static [u8]> +{ + Raw::new(READ_RESPONSE, TEXT_PLAIN) +} + +const SEARCH_RESPONSE : &[u8] = b"AWqcQUdBRHXKh3at4u79mdupOAfEbnTcx71ogCVF0E"; +#[search(FooResource)] +async fn search(_body : FooSearch) -> Raw<&'static [u8]> +{ + Raw::new(SEARCH_RESPONSE, TEXT_PLAIN) +} + +const CREATE_RESPONSE : &[u8] = b"y6POY7wOMAB0jBRBw0FJT7DOpUNbhmT8KdpQPLkI83"; +#[create(FooResource)] +async fn create(_body : FooBody) -> Raw<&'static [u8]> +{ + Raw::new(CREATE_RESPONSE, TEXT_PLAIN) +} + +const CHANGE_ALL_RESPONSE : &[u8] = b"QlbYg8gHE9OQvvk3yKjXJLTSXlIrg9mcqhfMXJmQkv"; +#[change_all(FooResource)] +async fn change_all(_body : FooBody) -> Raw<&'static [u8]> +{ + Raw::new(CHANGE_ALL_RESPONSE, TEXT_PLAIN) +} + +const CHANGE_RESPONSE : &[u8] = b"qGod55RUXkT1lgPO8h0uVM6l368O2S0GrwENZFFuRu"; +#[change(FooResource)] +async fn change(_id : u64, _body : FooBody) -> Raw<&'static [u8]> +{ + Raw::new(CHANGE_RESPONSE, TEXT_PLAIN) +} + +const REMOVE_ALL_RESPONSE : &[u8] = b"Y36kZ749MRk2Nem4BedJABOZiZWPLOtiwLfJlGTwm5"; +#[remove_all(FooResource)] +async fn remove_all() -> Raw<&'static [u8]> +{ + Raw::new(REMOVE_ALL_RESPONSE, TEXT_PLAIN) +} + +const REMOVE_RESPONSE : &[u8] = b"CwRzBrKErsVZ1N7yeNfjZuUn1MacvgBqk4uPOFfDDq"; +#[remove(FooResource)] +async fn remove(_id : u64) -> Raw<&'static [u8]> +{ + Raw::new(REMOVE_RESPONSE, TEXT_PLAIN) +} + +#[test] +fn async_methods() +{ + let server = TestServer::new(build_simple_router(|router| { + router.resource::("foo"); + })).unwrap(); + + 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/search?query=hello+world", SEARCH_RESPONSE); + test_post_response(&server, "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/1", REMOVE_RESPONSE); +} diff --git a/tests/custom_request_body.rs b/tests/custom_request_body.rs index 5b108c7..95fa748 100644 --- a/tests/custom_request_body.rs +++ b/tests/custom_request_body.rs @@ -1,7 +1,3 @@ -mod custom_request_body -{ - - use gotham::{ hyper::header::CONTENT_TYPE, router::builder::*, @@ -10,6 +6,7 @@ use gotham::{ use gotham_restful::*; use mime::TEXT_PLAIN; + const RESPONSE : &[u8] = b"This is the only valid response."; #[derive(Resource)] @@ -30,7 +27,7 @@ fn create(body : Foo) -> Raw> { #[test] -fn test() +fn custom_request_body() { let server = TestServer::new(build_simple_router(|router| { router.resource::("foo"); @@ -44,6 +41,3 @@ fn test() let body : &[u8] = res.as_ref(); assert_eq!(body, RESPONSE); } - - -} diff --git a/tests/openapi_supports_scope.rs b/tests/openapi_supports_scope.rs index 02ba509..62228da 100644 --- a/tests/openapi_supports_scope.rs +++ b/tests/openapi_supports_scope.rs @@ -10,6 +10,11 @@ use gotham::{ use gotham_restful::*; use mime::TEXT_PLAIN; +#[allow(dead_code)] +mod util { include!("util/mod.rs"); } +use util::test_get_response; + + const RESPONSE : &[u8] = b"This is the only valid response."; #[derive(Resource)] @@ -23,13 +28,6 @@ fn read_all() -> Raw<&'static [u8]> } -fn test_response(server : &TestServer, path : &str) -{ - let res = server.client().get(path).perform().unwrap().read_body().unwrap(); - let body : &[u8] = res.as_ref(); - assert_eq!(body, RESPONSE); -} - #[test] fn test() { @@ -51,10 +49,10 @@ fn test() }); })).unwrap(); - test_response(&server, "http://localhost/foo1"); - test_response(&server, "http://localhost/bar/foo2"); - test_response(&server, "http://localhost/bar/baz/foo3"); - test_response(&server, "http://localhost/foo4"); + test_get_response(&server, "http://localhost/foo1", RESPONSE); + test_get_response(&server, "http://localhost/bar/foo2", RESPONSE); + test_get_response(&server, "http://localhost/bar/baz/foo3", RESPONSE); + test_get_response(&server, "http://localhost/foo4", RESPONSE); } diff --git a/tests/sync_methods.rs b/tests/sync_methods.rs new file mode 100644 index 0000000..a13ec19 --- /dev/null +++ b/tests/sync_methods.rs @@ -0,0 +1,106 @@ +#[macro_use] extern crate gotham_derive; + +use gotham::{ + router::builder::*, + test::TestServer +}; +use gotham_restful::*; +use mime::{APPLICATION_JSON, TEXT_PLAIN}; +use serde::Deserialize; + +mod util { include!("util/mod.rs"); } +use util::{test_get_response, test_post_response, test_put_response, test_delete_response}; + + +#[derive(Resource)] +#[resource(read_all, read, search, create, change_all, change, remove_all, remove)] +struct FooResource; + +#[derive(Deserialize)] +#[cfg_attr(feature = "openapi", derive(OpenapiType))] +#[allow(dead_code)] +struct FooBody +{ + data : String +} + +#[derive(Deserialize, StateData, StaticResponseExtender)] +#[cfg_attr(feature = "openapi", derive(OpenapiType))] +#[allow(dead_code)] +struct FooSearch +{ + query : String +} + +const READ_ALL_RESPONSE : &[u8] = b"1ARwwSPVyOKpJKrYwqGgECPVWDl1BqajAAj7g7WJ3e"; +#[read_all(FooResource)] +fn read_all() -> Raw<&'static [u8]> +{ + Raw::new(READ_ALL_RESPONSE, TEXT_PLAIN) +} + +const READ_RESPONSE : &[u8] = b"FEReHoeBKU17X2bBpVAd1iUvktFL43CDu0cFYHdaP9"; +#[read(FooResource)] +fn read(_id : u64) -> Raw<&'static [u8]> +{ + Raw::new(READ_RESPONSE, TEXT_PLAIN) +} + +const SEARCH_RESPONSE : &[u8] = b"AWqcQUdBRHXKh3at4u79mdupOAfEbnTcx71ogCVF0E"; +#[search(FooResource)] +fn search(_body : FooSearch) -> Raw<&'static [u8]> +{ + Raw::new(SEARCH_RESPONSE, TEXT_PLAIN) +} + +const CREATE_RESPONSE : &[u8] = b"y6POY7wOMAB0jBRBw0FJT7DOpUNbhmT8KdpQPLkI83"; +#[create(FooResource)] +fn create(_body : FooBody) -> Raw<&'static [u8]> +{ + Raw::new(CREATE_RESPONSE, TEXT_PLAIN) +} + +const CHANGE_ALL_RESPONSE : &[u8] = b"QlbYg8gHE9OQvvk3yKjXJLTSXlIrg9mcqhfMXJmQkv"; +#[change_all(FooResource)] +fn change_all(_body : FooBody) -> Raw<&'static [u8]> +{ + Raw::new(CHANGE_ALL_RESPONSE, TEXT_PLAIN) +} + +const CHANGE_RESPONSE : &[u8] = b"qGod55RUXkT1lgPO8h0uVM6l368O2S0GrwENZFFuRu"; +#[change(FooResource)] +fn change(_id : u64, _body : FooBody) -> Raw<&'static [u8]> +{ + Raw::new(CHANGE_RESPONSE, TEXT_PLAIN) +} + +const REMOVE_ALL_RESPONSE : &[u8] = b"Y36kZ749MRk2Nem4BedJABOZiZWPLOtiwLfJlGTwm5"; +#[remove_all(FooResource)] +fn remove_all() -> Raw<&'static [u8]> +{ + Raw::new(REMOVE_ALL_RESPONSE, TEXT_PLAIN) +} + +const REMOVE_RESPONSE : &[u8] = b"CwRzBrKErsVZ1N7yeNfjZuUn1MacvgBqk4uPOFfDDq"; +#[remove(FooResource)] +fn remove(_id : u64) -> Raw<&'static [u8]> +{ + Raw::new(REMOVE_RESPONSE, TEXT_PLAIN) +} + +#[test] +fn sync_methods() +{ + let server = TestServer::new(build_simple_router(|router| { + router.resource::("foo"); + })).unwrap(); + + 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/search?query=hello+world", SEARCH_RESPONSE); + test_post_response(&server, "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/1", REMOVE_RESPONSE); +} diff --git a/tests/trybuild_ui.rs b/tests/trybuild_ui.rs index ac7adff..b791172 100644 --- a/tests/trybuild_ui.rs +++ b/tests/trybuild_ui.rs @@ -8,4 +8,11 @@ fn trybuild_ui() // always enabled t.compile_fail("tests/ui/from_body_enum.rs"); + t.compile_fail("tests/ui/method_async_state.rs"); + t.compile_fail("tests/ui/method_for_unknown_resource.rs"); + t.compile_fail("tests/ui/method_no_resource.rs"); + t.compile_fail("tests/ui/method_self.rs"); + t.compile_fail("tests/ui/method_too_few_args.rs"); + t.compile_fail("tests/ui/method_too_many_args.rs"); + t.compile_fail("tests/ui/method_unsafe.rs"); } diff --git a/tests/ui/method_async_state.rs b/tests/ui/method_async_state.rs new file mode 100644 index 0000000..66b9fc7 --- /dev/null +++ b/tests/ui/method_async_state.rs @@ -0,0 +1,15 @@ +#[macro_use] extern crate gotham_restful; +use gotham_restful::State; + +#[derive(Resource)] +#[resource(read_all)] +struct FooResource; + +#[read_all(FooResource)] +async fn read_all(state : &State) +{ +} + +fn main() +{ +} diff --git a/tests/ui/method_async_state.stderr b/tests/ui/method_async_state.stderr new file mode 100644 index 0000000..5c02836 --- /dev/null +++ b/tests/ui/method_async_state.stderr @@ -0,0 +1,21 @@ +error: async fn must not take &State as an argument as State is not Sync, consider boxing + --> $DIR/method_async_state.rs:9:19 + | +9 | async fn read_all(state : &State) + | ^^^^^ + +error[E0433]: failed to resolve: use of undeclared type or module `_gotham_restful_resource_foo_resource_method_read_all` + --> $DIR/method_async_state.rs:4:10 + | +4 | #[derive(Resource)] + | ^^^^^^^^ use of undeclared type or module `_gotham_restful_resource_foo_resource_method_read_all` + | + = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) + +warning: unused import: `gotham_restful::State` + --> $DIR/method_async_state.rs:2:5 + | +2 | use gotham_restful::State; + | ^^^^^^^^^^^^^^^^^^^^^ + | + = note: `#[warn(unused_imports)]` on by default diff --git a/tests/ui/method_for_unknown_resource.rs b/tests/ui/method_for_unknown_resource.rs new file mode 100644 index 0000000..162dc94 --- /dev/null +++ b/tests/ui/method_for_unknown_resource.rs @@ -0,0 +1,10 @@ +#[macro_use] extern crate gotham_restful; + +#[read_all(UnknownResource)] +fn read_all() +{ +} + +fn main() +{ +} diff --git a/tests/ui/method_for_unknown_resource.stderr b/tests/ui/method_for_unknown_resource.stderr new file mode 100644 index 0000000..1e10d24 --- /dev/null +++ b/tests/ui/method_for_unknown_resource.stderr @@ -0,0 +1,5 @@ +error[E0412]: cannot find type `UnknownResource` in this scope + --> $DIR/method_for_unknown_resource.rs:3:12 + | +3 | #[read_all(UnknownResource)] + | ^^^^^^^^^^^^^^^ not found in this scope diff --git a/tests/ui/method_no_resource.rs b/tests/ui/method_no_resource.rs new file mode 100644 index 0000000..f0232b7 --- /dev/null +++ b/tests/ui/method_no_resource.rs @@ -0,0 +1,14 @@ +#[macro_use] extern crate gotham_restful; + +#[derive(Resource)] +#[resource(read_all)] +struct FooResource; + +#[read_all] +fn read_all() +{ +} + +fn main() +{ +} diff --git a/tests/ui/method_no_resource.stderr b/tests/ui/method_no_resource.stderr new file mode 100644 index 0000000..d4bc1c4 --- /dev/null +++ b/tests/ui/method_no_resource.stderr @@ -0,0 +1,15 @@ +error: Missing Resource struct. Example: #[read_all(MyResource)] + --> $DIR/method_no_resource.rs:7:1 + | +7 | #[read_all] + | ^^^^^^^^^^^ + | + = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0433]: failed to resolve: use of undeclared type or module `_gotham_restful_resource_foo_resource_method_read_all` + --> $DIR/method_no_resource.rs:3:10 + | +3 | #[derive(Resource)] + | ^^^^^^^^ use of undeclared type or module `_gotham_restful_resource_foo_resource_method_read_all` + | + = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/method_self.rs b/tests/ui/method_self.rs new file mode 100644 index 0000000..3b19b11 --- /dev/null +++ b/tests/ui/method_self.rs @@ -0,0 +1,14 @@ +#[macro_use] extern crate gotham_restful; + +#[derive(Resource)] +#[resource(read_all)] +struct FooResource; + +#[read_all(FooResource)] +fn read_all(self) +{ +} + +fn main() +{ +} diff --git a/tests/ui/method_self.stderr b/tests/ui/method_self.stderr new file mode 100644 index 0000000..d4fea5f --- /dev/null +++ b/tests/ui/method_self.stderr @@ -0,0 +1,13 @@ +error: Didn't expect self parameter + --> $DIR/method_self.rs:8:13 + | +8 | fn read_all(self) + | ^^^^ + +error[E0433]: failed to resolve: use of undeclared type or module `_gotham_restful_resource_foo_resource_method_read_all` + --> $DIR/method_self.rs:3:10 + | +3 | #[derive(Resource)] + | ^^^^^^^^ use of undeclared type or module `_gotham_restful_resource_foo_resource_method_read_all` + | + = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/method_too_few_args.rs b/tests/ui/method_too_few_args.rs new file mode 100644 index 0000000..6f0309e --- /dev/null +++ b/tests/ui/method_too_few_args.rs @@ -0,0 +1,14 @@ +#[macro_use] extern crate gotham_restful; + +#[derive(Resource)] +#[resource(read)] +struct FooResource; + +#[read(FooResource)] +fn read() +{ +} + +fn main() +{ +} diff --git a/tests/ui/method_too_few_args.stderr b/tests/ui/method_too_few_args.stderr new file mode 100644 index 0000000..d8daeab --- /dev/null +++ b/tests/ui/method_too_few_args.stderr @@ -0,0 +1,13 @@ +error: Too few arguments + --> $DIR/method_too_few_args.rs:8:4 + | +8 | fn read() + | ^^^^ + +error[E0433]: failed to resolve: use of undeclared type or module `_gotham_restful_resource_foo_resource_method_read` + --> $DIR/method_too_few_args.rs:3:10 + | +3 | #[derive(Resource)] + | ^^^^^^^^ use of undeclared type or module `_gotham_restful_resource_foo_resource_method_read` + | + = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/method_too_many_args.rs b/tests/ui/method_too_many_args.rs new file mode 100644 index 0000000..5ae83eb --- /dev/null +++ b/tests/ui/method_too_many_args.rs @@ -0,0 +1,14 @@ +#[macro_use] extern crate gotham_restful; + +#[derive(Resource)] +#[resource(read_all)] +struct FooResource; + +#[read_all(FooResource)] +fn read_all(_id : u64) +{ +} + +fn main() +{ +} diff --git a/tests/ui/method_too_many_args.stderr b/tests/ui/method_too_many_args.stderr new file mode 100644 index 0000000..3f8bd39 --- /dev/null +++ b/tests/ui/method_too_many_args.stderr @@ -0,0 +1,13 @@ +error: Too many arguments + --> $DIR/method_too_many_args.rs:8:13 + | +8 | fn read_all(_id : u64) + | ^^^ + +error[E0433]: failed to resolve: use of undeclared type or module `_gotham_restful_resource_foo_resource_method_read_all` + --> $DIR/method_too_many_args.rs:3:10 + | +3 | #[derive(Resource)] + | ^^^^^^^^ use of undeclared type or module `_gotham_restful_resource_foo_resource_method_read_all` + | + = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/method_unsafe.rs b/tests/ui/method_unsafe.rs new file mode 100644 index 0000000..65a76bc --- /dev/null +++ b/tests/ui/method_unsafe.rs @@ -0,0 +1,14 @@ +#[macro_use] extern crate gotham_restful; + +#[derive(Resource)] +#[resource(read_all)] +struct FooResource; + +#[read_all(FooResource)] +unsafe fn read_all() +{ +} + +fn main() +{ +} diff --git a/tests/ui/method_unsafe.stderr b/tests/ui/method_unsafe.stderr new file mode 100644 index 0000000..aeb104e --- /dev/null +++ b/tests/ui/method_unsafe.stderr @@ -0,0 +1,13 @@ +error: Resource methods must not be unsafe + --> $DIR/method_unsafe.rs:8:1 + | +8 | unsafe fn read_all() + | ^^^^^^ + +error[E0433]: failed to resolve: use of undeclared type or module `_gotham_restful_resource_foo_resource_method_read_all` + --> $DIR/method_unsafe.rs:3:10 + | +3 | #[derive(Resource)] + | ^^^^^^^^ use of undeclared type or module `_gotham_restful_resource_foo_resource_method_read_all` + | + = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/util/mod.rs b/tests/util/mod.rs new file mode 100644 index 0000000..e09a37f --- /dev/null +++ b/tests/util/mod.rs @@ -0,0 +1,37 @@ +use gotham::{ + hyper::Body, + test::TestServer +}; +use mime::Mime; + +pub fn test_get_response(server : &TestServer, path : &str, expected : &[u8]) +{ + let res = server.client().get(path).perform().unwrap().read_body().unwrap(); + let body : &[u8] = res.as_ref(); + assert_eq!(body, expected); +} + +pub fn test_post_response(server : &TestServer, path : &str, body : B, mime : Mime, expected : &[u8]) +where + B : Into +{ + let res = server.client().post(path, body, mime).perform().unwrap().read_body().unwrap(); + let body : &[u8] = res.as_ref(); + assert_eq!(body, expected); +} + +pub fn test_put_response(server : &TestServer, path : &str, body : B, mime : Mime, expected : &[u8]) +where + B : Into +{ + let res = server.client().put(path, body, mime).perform().unwrap().read_body().unwrap(); + let body : &[u8] = res.as_ref(); + assert_eq!(body, expected); +} + +pub fn test_delete_response(server : &TestServer, path : &str, expected : &[u8]) +{ + let res = server.client().delete(path).perform().unwrap().read_body().unwrap(); + let body : &[u8] = res.as_ref(); + assert_eq!(body, expected); +} From b1b9858da4f4c321bc0edc066c5561e7e1dab5c1 Mon Sep 17 00:00:00 2001 From: Dominic Date: Fri, 8 May 2020 21:52:57 +0200 Subject: [PATCH 059/170] ci: run cargo clean before caching the shared runners ofter run out of memory when creating the cache, but are definitely far too slow to be able to work without caching, so try to minimize the amout of storage required. --- .gitlab-ci.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8d233ce..75db8e3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -15,6 +15,8 @@ test-default: script: - cargo test --workspace --tests - cargo test --workspace --tests -- --ignored + after_script: + - cargo clean cache: paths: - cargo/ @@ -30,6 +32,8 @@ test-all: - cargo test --workspace --all-features --doc - cargo test --workspace --tests -- --ignored - cargo tarpaulin --target-dir target/tarpaulin --all --all-features --exclude-files 'cargo/*' --exclude-files 'derive/*' --exclude-files 'example/*' --ignore-panics --ignore-tests --out Html -v + after_script: + - cargo clean artifacts: paths: - tarpaulin-report.html @@ -53,6 +57,8 @@ doc: script: - cargo doc --all-features - echo 'The documentation is located here' >target/doc/index.html + after_script: + - cargo clean artifacts: paths: - target/doc/ @@ -84,7 +90,7 @@ publish: script: - cd gotham_restful_derive - cargo publish - - sleep 10s + - sleep 1m - cd ../gotham_restful - cargo publish - cd .. From e2eb9b0fcc497e918b9030ad2d391f2f2f8ed795 Mon Sep 17 00:00:00 2001 From: Dominic Date: Fri, 8 May 2020 22:54:54 +0200 Subject: [PATCH 060/170] fix --- src/result/auth_result.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/result/auth_result.rs b/src/result/auth_result.rs index bed279f..bbfbe56 100644 --- a/src/result/auth_result.rs +++ b/src/result/auth_result.rs @@ -58,7 +58,7 @@ error, or delegates to another error type. This type is best used with [`AuthRes #[derive(Debug, ResourceError)] pub enum AuthErrorOrOther { - #[status(UNAUTHORIZED)] + #[status(FORBIDDEN)] #[display("Forbidden")] Forbidden, #[display("{0}")] From 9ed24c9bcb043b88ce4e1963507405f220e2bf46 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sat, 9 May 2020 03:27:58 +0200 Subject: [PATCH 061/170] ci: use cargo sweep instead of cargo clean --- .gitlab-ci.yml | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 75db8e3..4f38bdf 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -9,35 +9,39 @@ variables: test-default: stage: test - image: msrd0/rust:alpine + image: msrd0/rust:alpine-sweep before_script: - cargo -V + - cargo sweep -s script: - cargo test --workspace --tests - cargo test --workspace --tests -- --ignored after_script: - - cargo clean + - cargo sweep -f cache: + key: cargo-default paths: - cargo/ - target/ test-all: stage: test - image: msrd0/rust:alpine-tarpaulin + image: msrd0/rust:alpine-tarpaulin-sweep before_script: - apk add --no-cache postgresql-dev - cargo -V + - cargo sweep -s script: - cargo test --workspace --all-features --doc - cargo test --workspace --tests -- --ignored - cargo tarpaulin --target-dir target/tarpaulin --all --all-features --exclude-files 'cargo/*' --exclude-files 'derive/*' --exclude-files 'example/*' --ignore-panics --ignore-tests --out Html -v after_script: - - cargo clean + - cargo sweep -f artifacts: paths: - tarpaulin-report.html cache: + key: cargo-all paths: - cargo/ - target/ @@ -51,18 +55,20 @@ readme: doc: stage: build - image: msrd0/rust:alpine + image: msrd0/rust:alpine-sweep before_script: - cargo -V + - cargo sweep -s script: - cargo doc --all-features - echo 'The documentation is located here' >target/doc/index.html after_script: - - cargo clean + - cargo sweep -f artifacts: paths: - target/doc/ cache: + key: cargo-doc paths: - cargo/ - target/ From 6680887b8462fb024f121abbd7ae46aebbfd2ecb Mon Sep 17 00:00:00 2001 From: Dominic Date: Sat, 9 May 2020 15:29:29 +0200 Subject: [PATCH 062/170] add trybuild tests for OpenapiType and Resource derive macros --- derive/src/openapi_type.rs | 6 +++--- tests/trybuild_ui.rs | 12 ++++++++++++ tests/ui/openapi_type_enum_with_fields.rs | 14 ++++++++++++++ tests/ui/openapi_type_enum_with_fields.stderr | 11 +++++++++++ tests/ui/openapi_type_nullable_non_bool.rs | 12 ++++++++++++ tests/ui/openapi_type_nullable_non_bool.stderr | 5 +++++ tests/ui/openapi_type_rename_non_string.rs | 12 ++++++++++++ tests/ui/openapi_type_rename_non_string.stderr | 5 +++++ tests/ui/openapi_type_tuple_struct.rs | 8 ++++++++ tests/ui/openapi_type_tuple_struct.stderr | 5 +++++ tests/ui/openapi_type_union.rs | 12 ++++++++++++ tests/ui/openapi_type_union.stderr | 5 +++++ tests/ui/openapi_type_unknown_key.rs | 12 ++++++++++++ tests/ui/openapi_type_unknown_key.stderr | 5 +++++ tests/ui/resource_unknown_method.rs | 14 ++++++++++++++ tests/ui/resource_unknown_method.stderr | 14 ++++++++++++++ 16 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 tests/ui/openapi_type_enum_with_fields.rs create mode 100644 tests/ui/openapi_type_enum_with_fields.stderr create mode 100644 tests/ui/openapi_type_nullable_non_bool.rs create mode 100644 tests/ui/openapi_type_nullable_non_bool.stderr create mode 100644 tests/ui/openapi_type_rename_non_string.rs create mode 100644 tests/ui/openapi_type_rename_non_string.stderr create mode 100644 tests/ui/openapi_type_tuple_struct.rs create mode 100644 tests/ui/openapi_type_tuple_struct.stderr create mode 100644 tests/ui/openapi_type_union.rs create mode 100644 tests/ui/openapi_type_union.stderr create mode 100644 tests/ui/openapi_type_unknown_key.rs create mode 100644 tests/ui/openapi_type_unknown_key.stderr create mode 100644 tests/ui/resource_unknown_method.rs create mode 100644 tests/ui/resource_unknown_method.stderr diff --git a/derive/src/openapi_type.rs b/derive/src/openapi_type.rs index d73507e..bd9dd6a 100644 --- a/derive/src/openapi_type.rs +++ b/derive/src/openapi_type.rs @@ -108,7 +108,7 @@ fn expand_variant(variant : &Variant) -> Result { if variant.fields != Fields::Unit { - return Err(Error::new(variant.span(), "Enum Variants with Fields not supported")); + return Err(Error::new(variant.span(), "#[derive(OpenapiType)] does not support enum variants with fields")); } let ident = &variant.ident; @@ -173,7 +173,7 @@ fn expand_field(field : &Field) -> Result { let ident = match &field.ident { Some(ident) => ident, - None => return Err(Error::new(field.span(), "Fields without ident are not supported")) + None => return Err(Error::new(field.span(), "#[derive(OpenapiType)] does not support fields without an ident")) }; let ty = &field.ty; @@ -242,7 +242,7 @@ fn expand_struct(ident : Ident, generics : Generics, attrs : Vec, inp .map(expand_field) .collect_to_result()? }, - Fields::Unnamed(fields) => return Err(Error::new(fields.span(), "Unnamed fields are not supported")), + Fields::Unnamed(fields) => return Err(Error::new(fields.span(), "#[derive(OpenapiType)] does not support unnamed fields")), Fields::Unit => Vec::new() }; diff --git a/tests/trybuild_ui.rs b/tests/trybuild_ui.rs index b791172..b5a572a 100644 --- a/tests/trybuild_ui.rs +++ b/tests/trybuild_ui.rs @@ -15,4 +15,16 @@ fn trybuild_ui() t.compile_fail("tests/ui/method_too_few_args.rs"); t.compile_fail("tests/ui/method_too_many_args.rs"); t.compile_fail("tests/ui/method_unsafe.rs"); + t.compile_fail("tests/ui/resource_unknown_method.rs"); + + // require the openapi feature + if cfg!(feature = "openapi") + { + 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_rename_non_string.rs"); + t.compile_fail("tests/ui/openapi_type_tuple_struct.rs"); + t.compile_fail("tests/ui/openapi_type_union.rs"); + t.compile_fail("tests/ui/openapi_type_unknown_key.rs"); + } } diff --git a/tests/ui/openapi_type_enum_with_fields.rs b/tests/ui/openapi_type_enum_with_fields.rs new file mode 100644 index 0000000..6bc6814 --- /dev/null +++ b/tests/ui/openapi_type_enum_with_fields.rs @@ -0,0 +1,14 @@ +#[macro_use] extern crate gotham_restful; + +#[derive(OpenapiType)] +enum Food +{ + Pasta, + Pizza { pineapple : bool }, + Rice, + Other(String) +} + +fn main() +{ +} diff --git a/tests/ui/openapi_type_enum_with_fields.stderr b/tests/ui/openapi_type_enum_with_fields.stderr new file mode 100644 index 0000000..1620970 --- /dev/null +++ b/tests/ui/openapi_type_enum_with_fields.stderr @@ -0,0 +1,11 @@ +error: #[derive(OpenapiType)] does not support enum variants with fields + --> $DIR/openapi_type_enum_with_fields.rs:7:2 + | +7 | Pizza { pineapple : bool }, + | ^^^^^ + +error: #[derive(OpenapiType)] does not support enum variants with fields + --> $DIR/openapi_type_enum_with_fields.rs:9:2 + | +9 | Other(String) + | ^^^^^ diff --git a/tests/ui/openapi_type_nullable_non_bool.rs b/tests/ui/openapi_type_nullable_non_bool.rs new file mode 100644 index 0000000..1e0af28 --- /dev/null +++ b/tests/ui/openapi_type_nullable_non_bool.rs @@ -0,0 +1,12 @@ +#[macro_use] extern crate gotham_restful; + +#[derive(OpenapiType)] +struct Foo +{ + #[openapi(nullable = "yes, please")] + bar : String +} + +fn main() +{ +} diff --git a/tests/ui/openapi_type_nullable_non_bool.stderr b/tests/ui/openapi_type_nullable_non_bool.stderr new file mode 100644 index 0000000..0ce2be3 --- /dev/null +++ b/tests/ui/openapi_type_nullable_non_bool.stderr @@ -0,0 +1,5 @@ +error: Expected bool + --> $DIR/openapi_type_nullable_non_bool.rs:6:23 + | +6 | #[openapi(nullable = "yes, please")] + | ^^^^^^^^^^^^^ diff --git a/tests/ui/openapi_type_rename_non_string.rs b/tests/ui/openapi_type_rename_non_string.rs new file mode 100644 index 0000000..0847f14 --- /dev/null +++ b/tests/ui/openapi_type_rename_non_string.rs @@ -0,0 +1,12 @@ +#[macro_use] extern crate gotham_restful; + +#[derive(OpenapiType)] +struct Foo +{ + #[openapi(rename = 42)] + bar : String +} + +fn main() +{ +} diff --git a/tests/ui/openapi_type_rename_non_string.stderr b/tests/ui/openapi_type_rename_non_string.stderr new file mode 100644 index 0000000..9bb9dd2 --- /dev/null +++ b/tests/ui/openapi_type_rename_non_string.stderr @@ -0,0 +1,5 @@ +error: Expected string literal + --> $DIR/openapi_type_rename_non_string.rs:6:21 + | +6 | #[openapi(rename = 42)] + | ^^ diff --git a/tests/ui/openapi_type_tuple_struct.rs b/tests/ui/openapi_type_tuple_struct.rs new file mode 100644 index 0000000..0247478 --- /dev/null +++ b/tests/ui/openapi_type_tuple_struct.rs @@ -0,0 +1,8 @@ +#[macro_use] extern crate gotham_restful; + +#[derive(OpenapiType)] +struct Foo(String); + +fn main() +{ +} diff --git a/tests/ui/openapi_type_tuple_struct.stderr b/tests/ui/openapi_type_tuple_struct.stderr new file mode 100644 index 0000000..2028d18 --- /dev/null +++ b/tests/ui/openapi_type_tuple_struct.stderr @@ -0,0 +1,5 @@ +error: #[derive(OpenapiType)] does not support unnamed fields + --> $DIR/openapi_type_tuple_struct.rs:4:11 + | +4 | struct Foo(String); + | ^^^^^^^^ diff --git a/tests/ui/openapi_type_union.rs b/tests/ui/openapi_type_union.rs new file mode 100644 index 0000000..4bc7355 --- /dev/null +++ b/tests/ui/openapi_type_union.rs @@ -0,0 +1,12 @@ +#[macro_use] extern crate gotham_restful; + +#[derive(OpenapiType)] +union IntOrPointer +{ + int: u64, + pointer: *mut String +} + +fn main() +{ +} diff --git a/tests/ui/openapi_type_union.stderr b/tests/ui/openapi_type_union.stderr new file mode 100644 index 0000000..52639fe --- /dev/null +++ b/tests/ui/openapi_type_union.stderr @@ -0,0 +1,5 @@ +error: #[derive(OpenapiType)] only works for structs and enums + --> $DIR/openapi_type_union.rs:4:1 + | +4 | union IntOrPointer + | ^^^^^ diff --git a/tests/ui/openapi_type_unknown_key.rs b/tests/ui/openapi_type_unknown_key.rs new file mode 100644 index 0000000..9157e16 --- /dev/null +++ b/tests/ui/openapi_type_unknown_key.rs @@ -0,0 +1,12 @@ +#[macro_use] extern crate gotham_restful; + +#[derive(OpenapiType)] +struct Foo +{ + #[openapi(like = "pizza")] + bar : String +} + +fn main() +{ +} diff --git a/tests/ui/openapi_type_unknown_key.stderr b/tests/ui/openapi_type_unknown_key.stderr new file mode 100644 index 0000000..f8e78b7 --- /dev/null +++ b/tests/ui/openapi_type_unknown_key.stderr @@ -0,0 +1,5 @@ +error: Unknown key + --> $DIR/openapi_type_unknown_key.rs:6:12 + | +6 | #[openapi(like = "pizza")] + | ^^^^ diff --git a/tests/ui/resource_unknown_method.rs b/tests/ui/resource_unknown_method.rs new file mode 100644 index 0000000..f246ed1 --- /dev/null +++ b/tests/ui/resource_unknown_method.rs @@ -0,0 +1,14 @@ +#[macro_use] extern crate gotham_restful; + +#[derive(Resource)] +#[resource(read_any)] +struct FooResource; + +#[read_all(FooResource)] +fn read_all() +{ +} + +fn main() +{ +} diff --git a/tests/ui/resource_unknown_method.stderr b/tests/ui/resource_unknown_method.stderr new file mode 100644 index 0000000..3282dbe --- /dev/null +++ b/tests/ui/resource_unknown_method.stderr @@ -0,0 +1,14 @@ +error: Unknown method: `read_any' + --> $DIR/resource_unknown_method.rs:4:12 + | +4 | #[resource(read_any)] + | ^^^^^^^^ + +error[E0277]: the trait bound `FooResource: gotham_restful::Resource` is not satisfied + --> $DIR/resource_unknown_method.rs:7:1 + | +7 | #[read_all(FooResource)] + | ^^^^^^^^^^^^^^^^^^^^^^^^ the trait `gotham_restful::Resource` is not implemented for `FooResource` + | + = help: see issue #48214 + = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info) From b9002bd70d97662bce719067b536eda2f9a36018 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sat, 9 May 2020 18:01:47 +0200 Subject: [PATCH 063/170] remove Resource::name() method and update resource documentation --- derive/src/resource.rs | 5 ----- src/resource.rs | 49 +++++++++++++++++++++++++++++------------- 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/derive/src/resource.rs b/derive/src/resource.rs index a51ecbb..79482e6 100644 --- a/derive/src/resource.rs +++ b/derive/src/resource.rs @@ -48,11 +48,6 @@ pub fn expand_resource(input : DeriveInput) -> Result Ok(quote! { impl #krate::Resource for #ident { - fn name() -> String - { - stringify!(#ident).to_string() - } - fn setup(mut route : D) { #(#methods)* diff --git a/src/resource.rs b/src/resource.rs index 6e2d5cd..daedd3d 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -9,18 +9,22 @@ use std::{ pin::Pin }; -/// This trait must be implemented by every RESTful Resource. It will -/// allow you to register the different methods for this Resource. +/// 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. +/// +/// It is not recommended to implement this yourself, rather just use `#[derive(Resource)]`. pub trait Resource { - /// The name of this resource. Must be unique. - fn name() -> String; - - /// Setup all routes of this resource. Take a look at the rest_resource! - /// macro if you don't feel like caring yourself. + /// Register all methods handled by this resource with the underlying router. fn setup(route : D); } +/// A common trait for every resource method. It defines the return type as well as some general +/// information about a resource method. +/// +/// It is not recommended to implement this yourself. Rather, just write your handler method and +/// annotate it with `#[(YourResource)]`, where `` is one of the supported +/// resource methods. pub trait ResourceMethod { type Res : ResourceResult + Send + 'static; @@ -37,63 +41,78 @@ pub trait ResourceMethod } } -/// Handle a GET request on the Resource root. +/// The read_all [`ResourceMethod`](trait.ResourceMethod.html). pub trait ResourceReadAll : ResourceMethod { + /// Handle a GET request on the Resource root. fn read_all(state : State) -> Pin + Send>>; } -/// Handle a GET request on the Resource with an id. +/// The read [`ResourceMethod`](trait.ResourceMethod.html). pub trait ResourceRead : ResourceMethod { + /// The ID type to be parsed from the request path. type ID : ResourceID + 'static; + /// Handle a GET request on the Resource with an id. fn read(state : State, id : Self::ID) -> Pin + Send>>; } -/// Handle a GET request on the Resource with additional search parameters. +/// The search [`ResourceMethod`](trait.ResourceMethod.html). pub trait ResourceSearch : ResourceMethod { + /// The Query type to be parsed from the request parameters. type Query : ResourceType + QueryStringExtractor + Sync; + /// Handle a GET request on the Resource with additional search parameters. fn search(state : State, query : Self::Query) -> Pin + Send>>; } -/// Handle a POST request on the Resource root. +/// The create [`ResourceMethod`](trait.ResourceMethod.html). pub trait ResourceCreate : ResourceMethod { + /// The Body type to be parsed from the request body. type Body : RequestBody; + /// Handle a POST request on the Resource root. fn create(state : State, body : Self::Body) -> Pin + Send>>; } -/// Handle a PUT request on the Resource root. +/// The change_all [`ResourceMethod`](trait.ResourceMethod.html). pub trait ResourceChangeAll : ResourceMethod { + /// The Body type to be parsed from the request body. type Body : RequestBody; + /// Handle a PUT request on the Resource root. fn change_all(state : State, body : Self::Body) -> Pin + Send>>; } -/// Handle a PUT request on the Resource with an id. +/// The change [`ResourceMethod`](trait.ResourceMethod.html). pub trait ResourceChange : ResourceMethod { + /// The Body type to be parsed from the request body. type Body : RequestBody; + /// The ID type to be parsed from the request path. type ID : ResourceID + 'static; + /// Handle a PUT request on the Resource with an id. fn change(state : State, id : Self::ID, body : Self::Body) -> Pin + Send>>; } -/// Handle a DELETE request on the Resource root. +/// The remove_all [`ResourceMethod`](trait.ResourceMethod.html). pub trait ResourceRemoveAll : ResourceMethod { + /// Handle a DELETE request on the Resource root. fn remove_all(state : State) -> Pin + Send>>; } -/// Handle a DELETE request on the Resource with an id. +/// The remove [`ResourceMethod`](trait.ResourceMethod.html). pub trait ResourceRemove : ResourceMethod { + /// The ID type to be parsed from the request path. type ID : ResourceID + 'static; + /// Handle a DELETE request on the Resource with an id. fn remove(state : State, id : Self::ID) -> Pin + Send>>; } From 40c90e6b4a1d3a413492451f8aad97281f9597f9 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sat, 9 May 2020 18:10:50 +0200 Subject: [PATCH 064/170] no need to use stringify! in a proc macro --- derive/src/openapi_type.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/derive/src/openapi_type.rs b/derive/src/openapi_type.rs index bd9dd6a..57a7ceb 100644 --- a/derive/src/openapi_type.rs +++ b/derive/src/openapi_type.rs @@ -16,6 +16,7 @@ use syn::{ Generics, GenericParam, Lit, + LitStr, Meta, NestedMeta, Result, @@ -175,6 +176,7 @@ fn expand_field(field : &Field) -> Result Some(ident) => 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 ty = &field.ty; let attrs = parse_attributes(&field.attrs)?; @@ -193,7 +195,7 @@ fn expand_field(field : &Field) -> Result } else if !#nullable { - required.push(stringify!(#ident).to_string()); + required.push(#ident_str.to_string()); } let keys : Vec = schema.dependencies.keys().map(|k| k.to_string()).collect(); From 748bf65d3e50c5490ad7037f768d029dab5ef09f Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 13 May 2020 19:10:53 +0200 Subject: [PATCH 065/170] cors for non-preflight requests --- Cargo.toml | 1 + src/cors.rs | 152 +++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 9 +++ src/routing.rs | 8 ++- 4 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 src/cors.rs diff --git a/Cargo.toml b/Cargo.toml index 59519a6..618f11d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ trybuild = "1.0.26" [features] default = ["errorlog"] auth = ["gotham_restful_derive/auth", "base64", "cookie", "jsonwebtoken"] +cors = [] errorlog = [] database = ["gotham_restful_derive/database", "gotham_middleware_diesel"] openapi = ["gotham_restful_derive/openapi", "indexmap", "openapiv3"] diff --git a/src/cors.rs b/src/cors.rs new file mode 100644 index 0000000..c48a38a --- /dev/null +++ b/src/cors.rs @@ -0,0 +1,152 @@ +use gotham::{ + handler::HandlerFuture, + hyper::{ + header::{ACCESS_CONTROL_ALLOW_ORIGIN, ORIGIN, HeaderMap, HeaderValue}, + Body, Method, Response + }, + middleware::Middleware, + state::{FromState, State}, +}; +use std::pin::Pin; + +/** +Specify the allowed origins of the request. It is up to the browser to check the validity of the +origin. This, when sent to the browser, will indicate whether or not the request's origin was +allowed to make the request. +*/ +#[derive(Clone, Debug)] +pub enum Origin +{ + /// Do not send any `Access-Control-Allow-Origin` headers. + None, + /// Send `Access-Control-Allow-Origin: *`. Note that browser will not send credentials. + Star, + /// Set the `Access-Control-Allow-Origin` header to a single origin. + Single(String), + /// Copy the `Origin` header into the `Access-Control-Allow-Origin` header. + Copy +} + +impl Default for Origin +{ + fn default() -> Self + { + Self::None + } +} + +impl Origin +{ + /// Get the header value for the `Access-Control-Allow-Origin` header. + fn header_value(&self, state : &State) -> Option + { + match self { + Self::None => None, + Self::Star => Some("*".parse().unwrap()), + Self::Single(origin) => Some(origin.parse().unwrap()), + Self::Copy => { + let headers = HeaderMap::borrow_from(state); + headers.get(ORIGIN).map(Clone::clone) + } + } + } +} + +/** +This is the configuration that the CORS handler will follow. Its default configuration is basically +not to touch any responses, resulting in the browser's default behaviour. + +To change settings, you need to put this type into gotham's [`State`]: + +```rust,no_run +# use gotham::{router::builder::*, pipeline::{new_pipeline, single::single_pipeline}, state::State}; +# use gotham_restful::*; +fn main() { + let cors = CorsConfig { + origin: Origin::Star + }; + let (chain, pipelines) = single_pipeline(new_pipeline().add(cors).build()); + gotham::start("127.0.0.1:8080", build_router(chain, pipelines, |route| { + // your routing logic + })); +} +``` + +This easy approach allows you to have one global cors configuration. If you prefer to have separate +configurations for different scopes, you need to register the middleware inside your routing logic: + +```rust,no_run +# use gotham::{router::builder::*, pipeline::*, pipeline::set::*, state::State}; +# use gotham_restful::*; +fn main() { + let pipelines = new_pipeline_set(); + + let cors_a = CorsConfig { + origin: Origin::Star + }; + let (pipelines, chain_a) = pipelines.add( + new_pipeline().add(cors_a).build() + ); + + let cors_b = CorsConfig { + origin: Origin::Copy + }; + let (pipelines, chain_b) = pipelines.add( + new_pipeline().add(cors_b).build() + ); + + let pipeline_set = finalize_pipeline_set(pipelines); + gotham::start("127.0.0.1:8080", build_router((), pipeline_set, |route| { + // routing without any cors config + route.with_pipeline_chain((chain_a, ()), |route| { + // routing with cors config a + }); + route.with_pipeline_chain((chain_b, ()), |route| { + // routing with cors config b + }); + })); +} +``` + + [`State`]: ../gotham/state/struct.State.html +*/ +#[derive(Clone, Debug, Default, NewMiddleware, StateData)] +pub struct CorsConfig +{ + pub origin : Origin +} + +impl Middleware for CorsConfig +{ + fn call(self, mut state : State, chain : Chain) -> Pin> + where + Chain : FnOnce(State) -> Pin> + { + state.put(self); + chain(state) + } +} + +/** +Handle CORS for a non-preflight request. This means manipulating the `res` HTTP headers so that +the response is aligned with the `state`'s [`CorsConfig`]. + +If you are using the [`Resource`] type (which is the recommended way), you'll never have to call +this method. However, if you are writing your own handler method, you might want to call this +after your request to add the required CORS headers. + +For further information on CORS, read https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS. + + [`CorsConfig`]: ./struct.CorsConfig.html +*/ +pub fn handle_cors(state : &State, res : &mut Response) +{ + let method = Method::borrow_from(state); + let config = CorsConfig::try_borrow_from(state); + + // non-preflight requests require nothing other than the Access-Control-Allow-Origin header + if let Some(header) = config.and_then(|cfg| cfg.origin.header_value(state)) + { + res.headers_mut().insert(ACCESS_CONTROL_ALLOW_ORIGIN, header); + } +} diff --git a/src/lib.rs b/src/lib.rs index 1fa19fd..d45363d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -285,6 +285,15 @@ pub use auth::{ StaticAuthHandler }; +#[cfg(feature = "cors")] +mod cors; +#[cfg(feature = "cors")] +pub use cors::{ + handle_cors, + CorsConfig, + Origin +}; + pub mod matcher; #[cfg(feature = "openapi")] diff --git a/src/routing.rs b/src/routing.rs index 1b0aa46..320cd7a 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -100,10 +100,16 @@ fn response_from(res : Response, state : &State) -> gotham::hyper::Response Date: Thu, 14 May 2020 23:30:59 +0200 Subject: [PATCH 066/170] cors preflight --- Cargo.toml | 2 +- src/cors.rs | 112 +++++++++++++++++-- src/lib.rs | 1 + src/matcher/access_control_request_method.rs | 57 ++++++++++ src/matcher/mod.rs | 4 + src/routing.rs | 18 ++- 6 files changed, 182 insertions(+), 12 deletions(-) create mode 100644 src/matcher/access_control_request_method.rs diff --git a/Cargo.toml b/Cargo.toml index 618f11d..ebed5db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,7 +45,7 @@ paste = "0.1.12" trybuild = "1.0.26" [features] -default = ["errorlog"] +default = ["cors", "errorlog"] auth = ["gotham_restful_derive/auth", "base64", "cookie", "jsonwebtoken"] cors = [] errorlog = [] diff --git a/src/cors.rs b/src/cors.rs index c48a38a..558bedb 100644 --- a/src/cors.rs +++ b/src/cors.rs @@ -1,13 +1,25 @@ +use crate::matcher::AccessControlRequestMethodMatcher; use gotham::{ handler::HandlerFuture, + helpers::http::response::create_empty_response, hyper::{ - header::{ACCESS_CONTROL_ALLOW_ORIGIN, ORIGIN, HeaderMap, HeaderValue}, - Body, Method, Response + header::{ + ACCESS_CONTROL_ALLOW_CREDENTIALS, ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_METHODS, + ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_MAX_AGE, ACCESS_CONTROL_REQUEST_METHOD, ORIGIN, VARY, + HeaderMap, HeaderName, HeaderValue + }, + Body, Method, Response, StatusCode }, middleware::Middleware, + pipeline::chain::PipelineHandleChain, + router::builder::*, state::{FromState, State}, }; -use std::pin::Pin; +use itertools::Itertools; +use std::{ + panic::RefUnwindSafe, + pin::Pin +}; /** Specify the allowed origins of the request. It is up to the browser to check the validity of the @@ -63,7 +75,8 @@ To change settings, you need to put this type into gotham's [`State`]: # use gotham_restful::*; fn main() { let cors = CorsConfig { - origin: Origin::Star + origin: Origin::Star, + ..Default::default() }; let (chain, pipelines) = single_pipeline(new_pipeline().add(cors).build()); gotham::start("127.0.0.1:8080", build_router(chain, pipelines, |route| { @@ -82,14 +95,16 @@ fn main() { let pipelines = new_pipeline_set(); let cors_a = CorsConfig { - origin: Origin::Star + origin: Origin::Star, + ..Default::default() }; let (pipelines, chain_a) = pipelines.add( new_pipeline().add(cors_a).build() ); let cors_b = CorsConfig { - origin: Origin::Copy + origin: Origin::Copy, + ..Default::default() }; let (pipelines, chain_b) = pipelines.add( new_pipeline().add(cors_b).build() @@ -113,7 +128,14 @@ fn main() { #[derive(Clone, Debug, Default, NewMiddleware, StateData)] pub struct CorsConfig { - pub origin : Origin + /// The allowed origins. + pub origin : Origin, + /// The allowed headers. + pub headers : Vec, + /// The amount of seconds that the preflight request can be cached. + pub max_age : u64, + /// Whether or not the request may be made with supplying credentials. + pub credentials : bool } impl Middleware for CorsConfig @@ -141,12 +163,84 @@ For further information on CORS, read https://developer.mozilla.org/en-US/docs/W */ pub fn handle_cors(state : &State, res : &mut Response) { - let method = Method::borrow_from(state); let config = CorsConfig::try_borrow_from(state); + let headers = res.headers_mut(); // non-preflight requests require nothing other than the Access-Control-Allow-Origin header if let Some(header) = config.and_then(|cfg| cfg.origin.header_value(state)) { - res.headers_mut().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 matches!(config.map(|cfg| &cfg.origin), Some(Origin::Copy)) + { + let vary = headers.get(VARY).map(|vary| format!("{},Origin", vary.to_str().unwrap())); + headers.insert(VARY, vary.as_deref().unwrap_or("Origin").parse().unwrap()); + } + + // if we allow credentials, tell the browser + if config.map(|cfg| cfg.credentials).unwrap_or(false) + { + headers.insert(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true".parse().unwrap()); + } +} + +/// Add CORS routing for your path. +pub trait CorsRoute +where + C : PipelineHandleChain

+ Copy + Send + Sync + 'static, + P : RefUnwindSafe + Send + Sync + 'static +{ + fn cors(&mut self, path : &str, method : Method); +} + +fn cors_preflight_handler(state : State) -> (State, Response) +{ + let config = CorsConfig::try_borrow_from(&state); + + // prepare the response + let mut res = create_empty_response(&state, StatusCode::NO_CONTENT); + let headers = res.headers_mut(); + + // copy the request method over to the response + let method = HeaderMap::borrow_from(&state).get(ACCESS_CONTROL_REQUEST_METHOD).unwrap().clone(); + headers.insert(ACCESS_CONTROL_ALLOW_METHODS, method); + + // if we allow any headers, put them in + if let Some(hdrs) = config.map(|cfg| &cfg.headers) + { + if hdrs.len() > 0 + { + // 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()); + } + } + + // set the max age for the preflight cache + if let Some(age) = config.map(|cfg| cfg.max_age) + { + headers.insert(ACCESS_CONTROL_MAX_AGE, age.into()); + } + + // make sure the browser knows that this request was based on the method + headers.insert(VARY, "Access-Control-Request-Method".parse().unwrap()); + + handle_cors(&state, &mut res); + (state, res) +} + +impl CorsRoute for D +where + D : DrawRoutes, + C : PipelineHandleChain

+ Copy + Send + Sync + 'static, + P : RefUnwindSafe + Send + Sync + 'static +{ + fn cors(&mut self, path : &str, method : Method) + { + let matcher = AccessControlRequestMethodMatcher::new(method); + self.options(path) + .extend_route_matcher(matcher) + .to(cors_preflight_handler); } } diff --git a/src/lib.rs b/src/lib.rs index d45363d..ff60998 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -291,6 +291,7 @@ mod cors; pub use cors::{ handle_cors, CorsConfig, + CorsRoute, Origin }; diff --git a/src/matcher/access_control_request_method.rs b/src/matcher/access_control_request_method.rs new file mode 100644 index 0000000..6c912cc --- /dev/null +++ b/src/matcher/access_control_request_method.rs @@ -0,0 +1,57 @@ +use gotham::{ + hyper::{header::{ACCESS_CONTROL_REQUEST_METHOD, HeaderMap}, Method, StatusCode}, + router::{non_match::RouteNonMatch, route::matcher::RouteMatcher}, + state::{FromState, State} +}; + +/// A route matcher that checks whether the value of the `Access-Control-Request-Method` header matches the defined value. +/// +/// Usage: +/// +/// ```rust +/// # use gotham::{helpers::http::response::create_empty_response, +/// # hyper::{header::ACCESS_CONTROL_ALLOW_METHODS, Method, StatusCode}, +/// # router::builder::* +/// # }; +/// # use gotham_restful::matcher::AccessControlRequestMethodMatcher; +/// let matcher = AccessControlRequestMethodMatcher::new(Method::PUT); +/// +/// # build_simple_router(|route| { +/// // use the matcher for your request +/// route.options("/foo") +/// .extend_route_matcher(matcher) +/// .to(|state| { +/// // we know that this is a CORS preflight for a PUT request +/// let mut res = create_empty_response(&state, StatusCode::NO_CONTENT); +/// res.headers_mut().insert(ACCESS_CONTROL_ALLOW_METHODS, "PUT".parse().unwrap()); +/// (state, res) +/// }); +/// # }); +/// ``` +#[derive(Clone, Debug)] +pub struct AccessControlRequestMethodMatcher +{ + method : Method +} + +impl AccessControlRequestMethodMatcher +{ + pub fn new(method : Method) -> Self + { + Self { method } + } +} + +impl RouteMatcher for AccessControlRequestMethodMatcher +{ + fn is_match(&self, state : &State) -> Result<(), RouteNonMatch> + { + match HeaderMap::borrow_from(state).get(ACCESS_CONTROL_REQUEST_METHOD) + .and_then(|value| value.to_str().ok()) + .and_then(|str| str.parse::().ok()) + { + Some(m) if m == self.method => Ok(()), + _ => Err(RouteNonMatch::new(StatusCode::NOT_FOUND)) + } + } +} diff --git a/src/matcher/mod.rs b/src/matcher/mod.rs index 4d5268e..3168ec3 100644 --- a/src/matcher/mod.rs +++ b/src/matcher/mod.rs @@ -8,6 +8,10 @@ pub use accept::AcceptHeaderMatcher; mod content_type; pub use content_type::ContentTypeMatcher; +#[cfg(feature = "cors")] +mod access_control_request_method; +pub use access_control_request_method::AccessControlRequestMethodMatcher; + type LookupTable = HashMap>; trait LookupTableFromTypes diff --git a/src/routing.rs b/src/routing.rs index 320cd7a..916b244 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -6,6 +6,8 @@ use crate::{ Response, StatusCode }; +#[cfg(feature = "cors")] +use crate::CorsRoute; #[cfg(feature = "openapi")] use crate::openapi::{ builder::{OpenapiBuilder, OpenapiInfo}, @@ -391,6 +393,8 @@ macro_rules! implDrawResourceRoutes { .extend_route_matcher(accept_matcher) .extend_route_matcher(content_matcher) .to(|state| create_handler::(state)); + #[cfg(feature = "cors")] + self.0.cors(&self.1, Method::POST); } fn change_all(&mut self) @@ -404,6 +408,8 @@ macro_rules! implDrawResourceRoutes { .extend_route_matcher(accept_matcher) .extend_route_matcher(content_matcher) .to(|state| change_all_handler::(state)); + #[cfg(feature = "cors")] + self.0.cors(&self.1, Method::PUT); } fn change(&mut self) @@ -413,11 +419,14 @@ macro_rules! implDrawResourceRoutes { { let accept_matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); let content_matcher : MaybeMatchContentTypeHeader = Handler::Body::supported_types().into(); - self.0.put(&format!("{}/:id", self.1)) + let path = format!("{}/:id", self.1); + self.0.put(&path) .extend_route_matcher(accept_matcher) .extend_route_matcher(content_matcher) .with_path_extractor::>() .to(|state| change_handler::(state)); + #[cfg(feature = "cors")] + self.0.cors(&path, Method::PUT); } fn remove_all(&mut self) @@ -426,15 +435,20 @@ macro_rules! implDrawResourceRoutes { self.0.delete(&self.1) .extend_route_matcher(matcher) .to(|state| remove_all_handler::(state)); + #[cfg(feature = "cors")] + self.0.cors(&self.1, Method::DELETE); } fn remove(&mut self) { let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); - self.0.delete(&format!("{}/:id", self.1)) + let path = format!("{}/:id", self.1); + self.0.delete(&path) .extend_route_matcher(matcher) .with_path_extractor::>() .to(|state| remove_handler::(state)); + #[cfg(feature = "cors")] + self.0.cors(&path, Method::POST); } } } From 74ef0af51267db5164cc4eb4fbceeb87a2ccb214 Mon Sep 17 00:00:00 2001 From: Dominic Date: Fri, 15 May 2020 21:19:26 +0200 Subject: [PATCH 067/170] cors tests --- src/cors.rs | 2 +- src/matcher/mod.rs | 1 + tests/cors_handling.rs | 156 ++++++++++++++++++++++++++++++++ tests/openapi_supports_scope.rs | 11 +-- 4 files changed, 160 insertions(+), 10 deletions(-) create mode 100644 tests/cors_handling.rs diff --git a/src/cors.rs b/src/cors.rs index 558bedb..57e1a10 100644 --- a/src/cors.rs +++ b/src/cors.rs @@ -166,7 +166,7 @@ pub fn handle_cors(state : &State, res : &mut Response) let config = CorsConfig::try_borrow_from(state); let headers = res.headers_mut(); - // non-preflight requests require nothing other than 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)) { headers.insert(ACCESS_CONTROL_ALLOW_ORIGIN, header); diff --git a/src/matcher/mod.rs b/src/matcher/mod.rs index 3168ec3..9cbfcbb 100644 --- a/src/matcher/mod.rs +++ b/src/matcher/mod.rs @@ -10,6 +10,7 @@ pub use content_type::ContentTypeMatcher; #[cfg(feature = "cors")] mod access_control_request_method; +#[cfg(feature = "cors")] pub use access_control_request_method::AccessControlRequestMethodMatcher; type LookupTable = HashMap>; diff --git a/tests/cors_handling.rs b/tests/cors_handling.rs new file mode 100644 index 0000000..a9fe498 --- /dev/null +++ b/tests/cors_handling.rs @@ -0,0 +1,156 @@ +#![cfg(feature = "cors")] +use gotham::{ + hyper::{body::Body, client::connect::Connect, header::*, StatusCode}, + pipeline::{new_pipeline, single::single_pipeline}, + router::builder::*, + test::{Server, TestRequest, TestServer} +}; +use gotham_restful::{CorsConfig, DrawResources, Origin, Raw, Resource, change_all, read_all}; +use itertools::Itertools; +use mime::TEXT_PLAIN; + +#[derive(Resource)] +#[resource(read_all, change_all)] +struct FooResource; + +#[read_all(FooResource)] +fn read_all() +{ +} + +#[change_all(FooResource)] +fn change_all(_body : Raw>) +{ +} + +fn test_server(cfg : CorsConfig) -> TestServer +{ + let (chain, pipeline) = single_pipeline(new_pipeline().add(cfg).build()); + TestServer::new(build_router(chain, pipeline, |router| { + router.resource::("/foo") + })).unwrap() +} + +fn test_response(req : TestRequest, origin : Option<&str>, vary : Option<&str>, credentials : bool) +where + TS : Server + 'static, + C : Connect + Clone + Send + Sync + 'static +{ + let res = req.with_header(ORIGIN, "http://example.org".parse().unwrap()).perform().unwrap(); + assert_eq!(res.status(), StatusCode::NO_CONTENT); + let headers = res.headers(); + 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(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!(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) +{ + let res = server.client().options("http://example.org/foo") + .with_header(ACCESS_CONTROL_REQUEST_METHOD, method.parse().unwrap()) + .with_header(ORIGIN, "http://example.org".parse().unwrap()) + .perform().unwrap(); + assert_eq!(res.status(), StatusCode::NO_CONTENT); + let headers = res.headers(); + 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!(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(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] +fn cors_origin_none() +{ + let cfg = CorsConfig { + origin: Origin::None, + ..Default::default() + }; + let server = test_server(cfg); + + 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().put("http://example.org/foo", Body::empty(), TEXT_PLAIN), None, None, false); +} + +#[test] +fn cors_origin_star() +{ + let cfg = CorsConfig { + origin: Origin::Star, + ..Default::default() + }; + let server = test_server(cfg); + + 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().put("http://example.org/foo", Body::empty(), TEXT_PLAIN), Some("*"), None, false); +} + +#[test] +fn cors_origin_single() +{ + let cfg = CorsConfig { + origin: Origin::Single("https://foo.com".to_owned()), + ..Default::default() + }; + let server = test_server(cfg); + + 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(server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN), Some("https://foo.com"), None, false); +} + +#[test] +fn cors_origin_copy() +{ + let cfg = CorsConfig { + origin: Origin::Copy, + ..Default::default() + }; + let server = test_server(cfg); + + 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(server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN), Some("http://example.org"), Some("Origin"), false); +} + +#[test] +fn cors_credentials() +{ + let cfg = CorsConfig { + origin: Origin::None, + credentials: true, + ..Default::default() + }; + let server = test_server(cfg); + + 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().put("http://example.org/foo", Body::empty(), TEXT_PLAIN), None, None, true); +} + +#[test] +fn cors_max_age() +{ + let cfg = CorsConfig { + origin: Origin::None, + max_age: 31536000, + ..Default::default() + }; + let server = test_server(cfg); + + 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().put("http://example.org/foo", Body::empty(), TEXT_PLAIN), None, None, false); +} diff --git a/tests/openapi_supports_scope.rs b/tests/openapi_supports_scope.rs index 62228da..3b9aa2c 100644 --- a/tests/openapi_supports_scope.rs +++ b/tests/openapi_supports_scope.rs @@ -1,8 +1,4 @@ -#[cfg(feature = "openapi")] -mod openapi_supports_scope -{ - - +#![cfg(feature = "openapi")] use gotham::{ router::builder::*, test::TestServer @@ -29,7 +25,7 @@ fn read_all() -> Raw<&'static [u8]> #[test] -fn test() +fn openapi_supports_scope() { let info = OpenapiInfo { title: "Test".to_owned(), @@ -54,6 +50,3 @@ fn test() test_get_response(&server, "http://localhost/bar/baz/foo3", RESPONSE); test_get_response(&server, "http://localhost/foo4", RESPONSE); } - - -} // mod test From 4ff5a8d7e44a68e4526e5a27127a5328f89c585a Mon Sep 17 00:00:00 2001 From: Dominic Date: Sat, 16 May 2020 01:01:20 +0200 Subject: [PATCH 068/170] doctest fix #26 --- src/lib.rs | 12 +++++++++--- src/result/auth_result.rs | 8 +++++--- src/result/success.rs | 2 +- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index ff60998..5b9dc6d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,7 +43,7 @@ struct FooResource; /// The return type of the foo read method. #[derive(Serialize)] -# #[derive(OpenapiType)] +# #[cfg_attr(feature = "openapi", derive(OpenapiType))] struct Foo { id: u64 } @@ -123,6 +123,8 @@ None of this is currently supported by gotham's own JWT middleware. A simple example that uses only a single secret could look like this: ```rust,no_run +# #[cfg(feature = "auth")] +# mod auth_feature_enabled { # #[macro_use] extern crate gotham_restful_derive; # use gotham::{router::builder::*, pipeline::{new_pipeline, single::single_pipeline}, state::State}; # use gotham_restful::*; @@ -132,7 +134,7 @@ A simple example that uses only a single secret could look like this: struct SecretResource; #[derive(Serialize)] -# #[derive(OpenapiType)] +# #[cfg_attr(feature = "openapi", derive(OpenapiType))] struct Secret { id: u64, intended_for: String @@ -161,6 +163,7 @@ fn main() { route.resource::("secret"); })); } +# } ``` ## Database Feature @@ -173,6 +176,8 @@ you'll need to borrow the connection from the [`State`] yourself and return a bo A simple non-async example could look like this: ```rust,no_run +# #[cfg(feature = "database")] +# mod database_feature_enabled { # #[macro_use] extern crate diesel; # #[macro_use] extern crate gotham_restful_derive; # use diesel::{table, PgConnection, QueryResult, RunQueryDsl}; @@ -192,7 +197,7 @@ A simple non-async example could look like this: struct FooResource; #[derive(Queryable, Serialize)] -# #[derive(OpenapiType)] +# #[cfg_attr(feature = "openapi", derive(OpenapiType))] struct Foo { id: i64, value: String @@ -214,6 +219,7 @@ fn main() { route.resource::("foo"); })); } +# } ``` # Examples diff --git a/src/result/auth_result.rs b/src/result/auth_result.rs index bbfbe56..5c54efc 100644 --- a/src/result/auth_result.rs +++ b/src/result/auth_result.rs @@ -21,9 +21,10 @@ This return type can be used to map another `ResourceResult` that can only be re client is authenticated. Otherwise, an empty _403 Forbidden_ response will be issued. Use can look something like this (assuming the `auth` feature is enabled): -``` +```rust +# #[cfg(feature = "auth")] +# mod auth_feature_enabled { # #[macro_use] extern crate gotham_restful_derive; -# mod doc_tests_are_broken { # use gotham::state::State; # use gotham_restful::*; # use serde::Deserialize; @@ -81,8 +82,9 @@ client is authenticated. Otherwise, an empty _403 Forbidden_ response will be is look something like this (assuming the `auth` feature is enabled): ``` +# #[cfg(feature = "auth")] +# mod auth_feature_enabled { # #[macro_use] extern crate gotham_restful_derive; -# mod doc_tests_are_broken { # use gotham::state::State; # use gotham_restful::*; # use serde::Deserialize; diff --git a/src/result/success.rs b/src/result/success.rs index f622f12..dffd740 100644 --- a/src/result/success.rs +++ b/src/result/success.rs @@ -29,7 +29,7 @@ Usage example: # struct MyResource; # #[derive(Deserialize, Serialize)] -# #[derive(OpenapiType)] +# #[cfg_attr(feature = "openapi", derive(OpenapiType))] struct MyResponse { message: &'static str } From 20818b0f951f75cb8a6e86582784e47b1975cbc1 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sat, 16 May 2020 01:03:17 +0200 Subject: [PATCH 069/170] enable doc test for default features --- .gitlab-ci.yml | 1 + src/lib.rs | 2 +- src/result/auth_result.rs | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4f38bdf..fb7c703 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -14,6 +14,7 @@ test-default: - cargo -V - cargo sweep -s script: + - cargo test --workspace --doc - cargo test --workspace --tests - cargo test --workspace --tests -- --ignored after_script: diff --git a/src/lib.rs b/src/lib.rs index 5b9dc6d..6bef905 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -123,9 +123,9 @@ None of this is currently supported by gotham's own JWT middleware. A simple example that uses only a single secret could look like this: ```rust,no_run +# #[macro_use] extern crate gotham_restful_derive; # #[cfg(feature = "auth")] # mod auth_feature_enabled { -# #[macro_use] extern crate gotham_restful_derive; # use gotham::{router::builder::*, pipeline::{new_pipeline, single::single_pipeline}, state::State}; # use gotham_restful::*; # use serde::{Deserialize, Serialize}; diff --git a/src/result/auth_result.rs b/src/result/auth_result.rs index 5c54efc..bc3f84f 100644 --- a/src/result/auth_result.rs +++ b/src/result/auth_result.rs @@ -22,9 +22,9 @@ client is authenticated. Otherwise, an empty _403 Forbidden_ response will be is look something like this (assuming the `auth` feature is enabled): ```rust +# #[macro_use] extern crate gotham_restful_derive; # #[cfg(feature = "auth")] # mod auth_feature_enabled { -# #[macro_use] extern crate gotham_restful_derive; # use gotham::state::State; # use gotham_restful::*; # use serde::Deserialize; @@ -82,9 +82,9 @@ client is authenticated. Otherwise, an empty _403 Forbidden_ response will be is look something like this (assuming the `auth` feature is enabled): ``` +# #[macro_use] extern crate gotham_restful_derive; # #[cfg(feature = "auth")] # mod auth_feature_enabled { -# #[macro_use] extern crate gotham_restful_derive; # use gotham::state::State; # use gotham_restful::*; # use serde::Deserialize; From 94abc7526857a7ae08b90be231e538ce625401ea Mon Sep 17 00:00:00 2001 From: Dominic Date: Sat, 16 May 2020 13:59:47 +0200 Subject: [PATCH 070/170] works on my machineTM --- src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6bef905..4025e9b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -176,10 +176,10 @@ you'll need to borrow the connection from the [`State`] yourself and return a bo A simple non-async example could look like this: ```rust,no_run -# #[cfg(feature = "database")] -# mod database_feature_enabled { # #[macro_use] extern crate diesel; # #[macro_use] extern crate gotham_restful_derive; +# #[cfg(feature = "database")] +# mod database_feature_enabled { # use diesel::{table, PgConnection, QueryResult, RunQueryDsl}; # use gotham::{router::builder::*, pipeline::{new_pipeline, single::single_pipeline}, state::State}; # use gotham_middleware_diesel::DieselMiddleware; From dc26e9a02eeeabb6d2df2d36d7bf86406f2e93cb Mon Sep 17 00:00:00 2001 From: Dominic Date: Sat, 16 May 2020 14:22:23 +0200 Subject: [PATCH 071/170] improve cors doc --- src/cors.rs | 81 +++++++++++++++++++++++++++++++++-------------------- src/lib.rs | 44 +++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 31 deletions(-) diff --git a/src/cors.rs b/src/cors.rs index 57e1a10..f9f8170 100644 --- a/src/cors.rs +++ b/src/cors.rs @@ -91,36 +91,36 @@ configurations for different scopes, you need to register the middleware inside ```rust,no_run # use gotham::{router::builder::*, pipeline::*, pipeline::set::*, state::State}; # use gotham_restful::*; -fn main() { - let pipelines = new_pipeline_set(); - - let cors_a = CorsConfig { - origin: Origin::Star, - ..Default::default() - }; - let (pipelines, chain_a) = pipelines.add( - new_pipeline().add(cors_a).build() - ); - - let cors_b = CorsConfig { - origin: Origin::Copy, - ..Default::default() - }; - let (pipelines, chain_b) = pipelines.add( - new_pipeline().add(cors_b).build() - ); - - let pipeline_set = finalize_pipeline_set(pipelines); - gotham::start("127.0.0.1:8080", build_router((), pipeline_set, |route| { - // routing without any cors config - route.with_pipeline_chain((chain_a, ()), |route| { - // routing with cors config a - }); - route.with_pipeline_chain((chain_b, ()), |route| { - // routing with cors config b - }); - })); -} +let pipelines = new_pipeline_set(); + +// The first cors configuration +let cors_a = CorsConfig { + origin: Origin::Star, + ..Default::default() +}; +let (pipelines, chain_a) = pipelines.add( + new_pipeline().add(cors_a).build() +); + +// The second cors configuration +let cors_b = CorsConfig { + origin: Origin::Copy, + ..Default::default() +}; +let (pipelines, chain_b) = pipelines.add( + new_pipeline().add(cors_b).build() +); + +let pipeline_set = finalize_pipeline_set(pipelines); +gotham::start("127.0.0.1:8080", build_router((), pipeline_set, |route| { + // routing without any cors config + route.with_pipeline_chain((chain_a, ()), |route| { + // routing with cors config a + }); + route.with_pipeline_chain((chain_b, ()), |route| { + // routing with cors config b + }); +})); ``` [`State`]: ../gotham/state/struct.State.html @@ -186,12 +186,31 @@ pub fn handle_cors(state : &State, res : &mut Response) } } -/// Add CORS routing for your path. +/// Add CORS routing for your path. This is required for handling preflight requests. +/// +/// Example: +/// +/// ```rust,no_run +/// # use gotham::{hyper::{Body, Method, Response}, router::builder::*}; +/// # use gotham_restful::*; +/// build_simple_router(|router| { +/// // The handler that needs preflight handling +/// router.post("/foo").to(|state| { +/// let mut res : Response = unimplemented!(); +/// handle_cors(&state, &mut res); +/// (state, res) +/// }); +/// // Add preflight handling +/// router.cors("/foo", Method::POST); +/// }); +/// ``` pub trait CorsRoute where C : PipelineHandleChain

+ Copy + Send + Sync + 'static, P : RefUnwindSafe + Send + Sync + 'static { + /// Handle a preflight request on `path` for `method`. To configure the behaviour, use + /// [`CorsConfig`](struct.CorsConfig.html). fn cors(&mut self, path : &str, method : Method); } diff --git a/src/lib.rs b/src/lib.rs index 4025e9b..d7aa095 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -166,6 +166,49 @@ fn main() { # } ``` +## CORS Feature + +The cors feature allows an easy usage of this web server from other origins. By default, only +the `Access-Control-Allow-Methods` header is touched. To change the behaviour, add your desired +configuration as a middleware. + +A simple example that allows authentication from every origin (note that `*` always disallows +authentication), and every content type, could look like this: + +```rust,no_run +# #[macro_use] extern crate gotham_restful_derive; +# #[cfg(feature = "cors")] +# mod cors_feature_enabled { +# use gotham::{hyper::header::*, router::builder::*, pipeline::{new_pipeline, single::single_pipeline}, state::State}; +# use gotham_restful::*; +# use serde::{Deserialize, Serialize}; +#[derive(Resource)] +#[resource(read_all)] +struct FooResource; + +#[read_all(FooResource)] +fn read_all() { + // your handler +} + +fn main() { + let cors = CorsConfig { + origin: Origin::Copy, + headers: vec![CONTENT_TYPE], + max_age: 0, + credentials: true + }; + let (chain, pipelines) = single_pipeline(new_pipeline().add(cors).build()); + gotham::start("127.0.0.1:8080", build_router(chain, pipelines, |route| { + route.resource::("foo"); + })); +} +# } +``` + +The cors feature can also be used for non-resource handlers. Take a look at [`CorsRoute`] +for an example. + ## Database Feature The database feature allows an easy integration of [diesel] into your handler functions. Please @@ -238,6 +281,7 @@ Licensed under your option of: [example]: https://gitlab.com/msrd0/gotham-restful/tree/master/example [gotham]: https://gotham.rs/ [serde_json]: https://github.com/serde-rs/json#serde-json---- + [`CorsRoute`]: trait.CorsRoute.html [`QueryStringExtractor`]: ../gotham/extractor/trait.QueryStringExtractor.html [`RequestBody`]: trait.RequestBody.html [`State`]: ../gotham/state/struct.State.html From 955715eea67aa2cc55d58b88b7993c20ad5dea82 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sat, 16 May 2020 14:26:21 +0200 Subject: [PATCH 072/170] I don't know how I ended up with spaces --- src/cors.rs | 42 ++++++++++---------- src/matcher/accept.rs | 2 +- src/matcher/access_control_request_method.rs | 2 +- src/matcher/content_type.rs | 2 +- tests/cors_handling.rs | 4 +- 5 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/cors.rs b/src/cors.rs index f9f8170..46af65c 100644 --- a/src/cors.rs +++ b/src/cors.rs @@ -1,8 +1,8 @@ use crate::matcher::AccessControlRequestMethodMatcher; use gotham::{ - handler::HandlerFuture, + handler::HandlerFuture, helpers::http::response::create_empty_response, - hyper::{ + hyper::{ header::{ ACCESS_CONTROL_ALLOW_CREDENTIALS, ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_MAX_AGE, ACCESS_CONTROL_REQUEST_METHOD, ORIGIN, VARY, @@ -10,10 +10,10 @@ use gotham::{ }, Body, Method, Response, StatusCode }, - middleware::Middleware, + middleware::Middleware, pipeline::chain::PipelineHandleChain, router::builder::*, - state::{FromState, State}, + state::{FromState, State}, }; use itertools::Itertools; use std::{ @@ -30,11 +30,11 @@ allowed to make the request. pub enum Origin { /// Do not send any `Access-Control-Allow-Origin` headers. - None, + None, /// Send `Access-Control-Allow-Origin: *`. Note that browser will not send credentials. - Star, + Star, /// Set the `Access-Control-Allow-Origin` header to a single origin. - Single(String), + Single(String), /// Copy the `Origin` header into the `Access-Control-Allow-Origin` header. Copy } @@ -75,7 +75,7 @@ To change settings, you need to put this type into gotham's [`State`]: # use gotham_restful::*; fn main() { let cors = CorsConfig { - origin: Origin::Star, + origin: Origin::Star, ..Default::default() }; let (chain, pipelines) = single_pipeline(new_pipeline().add(cors).build()); @@ -140,13 +140,13 @@ pub struct CorsConfig impl Middleware for CorsConfig { - fn call(self, mut state : State, chain : Chain) -> Pin> - where - Chain : FnOnce(State) -> Pin> - { - state.put(self); - chain(state) - } + fn call(self, mut state : State, chain : Chain) -> Pin> + where + Chain : FnOnce(State) -> Pin> + { + state.put(self); + chain(state) + } } /** @@ -166,7 +166,7 @@ pub fn handle_cors(state : &State, res : &mut Response) let config = CorsConfig::try_borrow_from(state); 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)) { headers.insert(ACCESS_CONTROL_ALLOW_ORIGIN, header); @@ -207,7 +207,7 @@ pub fn handle_cors(state : &State, res : &mut Response) pub trait CorsRoute where C : PipelineHandleChain

+ 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 /// [`CorsConfig`](struct.CorsConfig.html). @@ -252,14 +252,14 @@ fn cors_preflight_handler(state : State) -> (State, Response) impl CorsRoute for D where D : DrawRoutes, - C : PipelineHandleChain

+ Copy + Send + Sync + 'static, - P : RefUnwindSafe + Send + Sync + 'static + C : PipelineHandleChain

+ Copy + Send + Sync + 'static, + P : RefUnwindSafe + Send + Sync + 'static { fn cors(&mut self, path : &str, method : Method) { let matcher = AccessControlRequestMethodMatcher::new(method); self.options(path) - .extend_route_matcher(matcher) - .to(cors_preflight_handler); + .extend_route_matcher(matcher) + .to(cors_preflight_handler); } } diff --git a/src/matcher/accept.rs b/src/matcher/accept.rs index 2e89ac2..9aa1d91 100644 --- a/src/matcher/accept.rs +++ b/src/matcher/accept.rs @@ -76,7 +76,7 @@ let matcher = AcceptHeaderMatcher::new(types); # build_simple_router(|route| { // use the matcher for your request route.post("/foo") - .extend_route_matcher(matcher) + .extend_route_matcher(matcher) .to(|state| { // we know that the client is a modern browser and can handle webp images # let IMAGE_WEBP : mime::Mime = "image/webp".parse().unwrap(); diff --git a/src/matcher/access_control_request_method.rs b/src/matcher/access_control_request_method.rs index 6c912cc..e356c0a 100644 --- a/src/matcher/access_control_request_method.rs +++ b/src/matcher/access_control_request_method.rs @@ -48,7 +48,7 @@ impl RouteMatcher for AccessControlRequestMethodMatcher { match HeaderMap::borrow_from(state).get(ACCESS_CONTROL_REQUEST_METHOD) .and_then(|value| value.to_str().ok()) - .and_then(|str| str.parse::().ok()) + .and_then(|str| str.parse::().ok()) { Some(m) if m == self.method => Ok(()), _ => Err(RouteNonMatch::new(StatusCode::NOT_FOUND)) diff --git a/src/matcher/content_type.rs b/src/matcher/content_type.rs index d33ed1a..aca8b9a 100644 --- a/src/matcher/content_type.rs +++ b/src/matcher/content_type.rs @@ -26,7 +26,7 @@ let matcher = ContentTypeMatcher::new(types) # build_simple_router(|route| { // use the matcher for your request route.post("/foo") - .extend_route_matcher(matcher) + .extend_route_matcher(matcher) .to(|state| { let res = create_response(&state, StatusCode::OK, mime::TEXT_PLAIN, "Correct Content Type!"); (state, res) diff --git a/tests/cors_handling.rs b/tests/cors_handling.rs index a9fe498..24450de 100644 --- a/tests/cors_handling.rs +++ b/tests/cors_handling.rs @@ -50,8 +50,8 @@ fn test_preflight(server : &TestServer, method : &str, origin : Option<&str>, va { let res = server.client().options("http://example.org/foo") .with_header(ACCESS_CONTROL_REQUEST_METHOD, method.parse().unwrap()) - .with_header(ORIGIN, "http://example.org".parse().unwrap()) - .perform().unwrap(); + .with_header(ORIGIN, "http://example.org".parse().unwrap()) + .perform().unwrap(); assert_eq!(res.status(), StatusCode::NO_CONTENT); let headers = res.headers(); println!("{}", headers.keys().join(",")); From b39b30694e2121fee35be697746f48f007dfce97 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 17 May 2020 01:41:58 +0200 Subject: [PATCH 073/170] update readme --- README.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/README.md b/README.md index ae7da3d..4de2465 100644 --- a/README.md +++ b/README.md @@ -167,6 +167,42 @@ fn main() { } ``` +### CORS Feature + +The cors feature allows an easy usage of this web server from other origins. By default, only +the `Access-Control-Allow-Methods` header is touched. To change the behaviour, add your desired +configuration as a middleware. + +A simple example that allows authentication from every origin (note that `*` always disallows +authentication), and every content type, could look like this: + +```rust +#[derive(Resource)] +#[resource(read_all)] +struct FooResource; + +#[read_all(FooResource)] +fn read_all() { + // your handler +} + +fn main() { + let cors = CorsConfig { + origin: Origin::Copy, + headers: vec![CONTENT_TYPE], + max_age: 0, + credentials: true + }; + let (chain, pipelines) = single_pipeline(new_pipeline().add(cors).build()); + gotham::start("127.0.0.1:8080", build_router(chain, pipelines, |route| { + route.resource::("foo"); + })); +} +``` + +The cors feature can also be used for non-resource handlers. Take a look at [`CorsRoute`] +for an example. + ### Database Feature The database feature allows an easy integration of [diesel] into your handler functions. Please @@ -221,6 +257,7 @@ Licensed under your option of: [example]: https://gitlab.com/msrd0/gotham-restful/tree/master/example [gotham]: https://gotham.rs/ [serde_json]: https://github.com/serde-rs/json#serde-json---- + [`CorsRoute`]: trait.CorsRoute.html [`QueryStringExtractor`]: ../gotham/extractor/trait.QueryStringExtractor.html [`RequestBody`]: trait.RequestBody.html [`State`]: ../gotham/state/struct.State.html From 7268cc05671a803606703fd9a4f3606f96638f7b Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 19 May 2020 19:09:23 +0200 Subject: [PATCH 074/170] cors in the example --- example/src/main.rs | 9 +++++++++ tests/cors_handling.rs | 5 +---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/example/src/main.rs b/example/src/main.rs index acb56d3..f953e45 100644 --- a/example/src/main.rs +++ b/example/src/main.rs @@ -3,6 +3,7 @@ use fake::{faker::internet::en::Username, Fake}; use gotham::{ + hyper::header::CONTENT_TYPE, middleware::logger::RequestLogger, pipeline::{new_pipeline, single::single_pipeline}, router::builder::*, @@ -124,12 +125,20 @@ fn main() .unwrap(); log4rs::init_config(config).unwrap(); + let cors = CorsConfig { + origin: Origin::Copy, + headers: vec![CONTENT_TYPE], + credentials: true, + ..Default::default() + }; + let auth = >::from_source(AuthSource::AuthorizationHeader); let logging = RequestLogger::new(log::Level::Info); let (chain, pipelines) = single_pipeline( new_pipeline() .add(auth) .add(logging) + .add(cors) .build() ); diff --git a/tests/cors_handling.rs b/tests/cors_handling.rs index 24450de..80ad346 100644 --- a/tests/cors_handling.rs +++ b/tests/cors_handling.rs @@ -66,10 +66,7 @@ fn test_preflight(server : &TestServer, method : &str, origin : Option<&str>, va #[test] fn cors_origin_none() { - let cfg = CorsConfig { - origin: Origin::None, - ..Default::default() - }; + let cfg = Default::default(); let server = test_server(cfg); test_preflight(&server, "PUT", None, "Access-Control-Request-Method", false, 0); From 81803fd54a6f5cace5fd8d8bafb45bd5f839860e Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 19 May 2020 19:23:29 +0200 Subject: [PATCH 075/170] add HashMap as OpenapiType --- src/openapi/types.rs | 43 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/src/openapi/types.rs b/src/openapi/types.rs index 49e5e57..c7ff5e4 100644 --- a/src/openapi/types.rs +++ b/src/openapi/types.rs @@ -10,7 +10,7 @@ use openapiv3::{ #[cfg(feature = "uuid")] use uuid::Uuid; use std::{ - collections::{BTreeSet, HashSet}, + collections::{BTreeSet, HashMap, HashSet}, hash::BuildHasher }; @@ -267,8 +267,7 @@ impl OpenapiType for Vec let schema = T::schema(); let mut dependencies = schema.dependencies.clone(); - let items = match schema.name.clone() - { + let items = match schema.name.clone() { Some(name) => { let reference = Reference { reference: format!("#/components/schemas/{}", name) }; dependencies.insert(name, schema); @@ -307,6 +306,34 @@ impl OpenapiType for HashSet } } +impl OpenapiType for HashMap +{ + fn schema() -> OpenapiSchema + { + let schema = T::schema(); + let mut dependencies = schema.dependencies.clone(); + + let items = Box::new(match schema.name.clone() { + Some(name) => { + let reference = Reference { reference: format!("#/components/schemas/{}", name) }; + dependencies.insert(name, schema); + reference + }, + None => Item(schema.into_schema()) + }); + + OpenapiSchema { + nullable: false, + name: None, + schema: SchemaKind::Type(Type::Object(ObjectType { + additional_properties: Some(AdditionalProperties::Schema(items)), + ..Default::default() + })), + dependencies + } + } +} + impl OpenapiType for serde_json::Value { fn schema() -> OpenapiSchema @@ -330,12 +357,12 @@ mod test type Unit = (); macro_rules! assert_schema { - ($ty:ident $(<$generic:ident>)* => $json:expr) => { + ($ty:ident $(<$($generic:ident),+>)* => $json:expr) => { paste::item! { #[test] - fn []() + fn []() { - let schema = <$ty $(<$generic>)* as OpenapiType>::schema().into_schema(); + let schema = <$ty $(<$($generic),+>)* as OpenapiType>::schema().into_schema(); let schema_json = serde_json::to_string(&schema).expect(&format!("Unable to serialize schema for {}", stringify!($ty))); assert_eq!(schema_json, $json); } @@ -382,6 +409,8 @@ mod test assert_schema!(Option => r#"{"nullable":true,"type":"string"}"#); assert_schema!(Vec => r#"{"type":"array","items":{"type":"string"}}"#); - + assert_schema!(BTreeSet => r#"{"type":"array","items":{"type":"string"}}"#); + assert_schema!(HashSet => r#"{"type":"array","items":{"type":"string"}}"#); + assert_schema!(HashMap => r#"{"type":"object","additionalProperties":{"type":"string"}}"#); assert_schema!(Value => r#"{"nullable":true}"#); } From e5e9cd5d3c3802001387fdf95f8e7f20d526afa4 Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 19 May 2020 21:07:29 +0200 Subject: [PATCH 076/170] openapi spec tests --- Cargo.toml | 2 +- derive/src/request_body.rs | 18 +-- tests/openapi_specification.json | 202 ++++++++++++++++++++++++++++++ tests/openapi_specification.rs | 116 +++++++++++++++++ tests/openapi_supports_scope.json | 78 ++++++++++++ tests/openapi_supports_scope.rs | 4 +- tests/util/mod.rs | 20 +++ 7 files changed, 431 insertions(+), 9 deletions(-) create mode 100644 tests/openapi_specification.json create mode 100644 tests/openapi_specification.rs create mode 100644 tests/openapi_supports_scope.json diff --git a/Cargo.toml b/Cargo.toml index ebed5db..78e61d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ gitlab = { repository = "msrd0/gotham-restful", branch = "master" } [dependencies] base64 = { version = "0.12.0", optional = true } -chrono = { version = "0.4.11", optional = true } +chrono = { version = "0.4.11", features = ["serde"], optional = true } cookie = { version = "0.13.3", optional = true } futures-core = "0.3.4" futures-util = "0.3.4" diff --git a/derive/src/request_body.rs b/derive/src/request_body.rs index ddcc7e8..76c80aa 100644 --- a/derive/src/request_body.rs +++ b/derive/src/request_body.rs @@ -3,10 +3,11 @@ use proc_macro2::{Ident, TokenStream}; use quote::quote; use std::iter; use syn::{ - parenthesized, parse::{Parse, ParseStream}, punctuated::Punctuated, + spanned::Spanned, DeriveInput, + Error, Generics, Path, Result, @@ -19,9 +20,7 @@ impl Parse for MimeList { fn parse(input: ParseStream) -> Result { - let content; - let _paren = parenthesized!(content in input); - let list = Punctuated::parse_separated_nonempty(&content)?; + let list = Punctuated::parse_separated_nonempty(&input)?; Ok(Self(list)) } } @@ -61,10 +60,15 @@ pub fn expand_request_body(input : DeriveInput) -> Result let types = input.attrs.into_iter() .filter(|attr| attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("supported_types".to_string())) - .flat_map(|attr| - syn::parse2::(attr.tokens) + .flat_map(|attr| { + let span = attr.span(); + attr.parse_args::() .map(|list| Box::new(list.0.into_iter().map(Ok)) as Box>>) - .unwrap_or_else(|err| Box::new(iter::once(Err(err))))) + .unwrap_or_else(|mut err| { + err.combine(Error::new(span, "Hint: Types list should look like #[supported_types(TEXT_PLAIN, APPLICATION_JSON)]")); + Box::new(iter::once(Err(err))) + }) + }) .collect_to_result()?; let types = match types { diff --git a/tests/openapi_specification.json b/tests/openapi_specification.json new file mode 100644 index 0000000..c9e6c53 --- /dev/null +++ b/tests/openapi_specification.json @@ -0,0 +1,202 @@ +{ + "components": { + "schemas": { + "Secret": { + "properties": { + "code": { + "format": "float", + "type": "number" + } + }, + "required": [ + "code" + ], + "title": "Secret", + "type": "object" + }, + "Secrets": { + "properties": { + "secrets": { + "items": { + "$ref": "#/components/schemas/Secret" + }, + "type": "array" + } + }, + "required": [ + "secrets" + ], + "title": "Secrets", + "type": "object" + } + }, + "securitySchemes": { + "authToken": { + "bearerFormat": "JWT", + "scheme": "bearer", + "type": "http" + } + } + }, + "info": { + "title": "This is just a test", + "version": "1.2.3" + }, + "openapi": "3.0.2", + "paths": { + "/img/{id}": { + "get": { + "operationId": "getImage", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "int64", + "minimum": 0, + "type": "integer" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "content": { + "*/*": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "description": "OK" + } + } + }, + "put": { + "operationId": "setImage", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "int64", + "minimum": 0, + "type": "integer" + }, + "style": "simple" + } + ], + "requestBody": { + "content": { + "image/png": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/secret/search": { + "get": { + "parameters": [ + { + "in": "query", + "name": "date", + "required": true, + "schema": { + "format": "date", + "type": "string" + }, + "style": "form" + }, + { + "in": "query", + "name": "hour", + "schema": { + "format": "int16", + "minimum": 0, + "type": "integer" + }, + "style": "form" + }, + { + "in": "query", + "name": "minute", + "schema": { + "format": "int16", + "minimum": 0, + "type": "integer" + }, + "style": "form" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Secrets" + } + } + }, + "description": "OK" + } + }, + "security": [ + { + "authToken": [] + } + ] + } + }, + "/secret/{id}": { + "get": { + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "date-time", + "type": "string" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Secret" + } + } + }, + "description": "OK" + } + }, + "security": [ + { + "authToken": [] + } + ] + } + } + }, + "servers": [ + { + "url": "http://localhost:12345/api/v1" + } + ] +} \ No newline at end of file diff --git a/tests/openapi_specification.rs b/tests/openapi_specification.rs new file mode 100644 index 0000000..2171526 --- /dev/null +++ b/tests/openapi_specification.rs @@ -0,0 +1,116 @@ +#![cfg(all(feature = "auth", feature = "chrono", feature = "openapi"))] + +#[macro_use] extern crate gotham_derive; + +use chrono::{NaiveDate, NaiveDateTime}; +use gotham::{ + pipeline::{new_pipeline, single::single_pipeline}, + router::builder::*, + test::TestServer +}; +use gotham_restful::*; +use mime::IMAGE_PNG; +use serde::{Deserialize, Serialize}; + +#[allow(dead_code)] +mod util { include!("util/mod.rs"); } +use util::{test_get_response, test_openapi_response}; + + +const IMAGE_RESPONSE : &[u8] = b"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUA/wA0XsCoAAAAAXRSTlN/gFy0ywAAAApJREFUeJxjYgAAAAYAAzY3fKgAAAAASUVORK5CYII="; + +#[derive(Resource)] +#[resource(read, change)] +struct ImageResource; + +#[derive(FromBody, RequestBody)] +#[supported_types(IMAGE_PNG)] +struct Image(Vec); + +#[read(ImageResource, operation_id = "getImage")] +fn get_image(_id : u64) -> Raw<&'static [u8]> +{ + Raw::new(IMAGE_RESPONSE, "image/png;base64".parse().unwrap()) +} + +#[change(ImageResource, operation_id = "setImage")] +fn set_image(_id : u64, _image : Image) +{ +} + + +#[derive(Resource)] +#[resource(read, search)] +struct SecretResource; + +#[derive(Deserialize, Clone)] +struct AuthData +{ + sub : String, + iat : u64, + exp : u64 +} + +type AuthStatus = gotham_restful::AuthStatus; + +#[derive(OpenapiType, Serialize)] +struct Secret +{ + code : f32 +} + +#[derive(OpenapiType, Serialize)] +struct Secrets +{ + secrets : Vec +} + +#[derive(Deserialize, OpenapiType, StateData, StaticResponseExtender)] +struct SecretQuery +{ + date : NaiveDate, + hour : Option, + minute : Option +} + +#[read(SecretResource)] +fn read_secret(auth : AuthStatus, _id : NaiveDateTime) -> AuthSuccess +{ + auth.ok()?; + Ok(Secret { code: 4.2 }) +} + +#[search(SecretResource)] +fn search_secret(auth : AuthStatus, _query : SecretQuery) -> AuthSuccess +{ + auth.ok()?; + Ok(Secrets { + secrets: vec![Secret { code: 4.2 }, Secret { code: 3.14 }] + }) +} + + +#[test] +fn openapi_supports_scope() +{ + let info = OpenapiInfo { + title: "This is just a test".to_owned(), + version: "1.2.3".to_owned(), + urls: vec!["http://localhost:12345/api/v1".to_owned()] + }; + let auth: AuthMiddleware = AuthMiddleware::new( + AuthSource::AuthorizationHeader, + AuthValidation::default(), + StaticAuthHandler::from_array(b"zlBsA2QXnkmpe0QTh8uCvtAEa4j33YAc") + ); + let (chain, pipelines) = single_pipeline(new_pipeline().add(auth).build()); + let server = TestServer::new(build_router(chain, pipelines, |router| { + router.with_openapi(info, |mut router| { + router.resource::("img"); + router.get_openapi("openapi"); + router.resource::("secret"); + }); + })).unwrap(); + + test_openapi_response(&server, "http://localhost/openapi", "tests/openapi_specification.json"); +} diff --git a/tests/openapi_supports_scope.json b/tests/openapi_supports_scope.json new file mode 100644 index 0000000..bdef1fd --- /dev/null +++ b/tests/openapi_supports_scope.json @@ -0,0 +1,78 @@ +{ + "components": {}, + "info": { + "title": "Test", + "version": "1.2.3" + }, + "openapi": "3.0.2", + "paths": { + "/bar/baz/foo3": { + "get": { + "responses": { + "200": { + "content": { + "*/*": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "description": "OK" + } + } + } + }, + "/bar/foo2": { + "get": { + "responses": { + "200": { + "content": { + "*/*": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "description": "OK" + } + } + } + }, + "/foo1": { + "get": { + "responses": { + "200": { + "content": { + "*/*": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "description": "OK" + } + } + } + }, + "/foo4": { + "get": { + "responses": { + "200": { + "content": { + "*/*": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "description": "OK" + } + } + } + } + } +} \ No newline at end of file diff --git a/tests/openapi_supports_scope.rs b/tests/openapi_supports_scope.rs index 3b9aa2c..d126bb8 100644 --- a/tests/openapi_supports_scope.rs +++ b/tests/openapi_supports_scope.rs @@ -8,7 +8,7 @@ use mime::TEXT_PLAIN; #[allow(dead_code)] mod util { include!("util/mod.rs"); } -use util::test_get_response; +use util::{test_get_response, test_openapi_response}; const RESPONSE : &[u8] = b"This is the only valid response."; @@ -34,6 +34,7 @@ fn openapi_supports_scope() }; let server = TestServer::new(build_simple_router(|router| { router.with_openapi(info, |mut router| { + router.get_openapi("openapi"); router.resource::("foo1"); router.scope("/bar", |router| { router.resource::("foo2"); @@ -49,4 +50,5 @@ fn openapi_supports_scope() test_get_response(&server, "http://localhost/bar/foo2", RESPONSE); test_get_response(&server, "http://localhost/bar/baz/foo3", RESPONSE); test_get_response(&server, "http://localhost/foo4", RESPONSE); + test_openapi_response(&server, "http://localhost/openapi", "tests/openapi_supports_scope.json"); } diff --git a/tests/util/mod.rs b/tests/util/mod.rs index e09a37f..8846352 100644 --- a/tests/util/mod.rs +++ b/tests/util/mod.rs @@ -3,6 +3,8 @@ use gotham::{ test::TestServer }; use mime::Mime; +#[allow(unused_imports)] +use std::{fs::File, io::{Read, Write}, str}; pub fn test_get_response(server : &TestServer, path : &str, expected : &[u8]) { @@ -35,3 +37,21 @@ pub fn test_delete_response(server : &TestServer, path : &str, expected : &[u8]) let body : &[u8] = res.as_ref(); assert_eq!(body, expected); } + +#[cfg(feature = "openapi")] +pub fn test_openapi_response(server : &TestServer, path : &str, output_file : &str) +{ + let res = server.client().get(path).perform().unwrap().read_body().unwrap(); + let body = serde_json::to_string_pretty(&serde_json::from_slice::(res.as_ref()).unwrap()).unwrap(); + match File::open(output_file) { + Ok(mut file) => { + let mut expected = String::new(); + file.read_to_string(&mut expected).unwrap(); + assert_eq!(body, expected); + }, + Err(_) => { + let mut file = File::create(output_file).unwrap(); + file.write_all(body.as_bytes()).unwrap(); + } + }; +} From 8321b63982178916b88d7d5619e652e923bcf587 Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 20 May 2020 09:01:11 +0200 Subject: [PATCH 077/170] tests for the access control request method matcher --- src/matcher/access_control_request_method.rs | 51 +++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/src/matcher/access_control_request_method.rs b/src/matcher/access_control_request_method.rs index e356c0a..a5e03f2 100644 --- a/src/matcher/access_control_request_method.rs +++ b/src/matcher/access_control_request_method.rs @@ -36,6 +36,10 @@ pub struct AccessControlRequestMethodMatcher impl AccessControlRequestMethodMatcher { + /// 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, + /// 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. pub fn new(method : Method) -> Self { Self { method } @@ -46,12 +50,57 @@ impl RouteMatcher for AccessControlRequestMethodMatcher { fn is_match(&self, state : &State) -> Result<(), RouteNonMatch> { + // according to the fetch specification, methods should be normalized by byte-uppercase + // https://fetch.spec.whatwg.org/#concept-method match HeaderMap::borrow_from(state).get(ACCESS_CONTROL_REQUEST_METHOD) .and_then(|value| value.to_str().ok()) - .and_then(|str| str.parse::().ok()) + .and_then(|str| str.to_ascii_uppercase().parse::().ok()) { Some(m) if m == self.method => Ok(()), _ => Err(RouteNonMatch::new(StatusCode::NOT_FOUND)) } } } + + +#[cfg(test)] +mod test +{ + use super::*; + + fn with_state(accept : Option<&str>, block : F) + where F : FnOnce(&mut State) -> () + { + State::with_new(|state| { + let mut headers = HeaderMap::new(); + if let Some(acc) = accept + { + headers.insert(ACCESS_CONTROL_REQUEST_METHOD, acc.parse().unwrap()); + } + state.put(headers); + block(state); + }); + } + + #[test] + fn no_acrm_header() + { + let matcher = AccessControlRequestMethodMatcher::new(Method::PUT); + with_state(None, |state| assert!(matcher.is_match(&state).is_err())); + } + + #[test] + fn correct_acrm_header() + { + 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())); + } + + #[test] + fn incorrect_acrm_header() + { + let matcher = AccessControlRequestMethodMatcher::new(Method::PUT); + with_state(Some("DELETE"), |state| assert!(matcher.is_match(&state).is_err())); + } +} From c1cb0e692a3370d12151c35ac216efc325cfdc3d Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 20 May 2020 09:33:12 +0200 Subject: [PATCH 078/170] gotham finally has a release candidate --- Cargo.toml | 6 +- example/Cargo.toml | 4 +- src/matcher/accept.rs | 217 ------------------------------------ src/matcher/content_type.rs | 173 ---------------------------- src/matcher/mod.rs | 35 ------ src/routing.rs | 11 +- 6 files changed, 10 insertions(+), 436 deletions(-) delete mode 100644 src/matcher/accept.rs delete mode 100644 src/matcher/content_type.rs diff --git a/Cargo.toml b/Cargo.toml index 78e61d2..ef44aa0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,9 +23,9 @@ chrono = { version = "0.4.11", features = ["serde"], optional = true } cookie = { version = "0.13.3", optional = true } futures-core = "0.3.4" futures-util = "0.3.4" -gotham = { git = "https://github.com/gotham-rs/gotham", version = "0.5.0-dev", default-features = false } -gotham_derive = { git = "https://github.com/gotham-rs/gotham", version = "0.5.0-dev" } -gotham_middleware_diesel = { git = "https://github.com/gotham-rs/gotham", version = "0.1.0", optional = true } +gotham = { version = "0.5.0-rc.1", default-features = false } +gotham_derive = "0.5.0-rc.1" +gotham_middleware_diesel = { version = "0.1.2", optional = true } gotham_restful_derive = { version = "0.1.0-dev" } indexmap = { version = "1.3.2", optional = true } itertools = "0.9.0" diff --git a/example/Cargo.toml b/example/Cargo.toml index d594b89..763ef0c 100644 --- a/example/Cargo.toml +++ b/example/Cargo.toml @@ -15,8 +15,8 @@ gitlab = { repository = "msrd0/gotham-restful", branch = "master" } [dependencies] fake = "2.2" -gotham = { git = "https://github.com/gotham-rs/gotham", version = "0.5.0-dev", default-features = false } -gotham_derive = { git = "https://github.com/gotham-rs/gotham", version = "0.5.0-dev", default-features = false } +gotham = { version = "0.5.0-rc.1", default-features = false } +gotham_derive = "0.5.0-rc.1" gotham_restful = { version = "0.1.0-dev", features = ["auth", "openapi"] } log = "0.4.8" log4rs = { version = "0.12", features = ["console_appender"], default-features = false } diff --git a/src/matcher/accept.rs b/src/matcher/accept.rs deleted file mode 100644 index 9aa1d91..0000000 --- a/src/matcher/accept.rs +++ /dev/null @@ -1,217 +0,0 @@ -use super::{LookupTable, LookupTableFromTypes}; -use gotham::{ - hyper::{ - header::{HeaderMap, ACCEPT}, - StatusCode - }, - router::{non_match::RouteNonMatch, route::matcher::RouteMatcher}, - state::{FromState, State} -}; -use mime::Mime; -use std::{ - num::ParseFloatError, - str::FromStr -}; -use thiserror::Error; - - -/// A mime type that is optionally weighted with a quality. -#[derive(Debug)] -struct QMime -{ - mime : Mime, - weight : Option -} - -impl QMime -{ - fn new(mime : Mime, weight : Option) -> Self - { - Self { mime, weight } - } -} - -#[derive(Debug, Error)] -enum QMimeError -{ - #[error("Unable to parse mime type: {0}")] - MimeError(#[from] mime::FromStrError), - #[error("Unable to parse mime quality: {0}")] - NumError(#[from] ParseFloatError) -} - -impl FromStr for QMime -{ - type Err = QMimeError; - - fn from_str(str : &str) -> Result - { - match str.find(";q=") { - None => Ok(Self::new(str.parse()?, None)), - Some(index) => { - let mime = str[..index].parse()?; - let weight = str[index+3..].parse()?; - Ok(Self::new(mime, Some(weight))) - } - } - } -} - - -/** -A route matcher that checks whether the supported types match the accept header of the request. - -Usage: - -``` -# use gotham::{helpers::http::response::create_response, hyper::StatusCode, router::builder::*}; -# use gotham_restful::matcher::AcceptHeaderMatcher; -# -# const img_content : &[u8] = b"This is the content of a webp image"; -# -# let IMAGE_WEBP : mime::Mime = "image/webp".parse().unwrap(); -let types = vec![IMAGE_WEBP]; -let matcher = AcceptHeaderMatcher::new(types); - -# build_simple_router(|route| { -// use the matcher for your request -route.post("/foo") - .extend_route_matcher(matcher) - .to(|state| { - // we know that the client is a modern browser and can handle webp images -# let IMAGE_WEBP : mime::Mime = "image/webp".parse().unwrap(); - let res = create_response(&state, StatusCode::OK, IMAGE_WEBP, img_content); - (state, res) - }); -# }); -``` -*/ -#[derive(Clone, Debug)] -pub struct AcceptHeaderMatcher -{ - types : Vec, - lookup_table : LookupTable -} - -impl AcceptHeaderMatcher -{ - /// Create a new `AcceptHeaderMatcher` with the given types that can be produced by the route. - pub fn new(types : Vec) -> Self - { - let lookup_table = LookupTable::from_types(types.iter(), true); - Self { types, lookup_table } - } -} - -#[inline] -fn err() -> RouteNonMatch -{ - RouteNonMatch::new(StatusCode::NOT_ACCEPTABLE) -} - -impl RouteMatcher for AcceptHeaderMatcher -{ - fn is_match(&self, state : &State) -> Result<(), RouteNonMatch> - { - HeaderMap::borrow_from(state).get(ACCEPT) - .map(|header| { - // parse mime types from the accept header - let acceptable = header.to_str() - .map_err(|_| err())? - .split(',') - .map(|str| str.trim().parse()) - .collect::, _>>() - .map_err(|_| err())?; - - for qmime in acceptable - { - // get mime type candidates from the lookup table - let essence = qmime.mime.essence_str(); - let candidates = match self.lookup_table.get(essence) { - Some(candidates) => candidates, - None => continue - }; - for i in candidates - { - let candidate = &self.types[*i]; - - // check that the candidates have the same suffix - this is not included in the - // essence string - if candidate.suffix() != qmime.mime.suffix() - { - continue - } - - // this candidate matches - params don't play a role in accept header matching - return Ok(()) - } - } - - // no candidates found - Err(err()) - }).unwrap_or_else(|| { - // no accept header - assume all types are acceptable - Ok(()) - }) - } -} - - -#[cfg(test)] -mod test -{ - use super::*; - - fn with_state(accept : Option<&str>, block : F) - where F : FnOnce(&mut State) -> () - { - State::with_new(|state| { - let mut headers = HeaderMap::new(); - if let Some(acc) = accept - { - headers.insert(ACCEPT, acc.parse().unwrap()); - } - state.put(headers); - block(state); - }); - } - - #[test] - fn no_accept_header() - { - let matcher = AcceptHeaderMatcher::new(vec!(mime::TEXT_PLAIN)); - with_state(None, |state| assert!(matcher.is_match(&state).is_ok())); - } - - #[test] - fn single_mime_type() - { - let matcher = AcceptHeaderMatcher::new(vec!(mime::TEXT_PLAIN, mime::IMAGE_PNG)); - with_state(Some("text/plain"), |state| assert!(matcher.is_match(&state).is_ok())); - with_state(Some("text/html"), |state| assert!(matcher.is_match(&state).is_err())); - with_state(Some("image/png"), |state| assert!(matcher.is_match(&state).is_ok())); - with_state(Some("image/webp"), |state| assert!(matcher.is_match(&state).is_err())); - } - - #[test] - fn star_star() - { - let matcher = AcceptHeaderMatcher::new(vec!(mime::IMAGE_PNG)); - with_state(Some("*/*"), |state| assert!(matcher.is_match(&state).is_ok())); - } - - #[test] - fn image_star() - { - let matcher = AcceptHeaderMatcher::new(vec!(mime::IMAGE_PNG)); - with_state(Some("image/*"), |state| assert!(matcher.is_match(&state).is_ok())); - } - - #[test] - fn complex_header() - { - let matcher = AcceptHeaderMatcher::new(vec!(mime::IMAGE_PNG)); - with_state(Some("text/html,image/webp;q=0.8"), |state| assert!(matcher.is_match(&state).is_err())); - with_state(Some("text/html,image/webp;q=0.8,*/*;q=0.1"), |state| assert!(matcher.is_match(&state).is_ok())); - } -} diff --git a/src/matcher/content_type.rs b/src/matcher/content_type.rs deleted file mode 100644 index aca8b9a..0000000 --- a/src/matcher/content_type.rs +++ /dev/null @@ -1,173 +0,0 @@ -use super::{LookupTable, LookupTableFromTypes}; -use gotham::{ - hyper::{ - header::{HeaderMap, CONTENT_TYPE}, - StatusCode - }, - router::{non_match::RouteNonMatch, route::matcher::RouteMatcher}, - state::{FromState, State} -}; -use mime::Mime; - -/** -A route matcher that checks for the presence of a supported content type. - -Usage: - -``` -# use gotham::{helpers::http::response::create_response, hyper::StatusCode, router::builder::*}; -# use gotham_restful::matcher::ContentTypeMatcher; -# -let types = vec![mime::TEXT_HTML, mime::TEXT_PLAIN]; -let matcher = ContentTypeMatcher::new(types) - // optionally accept requests with no content type - .allow_no_type(); - -# build_simple_router(|route| { -// use the matcher for your request -route.post("/foo") - .extend_route_matcher(matcher) - .to(|state| { - let res = create_response(&state, StatusCode::OK, mime::TEXT_PLAIN, "Correct Content Type!"); - (state, res) - }); -# }); -``` -*/ -#[derive(Clone, Debug)] -pub struct ContentTypeMatcher -{ - types : Vec, - lookup_table : LookupTable, - allow_no_type : bool -} - -impl ContentTypeMatcher -{ - /// Create a new `ContentTypeMatcher` with the given supported types that does not allow requests - /// that don't include a content-type header. - pub fn new(types : Vec) -> Self - { - let lookup_table = LookupTable::from_types(types.iter(), false); - Self { types, lookup_table, allow_no_type: false } - } - - /// Modify this matcher to allow requests that don't include a content-type header. - pub fn allow_no_type(mut self) -> Self - { - self.allow_no_type = true; - self - } -} - -#[inline] -fn err() -> RouteNonMatch -{ - RouteNonMatch::new(StatusCode::UNSUPPORTED_MEDIA_TYPE) -} - -impl RouteMatcher for ContentTypeMatcher -{ - fn is_match(&self, state : &State) -> Result<(), RouteNonMatch> - { - HeaderMap::borrow_from(state).get(CONTENT_TYPE) - .map(|ty| { - // parse mime type from the content type header - let mime : Mime = ty.to_str() - .map_err(|_| err())? - .parse() - .map_err(|_| err())?; - - // get mime type candidates from the lookup table - let essence = mime.essence_str(); - let candidates = self.lookup_table.get(essence).ok_or_else(err)?; - for i in candidates - { - let candidate = &self.types[*i]; - - // check that the candidates have the same suffix - this is not included in the - // essence string - if candidate.suffix() != mime.suffix() - { - continue - } - - // check that this candidate has at least the parameters that the content type - // has and that their values are equal - if candidate.params().any(|(key, value)| mime.get_param(key) != Some(value)) - { - continue - } - - // this candidate matches - return Ok(()) - } - - // no candidates found - Err(err()) - }).unwrap_or_else(|| { - // no type present - if self.allow_no_type { Ok(()) } else { Err(err()) } - }) - } -} - - -#[cfg(test)] -mod test -{ - use super::*; - - fn with_state(content_type : Option<&str>, block : F) - where F : FnOnce(&mut State) -> () - { - State::with_new(|state| { - let mut headers = HeaderMap::new(); - if let Some(ty) = content_type - { - headers.insert(CONTENT_TYPE, ty.parse().unwrap()); - } - state.put(headers); - block(state); - }); - } - - #[test] - fn empty_type_list() - { - let matcher = ContentTypeMatcher::new(Vec::new()); - with_state(None, |state| assert!(matcher.is_match(&state).is_err())); - with_state(Some("text/plain"), |state| assert!(matcher.is_match(&state).is_err())); - - let matcher = matcher.allow_no_type(); - with_state(None, |state| assert!(matcher.is_match(&state).is_ok())); - } - - #[test] - fn simple_type() - { - let matcher = ContentTypeMatcher::new(vec![mime::TEXT_PLAIN]); - with_state(None, |state| assert!(matcher.is_match(&state).is_err())); - with_state(Some("text/plain"), |state| assert!(matcher.is_match(&state).is_ok())); - with_state(Some("text/plain; charset=utf-8"), |state| assert!(matcher.is_match(&state).is_ok())); - } - - #[test] - fn complex_type() - { - let matcher = ContentTypeMatcher::new(vec!["image/svg+xml; charset=utf-8".parse().unwrap()]); - with_state(Some("image/svg"), |state| assert!(matcher.is_match(&state).is_err())); - with_state(Some("image/svg+xml"), |state| assert!(matcher.is_match(&state).is_err())); - with_state(Some("image/svg+xml; charset=utf-8"), |state| assert!(matcher.is_match(&state).is_ok())); - with_state(Some("image/svg+xml; charset=utf-8; eol=lf"), |state| assert!(matcher.is_match(&state).is_ok())); - with_state(Some("image/svg+xml; charset=us-ascii"), |state| assert!(matcher.is_match(&state).is_err())); - with_state(Some("image/svg+json; charset=utf-8"), |state| assert!(matcher.is_match(&state).is_err())); - } - - #[test] - fn type_mismatch() - { - let matcher = ContentTypeMatcher::new(vec![mime::TEXT_HTML]); - with_state(Some("text/plain"), |state| assert!(matcher.is_match(&state).is_err())); - } -} diff --git a/src/matcher/mod.rs b/src/matcher/mod.rs index 9cbfcbb..cc7e734 100644 --- a/src/matcher/mod.rs +++ b/src/matcher/mod.rs @@ -1,40 +1,5 @@ -use itertools::Itertools; -use mime::Mime; -use std::collections::HashMap; - -mod accept; -pub use accept::AcceptHeaderMatcher; - -mod content_type; -pub use content_type::ContentTypeMatcher; - #[cfg(feature = "cors")] mod access_control_request_method; #[cfg(feature = "cors")] pub use access_control_request_method::AccessControlRequestMethodMatcher; -type LookupTable = HashMap>; - -trait LookupTableFromTypes -{ - fn from_types<'a, I : Iterator>(types : I, include_stars : bool) -> Self; -} - -impl LookupTableFromTypes for LookupTable -{ - fn from_types<'a, I : Iterator>(types : I, include_stars : bool) -> Self - { - if include_stars - { - return types - .enumerate() - .flat_map(|(i, mime)| vec![("*/*".to_owned(), i), (format!("{}/*", mime.type_()), i), (mime.essence_str().to_owned(), i)].into_iter()) - .into_group_map(); - } - - types - .enumerate() - .map(|(i, mime)| (mime.essence_str().to_owned(), i)) - .into_group_map() - } -} diff --git a/src/routing.rs b/src/routing.rs index 916b244..5610379 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -1,5 +1,4 @@ use crate::{ - matcher::{AcceptHeaderMatcher, ContentTypeMatcher}, resource::*, result::{ResourceError, ResourceResult}, RequestBody, @@ -22,7 +21,7 @@ use gotham::{ router::{ builder::*, non_match::RouteNonMatch, - route::matcher::RouteMatcher + route::matcher::{AcceptHeaderRouteMatcher, ContentTypeHeaderRouteMatcher, RouteMatcher} }, state::{FromState, State} }; @@ -262,7 +261,7 @@ fn remove_handler(state : State) -> Pin + matcher : Option } impl RouteMatcher for MaybeMatchAcceptHeader @@ -285,7 +284,7 @@ impl From>> for MaybeMatchAcceptHeader types => types }; Self { - matcher: types.map(AcceptHeaderMatcher::new) + matcher: types.map(AcceptHeaderRouteMatcher::new) } } } @@ -293,7 +292,7 @@ impl From>> for MaybeMatchAcceptHeader #[derive(Clone)] struct MaybeMatchContentTypeHeader { - matcher : Option + matcher : Option } impl RouteMatcher for MaybeMatchContentTypeHeader @@ -312,7 +311,7 @@ impl From>> for MaybeMatchContentTypeHeader fn from(types : Option>) -> Self { Self { - matcher: types.map(ContentTypeMatcher::new).map(ContentTypeMatcher::allow_no_type) + matcher: types.map(|types| ContentTypeHeaderRouteMatcher::new(types).allow_no_type()) } } } From 0b0652874235de5e342ffdc00b6c6c77d9745a8d Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 20 May 2020 19:36:07 +0200 Subject: [PATCH 079/170] fix #19 remove ugly regex --- Cargo.toml | 1 - derive/Cargo.toml | 2 -- derive/src/resource_error.rs | 45 ++++++++++++++++++++++++++++-------- 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ef44aa0..6cbd276 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,6 @@ mime = "0.3.16" openapiv3 = { version = "0.3", optional = true } serde = { version = "1.0.106", features = ["derive"] } serde_json = "1.0.52" -thiserror = "1.0.16" uuid = { version = "0.8.1", optional = true } [dev-dependencies] diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 62678f0..e6cac57 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -18,10 +18,8 @@ gitlab = { repository = "msrd0/gotham-restful", branch = "master" } [dependencies] heck = "0.3.1" -lazy_static = "1.4.0" proc-macro2 = "1.0.12" quote = "1.0.4" -regex = "1.3.7" syn = "1.0.18" [features] diff --git a/derive/src/resource_error.rs b/derive/src/resource_error.rs index 60ed605..cf9040d 100644 --- a/derive/src/resource_error.rs +++ b/derive/src/resource_error.rs @@ -1,8 +1,6 @@ use crate::util::{CollectToResult, remove_parens}; -use lazy_static::lazy_static; use proc_macro2::{Ident, TokenStream}; use quote::{format_ident, quote}; -use regex::Regex; use std::iter; use syn::{ spanned::Spanned, @@ -105,11 +103,6 @@ fn path_segment(name : &str) -> PathSegment } } -lazy_static! { - // TODO this is a really ugly regex that requires at least two characters between captures - static ref DISPLAY_REGEX : Regex = Regex::new(r"(^|[^\{])\{(?P[^\}]+)\}([^\}]|$)").unwrap(); -} - impl ErrorVariant { fn fields_pat(&self) -> TokenStream @@ -131,8 +124,42 @@ impl ErrorVariant // lets find all required format parameters let display_str = display.value(); - let params = DISPLAY_REGEX.captures_iter(&display_str) - .map(|cap| format_ident!("{}{}", if self.is_named { "" } else { "arg" }, cap.name("param").unwrap().as_str())); + let mut params : Vec<&str> = Vec::new(); + let len = display_str.len(); + let mut start = len; + let mut iter = display_str.chars().enumerate().peekable(); + while let Some((i, c)) = iter.next() + { + // we found a new opening brace + if start == len && c == '{' + { + start = i + 1; + } + // we found a duplicate opening brace + else if start == i && c == '{' + { + start = len; + } + // we found a closing brace + else if start < i && c == '}' + { + match iter.peek() { + Some((_, '}')) => return Err(Error::new(display.span(), "Error parsing format string: curly braces not allowed inside parameter name")), + _ => params.push(&display_str[start..i]) + }; + start = len; + } + // we found a closing brace without content + else if start == i && c == '}' + { + return Err(Error::new(display.span(), "Error parsing format string: parameter name must not be empty")) + } + } + if start != len + { + return Err(Error::new(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 fields_pat = self.fields_pat(); Ok(quote! { From 912f030bfdba2c8bf6b62d309e32f96b9022d754 Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 20 May 2020 19:50:17 +0200 Subject: [PATCH 080/170] improve the From impl for AuthErrorOrOther #20 --- Cargo.toml | 1 + derive/src/resource_error.rs | 20 ++++++++++++-------- src/result/auth_result.rs | 14 +++++++++++++- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6cbd276..befc911 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ uuid = { version = "0.8.1", optional = true } diesel = { version = "1.4.4", features = ["postgres"] } futures-executor = "0.3.4" paste = "0.1.12" +thiserror = "1.0.18" trybuild = "1.0.26" [features] diff --git a/derive/src/resource_error.rs b/derive/src/resource_error.rs index cf9040d..032151b 100644 --- a/derive/src/resource_error.rs +++ b/derive/src/resource_error.rs @@ -167,7 +167,7 @@ impl ErrorVariant }) } - fn into_match_arm(self, krate : &TokenStream, enum_ident : &Ident) -> TokenStream + fn into_match_arm(self, krate : &TokenStream, enum_ident : &Ident) -> Result { let ident = &self.ident; let fields_pat = self.fields_pat(); @@ -185,21 +185,23 @@ impl ErrorVariant }); // the response will come directly from the from_ty if present - let res = match self.from_ty { - Some((from_index, _)) => { + let res = match (self.from_ty, status) { + (Some((from_index, _)), None) => { let from_field = &self.fields[from_index].ident; quote!(#from_field.into_response_error()) }, - None => quote!(Ok(#krate::Response { + (Some(_), Some(_)) => return Err(Error::new(ident.span(), "When #[from] is used, #[status] must not be used!")), + (None, Some(status)) => quote!(Ok(#krate::Response { status: { #status }.into(), body: #krate::gotham::hyper::Body::empty(), mime: None - })) + })), + (None, None) => return Err(Error::new(ident.span(), "Missing #[status(code)] for this variant")) }; - quote! { + Ok(quote! { #enum_ident::#ident #fields_pat => #res - } + }) } fn were(&self) -> Option @@ -293,7 +295,9 @@ pub fn expand_resource_error(input : DeriveInput) -> Result } let were = variants.iter().filter_map(|variant| variant.were()).collect::>(); - let variants = variants.into_iter().map(|variant| variant.into_match_arm(&krate, &ident)); + let variants = variants.into_iter() + .map(|variant| variant.into_match_arm(&krate, &ident)) + .collect_to_result()?; Ok(quote! { #display_impl diff --git a/src/result/auth_result.rs b/src/result/auth_result.rs index bc3f84f..10f0183 100644 --- a/src/result/auth_result.rs +++ b/src/result/auth_result.rs @@ -62,8 +62,9 @@ pub enum AuthErrorOrOther #[status(FORBIDDEN)] #[display("Forbidden")] Forbidden, + #[status(INTERNAL_SERVER_ERROR)] #[display("{0}")] - Other(#[from] E) + Other(E) } impl From for AuthErrorOrOther @@ -76,6 +77,17 @@ impl From for AuthErrorOrOther } } +impl From for AuthErrorOrOther +where + // TODO https://gitlab.com/msrd0/gotham-restful/-/issues/20 + F : std::error::Error + Into +{ + fn from(err : F) -> Self + { + Self::Other(err.into()) + } +} + /** This return type can be used to map another `ResourceResult` that can only be returned if the client is authenticated. Otherwise, an empty _403 Forbidden_ response will be issued. Use can From f72b9ac79775d97ad2ec8c171fd4f417d27d6dde Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 20 May 2020 23:55:19 +0200 Subject: [PATCH 081/170] bump version to 0.1.0-rc0 --- Cargo.toml | 21 ++++++++++----------- derive/Cargo.toml | 8 ++++---- example/Cargo.toml | 8 ++++---- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index befc911..1a28e4a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ members = ["derive", "example"] [package] name = "gotham_restful" -version = "0.1.0-dev" +version = "0.1.0-rc0" authors = ["Dominic Meiser "] edition = "2018" description = "RESTful additions for the gotham web framework" @@ -18,31 +18,31 @@ repository = "https://gitlab.com/msrd0/gotham-restful" gitlab = { repository = "msrd0/gotham-restful", branch = "master" } [dependencies] -base64 = { version = "0.12.0", optional = true } +base64 = { version = "0.12.1", optional = true } chrono = { version = "0.4.11", features = ["serde"], optional = true } cookie = { version = "0.13.3", optional = true } -futures-core = "0.3.4" -futures-util = "0.3.4" +futures-core = "0.3.5" +futures-util = "0.3.5" gotham = { version = "0.5.0-rc.1", default-features = false } gotham_derive = "0.5.0-rc.1" gotham_middleware_diesel = { version = "0.1.2", optional = true } -gotham_restful_derive = { version = "0.1.0-dev" } +gotham_restful_derive = { version = "0.1.0-rc0" } indexmap = { version = "1.3.2", optional = true } itertools = "0.9.0" jsonwebtoken = { version = "7.1.0", optional = true } log = "0.4.8" mime = "0.3.16" -openapiv3 = { version = "0.3", optional = true } -serde = { version = "1.0.106", features = ["derive"] } -serde_json = "1.0.52" +openapiv3 = { version = "0.3.2", optional = true } +serde = { version = "1.0.110", features = ["derive"] } +serde_json = "1.0.53" uuid = { version = "0.8.1", optional = true } [dev-dependencies] diesel = { version = "1.4.4", features = ["postgres"] } -futures-executor = "0.3.4" +futures-executor = "0.3.5" paste = "0.1.12" thiserror = "1.0.18" -trybuild = "1.0.26" +trybuild = "1.0.27" [features] default = ["cors", "errorlog"] @@ -58,4 +58,3 @@ all-features = true [patch.crates-io] gotham_restful = { path = "." } gotham_restful_derive = { path = "./derive" } -openapiv3 = { git = "https://github.com/glademiller/openapiv3", rev = "4c3bd95c966a3f9d59bb494c3d8e30c5c3068bdb" } diff --git a/derive/Cargo.toml b/derive/Cargo.toml index e6cac57..3511f73 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "gotham_restful_derive" -version = "0.1.0-dev" +version = "0.1.0-rc0" authors = ["Dominic Meiser "] edition = "2018" description = "RESTful additions for the gotham web framework - Derive" @@ -18,9 +18,9 @@ gitlab = { repository = "msrd0/gotham-restful", branch = "master" } [dependencies] heck = "0.3.1" -proc-macro2 = "1.0.12" -quote = "1.0.4" -syn = "1.0.18" +proc-macro2 = "1.0.13" +quote = "1.0.6" +syn = "1.0.22" [features] default = [] diff --git a/example/Cargo.toml b/example/Cargo.toml index 763ef0c..de21cba 100644 --- a/example/Cargo.toml +++ b/example/Cargo.toml @@ -14,10 +14,10 @@ repository = "https://gitlab.com/msrd0/gotham-restful" gitlab = { repository = "msrd0/gotham-restful", branch = "master" } [dependencies] -fake = "2.2" +fake = "2.2.2" gotham = { version = "0.5.0-rc.1", default-features = false } gotham_derive = "0.5.0-rc.1" -gotham_restful = { version = "0.1.0-dev", features = ["auth", "openapi"] } +gotham_restful = { version = "0.1.0-rc0", features = ["auth", "openapi"] } log = "0.4.8" -log4rs = { version = "0.12", features = ["console_appender"], default-features = false } -serde = "1.0.106" +log4rs = { version = "0.12.0", features = ["console_appender"], default-features = false } +serde = "1.0.110" From 0541ee2e0b24b4e7a4449de3e9f12a4e78245863 Mon Sep 17 00:00:00 2001 From: Dominic Date: Thu, 21 May 2020 01:44:18 +0200 Subject: [PATCH 082/170] fix derive's Cargo.toml --- derive/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 3511f73..d293eb4 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -6,7 +6,7 @@ version = "0.1.0-rc0" authors = ["Dominic Meiser "] edition = "2018" description = "RESTful additions for the gotham web framework - Derive" -keywords = ["gotham", "rest", "restful", "web", "http", "derive"] +keywords = ["gotham", "rest", "restful", "web", "http"] license = "EPL-2.0 OR Apache-2.0" repository = "https://gitlab.com/msrd0/gotham-restful" @@ -20,7 +20,7 @@ gitlab = { repository = "msrd0/gotham-restful", branch = "master" } heck = "0.3.1" proc-macro2 = "1.0.13" quote = "1.0.6" -syn = "1.0.22" +syn = { version = "1.0.22", features = ["full"] } [features] default = [] From 5317e50961bdc6bb57fde7db665ebe36b6a2cf33 Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 27 May 2020 10:22:13 +0200 Subject: [PATCH 083/170] improve feature documentation --- README.md | 73 +++++++++++++++++++++++++++++++++++++++++++++++- src/lib.rs | 82 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 153 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4de2465..643174c 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,14 @@ fn create(body : RawImage) -> Raw> { ## Features To make life easier for common use-cases, this create offers a few features that might be helpful -when you implement your web server. +when you implement your web server. The complete feature list is + - [`auth`](#authentication-feature) Advanced JWT middleware + - `chrono` openapi support for chrono types + - [`cors`](#cors-feature) CORS handling for all method handlers + - [`database`](#database-feature) diesel middleware support + - `errorlog` log errors returned from method handlers + - [`openapi`](#openapi-feature) router additions to generate an openapi spec + - `uuid` openapi support for uuid ### Authentication Feature @@ -241,6 +248,70 @@ fn main() { } ``` +### OpenAPI Feature + +The OpenAPI feature is probably the most powerful one of this crate. Definitely read this section +carefully both as a binary as well as a library author to avoid unwanted suprises. + +In order to automatically create an openapi specification, gotham-restful needs knowledge over +all routes and the types returned. `serde` does a great job at serialization but doesn't give +enough type information, so all types used in the router need to implement `OpenapiType`. This +can be derived for almoust any type and there should be no need to implement it manually. A simple +example could look like this: + +```rust +#[derive(Resource)] +#[resource(read_all)] +struct FooResource; + +#[derive(OpenapiType, Serialize)] +struct Foo { + bar: String +} + +#[read_all(FooResource)] +fn read_all() -> Success { + Foo { bar: "Hello World".to_owned() }.into() +} + +fn main() { + gotham::start("127.0.0.1:8080", build_simple_router(|route| { + let info = OpenapiInfo { + title: "My Foo API".to_owned(), + version: "0.1.0".to_owned(), + urls: vec!["https://example.org/foo/api/v1".to_owned()] + }; + route.with_openapi(info, |mut route| { + route.resource::("foo"); + route.get_openapi("openapi"); + }); + })); +} +``` + +Above example adds the resource as before, but adds another endpoint that we specified as `/openapi` +that will return the generated openapi specification. This allows you to easily write clients +in different languages without worying to exactly replicate your api in each of those languages. + +However, as of right now there is one caveat. If you wrote code before enabling the openapi feature, +it is likely to break. This is because of the new requirement of `OpenapiType` for all types used +with resources, even outside of the `with_openapi` scope. This issue will eventually be resolved. +If you are writing a library that uses gotham-restful, make sure that you expose an openapi feature. +In other words, put + +```toml +[features] +openapi = ["gotham-restful/openapi"] +``` + +into your libraries `Cargo.toml` and use the following for all types used with handlers: + +```rust +#[derive(Deserialize, Serialize)] +#[cfg_attr(feature = "openapi", derive(OpenapiType))] +struct Foo; +``` + ## Examples There is a lack of good examples, but there is currently a collection of code in the [example] diff --git a/src/lib.rs b/src/lib.rs index d7aa095..8a9ea87 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -110,7 +110,14 @@ fn create(body : RawImage) -> Raw> { # Features To make life easier for common use-cases, this create offers a few features that might be helpful -when you implement your web server. +when you implement your web server. The complete feature list is + - [`auth`](#authentication-feature) Advanced JWT middleware + - `chrono` openapi support for chrono types + - [`cors`](#cors-feature) CORS handling for all method handlers + - [`database`](#database-feature) diesel middleware support + - `errorlog` log errors returned from method handlers + - [`openapi`](#openapi-feature) router additions to generate an openapi spec + - `uuid` openapi support for uuid ## Authentication Feature @@ -265,6 +272,79 @@ fn main() { # } ``` +## OpenAPI Feature + +The OpenAPI feature is probably the most powerful one of this crate. Definitely read this section +carefully both as a binary as well as a library author to avoid unwanted suprises. + +In order to automatically create an openapi specification, gotham-restful needs knowledge over +all routes and the types returned. `serde` does a great job at serialization but doesn't give +enough type information, so all types used in the router need to implement `OpenapiType`. This +can be derived for almoust any type and there should be no need to implement it manually. A simple +example could look like this: + +```rust,no_run +# #[macro_use] extern crate gotham_restful_derive; +# #[cfg(feature = "openapi")] +# mod openapi_feature_enabled { +# use gotham::{router::builder::*, state::State}; +# use gotham_restful::*; +# use serde::{Deserialize, Serialize}; +#[derive(Resource)] +#[resource(read_all)] +struct FooResource; + +#[derive(OpenapiType, Serialize)] +struct Foo { + bar: String +} + +#[read_all(FooResource)] +fn read_all() -> Success { + Foo { bar: "Hello World".to_owned() }.into() +} + +fn main() { + gotham::start("127.0.0.1:8080", build_simple_router(|route| { + let info = OpenapiInfo { + title: "My Foo API".to_owned(), + version: "0.1.0".to_owned(), + urls: vec!["https://example.org/foo/api/v1".to_owned()] + }; + route.with_openapi(info, |mut route| { + route.resource::("foo"); + route.get_openapi("openapi"); + }); + })); +} +# } +``` + +Above example adds the resource as before, but adds another endpoint that we specified as `/openapi` +that will return the generated openapi specification. This allows you to easily write clients +in different languages without worying to exactly replicate your api in each of those languages. + +However, as of right now there is one caveat. If you wrote code before enabling the openapi feature, +it is likely to break. This is because of the new requirement of `OpenapiType` for all types used +with resources, even outside of the `with_openapi` scope. This issue will eventually be resolved. +If you are writing a library that uses gotham-restful, make sure that you expose an openapi feature. +In other words, put + +```toml +[features] +openapi = ["gotham-restful/openapi"] +``` + +into your libraries `Cargo.toml` and use the following for all types used with handlers: + +``` +# use gotham_restful::OpenapiType; +# use serde::{Deserialize, Serialize}; +#[derive(Deserialize, Serialize)] +#[cfg_attr(feature = "openapi", derive(OpenapiType))] +struct Foo; +``` + # Examples There is a lack of good examples, but there is currently a collection of code in the [example] From d55b0897e9dd93aa59c960fcb561ac403f18b4cc Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 15 Sep 2020 15:10:41 +0200 Subject: [PATCH 084/170] update to gotham 0.5 and start using rustfmt --- Cargo.toml | 11 +- derive/src/from_body.rs | 82 ++--- derive/src/lib.rs | 64 ++-- derive/src/method.rs | 357 +++++++++---------- derive/src/openapi_type.rs | 160 ++++----- derive/src/request_body.rs | 49 ++- derive/src/resource.rs | 53 +-- derive/src/resource_error.rs | 243 +++++++------ derive/src/util.rs | 40 +-- example/src/main.rs | 120 +++---- rustfmt.toml | 19 + src/auth.rs | 260 ++++++-------- src/cors.rs | 102 +++--- src/lib.rs | 64 +--- src/matcher/access_control_request_method.rs | 64 ++-- src/matcher/mod.rs | 1 - src/openapi/builder.rs | 121 +++---- src/openapi/handler.rs | 47 +-- src/openapi/mod.rs | 3 +- src/openapi/operation.rs | 168 ++++----- src/openapi/router.rs | 167 +++++---- src/openapi/types.rs | 142 ++++---- src/resource.rs | 97 ++--- src/response.rs | 41 +-- src/result/auth_result.rs | 18 +- src/result/mod.rs | 118 +++--- src/result/no_content.rs | 77 ++-- src/result/raw.rs | 90 ++--- src/result/result.rs | 95 +++-- src/result/success.rs | 98 ++--- src/routing.rs | 345 +++++++++--------- src/types.rs | 69 ++-- tests/async_methods.rs | 97 ++--- tests/cors_handling.rs | 193 +++++++--- tests/custom_request_body.rs | 28 +- tests/openapi_specification.rs | 59 ++- tests/openapi_supports_scope.rs | 24 +- tests/sync_methods.rs | 97 ++--- tests/trybuild_ui.rs | 10 +- 39 files changed, 1798 insertions(+), 2095 deletions(-) create mode 100644 rustfmt.toml diff --git a/Cargo.toml b/Cargo.toml index 1a28e4a..61682b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,12 +20,12 @@ gitlab = { repository = "msrd0/gotham-restful", branch = "master" } [dependencies] base64 = { version = "0.12.1", 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-util = "0.3.5" -gotham = { version = "0.5.0-rc.1", default-features = false } -gotham_derive = "0.5.0-rc.1" -gotham_middleware_diesel = { version = "0.1.2", optional = true } +gotham = { version = "0.5.0", default-features = false } +gotham_derive = "0.5.0" +gotham_middleware_diesel = { version = "0.2.0", optional = true } gotham_restful_derive = { version = "0.1.0-rc0" } indexmap = { version = "1.3.2", optional = true } itertools = "0.9.0" @@ -40,7 +40,7 @@ uuid = { version = "0.8.1", optional = true } [dev-dependencies] diesel = { version = "1.4.4", features = ["postgres"] } futures-executor = "0.3.5" -paste = "0.1.12" +paste = "1.0" thiserror = "1.0.18" trybuild = "1.0.27" @@ -56,5 +56,6 @@ openapi = ["gotham_restful_derive/openapi", "indexmap", "openapiv3"] all-features = true [patch.crates-io] +gotham = { path = "../gotham/gotham" } gotham_restful = { path = "." } gotham_restful_derive = { path = "./derive" } diff --git a/derive/src/from_body.rs b/derive/src/from_body.rs index f7c04ad..a4ec7b2 100644 --- a/derive/src/from_body.rs +++ b/derive/src/from_body.rs @@ -1,73 +1,65 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; use std::cmp::min; -use syn::{ - spanned::Spanned, - Data, - DeriveInput, - Error, - Field, - Fields, - Ident, - Result, - Type -}; +use syn::{spanned::Spanned, Data, DeriveInput, Error, Field, Fields, Ident, Result, Type}; -struct ParsedFields -{ - fields : Vec<(Ident, Type)>, - named : bool +struct ParsedFields { + fields: Vec<(Ident, Type)>, + named: bool } -impl ParsedFields -{ - fn from_named(fields : I) -> Self +impl ParsedFields { + fn from_named(fields: I) -> Self where - I : Iterator + I: Iterator { let fields = fields.map(|field| (field.ident.unwrap(), field.ty)).collect(); Self { fields, named: true } } - - fn from_unnamed(fields : I) -> Self + + fn from_unnamed(fields: I) -> Self where - I : Iterator + I: Iterator { - 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 } } - - fn from_unit() -> Self - { - Self { fields: Vec::new(), named: false } + + fn from_unit() -> Self { + Self { + fields: Vec::new(), + named: false + } } } -pub fn expand_from_body(input : DeriveInput) -> Result -{ +pub fn expand_from_body(input: DeriveInput) -> Result { let krate = super::krate(); let ident = input.ident; let generics = input.generics; - + let strukt = match input.data { Data::Enum(inum) => Err(inum.enum_token.span()), Data::Struct(strukt) => Ok(strukt), Data::Union(uni) => Err(uni.union_token.span()) - }.map_err(|span| Error::new(span, "#[derive(FromBody)] only works for structs"))?; - + } + .map_err(|span| Error::new(span, "#[derive(FromBody)] only works for structs"))?; + let fields = match strukt.fields { Fields::Named(named) => ParsedFields::from_named(named.named.into_iter()), Fields::Unnamed(unnamed) => ParsedFields::from_unnamed(unnamed.unnamed.into_iter()), Fields::Unit => ParsedFields::from_unit() }; - + let mut where_clause = quote!(); let mut block = quote!(); let mut body_ident = format_ident!("_body"); let mut type_ident = format_ident!("_type"); - - if let Some(body_field) = fields.fields.get(0) - { + + if let Some(body_field) = fields.fields.get(0) { body_ident = body_field.0.clone(); let body_ty = &body_field.1; where_clause = quote! { @@ -80,9 +72,8 @@ pub fn expand_from_body(input : DeriveInput) -> Result let #body_ident : #body_ty = #body_ident.into(); }; } - - if let Some(type_field) = fields.fields.get(1) - { + + if let Some(type_field) = fields.fields.get(1) { type_ident = type_field.0.clone(); let type_ty = &type_field.1; where_clause = quote! { @@ -94,9 +85,8 @@ pub fn expand_from_body(input : DeriveInput) -> Result let #type_ident : #type_ty = #type_ident.into(); }; } - - 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_ty = &field.1; where_clause = quote! { @@ -108,20 +98,20 @@ pub fn expand_from_body(input : DeriveInput) -> Result let #field_ident : #field_ty = Default::default(); }; } - - 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 { quote!(Self { #(#field_names),* }) } else { quote!(Self ( #(#field_names),* )) }; - + Ok(quote! { impl #generics #krate::FromBody for #ident #generics where #where_clause { type Err = ::std::convert::Infallible; - + fn from_body(#body_ident : #krate::gotham::hyper::body::Bytes, #type_ident : #krate::Mime) -> Result { #block diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 7e0dc00..c3f313a 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -21,114 +21,96 @@ mod openapi_type; use openapi_type::expand_openapi_type; #[inline] -fn print_tokens(tokens : TokenStream2) -> TokenStream -{ +fn print_tokens(tokens: TokenStream2) -> TokenStream { //eprintln!("{}", tokens); tokens.into() } #[inline] -fn expand_derive(input : TokenStream, expand : F) -> TokenStream +fn expand_derive(input: TokenStream, expand: F) -> TokenStream where - F : FnOnce(DeriveInput) -> Result + F: FnOnce(DeriveInput) -> Result { - print_tokens(expand(parse_macro_input!(input)) - .unwrap_or_else(|err| err.to_compile_error())) + print_tokens(expand(parse_macro_input!(input)).unwrap_or_else(|err| err.to_compile_error())) } #[inline] -fn expand_macro(attrs : TokenStream, item : TokenStream, expand : F) -> TokenStream +fn expand_macro(attrs: TokenStream, item: TokenStream, expand: F) -> TokenStream where - F : FnOnce(A, I) -> Result, - A : ParseMacroInput, - I : ParseMacroInput + F: FnOnce(A, I) -> Result, + A: ParseMacroInput, + I: ParseMacroInput { - print_tokens(expand(parse_macro_input!(attrs), parse_macro_input!(item)) - .unwrap_or_else(|err| err.to_compile_error())) + print_tokens(expand(parse_macro_input!(attrs), parse_macro_input!(item)).unwrap_or_else(|err| err.to_compile_error())) } #[inline] -fn krate() -> TokenStream2 -{ +fn krate() -> TokenStream2 { quote!(::gotham_restful) } #[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) } #[cfg(feature = "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) } #[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) } #[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) } #[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) } - #[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)) } #[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)) } #[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)) } #[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)) } #[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)) } #[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)) } #[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)) } #[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)) } diff --git a/derive/src/method.rs b/derive/src/method.rs index be19e0b..dc14359 100644 --- a/derive/src/method.rs +++ b/derive/src/method.rs @@ -2,26 +2,13 @@ use crate::util::CollectToResult; use heck::{CamelCase, SnakeCase}; use proc_macro2::{Ident, Span, TokenStream}; use quote::{format_ident, quote}; -use syn::{ - spanned::Spanned, - Attribute, - AttributeArgs, - Error, - FnArg, - ItemFn, - Lit, - LitBool, - Meta, - NestedMeta, - PatType, - Result, - ReturnType, - Type -}; use std::str::FromStr; +use syn::{ + spanned::Spanned, Attribute, AttributeArgs, Error, FnArg, ItemFn, Lit, LitBool, Meta, NestedMeta, PatType, Result, + ReturnType, Type +}; -pub enum Method -{ +pub enum Method { ReadAll, Read, Search, @@ -32,12 +19,10 @@ pub enum Method Remove } -impl FromStr for Method -{ +impl FromStr for Method { type Err = Error; - - fn from_str(str : &str) -> Result - { + + fn from_str(str: &str) -> Result { match str { "ReadAll" | "read_all" => Ok(Self::ReadAll), "Read" | "read" => Ok(Self::Read), @@ -52,12 +37,10 @@ impl FromStr for Method } } -impl Method -{ - pub fn type_names(&self) -> Vec<&'static str> - { +impl Method { + pub fn type_names(&self) -> Vec<&'static str> { use Method::*; - + match self { ReadAll | RemoveAll => vec![], Read | Remove => vec!["ID"], @@ -66,11 +49,10 @@ impl Method Change => vec!["ID", "Body"] } } - - pub fn trait_ident(&self) -> Ident - { + + pub fn trait_ident(&self) -> Ident { use Method::*; - + let name = match self { ReadAll => "ReadAll", Read => "Read", @@ -83,11 +65,10 @@ impl Method }; format_ident!("Resource{}", name) } - - pub fn fn_ident(&self) -> Ident - { + + pub fn fn_ident(&self) -> Ident { use Method::*; - + let name = match self { ReadAll => "read_all", Read => "read", @@ -100,26 +81,26 @@ impl Method }; format_ident!("{}", name) } - - pub fn mod_ident(&self, resource : &str) -> Ident - { - format_ident!("_gotham_restful_resource_{}_method_{}", resource.to_snake_case(), self.fn_ident()) + + pub fn mod_ident(&self, resource: &str) -> Ident { + format_ident!( + "_gotham_restful_resource_{}_method_{}", + resource.to_snake_case(), + self.fn_ident() + ) } - - pub fn handler_struct_ident(&self, resource : &str) -> Ident - { + + pub fn handler_struct_ident(&self, resource: &str) -> Ident { format_ident!("{}{}Handler", resource.to_camel_case(), self.trait_ident()) } - - pub fn setup_ident(&self, resource : &str) -> Ident - { + + pub fn setup_ident(&self, resource: &str) -> Ident { format_ident!("{}_{}_setup_impl", resource.to_snake_case(), self.fn_ident()) } } #[allow(clippy::large_enum_variant)] -enum MethodArgumentType -{ +enum MethodArgumentType { StateRef, StateMutRef, MethodArg(Type), @@ -128,116 +109,111 @@ enum MethodArgumentType AuthStatusRef(Type) } -impl MethodArgumentType -{ - fn is_state_ref(&self) -> bool - { +impl MethodArgumentType { + fn is_state_ref(&self) -> bool { matches!(self, Self::StateRef | Self::StateMutRef) } - - fn is_method_arg(&self) -> bool - { + + fn is_method_arg(&self) -> bool { matches!(self, Self::MethodArg(_)) } - - fn is_database_conn(&self) -> bool - { + + fn is_database_conn(&self) -> bool { matches!(self, Self::DatabaseConnection(_)) } - - fn is_auth_status(&self) -> bool - { + + fn is_auth_status(&self) -> bool { matches!(self, Self::AuthStatus(_) | Self::AuthStatusRef(_)) } - - fn ty(&self) -> Option<&Type> - { + + fn ty(&self) -> Option<&Type> { match self { Self::MethodArg(ty) | Self::DatabaseConnection(ty) | Self::AuthStatus(ty) | Self::AuthStatusRef(ty) => Some(ty), _ => None } } - - fn quote_ty(&self) -> Option - { + + fn quote_ty(&self) -> Option { self.ty().map(|ty| quote!(#ty)) } } -struct MethodArgument -{ - ident : Ident, - ident_span : Span, - ty : MethodArgumentType +struct MethodArgument { + ident: Ident, + ident_span: Span, + ty: MethodArgumentType } -impl Spanned for MethodArgument -{ - fn span(&self) -> Span - { +impl Spanned for MethodArgument { + fn span(&self) -> Span { self.ident_span } } -fn interpret_arg_ty(attrs : &[Attribute], name : &str, ty : Type) -> Result -{ - let attr = attrs.iter() +fn interpret_arg_ty(attrs: &[Attribute], name: &str, ty: Type) -> Result { + let attr = attrs + .iter() .find(|arg| arg.path.segments.iter().any(|path| &path.ident.to_string() == "rest_arg")) .map(|arg| arg.tokens.to_string()); - + // TODO issue a warning for _state usage once diagnostics become stable - if attr.as_deref() == Some("state") || (attr.is_none() && (name == "state" || name == "_state")) - { + if attr.as_deref() == Some("state") || (attr.is_none() && (name == "state" || name == "_state")) { return match ty { - Type::Reference(ty) => Ok(if ty.mutability.is_none() { MethodArgumentType::StateRef } else { MethodArgumentType::StateMutRef }), - _ => Err(Error::new(ty.span(), "The state parameter has to be a (mutable) reference to gotham_restful::State")) + Type::Reference(ty) => Ok(if ty.mutability.is_none() { + MethodArgumentType::StateRef + } else { + MethodArgumentType::StateMutRef + }), + _ => Err(Error::new( + ty.span(), + "The state parameter has to be a (mutable) reference to gotham_restful::State" + )) }; } - - if cfg!(feature = "auth") && (attr.as_deref() == Some("auth") || (attr.is_none() && name == "auth")) - { + + if cfg!(feature = "auth") && (attr.as_deref() == Some("auth") || (attr.is_none() && name == "auth")) { return Ok(match ty { Type::Reference(ty) => MethodArgumentType::AuthStatusRef(*ty.elem), ty => MethodArgumentType::AuthStatus(ty) }); } - - if cfg!(feature = "database") && (attr.as_deref() == Some("connection") || attr.as_deref() == Some("conn") || (attr.is_none() && name == "conn")) + + if cfg!(feature = "database") + && (attr.as_deref() == Some("connection") || attr.as_deref() == Some("conn") || (attr.is_none() && name == "conn")) { return Ok(MethodArgumentType::DatabaseConnection(match ty { Type::Reference(ty) => *ty.elem, ty => ty })); } - + Ok(MethodArgumentType::MethodArg(ty)) } -fn interpret_arg(index : usize, arg : &PatType) -> Result -{ +fn interpret_arg(index: usize, arg: &PatType) -> Result { let pat = &arg.pat; let ident = format_ident!("arg{}", index); let orig_name = quote!(#pat); let ty = interpret_arg_ty(&arg.attrs, &orig_name.to_string(), *arg.ty.clone())?; - - Ok(MethodArgument { ident, ident_span: arg.pat.span(), ty }) + + Ok(MethodArgument { + ident, + ident_span: arg.pat.span(), + ty + }) } #[cfg(feature = "openapi")] -fn expand_operation_id(attrs : &[NestedMeta]) -> TokenStream -{ - let mut operation_id : Option<&Lit> = None; - for meta in attrs - { - if let NestedMeta::Meta(Meta::NameValue(kv)) = meta - { - if kv.path.segments.last().map(|p| p.ident.to_string()) == Some("operation_id".to_owned()) - { +fn expand_operation_id(attrs: &[NestedMeta]) -> TokenStream { + let mut operation_id: Option<&Lit> = None; + for meta in attrs { + if let NestedMeta::Meta(Meta::NameValue(kv)) = meta { + if kv.path.segments.last().map(|p| p.ident.to_string()) == Some("operation_id".to_owned()) { operation_id = Some(&kv.lit) } } } - + match operation_id { Some(operation_id) => quote! { fn operation_id() -> Option @@ -250,26 +226,24 @@ fn expand_operation_id(attrs : &[NestedMeta]) -> TokenStream } #[cfg(not(feature = "openapi"))] -fn expand_operation_id(_ : &[NestedMeta]) -> TokenStream -{ +fn expand_operation_id(_: &[NestedMeta]) -> TokenStream { quote!() } -fn expand_wants_auth(attrs : &[NestedMeta], default : bool) -> TokenStream -{ - let default_lit = Lit::Bool(LitBool { value: default, span: Span::call_site() }); +fn expand_wants_auth(attrs: &[NestedMeta], default: bool) -> TokenStream { + let default_lit = Lit::Bool(LitBool { + value: default, + span: Span::call_site() + }); let mut wants_auth = &default_lit; - for meta in attrs - { - if let NestedMeta::Meta(Meta::NameValue(kv)) = meta - { - if kv.path.segments.last().map(|p| p.ident.to_string()) == Some("wants_auth".to_owned()) - { + for meta in attrs { + if let NestedMeta::Meta(Meta::NameValue(kv)) = meta { + if kv.path.segments.last().map(|p| p.ident.to_string()) == Some("wants_auth".to_owned()) { wants_auth = &kv.lit } } } - + quote! { fn wants_auth() -> bool { @@ -279,73 +253,80 @@ fn expand_wants_auth(attrs : &[NestedMeta], default : bool) -> TokenStream } #[allow(clippy::comparison_chain)] -pub fn expand_method(method : Method, mut attrs : AttributeArgs, fun : ItemFn) -> Result -{ +pub fn expand_method(method: Method, mut attrs: AttributeArgs, fun: ItemFn) -> Result { let krate = super::krate(); - + // parse attributes - if attrs.len() < 1 - { - return Err(Error::new(Span::call_site(), "Missing Resource struct. Example: #[read_all(MyResource)]")); + if attrs.len() < 1 { + return Err(Error::new( + Span::call_site(), + "Missing Resource struct. Example: #[read_all(MyResource)]" + )); } let resource_path = match attrs.remove(0) { NestedMeta::Meta(Meta::Path(path)) => path, - p => return Err(Error::new(p.span(), "Expected name of the Resource struct this method belongs to")) + p => { + return Err(Error::new( + p.span(), + "Expected name of the Resource struct this method belongs to" + )) + }, }; - let resource_name = resource_path.segments.last().map(|s| s.ident.to_string()) - .ok_or_else(|| Error::new(resource_path.span(), "Resource name must not be empty"))?; - + let resource_name = resource_path + .segments + .last() + .map(|s| s.ident.to_string()) + .ok_or_else(|| Error::new(resource_path.span(), "Resource name must not be empty"))?; + let fun_ident = &fun.sig.ident; let fun_vis = &fun.vis; let fun_is_async = fun.sig.asyncness.is_some(); - - if let Some(unsafety) = fun.sig.unsafety - { + + if let Some(unsafety) = fun.sig.unsafety { return Err(Error::new(unsafety.span(), "Resource methods must not be unsafe")); } - + let trait_ident = method.trait_ident(); let method_ident = method.fn_ident(); let mod_ident = method.mod_ident(&resource_name); let handler_ident = method.handler_struct_ident(&resource_name); let setup_ident = method.setup_ident(&resource_name); - + let (ret, is_no_content) = match &fun.sig.output { ReturnType::Default => (quote!(#krate::NoContent), true), ReturnType::Type(_, ty) => (quote!(#ty), false) }; - + // some default idents we'll need let state_ident = format_ident!("state"); let repo_ident = format_ident!("repo"); let conn_ident = format_ident!("conn"); let auth_ident = format_ident!("auth"); let res_ident = format_ident!("res"); - + // extract arguments into pattern, ident and type - let args = fun.sig.inputs.iter() + let args = fun + .sig + .inputs + .iter() .enumerate() .map(|(i, arg)| match arg { FnArg::Typed(arg) => interpret_arg(i, arg), FnArg::Receiver(_) => Err(Error::new(arg.span(), "Didn't expect self parameter")) }) .collect_to_result()?; - + // extract the generic parameters to use let ty_names = method.type_names(); let ty_len = ty_names.len(); - let generics_args : Vec<&MethodArgument> = args.iter() - .filter(|arg| (*arg).ty.is_method_arg()) - .collect(); - if generics_args.len() > ty_len - { + let generics_args: Vec<&MethodArgument> = args.iter().filter(|arg| (*arg).ty.is_method_arg()).collect(); + if generics_args.len() > ty_len { return Err(Error::new(generics_args[ty_len].span(), "Too many arguments")); - } - else if generics_args.len() < ty_len - { + } else if generics_args.len() < ty_len { return Err(Error::new(fun_ident.span(), "Too few arguments")); } - let generics : Vec = generics_args.iter() + let generics: Vec = generics_args + .iter() .map(|arg| arg.ty.quote_ty().unwrap()) .zip(ty_names) .map(|(arg, name)| { @@ -353,47 +334,53 @@ pub fn expand_method(method : Method, mut attrs : AttributeArgs, fun : ItemFn) - quote!(type #ident = #arg;) }) .collect(); - + // extract the definition of our method - let mut args_def : Vec = args.iter() + let mut args_def: Vec = args + .iter() .filter(|arg| (*arg).ty.is_method_arg()) .map(|arg| { let ident = &arg.ident; let ty = arg.ty.quote_ty(); quote!(#ident : #ty) - }).collect(); + }) + .collect(); args_def.insert(0, quote!(mut #state_ident : #krate::State)); - + // extract the arguments to pass over to the supplied method - let args_pass : Vec = args.iter().map(|arg| match (&arg.ty, &arg.ident) { - (MethodArgumentType::StateRef, _) => quote!(&#state_ident), - (MethodArgumentType::StateMutRef, _) => quote!(&mut #state_ident), - (MethodArgumentType::MethodArg(_), ident) => quote!(#ident), - (MethodArgumentType::DatabaseConnection(_), _) => quote!(&#conn_ident), - (MethodArgumentType::AuthStatus(_), _) => quote!(#auth_ident), - (MethodArgumentType::AuthStatusRef(_), _) => quote!(&#auth_ident) - }).collect(); - + let args_pass: Vec = args + .iter() + .map(|arg| match (&arg.ty, &arg.ident) { + (MethodArgumentType::StateRef, _) => quote!(&#state_ident), + (MethodArgumentType::StateMutRef, _) => quote!(&mut #state_ident), + (MethodArgumentType::MethodArg(_), ident) => quote!(#ident), + (MethodArgumentType::DatabaseConnection(_), _) => quote!(&#conn_ident), + (MethodArgumentType::AuthStatus(_), _) => quote!(#auth_ident), + (MethodArgumentType::AuthStatusRef(_), _) => quote!(&#auth_ident) + }) + .collect(); + // prepare the method block let mut block = quote!(#fun_ident(#(#args_pass),*)); let mut state_block = quote!(); - if fun_is_async - { - if let Some(arg) = args.iter().find(|arg| (*arg).ty.is_state_ref()) - { - return Err(Error::new(arg.span(), "async fn must not take &State as an argument as State is not Sync, consider boxing")); + if fun_is_async { + if let Some(arg) = args.iter().find(|arg| (*arg).ty.is_state_ref()) { + return Err(Error::new( + arg.span(), + "async fn must not take &State as an argument as State is not Sync, consider boxing" + )); } block = quote!(#block.await); } - if is_no_content - { + if is_no_content { block = quote!(#block; Default::default()) } - if let Some(arg) = args.iter().find(|arg| (*arg).ty.is_database_conn()) - { - if fun_is_async - { - return Err(Error::new(arg.span(), "async fn is not supported when database support is required, consider boxing")); + if let Some(arg) = args.iter().find(|arg| (*arg).ty.is_database_conn()) { + if fun_is_async { + return Err(Error::new( + arg.span(), + "async fn is not supported when database support is required, consider boxing" + )); } let conn_ty = arg.ty.quote_ty(); state_block = quote! { @@ -411,70 +398,68 @@ 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(); state_block = quote! { #state_block let #auth_ident : #auth_ty = <#auth_ty>::borrow_from(&#state_ident).clone(); }; } - + // prepare the where clause let mut where_clause = quote!(#resource_path : #krate::Resource,); - for arg in args.iter().filter(|arg| (*arg).ty.is_auth_status()) - { + for arg in args.iter().filter(|arg| (*arg).ty.is_auth_status()) { let auth_ty = arg.ty.quote_ty(); where_clause = quote!(#where_clause #auth_ty : Clone,); } - + // attribute generated code let operation_id = expand_operation_id(&attrs); let wants_auth = expand_wants_auth(&attrs, args.iter().any(|arg| (*arg).ty.is_auth_status())); - + // put everything together Ok(quote! { #fun - + #fun_vis mod #mod_ident { use super::*; - + struct #handler_ident; - + impl #krate::ResourceMethod for #handler_ident { type Res = #ret; - + #operation_id #wants_auth } - + impl #krate::#trait_ident for #handler_ident where #where_clause { #(#generics)* - + fn #method_ident(#(#args_def),*) -> std::pin::Pin + Send>> { #[allow(unused_imports)] use #krate::{export::FutureExt, FromState}; - + #state_block - + async move { let #res_ident = { #block }; (#state_ident, #res_ident) }.boxed() } } - + #[deny(dead_code)] pub fn #setup_ident(route : &mut D) { route.#method_ident::<#handler_ident>(); } - + } }) } diff --git a/derive/src/openapi_type.rs b/derive/src/openapi_type.rs index 57a7ceb..58965ba 100644 --- a/derive/src/openapi_type.rs +++ b/derive/src/openapi_type.rs @@ -1,99 +1,77 @@ -use crate::util::{CollectToResult, remove_parens}; +use crate::util::{remove_parens, CollectToResult}; use proc_macro2::{Ident, TokenStream}; use quote::quote; use syn::{ - parse_macro_input, - spanned::Spanned, - Attribute, - AttributeArgs, - Data, - DataEnum, - DataStruct, - DeriveInput, - Error, - Field, - Fields, - Generics, - GenericParam, - Lit, - LitStr, - Meta, - NestedMeta, - Result, - Variant + parse_macro_input, spanned::Spanned, Attribute, AttributeArgs, Data, DataEnum, DataStruct, DeriveInput, Error, Field, + Fields, GenericParam, Generics, Lit, LitStr, Meta, NestedMeta, Result, Variant }; -pub fn expand_openapi_type(input : DeriveInput) -> Result -{ +pub fn expand_openapi_type(input: DeriveInput) -> Result { match (input.ident, input.generics, input.attrs, input.data) { (ident, generics, attrs, Data::Enum(inum)) => expand_enum(ident, generics, attrs, inum), (ident, generics, attrs, Data::Struct(strukt)) => expand_struct(ident, generics, attrs, strukt), - (_, _, _, Data::Union(uni)) => Err(Error::new(uni.union_token.span(), "#[derive(OpenapiType)] only works for structs and enums")) + (_, _, _, Data::Union(uni)) => Err(Error::new( + uni.union_token.span(), + "#[derive(OpenapiType)] only works for structs and enums" + )) } } -fn expand_where(generics : &Generics) -> TokenStream -{ - if generics.params.is_empty() - { +fn expand_where(generics: &Generics) -> TokenStream { + if generics.params.is_empty() { return quote!(); } - + let krate = super::krate(); - let idents = generics.params.iter() + let idents = generics + .params + .iter() .map(|param| match param { GenericParam::Type(ty) => Some(ty.ident.clone()), _ => None }) .filter(|param| param.is_some()) .map(|param| param.unwrap()); - + quote! { where #(#idents : #krate::OpenapiType),* } } #[derive(Debug, Default)] -struct Attrs -{ - nullable : bool, - rename : Option +struct Attrs { + nullable: bool, + rename: Option } -fn to_string(lit : &Lit) -> Result -{ +fn to_string(lit: &Lit) -> Result { match lit { Lit::Str(str) => Ok(str.value()), _ => Err(Error::new(lit.span(), "Expected string literal")) } } -fn to_bool(lit : &Lit) -> Result -{ +fn to_bool(lit: &Lit) -> Result { match lit { Lit::Bool(bool) => Ok(bool.value), _ => Err(Error::new(lit.span(), "Expected bool")) } } -fn parse_attributes(input : &[Attribute]) -> Result -{ +fn parse_attributes(input: &[Attribute]) -> Result { let mut parsed = Attrs::default(); - for attr in input - { - if attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("openapi".to_owned()) - { + for attr in input { + if attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("openapi".to_owned()) { let tokens = remove_parens(attr.tokens.clone()); // TODO this is not public api but syn currently doesn't offer another convenient way to parse AttributeArgs let nested = parse_macro_input::parse::(tokens.into())?; - for meta in nested - { + for meta in nested { match &meta { NestedMeta::Meta(Meta::NameValue(kv)) => match kv.path.segments.last().map(|s| s.ident.to_string()) { Some(key) => match key.as_ref() { "nullable" => parsed.nullable = to_bool(&kv.lit)?, - "rename" => parsed.rename = Some(to_string(&kv.lit)?), - _ => return Err(Error::new(kv.path.span(), "Unknown key")), + "rename" => parsed.rename = Some(to_string(&kv.lit)?), + _ => return Err(Error::new(kv.path.span(), "Unknown key")) }, _ => return Err(Error::new(meta.span(), "Unexpected token")) }, @@ -105,42 +83,40 @@ fn parse_attributes(input : &[Attribute]) -> Result Ok(parsed) } -fn expand_variant(variant : &Variant) -> Result -{ - if variant.fields != Fields::Unit - { - return Err(Error::new(variant.span(), "#[derive(OpenapiType)] does not support enum variants with fields")); +fn expand_variant(variant: &Variant) -> Result { + if variant.fields != Fields::Unit { + return Err(Error::new( + variant.span(), + "#[derive(OpenapiType)] does not support enum variants with fields" + )); } - + let ident = &variant.ident; - + let attrs = parse_attributes(&variant.attrs)?; let name = match attrs.rename { Some(rename) => rename, None => ident.to_string() }; - + Ok(quote! { enumeration.push(#name.to_string()); }) } -fn expand_enum(ident : Ident, generics : Generics, attrs : Vec, input : DataEnum) -> Result -{ +fn expand_enum(ident: Ident, generics: Generics, attrs: Vec, input: DataEnum) -> Result { let krate = super::krate(); let where_clause = expand_where(&generics); - + let attrs = parse_attributes(&attrs)?; let nullable = attrs.nullable; let name = match attrs.rename { Some(rename) => rename, None => ident.to_string() }; - - let variants = input.variants.iter() - .map(expand_variant) - .collect_to_result()?; - + + let variants = input.variants.iter().map(expand_variant).collect_to_result()?; + Ok(quote! { impl #generics #krate::OpenapiType for #ident #generics #where_clause @@ -148,11 +124,11 @@ fn expand_enum(ident : Ident, generics : Generics, attrs : Vec, input fn schema() -> #krate::OpenapiSchema { use #krate::{export::openapi::*, OpenapiSchema}; - + let mut enumeration : Vec = Vec::new(); - + #(#variants)* - + let schema = SchemaKind::Type(Type::String(StringType { format: VariantOrUnknownOrEmpty::Empty, enumeration, @@ -170,25 +146,29 @@ fn expand_enum(ident : Ident, generics : Generics, attrs : Vec, input }) } -fn expand_field(field : &Field) -> Result -{ +fn expand_field(field: &Field) -> Result { let ident = match &field.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 ty = &field.ty; - + let attrs = parse_attributes(&field.attrs)?; let nullable = attrs.nullable; let name = match attrs.rename { Some(rename) => rename, None => ident.to_string() }; - + Ok(quote! {{ let mut schema = <#ty>::schema(); - + if schema.nullable { schema.nullable = false; @@ -197,7 +177,7 @@ fn expand_field(field : &Field) -> Result { required.push(#ident_str.to_string()); } - + let keys : Vec = schema.dependencies.keys().map(|k| k.to_string()).collect(); for dep in keys { @@ -207,7 +187,7 @@ fn expand_field(field : &Field) -> Result dependencies.insert(dep, dep_schema); } } - + match schema.name.clone() { Some(schema_name) => { properties.insert( @@ -226,42 +206,42 @@ fn expand_field(field : &Field) -> Result }}) } -fn expand_struct(ident : Ident, generics : Generics, attrs : Vec, input : DataStruct) -> Result -{ +fn expand_struct(ident: Ident, generics: Generics, attrs: Vec, input: DataStruct) -> Result { let krate = super::krate(); let where_clause = expand_where(&generics); - + let attrs = parse_attributes(&attrs)?; let nullable = attrs.nullable; let name = match attrs.rename { Some(rename) => rename, None => ident.to_string() }; - - let fields : Vec = match input.fields { - Fields::Named(named_fields) => { - named_fields.named.iter() - .map(expand_field) - .collect_to_result()? + + let fields: Vec = match input.fields { + Fields::Named(named_fields) => named_fields.named.iter().map(expand_field).collect_to_result()?, + Fields::Unnamed(fields) => { + return Err(Error::new( + fields.span(), + "#[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() }; - - Ok(quote!{ + + Ok(quote! { impl #generics #krate::OpenapiType for #ident #generics #where_clause { fn schema() -> #krate::OpenapiSchema { use #krate::{export::{openapi::*, IndexMap}, OpenapiSchema}; - + let mut properties : IndexMap>> = IndexMap::new(); let mut required : Vec = Vec::new(); let mut dependencies : IndexMap = IndexMap::new(); - + #(#fields)* - + let schema = SchemaKind::Type(Type::Object(ObjectType { properties, required, diff --git a/derive/src/request_body.rs b/derive/src/request_body.rs index 76c80aa..c469a79 100644 --- a/derive/src/request_body.rs +++ b/derive/src/request_body.rs @@ -6,43 +6,34 @@ use syn::{ parse::{Parse, ParseStream}, punctuated::Punctuated, spanned::Spanned, - DeriveInput, - Error, - Generics, - Path, - Result, - Token + DeriveInput, Error, Generics, Path, Result, Token }; struct MimeList(Punctuated); -impl Parse for MimeList -{ - fn parse(input: ParseStream) -> Result - { +impl Parse for MimeList { + fn parse(input: ParseStream) -> Result { let list = Punctuated::parse_separated_nonempty(&input)?; Ok(Self(list)) } } #[cfg(not(feature = "openapi"))] -fn impl_openapi_type(_ident : &Ident, _generics : &Generics) -> TokenStream -{ +fn impl_openapi_type(_ident: &Ident, _generics: &Generics) -> TokenStream { quote!() } #[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(); - + quote! { impl #generics #krate::OpenapiType for #ident #generics { fn schema() -> #krate::OpenapiSchema { use #krate::{export::openapi::*, OpenapiSchema}; - + OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { format: VariantOrUnknownOrEmpty::Item(StringFormat::Binary), ..Default::default() @@ -52,32 +43,38 @@ fn impl_openapi_type(ident : &Ident, generics : &Generics) -> TokenStream } } -pub fn expand_request_body(input : DeriveInput) -> Result -{ +pub fn expand_request_body(input: DeriveInput) -> Result { let krate = super::krate(); let ident = input.ident; let generics = input.generics; - - let types = input.attrs.into_iter() - .filter(|attr| attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("supported_types".to_string())) + + let types = input + .attrs + .into_iter() + .filter(|attr| { + attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("supported_types".to_string()) + }) .flat_map(|attr| { let span = attr.span(); attr.parse_args::() .map(|list| Box::new(list.0.into_iter().map(Ok)) as Box>>) .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))) }) }) .collect_to_result()?; - + let types = match types { ref types if types.is_empty() => quote!(None), types => quote!(Some(vec![#(#types),*])) }; - + let impl_openapi_type = impl_openapi_type(&ident, &generics); - + Ok(quote! { impl #generics #krate::RequestBody for #ident #generics where #ident #generics : #krate::FromBody @@ -87,7 +84,7 @@ pub fn expand_request_body(input : DeriveInput) -> Result #types } } - + #impl_openapi_type }) } diff --git a/derive/src/resource.rs b/derive/src/resource.rs index 79482e6..a81e6d9 100644 --- a/derive/src/resource.rs +++ b/derive/src/resource.rs @@ -1,23 +1,18 @@ use crate::{method::Method, util::CollectToResult}; use proc_macro2::{Ident, TokenStream}; use quote::quote; +use std::{iter, str::FromStr}; use syn::{ parenthesized, parse::{Parse, ParseStream}, punctuated::Punctuated, - DeriveInput, - Error, - Result, - Token + DeriveInput, Error, Result, Token }; -use std::{iter, str::FromStr}; struct MethodList(Punctuated); -impl Parse for MethodList -{ - fn parse(input: ParseStream) -> Result - { +impl Parse for MethodList { + fn parse(input: ParseStream) -> Result { let content; let _paren = parenthesized!(content in input); let list = Punctuated::parse_separated_nonempty(&content)?; @@ -25,26 +20,32 @@ impl Parse for MethodList } } -pub fn expand_resource(input : DeriveInput) -> Result -{ +pub fn expand_resource(input: DeriveInput) -> Result { let krate = super::krate(); let ident = input.ident; let name = ident.to_string(); - - let methods = input.attrs.into_iter().filter(|attr| - attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("resource".to_string()) // TODO wtf - ).map(|attr| { - syn::parse2(attr.tokens).map(|m : MethodList| m.0.into_iter()) - }).flat_map(|list| match list { - Ok(iter) => Box::new(iter.map(|method| { - let method = Method::from_str(&method.to_string()).map_err(|err| Error::new(method.span(), err))?; - let mod_ident = method.mod_ident(&name); - let ident = method.setup_ident(&name); - Ok(quote!(#mod_ident::#ident(&mut route);)) - })) as Box>>, - Err(err) => Box::new(iter::once(Err(err))) - }).collect_to_result()?; - + + let methods = + input + .attrs + .into_iter() + .filter( + |attr| { + attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("resource".to_string()) + } // TODO wtf + ) + .map(|attr| syn::parse2(attr.tokens).map(|m: MethodList| m.0.into_iter())) + .flat_map(|list| match list { + Ok(iter) => Box::new(iter.map(|method| { + let method = Method::from_str(&method.to_string()).map_err(|err| Error::new(method.span(), err))?; + let mod_ident = method.mod_ident(&name); + let ident = method.setup_ident(&name); + Ok(quote!(#mod_ident::#ident(&mut route);)) + })) as Box>>, + Err(err) => Box::new(iter::once(Err(err))) + }) + .collect_to_result()?; + Ok(quote! { impl #krate::Resource for #ident { diff --git a/derive/src/resource_error.rs b/derive/src/resource_error.rs index 032151b..b3af040 100644 --- a/derive/src/resource_error.rs +++ b/derive/src/resource_error.rs @@ -1,68 +1,54 @@ -use crate::util::{CollectToResult, remove_parens}; +use crate::util::{remove_parens, CollectToResult}; use proc_macro2::{Ident, TokenStream}; use quote::{format_ident, quote}; use std::iter; use syn::{ - spanned::Spanned, - Attribute, - Data, - DeriveInput, - Error, - Fields, - GenericParam, - LitStr, - Path, - PathSegment, - Result, - Type, + spanned::Spanned, Attribute, Data, DeriveInput, Error, Fields, GenericParam, LitStr, Path, PathSegment, Result, Type, Variant }; - -struct ErrorVariantField -{ - attrs : Vec, - ident : Ident, - ty : Type +struct ErrorVariantField { + attrs: Vec, + ident: Ident, + ty: Type } -struct ErrorVariant -{ - ident : Ident, - status : Option, - is_named : bool, - fields : Vec, - from_ty : Option<(usize, Type)>, - display : Option +struct ErrorVariant { + ident: Ident, + status: Option, + is_named: bool, + fields: Vec, + from_ty: Option<(usize, Type)>, + display: Option } -fn process_variant(variant : Variant) -> Result -{ - let status = match variant.attrs.iter() - .find(|attr| attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("status".to_string())) - { - Some(attr) => Some(syn::parse2(remove_parens(attr.tokens.clone()))?), - None => None - }; - +fn process_variant(variant: Variant) -> Result { + let status = + match variant.attrs.iter().find(|attr| { + attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("status".to_string()) + }) { + Some(attr) => Some(syn::parse2(remove_parens(attr.tokens.clone()))?), + None => None + }; + let mut is_named = false; let mut fields = Vec::new(); match variant.fields { Fields::Named(named) => { is_named = true; - for field in named.named - { + for field in named.named { let span = field.span(); fields.push(ErrorVariantField { attrs: field.attrs, - ident: field.ident.ok_or_else(|| Error::new(span, "Missing ident for this enum variant field"))?, + ident: field + .ident + .ok_or_else(|| Error::new(span, "Missing ident for this enum variant field"))?, ty: field.ty }); } }, Fields::Unnamed(unnamed) => { - for (i, field) in unnamed.unnamed.into_iter().enumerate() - { + for (i, field) in unnamed.unnamed.into_iter().enumerate() { fields.push(ErrorVariantField { attrs: field.attrs, ident: format_ident!("arg{}", i), @@ -72,19 +58,25 @@ fn process_variant(variant : Variant) -> Result }, Fields::Unit => {} } - - let from_ty = fields.iter() + + let from_ty = fields + .iter() .enumerate() - .find(|(_, field)| field.attrs.iter().any(|attr| attr.path.segments.last().map(|segment| segment.ident.to_string()) == Some("from".to_string()))) + .find(|(_, field)| { + field + .attrs + .iter() + .any(|attr| attr.path.segments.last().map(|segment| segment.ident.to_string()) == Some("from".to_string())) + }) .map(|(i, field)| (i, field.ty.clone())); - - let display = match variant.attrs.iter() - .find(|attr| attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("display".to_string())) - { + + let display = match variant.attrs.iter().find(|attr| { + attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("display".to_string()) + }) { Some(attr) => Some(syn::parse2(remove_parens(attr.tokens.clone()))?), None => None }; - + Ok(ErrorVariant { ident: variant.ident, status, @@ -95,18 +87,15 @@ fn process_variant(variant : Variant) -> Result }) } -fn path_segment(name : &str) -> PathSegment -{ +fn path_segment(name: &str) -> PathSegment { PathSegment { ident: format_ident!("{}", name), arguments: Default::default() } } -impl ErrorVariant -{ - fn fields_pat(&self) -> TokenStream - { +impl ErrorVariant { + fn fields_pat(&self) -> TokenStream { let mut fields = self.fields.iter().map(|field| &field.ident).peekable(); if fields.peek().is_none() { quote!() @@ -116,74 +105,90 @@ impl ErrorVariant quote!( ( #( #fields ),* ) ) } } - - fn to_display_match_arm(&self, formatter_ident : &Ident, enum_ident : &Ident) -> Result - { + + fn to_display_match_arm(&self, formatter_ident: &Ident, enum_ident: &Ident) -> Result { 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 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 mut start = len; 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 - if start == len && c == '{' - { + if start == len && c == '{' { start = i + 1; } // we found a duplicate opening brace - else if start == i && c == '{' - { + else if start == i && c == '{' { start = len; } // we found a closing brace - else if start < i && c == '}' - { + else if start < i && c == '}' { 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]) }; start = len; } // we found a closing brace without content - else if start == i && c == '}' - { - return Err(Error::new(display.span(), "Error parsing format string: parameter name must not be empty")) + else if start == i && c == '}' { + return Err(Error::new( + display.span(), + "Error parsing format string: parameter name must not be empty" + )); } } - if start != len - { - return Err(Error::new(display.span(), "Error parsing format string: Unmatched opening brace")); + if start != len { + return Err(Error::new( + 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(); Ok(quote! { #enum_ident::#ident #fields_pat => write!(#formatter_ident, #display #(, #params = #params)*) }) } - - fn into_match_arm(self, krate : &TokenStream, enum_ident : &Ident) -> Result - { + + fn into_match_arm(self, krate: &TokenStream, enum_ident: &Ident) -> Result { let ident = &self.ident; let fields_pat = self.fields_pat(); let status = self.status.map(|status| { // the status might be relative to StatusCode, so let's fix that - if status.leading_colon.is_none() && status.segments.len() < 2 - { + if status.leading_colon.is_none() && status.segments.len() < 2 { let status_ident = status.segments.first().cloned().unwrap_or_else(|| path_segment("OK")); Path { leading_colon: Some(Default::default()), - segments: vec![path_segment("gotham_restful"), path_segment("gotham"), path_segment("hyper"), path_segment("StatusCode"), status_ident].into_iter().collect() + 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 let res = match (self.from_ty, status) { (Some((from_index, _)), None) => { @@ -198,14 +203,13 @@ impl ErrorVariant })), (None, None) => return Err(Error::new(ident.span(), "Missing #[status(code)] for this variant")) }; - + Ok(quote! { #enum_ident::#ident #fields_pat => #res }) } - - fn were(&self) -> Option - { + + fn were(&self) -> Option { match self.from_ty.as_ref() { Some((_, ty)) => Some(quote!( #ty : ::std::error::Error )), None => None @@ -213,22 +217,22 @@ impl ErrorVariant } } -pub fn expand_resource_error(input : DeriveInput) -> Result -{ +pub fn expand_resource_error(input: DeriveInput) -> Result { let krate = super::krate(); let ident = input.ident; let generics = input.generics; - + let inum = match input.data { Data::Enum(inum) => Ok(inum), Data::Struct(strukt) => Err(strukt.struct_token.span()), Data::Union(uni) => Err(uni.union_token.span()) - }.map_err(|span| Error::new(span, "#[derive(ResourceError)] only works for enums"))?; - let variants = inum.variants.into_iter() - .map(process_variant) - .collect_to_result()?; - - let display_impl = if variants.iter().any(|v| v.display.is_none()) { None } else { + } + .map_err(|span| Error::new(span, "#[derive(ResourceError)] only works for enums"))?; + let variants = inum.variants.into_iter().map(process_variant).collect_to_result()?; + + let display_impl = if variants.iter().any(|v| v.display.is_none()) { + None + } else { let were = generics.params.iter().filter_map(|param| match param { GenericParam::Type(ty) => { let ident = &ty.ident; @@ -237,7 +241,8 @@ pub fn expand_resource_error(input : DeriveInput) -> Result _ => None }); 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)) .collect_to_result()?; Some(quote! { @@ -253,34 +258,39 @@ pub fn expand_resource_error(input : DeriveInput) -> Result } }) }; - - let mut from_impls : Vec = Vec::new(); - - for var in &variants - { + + let mut from_impls: Vec = Vec::new(); + + for var in &variants { let var_ident = &var.ident; let (from_index, from_ty) = match var.from_ty.as_ref() { Some(f) => f, None => continue }; let from_ident = &var.fields[*from_index].ident; - + let fields_pat = var.fields_pat(); - let fields_where = var.fields.iter().enumerate() + let fields_where = var + .fields + .iter() + .enumerate() .filter(|(i, _)| i != from_index) .map(|(_, field)| { let ty = &field.ty; quote!( #ty : Default ) }) .chain(iter::once(quote!( #from_ty : ::std::error::Error ))); - let fields_let = var.fields.iter().enumerate() + let fields_let = var + .fields + .iter() + .enumerate() .filter(|(i, _)| i != from_index) .map(|(_, field)| { let id = &field.ident; let ty = &field.ty; quote!( let #id : #ty = Default::default(); ) }); - + from_impls.push(quote! { impl #generics ::std::convert::From<#from_ty> for #ident #generics where #( #fields_where ),* @@ -293,20 +303,21 @@ pub fn expand_resource_error(input : DeriveInput) -> Result } }); } - + let were = variants.iter().filter_map(|variant| variant.were()).collect::>(); - let variants = variants.into_iter() + let variants = variants + .into_iter() .map(|variant| variant.into_match_arm(&krate, &ident)) - .collect_to_result()?; - + .collect_to_result()?; + Ok(quote! { #display_impl - + impl #generics #krate::IntoResponseError for #ident #generics where #( #were ),* { type Err = #krate::export::serde_json::Error; - + fn into_response_error(self) -> Result<#krate::Response, Self::Err> { match self { @@ -314,7 +325,7 @@ pub fn expand_resource_error(input : DeriveInput) -> Result } } } - + #( #from_impls )* }) } diff --git a/derive/src/util.rs b/derive/src/util.rs index d82dc31..aedb9e6 100644 --- a/derive/src/util.rs +++ b/derive/src/util.rs @@ -2,40 +2,38 @@ use proc_macro2::{Delimiter, TokenStream, TokenTree}; use std::iter; use syn::Error; -pub trait CollectToResult -{ +pub trait CollectToResult { type Item; - + fn collect_to_result(self) -> Result, Error>; } impl CollectToResult for I where - I : Iterator> + I: Iterator> { type Item = Item; - - fn collect_to_result(self) -> Result, Error> - { - self.fold(, Error>>::Ok(Vec::new()), |res, code| { - match (code, res) { - (Ok(code), Ok(mut codes)) => { codes.push(code); Ok(codes) }, - (Ok(_), Err(errors)) => Err(errors), - (Err(err), Ok(_)) => Err(err), - (Err(err), Err(mut errors)) => { errors.combine(err); Err(errors) } - } + + fn collect_to_result(self) -> Result, Error> { + self.fold(, Error>>::Ok(Vec::new()), |res, code| match (code, res) { + (Ok(code), Ok(mut codes)) => { + codes.push(code); + Ok(codes) + }, + (Ok(_), Err(errors)) => Err(errors), + (Err(err), Ok(_)) => Err(err), + (Err(err), Err(mut errors)) => { + errors.combine(err); + Err(errors) + } }) } } - -pub fn remove_parens(input : TokenStream) -> TokenStream -{ +pub fn remove_parens(input: TokenStream) -> TokenStream { let iter = input.into_iter().flat_map(|tt| { - if let TokenTree::Group(group) = &tt - { - if group.delimiter() == Delimiter::Parenthesis - { + if let TokenTree::Group(group) = &tt { + if group.delimiter() == Delimiter::Parenthesis { return Box::new(group.stream().into_iter()) as Box>; } } diff --git a/example/src/main.rs b/example/src/main.rs index f953e45..4689ebe 100644 --- a/example/src/main.rs +++ b/example/src/main.rs @@ -1,5 +1,7 @@ -#[macro_use] extern crate gotham_derive; -#[macro_use] extern crate log; +#[macro_use] +extern crate gotham_derive; +#[macro_use] +extern crate log; use fake::{faker::internet::en::Username, Fake}; use gotham::{ @@ -20,25 +22,19 @@ use serde::{Deserialize, Serialize}; #[derive(Resource)] #[resource(read_all, read, search, create, change_all, change, remove, remove_all)] -struct Users -{ -} +struct Users {} #[derive(Resource)] #[resource(ReadAll)] -struct Auth -{ -} +struct Auth {} #[derive(Deserialize, OpenapiType, Serialize, StateData, StaticResponseExtender)] -struct User -{ - username : String +struct User { + username: String } #[read_all(Users)] -fn read_all() -> Success>> -{ +fn read_all() -> Success>> { vec![Username().fake(), Username().fake()] .into_iter() .map(|username| Some(User { username })) @@ -47,113 +43,101 @@ fn read_all() -> Success>> } #[read(Users)] -fn read(id : u64) -> Success -{ - let username : String = Username().fake(); - User { username: format!("{}{}", username, id) }.into() +fn read(id: u64) -> Success { + let username: String = Username().fake(); + User { + username: format!("{}{}", username, id) + } + .into() } #[search(Users)] -fn search(query : User) -> Success -{ +fn search(query: User) -> Success { query.into() } #[create(Users)] -fn create(body : User) -{ +fn create(body: User) { info!("Created User: {}", body.username); } #[change_all(Users)] -fn update_all(body : Vec) -{ - info!("Changing all Users to {:?}", body.into_iter().map(|u| u.username).collect::>()); +fn update_all(body: Vec) { + info!( + "Changing all Users to {:?}", + body.into_iter().map(|u| u.username).collect::>() + ); } #[change(Users)] -fn update(id : u64, body : User) -{ +fn update(id: u64, body: User) { info!("Change User {} to {}", id, body.username); } #[remove_all(Users)] -fn remove_all() -{ +fn remove_all() { info!("Delete all Users"); } #[remove(Users)] -fn remove(id : u64) -{ +fn remove(id: u64) { info!("Delete User {}", id); } #[read_all(Auth)] -fn auth_read_all(auth : AuthStatus<()>) -> AuthSuccess -{ +fn auth_read_all(auth: AuthStatus<()>) -> AuthSuccess { match auth { AuthStatus::Authenticated(data) => Ok(format!("{:?}", data)), _ => Err(Forbidden) } } -const ADDR : &str = "127.0.0.1:18080"; +const ADDR: &str = "127.0.0.1:18080"; #[derive(Clone, Default)] struct Handler; -impl AuthHandler for Handler -{ - fn jwt_secret Option>(&self, _state : &mut State, _decode_data : F) -> Option> - { +impl AuthHandler for Handler { + fn jwt_secret Option>(&self, _state: &mut State, _decode_data: F) -> Option> { None } } -fn main() -{ +fn main() { let encoder = PatternEncoder::new("{d(%Y-%m-%d %H:%M:%S%.3f %Z)} [{l}] {M} - {m}\n"); let config = Config::builder() - .appender( - Appender::builder() - .build("stdout", Box::new( - ConsoleAppender::builder() - .encoder(Box::new(encoder)) - .build() - ))) + .appender(Appender::builder().build( + "stdout", + Box::new(ConsoleAppender::builder().encoder(Box::new(encoder)).build()) + )) .build(Root::builder().appender("stdout").build(LevelFilter::Info)) .unwrap(); log4rs::init_config(config).unwrap(); - + let cors = CorsConfig { origin: Origin::Copy, headers: vec![CONTENT_TYPE], credentials: true, ..Default::default() }; - + let auth = >::from_source(AuthSource::AuthorizationHeader); let logging = RequestLogger::new(log::Level::Info); - let (chain, pipelines) = single_pipeline( - new_pipeline() - .add(auth) - .add(logging) - .add(cors) - .build() - ); + let (chain, pipelines) = single_pipeline(new_pipeline().add(auth).add(logging).add(cors).build()); - gotham::start(ADDR, build_router(chain, pipelines, |route| { - let info = OpenapiInfo { - title: "Users Example".to_owned(), - version: "0.0.1".to_owned(), - urls: vec![format!("http://{}", ADDR)] - }; - route.with_openapi(info, |mut route| { - route.resource::("users"); - route.resource::("auth"); - route.get_openapi("openapi"); - }); - })); + gotham::start( + ADDR, + build_router(chain, pipelines, |route| { + let info = OpenapiInfo { + title: "Users Example".to_owned(), + version: "0.0.1".to_owned(), + urls: vec![format!("http://{}", ADDR)] + }; + route.with_openapi(info, |mut route| { + route.resource::("users"); + route.resource::("auth"); + route.get_openapi("openapi"); + }); + }) + ); println!("Gotham started on {} for testing", ADDR); } - diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..627d9e1 --- /dev/null +++ b/rustfmt.toml @@ -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 diff --git a/src/auth.rs b/src/auth.rs index 0888ac3..e4896e3 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,29 +1,24 @@ use crate::{AuthError, Forbidden, HeaderName}; use cookie::CookieJar; -use futures_util::{future, future::{FutureExt, TryFutureExt}}; +use futures_util::{ + future, + future::{FutureExt, TryFutureExt} +}; use gotham::{ handler::HandlerFuture, - hyper::header::{AUTHORIZATION, HeaderMap}, + hyper::header::{HeaderMap, AUTHORIZATION}, middleware::{Middleware, NewMiddleware}, state::{FromState, State} }; -use jsonwebtoken::{ - errors::ErrorKind, - DecodingKey -}; +use jsonwebtoken::{errors::ErrorKind, DecodingKey}; use serde::de::DeserializeOwned; -use std::{ - marker::PhantomData, - panic::RefUnwindSafe, - pin::Pin -}; +use std::{marker::PhantomData, panic::RefUnwindSafe, pin::Pin}; pub use jsonwebtoken::Validation as AuthValidation; /// The authentication status returned by the auth middleware for each request. #[derive(Debug, StateData)] -pub enum AuthStatus -{ +pub enum AuthStatus { /// The auth status is unknown. Unknown, /// The request has been performed without any kind of authentication. @@ -38,10 +33,9 @@ pub enum AuthStatus impl Clone for AuthStatus where - T : Clone + Send + 'static + T: Clone + Send + 'static { - fn clone(&self) -> Self - { + fn clone(&self) -> Self { match self { Self::Unknown => Self::Unknown, Self::Unauthenticated => Self::Unauthenticated, @@ -52,16 +46,10 @@ where } } -impl Copy for AuthStatus -where - T : Copy + Send + 'static -{ -} +impl Copy for AuthStatus where T: Copy + Send + 'static {} -impl AuthStatus -{ - pub fn ok(self) -> Result - { +impl AuthStatus { + pub fn ok(self) -> Result { match self { Self::Authenticated(data) => Ok(data), _ => Err(Forbidden) @@ -71,8 +59,7 @@ impl AuthStatus /// The source of the authentication token in the request. #[derive(Clone, Debug, StateData)] -pub enum AuthSource -{ +pub enum AuthSource { /// Take the token from a cookie with the given name. Cookie(String), /// Take the token from a header with the given name. @@ -100,36 +87,29 @@ impl AuthHandler for CustomAuthHandler { } ``` */ -pub trait AuthHandler -{ +pub trait AuthHandler { /// Return the SHA256-HMAC secret used to verify the JWT token. - fn jwt_secret Option>(&self, state : &mut State, decode_data : F) -> Option>; + fn jwt_secret Option>(&self, state: &mut State, decode_data: F) -> Option>; } /// An `AuthHandler` returning always the same secret. See `AuthMiddleware` for a usage example. #[derive(Clone, Debug)] -pub struct StaticAuthHandler -{ - secret : Vec +pub struct StaticAuthHandler { + secret: Vec } -impl StaticAuthHandler -{ - pub fn from_vec(secret : Vec) -> Self - { +impl StaticAuthHandler { + pub fn from_vec(secret: Vec) -> Self { Self { secret } } - - pub fn from_array(secret : &[u8]) -> Self - { + + pub fn from_array(secret: &[u8]) -> Self { Self::from_vec(secret.to_vec()) } } -impl AuthHandler for StaticAuthHandler -{ - fn jwt_secret Option>(&self, _state : &mut State, _decode_data : F) -> Option> - { +impl AuthHandler for StaticAuthHandler { + fn jwt_secret Option>(&self, _state: &mut State, _decode_data: F) -> Option> { Some(self.secret.clone()) } } @@ -173,19 +153,18 @@ fn main() { ``` */ #[derive(Debug)] -pub struct AuthMiddleware -{ - source : AuthSource, - validation : AuthValidation, - handler : Handler, - _data : PhantomData +pub struct AuthMiddleware { + source: AuthSource, + validation: AuthValidation, + handler: Handler, + _data: PhantomData } impl Clone for AuthMiddleware -where Handler : Clone +where + Handler: Clone { - fn clone(&self) -> Self - { + fn clone(&self) -> Self { Self { source: self.source.clone(), validation: self.validation.clone(), @@ -197,11 +176,10 @@ where Handler : Clone impl AuthMiddleware where - Data : DeserializeOwned + Send, - Handler : AuthHandler + Default + Data: DeserializeOwned + Send, + Handler: AuthHandler + Default { - pub fn from_source(source : AuthSource) -> Self - { + pub fn from_source(source: AuthSource) -> Self { Self { source, validation: Default::default(), @@ -213,11 +191,10 @@ where impl AuthMiddleware where - Data : DeserializeOwned + Send, - Handler : AuthHandler + Data: DeserializeOwned + Send, + Handler: AuthHandler { - pub fn new(source : AuthSource, validation : AuthValidation, handler : Handler) -> Self - { + pub fn new(source: AuthSource, validation: AuthValidation, handler: Handler) -> Self { Self { source, validation, @@ -225,59 +202,52 @@ where _data: Default::default() } } - - fn auth_status(&self, state : &mut State) -> AuthStatus - { + + fn auth_status(&self, state: &mut State) -> AuthStatus { // extract the provided token, if any let token = match &self.source { - AuthSource::Cookie(name) => { - CookieJar::try_borrow_from(&state) - .and_then(|jar| jar.get(&name)) - .map(|cookie| cookie.value().to_owned()) - }, - AuthSource::Header(name) => { - HeaderMap::try_borrow_from(&state) - .and_then(|map| map.get(name)) - .and_then(|header| header.to_str().ok()) - .map(|value| value.to_owned()) - }, - AuthSource::AuthorizationHeader => { - 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()) - } + AuthSource::Cookie(name) => CookieJar::try_borrow_from(&state) + .and_then(|jar| jar.get(&name)) + .map(|cookie| cookie.value().to_owned()), + AuthSource::Header(name) => HeaderMap::try_borrow_from(&state) + .and_then(|map| map.get(name)) + .and_then(|header| header.to_str().ok()) + .map(|value| value.to_owned()), + AuthSource::AuthorizationHeader => 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 let token = match token { Some(token) => token, None => return AuthStatus::Unauthenticated }; - + // get the secret from the handler, possibly decoding claims ourselves let secret = self.handler.jwt_secret(state, || { let b64 = token.split('.').nth(1)?; let raw = base64::decode_config(b64, base64::URL_SAFE_NO_PAD).ok()?; serde_json::from_slice(&raw).ok()? }); - + // unknown if no secret let secret = match secret { Some(secret) => secret, None => return AuthStatus::Unknown }; - + // 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, Err(e) => match dbg!(e.into_kind()) { ErrorKind::ExpiredSignature => return AuthStatus::Expired, _ => return AuthStatus::Invalid } }; - + // we found a valid token AuthStatus::Authenticated(data) } @@ -285,20 +255,20 @@ where impl Middleware for AuthMiddleware where - Data : DeserializeOwned + Send + 'static, - Handler : AuthHandler + Data: DeserializeOwned + Send + 'static, + Handler: AuthHandler { - fn call(self, mut state : State, chain : Chain) -> Pin> + fn call(self, mut state: State, chain: Chain) -> Pin> where - Chain : FnOnce(State) -> Pin> + Chain: FnOnce(State) -> Pin> { // put the source in our state, required for e.g. openapi state.put(self.source.clone()); - + // put the status in our state let status = self.auth_status(&mut state); state.put(status); - + // call the rest of the chain chain(state).and_then(|(state, res)| future::ok((state, res))).boxed() } @@ -306,45 +276,40 @@ where impl NewMiddleware for AuthMiddleware where - Self : Clone + Middleware + Sync + RefUnwindSafe + Self: Clone + Middleware + Sync + RefUnwindSafe { type Instance = Self; - - fn new_middleware(&self) -> Result - { - let c : Self = self.clone(); + + fn new_middleware(&self) -> Result { + let c: Self = self.clone(); Ok(c) } } #[cfg(test)] -mod test -{ +mod test { use super::*; use cookie::Cookie; use std::fmt::Debug; - + // 256-bit random string - const JWT_SECRET : &'static [u8; 32] = b"Lyzsfnta0cdxyF0T9y6VGxp3jpgoMUuW"; - + const JWT_SECRET: &'static [u8; 32] = b"Lyzsfnta0cdxyF0T9y6VGxp3jpgoMUuW"; + // some known tokens const VALID_TOKEN : &'static str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJtc3JkMCIsInN1YiI6ImdvdGhhbS1yZXN0ZnVsIiwiaWF0IjoxNTc3ODM2ODAwLCJleHAiOjQxMDI0NDQ4MDB9.8h8Ax-nnykqEQ62t7CxmM3ja6NzUQ4L0MLOOzddjLKk"; const EXPIRED_TOKEN : &'static str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJtc3JkMCIsInN1YiI6ImdvdGhhbS1yZXN0ZnVsIiwiaWF0IjoxNTc3ODM2ODAwLCJleHAiOjE1Nzc4MzcxMDB9.eV1snaGLYrJ7qUoMk74OvBY3WUU9M0Je5HTU2xtX1v0"; const INVALID_TOKEN : &'static str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJtc3JkMCIsInN1YiI6ImdvdGhhbS1yZXN0ZnVsIiwiaWF0IjoxNTc3ODM2ODAwLCJleHAiOjQxMDI0NDQ4MDB9"; - + #[derive(Debug, Deserialize, PartialEq)] - struct TestData - { - iss : String, - sub : String, - iat : u64, - exp : u64 + struct TestData { + iss: String, + sub: String, + iat: u64, + exp: u64 } - - impl Default for TestData - { - fn default() -> Self - { + + impl Default for TestData { + fn default() -> Self { Self { iss: "msrd0".to_owned(), sub: "gotham-restful".to_owned(), @@ -353,20 +318,17 @@ mod test } } } - + #[derive(Default)] struct NoneAuthHandler; - impl AuthHandler for NoneAuthHandler - { - fn jwt_secret Option>(&self, _state : &mut State, _decode_data : F) -> Option> - { + impl AuthHandler for NoneAuthHandler { + fn jwt_secret Option>(&self, _state: &mut State, _decode_data: F) -> Option> { None } } - + #[test] - fn test_auth_middleware_none_secret() - { + fn test_auth_middleware_none_secret() { let middleware = >::from_source(AuthSource::AuthorizationHeader); State::with_new(|mut state| { let mut headers = HeaderMap::new(); @@ -375,22 +337,21 @@ mod test middleware.auth_status(&mut state); }); } - + #[derive(Default)] struct TestAssertingHandler; impl AuthHandler for TestAssertingHandler - where T : Debug + Default + PartialEq + where + T: Debug + Default + PartialEq { - fn jwt_secret Option>(&self, _state : &mut State, decode_data : F) -> Option> - { + fn jwt_secret Option>(&self, _state: &mut State, decode_data: F) -> Option> { assert_eq!(decode_data(), Some(T::default())); Some(JWT_SECRET.to_vec()) } } - + #[test] - fn test_auth_middleware_decode_data() - { + fn test_auth_middleware_decode_data() { let middleware = >::from_source(AuthSource::AuthorizationHeader); State::with_new(|mut state| { let mut headers = HeaderMap::new(); @@ -399,16 +360,16 @@ mod test middleware.auth_status(&mut state); }); } - - fn new_middleware(source : AuthSource) -> AuthMiddleware - where T : DeserializeOwned + Send + + fn new_middleware(source: AuthSource) -> AuthMiddleware + where + T: DeserializeOwned + Send { AuthMiddleware::new(source, Default::default(), StaticAuthHandler::from_array(JWT_SECRET)) } - + #[test] - fn test_auth_middleware_no_token() - { + fn test_auth_middleware_no_token() { let middleware = new_middleware::(AuthSource::AuthorizationHeader); State::with_new(|mut state| { let status = middleware.auth_status(&mut state); @@ -418,10 +379,9 @@ mod test }; }); } - + #[test] - fn test_auth_middleware_expired_token() - { + fn test_auth_middleware_expired_token() { let middleware = new_middleware::(AuthSource::AuthorizationHeader); State::with_new(|mut state| { let mut headers = HeaderMap::new(); @@ -434,10 +394,9 @@ mod test }; }); } - + #[test] - fn test_auth_middleware_invalid_token() - { + fn test_auth_middleware_invalid_token() { let middleware = new_middleware::(AuthSource::AuthorizationHeader); State::with_new(|mut state| { let mut headers = HeaderMap::new(); @@ -450,10 +409,9 @@ mod test }; }); } - + #[test] - fn test_auth_middleware_auth_header_token() - { + fn test_auth_middleware_auth_header_token() { let middleware = new_middleware::(AuthSource::AuthorizationHeader); State::with_new(|mut state| { let mut headers = HeaderMap::new(); @@ -466,10 +424,9 @@ mod test }; }) } - + #[test] - fn test_auth_middleware_header_token() - { + fn test_auth_middleware_header_token() { let header_name = "x-znoiprwmvfexju"; let middleware = new_middleware::(AuthSource::Header(HeaderName::from_static(header_name))); State::with_new(|mut state| { @@ -483,10 +440,9 @@ mod test }; }) } - + #[test] - fn test_auth_middleware_cookie_token() - { + fn test_auth_middleware_cookie_token() { let cookie_name = "znoiprwmvfexju"; let middleware = new_middleware::(AuthSource::Cookie(cookie_name.to_owned())); State::with_new(|mut state| { diff --git a/src/cors.rs b/src/cors.rs index 46af65c..c17e28e 100644 --- a/src/cors.rs +++ b/src/cors.rs @@ -4,22 +4,19 @@ use gotham::{ helpers::http::response::create_empty_response, hyper::{ header::{ - ACCESS_CONTROL_ALLOW_CREDENTIALS, ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_METHODS, - ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_MAX_AGE, ACCESS_CONTROL_REQUEST_METHOD, ORIGIN, VARY, - HeaderMap, HeaderName, HeaderValue + HeaderMap, HeaderName, HeaderValue, ACCESS_CONTROL_ALLOW_CREDENTIALS, ACCESS_CONTROL_ALLOW_HEADERS, + ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_MAX_AGE, + ACCESS_CONTROL_REQUEST_METHOD, ORIGIN, VARY }, Body, Method, Response, StatusCode }, middleware::Middleware, pipeline::chain::PipelineHandleChain, router::builder::*, - state::{FromState, State}, + state::{FromState, State} }; use itertools::Itertools; -use std::{ - panic::RefUnwindSafe, - pin::Pin -}; +use std::{panic::RefUnwindSafe, pin::Pin}; /** 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. */ #[derive(Clone, Debug)] -pub enum Origin -{ +pub enum Origin { /// Do not send any `Access-Control-Allow-Origin` headers. None, /// Send `Access-Control-Allow-Origin: *`. Note that browser will not send credentials. @@ -39,19 +35,15 @@ pub enum Origin Copy } -impl Default for Origin -{ - fn default() -> Self - { +impl Default for Origin { + fn default() -> Self { Self::None } } -impl Origin -{ +impl Origin { /// Get the header value for the `Access-Control-Allow-Origin` header. - fn header_value(&self, state : &State) -> Option - { + fn header_value(&self, state: &State) -> Option { match self { Self::None => None, 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 */ #[derive(Clone, Debug, Default, NewMiddleware, StateData)] -pub struct CorsConfig -{ +pub struct CorsConfig { /// The allowed origins. - pub origin : Origin, + pub origin: Origin, /// The allowed headers. - pub headers : Vec, + pub headers: Vec, /// 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. - pub credentials : bool + pub credentials: bool } -impl Middleware for CorsConfig -{ - fn call(self, mut state : State, chain : Chain) -> Pin> +impl Middleware for CorsConfig { + fn call(self, mut state: State, chain: Chain) -> Pin> where - Chain : FnOnce(State) -> Pin> + Chain: FnOnce(State) -> Pin> { state.put(self); chain(state) @@ -161,35 +151,31 @@ For further information on CORS, read https://developer.mozilla.org/en-US/docs/W [`CorsConfig`]: ./struct.CorsConfig.html */ -pub fn handle_cors(state : &State, res : &mut Response) -{ +pub fn handle_cors(state: &State, res: &mut Response) { let config = CorsConfig::try_borrow_from(state); let headers = res.headers_mut(); - + // 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); } // 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())); headers.insert(VARY, vary.as_deref().unwrap_or("Origin").parse().unwrap()); } // 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()); } } /// Add CORS routing for your path. This is required for handling preflight requests. -/// +/// /// Example: -/// +/// /// ```rust,no_run /// # use gotham::{hyper::{Body, Method, Response}, router::builder::*}; /// # use gotham_restful::*; @@ -206,16 +192,15 @@ pub fn handle_cors(state : &State, res : &mut Response) /// ``` pub trait CorsRoute where - C : PipelineHandleChain

+ Copy + Send + Sync + 'static, - P : RefUnwindSafe + Send + Sync + 'static + C: PipelineHandleChain

+ Copy + Send + Sync + 'static, + P: RefUnwindSafe + Send + Sync + 'static { /// Handle a preflight request on `path` for `method`. To configure the behaviour, use /// [`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) -{ +fn cors_preflight_handler(state: State) -> (State, Response) { let config = CorsConfig::try_borrow_from(&state); // prepare the response @@ -223,43 +208,40 @@ fn cors_preflight_handler(state : State) -> (State, Response) let headers = res.headers_mut(); // 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); // if we allow any headers, put them in - if let Some(hdrs) = config.map(|cfg| &cfg.headers) - { - if hdrs.len() > 0 - { + if let Some(hdrs) = config.map(|cfg| &cfg.headers) { + if hdrs.len() > 0 { // 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()); } } // 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()); } // make sure the browser knows that this request was based on the method headers.insert(VARY, "Access-Control-Request-Method".parse().unwrap()); - + handle_cors(&state, &mut res); (state, res) } impl CorsRoute for D where - D : DrawRoutes, - C : PipelineHandleChain

+ Copy + Send + Sync + 'static, - P : RefUnwindSafe + Send + Sync + 'static + D: DrawRoutes, + C: PipelineHandleChain

+ Copy + 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); - self.options(path) - .extend_route_matcher(matcher) - .to(cors_preflight_handler); + self.options(path).extend_route_matcher(matcher).to(cors_preflight_handler); } } diff --git a/src/lib.rs b/src/lib.rs index 8a9ea87..ab08c98 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -263,7 +263,7 @@ type Repo = gotham_middleware_diesel::Repo; fn main() { let repo = Repo::new(&env::var("DATABASE_URL").unwrap()); let diesel = DieselMiddleware::new(repo); - + let (chain, pipelines) = single_pipeline(new_pipeline().add(diesel).build()); gotham::start("127.0.0.1:8080", build_router(chain, pipelines, |route| { route.resource::("foo"); @@ -347,7 +347,7 @@ struct Foo; # Examples -There is a lack of good examples, but there is currently a collection of code in the [example] +There is a lack of good examples, but there is currently a collection of code in the [example] directory, that might help you. Any help writing more examples is highly appreciated. # License @@ -370,9 +370,12 @@ Licensed under your option of: // weird proc macro issue extern crate self as gotham_restful; -#[macro_use] extern crate gotham_derive; -#[macro_use] extern crate log; -#[macro_use] extern crate serde; +#[macro_use] +extern crate gotham_derive; +#[macro_use] +extern crate log; +#[macro_use] +extern crate serde; #[doc(no_inline)] pub use gotham; @@ -388,15 +391,14 @@ pub use gotham_restful_derive::*; /// Not public API #[doc(hidden)] -pub mod export -{ +pub mod export { pub use futures_util::future::FutureExt; - + pub use serde_json; - + #[cfg(feature = "database")] pub use gotham_middleware_diesel::Repo; - + #[cfg(feature = "openapi")] pub use indexmap::IndexMap; #[cfg(feature = "openapi")] @@ -406,24 +408,12 @@ pub mod export #[cfg(feature = "auth")] mod auth; #[cfg(feature = "auth")] -pub use auth::{ - AuthHandler, - AuthMiddleware, - AuthSource, - AuthStatus, - AuthValidation, - StaticAuthHandler -}; +pub use auth::{AuthHandler, AuthMiddleware, AuthSource, AuthStatus, AuthValidation, StaticAuthHandler}; #[cfg(feature = "cors")] mod cors; #[cfg(feature = "cors")] -pub use cors::{ - handle_cors, - CorsConfig, - CorsRoute, - Origin -}; +pub use cors::{handle_cors, CorsConfig, CorsRoute, Origin}; pub mod matcher; @@ -438,16 +428,8 @@ pub use openapi::{ mod resource; pub use resource::{ - Resource, - ResourceMethod, - ResourceReadAll, - ResourceRead, - ResourceSearch, - ResourceCreate, - ResourceChangeAll, - ResourceChange, - ResourceRemoveAll, - ResourceRemove + Resource, ResourceChange, ResourceChangeAll, ResourceCreate, ResourceMethod, ResourceRead, ResourceReadAll, + ResourceRemove, ResourceRemoveAll, ResourceSearch }; mod response; @@ -455,22 +437,14 @@ pub use response::Response; mod result; pub use result::{ - AuthError, - AuthError::Forbidden, - AuthErrorOrOther, - AuthResult, - AuthSuccess, - IntoResponseError, - NoContent, - Raw, - ResourceResult, - Success + AuthError, AuthError::Forbidden, AuthErrorOrOther, AuthResult, AuthSuccess, IntoResponseError, NoContent, Raw, + ResourceResult, Success }; mod routing; -pub use routing::{DrawResources, DrawResourceRoutes}; #[cfg(feature = "openapi")] pub use routing::WithOpenapi; +pub use routing::{DrawResourceRoutes, DrawResources}; mod types; pub use types::*; diff --git a/src/matcher/access_control_request_method.rs b/src/matcher/access_control_request_method.rs index a5e03f2..7668e37 100644 --- a/src/matcher/access_control_request_method.rs +++ b/src/matcher/access_control_request_method.rs @@ -1,13 +1,16 @@ 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}, state::{FromState, State} }; /// A route matcher that checks whether the value of the `Access-Control-Request-Method` header matches the defined value. -/// +/// /// Usage: -/// +/// /// ```rust /// # use gotham::{helpers::http::response::create_empty_response, /// # hyper::{header::ACCESS_CONTROL_ALLOW_METHODS, Method, StatusCode}, @@ -15,44 +18,38 @@ use gotham::{ /// # }; /// # use gotham_restful::matcher::AccessControlRequestMethodMatcher; /// let matcher = AccessControlRequestMethodMatcher::new(Method::PUT); -/// +/// /// # build_simple_router(|route| { /// // use the matcher for your request -/// route.options("/foo") -/// .extend_route_matcher(matcher) -/// .to(|state| { -/// // we know that this is a CORS preflight for a PUT request -/// let mut res = create_empty_response(&state, StatusCode::NO_CONTENT); -/// res.headers_mut().insert(ACCESS_CONTROL_ALLOW_METHODS, "PUT".parse().unwrap()); -/// (state, res) +/// route.options("/foo").extend_route_matcher(matcher).to(|state| { +/// // we know that this is a CORS preflight for a PUT request +/// let mut res = create_empty_response(&state, StatusCode::NO_CONTENT); +/// res.headers_mut().insert(ACCESS_CONTROL_ALLOW_METHODS, "PUT".parse().unwrap()); +/// (state, res) /// }); /// # }); /// ``` #[derive(Clone, Debug)] -pub struct AccessControlRequestMethodMatcher -{ - method : Method +pub struct AccessControlRequestMethodMatcher { + method: Method } -impl AccessControlRequestMethodMatcher -{ +impl AccessControlRequestMethodMatcher { /// 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, /// 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. - pub fn new(method : Method) -> Self - { + pub fn new(method: Method) -> Self { Self { method } } } -impl RouteMatcher for AccessControlRequestMethodMatcher -{ - fn is_match(&self, state : &State) -> Result<(), RouteNonMatch> - { +impl RouteMatcher for AccessControlRequestMethodMatcher { + fn is_match(&self, state: &State) -> Result<(), RouteNonMatch> { // according to the fetch specification, methods should be normalized by byte-uppercase // 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(|str| str.to_ascii_uppercase().parse::().ok()) { @@ -62,19 +59,17 @@ impl RouteMatcher for AccessControlRequestMethodMatcher } } - #[cfg(test)] -mod test -{ +mod test { use super::*; - fn with_state(accept : Option<&str>, block : F) - where F : FnOnce(&mut State) -> () + fn with_state(accept: Option<&str>, block: F) + where + F: FnOnce(&mut State) -> () { State::with_new(|state| { let mut headers = HeaderMap::new(); - if let Some(acc) = accept - { + if let Some(acc) = accept { headers.insert(ACCESS_CONTROL_REQUEST_METHOD, acc.parse().unwrap()); } state.put(headers); @@ -83,23 +78,20 @@ mod test } #[test] - fn no_acrm_header() - { + fn no_acrm_header() { let matcher = AccessControlRequestMethodMatcher::new(Method::PUT); with_state(None, |state| assert!(matcher.is_match(&state).is_err())); } #[test] - fn correct_acrm_header() - { + fn correct_acrm_header() { 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())); } #[test] - fn incorrect_acrm_header() - { + fn incorrect_acrm_header() { let matcher = AccessControlRequestMethodMatcher::new(Method::PUT); with_state(Some("DELETE"), |state| assert!(matcher.is_match(&state).is_err())); } diff --git a/src/matcher/mod.rs b/src/matcher/mod.rs index cc7e734..153bdfe 100644 --- a/src/matcher/mod.rs +++ b/src/matcher/mod.rs @@ -2,4 +2,3 @@ mod access_control_request_method; #[cfg(feature = "cors")] pub use access_control_request_method::AccessControlRequestMethodMatcher; - diff --git a/src/openapi/builder.rs b/src/openapi/builder.rs index bf81caa..f582015 100644 --- a/src/openapi/builder.rs +++ b/src/openapi/builder.rs @@ -1,29 +1,26 @@ -use crate::{OpenapiType, OpenapiSchema}; +use crate::{OpenapiSchema, OpenapiType}; use indexmap::IndexMap; use openapiv3::{ - Components, OpenAPI, PathItem, ReferenceOr, ReferenceOr::Item, ReferenceOr::Reference, Schema, - Server + Components, OpenAPI, PathItem, ReferenceOr, + ReferenceOr::{Item, Reference}, + Schema, Server }; use std::sync::{Arc, RwLock}; #[derive(Clone, Debug)] -pub struct OpenapiInfo -{ - pub title : String, - pub version : String, - pub urls : Vec +pub struct OpenapiInfo { + pub title: String, + pub version: String, + pub urls: Vec } #[derive(Clone, Debug)] -pub struct OpenapiBuilder -{ - pub openapi : Arc> +pub struct OpenapiBuilder { + pub openapi: Arc> } -impl OpenapiBuilder -{ - pub fn new(info : OpenapiInfo) -> Self - { +impl OpenapiBuilder { + pub fn new(info: OpenapiInfo) -> Self { Self { openapi: Arc::new(RwLock::new(OpenAPI { openapi: "3.0.2".to_string(), @@ -32,18 +29,22 @@ impl OpenapiBuilder version: info.version, ..Default::default() }, - servers: info.urls.into_iter() - .map(|url| Server { url, ..Default::default() }) + servers: info + .urls + .into_iter() + .map(|url| Server { + url, + ..Default::default() + }) .collect(), ..Default::default() })) } } - + /// 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 - 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(); match openapi.paths.swap_remove(path) { Some(Item(item)) => item, @@ -51,16 +52,14 @@ impl OpenapiBuilder } } - pub fn add_path(&mut self, path : Path, item : PathItem) - { + pub fn add_path(&mut self, path: Path, item: PathItem) { let mut openapi = self.openapi.write().unwrap(); 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); - + let mut openapi = self.openapi.write().unwrap(); match &mut openapi.components { Some(comp) => { @@ -74,25 +73,23 @@ impl OpenapiBuilder }; } - fn add_schema_dependencies(&mut self, dependencies : &mut IndexMap) - { - let keys : Vec = dependencies.keys().map(|k| k.to_string()).collect(); - for dep in keys - { + fn add_schema_dependencies(&mut self, dependencies: &mut IndexMap) { + let keys: Vec = dependencies.keys().map(|k| k.to_string()).collect(); + for dep in keys { 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); } } } - - pub fn add_schema(&mut self) -> ReferenceOr - { + + pub fn add_schema(&mut self) -> ReferenceOr { let mut schema = T::schema(); match schema.name.clone() { Some(name) => { - let reference = Reference { reference: format!("#/components/schemas/{}", name) }; + let reference = Reference { + reference: format!("#/components/schemas/{}", name) + }; self.add_schema_impl(name, schema); reference }, @@ -104,59 +101,57 @@ impl OpenapiBuilder } } - #[cfg(test)] #[allow(dead_code)] -mod test -{ +mod test { use super::*; - + #[derive(OpenapiType)] - struct Message - { - msg : String + struct Message { + msg: String } - + #[derive(OpenapiType)] - struct Messages - { - msgs : Vec + struct Messages { + msgs: Vec } - - fn info() -> OpenapiInfo - { + + fn info() -> OpenapiInfo { OpenapiInfo { title: "TEST CASE".to_owned(), version: "1.2.3".to_owned(), urls: vec!["http://localhost:1234".to_owned(), "https://example.org".to_owned()] } } - - fn openapi(builder : OpenapiBuilder) -> OpenAPI - { + + fn openapi(builder: OpenapiBuilder) -> OpenAPI { Arc::try_unwrap(builder.openapi).unwrap().into_inner().unwrap() } - + #[test] - fn new_builder() - { + fn new_builder() { let info = info(); let builder = OpenapiBuilder::new(info.clone()); let openapi = openapi(builder); - + assert_eq!(info.title, openapi.info.title); assert_eq!(info.version, openapi.info.version); assert_eq!(info.urls.len(), openapi.servers.len()); } - + #[test] - fn add_schema() - { + fn add_schema() { let mut builder = OpenapiBuilder::new(info()); builder.add_schema::>(); let openapi = openapi(builder); - - assert_eq!(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())); + + assert_eq!( + 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()) + ); } } diff --git a/src/openapi/handler.rs b/src/openapi/handler.rs index 2054b0d..359f1f3 100644 --- a/src/openapi/handler.rs +++ b/src/openapi/handler.rs @@ -15,40 +15,34 @@ use std::{ }; #[derive(Clone)] -pub struct OpenapiHandler -{ - openapi : Arc> +pub struct OpenapiHandler { + openapi: Arc> } -impl OpenapiHandler -{ - pub fn new(openapi : Arc>) -> Self - { +impl OpenapiHandler { + pub fn new(openapi: Arc>) -> Self { Self { openapi } } } -impl NewHandler for OpenapiHandler -{ +impl NewHandler for OpenapiHandler { type Instance = Self; - - fn new_handler(&self) -> Result - { + + fn new_handler(&self) -> Result { Ok(self.clone()) } } #[cfg(feature = "auth")] -fn get_security(state : &mut State) -> IndexMap> -{ +fn get_security(state: &mut State) -> IndexMap> { use crate::AuthSource; use gotham::state::FromState; - + let source = match AuthSource::try_borrow_from(state) { Some(source) => source, None => return Default::default() }; - + let security_scheme = match source { AuthSource::Cookie(name) => SecurityScheme::APIKey { location: APIKeyLocation::Cookie, @@ -63,38 +57,35 @@ fn get_security(state : &mut State) -> IndexMap> = Default::default(); + + let mut security_schemes: IndexMap> = Default::default(); security_schemes.insert(SECURITY_NAME.to_owned(), ReferenceOr::Item(security_scheme)); - + security_schemes } #[cfg(not(feature = "auth"))] -fn get_security(state : &mut State) -> (Vec, IndexMap>) -{ +fn get_security(state: &mut State) -> (Vec, IndexMap>) { Default::default() } -impl Handler for OpenapiHandler -{ - fn handle(self, mut state : State) -> Pin> - { +impl Handler for OpenapiHandler { + fn handle(self, mut state: State) -> Pin> { let openapi = match self.openapi.read() { Ok(openapi) => openapi, Err(e) => { error!("Unable to acquire read lock for the OpenAPI specification: {}", e); let res = create_response(&state, crate::StatusCode::INTERNAL_SERVER_ERROR, TEXT_PLAIN, ""); - return future::ok((state, res)).boxed() + return future::ok((state, res)).boxed(); } }; - + let mut openapi = openapi.clone(); let security_schemes = get_security(&mut state); let mut components = openapi.components.unwrap_or_default(); components.security_schemes = security_schemes; openapi.components = Some(components); - + match serde_json::to_string(&openapi) { Ok(body) => { let res = create_response(&state, crate::StatusCode::OK, APPLICATION_JSON, body); diff --git a/src/openapi/mod.rs b/src/openapi/mod.rs index 141ea22..500d190 100644 --- a/src/openapi/mod.rs +++ b/src/openapi/mod.rs @@ -1,5 +1,4 @@ - -const SECURITY_NAME : &str = "authToken"; +const SECURITY_NAME: &str = "authToken"; pub mod builder; pub mod handler; diff --git a/src/openapi/operation.rs b/src/openapi/operation.rs index fc06e43..a70e258 100644 --- a/src/openapi/operation.rs +++ b/src/openapi/operation.rs @@ -1,32 +1,21 @@ -use crate::{ - resource::*, - result::*, - OpenapiSchema, - RequestBody -}; use super::SECURITY_NAME; +use crate::{resource::*, result::*, OpenapiSchema, RequestBody}; use indexmap::IndexMap; use mime::Mime; use openapiv3::{ - MediaType, Operation, Parameter, ParameterData, ParameterSchemaOrContent, ReferenceOr, - ReferenceOr::Item, RequestBody as OARequestBody, Response, Responses, Schema, SchemaKind, - StatusCode, Type + MediaType, Operation, Parameter, ParameterData, ParameterSchemaOrContent, ReferenceOr, ReferenceOr::Item, + RequestBody as OARequestBody, Response, Responses, Schema, SchemaKind, StatusCode, Type }; - #[derive(Default)] -struct OperationParams<'a> -{ - path_params : Vec<(&'a str, ReferenceOr)>, - query_params : Option +struct OperationParams<'a> { + path_params: Vec<(&'a str, ReferenceOr)>, + query_params: Option } -impl<'a> OperationParams<'a> -{ - fn add_path_params(&self, params : &mut Vec>) - { - for param in &self.path_params - { +impl<'a> OperationParams<'a> { + fn add_path_params(&self, params: &mut Vec>) { + for param in &self.path_params { params.push(Item(Parameter::Path { parameter_data: ParameterData { name: (*param).0.to_string(), @@ -37,13 +26,12 @@ impl<'a> OperationParams<'a> example: None, examples: IndexMap::new() }, - style: Default::default(), + style: Default::default() })); } } - - fn add_query_params(self, params : &mut Vec>) - { + + fn add_query_params(self, params: &mut Vec>) { let query_params = match self.query_params { Some(qp) => qp.schema, None => return @@ -52,8 +40,7 @@ impl<'a> OperationParams<'a> SchemaKind::Type(Type::Object(ty)) => ty, _ => 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); params.push(Item(Parameter::Query { parameter_data: ParameterData { @@ -71,32 +58,28 @@ impl<'a> OperationParams<'a> })) } } - - fn into_params(self) -> Vec> - { - let mut params : Vec> = Vec::new(); + + fn into_params(self) -> Vec> { + let mut params: Vec> = Vec::new(); self.add_path_params(&mut params); self.add_query_params(&mut params); params } } -pub struct OperationDescription<'a> -{ - operation_id : Option, - default_status : crate::StatusCode, - accepted_types : Option>, - schema : ReferenceOr, - params : OperationParams<'a>, - body_schema : Option>, - supported_types : Option>, - requires_auth : bool +pub struct OperationDescription<'a> { + operation_id: Option, + default_status: crate::StatusCode, + accepted_types: Option>, + schema: ReferenceOr, + params: OperationParams<'a>, + body_schema: Option>, + supported_types: Option>, + requires_auth: bool } -impl<'a> OperationDescription<'a> -{ - pub fn new(schema : ReferenceOr) -> Self - { +impl<'a> OperationDescription<'a> { + pub fn new(schema: ReferenceOr) -> Self { Self { operation_id: Handler::operation_id(), default_status: Handler::Res::default_status(), @@ -108,32 +91,26 @@ impl<'a> OperationDescription<'a> requires_auth: Handler::wants_auth() } } - - pub fn add_path_param(mut self, name : &'a str, schema : ReferenceOr) -> Self - { + + pub fn add_path_param(mut self, name: &'a str, schema: ReferenceOr) -> Self { self.params.path_params.push((name, schema)); 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 } - - pub fn with_body(mut self, schema : ReferenceOr) -> Self - { + + pub fn with_body(mut self, schema: ReferenceOr) -> Self { self.body_schema = Some(schema); self.supported_types = Body::supported_types(); self } - - - fn schema_to_content(types : Vec, schema : ReferenceOr) -> IndexMap - { - let mut content : IndexMap = IndexMap::new(); - for ty in types - { + + fn schema_to_content(types: Vec, schema: ReferenceOr) -> IndexMap { + let mut content: IndexMap = IndexMap::new(); + for ty in types { content.insert(ty.to_string(), MediaType { schema: Some(schema.clone()), ..Default::default() @@ -141,36 +118,47 @@ impl<'a> OperationDescription<'a> } 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 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 mut responses : IndexMap> = IndexMap::new(); - responses.insert(StatusCode::Code(default_status.as_u16()), Item(Response { - 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 { - description: None, - content: Self::schema_to_content(supported_types.or_all_types(), schema), - required: true - })); - + + let mut responses: IndexMap> = IndexMap::new(); + responses.insert( + StatusCode::Code(default_status.as_u16()), + Item(Response { + 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 { + description: None, + content: Self::schema_to_content(supported_types.or_all_types(), schema), + required: true + }) + }); + let mut security = Vec::new(); - if requires_auth - { + if requires_auth { let mut sec = IndexMap::new(); sec.insert(SECURITY_NAME.to_owned(), Vec::new()); security.push(sec); } - + Operation { tags: Vec::new(), operation_id, @@ -187,25 +175,21 @@ impl<'a> OperationDescription<'a> } } - #[cfg(test)] -mod test -{ - use crate::{OpenapiType, ResourceResult}; +mod test { use super::*; - + use crate::{OpenapiType, ResourceResult}; + #[test] - fn no_content_schema_to_content() - { + fn no_content_schema_to_content() { let types = NoContent::accepted_types(); let schema = ::schema(); let content = OperationDescription::schema_to_content(types.or_all_types(), Item(schema.into_schema())); assert!(content.is_empty()); } - + #[test] - fn raw_schema_to_content() - { + fn raw_schema_to_content() { let types = Raw::<&str>::accepted_types(); let schema = as OpenapiType>::schema(); let content = OperationDescription::schema_to_content(types.or_all_types(), Item(schema.into_schema())); diff --git a/src/openapi/router.rs b/src/openapi/router.rs index 6dd7a13..19ce1a6 100644 --- a/src/openapi/router.rs +++ b/src/openapi/router.rs @@ -1,40 +1,30 @@ -use crate::{ - resource::*, - routing::*, - OpenapiType, -}; use super::{builder::OpenapiBuilder, handler::OpenapiHandler, operation::OperationDescription}; -use gotham::{ - pipeline::chain::PipelineHandleChain, - router::builder::* -}; +use crate::{resource::*, routing::*, OpenapiType}; +use gotham::{pipeline::chain::PipelineHandleChain, router::builder::*}; use std::panic::RefUnwindSafe; /// This trait adds the `get_openapi` method to an OpenAPI-aware router. -pub trait GetOpenapi -{ - fn get_openapi(&mut self, path : &str); +pub trait GetOpenapi { + fn get_openapi(&mut self, path: &str); } #[derive(Debug)] -pub struct OpenapiRouter<'a, D> -{ - pub(crate) router : &'a mut D, - pub(crate) scope : Option<&'a str>, - pub(crate) openapi_builder : &'a mut OpenapiBuilder +pub struct OpenapiRouter<'a, D> { + pub(crate) router: &'a mut D, + pub(crate) scope: Option<&'a str>, + pub(crate) openapi_builder: &'a mut OpenapiBuilder } macro_rules! implOpenapiRouter { ($implType:ident) => { - impl<'a, 'b, C, P> OpenapiRouter<'a, $implType<'b, C, P>> where - C : PipelineHandleChain

+ Copy + Send + Sync + 'static, - P : RefUnwindSafe + Send + Sync + 'static + C: PipelineHandleChain

+ Copy + Send + Sync + 'static, + P: RefUnwindSafe + Send + Sync + 'static { - pub fn scope(&mut self, path : &str, callback : F) + pub fn scope(&mut self, path: &str, callback: F) where - F : FnOnce(&mut OpenapiRouter<'_, ScopeBuilder<'_, C, P>>) + F: FnOnce(&mut OpenapiRouter<'_, ScopeBuilder<'_, C, P>>) { let mut openapi_builder = self.openapi_builder.clone(); let new_scope = self.scope.map(|scope| format!("{}/{}", scope, path).replace("//", "/")); @@ -48,107 +38,120 @@ macro_rules! implOpenapiRouter { }); } } - + impl<'a, 'b, C, P> GetOpenapi for OpenapiRouter<'a, $implType<'b, C, P>> where - C : PipelineHandleChain

+ Copy + Send + Sync + 'static, - P : RefUnwindSafe + Send + Sync + 'static + C: PipelineHandleChain

+ Copy + Send + Sync + 'static, + P: RefUnwindSafe + Send + Sync + 'static { - fn get_openapi(&mut self, path : &str) - { - self.router.get(path).to_new_handler(OpenapiHandler::new(self.openapi_builder.openapi.clone())); + fn get_openapi(&mut self, path: &str) { + self.router + .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>> where - C : PipelineHandleChain

+ Copy + Send + Sync + 'static, - P : RefUnwindSafe + Send + Sync + 'static + C: PipelineHandleChain

+ Copy + Send + Sync + 'static, + P: RefUnwindSafe + Send + Sync + 'static { - fn resource(&mut self, path : &str) - { + fn resource(&mut self, path: &str) { R::setup((self, path)); } } impl<'a, 'b, C, P> DrawResourceRoutes for (&mut OpenapiRouter<'a, $implType<'b, C, P>>, &str) where - C : PipelineHandleChain

+ Copy + Send + Sync + 'static, - P : RefUnwindSafe + Send + Sync + 'static + C: PipelineHandleChain

+ Copy + Send + Sync + 'static, + P: RefUnwindSafe + Send + Sync + 'static { - fn read_all(&mut self) - { + fn read_all(&mut self) { let schema = (self.0).openapi_builder.add_schema::(); - + let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1); let mut item = (self.0).openapi_builder.remove_path(&path); item.get = Some(OperationDescription::new::(schema).into_operation()); (self.0).openapi_builder.add_path(path, item); - + (&mut *(self.0).router, self.1).read_all::() } - - fn read(&mut self) - { + + fn read(&mut self) { let schema = (self.0).openapi_builder.add_schema::(); let id_schema = (self.0).openapi_builder.add_schema::(); let path = format!("{}/{}/{{id}}", self.0.scope.unwrap_or_default(), self.1); let mut item = (self.0).openapi_builder.remove_path(&path); - item.get = Some(OperationDescription::new::(schema).add_path_param("id", id_schema).into_operation()); + item.get = Some( + OperationDescription::new::(schema) + .add_path_param("id", id_schema) + .into_operation() + ); (self.0).openapi_builder.add_path(path, item); - + (&mut *(self.0).router, self.1).read::() } - - fn search(&mut self) - { + + fn search(&mut self) { let schema = (self.0).openapi_builder.add_schema::(); - + let path = format!("{}/{}/search", self.0.scope.unwrap_or_default(), self.1); let mut item = (self.0).openapi_builder.remove_path(&path); - item.get = Some(OperationDescription::new::(schema).with_query_params(Handler::Query::schema()).into_operation()); + item.get = Some( + OperationDescription::new::(schema) + .with_query_params(Handler::Query::schema()) + .into_operation() + ); (self.0).openapi_builder.add_path(path, item); - + (&mut *(self.0).router, self.1).search::() } - - fn create(&mut self) + + fn create(&mut self) where - Handler::Res : 'static, - Handler::Body : 'static + Handler::Res: 'static, + Handler::Body: 'static { let schema = (self.0).openapi_builder.add_schema::(); let body_schema = (self.0).openapi_builder.add_schema::(); let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1); let mut item = (self.0).openapi_builder.remove_path(&path); - item.post = Some(OperationDescription::new::(schema).with_body::(body_schema).into_operation()); + item.post = Some( + OperationDescription::new::(schema) + .with_body::(body_schema) + .into_operation() + ); (self.0).openapi_builder.add_path(path, item); - + (&mut *(self.0).router, self.1).create::() } - - fn change_all(&mut self) + + fn change_all(&mut self) where - Handler::Res : 'static, - Handler::Body : 'static + Handler::Res: 'static, + Handler::Body: 'static { let schema = (self.0).openapi_builder.add_schema::(); let body_schema = (self.0).openapi_builder.add_schema::(); let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1); let mut item = (self.0).openapi_builder.remove_path(&path); - item.put = Some(OperationDescription::new::(schema).with_body::(body_schema).into_operation()); + item.put = Some( + OperationDescription::new::(schema) + .with_body::(body_schema) + .into_operation() + ); (self.0).openapi_builder.add_path(path, item); - + (&mut *(self.0).router, self.1).change_all::() } - - fn change(&mut self) + + fn change(&mut self) where - Handler::Res : 'static, - Handler::Body : 'static + Handler::Res: 'static, + Handler::Body: 'static { let schema = (self.0).openapi_builder.add_schema::(); let id_schema = (self.0).openapi_builder.add_schema::(); @@ -156,39 +159,45 @@ macro_rules! implOpenapiRouter { let path = format!("{}/{}/{{id}}", self.0.scope.unwrap_or_default(), self.1); let mut item = (self.0).openapi_builder.remove_path(&path); - item.put = Some(OperationDescription::new::(schema).add_path_param("id", id_schema).with_body::(body_schema).into_operation()); + item.put = Some( + OperationDescription::new::(schema) + .add_path_param("id", id_schema) + .with_body::(body_schema) + .into_operation() + ); (self.0).openapi_builder.add_path(path, item); - + (&mut *(self.0).router, self.1).change::() } - - fn remove_all(&mut self) - { + + fn remove_all(&mut self) { let schema = (self.0).openapi_builder.add_schema::(); let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1); let mut item = (self.0).openapi_builder.remove_path(&path); item.delete = Some(OperationDescription::new::(schema).into_operation()); (self.0).openapi_builder.add_path(path, item); - + (&mut *(self.0).router, self.1).remove_all::() } - - fn remove(&mut self) - { + + fn remove(&mut self) { let schema = (self.0).openapi_builder.add_schema::(); let id_schema = (self.0).openapi_builder.add_schema::(); let path = format!("{}/{}/{{id}}", self.0.scope.unwrap_or_default(), self.1); let mut item = (self.0).openapi_builder.remove_path(&path); - item.delete = Some(OperationDescription::new::(schema).add_path_param("id", id_schema).into_operation()); + item.delete = Some( + OperationDescription::new::(schema) + .add_path_param("id", id_schema) + .into_operation() + ); (self.0).openapi_builder.add_path(path, item); - + (&mut *(self.0).router, self.1).remove::() } } - - } + }; } implOpenapiRouter!(RouterBuilder); diff --git a/src/openapi/types.rs b/src/openapi/types.rs index c7ff5e4..e506d7a 100644 --- a/src/openapi/types.rs +++ b/src/openapi/types.rs @@ -1,18 +1,17 @@ #[cfg(feature = "chrono")] -use chrono::{ - Date, DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, Utc -}; +use chrono::{Date, DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, Utc}; use indexmap::IndexMap; use openapiv3::{ - AdditionalProperties, ArrayType, IntegerType, NumberFormat, NumberType, ObjectType, ReferenceOr::Item, - ReferenceOr::Reference, Schema, SchemaData, SchemaKind, StringType, Type, VariantOrUnknownOrEmpty + AdditionalProperties, ArrayType, IntegerType, NumberFormat, NumberType, ObjectType, + ReferenceOr::{Item, Reference}, + Schema, SchemaData, SchemaKind, StringType, Type, VariantOrUnknownOrEmpty }; -#[cfg(feature = "uuid")] -use uuid::Uuid; use std::{ collections::{BTreeSet, HashMap, HashSet}, 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 @@ -22,26 +21,23 @@ for your type, simply derive from [`OpenapiType`]. [`OpenapiType`]: trait.OpenapiType.html */ #[derive(Debug, Clone, PartialEq)] -pub struct OpenapiSchema -{ +pub struct OpenapiSchema { /// The name of this schema. If it is None, the schema will be inlined. - pub name : Option, + pub name: Option, /// 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 /// an optional parameter. - pub nullable : bool, + pub nullable: bool, /// 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 /// along with this schema. - pub dependencies : IndexMap + pub dependencies: IndexMap } -impl OpenapiSchema -{ +impl OpenapiSchema { /// Create a new schema that has no name. - pub fn new(schema : SchemaKind) -> Self - { + pub fn new(schema: SchemaKind) -> Self { Self { name: None, nullable: false, @@ -49,10 +45,9 @@ impl OpenapiSchema dependencies: IndexMap::new() } } - + /// 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_data: SchemaData { nullable: self.nullable, @@ -80,15 +75,12 @@ struct MyResponse { [`OpenapiSchema`]: struct.OpenapiSchema.html */ -pub trait OpenapiType -{ +pub trait OpenapiType { fn schema() -> OpenapiSchema; } -impl OpenapiType for () -{ - fn schema() -> OpenapiSchema - { +impl OpenapiType for () { + fn schema() -> OpenapiSchema { OpenapiSchema::new(SchemaKind::Type(Type::Object(ObjectType { additional_properties: Some(AdditionalProperties::Any(false)), ..Default::default() @@ -96,11 +88,9 @@ impl OpenapiType for () } } -impl OpenapiType for bool -{ - fn schema() -> OpenapiSchema - { - OpenapiSchema::new(SchemaKind::Type(Type::Boolean{})) +impl OpenapiType for bool { + fn schema() -> OpenapiSchema { + OpenapiSchema::new(SchemaKind::Type(Type::Boolean {})) } } @@ -114,7 +104,7 @@ macro_rules! int_types { } } )*}; - + (unsigned $($int_ty:ty),*) => {$( impl OpenapiType for $int_ty { @@ -127,7 +117,7 @@ macro_rules! int_types { } } )*}; - + (bits = $bits:expr, $($int_ty:ty),*) => {$( impl OpenapiType for $int_ty { @@ -140,7 +130,7 @@ macro_rules! int_types { } } )*}; - + (unsigned bits = $bits:expr, $($int_ty:ty),*) => {$( impl OpenapiType for $int_ty { @@ -203,7 +193,7 @@ macro_rules! str_types { fn schema() -> OpenapiSchema { use openapiv3::StringFormat; - + OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { format: VariantOrUnknownOrEmpty::Item(StringFormat::$format), ..Default::default() @@ -211,7 +201,7 @@ macro_rules! str_types { } } )*}; - + (format_str = $format:expr, $($str_ty:ty),*) => {$( impl OpenapiType for $str_ty { @@ -231,26 +221,32 @@ str_types!(String, &str); #[cfg(feature = "chrono")] str_types!(format = Date, Date, Date, Date, NaiveDate); #[cfg(feature = "chrono")] -str_types!(format = DateTime, DateTime, DateTime, DateTime, NaiveDateTime); +str_types!( + format = DateTime, + DateTime, + DateTime, + DateTime, + NaiveDateTime +); #[cfg(feature = "uuid")] str_types!(format_str = "uuid", Uuid); -impl OpenapiType for Option -{ - fn schema() -> OpenapiSchema - { +impl OpenapiType for Option { + fn schema() -> OpenapiSchema { let schema = T::schema(); let mut dependencies = schema.dependencies.clone(); let schema = match schema.name.clone() { Some(name) => { - let reference = Reference { reference: format!("#/components/schemas/{}", name) }; + let reference = Reference { + reference: format!("#/components/schemas/{}", name) + }; dependencies.insert(name, schema); SchemaKind::AllOf { all_of: vec![reference] } }, None => schema.schema }; - + OpenapiSchema { nullable: true, name: None, @@ -260,22 +256,22 @@ impl OpenapiType for Option } } -impl OpenapiType for Vec -{ - fn schema() -> OpenapiSchema - { +impl OpenapiType for Vec { + fn schema() -> OpenapiSchema { let schema = T::schema(); let mut dependencies = schema.dependencies.clone(); - + let items = match schema.name.clone() { Some(name) => { - let reference = Reference { reference: format!("#/components/schemas/{}", name) }; + let reference = Reference { + reference: format!("#/components/schemas/{}", name) + }; dependencies.insert(name, schema); reference }, None => Item(Box::new(schema.into_schema())) }; - + OpenapiSchema { nullable: false, name: None, @@ -290,38 +286,34 @@ impl OpenapiType for Vec } } -impl OpenapiType for BTreeSet -{ - fn schema() -> OpenapiSchema - { +impl OpenapiType for BTreeSet { + fn schema() -> OpenapiSchema { as OpenapiType>::schema() } } -impl OpenapiType for HashSet -{ - fn schema() -> OpenapiSchema - { +impl OpenapiType for HashSet { + fn schema() -> OpenapiSchema { as OpenapiType>::schema() } } -impl OpenapiType for HashMap -{ - fn schema() -> OpenapiSchema - { +impl OpenapiType for HashMap { + fn schema() -> OpenapiSchema { let schema = T::schema(); let mut dependencies = schema.dependencies.clone(); - + let items = Box::new(match schema.name.clone() { Some(name) => { - let reference = Reference { reference: format!("#/components/schemas/{}", name) }; + let reference = Reference { + reference: format!("#/components/schemas/{}", name) + }; dependencies.insert(name, schema); reference }, None => Item(schema.into_schema()) }); - + OpenapiSchema { nullable: false, name: None, @@ -334,10 +326,8 @@ impl OpenapiType for HashMap } } -impl OpenapiType for serde_json::Value -{ - fn schema() -> OpenapiSchema - { +impl OpenapiType for serde_json::Value { + fn schema() -> OpenapiSchema { OpenapiSchema { nullable: true, name: None, @@ -347,15 +337,13 @@ impl OpenapiType for serde_json::Value } } - #[cfg(test)] -mod test -{ +mod test { use super::*; use serde_json::Value; - + type Unit = (); - + macro_rules! assert_schema { ($ty:ident $(<$($generic:ident),+>)* => $json:expr) => { paste::item! { @@ -369,7 +357,7 @@ mod test } }; } - + assert_schema!(Unit => r#"{"type":"object","additionalProperties":false}"#); assert_schema!(bool => r#"{"type":"boolean"}"#); assert_schema!(isize => r#"{"type":"integer"}"#); @@ -386,7 +374,7 @@ mod test assert_schema!(u128 => r#"{"type":"integer","format":"int128","minimum":0}"#); assert_schema!(f32 => r#"{"type":"number","format":"float"}"#); assert_schema!(f64 => r#"{"type":"number","format":"double"}"#); - + assert_schema!(String => r#"{"type":"string"}"#); #[cfg(feature = "chrono")] assert_schema!(Date => r#"{"type":"string","format":"date"}"#); @@ -406,7 +394,7 @@ mod test assert_schema!(NaiveDateTime => r#"{"type":"string","format":"date-time"}"#); #[cfg(feature = "uuid")] assert_schema!(Uuid => r#"{"type":"string","format":"uuid"}"#); - + assert_schema!(Option => r#"{"nullable":true,"type":"string"}"#); assert_schema!(Vec => r#"{"type":"array","items":{"type":"string"}}"#); assert_schema!(BTreeSet => r#"{"type":"array","items":{"type":"string"}}"#); diff --git a/src/resource.rs b/src/resource.rs index daedd3d..7784992 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -1,22 +1,14 @@ use crate::{DrawResourceRoutes, RequestBody, ResourceID, ResourceResult, ResourceType}; -use gotham::{ - extractor::QueryStringExtractor, - hyper::Body, - state::State -}; -use std::{ - future::Future, - pin::Pin -}; +use gotham::{extractor::QueryStringExtractor, 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 /// 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)]`. -pub trait Resource -{ +pub trait Resource { /// Register all methods handled by this resource with the underlying router. - fn setup(route : D); + fn setup(route: D); } /// 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 /// annotate it with `#[(YourResource)]`, where `` is one of the supported /// resource methods. -pub trait ResourceMethod -{ - type Res : ResourceResult + Send + 'static; - +pub trait ResourceMethod { + type Res: ResourceResult + Send + 'static; + #[cfg(feature = "openapi")] - fn operation_id() -> Option - { + fn operation_id() -> Option { None } - - fn wants_auth() -> bool - { + + fn wants_auth() -> bool { false } } /// The read_all [`ResourceMethod`](trait.ResourceMethod.html). -pub trait ResourceReadAll : ResourceMethod -{ +pub trait ResourceReadAll: ResourceMethod { /// Handle a GET request on the Resource root. - fn read_all(state : State) -> Pin + Send>>; + fn read_all(state: State) -> Pin + Send>>; } /// The read [`ResourceMethod`](trait.ResourceMethod.html). -pub trait ResourceRead : ResourceMethod -{ +pub trait ResourceRead: ResourceMethod { /// 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. - fn read(state : State, id : Self::ID) -> Pin + Send>>; + fn read(state: State, id: Self::ID) -> Pin + Send>>; } /// The search [`ResourceMethod`](trait.ResourceMethod.html). -pub trait ResourceSearch : ResourceMethod -{ +pub trait ResourceSearch: ResourceMethod { /// The Query type to be parsed from the request parameters. - type Query : ResourceType + QueryStringExtractor + Sync; - + type Query: ResourceType + QueryStringExtractor + Sync; + /// Handle a GET request on the Resource with additional search parameters. - fn search(state : State, query : Self::Query) -> Pin + Send>>; + fn search(state: State, query: Self::Query) -> Pin + Send>>; } /// The create [`ResourceMethod`](trait.ResourceMethod.html). -pub trait ResourceCreate : ResourceMethod -{ +pub trait ResourceCreate: ResourceMethod { /// The Body type to be parsed from the request body. - type Body : RequestBody; - + type Body: RequestBody; + /// Handle a POST request on the Resource root. - fn create(state : State, body : Self::Body) -> Pin + Send>>; + fn create(state: State, body: Self::Body) -> Pin + Send>>; } /// 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. - type Body : RequestBody; - + type Body: RequestBody; + /// Handle a PUT request on the Resource root. - fn change_all(state : State, body : Self::Body) -> Pin + Send>>; + fn change_all(state: State, body: Self::Body) -> Pin + Send>>; } /// The change [`ResourceMethod`](trait.ResourceMethod.html). -pub trait ResourceChange : ResourceMethod -{ +pub trait ResourceChange: ResourceMethod { /// 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. - type ID : ResourceID + 'static; - + type ID: ResourceID + 'static; + /// Handle a PUT request on the Resource with an id. - fn change(state : State, id : Self::ID, body : Self::Body) -> Pin + Send>>; + fn change(state: State, id: Self::ID, body: Self::Body) -> Pin + Send>>; } /// The remove_all [`ResourceMethod`](trait.ResourceMethod.html). -pub trait ResourceRemoveAll : ResourceMethod -{ +pub trait ResourceRemoveAll: ResourceMethod { /// Handle a DELETE request on the Resource root. - fn remove_all(state : State) -> Pin + Send>>; + fn remove_all(state: State) -> Pin + Send>>; } /// The remove [`ResourceMethod`](trait.ResourceMethod.html). -pub trait ResourceRemove : ResourceMethod -{ +pub trait ResourceRemove: ResourceMethod { /// 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. - fn remove(state : State, id : Self::ID) -> Pin + Send>>; + fn remove(state: State, id: Self::ID) -> Pin + Send>>; } diff --git a/src/response.rs b/src/response.rs index dbbf8c7..d542a1a 100644 --- a/src/response.rs +++ b/src/response.rs @@ -3,62 +3,55 @@ use mime::{Mime, APPLICATION_JSON}; /// A response, used to create the final gotham response from. #[derive(Debug)] -pub struct Response -{ - pub status : StatusCode, - pub body : Body, - pub mime : Option +pub struct Response { + pub status: StatusCode, + pub body: Body, + pub mime: Option } -impl Response -{ +impl Response { /// Create a new `Response` from raw data. - pub fn new>(status : StatusCode, body : B, mime : Option) -> Self - { + pub fn new>(status: StatusCode, body: B, mime: Option) -> Self { Self { status, body: body.into(), mime } } - + /// Create a `Response` with mime type json from already serialized data. - pub fn json>(status : StatusCode, body : B) -> Self - { + pub fn json>(status: StatusCode, body: B) -> Self { Self { status, body: body.into(), mime: Some(APPLICATION_JSON) } } - + /// Create a _204 No Content_ `Response`. - pub fn no_content() -> Self - { + pub fn no_content() -> Self { Self { status: StatusCode::NO_CONTENT, body: Body::empty(), mime: None } } - + /// Create an empty _403 Forbidden_ `Response`. - pub fn forbidden() -> Self - { + pub fn forbidden() -> Self { Self { status: StatusCode::FORBIDDEN, body: Body::empty(), mime: None } } - + #[cfg(test)] - pub(crate) fn full_body(mut self) -> Result, ::Error> - { + pub(crate) fn full_body(mut self) -> Result, ::Error> { use futures_executor::block_on; 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()) } -} \ No newline at end of file +} diff --git a/src/result/auth_result.rs b/src/result/auth_result.rs index 10f0183..6aab5a8 100644 --- a/src/result/auth_result.rs +++ b/src/result/auth_result.rs @@ -1,6 +1,5 @@ use gotham_restful_derive::ResourceError; - /** This is an error type that always yields a _403 Forbidden_ response. This type is best used in combination with [`AuthSuccess`] or [`AuthResult`]. @@ -9,8 +8,7 @@ combination with [`AuthSuccess`] or [`AuthResult`]. [`AuthResult`]: type.AuthResult.html */ #[derive(Debug, Clone, Copy, ResourceError)] -pub enum AuthError -{ +pub enum AuthError { #[status(FORBIDDEN)] #[display("Forbidden")] Forbidden @@ -57,8 +55,7 @@ error, or delegates to another error type. This type is best used with [`AuthRes [`AuthResult`]: type.AuthResult.html */ #[derive(Debug, ResourceError)] -pub enum AuthErrorOrOther -{ +pub enum AuthErrorOrOther { #[status(FORBIDDEN)] #[display("Forbidden")] Forbidden, @@ -67,10 +64,8 @@ pub enum AuthErrorOrOther Other(E) } -impl From for AuthErrorOrOther -{ - fn from(err : AuthError) -> Self - { +impl From for AuthErrorOrOther { + fn from(err: AuthError) -> Self { match err { AuthError::Forbidden => Self::Forbidden } @@ -80,10 +75,9 @@ impl From for AuthErrorOrOther impl From for AuthErrorOrOther where // TODO https://gitlab.com/msrd0/gotham-restful/-/issues/20 - F : std::error::Error + Into + F: std::error::Error + Into { - fn from(err : F) -> Self - { + fn from(err: F) -> Self { Self::Other(err.into()) } } diff --git a/src/result/mod.rs b/src/result/mod.rs index 314b9ac..100ba55 100644 --- a/src/result/mod.rs +++ b/src/result/mod.rs @@ -1,13 +1,13 @@ -use crate::Response; #[cfg(feature = "openapi")] use crate::OpenapiSchema; +use crate::Response; use futures_util::future::FutureExt; use mime::{Mime, STAR_STAR}; use serde::Serialize; use std::{ error::Error, - future::Future, fmt::{Debug, Display}, + future::Future, pin::Pin }; @@ -27,67 +27,54 @@ pub use result::IntoResponseError; mod success; pub use success::Success; - -pub(crate) trait OrAllTypes -{ +pub(crate) trait OrAllTypes { fn or_all_types(self) -> Vec; } -impl OrAllTypes for Option> -{ - fn or_all_types(self) -> Vec - { +impl OrAllTypes for Option> { + fn or_all_types(self) -> Vec { self.unwrap_or_else(|| vec![STAR_STAR]) } } - /// A trait provided to convert a resource's result to json. -pub trait ResourceResult -{ - type Err : Error + Send + 'static; - +pub trait ResourceResult { + type Err: Error + Send + Sync + 'static; + /// Turn this into a response that can be returned to the browser. This api will likely /// change in the future. fn into_response(self) -> Pin> + Send>>; - + /// Return a list of supported mime types. - fn accepted_types() -> Option> - { + fn accepted_types() -> Option> { None } - + #[cfg(feature = "openapi")] fn schema() -> OpenapiSchema; - + #[cfg(feature = "openapi")] - fn default_status() -> crate::StatusCode - { + fn default_status() -> crate::StatusCode { crate::StatusCode::OK } } #[cfg(feature = "openapi")] -impl crate::OpenapiType for Res -{ - fn schema() -> OpenapiSchema - { +impl crate::OpenapiType for Res { + fn schema() -> OpenapiSchema { Self::schema() } } /// The default json returned on an 500 Internal Server Error. #[derive(Debug, Serialize)] -pub(crate) struct ResourceError -{ - error : bool, - message : String +pub(crate) struct ResourceError { + error: bool, + message: String } -impl From for ResourceError -{ - fn from(message : T) -> Self - { +impl From for ResourceError { + fn from(message: T) -> Self { Self { error: true, message: message.to_string() @@ -95,27 +82,26 @@ impl From for ResourceError } } -fn into_response_helper(create_response : F) -> Pin> + Send>> +fn into_response_helper(create_response: F) -> Pin> + Send>> where - Err : Send + 'static, - F : FnOnce() -> Result + Err: Send + 'static, + F: FnOnce() -> Result { let res = create_response(); async move { res }.boxed() } #[cfg(feature = "errorlog")] -fn errorlog(e : E) -{ +fn errorlog(e: E) { error!("The handler encountered an error: {}", e); } #[cfg(not(feature = "errorlog"))] -fn errorlog(_e : E) {} +fn errorlog(_e: E) {} -fn handle_error(e : E) -> Pin> + Send>> +fn handle_error(e: E) -> Pin> + Send>> where - E : Display + IntoResponseError + E: Display + IntoResponseError { into_response_helper(|| { errorlog(&e); @@ -123,67 +109,55 @@ where }) } - impl ResourceResult for Pin + Send>> where - Res : ResourceResult + 'static + Res: ResourceResult + 'static { type Err = Res::Err; - - fn into_response(self) -> Pin> + Send>> - { - self.then(|result| { - result.into_response() - }).boxed() + + fn into_response(self) -> Pin> + Send>> { + self.then(|result| result.into_response()).boxed() } - - fn accepted_types() -> Option> - { + + fn accepted_types() -> Option> { Res::accepted_types() } - + #[cfg(feature = "openapi")] - fn schema() -> OpenapiSchema - { + fn schema() -> OpenapiSchema { Res::schema() } - + #[cfg(feature = "openapi")] - fn default_status() -> crate::StatusCode - { + fn default_status() -> crate::StatusCode { Res::default_status() } } - - #[cfg(test)] -mod test -{ +mod test { use super::*; use futures_executor::block_on; use thiserror::Error; - + #[derive(Debug, Default, Deserialize, Serialize)] #[cfg_attr(feature = "openapi", derive(crate::OpenapiType))] - struct Msg - { - msg : String + struct Msg { + msg: String } - + #[derive(Debug, Default, Error)] #[error("An Error")] struct MsgError; - + #[test] - fn result_from_future() - { + fn result_from_future() { let nc = NoContent::default(); let res = block_on(nc.into_response()).unwrap(); - + let fut_nc = async move { NoContent::default() }.boxed(); let fut_res = block_on(fut_nc.into_response()).unwrap(); - + assert_eq!(res.status, fut_res.status); assert_eq!(res.mime, fut_res.mime); assert_eq!(res.full_body().unwrap(), fut_res.full_body().unwrap()); diff --git a/src/result/no_content.rs b/src/result/no_content.rs index 3377b66..0c4fe05 100644 --- a/src/result/no_content.rs +++ b/src/result/no_content.rs @@ -1,14 +1,10 @@ -use super::{ResourceResult, handle_error}; +use super::{handle_error, ResourceResult}; use crate::{IntoResponseError, Response}; #[cfg(feature = "openapi")] use crate::{OpenapiSchema, OpenapiType}; use futures_util::{future, future::FutureExt}; use mime::Mime; -use std::{ - fmt::Display, - future::Future, - pin::Pin -}; +use std::{fmt::Display, future::Future, pin::Pin}; /** This is the return type of a resource that doesn't actually return something. It will result @@ -35,104 +31,89 @@ fn read_all(_state: &mut State) { #[derive(Clone, Copy, Debug, Default)] pub struct NoContent; -impl From<()> for NoContent -{ - fn from(_ : ()) -> Self - { +impl From<()> for NoContent { + fn from(_: ()) -> Self { Self {} } } -impl ResourceResult for NoContent -{ +impl ResourceResult for NoContent { // TODO this shouldn't be a serde_json::Error type Err = serde_json::Error; // just for easier handling of `Result` - + /// This will always be a _204 No Content_ together with an empty string. - fn into_response(self) -> Pin> + Send>> - { + fn into_response(self) -> Pin> + Send>> { future::ok(Response::no_content()).boxed() } - - fn accepted_types() -> Option> - { + + fn accepted_types() -> Option> { Some(Vec::new()) } - + /// Returns the schema of the `()` type. #[cfg(feature = "openapi")] - fn schema() -> OpenapiSchema - { + fn schema() -> OpenapiSchema { <()>::schema() } - + /// This will always be a _204 No Content_ #[cfg(feature = "openapi")] - fn default_status() -> crate::StatusCode - { + fn default_status() -> crate::StatusCode { crate::StatusCode::NO_CONTENT } } impl ResourceResult for Result where - E : Display + IntoResponseError + E: Display + IntoResponseError { type Err = serde_json::Error; - - fn into_response(self) -> Pin> + Send>> - { + + fn into_response(self) -> Pin> + Send>> { match self { Ok(nc) => nc.into_response(), Err(e) => handle_error(e) } } - - fn accepted_types() -> Option> - { + + fn accepted_types() -> Option> { NoContent::accepted_types() } - + #[cfg(feature = "openapi")] - fn schema() -> OpenapiSchema - { + fn schema() -> OpenapiSchema { ::schema() } - + #[cfg(feature = "openapi")] - fn default_status() -> crate::StatusCode - { + fn default_status() -> crate::StatusCode { NoContent::default_status() } } - #[cfg(test)] -mod test -{ +mod test { use super::*; use futures_executor::block_on; use gotham::hyper::StatusCode; use thiserror::Error; - + #[derive(Debug, Default, Error)] #[error("An Error")] struct MsgError; - + #[test] - fn no_content_has_empty_response() - { + fn no_content_has_empty_response() { let no_content = NoContent::default(); let res = block_on(no_content.into_response()).expect("didn't expect error response"); assert_eq!(res.status, StatusCode::NO_CONTENT); assert_eq!(res.mime, None); assert_eq!(res.full_body().unwrap(), &[] as &[u8]); } - + #[test] - fn no_content_result() - { - let no_content : Result = Ok(NoContent::default()); + fn no_content_result() { + let no_content: Result = Ok(NoContent::default()); let res = block_on(no_content.into_response()).expect("didn't expect error response"); assert_eq!(res.status, StatusCode::NO_CONTENT); assert_eq!(res.mime, None); diff --git a/src/result/raw.rs b/src/result/raw.rs index a44e15a..bf1997f 100644 --- a/src/result/raw.rs +++ b/src/result/raw.rs @@ -1,7 +1,7 @@ -use super::{IntoResponseError, ResourceResult, handle_error}; -use crate::{FromBody, RequestBody, ResourceType, Response, StatusCode}; +use super::{handle_error, IntoResponseError, ResourceResult}; #[cfg(feature = "openapi")] use crate::OpenapiSchema; +use crate::{FromBody, RequestBody, ResourceType, Response, StatusCode}; use futures_core::future::Future; use futures_util::{future, future::FutureExt}; use gotham::hyper::body::{Body, Bytes}; @@ -9,11 +9,7 @@ use mime::Mime; #[cfg(feature = "openapi")] use openapiv3::{SchemaKind, StringFormat, StringType, Type, VariantOrUnknownOrEmpty}; use serde_json::error::Error as SerdeJsonError; -use std::{ - convert::Infallible, - fmt::Display, - pin::Pin -}; +use std::{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 @@ -43,44 +39,37 @@ fn create(body : Raw>) -> Raw> { [`OpenapiType`]: trait.OpenapiType.html */ #[derive(Debug)] -pub struct Raw -{ - pub raw : T, - pub mime : Mime +pub struct Raw { + pub raw: T, + pub mime: Mime } -impl Raw -{ - pub fn new(raw : T, mime : Mime) -> Self - { +impl Raw { + pub fn new(raw: T, mime: Mime) -> Self { Self { raw, mime } } } impl AsMut for Raw where - T : AsMut + T: AsMut { - fn as_mut(&mut self) -> &mut U - { + fn as_mut(&mut self) -> &mut U { self.raw.as_mut() } } impl AsRef for Raw where - T : AsRef + T: AsRef { - fn as_ref(&self) -> &U - { + fn as_ref(&self) -> &U { self.raw.as_ref() } } -impl Clone for Raw -{ - fn clone(&self) -> Self - { +impl Clone for Raw { + fn clone(&self) -> Self { Self { raw: self.raw.clone(), mime: self.mime.clone() @@ -88,36 +77,28 @@ impl Clone for Raw } } -impl From<&'a [u8]>> FromBody for Raw -{ +impl From<&'a [u8]>> FromBody for Raw { type Err = Infallible; - - fn from_body(body : Bytes, mime : Mime) -> Result - { + + fn from_body(body: Bytes, mime: Mime) -> Result { Ok(Self::new(body.as_ref().into(), mime)) } } -impl RequestBody for Raw -where - Raw : FromBody + ResourceType -{ -} +impl RequestBody for Raw where Raw: FromBody + ResourceType {} -impl> ResourceResult for Raw +impl> ResourceResult for Raw where - Self : Send + Self: Send { type Err = SerdeJsonError; // just for easier handling of `Result, E>` - - fn into_response(self) -> Pin> + Send>> - { + + fn into_response(self) -> Pin> + Send>> { future::ok(Response::new(StatusCode::OK, self.raw, Some(self.mime.clone()))).boxed() } - + #[cfg(feature = "openapi")] - fn schema() -> OpenapiSchema - { + fn schema() -> OpenapiSchema { OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { format: VariantOrUnknownOrEmpty::Item(StringFormat::Binary), ..Default::default() @@ -127,37 +108,32 @@ where impl ResourceResult for Result, E> where - Raw : ResourceResult, - E : Display + IntoResponseError as ResourceResult>::Err> + Raw: ResourceResult, + E: Display + IntoResponseError as ResourceResult>::Err> { type Err = E::Err; - - fn into_response(self) -> Pin> + Send>> - { + + fn into_response(self) -> Pin> + Send>> { match self { Ok(raw) => raw.into_response(), Err(e) => handle_error(e) } } - + #[cfg(feature = "openapi")] - fn schema() -> OpenapiSchema - { + fn schema() -> OpenapiSchema { as ResourceResult>::schema() } } - #[cfg(test)] -mod test -{ +mod test { use super::*; use futures_executor::block_on; use mime::TEXT_PLAIN; - + #[test] - fn raw_response() - { + fn raw_response() { let msg = "Test"; let raw = Raw::new(msg, TEXT_PLAIN); let res = block_on(raw.into_response()).expect("didn't expect error response"); diff --git a/src/result/result.rs b/src/result/result.rs index 5de2e44..71c969a 100644 --- a/src/result/result.rs +++ b/src/result/result.rs @@ -1,106 +1,95 @@ -use super::{ResourceResult, handle_error, into_response_helper}; -use crate::{ - result::ResourceError, - Response, ResponseBody, StatusCode -}; +use super::{handle_error, into_response_helper, ResourceResult}; #[cfg(feature = "openapi")] use crate::OpenapiSchema; +use crate::{result::ResourceError, Response, ResponseBody, StatusCode}; use futures_core::future::Future; use mime::{Mime, APPLICATION_JSON}; -use std::{ - error::Error, - fmt::Display, - pin::Pin -}; +use std::{error::Error, fmt::Display, pin::Pin}; + +pub trait IntoResponseError { + type Err: Error + Send + 'static; -pub trait IntoResponseError -{ - type Err : Error + Send + 'static; - fn into_response_error(self) -> Result; } -impl IntoResponseError for E -{ +impl IntoResponseError for E { type Err = serde_json::Error; - - fn into_response_error(self) -> Result - { - let err : ResourceError = self.into(); - Ok(Response::json(StatusCode::INTERNAL_SERVER_ERROR, serde_json::to_string(&err)?)) + + fn into_response_error(self) -> Result { + let err: ResourceError = self.into(); + Ok(Response::json( + StatusCode::INTERNAL_SERVER_ERROR, + serde_json::to_string(&err)? + )) } } impl ResourceResult for Result where - R : ResponseBody, - E : Display + IntoResponseError + R: ResponseBody, + E: Display + IntoResponseError { type Err = E::Err; - - fn into_response(self) -> Pin> + Send>> - { + + fn into_response(self) -> Pin> + Send>> { match self { Ok(r) => into_response_helper(|| Ok(Response::json(StatusCode::OK, serde_json::to_string(&r)?))), Err(e) => handle_error(e) } } - - fn accepted_types() -> Option> - { + + fn accepted_types() -> Option> { Some(vec![APPLICATION_JSON]) } - + #[cfg(feature = "openapi")] - fn schema() -> OpenapiSchema - { + fn schema() -> OpenapiSchema { R::schema() } } - #[cfg(test)] -mod test -{ +mod test { use super::*; use crate::result::OrAllTypes; use futures_executor::block_on; use thiserror::Error; - + #[derive(Debug, Default, Deserialize, Serialize)] #[cfg_attr(feature = "openapi", derive(crate::OpenapiType))] - struct Msg - { - msg : String + struct Msg { + msg: String } - + #[derive(Debug, Default, Error)] #[error("An Error")] struct MsgError; - + #[test] - fn result_ok() - { - let ok : Result = Ok(Msg::default()); + fn result_ok() { + let ok: Result = Ok(Msg::default()); let res = block_on(ok.into_response()).expect("didn't expect error response"); assert_eq!(res.status, StatusCode::OK); assert_eq!(res.mime, Some(APPLICATION_JSON)); assert_eq!(res.full_body().unwrap(), r#"{"msg":""}"#.as_bytes()); } - + #[test] - fn result_err() - { - let err : Result = Err(MsgError::default()); + fn result_err() { + let err: Result = Err(MsgError::default()); let res = block_on(err.into_response()).expect("didn't expect error response"); assert_eq!(res.status, StatusCode::INTERNAL_SERVER_ERROR); 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] - fn success_accepts_json() - { - assert!(>::accepted_types().or_all_types().contains(&APPLICATION_JSON)) + fn success_accepts_json() { + assert!(>::accepted_types() + .or_all_types() + .contains(&APPLICATION_JSON)) } } diff --git a/src/result/success.rs b/src/result/success.rs index dffd740..43cdd6f 100644 --- a/src/result/success.rs +++ b/src/result/success.rs @@ -1,14 +1,14 @@ -use super::{ResourceResult, into_response_helper}; -use crate::{Response, ResponseBody}; +use super::{into_response_helper, ResourceResult}; #[cfg(feature = "openapi")] use crate::OpenapiSchema; +use crate::{Response, ResponseBody}; use gotham::hyper::StatusCode; use mime::{Mime, APPLICATION_JSON}; use std::{ fmt::Debug, future::Future, - pin::Pin, - ops::{Deref, DerefMut} + ops::{Deref, DerefMut}, + pin::Pin }; /** @@ -45,119 +45,95 @@ fn read_all(_state: &mut State) -> Success { #[derive(Debug)] pub struct Success(T); -impl AsMut for Success -{ - fn as_mut(&mut self) -> &mut T - { +impl AsMut for Success { + fn as_mut(&mut self) -> &mut T { &mut self.0 } } -impl AsRef for Success -{ - fn as_ref(&self) -> &T - { +impl AsRef for Success { + fn as_ref(&self) -> &T { &self.0 } } -impl Deref for Success -{ +impl Deref for Success { type Target = T; - - fn deref(&self) -> &T - { + + fn deref(&self) -> &T { &self.0 } } -impl DerefMut for Success -{ - fn deref_mut(&mut self) -> &mut T - { +impl DerefMut for Success { + fn deref_mut(&mut self) -> &mut T { &mut self.0 } } -impl From for Success -{ - fn from(t : T) -> Self - { +impl From for Success { + fn from(t: T) -> Self { Self(t) } } -impl Clone for Success -{ - fn clone(&self) -> Self - { +impl Clone for Success { + fn clone(&self) -> Self { Self(self.0.clone()) } } -impl Copy for Success -{ -} +impl Copy for Success {} -impl Default for Success -{ - fn default() -> Self - { +impl Default for Success { + fn default() -> Self { Self(T::default()) } } -impl ResourceResult for Success +impl ResourceResult for Success where - Self : Send + Self: Send { type Err = serde_json::Error; - - fn into_response(self) -> Pin> + Send>> - { + + fn into_response(self) -> Pin> + Send>> { into_response_helper(|| Ok(Response::json(StatusCode::OK, serde_json::to_string(self.as_ref())?))) } - - fn accepted_types() -> Option> - { + + fn accepted_types() -> Option> { Some(vec![APPLICATION_JSON]) } - + #[cfg(feature = "openapi")] - fn schema() -> OpenapiSchema - { + fn schema() -> OpenapiSchema { T::schema() } } - #[cfg(test)] -mod test -{ +mod test { use super::*; use crate::result::OrAllTypes; use futures_executor::block_on; - + #[derive(Debug, Default, Serialize)] #[cfg_attr(feature = "openapi", derive(crate::OpenapiType))] - struct Msg - { - msg : String + struct Msg { + msg: String } - + #[test] - fn success_always_successfull() - { - let success : Success = Msg::default().into(); + fn success_always_successfull() { + let success: Success = Msg::default().into(); let res = block_on(success.into_response()).expect("didn't expect error response"); assert_eq!(res.status, StatusCode::OK); assert_eq!(res.mime, Some(APPLICATION_JSON)); assert_eq!(res.full_body().unwrap(), r#"{"msg":""}"#.as_bytes()); } - + #[test] - fn success_accepts_json() - { + fn success_accepts_json() { assert!(>::accepted_types().or_all_types().contains(&APPLICATION_JSON)) } } diff --git a/src/routing.rs b/src/routing.rs index 5610379..5e952f6 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -1,22 +1,21 @@ -use crate::{ - resource::*, - result::{ResourceError, ResourceResult}, - RequestBody, - Response, - StatusCode -}; -#[cfg(feature = "cors")] -use crate::CorsRoute; #[cfg(feature = "openapi")] use crate::openapi::{ builder::{OpenapiBuilder, OpenapiInfo}, router::OpenapiRouter }; +#[cfg(feature = "cors")] +use crate::CorsRoute; +use crate::{ + resource::*, + result::{ResourceError, ResourceResult}, + RequestBody, Response, StatusCode +}; use futures_util::{future, future::FutureExt}; use gotham::{ - handler::{HandlerError, HandlerFuture, IntoHandlerError}, + handler::{HandlerError, HandlerFuture}, helpers::http::response::{create_empty_response, create_response}, + hyper::{body::to_bytes, header::CONTENT_TYPE, Body, HeaderMap, Method}, pipeline::chain::PipelineHandleChain, router::{ builder::*, @@ -25,99 +24,84 @@ use gotham::{ }, state::{FromState, State} }; -use gotham::hyper::{ - body::to_bytes, - header::CONTENT_TYPE, - Body, - HeaderMap, - Method -}; use mime::{Mime, APPLICATION_JSON}; -use std::{ - future::Future, - panic::RefUnwindSafe, - pin::Pin -}; +use std::{future::Future, panic::RefUnwindSafe, pin::Pin}; /// Allow us to extract an id from a path. #[derive(Deserialize, StateData, StaticResponseExtender)] -struct PathExtractor -{ - id : ID +struct PathExtractor { + id: ID } /// 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 /// an OpenAPI specification on request. #[cfg(feature = "openapi")] -pub trait WithOpenapi -{ - fn with_openapi(&mut self, info : OpenapiInfo, block : F) +pub trait WithOpenapi { + fn with_openapi(&mut self, info: OpenapiInfo, block: F) where - F : FnOnce(OpenapiRouter<'_, D>); + F: FnOnce(OpenapiRouter<'_, D>); } /// This trait adds the `resource` method to gotham's routing. It allows you to register /// any RESTful `Resource` with a path. -pub trait DrawResources -{ - fn resource(&mut self, path : &str); +pub trait DrawResources { + fn resource(&mut self, path: &str); } /// This trait allows to draw routes within an resource. Use this only inside the /// `Resource::setup` method. -pub trait DrawResourceRoutes -{ - fn read_all(&mut self); - - fn read(&mut self); - - fn search(&mut self); - - fn create(&mut self) +pub trait DrawResourceRoutes { + fn read_all(&mut self); + + fn read(&mut self); + + fn search(&mut self); + + fn create(&mut self) where - Handler::Res : 'static, - Handler::Body : 'static; - - fn change_all(&mut self) + Handler::Res: 'static, + Handler::Body: 'static; + + fn change_all(&mut self) where - Handler::Res : 'static, - Handler::Body : 'static; - - fn change(&mut self) + Handler::Res: 'static, + Handler::Body: 'static; + + fn change(&mut self) where - Handler::Res : 'static, - Handler::Body : 'static; - - fn remove_all(&mut self); - - fn remove(&mut self); + Handler::Res: 'static, + Handler::Body: 'static; + + fn remove_all(&mut self); + + fn remove(&mut self); } -fn response_from(res : Response, state : &State) -> gotham::hyper::Response -{ +fn response_from(res: Response, state: &State) -> gotham::hyper::Response { 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()); } - + let method = Method::borrow_from(state); - if method != Method::HEAD - { + if method != Method::HEAD { *r.body_mut() = res.body; } - + #[cfg(feature = "cors")] crate::cors::handle_cors(state, &mut r); - + r } -async fn to_handler_future(state : State, get_result : F) -> Result<(State, gotham::hyper::Response), (State, HandlerError)> +async fn to_handler_future( + state: State, + get_result: F +) -> Result<(State, gotham::hyper::Response), (State, HandlerError)> where - F : FnOnce(State) -> Pin + Send>>, - R : ResourceResult + F: FnOnce(State) -> Pin + Send>>, + R: ResourceResult { let (state, res) = get_result(state).await; let res = res.into_response().await; @@ -126,67 +110,70 @@ where let r = response_from(res, &state); Ok((state, r)) }, - Err(e) => Err((state, e.into_handler_error())) + Err(e) => Err((state, e.into())) } } -async fn body_to_res(mut state : State, get_result : F) -> (State, Result, HandlerError>) +async fn body_to_res( + mut state: State, + get_result: F +) -> (State, Result, HandlerError>) where - B : RequestBody, - F : FnOnce(State, B) -> Pin + Send>>, - R : ResourceResult + B: RequestBody, + F: FnOnce(State, B) -> Pin + Send>>, + R: ResourceResult { let body = to_bytes(Body::take_from(&mut state)).await; - + let body = match 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(), None => { let res = create_empty_response(&state, StatusCode::UNSUPPORTED_MEDIA_TYPE); - return (state, Ok(res)) + return (state, Ok(res)); } }; - + let res = { let body = match B::from_body(body, content_type) { Ok(body) => body, Err(e) => { - let error : ResourceError = e.into(); + let error: ResourceError = e.into(); let res = match serde_json::to_string(&error) { Ok(json) => { let res = create_response(&state, StatusCode::BAD_REQUEST, APPLICATION_JSON, json); Ok(res) }, - Err(e) => Err(e.into_handler_error()) + Err(e) => Err(e.into()) }; - return (state, res) + return (state, res); } }; get_result(state, body) }; - + let (state, res) = res.await; let res = res.into_response().await; - + let res = match res { Ok(res) => { let r = response_from(res, &state); Ok(r) }, - Err(e) => Err(e.into_handler_error()) + Err(e) => Err(e.into()) }; (state, res) } -fn handle_with_body(state : State, get_result : F) -> Pin> +fn handle_with_body(state: State, get_result: F) -> Pin> where - B : RequestBody + 'static, - F : FnOnce(State, B) -> Pin + Send>> + Send + 'static, - R : ResourceResult + Send + 'static + B: RequestBody + 'static, + F: FnOnce(State, B) -> Pin + Send>> + Send + 'static, + R: ResourceResult + Send + 'static { body_to_res(state, get_result) .then(|(state, res)| match res { @@ -196,78 +183,70 @@ where .boxed() } -fn read_all_handler(state : State) -> Pin> -{ +fn read_all_handler(state: State) -> Pin> { to_handler_future(state, |state| Handler::read_all(state)).boxed() } -fn read_handler(state : State) -> Pin> -{ +fn read_handler(state: State) -> Pin> { let id = { - let path : &PathExtractor = PathExtractor::borrow_from(&state); + let path: &PathExtractor = PathExtractor::borrow_from(&state); path.id.clone() }; to_handler_future(state, |state| Handler::read(state, id)).boxed() } -fn search_handler(mut state : State) -> Pin> -{ +fn search_handler(mut state: State) -> Pin> { let query = Handler::Query::take_from(&mut state); to_handler_future(state, |state| Handler::search(state, query)).boxed() } -fn create_handler(state : State) -> Pin> +fn create_handler(state: State) -> Pin> where - Handler::Res : 'static, - Handler::Body : 'static + Handler::Res: 'static, + Handler::Body: 'static { handle_with_body::(state, |state, body| Handler::create(state, body)) } -fn change_all_handler(state : State) -> Pin> +fn change_all_handler(state: State) -> Pin> where - Handler::Res : 'static, - Handler::Body : 'static + Handler::Res: 'static, + Handler::Body: 'static { handle_with_body::(state, |state, body| Handler::change_all(state, body)) } -fn change_handler(state : State) -> Pin> +fn change_handler(state: State) -> Pin> where - Handler::Res : 'static, - Handler::Body : 'static + Handler::Res: 'static, + Handler::Body: 'static { let id = { - let path : &PathExtractor = PathExtractor::borrow_from(&state); + let path: &PathExtractor = PathExtractor::borrow_from(&state); path.id.clone() }; handle_with_body::(state, |state, body| Handler::change(state, id, body)) } -fn remove_all_handler(state : State) -> Pin> -{ +fn remove_all_handler(state: State) -> Pin> { to_handler_future(state, |state| Handler::remove_all(state)).boxed() } -fn remove_handler(state : State) -> Pin> -{ +fn remove_handler(state: State) -> Pin> { let id = { - let path : &PathExtractor = PathExtractor::borrow_from(&state); + let path: &PathExtractor = PathExtractor::borrow_from(&state); path.id.clone() }; to_handler_future(state, |state| Handler::remove(state, id)).boxed() } #[derive(Clone)] -struct MaybeMatchAcceptHeader -{ - matcher : Option +struct MaybeMatchAcceptHeader { + matcher: Option } -impl RouteMatcher for MaybeMatchAcceptHeader -{ - fn is_match(&self, state : &State) -> Result<(), RouteNonMatch> - { +impl RouteMatcher for MaybeMatchAcceptHeader { + fn is_match(&self, state: &State) -> Result<(), RouteNonMatch> { match &self.matcher { Some(matcher) => matcher.is_match(state), None => Ok(()) @@ -275,10 +254,8 @@ impl RouteMatcher for MaybeMatchAcceptHeader } } -impl From>> for MaybeMatchAcceptHeader -{ - fn from(types : Option>) -> Self - { +impl From>> for MaybeMatchAcceptHeader { + fn from(types: Option>) -> Self { let types = match types { Some(types) if types.is_empty() => None, types => types @@ -290,15 +267,12 @@ impl From>> for MaybeMatchAcceptHeader } #[derive(Clone)] -struct MaybeMatchContentTypeHeader -{ - matcher : Option +struct MaybeMatchContentTypeHeader { + matcher: Option } -impl RouteMatcher for MaybeMatchContentTypeHeader -{ - fn is_match(&self, state : &State) -> Result<(), RouteNonMatch> - { +impl RouteMatcher for MaybeMatchContentTypeHeader { + fn is_match(&self, state: &State) -> Result<(), RouteNonMatch> { match &self.matcher { Some(matcher) => matcher.is_match(state), None => Ok(()) @@ -306,10 +280,8 @@ impl RouteMatcher for MaybeMatchContentTypeHeader } } -impl From>> for MaybeMatchContentTypeHeader -{ - fn from(types : Option>) -> Self - { +impl From>> for MaybeMatchContentTypeHeader { + fn from(types: Option>) -> Self { Self { matcher: types.map(|types| ContentTypeHeaderRouteMatcher::new(types).allow_no_type()) } @@ -318,16 +290,15 @@ impl From>> for MaybeMatchContentTypeHeader macro_rules! implDrawResourceRoutes { ($implType:ident) => { - #[cfg(feature = "openapi")] impl<'a, C, P> WithOpenapi for $implType<'a, C, P> where - C : PipelineHandleChain

+ Copy + Send + Sync + 'static, - P : RefUnwindSafe + Send + Sync + 'static + C: PipelineHandleChain

+ Copy + Send + Sync + 'static, + P: RefUnwindSafe + Send + Sync + 'static { - fn with_openapi(&mut self, info : OpenapiInfo, block : F) + fn with_openapi(&mut self, info: OpenapiInfo, block: F) where - F : FnOnce(OpenapiRouter<'_, $implType<'a, C, P>>) + F: FnOnce(OpenapiRouter<'_, $implType<'a, C, P>>) { let router = OpenapiRouter { router: self, @@ -337,58 +308,58 @@ macro_rules! implDrawResourceRoutes { block(router); } } - + impl<'a, C, P> DrawResources for $implType<'a, C, P> where - C : PipelineHandleChain

+ Copy + Send + Sync + 'static, - P : RefUnwindSafe + Send + Sync + 'static + C: PipelineHandleChain

+ Copy + Send + Sync + 'static, + P: RefUnwindSafe + Send + Sync + 'static { - fn resource(&mut self, path : &str) - { + fn resource(&mut self, path: &str) { R::setup((self, path)); } } - + #[allow(clippy::redundant_closure)] // doesn't work because of type parameters impl<'a, C, P> DrawResourceRoutes for (&mut $implType<'a, C, P>, &str) where - C : PipelineHandleChain

+ Copy + Send + Sync + 'static, - P : RefUnwindSafe + Send + Sync + 'static + C: PipelineHandleChain

+ Copy + Send + Sync + 'static, + P: RefUnwindSafe + Send + Sync + 'static { - fn read_all(&mut self) - { - let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); - self.0.get(&self.1) + fn read_all(&mut self) { + let matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); + self.0 + .get(&self.1) .extend_route_matcher(matcher) .to(|state| read_all_handler::(state)); } - fn read(&mut self) - { - let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); - self.0.get(&format!("{}/:id", self.1)) + fn read(&mut self) { + let matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); + self.0 + .get(&format!("{}/:id", self.1)) .extend_route_matcher(matcher) .with_path_extractor::>() .to(|state| read_handler::(state)); } - - fn search(&mut self) - { - let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); - self.0.get(&format!("{}/search", self.1)) + + fn search(&mut self) { + let matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); + self.0 + .get(&format!("{}/search", self.1)) .extend_route_matcher(matcher) .with_query_string_extractor::() .to(|state| search_handler::(state)); } - - fn create(&mut self) + + fn create(&mut self) where - Handler::Res : Send + 'static, - Handler::Body : 'static + Handler::Res: Send + 'static, + Handler::Body: 'static { - let accept_matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); - let content_matcher : MaybeMatchContentTypeHeader = Handler::Body::supported_types().into(); - self.0.post(&self.1) + let accept_matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); + let content_matcher: MaybeMatchContentTypeHeader = Handler::Body::supported_types().into(); + self.0 + .post(&self.1) .extend_route_matcher(accept_matcher) .extend_route_matcher(content_matcher) .to(|state| create_handler::(state)); @@ -396,14 +367,15 @@ macro_rules! implDrawResourceRoutes { self.0.cors(&self.1, Method::POST); } - fn change_all(&mut self) + fn change_all(&mut self) where - Handler::Res : Send + 'static, - Handler::Body : 'static + Handler::Res: Send + 'static, + Handler::Body: 'static { - let accept_matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); - let content_matcher : MaybeMatchContentTypeHeader = Handler::Body::supported_types().into(); - self.0.put(&self.1) + let accept_matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); + let content_matcher: MaybeMatchContentTypeHeader = Handler::Body::supported_types().into(); + self.0 + .put(&self.1) .extend_route_matcher(accept_matcher) .extend_route_matcher(content_matcher) .to(|state| change_all_handler::(state)); @@ -411,15 +383,16 @@ macro_rules! implDrawResourceRoutes { self.0.cors(&self.1, Method::PUT); } - fn change(&mut self) + fn change(&mut self) where - Handler::Res : Send + 'static, - Handler::Body : 'static + Handler::Res: Send + 'static, + Handler::Body: 'static { - let accept_matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); - let content_matcher : MaybeMatchContentTypeHeader = Handler::Body::supported_types().into(); + let accept_matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); + let content_matcher: MaybeMatchContentTypeHeader = Handler::Body::supported_types().into(); let path = format!("{}/:id", self.1); - self.0.put(&path) + self.0 + .put(&path) .extend_route_matcher(accept_matcher) .extend_route_matcher(content_matcher) .with_path_extractor::>() @@ -428,21 +401,21 @@ macro_rules! implDrawResourceRoutes { self.0.cors(&path, Method::PUT); } - fn remove_all(&mut self) - { - let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); - self.0.delete(&self.1) + fn remove_all(&mut self) { + let matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); + self.0 + .delete(&self.1) .extend_route_matcher(matcher) .to(|state| remove_all_handler::(state)); #[cfg(feature = "cors")] self.0.cors(&self.1, Method::DELETE); } - fn remove(&mut self) - { - let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); + fn remove(&mut self) { + let matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); let path = format!("{}/:id", self.1); - self.0.delete(&path) + self.0 + .delete(&path) .extend_route_matcher(matcher) .with_path_extractor::>() .to(|state| remove_handler::(state)); @@ -450,7 +423,7 @@ macro_rules! implDrawResourceRoutes { self.0.cors(&path, Method::POST); } } - } + }; } implDrawResourceRoutes!(RouterBuilder); diff --git a/src/types.rs b/src/types.rs index 576f7e9..030c02c 100644 --- a/src/types.rs +++ b/src/types.rs @@ -4,43 +4,26 @@ use crate::OpenapiType; use gotham::hyper::body::Bytes; use mime::{Mime, APPLICATION_JSON}; use serde::{de::DeserializeOwned, Serialize}; -use std::{ - error::Error, - panic::RefUnwindSafe -}; +use std::{error::Error, panic::RefUnwindSafe}; #[cfg(not(feature = "openapi"))] -pub trait ResourceType -{ -} +pub trait ResourceType {} #[cfg(not(feature = "openapi"))] -impl ResourceType for T -{ -} +impl ResourceType for T {} #[cfg(feature = "openapi")] -pub trait ResourceType : OpenapiType -{ -} +pub trait ResourceType: OpenapiType {} #[cfg(feature = "openapi")] -impl ResourceType for T -{ -} - +impl ResourceType for T {} /// 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 /// `OpenapiType`. -pub trait ResponseBody : ResourceType + Serialize -{ -} - -impl ResponseBody for T -{ -} +pub trait ResponseBody: ResourceType + Serialize {} +impl ResponseBody for T {} /** 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 [`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 /// macro, there is no way to trigger an error, so `Infallible` is used here. However, this /// might change in the future. - type Err : Error; - + type Err: Error; + /// Perform the conversion. - fn from_body(body : Bytes, content_type : Mime) -> Result; + fn from_body(body: Bytes, content_type: Mime) -> Result; } -impl FromBody for T -{ +impl FromBody for T { type Err = serde_json::Error; - - fn from_body(body : Bytes, _content_type : Mime) -> Result - { + + fn from_body(body: Bytes, _content_type: Mime) -> Result { serde_json::from_slice(&body) } } - /** 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`]. @@ -108,19 +87,15 @@ struct RawImage { [`FromBody`]: trait.FromBody.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. - fn supported_types() -> Option> - { + fn supported_types() -> Option> { None } } -impl RequestBody for T -{ - fn supported_types() -> Option> - { +impl RequestBody for T { + fn supported_types() -> Option> { Some(vec![APPLICATION_JSON]) } } @@ -128,10 +103,6 @@ impl RequestBody for T /// 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 /// type `OpenapiType`. -pub trait ResourceID : ResourceType + DeserializeOwned + Clone + RefUnwindSafe + Send + Sync -{ -} +pub trait ResourceID: ResourceType + DeserializeOwned + Clone + RefUnwindSafe + Send + Sync {} -impl ResourceID for T -{ -} +impl ResourceID for T {} diff --git a/tests/async_methods.rs b/tests/async_methods.rs index 42cbc25..9de8465 100644 --- a/tests/async_methods.rs +++ b/tests/async_methods.rs @@ -1,16 +1,15 @@ -#[macro_use] extern crate gotham_derive; +#[macro_use] +extern crate gotham_derive; -use gotham::{ - router::builder::*, - test::TestServer -}; +use gotham::{router::builder::*, test::TestServer}; use gotham_restful::*; use mime::{APPLICATION_JSON, TEXT_PLAIN}; use serde::Deserialize; -mod util { include!("util/mod.rs"); } -use util::{test_get_response, test_post_response, test_put_response, test_delete_response}; - +mod util { + include!("util/mod.rs"); +} +use util::{test_delete_response, test_get_response, test_post_response, test_put_response}; #[derive(Resource)] #[resource(read_all, read, search, create, change_all, change, remove_all, remove)] @@ -19,88 +18,96 @@ struct FooResource; #[derive(Deserialize)] #[cfg_attr(feature = "openapi", derive(OpenapiType))] #[allow(dead_code)] -struct FooBody -{ - data : String +struct FooBody { + data: String } #[derive(Deserialize, StateData, StaticResponseExtender)] #[cfg_attr(feature = "openapi", derive(OpenapiType))] #[allow(dead_code)] -struct FooSearch -{ - query : String +struct FooSearch { + query: String } -const READ_ALL_RESPONSE : &[u8] = b"1ARwwSPVyOKpJKrYwqGgECPVWDl1BqajAAj7g7WJ3e"; +const READ_ALL_RESPONSE: &[u8] = b"1ARwwSPVyOKpJKrYwqGgECPVWDl1BqajAAj7g7WJ3e"; #[read_all(FooResource)] -async fn read_all() -> Raw<&'static [u8]> -{ +async fn read_all() -> Raw<&'static [u8]> { Raw::new(READ_ALL_RESPONSE, TEXT_PLAIN) } -const READ_RESPONSE : &[u8] = b"FEReHoeBKU17X2bBpVAd1iUvktFL43CDu0cFYHdaP9"; +const READ_RESPONSE: &[u8] = b"FEReHoeBKU17X2bBpVAd1iUvktFL43CDu0cFYHdaP9"; #[read(FooResource)] -async fn read(_id : u64) -> Raw<&'static [u8]> -{ +async fn read(_id: u64) -> Raw<&'static [u8]> { Raw::new(READ_RESPONSE, TEXT_PLAIN) } -const SEARCH_RESPONSE : &[u8] = b"AWqcQUdBRHXKh3at4u79mdupOAfEbnTcx71ogCVF0E"; +const SEARCH_RESPONSE: &[u8] = b"AWqcQUdBRHXKh3at4u79mdupOAfEbnTcx71ogCVF0E"; #[search(FooResource)] -async fn search(_body : FooSearch) -> Raw<&'static [u8]> -{ +async fn search(_body: FooSearch) -> Raw<&'static [u8]> { Raw::new(SEARCH_RESPONSE, TEXT_PLAIN) } -const CREATE_RESPONSE : &[u8] = b"y6POY7wOMAB0jBRBw0FJT7DOpUNbhmT8KdpQPLkI83"; +const CREATE_RESPONSE: &[u8] = b"y6POY7wOMAB0jBRBw0FJT7DOpUNbhmT8KdpQPLkI83"; #[create(FooResource)] -async fn create(_body : FooBody) -> Raw<&'static [u8]> -{ +async fn create(_body: FooBody) -> Raw<&'static [u8]> { Raw::new(CREATE_RESPONSE, TEXT_PLAIN) } -const CHANGE_ALL_RESPONSE : &[u8] = b"QlbYg8gHE9OQvvk3yKjXJLTSXlIrg9mcqhfMXJmQkv"; +const CHANGE_ALL_RESPONSE: &[u8] = b"QlbYg8gHE9OQvvk3yKjXJLTSXlIrg9mcqhfMXJmQkv"; #[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) } -const CHANGE_RESPONSE : &[u8] = b"qGod55RUXkT1lgPO8h0uVM6l368O2S0GrwENZFFuRu"; +const CHANGE_RESPONSE: &[u8] = b"qGod55RUXkT1lgPO8h0uVM6l368O2S0GrwENZFFuRu"; #[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) } -const REMOVE_ALL_RESPONSE : &[u8] = b"Y36kZ749MRk2Nem4BedJABOZiZWPLOtiwLfJlGTwm5"; +const REMOVE_ALL_RESPONSE: &[u8] = b"Y36kZ749MRk2Nem4BedJABOZiZWPLOtiwLfJlGTwm5"; #[remove_all(FooResource)] -async fn remove_all() -> Raw<&'static [u8]> -{ +async fn remove_all() -> Raw<&'static [u8]> { Raw::new(REMOVE_ALL_RESPONSE, TEXT_PLAIN) } -const REMOVE_RESPONSE : &[u8] = b"CwRzBrKErsVZ1N7yeNfjZuUn1MacvgBqk4uPOFfDDq"; +const REMOVE_RESPONSE: &[u8] = b"CwRzBrKErsVZ1N7yeNfjZuUn1MacvgBqk4uPOFfDDq"; #[remove(FooResource)] -async fn remove(_id : u64) -> Raw<&'static [u8]> -{ +async fn remove(_id: u64) -> Raw<&'static [u8]> { Raw::new(REMOVE_RESPONSE, TEXT_PLAIN) } #[test] -fn async_methods() -{ +fn async_methods() { let server = TestServer::new(build_simple_router(|router| { router.resource::("foo"); - })).unwrap(); - + })) + .unwrap(); + 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/search?query=hello+world", SEARCH_RESPONSE); - test_post_response(&server, "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_post_response( + &server, + "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/1", REMOVE_RESPONSE); } diff --git a/tests/cors_handling.rs b/tests/cors_handling.rs index 80ad346..35d5841 100644 --- a/tests/cors_handling.rs +++ b/tests/cors_handling.rs @@ -5,7 +5,7 @@ use gotham::{ router::builder::*, 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 mime::TEXT_PLAIN; @@ -14,70 +14,108 @@ use mime::TEXT_PLAIN; struct FooResource; #[read_all(FooResource)] -fn read_all() -{ -} +fn read_all() {} #[change_all(FooResource)] -fn change_all(_body : Raw>) -{ -} +fn change_all(_body: Raw>) {} -fn test_server(cfg : CorsConfig) -> TestServer -{ +fn test_server(cfg: CorsConfig) -> TestServer { let (chain, pipeline) = single_pipeline(new_pipeline().add(cfg).build()); - TestServer::new(build_router(chain, pipeline, |router| { - router.resource::("/foo") - })).unwrap() + TestServer::new(build_router(chain, pipeline, |router| router.resource::("/foo"))).unwrap() } -fn test_response(req : TestRequest, origin : Option<&str>, vary : Option<&str>, credentials : bool) +fn test_response(req: TestRequest, origin: Option<&str>, vary: Option<&str>, credentials: bool) where - TS : Server + 'static, - C : Connect + Clone + Send + Sync + 'static + TS: Server + '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); let headers = res.headers(); 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(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()); } -fn test_preflight(server : &TestServer, method : &str, origin : Option<&str>, vary : &str, credentials : bool, max_age : u64) -{ - let res = server.client().options("http://example.org/foo") +fn test_preflight(server: &TestServer, method: &str, origin: Option<&str>, vary: &str, credentials: bool, max_age: u64) { + let res = server + .client() + .options("http://example.org/foo") .with_header(ACCESS_CONTROL_REQUEST_METHOD, method.parse().unwrap()) .with_header(ORIGIN, "http://example.org".parse().unwrap()) - .perform().unwrap(); + .perform() + .unwrap(); assert_eq!(res.status(), StatusCode::NO_CONTENT); let headers = res.headers(); 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!(headers.get(ACCESS_CONTROL_ALLOW_ORIGIN).and_then(|value| value.to_str().ok()).as_deref(), origin); + assert_eq!( + 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(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)); + 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_MAX_AGE) + .and_then(|value| value.to_str().ok()) + .and_then(|value| value.parse().ok()), + Some(max_age) + ); } - #[test] -fn cors_origin_none() -{ +fn cors_origin_none() { let cfg = Default::default(); let server = test_server(cfg); 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().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] -fn cors_origin_star() -{ +fn cors_origin_star() { let cfg = CorsConfig { origin: Origin::Star, ..Default::default() @@ -85,44 +123,80 @@ fn cors_origin_star() let server = test_server(cfg); 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().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] -fn cors_origin_single() -{ +fn cors_origin_single() { let cfg = CorsConfig { origin: Origin::Single("https://foo.com".to_owned()), ..Default::default() }; let server = test_server(cfg); - 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(server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN), Some("https://foo.com"), None, false); + 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( + server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN), + Some("https://foo.com"), + None, + false + ); } #[test] -fn cors_origin_copy() -{ +fn cors_origin_copy() { let cfg = CorsConfig { origin: Origin::Copy, ..Default::default() }; let server = test_server(cfg); - 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(server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN), Some("http://example.org"), Some("Origin"), false); + 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( + server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN), + Some("http://example.org"), + Some("Origin"), + false + ); } #[test] -fn cors_credentials() -{ +fn cors_credentials() { let cfg = CorsConfig { origin: Origin::None, credentials: true, @@ -131,14 +205,18 @@ fn cors_credentials() let server = test_server(cfg); 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().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] -fn cors_max_age() -{ +fn cors_max_age() { let cfg = CorsConfig { origin: Origin::None, max_age: 31536000, @@ -147,7 +225,12 @@ fn cors_max_age() let server = test_server(cfg); 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().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 + ); } diff --git a/tests/custom_request_body.rs b/tests/custom_request_body.rs index 95fa748..2a5baeb 100644 --- a/tests/custom_request_body.rs +++ b/tests/custom_request_body.rs @@ -1,13 +1,8 @@ -use gotham::{ - hyper::header::CONTENT_TYPE, - router::builder::*, - test::TestServer -}; +use gotham::{hyper::header::CONTENT_TYPE, router::builder::*, test::TestServer}; use gotham_restful::*; 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)] #[resource(create)] @@ -21,23 +16,24 @@ struct Foo { } #[create(FooResource)] -fn create(body : Foo) -> Raw> { +fn create(body: Foo) -> Raw> { Raw::new(body.content, body.content_type) } - #[test] -fn custom_request_body() -{ +fn custom_request_body() { let server = TestServer::new(build_simple_router(|router| { router.resource::("foo"); - })).unwrap(); - - let res = server.client() + })) + .unwrap(); + + let res = server + .client() .post("http://localhost/foo", RESPONSE, TEXT_PLAIN) - .perform().unwrap(); + .perform() + .unwrap(); assert_eq!(res.headers().get(CONTENT_TYPE).unwrap().to_str().unwrap(), "text/plain"); let res = res.read_body().unwrap(); - let body : &[u8] = res.as_ref(); + let body: &[u8] = res.as_ref(); assert_eq!(body, RESPONSE); } diff --git a/tests/openapi_specification.rs b/tests/openapi_specification.rs index 2171526..c5411af 100644 --- a/tests/openapi_specification.rs +++ b/tests/openapi_specification.rs @@ -1,6 +1,7 @@ #![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 gotham::{ @@ -13,10 +14,11 @@ use mime::IMAGE_PNG; use serde::{Deserialize, Serialize}; #[allow(dead_code)] -mod util { include!("util/mod.rs"); } +mod util { + include!("util/mod.rs"); +} use util::{test_get_response, test_openapi_response}; - const IMAGE_RESPONSE : &[u8] = b"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUA/wA0XsCoAAAAAXRSTlN/gFy0ywAAAApJREFUeJxjYgAAAAYAAzY3fKgAAAAASUVORK5CYII="; #[derive(Resource)] @@ -28,71 +30,59 @@ struct ImageResource; struct Image(Vec); #[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()) } #[change(ImageResource, operation_id = "setImage")] -fn set_image(_id : u64, _image : Image) -{ -} - +fn set_image(_id: u64, _image: Image) {} #[derive(Resource)] #[resource(read, search)] struct SecretResource; #[derive(Deserialize, Clone)] -struct AuthData -{ - sub : String, - iat : u64, - exp : u64 +struct AuthData { + sub: String, + iat: u64, + exp: u64 } type AuthStatus = gotham_restful::AuthStatus; #[derive(OpenapiType, Serialize)] -struct Secret -{ - code : f32 +struct Secret { + code: f32 } #[derive(OpenapiType, Serialize)] -struct Secrets -{ - secrets : Vec +struct Secrets { + secrets: Vec } #[derive(Deserialize, OpenapiType, StateData, StaticResponseExtender)] -struct SecretQuery -{ - date : NaiveDate, - hour : Option, - minute : Option +struct SecretQuery { + date: NaiveDate, + hour: Option, + minute: Option } #[read(SecretResource)] -fn read_secret(auth : AuthStatus, _id : NaiveDateTime) -> AuthSuccess -{ +fn read_secret(auth: AuthStatus, _id: NaiveDateTime) -> AuthSuccess { auth.ok()?; Ok(Secret { code: 4.2 }) } #[search(SecretResource)] -fn search_secret(auth : AuthStatus, _query : SecretQuery) -> AuthSuccess -{ +fn search_secret(auth: AuthStatus, _query: SecretQuery) -> AuthSuccess { auth.ok()?; Ok(Secrets { secrets: vec![Secret { code: 4.2 }, Secret { code: 3.14 }] }) } - #[test] -fn openapi_supports_scope() -{ +fn openapi_supports_scope() { let info = OpenapiInfo { title: "This is just a test".to_owned(), version: "1.2.3".to_owned(), @@ -110,7 +100,8 @@ fn openapi_supports_scope() router.get_openapi("openapi"); router.resource::("secret"); }); - })).unwrap(); - + })) + .unwrap(); + test_openapi_response(&server, "http://localhost/openapi", "tests/openapi_specification.json"); } diff --git a/tests/openapi_supports_scope.rs b/tests/openapi_supports_scope.rs index d126bb8..8d8e298 100644 --- a/tests/openapi_supports_scope.rs +++ b/tests/openapi_supports_scope.rs @@ -1,32 +1,27 @@ #![cfg(feature = "openapi")] -use gotham::{ - router::builder::*, - test::TestServer -}; +use gotham::{router::builder::*, test::TestServer}; use gotham_restful::*; use mime::TEXT_PLAIN; #[allow(dead_code)] -mod util { include!("util/mod.rs"); } +mod util { + include!("util/mod.rs"); +} 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)] #[resource(read_all)] struct FooResource; #[read_all(FooResource)] -fn read_all() -> Raw<&'static [u8]> -{ +fn read_all() -> Raw<&'static [u8]> { Raw::new(RESPONSE, TEXT_PLAIN) } - #[test] -fn openapi_supports_scope() -{ +fn openapi_supports_scope() { let info = OpenapiInfo { title: "Test".to_owned(), version: "1.2.3".to_owned(), @@ -44,8 +39,9 @@ fn openapi_supports_scope() }); router.resource::("foo4"); }); - })).unwrap(); - + })) + .unwrap(); + test_get_response(&server, "http://localhost/foo1", RESPONSE); test_get_response(&server, "http://localhost/bar/foo2", RESPONSE); test_get_response(&server, "http://localhost/bar/baz/foo3", RESPONSE); diff --git a/tests/sync_methods.rs b/tests/sync_methods.rs index a13ec19..211f5f3 100644 --- a/tests/sync_methods.rs +++ b/tests/sync_methods.rs @@ -1,16 +1,15 @@ -#[macro_use] extern crate gotham_derive; +#[macro_use] +extern crate gotham_derive; -use gotham::{ - router::builder::*, - test::TestServer -}; +use gotham::{router::builder::*, test::TestServer}; use gotham_restful::*; use mime::{APPLICATION_JSON, TEXT_PLAIN}; use serde::Deserialize; -mod util { include!("util/mod.rs"); } -use util::{test_get_response, test_post_response, test_put_response, test_delete_response}; - +mod util { + include!("util/mod.rs"); +} +use util::{test_delete_response, test_get_response, test_post_response, test_put_response}; #[derive(Resource)] #[resource(read_all, read, search, create, change_all, change, remove_all, remove)] @@ -19,88 +18,96 @@ struct FooResource; #[derive(Deserialize)] #[cfg_attr(feature = "openapi", derive(OpenapiType))] #[allow(dead_code)] -struct FooBody -{ - data : String +struct FooBody { + data: String } #[derive(Deserialize, StateData, StaticResponseExtender)] #[cfg_attr(feature = "openapi", derive(OpenapiType))] #[allow(dead_code)] -struct FooSearch -{ - query : String +struct FooSearch { + query: String } -const READ_ALL_RESPONSE : &[u8] = b"1ARwwSPVyOKpJKrYwqGgECPVWDl1BqajAAj7g7WJ3e"; +const READ_ALL_RESPONSE: &[u8] = b"1ARwwSPVyOKpJKrYwqGgECPVWDl1BqajAAj7g7WJ3e"; #[read_all(FooResource)] -fn read_all() -> Raw<&'static [u8]> -{ +fn read_all() -> Raw<&'static [u8]> { Raw::new(READ_ALL_RESPONSE, TEXT_PLAIN) } -const READ_RESPONSE : &[u8] = b"FEReHoeBKU17X2bBpVAd1iUvktFL43CDu0cFYHdaP9"; +const READ_RESPONSE: &[u8] = b"FEReHoeBKU17X2bBpVAd1iUvktFL43CDu0cFYHdaP9"; #[read(FooResource)] -fn read(_id : u64) -> Raw<&'static [u8]> -{ +fn read(_id: u64) -> Raw<&'static [u8]> { Raw::new(READ_RESPONSE, TEXT_PLAIN) } -const SEARCH_RESPONSE : &[u8] = b"AWqcQUdBRHXKh3at4u79mdupOAfEbnTcx71ogCVF0E"; +const SEARCH_RESPONSE: &[u8] = b"AWqcQUdBRHXKh3at4u79mdupOAfEbnTcx71ogCVF0E"; #[search(FooResource)] -fn search(_body : FooSearch) -> Raw<&'static [u8]> -{ +fn search(_body: FooSearch) -> Raw<&'static [u8]> { Raw::new(SEARCH_RESPONSE, TEXT_PLAIN) } -const CREATE_RESPONSE : &[u8] = b"y6POY7wOMAB0jBRBw0FJT7DOpUNbhmT8KdpQPLkI83"; +const CREATE_RESPONSE: &[u8] = b"y6POY7wOMAB0jBRBw0FJT7DOpUNbhmT8KdpQPLkI83"; #[create(FooResource)] -fn create(_body : FooBody) -> Raw<&'static [u8]> -{ +fn create(_body: FooBody) -> Raw<&'static [u8]> { Raw::new(CREATE_RESPONSE, TEXT_PLAIN) } -const CHANGE_ALL_RESPONSE : &[u8] = b"QlbYg8gHE9OQvvk3yKjXJLTSXlIrg9mcqhfMXJmQkv"; +const CHANGE_ALL_RESPONSE: &[u8] = b"QlbYg8gHE9OQvvk3yKjXJLTSXlIrg9mcqhfMXJmQkv"; #[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) } -const CHANGE_RESPONSE : &[u8] = b"qGod55RUXkT1lgPO8h0uVM6l368O2S0GrwENZFFuRu"; +const CHANGE_RESPONSE: &[u8] = b"qGod55RUXkT1lgPO8h0uVM6l368O2S0GrwENZFFuRu"; #[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) } -const REMOVE_ALL_RESPONSE : &[u8] = b"Y36kZ749MRk2Nem4BedJABOZiZWPLOtiwLfJlGTwm5"; +const REMOVE_ALL_RESPONSE: &[u8] = b"Y36kZ749MRk2Nem4BedJABOZiZWPLOtiwLfJlGTwm5"; #[remove_all(FooResource)] -fn remove_all() -> Raw<&'static [u8]> -{ +fn remove_all() -> Raw<&'static [u8]> { Raw::new(REMOVE_ALL_RESPONSE, TEXT_PLAIN) } -const REMOVE_RESPONSE : &[u8] = b"CwRzBrKErsVZ1N7yeNfjZuUn1MacvgBqk4uPOFfDDq"; +const REMOVE_RESPONSE: &[u8] = b"CwRzBrKErsVZ1N7yeNfjZuUn1MacvgBqk4uPOFfDDq"; #[remove(FooResource)] -fn remove(_id : u64) -> Raw<&'static [u8]> -{ +fn remove(_id: u64) -> Raw<&'static [u8]> { Raw::new(REMOVE_RESPONSE, TEXT_PLAIN) } #[test] -fn sync_methods() -{ +fn sync_methods() { let server = TestServer::new(build_simple_router(|router| { router.resource::("foo"); - })).unwrap(); - + })) + .unwrap(); + 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/search?query=hello+world", SEARCH_RESPONSE); - test_post_response(&server, "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_post_response( + &server, + "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/1", REMOVE_RESPONSE); } diff --git a/tests/trybuild_ui.rs b/tests/trybuild_ui.rs index b5a572a..8f2d20b 100644 --- a/tests/trybuild_ui.rs +++ b/tests/trybuild_ui.rs @@ -2,10 +2,9 @@ use trybuild::TestCases; #[test] #[ignore] -fn trybuild_ui() -{ +fn trybuild_ui() { let t = TestCases::new(); - + // always enabled t.compile_fail("tests/ui/from_body_enum.rs"); t.compile_fail("tests/ui/method_async_state.rs"); @@ -16,10 +15,9 @@ fn trybuild_ui() t.compile_fail("tests/ui/method_too_many_args.rs"); t.compile_fail("tests/ui/method_unsafe.rs"); t.compile_fail("tests/ui/resource_unknown_method.rs"); - + // 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_nullable_non_bool.rs"); t.compile_fail("tests/ui/openapi_type_rename_non_string.rs"); From 38feb5b781c1869a87406a1cf4186ad433ec9a26 Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 15 Sep 2020 15:12:11 +0200 Subject: [PATCH 085/170] oops --- Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 61682b0..88409a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,6 +56,5 @@ openapi = ["gotham_restful_derive/openapi", "indexmap", "openapiv3"] all-features = true [patch.crates-io] -gotham = { path = "../gotham/gotham" } gotham_restful = { path = "." } gotham_restful_derive = { path = "./derive" } From 31835fe57f49426b226242217838871508c74722 Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 15 Sep 2020 15:20:04 +0200 Subject: [PATCH 086/170] use upstream Access-Control-Request-Method route matcher --- src/cors.rs | 3 +- src/lib.rs | 2 - src/matcher/access_control_request_method.rs | 98 -------------------- src/matcher/mod.rs | 4 - 4 files changed, 1 insertion(+), 106 deletions(-) delete mode 100644 src/matcher/access_control_request_method.rs delete mode 100644 src/matcher/mod.rs diff --git a/src/cors.rs b/src/cors.rs index c17e28e..9b0c802 100644 --- a/src/cors.rs +++ b/src/cors.rs @@ -1,4 +1,3 @@ -use crate::matcher::AccessControlRequestMethodMatcher; use gotham::{ handler::HandlerFuture, helpers::http::response::create_empty_response, @@ -12,7 +11,7 @@ use gotham::{ }, middleware::Middleware, pipeline::chain::PipelineHandleChain, - router::builder::*, + router::{builder::*, route::matcher::AccessControlRequestMethodMatcher}, state::{FromState, State} }; use itertools::Itertools; diff --git a/src/lib.rs b/src/lib.rs index ab08c98..92bf691 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -415,8 +415,6 @@ mod cors; #[cfg(feature = "cors")] pub use cors::{handle_cors, CorsConfig, CorsRoute, Origin}; -pub mod matcher; - #[cfg(feature = "openapi")] mod openapi; #[cfg(feature = "openapi")] diff --git a/src/matcher/access_control_request_method.rs b/src/matcher/access_control_request_method.rs deleted file mode 100644 index 7668e37..0000000 --- a/src/matcher/access_control_request_method.rs +++ /dev/null @@ -1,98 +0,0 @@ -use gotham::{ - hyper::{ - header::{HeaderMap, ACCESS_CONTROL_REQUEST_METHOD}, - Method, StatusCode - }, - router::{non_match::RouteNonMatch, route::matcher::RouteMatcher}, - state::{FromState, State} -}; - -/// A route matcher that checks whether the value of the `Access-Control-Request-Method` header matches the defined value. -/// -/// Usage: -/// -/// ```rust -/// # use gotham::{helpers::http::response::create_empty_response, -/// # hyper::{header::ACCESS_CONTROL_ALLOW_METHODS, Method, StatusCode}, -/// # router::builder::* -/// # }; -/// # use gotham_restful::matcher::AccessControlRequestMethodMatcher; -/// let matcher = AccessControlRequestMethodMatcher::new(Method::PUT); -/// -/// # build_simple_router(|route| { -/// // use the matcher for your request -/// route.options("/foo").extend_route_matcher(matcher).to(|state| { -/// // we know that this is a CORS preflight for a PUT request -/// let mut res = create_empty_response(&state, StatusCode::NO_CONTENT); -/// res.headers_mut().insert(ACCESS_CONTROL_ALLOW_METHODS, "PUT".parse().unwrap()); -/// (state, res) -/// }); -/// # }); -/// ``` -#[derive(Clone, Debug)] -pub struct AccessControlRequestMethodMatcher { - method: Method -} - -impl AccessControlRequestMethodMatcher { - /// 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, - /// 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. - pub fn new(method: Method) -> Self { - Self { method } - } -} - -impl RouteMatcher for AccessControlRequestMethodMatcher { - fn is_match(&self, state: &State) -> Result<(), RouteNonMatch> { - // according to the fetch specification, methods should be normalized by byte-uppercase - // https://fetch.spec.whatwg.org/#concept-method - match HeaderMap::borrow_from(state) - .get(ACCESS_CONTROL_REQUEST_METHOD) - .and_then(|value| value.to_str().ok()) - .and_then(|str| str.to_ascii_uppercase().parse::().ok()) - { - Some(m) if m == self.method => Ok(()), - _ => Err(RouteNonMatch::new(StatusCode::NOT_FOUND)) - } - } -} - -#[cfg(test)] -mod test { - use super::*; - - fn with_state(accept: Option<&str>, block: F) - where - F: FnOnce(&mut State) -> () - { - State::with_new(|state| { - let mut headers = HeaderMap::new(); - if let Some(acc) = accept { - headers.insert(ACCESS_CONTROL_REQUEST_METHOD, acc.parse().unwrap()); - } - state.put(headers); - block(state); - }); - } - - #[test] - fn no_acrm_header() { - let matcher = AccessControlRequestMethodMatcher::new(Method::PUT); - with_state(None, |state| assert!(matcher.is_match(&state).is_err())); - } - - #[test] - fn correct_acrm_header() { - 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())); - } - - #[test] - fn incorrect_acrm_header() { - let matcher = AccessControlRequestMethodMatcher::new(Method::PUT); - with_state(Some("DELETE"), |state| assert!(matcher.is_match(&state).is_err())); - } -} diff --git a/src/matcher/mod.rs b/src/matcher/mod.rs deleted file mode 100644 index 153bdfe..0000000 --- a/src/matcher/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -#[cfg(feature = "cors")] -mod access_control_request_method; -#[cfg(feature = "cors")] -pub use access_control_request_method::AccessControlRequestMethodMatcher; From 6470bd59ef2b8c359a952c069ee6d6cf59d1131f Mon Sep 17 00:00:00 2001 From: Dominic Date: Thu, 17 Sep 2020 12:21:09 +0200 Subject: [PATCH 087/170] fix weird rust issue where PartialEq is not being detected since 1.44.0 I did not manage to get a repro of this since all old versions of rust fail to compile, eventhough the CI clearly confirms that it used to compile with 1.43.0 --- derive/src/openapi_type.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/derive/src/openapi_type.rs b/derive/src/openapi_type.rs index 58965ba..b3f2d36 100644 --- a/derive/src/openapi_type.rs +++ b/derive/src/openapi_type.rs @@ -84,7 +84,7 @@ fn parse_attributes(input: &[Attribute]) -> Result { } fn expand_variant(variant: &Variant) -> Result { - if variant.fields != Fields::Unit { + if !matches!(variant.fields, Fields::Unit) { return Err(Error::new( variant.span(), "#[derive(OpenapiType)] does not support enum variants with fields" From 0729d5b8ed0ad81204278e4be84ef5be25a50691 Mon Sep 17 00:00:00 2001 From: Dominic Date: Thu, 17 Sep 2020 12:25:58 +0200 Subject: [PATCH 088/170] use anyhow --- src/auth.rs | 3 ++- src/openapi/handler.rs | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index e4896e3..90bb40d 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -5,6 +5,7 @@ use futures_util::{ future::{FutureExt, TryFutureExt} }; use gotham::{ + anyhow, handler::HandlerFuture, hyper::header::{HeaderMap, AUTHORIZATION}, middleware::{Middleware, NewMiddleware}, @@ -280,7 +281,7 @@ where { type Instance = Self; - fn new_middleware(&self) -> Result { + fn new_middleware(&self) -> anyhow::Result { let c: Self = self.clone(); Ok(c) } diff --git a/src/openapi/handler.rs b/src/openapi/handler.rs index 359f1f3..6c321b8 100644 --- a/src/openapi/handler.rs +++ b/src/openapi/handler.rs @@ -1,7 +1,7 @@ use super::SECURITY_NAME; use futures_util::{future, future::FutureExt}; use gotham::{ - error::Result, + anyhow, handler::{Handler, HandlerFuture, NewHandler}, helpers::http::response::create_response, state::State @@ -28,7 +28,7 @@ impl OpenapiHandler { impl NewHandler for OpenapiHandler { type Instance = Self; - fn new_handler(&self) -> Result { + fn new_handler(&self) -> anyhow::Result { Ok(self.clone()) } } From eeea62859f56a7adff030ba6069e3e18c5844c2e Mon Sep 17 00:00:00 2001 From: Dominic Date: Sat, 19 Sep 2020 19:31:43 +0200 Subject: [PATCH 089/170] update log4rs to 0.13.0 --- example/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/Cargo.toml b/example/Cargo.toml index de21cba..d65544d 100644 --- a/example/Cargo.toml +++ b/example/Cargo.toml @@ -19,5 +19,5 @@ gotham = { version = "0.5.0-rc.1", default-features = false } gotham_derive = "0.5.0-rc.1" gotham_restful = { version = "0.1.0-rc0", features = ["auth", "openapi"] } log = "0.4.8" -log4rs = { version = "0.12.0", features = ["console_appender"], default-features = false } +log4rs = { version = "0.13.0", features = ["console_appender"], default-features = false } serde = "1.0.110" From d9c22579b06d38e7dcad84ffbb632eb34b03cba0 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sat, 19 Sep 2020 19:40:39 +0200 Subject: [PATCH 090/170] only log errors on 500 responses (fixes #30) --- src/lib.rs | 3 +++ src/result/mod.rs | 13 +++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 92bf691..a45dc83 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -338,11 +338,14 @@ openapi = ["gotham-restful/openapi"] into your libraries `Cargo.toml` and use the following for all types used with handlers: ``` +# #[cfg(feature = "openapi")] +# mod openapi_feature_enabled { # use gotham_restful::OpenapiType; # use serde::{Deserialize, Serialize}; #[derive(Deserialize, Serialize)] #[cfg_attr(feature = "openapi", derive(OpenapiType))] struct Foo; +# } ``` # Examples diff --git a/src/result/mod.rs b/src/result/mod.rs index 100ba55..2c79d37 100644 --- a/src/result/mod.rs +++ b/src/result/mod.rs @@ -104,8 +104,17 @@ where E: Display + IntoResponseError { into_response_helper(|| { - errorlog(&e); - e.into_response_error() + let msg = e.to_string(); + let res = e.into_response_error(); + match &res { + Ok(res) if res.status.is_server_error() => errorlog(msg), + Err(err) => { + errorlog(msg); + errorlog(&err); + }, + _ => {} + }; + res }) } From a0059fd7b9cb74bf9d26d50c195da58aea15685b Mon Sep 17 00:00:00 2001 From: Dominic Date: Fri, 2 Oct 2020 13:37:33 +0200 Subject: [PATCH 091/170] some dependency updates --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 88409a0..e060801 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,8 +18,8 @@ repository = "https://gitlab.com/msrd0/gotham-restful" gitlab = { repository = "msrd0/gotham-restful", branch = "master" } [dependencies] -base64 = { version = "0.12.1", optional = true } -chrono = { version = "0.4.11", features = ["serde"], optional = true } +base64 = { version = "0.13.0", optional = true } +chrono = { version = "0.4.19", features = ["serde"], optional = true } cookie = { version = "0.14", optional = true } futures-core = "0.3.5" futures-util = "0.3.5" @@ -34,7 +34,7 @@ log = "0.4.8" mime = "0.3.16" openapiv3 = { version = "0.3.2", optional = true } serde = { version = "1.0.110", features = ["derive"] } -serde_json = "1.0.53" +serde_json = "1.0.58" uuid = { version = "0.8.1", optional = true } [dev-dependencies] From 1369d25c8a9857357c2a56e8886f7c812d6b879c Mon Sep 17 00:00:00 2001 From: Dominic Date: Fri, 2 Oct 2020 14:21:56 +0200 Subject: [PATCH 092/170] bump version to 0.1.0 --- Cargo.toml | 4 ++-- derive/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e060801..92d005a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ members = ["derive", "example"] [package] name = "gotham_restful" -version = "0.1.0-rc0" +version = "0.1.0" authors = ["Dominic Meiser "] edition = "2018" description = "RESTful additions for the gotham web framework" @@ -26,7 +26,7 @@ futures-util = "0.3.5" gotham = { version = "0.5.0", default-features = false } gotham_derive = "0.5.0" gotham_middleware_diesel = { version = "0.2.0", optional = true } -gotham_restful_derive = { version = "0.1.0-rc0" } +gotham_restful_derive = { version = "0.1.0" } indexmap = { version = "1.3.2", optional = true } itertools = "0.9.0" jsonwebtoken = { version = "7.1.0", optional = true } diff --git a/derive/Cargo.toml b/derive/Cargo.toml index d293eb4..f238f10 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "gotham_restful_derive" -version = "0.1.0-rc0" +version = "0.1.0" authors = ["Dominic Meiser "] edition = "2018" description = "RESTful additions for the gotham web framework - Derive" From d3da7d018247e9b839627634a162516ce31c4b89 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 4 Oct 2020 18:25:02 +0200 Subject: [PATCH 093/170] remove publish from ci --- .gitlab-ci.yml | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index fb7c703..f4a69ea 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -87,19 +87,3 @@ pages: - public only: - master - -publish: - stage: publish - image: msrd0/rust:alpine - before_script: - - cargo -V - - cargo login $CRATES_IO_TOKEN - script: - - cd gotham_restful_derive - - cargo publish - - sleep 1m - - cd ../gotham_restful - - cargo publish - - cd .. - only: - - tags From 6ded517824a64d78df573d6898076900721fca97 Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 4 Nov 2020 19:34:25 +0100 Subject: [PATCH 094/170] update futures to 0.3.7 (https://rustsec.org/advisories/RUSTSEC-2020-0059.html) --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 92d005a..96b9f52 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,8 +21,8 @@ gitlab = { repository = "msrd0/gotham-restful", branch = "master" } base64 = { version = "0.13.0", optional = true } chrono = { version = "0.4.19", features = ["serde"], optional = true } cookie = { version = "0.14", optional = true } -futures-core = "0.3.5" -futures-util = "0.3.5" +futures-core = "0.3.7" +futures-util = "0.3.7" gotham = { version = "0.5.0", default-features = false } gotham_derive = "0.5.0" gotham_middleware_diesel = { version = "0.2.0", optional = true } From ce570c4787c6746b7b61d7d27e9c907ecbd5bf0d Mon Sep 17 00:00:00 2001 From: Dominic Date: Fri, 20 Nov 2020 01:12:20 +0100 Subject: [PATCH 095/170] allow &mut State in async fn (needs test) --- derive/src/method.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/derive/src/method.rs b/derive/src/method.rs index dc14359..a791637 100644 --- a/derive/src/method.rs +++ b/derive/src/method.rs @@ -364,10 +364,10 @@ pub fn expand_method(method: Method, mut attrs: AttributeArgs, fun: ItemFn) -> R let mut block = quote!(#fun_ident(#(#args_pass),*)); let mut state_block = quote!(); if fun_is_async { - if let Some(arg) = args.iter().find(|arg| (*arg).ty.is_state_ref()) { + if let Some(arg) = args.iter().find(|arg| matches!((*arg).ty, MethodArgumentType::StateRef)) { return Err(Error::new( arg.span(), - "async fn must not take &State as an argument as State is not Sync, consider boxing" + "async fn must not take &State as an argument as State is not Sync, consider taking &mut State" )); } block = quote!(#block.await); From 24893223b202061e09049aed75a8d1e255dbff39 Mon Sep 17 00:00:00 2001 From: Dominic Date: Fri, 20 Nov 2020 01:30:12 +0100 Subject: [PATCH 096/170] remove unused code and fix lint warning --- derive/src/method.rs | 4 ---- src/lib.rs | 2 +- tests/ui/method_async_state.stderr | 2 +- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/derive/src/method.rs b/derive/src/method.rs index a791637..f892734 100644 --- a/derive/src/method.rs +++ b/derive/src/method.rs @@ -110,10 +110,6 @@ enum MethodArgumentType { } impl MethodArgumentType { - fn is_state_ref(&self) -> bool { - matches!(self, Self::StateRef | Self::StateMutRef) - } - fn is_method_arg(&self) -> bool { matches!(self, Self::MethodArg(_)) } diff --git a/src/lib.rs b/src/lib.rs index a45dc83..c030d37 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,6 @@ #![allow(clippy::tabs_in_doc_comments)] #![warn(missing_debug_implementations, rust_2018_idioms)] -#![deny(intra_doc_link_resolution_failure)] +#![deny(broken_intra_doc_links)] /*! This crate is an extension to the popular [gotham web framework][gotham] for Rust. It allows you to create resources with assigned methods that aim to be a more convenient way of creating handlers diff --git a/tests/ui/method_async_state.stderr b/tests/ui/method_async_state.stderr index 5c02836..4581a45 100644 --- a/tests/ui/method_async_state.stderr +++ b/tests/ui/method_async_state.stderr @@ -1,4 +1,4 @@ -error: async fn must not take &State as an argument as State is not Sync, consider boxing +error: async fn must not take &State as an argument as State is not Sync, consider taking &mut State --> $DIR/method_async_state.rs:9:19 | 9 | async fn read_all(state : &State) From e6721275ae471439b68d2ec6ce89346b7bb2373c Mon Sep 17 00:00:00 2001 From: Dominic Date: Sat, 21 Nov 2020 02:07:26 +0100 Subject: [PATCH 097/170] ci: remove 'cargo sweep' --- .gitlab-ci.yml | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f4a69ea..eb3eb56 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -9,16 +9,13 @@ variables: test-default: stage: test - image: msrd0/rust:alpine-sweep + image: msrd0/rust:alpine before_script: - cargo -V - - cargo sweep -s script: - cargo test --workspace --doc - cargo test --workspace --tests - cargo test --workspace --tests -- --ignored - after_script: - - cargo sweep -f cache: key: cargo-default paths: @@ -27,17 +24,14 @@ test-default: test-all: stage: test - image: msrd0/rust:alpine-tarpaulin-sweep + image: msrd0/rust:alpine-tarpaulin before_script: - apk add --no-cache postgresql-dev - cargo -V - - cargo sweep -s script: - cargo test --workspace --all-features --doc - cargo test --workspace --tests -- --ignored - cargo tarpaulin --target-dir target/tarpaulin --all --all-features --exclude-files 'cargo/*' --exclude-files 'derive/*' --exclude-files 'example/*' --ignore-panics --ignore-tests --out Html -v - after_script: - - cargo sweep -f artifacts: paths: - tarpaulin-report.html @@ -56,15 +50,12 @@ readme: doc: stage: build - image: msrd0/rust:alpine-sweep + image: msrd0/rust:alpine before_script: - cargo -V - - cargo sweep -s script: - cargo doc --all-features - echo 'The documentation is located here' >target/doc/index.html - after_script: - - cargo sweep -f artifacts: paths: - target/doc/ From 37aa497e598e0f4114f682514755723e63262c6b Mon Sep 17 00:00:00 2001 From: msrd0 <1182023-msrd0@users.noreply.gitlab.com> Date: Sat, 21 Nov 2020 14:47:29 +0000 Subject: [PATCH 098/170] Update CI to use the rust Docker image --- .gitlab-ci.yml | 58 ++++++++++++++++++------- tests/ui/method_async_state.stderr | 4 +- tests/ui/method_no_resource.stderr | 4 +- tests/ui/method_self.stderr | 4 +- tests/ui/method_too_few_args.stderr | 4 +- tests/ui/method_too_many_args.stderr | 4 +- tests/ui/method_unsafe.stderr | 4 +- tests/ui/resource_unknown_method.stderr | 4 +- 8 files changed, 56 insertions(+), 30 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index eb3eb56..771a4a3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -9,34 +9,62 @@ variables: test-default: stage: test - image: msrd0/rust:alpine + image: rust:1.42-slim before_script: - cargo -V script: - - cargo test --workspace --doc - - cargo test --workspace --tests - - cargo test --workspace --tests -- --ignored + - cargo test --workspace cache: - key: cargo-default + key: cargo-1-42-default paths: - cargo/ - target/ test-all: stage: test - image: msrd0/rust:alpine-tarpaulin + image: rust:1.42-slim before_script: - - apk add --no-cache postgresql-dev + - apt update -y + - apt install -y --no-install-recommends libpq-dev - cargo -V script: - - cargo test --workspace --all-features --doc - - cargo test --workspace --tests -- --ignored + - cargo test --workspace --all-features + cache: + key: cargo-1-42-all + paths: + - cargo/ + - target/ + +test-tarpaulin: + stage: test + image: rust:slim + before_script: + - apt update -y + - apt install -y --no-install-recommends libpq-dev libssl-dev pkgconf + - cargo -V + - cargo install cargo-tarpaulin + script: - cargo tarpaulin --target-dir target/tarpaulin --all --all-features --exclude-files 'cargo/*' --exclude-files 'derive/*' --exclude-files 'example/*' --ignore-panics --ignore-tests --out Html -v artifacts: paths: - tarpaulin-report.html cache: - key: cargo-all + key: cargo-stable-all + paths: + - cargo/ + - target/ + +test-trybuild-ui: + stage: test + image: rust:1.48-slim + before_script: + - apt update -y + - apt install -y --no-install-recommends libpq-dev + - cargo -V + script: + - cargo test --workspace --all-features --tests -- --ignored + cache: + key: cargo-1-48-all paths: - cargo/ - target/ @@ -50,29 +78,27 @@ readme: doc: stage: build - image: msrd0/rust:alpine + image: rust:slim before_script: - cargo -V script: - cargo doc --all-features - - echo 'The documentation is located here' >target/doc/index.html artifacts: paths: - target/doc/ cache: - key: cargo-doc + key: cargo-stable-doc paths: - cargo/ - target/ - only: - - master pages: stage: publish image: busybox script: - - mv target/doc public - mv tarpaulin-report.html public/coverage.html + - mv target/doc public + - echo 'The documentation is located here' >public/index.html artifacts: paths: - public diff --git a/tests/ui/method_async_state.stderr b/tests/ui/method_async_state.stderr index 4581a45..f66a260 100644 --- a/tests/ui/method_async_state.stderr +++ b/tests/ui/method_async_state.stderr @@ -4,11 +4,11 @@ error: async fn must not take &State as an argument as State is not Sync, consid 9 | async fn read_all(state : &State) | ^^^^^ -error[E0433]: failed to resolve: use of undeclared type or module `_gotham_restful_resource_foo_resource_method_read_all` +error[E0433]: failed to resolve: use of undeclared crate or module `_gotham_restful_resource_foo_resource_method_read_all` --> $DIR/method_async_state.rs:4:10 | 4 | #[derive(Resource)] - | ^^^^^^^^ use of undeclared type or module `_gotham_restful_resource_foo_resource_method_read_all` + | ^^^^^^^^ use of undeclared crate or module `_gotham_restful_resource_foo_resource_method_read_all` | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/method_no_resource.stderr b/tests/ui/method_no_resource.stderr index d4bc1c4..d5c5521 100644 --- a/tests/ui/method_no_resource.stderr +++ b/tests/ui/method_no_resource.stderr @@ -6,10 +6,10 @@ error: Missing Resource struct. Example: #[read_all(MyResource)] | = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info) -error[E0433]: failed to resolve: use of undeclared type or module `_gotham_restful_resource_foo_resource_method_read_all` +error[E0433]: failed to resolve: use of undeclared crate or module `_gotham_restful_resource_foo_resource_method_read_all` --> $DIR/method_no_resource.rs:3:10 | 3 | #[derive(Resource)] - | ^^^^^^^^ use of undeclared type or module `_gotham_restful_resource_foo_resource_method_read_all` + | ^^^^^^^^ use of undeclared crate or module `_gotham_restful_resource_foo_resource_method_read_all` | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/method_self.stderr b/tests/ui/method_self.stderr index d4fea5f..0e4b492 100644 --- a/tests/ui/method_self.stderr +++ b/tests/ui/method_self.stderr @@ -4,10 +4,10 @@ error: Didn't expect self parameter 8 | fn read_all(self) | ^^^^ -error[E0433]: failed to resolve: use of undeclared type or module `_gotham_restful_resource_foo_resource_method_read_all` +error[E0433]: failed to resolve: use of undeclared crate or module `_gotham_restful_resource_foo_resource_method_read_all` --> $DIR/method_self.rs:3:10 | 3 | #[derive(Resource)] - | ^^^^^^^^ use of undeclared type or module `_gotham_restful_resource_foo_resource_method_read_all` + | ^^^^^^^^ use of undeclared crate or module `_gotham_restful_resource_foo_resource_method_read_all` | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/method_too_few_args.stderr b/tests/ui/method_too_few_args.stderr index d8daeab..0aed829 100644 --- a/tests/ui/method_too_few_args.stderr +++ b/tests/ui/method_too_few_args.stderr @@ -4,10 +4,10 @@ error: Too few arguments 8 | fn read() | ^^^^ -error[E0433]: failed to resolve: use of undeclared type or module `_gotham_restful_resource_foo_resource_method_read` +error[E0433]: failed to resolve: use of undeclared crate or module `_gotham_restful_resource_foo_resource_method_read` --> $DIR/method_too_few_args.rs:3:10 | 3 | #[derive(Resource)] - | ^^^^^^^^ use of undeclared type or module `_gotham_restful_resource_foo_resource_method_read` + | ^^^^^^^^ use of undeclared crate or module `_gotham_restful_resource_foo_resource_method_read` | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/method_too_many_args.stderr b/tests/ui/method_too_many_args.stderr index 3f8bd39..ad8a37b 100644 --- a/tests/ui/method_too_many_args.stderr +++ b/tests/ui/method_too_many_args.stderr @@ -4,10 +4,10 @@ error: Too many arguments 8 | fn read_all(_id : u64) | ^^^ -error[E0433]: failed to resolve: use of undeclared type or module `_gotham_restful_resource_foo_resource_method_read_all` +error[E0433]: failed to resolve: use of undeclared crate or module `_gotham_restful_resource_foo_resource_method_read_all` --> $DIR/method_too_many_args.rs:3:10 | 3 | #[derive(Resource)] - | ^^^^^^^^ use of undeclared type or module `_gotham_restful_resource_foo_resource_method_read_all` + | ^^^^^^^^ use of undeclared crate or module `_gotham_restful_resource_foo_resource_method_read_all` | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/method_unsafe.stderr b/tests/ui/method_unsafe.stderr index aeb104e..ad42326 100644 --- a/tests/ui/method_unsafe.stderr +++ b/tests/ui/method_unsafe.stderr @@ -4,10 +4,10 @@ error: Resource methods must not be unsafe 8 | unsafe fn read_all() | ^^^^^^ -error[E0433]: failed to resolve: use of undeclared type or module `_gotham_restful_resource_foo_resource_method_read_all` +error[E0433]: failed to resolve: use of undeclared crate or module `_gotham_restful_resource_foo_resource_method_read_all` --> $DIR/method_unsafe.rs:3:10 | 3 | #[derive(Resource)] - | ^^^^^^^^ use of undeclared type or module `_gotham_restful_resource_foo_resource_method_read_all` + | ^^^^^^^^ use of undeclared crate or module `_gotham_restful_resource_foo_resource_method_read_all` | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/resource_unknown_method.stderr b/tests/ui/resource_unknown_method.stderr index 3282dbe..61269c2 100644 --- a/tests/ui/resource_unknown_method.stderr +++ b/tests/ui/resource_unknown_method.stderr @@ -4,11 +4,11 @@ error: Unknown method: `read_any' 4 | #[resource(read_any)] | ^^^^^^^^ -error[E0277]: the trait bound `FooResource: gotham_restful::Resource` is not satisfied +error[E0277]: the trait bound `FooResource: Resource` is not satisfied --> $DIR/resource_unknown_method.rs:7:1 | 7 | #[read_all(FooResource)] - | ^^^^^^^^^^^^^^^^^^^^^^^^ the trait `gotham_restful::Resource` is not implemented for `FooResource` + | ^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Resource` is not implemented for `FooResource` | = help: see issue #48214 = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info) From 00ffe953548105304dc1d94ee52ecdc56ed89857 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sat, 21 Nov 2020 16:39:35 +0100 Subject: [PATCH 099/170] fix broken doc links --- src/cors.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cors.rs b/src/cors.rs index 9b0c802..717bbf6 100644 --- a/src/cors.rs +++ b/src/cors.rs @@ -140,13 +140,13 @@ impl Middleware for CorsConfig { /** Handle CORS for a non-preflight request. This means manipulating the `res` HTTP headers so that -the response is aligned with the `state`'s [`CorsConfig`]. +the response is aligned with the `state`'s [CorsConfig]. -If you are using the [`Resource`] type (which is the recommended way), you'll never have to call +If you are using the [Resource](crate::Resource) type (which is the recommended way), you'll never have to call this method. However, if you are writing your own handler method, you might want to call this after your request to add the required CORS headers. -For further information on CORS, read https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS. +For further information on CORS, read [https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS). [`CorsConfig`]: ./struct.CorsConfig.html */ From 2cb326a4c35be46a5bda27630973d67ceeceda47 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 22 Nov 2020 22:09:38 +0100 Subject: [PATCH 100/170] ci: inspect the present files in publish stage --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 771a4a3..b32751a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -96,6 +96,7 @@ pages: stage: publish image: busybox script: + - find . -maxdepth 3 - mv tarpaulin-report.html public/coverage.html - mv target/doc public - echo 'The documentation is located here' >public/index.html From bb945e2cc638b7bb069ba3a3bfe1953e229a71c7 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 22 Nov 2020 22:31:51 +0100 Subject: [PATCH 101/170] ci: fix tarpaulin report and include cobertura.xml --- .gitlab-ci.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b32751a..54b91db 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -44,10 +44,12 @@ test-tarpaulin: - cargo -V - cargo install cargo-tarpaulin script: - - cargo tarpaulin --target-dir target/tarpaulin --all --all-features --exclude-files 'cargo/*' --exclude-files 'derive/*' --exclude-files 'example/*' --ignore-panics --ignore-tests --out Html -v + - cargo tarpaulin --target-dir target/tarpaulin --all --all-features --exclude-files 'cargo/*' --exclude-files 'derive/*' --exclude-files 'example/*' --exclude-files 'target/*' --ignore-panics --ignore-tests --out Html --out Xml -v artifacts: paths: - tarpaulin-report.html + reports: + cobertura: cobertura.xml cache: key: cargo-stable-all paths: @@ -96,9 +98,8 @@ pages: stage: publish image: busybox script: - - find . -maxdepth 3 - - mv tarpaulin-report.html public/coverage.html - mv target/doc public + - mv tarpaulin-report.html public/coverage.html - echo 'The documentation is located here' >public/index.html artifacts: paths: From ed1bbbd1fbd7595c1c4372a3049a49fdbfda963a Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 22 Nov 2020 23:18:28 +0100 Subject: [PATCH 102/170] add resource error integration test --- derive/src/resource_error.rs | 2 +- tests/resource_error.rs | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 tests/resource_error.rs diff --git a/derive/src/resource_error.rs b/derive/src/resource_error.rs index b3af040..e2b78d0 100644 --- a/derive/src/resource_error.rs +++ b/derive/src/resource_error.rs @@ -231,7 +231,7 @@ pub fn expand_resource_error(input: DeriveInput) -> Result { let variants = inum.variants.into_iter().map(process_variant).collect_to_result()?; let display_impl = if variants.iter().any(|v| v.display.is_none()) { - None + None // TODO issue warning if display is present on some but not all } else { let were = generics.params.iter().filter_map(|param| match param { GenericParam::Type(ty) => { diff --git a/tests/resource_error.rs b/tests/resource_error.rs new file mode 100644 index 0000000..a00b08a --- /dev/null +++ b/tests/resource_error.rs @@ -0,0 +1,36 @@ +use gotham_restful::ResourceError; + +#[derive(ResourceError)] +enum Error { + #[display("I/O Error: {0}")] + IoError(#[from] std::io::Error), + + #[status(INTERNAL_SERVER_ERROR)] + #[display("Internal Server Error: {0}")] + InternalServerError(String) +} + +mod resource_error { + use super::Error; + use gotham::hyper::StatusCode; + use gotham_restful::IntoResponseError; + use mime::APPLICATION_JSON; + + #[test] + fn io_error() { + let err = Error::IoError(std::io::Error::last_os_error()); + let res = err.into_response_error().unwrap(); + assert_eq!(res.status, StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!(res.mime, Some(APPLICATION_JSON)); + } + + #[test] + fn internal_server_error() { + let err = Error::InternalServerError("Brocken".to_owned()); + assert_eq!(&format!("{}", err), "Internal Server Error: Brocken"); + + let res = err.into_response_error().unwrap(); + assert_eq!(res.status, StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!(res.mime, None); // TODO shouldn't this be a json error message? + } +} From 4ae860dd326074df0e4ce6a3a12871e9d3a1106e Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 22 Nov 2020 23:55:52 +0100 Subject: [PATCH 103/170] docs: use [Type] syntax from rust 1.48 --- src/auth.rs | 3 ++- src/cors.rs | 17 +++++++---------- src/lib.rs | 19 ++++++++++++++----- src/openapi/types.rs | 10 +++------- src/resource.rs | 16 ++++++++-------- src/response.rs | 8 ++++---- src/result/auth_result.rs | 25 ++++++++++++------------- src/result/raw.rs | 4 +--- src/result/success.rs | 2 +- src/routing.rs | 4 ++-- src/types.rs | 33 +++++++++++++++++---------------- 11 files changed, 71 insertions(+), 70 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index 90bb40d..0219bdc 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -75,6 +75,7 @@ pub enum AuthSource { This trait will help the auth middleware to determine the validity of an authentication token. A very basic implementation could look like this: + ``` # use gotham_restful::{AuthHandler, State}; # @@ -93,7 +94,7 @@ pub trait AuthHandler { fn jwt_secret Option>(&self, state: &mut State, decode_data: F) -> Option>; } -/// 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)] pub struct StaticAuthHandler { secret: Vec diff --git a/src/cors.rs b/src/cors.rs index 717bbf6..5463de1 100644 --- a/src/cors.rs +++ b/src/cors.rs @@ -59,7 +59,7 @@ impl Origin { This is the configuration that the CORS handler will follow. Its default configuration is basically not to touch any responses, resulting in the browser's default behaviour. -To change settings, you need to put this type into gotham's [`State`]: +To change settings, you need to put this type into gotham's [State]: ```rust,no_run # use gotham::{router::builder::*, pipeline::{new_pipeline, single::single_pipeline}, state::State}; @@ -113,8 +113,6 @@ gotham::start("127.0.0.1:8080", build_router((), pipeline_set, |route| { }); })); ``` - - [`State`]: ../gotham/state/struct.State.html */ #[derive(Clone, Debug, Default, NewMiddleware, StateData)] pub struct CorsConfig { @@ -142,13 +140,12 @@ impl Middleware for CorsConfig { Handle CORS for a non-preflight request. This means manipulating the `res` HTTP headers so that the response is aligned with the `state`'s [CorsConfig]. -If you are using the [Resource](crate::Resource) type (which is the recommended way), you'll never have to call -this method. However, if you are writing your own handler method, you might want to call this -after your request to add the required CORS headers. +If you are using the [Resource](crate::Resource) type (which is the recommended way), you'll never +have to call this method. However, if you are writing your own handler method, you might want to +call this after your request to add the required CORS headers. -For further information on CORS, read [https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS). - - [`CorsConfig`]: ./struct.CorsConfig.html +For further information on CORS, read +[https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS). */ pub fn handle_cors(state: &State, res: &mut Response) { let config = CorsConfig::try_borrow_from(state); @@ -195,7 +192,7 @@ where P: RefUnwindSafe + Send + Sync + 'static { /// Handle a preflight request on `path` for `method`. To configure the behaviour, use - /// [`CorsConfig`](struct.CorsConfig.html). + /// [CorsConfig]. fn cors(&mut self, path: &str, method: Method); } diff --git a/src/lib.rs b/src/lib.rs index c030d37..918e944 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,17 +1,26 @@ #![allow(clippy::tabs_in_doc_comments)] #![warn(missing_debug_implementations, rust_2018_idioms)] #![deny(broken_intra_doc_links)] +#![forbid(unsafe_code)] /*! This crate is an extension to the popular [gotham web framework][gotham] for Rust. It allows you to create resources with assigned methods that aim to be a more convenient way of creating handlers for requests. -# Design Goals +# Features -This is an opinionated framework on top of [gotham]. Unless your web server handles mostly JSON as -request/response bodies and does that in a RESTful way, this framework is probably a bad fit for -your application. The ultimate goal of gotham-restful is to provide a way to write a RESTful -web server in Rust as convenient as possible with the least amount of boilerplate neccessary. + - Automatically parse **JSON** request and produce response bodies + - Allow using **raw** request and response bodies + - Convenient **macros** to create responses that can be registered with gotham's router + - Auto-Generate an **OpenAPI** specification for your API + - Manage **CORS** headers so you don't have to + - Manage **Authentication** with JWT + - Integrate diesel connection pools for easy **database** integration + +# Safety + +This crate is just as safe as you'd expect from anything written in safe Rust - and +`#![forbid(unsafe_code)]` ensures that no unsafe was used. # Methods diff --git a/src/openapi/types.rs b/src/openapi/types.rs index e506d7a..20bb07d 100644 --- a/src/openapi/types.rs +++ b/src/openapi/types.rs @@ -16,9 +16,7 @@ use uuid::Uuid; /** This struct needs to be available for every type that can be part of an OpenAPI Spec. It is already implemented for primitive types, String, Vec, Option and the like. To have it available -for your type, simply derive from [`OpenapiType`]. - -[`OpenapiType`]: trait.OpenapiType.html +for your type, simply derive from [OpenapiType]. */ #[derive(Debug, Clone, PartialEq)] pub struct OpenapiSchema { @@ -46,7 +44,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 { Schema { schema_data: SchemaData { @@ -61,7 +59,7 @@ impl OpenapiSchema { /** This trait needs to be implemented by every type that is being used in the OpenAPI Spec. It gives -access to the [`OpenapiSchema`] of this type. It is provided for primitive types, String and the +access to the [OpenapiSchema] of this type. It is provided for primitive types, String and the like. For use on your own types, there is a derive macro: ``` @@ -72,8 +70,6 @@ struct MyResponse { message: String } ``` - -[`OpenapiSchema`]: struct.OpenapiSchema.html */ pub trait OpenapiType { fn schema() -> OpenapiSchema; diff --git a/src/resource.rs b/src/resource.rs index 7784992..8c6bec5 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -30,13 +30,13 @@ pub trait ResourceMethod { } } -/// The read_all [`ResourceMethod`](trait.ResourceMethod.html). +/// The read_all [ResourceMethod]. pub trait ResourceReadAll: ResourceMethod { /// Handle a GET request on the Resource root. fn read_all(state: State) -> Pin + Send>>; } -/// The read [`ResourceMethod`](trait.ResourceMethod.html). +/// The read [ResourceMethod]. pub trait ResourceRead: ResourceMethod { /// The ID type to be parsed from the request path. type ID: ResourceID + 'static; @@ -45,7 +45,7 @@ pub trait ResourceRead: ResourceMethod { fn read(state: State, id: Self::ID) -> Pin + Send>>; } -/// The search [`ResourceMethod`](trait.ResourceMethod.html). +/// The search [ResourceMethod]. pub trait ResourceSearch: ResourceMethod { /// The Query type to be parsed from the request parameters. type Query: ResourceType + QueryStringExtractor + Sync; @@ -54,7 +54,7 @@ pub trait ResourceSearch: ResourceMethod { fn search(state: State, query: Self::Query) -> Pin + Send>>; } -/// The create [`ResourceMethod`](trait.ResourceMethod.html). +/// The create [ResourceMethod]. pub trait ResourceCreate: ResourceMethod { /// The Body type to be parsed from the request body. type Body: RequestBody; @@ -63,7 +63,7 @@ pub trait ResourceCreate: ResourceMethod { fn create(state: State, body: Self::Body) -> Pin + Send>>; } -/// The change_all [`ResourceMethod`](trait.ResourceMethod.html). +/// The change_all [ResourceMethod]. pub trait ResourceChangeAll: ResourceMethod { /// The Body type to be parsed from the request body. type Body: RequestBody; @@ -72,7 +72,7 @@ pub trait ResourceChangeAll: ResourceMethod { fn change_all(state: State, body: Self::Body) -> Pin + Send>>; } -/// The change [`ResourceMethod`](trait.ResourceMethod.html). +/// The change [ResourceMethod]. pub trait ResourceChange: ResourceMethod { /// The Body type to be parsed from the request body. type Body: RequestBody; @@ -83,13 +83,13 @@ pub trait ResourceChange: ResourceMethod { fn change(state: State, id: Self::ID, body: Self::Body) -> Pin + Send>>; } -/// The remove_all [`ResourceMethod`](trait.ResourceMethod.html). +/// The remove_all [ResourceMethod]. pub trait ResourceRemoveAll: ResourceMethod { /// Handle a DELETE request on the Resource root. fn remove_all(state: State) -> Pin + Send>>; } -/// The remove [`ResourceMethod`](trait.ResourceMethod.html). +/// The remove [ResourceMethod]. pub trait ResourceRemove: ResourceMethod { /// The ID type to be parsed from the request path. type ID: ResourceID + 'static; diff --git a/src/response.rs b/src/response.rs index d542a1a..5fe1d32 100644 --- a/src/response.rs +++ b/src/response.rs @@ -10,7 +10,7 @@ pub struct Response { } impl Response { - /// Create a new `Response` from raw data. + /// Create a new [Response] from raw data. pub fn new>(status: StatusCode, body: B, mime: Option) -> Self { Self { status, @@ -19,7 +19,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>(status: StatusCode, body: B) -> Self { Self { status, @@ -28,7 +28,7 @@ impl Response { } } - /// Create a _204 No Content_ `Response`. + /// Create a _204 No Content_ [Response]. pub fn no_content() -> Self { Self { status: StatusCode::NO_CONTENT, @@ -37,7 +37,7 @@ impl Response { } } - /// Create an empty _403 Forbidden_ `Response`. + /// Create an empty _403 Forbidden_ [Response]. pub fn forbidden() -> Self { Self { status: StatusCode::FORBIDDEN, diff --git a/src/result/auth_result.rs b/src/result/auth_result.rs index 6aab5a8..cd56a84 100644 --- a/src/result/auth_result.rs +++ b/src/result/auth_result.rs @@ -2,10 +2,7 @@ use gotham_restful_derive::ResourceError; /** This is an error type that always yields a _403 Forbidden_ response. This type is best used in -combination with [`AuthSuccess`] or [`AuthResult`]. - - [`AuthSuccess`]: type.AuthSuccess.html - [`AuthResult`]: type.AuthResult.html +combination with [AuthSuccess] or [AuthResult]. */ #[derive(Debug, Clone, Copy, ResourceError)] pub enum AuthError { @@ -15,9 +12,11 @@ pub enum AuthError { } /** -This return type can be used to map another `ResourceResult` that can only be returned if the -client is authenticated. Otherwise, an empty _403 Forbidden_ response will be issued. Use can -look something like this (assuming the `auth` feature is enabled): +This return type can be used to map another [ResourceResult](crate::ResourceResult) that can +only be returned if the client is authenticated. Otherwise, an empty _403 Forbidden_ response +will be issued. + +Use can look something like this (assuming the `auth` feature is enabled): ```rust # #[macro_use] extern crate gotham_restful_derive; @@ -50,9 +49,7 @@ pub type AuthSuccess = Result; /** This is an error type that either yields a _403 Forbidden_ respone if produced from an authentication -error, or delegates to another error type. This type is best used with [`AuthResult`]. - - [`AuthResult`]: type.AuthResult.html +error, or delegates to another error type. This type is best used with [AuthResult]. */ #[derive(Debug, ResourceError)] pub enum AuthErrorOrOther { @@ -83,9 +80,11 @@ where } /** -This return type can be used to map another `ResourceResult` that can only be returned if the -client is authenticated. Otherwise, an empty _403 Forbidden_ response will be issued. Use can -look something like this (assuming the `auth` feature is enabled): +This return type can be used to map another [ResourceResult](crate::ResourceResult) that can +only be returned if the client is authenticated. Otherwise, an empty _403 Forbidden_ response +will be issued. + +Use can look something like this (assuming the `auth` feature is enabled): ``` # #[macro_use] extern crate gotham_restful_derive; diff --git a/src/result/raw.rs b/src/result/raw.rs index bf1997f..dd174d0 100644 --- a/src/result/raw.rs +++ b/src/result/raw.rs @@ -14,7 +14,7 @@ use std::{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 of request bodies are accepted by this type. It is therefore recommended to derive your own type -from [`RequestBody`] and only use this when you need to return a raw response. This is a usage +from [RequestBody] and only use this when you need to return a raw response. This is a usage example that simply returns its body: ```rust,no_run @@ -35,8 +35,6 @@ fn create(body : Raw>) -> Raw> { # })); # } ``` - - [`OpenapiType`]: trait.OpenapiType.html */ #[derive(Debug)] pub struct Raw { diff --git a/src/result/success.rs b/src/result/success.rs index 43cdd6f..7ed7cf7 100644 --- a/src/result/success.rs +++ b/src/result/success.rs @@ -13,7 +13,7 @@ use std::{ /** This can be returned from a resource when there is no cause of an error. It behaves similar to a -smart pointer like box, it that it implements `AsRef`, `Deref` and the likes. +smart pointer like box, it that it implements [AsRef], [Deref] and the likes. Usage example: diff --git a/src/routing.rs b/src/routing.rs index 5e952f6..ea4d5dd 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -44,13 +44,13 @@ pub trait WithOpenapi { } /// 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 { fn resource(&mut self, path: &str); } /// This trait allows to draw routes within an resource. Use this only inside the -/// `Resource::setup` method. +/// [Resource::setup] method. pub trait DrawResourceRoutes { fn read_all(&mut self); diff --git a/src/types.rs b/src/types.rs index 030c02c..5d7f255 100644 --- a/src/types.rs +++ b/src/types.rs @@ -20,18 +20,21 @@ impl ResourceType for T {} /// 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 -/// `OpenapiType`. +/// [OpenapiType]. +/// +/// [OpenapiType]: trait.OpenapiType.html pub trait ResponseBody: ResourceType + Serialize {} impl ResponseBody for T {} /** This trait should be implemented for every type that can be built from an HTTP request body -plus its media type. For most use cases it is sufficient to derive this trait, you usually -don't need to manually implement this. Therefore, make sure that the first variable of -your struct can be built from [`Bytes`], and the second one can be build from [`Mime`]. -If you have any additional variables, they need to be `Default`. This is an example of -such a struct: +plus its media type. + +For most use cases it is sufficient to derive this trait, you usually don't need to manually +implement this. Therefore, make sure that the first variable of your struct can be built from +[Bytes], and the second one can be build from [Mime]. If you have any additional variables, they +need to be [Default]. This is an example of such a struct: ```rust # #[macro_use] extern crate gotham_restful; @@ -43,14 +46,11 @@ struct RawImage { content_type: Mime } ``` - - [`Bytes`]: ../bytes/struct.Bytes.html - [`Mime`]: ../mime/struct.Mime.html */ pub trait FromBody: Sized { /// 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 - /// might change in the future. + /// macro, there is no way to trigger an error, so [std::convert::Infallible] is used here. + /// However, this might change in the future. type Err: Error; /// Perform the conversion. @@ -67,11 +67,11 @@ impl FromBody for T { /** 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]. If you want a non-deserializable type to be used as a request body, e.g. because you'd like to get the raw data, you can derive it for your own type. All you need is to have a type implementing -[`FromBody`] and optionally a list of supported media types: +[FromBody] and optionally a list of supported media types: ```rust # #[macro_use] extern crate gotham_restful; @@ -84,8 +84,7 @@ struct RawImage { } ``` - [`FromBody`]: trait.FromBody.html - [`OpenapiType`]: trait.OpenapiType.html + [OpenapiType]: trait.OpenapiType.html */ pub trait RequestBody: ResourceType + FromBody { /// Return all types that are supported as content types. Use `None` if all types are supported. @@ -102,7 +101,9 @@ impl RequestBody for T { /// 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 -/// type `OpenapiType`. +/// type [OpenapiType]. +/// +/// [OpenapiType]: trait.OpenapiType.html pub trait ResourceID: ResourceType + DeserializeOwned + Clone + RefUnwindSafe + Send + Sync {} impl ResourceID for T {} From f1e1c5e5213f76cd8fdd2c1eaec1a7f6260d09bc Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 22 Nov 2020 23:56:25 +0100 Subject: [PATCH 104/170] update readme --- README.md | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 643174c..99cf632 100644 --- a/README.md +++ b/README.md @@ -34,12 +34,20 @@ This crate is an extension to the popular [gotham web framework][gotham] for Rus create resources with assigned methods that aim to be a more convenient way of creating handlers for requests. -## Design Goals +## Features -This is an opinionated framework on top of [gotham]. Unless your web server handles mostly JSON as -request/response bodies and does that in a RESTful way, this framework is probably a bad fit for -your application. The ultimate goal of gotham-restful is to provide a way to write a RESTful -web server in Rust as convenient as possible with the least amount of boilerplate neccessary. + - Automatically parse **JSON** request and produce response bodies + - Allow using **raw** request and response bodies + - Convenient **macros** to create responses that can be registered with gotham's router + - Auto-Generate an **OpenAPI** specification for your API + - Manage **CORS** headers so you don't have to + - Manage **Authentication** with JWT + - Integrate diesel connection pools for easy **database** integration + +## Safety + +This crate is just as safe as you'd expect from anything written in safe Rust - and +`#![forbid(unsafe_code)]` ensures that no unsafe was used. ## Methods From f9c2009023324dbbeed0088d8950d0db1cf9509e Mon Sep 17 00:00:00 2001 From: Dominic Date: Mon, 23 Nov 2020 00:02:16 +0100 Subject: [PATCH 105/170] ci: check rustfmt --- .gitlab-ci.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 54b91db..f4e847f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -78,6 +78,15 @@ readme: - cargo readme -t README.tpl >README.md.new - diff README.md README.md.new +rustfmt: + stage: test + image: rustlang/rust:nightly-slim + before_script: + - cargo -V + - cargo fmt --version + script: + - cargo fmt -- --check + doc: stage: build image: rust:slim From 4fd5464e448925ac5a3ce52be15e961f397370f3 Mon Sep 17 00:00:00 2001 From: Dominic Date: Mon, 23 Nov 2020 01:22:03 +0100 Subject: [PATCH 106/170] get rid of mod's in error messages --- derive/src/method.rs | 161 ++++++++++++++------------- derive/src/resource.rs | 3 +- tests/ui/method_async_state.stderr | 16 --- tests/ui/method_no_resource.stderr | 4 +- tests/ui/method_self.stderr | 10 +- tests/ui/method_too_few_args.stderr | 8 -- tests/ui/method_too_many_args.stderr | 8 -- tests/ui/method_unsafe.stderr | 8 -- 8 files changed, 91 insertions(+), 127 deletions(-) diff --git a/derive/src/method.rs b/derive/src/method.rs index f892734..c9e5056 100644 --- a/derive/src/method.rs +++ b/derive/src/method.rs @@ -4,7 +4,7 @@ use proc_macro2::{Ident, Span, TokenStream}; use quote::{format_ident, quote}; use std::str::FromStr; use syn::{ - spanned::Spanned, Attribute, AttributeArgs, Error, FnArg, ItemFn, Lit, LitBool, Meta, NestedMeta, PatType, Result, + spanned::Spanned, Attribute, AttributeArgs, Error, FnArg, ItemFn, Lit, LitBool, Meta, NestedMeta, PatType, Path, Result, ReturnType, Type }; @@ -82,20 +82,12 @@ impl Method { format_ident!("{}", name) } - pub fn mod_ident(&self, resource: &str) -> Ident { - format_ident!( - "_gotham_restful_resource_{}_method_{}", - resource.to_snake_case(), - self.fn_ident() - ) - } - pub fn handler_struct_ident(&self, resource: &str) -> Ident { format_ident!("{}{}Handler", resource.to_camel_case(), self.trait_ident()) } pub fn setup_ident(&self, resource: &str) -> Ident { - format_ident!("{}_{}_setup_impl", resource.to_snake_case(), self.fn_ident()) + format_ident!("_gotham_restful_{}_{}_setup_impl", resource.to_snake_case(), self.fn_ident()) } } @@ -249,33 +241,16 @@ fn expand_wants_auth(attrs: &[NestedMeta], default: bool) -> TokenStream { } #[allow(clippy::comparison_chain)] -pub fn expand_method(method: Method, mut attrs: AttributeArgs, fun: ItemFn) -> Result { +fn setup_body( + method: &Method, + fun: &ItemFn, + attrs: &[NestedMeta], + resource_name: &str, + resource_path: &Path +) -> Result { let krate = super::krate(); - // parse attributes - if attrs.len() < 1 { - return Err(Error::new( - Span::call_site(), - "Missing Resource struct. Example: #[read_all(MyResource)]" - )); - } - let resource_path = match attrs.remove(0) { - NestedMeta::Meta(Meta::Path(path)) => path, - p => { - return Err(Error::new( - p.span(), - "Expected name of the Resource struct this method belongs to" - )) - }, - }; - let resource_name = resource_path - .segments - .last() - .map(|s| s.ident.to_string()) - .ok_or_else(|| Error::new(resource_path.span(), "Resource name must not be empty"))?; - let fun_ident = &fun.sig.ident; - let fun_vis = &fun.vis; let fun_is_async = fun.sig.asyncness.is_some(); if let Some(unsafety) = fun.sig.unsafety { @@ -284,9 +259,7 @@ pub fn expand_method(method: Method, mut attrs: AttributeArgs, fun: ItemFn) -> R let trait_ident = method.trait_ident(); let method_ident = method.fn_ident(); - let mod_ident = method.mod_ident(&resource_name); - let handler_ident = method.handler_struct_ident(&resource_name); - let setup_ident = method.setup_ident(&resource_name); + let handler_ident = method.handler_struct_ident(resource_name); let (ret, is_no_content) = match &fun.sig.output { ReturnType::Default => (quote!(#krate::NoContent), true), @@ -410,52 +383,84 @@ pub fn expand_method(method: Method, mut attrs: AttributeArgs, fun: ItemFn) -> R } // attribute generated code - let operation_id = expand_operation_id(&attrs); - let wants_auth = expand_wants_auth(&attrs, args.iter().any(|arg| (*arg).ty.is_auth_status())); + let operation_id = expand_operation_id(attrs); + let wants_auth = expand_wants_auth(attrs, args.iter().any(|arg| (*arg).ty.is_auth_status())); // put everything together + let mut dummy = format_ident!("_IMPL_RESOURCEMETHOD_FOR_{}", fun_ident); + dummy.set_span(Span::call_site()); + Ok(quote! { + struct #handler_ident; + + impl #krate::ResourceMethod for #handler_ident { + type Res = #ret; + + #operation_id + #wants_auth + } + + impl #krate::#trait_ident for #handler_ident + where #where_clause + { + #(#generics)* + + fn #method_ident(#(#args_def),*) -> std::pin::Pin + Send>> { + #[allow(unused_imports)] + use #krate::{export::FutureExt, FromState}; + + #state_block + + async move { + let #res_ident = { #block }; + (#state_ident, #res_ident) + }.boxed() + } + } + + route.#method_ident::<#handler_ident>(); + }) +} + +pub fn expand_method(method: Method, mut attrs: AttributeArgs, fun: ItemFn) -> Result { + let krate = super::krate(); + + // parse attributes + if attrs.len() < 1 { + return Err(Error::new( + Span::call_site(), + "Missing Resource struct. Example: #[read_all(MyResource)]" + )); + } + let resource_path = match attrs.remove(0) { + NestedMeta::Meta(Meta::Path(path)) => path, + p => { + return Err(Error::new( + p.span(), + "Expected name of the Resource struct this method belongs to" + )) + }, + }; + let resource_name = resource_path + .segments + .last() + .map(|s| s.ident.to_string()) + .ok_or_else(|| Error::new(resource_path.span(), "Resource name must not be empty"))?; + + let fun_vis = &fun.vis; + let setup_ident = method.setup_ident(&resource_name); + let setup_body = match setup_body(&method, &fun, &attrs, &resource_name, &resource_path) { + Ok(body) => body, + Err(err) => err.to_compile_error() + }; + Ok(quote! { #fun - #fun_vis mod #mod_ident - { - use super::*; - - struct #handler_ident; - - impl #krate::ResourceMethod for #handler_ident - { - type Res = #ret; - - #operation_id - #wants_auth - } - - impl #krate::#trait_ident for #handler_ident - where #where_clause - { - #(#generics)* - - fn #method_ident(#(#args_def),*) -> std::pin::Pin + Send>> - { - #[allow(unused_imports)] - use #krate::{export::FutureExt, FromState}; - - #state_block - - async move { - let #res_ident = { #block }; - (#state_ident, #res_ident) - }.boxed() - } - } - - #[deny(dead_code)] - pub fn #setup_ident(route : &mut D) - { - route.#method_ident::<#handler_ident>(); - } - + #[deny(dead_code)] + #[doc(hidden)] + /// `gotham_restful` implementation detail. + #fun_vis fn #setup_ident(route : &mut D) { + #setup_body } }) } diff --git a/derive/src/resource.rs b/derive/src/resource.rs index a81e6d9..60ae159 100644 --- a/derive/src/resource.rs +++ b/derive/src/resource.rs @@ -38,9 +38,8 @@ pub fn expand_resource(input: DeriveInput) -> Result { .flat_map(|list| match list { Ok(iter) => Box::new(iter.map(|method| { let method = Method::from_str(&method.to_string()).map_err(|err| Error::new(method.span(), err))?; - let mod_ident = method.mod_ident(&name); let ident = method.setup_ident(&name); - Ok(quote!(#mod_ident::#ident(&mut route);)) + Ok(quote!(#ident(&mut route);)) })) as Box>>, Err(err) => Box::new(iter::once(Err(err))) }) diff --git a/tests/ui/method_async_state.stderr b/tests/ui/method_async_state.stderr index f66a260..571c334 100644 --- a/tests/ui/method_async_state.stderr +++ b/tests/ui/method_async_state.stderr @@ -3,19 +3,3 @@ error: async fn must not take &State as an argument as State is not Sync, consid | 9 | async fn read_all(state : &State) | ^^^^^ - -error[E0433]: failed to resolve: use of undeclared crate or module `_gotham_restful_resource_foo_resource_method_read_all` - --> $DIR/method_async_state.rs:4:10 - | -4 | #[derive(Resource)] - | ^^^^^^^^ use of undeclared crate or module `_gotham_restful_resource_foo_resource_method_read_all` - | - = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) - -warning: unused import: `gotham_restful::State` - --> $DIR/method_async_state.rs:2:5 - | -2 | use gotham_restful::State; - | ^^^^^^^^^^^^^^^^^^^^^ - | - = note: `#[warn(unused_imports)]` on by default diff --git a/tests/ui/method_no_resource.stderr b/tests/ui/method_no_resource.stderr index d5c5521..1cddd9e 100644 --- a/tests/ui/method_no_resource.stderr +++ b/tests/ui/method_no_resource.stderr @@ -6,10 +6,10 @@ error: Missing Resource struct. Example: #[read_all(MyResource)] | = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info) -error[E0433]: failed to resolve: use of undeclared crate or module `_gotham_restful_resource_foo_resource_method_read_all` +error[E0425]: cannot find function `_gotham_restful_foo_resource_read_all_setup_impl` in this scope --> $DIR/method_no_resource.rs:3:10 | 3 | #[derive(Resource)] - | ^^^^^^^^ use of undeclared crate or module `_gotham_restful_resource_foo_resource_method_read_all` + | ^^^^^^^^ not found in this scope | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/method_self.stderr b/tests/ui/method_self.stderr index 0e4b492..1ad62ca 100644 --- a/tests/ui/method_self.stderr +++ b/tests/ui/method_self.stderr @@ -4,10 +4,10 @@ error: Didn't expect self parameter 8 | fn read_all(self) | ^^^^ -error[E0433]: failed to resolve: use of undeclared crate or module `_gotham_restful_resource_foo_resource_method_read_all` - --> $DIR/method_self.rs:3:10 +error: `self` parameter is only allowed in associated functions + --> $DIR/method_self.rs:8:13 | -3 | #[derive(Resource)] - | ^^^^^^^^ use of undeclared crate or module `_gotham_restful_resource_foo_resource_method_read_all` +8 | fn read_all(self) + | ^^^^ not semantically valid as function parameter | - = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) + = note: associated functions are those in `impl` or `trait` definitions diff --git a/tests/ui/method_too_few_args.stderr b/tests/ui/method_too_few_args.stderr index 0aed829..2036d86 100644 --- a/tests/ui/method_too_few_args.stderr +++ b/tests/ui/method_too_few_args.stderr @@ -3,11 +3,3 @@ error: Too few arguments | 8 | fn read() | ^^^^ - -error[E0433]: failed to resolve: use of undeclared crate or module `_gotham_restful_resource_foo_resource_method_read` - --> $DIR/method_too_few_args.rs:3:10 - | -3 | #[derive(Resource)] - | ^^^^^^^^ use of undeclared crate or module `_gotham_restful_resource_foo_resource_method_read` - | - = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/method_too_many_args.stderr b/tests/ui/method_too_many_args.stderr index ad8a37b..0c251e4 100644 --- a/tests/ui/method_too_many_args.stderr +++ b/tests/ui/method_too_many_args.stderr @@ -3,11 +3,3 @@ error: Too many arguments | 8 | fn read_all(_id : u64) | ^^^ - -error[E0433]: failed to resolve: use of undeclared crate or module `_gotham_restful_resource_foo_resource_method_read_all` - --> $DIR/method_too_many_args.rs:3:10 - | -3 | #[derive(Resource)] - | ^^^^^^^^ use of undeclared crate or module `_gotham_restful_resource_foo_resource_method_read_all` - | - = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/method_unsafe.stderr b/tests/ui/method_unsafe.stderr index ad42326..92308bf 100644 --- a/tests/ui/method_unsafe.stderr +++ b/tests/ui/method_unsafe.stderr @@ -3,11 +3,3 @@ error: Resource methods must not be unsafe | 8 | unsafe fn read_all() | ^^^^^^ - -error[E0433]: failed to resolve: use of undeclared crate or module `_gotham_restful_resource_foo_resource_method_read_all` - --> $DIR/method_unsafe.rs:3:10 - | -3 | #[derive(Resource)] - | ^^^^^^^^ use of undeclared crate or module `_gotham_restful_resource_foo_resource_method_read_all` - | - = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) From fee9dc89b06c20952cb19c375173d59b64f3d9db Mon Sep 17 00:00:00 2001 From: Dominic Date: Mon, 23 Nov 2020 23:17:28 +0100 Subject: [PATCH 107/170] support cookie auth without cookie jar in state --- src/auth.rs | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index 0219bdc..15b5afe 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -8,7 +8,7 @@ use gotham::{ anyhow, handler::HandlerFuture, hyper::header::{HeaderMap, AUTHORIZATION}, - middleware::{Middleware, NewMiddleware}, + middleware::{cookie::CookieParser, Middleware, NewMiddleware}, state::{FromState, State} }; use jsonwebtoken::{errors::ErrorKind, DecodingKey}; @@ -209,8 +209,12 @@ where // extract the provided token, if any let token = match &self.source { AuthSource::Cookie(name) => CookieJar::try_borrow_from(&state) - .and_then(|jar| jar.get(&name)) - .map(|cookie| cookie.value().to_owned()), + .map(|jar| jar.get(&name).map(|cookie| cookie.value().to_owned())) + .unwrap_or_else(|| { + CookieParser::from_state(&state) + .get(&name) + .map(|cookie| cookie.value().to_owned()) + }), AuthSource::Header(name) => HeaderMap::try_borrow_from(&state) .and_then(|map| map.get(name)) .and_then(|header| header.to_str().ok()) @@ -292,6 +296,7 @@ where mod test { use super::*; use cookie::Cookie; + use gotham::hyper::header::COOKIE; use std::fmt::Debug; // 256-bit random string @@ -458,4 +463,20 @@ mod test { }; }) } + + #[test] + fn test_auth_middleware_cookie_no_jar() { + let cookie_name = "znoiprwmvfexju"; + let middleware = new_middleware::(AuthSource::Cookie(cookie_name.to_owned())); + State::with_new(|mut state| { + let mut headers = HeaderMap::new(); + headers.insert(COOKIE, format!("{}={}", cookie_name, VALID_TOKEN).parse().unwrap()); + state.put(headers); + let status = middleware.auth_status(&mut state); + match status { + AuthStatus::Authenticated(data) => assert_eq!(data, TestData::default()), + _ => panic!("Expected AuthStatus::Authenticated, got {:?}", status) + }; + }) + } } From 8a8e01e7577e9748c7143720c4f476b1cce539b6 Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 25 Nov 2020 03:11:30 +0100 Subject: [PATCH 108/170] add openapi support for NonZeroU types --- src/openapi/types.rs | 77 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 60 insertions(+), 17 deletions(-) diff --git a/src/openapi/types.rs b/src/openapi/types.rs index 20bb07d..416e8c0 100644 --- a/src/openapi/types.rs +++ b/src/openapi/types.rs @@ -6,9 +6,11 @@ use openapiv3::{ ReferenceOr::{Item, Reference}, Schema, SchemaData, SchemaKind, StringType, Type, VariantOrUnknownOrEmpty }; + use std::{ collections::{BTreeSet, HashMap, HashSet}, - hash::BuildHasher + hash::BuildHasher, + num::{NonZeroU128, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize} }; #[cfg(feature = "uuid")] use uuid::Uuid; @@ -114,6 +116,19 @@ macro_rules! int_types { } )*}; + (gtzero $($int_ty:ty),*) => {$( + impl OpenapiType for $int_ty + { + fn schema() -> OpenapiSchema + { + OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { + minimum: Some(1), + ..Default::default() + }))) + } + } + )*}; + (bits = $bits:expr, $($int_ty:ty),*) => {$( impl OpenapiType for $int_ty { @@ -140,20 +155,40 @@ macro_rules! int_types { } } )*}; + + (gtzero bits = $bits:expr, $($int_ty:ty),*) => {$( + impl OpenapiType for $int_ty + { + fn schema() -> OpenapiSchema + { + OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { + format: VariantOrUnknownOrEmpty::Unknown(format!("int{}", $bits)), + minimum: Some(1), + ..Default::default() + }))) + } + } + )*}; } int_types!(isize); int_types!(unsigned usize); +int_types!(gtzero NonZeroUsize); int_types!(bits = 8, i8); int_types!(unsigned bits = 8, u8); +int_types!(gtzero bits = 8, NonZeroU8); int_types!(bits = 16, i16); int_types!(unsigned bits = 16, u16); +int_types!(gtzero bits = 16, NonZeroU16); int_types!(bits = 32, i32); int_types!(unsigned bits = 32, u32); +int_types!(gtzero bits = 32, NonZeroU32); int_types!(bits = 64, i64); int_types!(unsigned bits = 64, u64); +int_types!(gtzero bits = 64, NonZeroU64); int_types!(bits = 128, i128); int_types!(unsigned bits = 128, u128); +int_types!(gtzero bits = 128, NonZeroU128); macro_rules! num_types { ($($num_ty:ty = $num_fmt:ident),*) => {$( @@ -356,6 +391,7 @@ mod test { assert_schema!(Unit => r#"{"type":"object","additionalProperties":false}"#); assert_schema!(bool => r#"{"type":"boolean"}"#); + assert_schema!(isize => r#"{"type":"integer"}"#); assert_schema!(usize => r#"{"type":"integer","minimum":0}"#); assert_schema!(i8 => r#"{"type":"integer","format":"int8"}"#); @@ -368,29 +404,36 @@ mod test { assert_schema!(u64 => r#"{"type":"integer","format":"int64","minimum":0}"#); assert_schema!(i128 => r#"{"type":"integer","format":"int128"}"#); assert_schema!(u128 => r#"{"type":"integer","format":"int128","minimum":0}"#); + + assert_schema!(NonZeroUsize => r#"{"type":"integer","minimum":1}"#); + assert_schema!(NonZeroU8 => r#"{"type":"integer","format":"int8","minimum":1}"#); + assert_schema!(NonZeroU16 => r#"{"type":"integer","format":"int16","minimum":1}"#); + assert_schema!(NonZeroU32 => r#"{"type":"integer","format":"int32","minimum":1}"#); + assert_schema!(NonZeroU64 => r#"{"type":"integer","format":"int64","minimum":1}"#); + assert_schema!(NonZeroU128 => r#"{"type":"integer","format":"int128","minimum":1}"#); + assert_schema!(f32 => r#"{"type":"number","format":"float"}"#); assert_schema!(f64 => r#"{"type":"number","format":"double"}"#); assert_schema!(String => r#"{"type":"string"}"#); - #[cfg(feature = "chrono")] - assert_schema!(Date => r#"{"type":"string","format":"date"}"#); - #[cfg(feature = "chrono")] - assert_schema!(Date => r#"{"type":"string","format":"date"}"#); - #[cfg(feature = "chrono")] - assert_schema!(Date => r#"{"type":"string","format":"date"}"#); - #[cfg(feature = "chrono")] - assert_schema!(NaiveDate => r#"{"type":"string","format":"date"}"#); - #[cfg(feature = "chrono")] - assert_schema!(DateTime => r#"{"type":"string","format":"date-time"}"#); - #[cfg(feature = "chrono")] - assert_schema!(DateTime => r#"{"type":"string","format":"date-time"}"#); - #[cfg(feature = "chrono")] - assert_schema!(DateTime => r#"{"type":"string","format":"date-time"}"#); - #[cfg(feature = "chrono")] - assert_schema!(NaiveDateTime => r#"{"type":"string","format":"date-time"}"#); + #[cfg(feature = "uuid")] assert_schema!(Uuid => r#"{"type":"string","format":"uuid"}"#); + #[cfg(feature = "chrono")] + mod chrono { + use super::*; + + assert_schema!(Date => r#"{"type":"string","format":"date"}"#); + assert_schema!(Date => r#"{"type":"string","format":"date"}"#); + assert_schema!(Date => r#"{"type":"string","format":"date"}"#); + assert_schema!(NaiveDate => r#"{"type":"string","format":"date"}"#); + assert_schema!(DateTime => r#"{"type":"string","format":"date-time"}"#); + assert_schema!(DateTime => r#"{"type":"string","format":"date-time"}"#); + assert_schema!(DateTime => r#"{"type":"string","format":"date-time"}"#); + assert_schema!(NaiveDateTime => r#"{"type":"string","format":"date-time"}"#); + } + assert_schema!(Option => r#"{"nullable":true,"type":"string"}"#); assert_schema!(Vec => r#"{"type":"array","items":{"type":"string"}}"#); assert_schema!(BTreeSet => r#"{"type":"array","items":{"type":"string"}}"#); From 7851b50bcde7348c1cf5dc4202402bdab7517dd1 Mon Sep 17 00:00:00 2001 From: Dominic Date: Mon, 28 Dec 2020 18:11:29 +0100 Subject: [PATCH 109/170] update itertools to 0.10 and mark it as optional --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 96b9f52..c510d7b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ gotham_derive = "0.5.0" gotham_middleware_diesel = { version = "0.2.0", optional = true } gotham_restful_derive = { version = "0.1.0" } indexmap = { version = "1.3.2", optional = true } -itertools = "0.9.0" +itertools = { version = "0.10.0", optional = true } jsonwebtoken = { version = "7.1.0", optional = true } log = "0.4.8" mime = "0.3.16" @@ -47,7 +47,7 @@ trybuild = "1.0.27" [features] default = ["cors", "errorlog"] auth = ["gotham_restful_derive/auth", "base64", "cookie", "jsonwebtoken"] -cors = [] +cors = ["itertools"] errorlog = [] database = ["gotham_restful_derive/database", "gotham_middleware_diesel"] openapi = ["gotham_restful_derive/openapi", "indexmap", "openapiv3"] From 1ac323accc706cb534eb3139097357ad387ac994 Mon Sep 17 00:00:00 2001 From: Dominic Date: Mon, 28 Dec 2020 18:18:10 +0100 Subject: [PATCH 110/170] example: replace log4rs with pretty_env_logger --- example/Cargo.toml | 2 +- example/src/main.rs | 16 +--------------- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/example/Cargo.toml b/example/Cargo.toml index d65544d..96c8e62 100644 --- a/example/Cargo.toml +++ b/example/Cargo.toml @@ -19,5 +19,5 @@ gotham = { version = "0.5.0-rc.1", default-features = false } gotham_derive = "0.5.0-rc.1" gotham_restful = { version = "0.1.0-rc0", features = ["auth", "openapi"] } log = "0.4.8" -log4rs = { version = "0.13.0", features = ["console_appender"], default-features = false } +pretty_env_logger = "0.4" serde = "1.0.110" diff --git a/example/src/main.rs b/example/src/main.rs index 4689ebe..f2d03d0 100644 --- a/example/src/main.rs +++ b/example/src/main.rs @@ -12,12 +12,6 @@ use gotham::{ state::State }; use gotham_restful::*; -use log::LevelFilter; -use log4rs::{ - append::console::ConsoleAppender, - config::{Appender, Config, Root}, - encode::pattern::PatternEncoder -}; use serde::{Deserialize, Serialize}; #[derive(Resource)] @@ -103,15 +97,7 @@ impl AuthHandler for Handler { } fn main() { - let encoder = PatternEncoder::new("{d(%Y-%m-%d %H:%M:%S%.3f %Z)} [{l}] {M} - {m}\n"); - let config = Config::builder() - .appender(Appender::builder().build( - "stdout", - Box::new(ConsoleAppender::builder().encoder(Box::new(encoder)).build()) - )) - .build(Root::builder().appender("stdout").build(LevelFilter::Info)) - .unwrap(); - log4rs::init_config(config).unwrap(); + pretty_env_logger::init_timed(); let cors = CorsConfig { origin: Origin::Copy, From 9efc1cb1924d8ba38d81259c0b02357d281a21cb Mon Sep 17 00:00:00 2001 From: Dominic Date: Mon, 28 Dec 2020 18:35:48 +0100 Subject: [PATCH 111/170] bump version to 0.1.1 and add changelog --- CHANGELOG.md | 20 ++++++++++++++++++++ Cargo.toml | 4 ++-- derive/Cargo.toml | 2 +- 3 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a561d6f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,20 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.1.1] - 2020-12-28 +### Added + - Support for `&mut State` parameters in method handlers + - Support for `NonZeroU` types in the OpenAPI Specification + +### Changed + - cookie auth does not require a middleware for parsing cookies anymore + - the derive macro produces no more private `mod`s which makes error message more readable + - documentation now makes use of the `[Type]` syntax introduced in Rust 1.48 + +## [0.1.0] - 2020-10-02 +Previous changes are not tracked by this changelog file. Refer to the [releases](https://gitlab.com/msrd0/gotham-restful/-/releases) for the changelog. diff --git a/Cargo.toml b/Cargo.toml index c510d7b..37d2bac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ members = ["derive", "example"] [package] name = "gotham_restful" -version = "0.1.0" +version = "0.1.1" authors = ["Dominic Meiser "] edition = "2018" description = "RESTful additions for the gotham web framework" @@ -26,7 +26,7 @@ futures-util = "0.3.7" gotham = { version = "0.5.0", default-features = false } gotham_derive = "0.5.0" gotham_middleware_diesel = { version = "0.2.0", optional = true } -gotham_restful_derive = { version = "0.1.0" } +gotham_restful_derive = { version = "0.1.1" } indexmap = { version = "1.3.2", optional = true } itertools = { version = "0.10.0", optional = true } jsonwebtoken = { version = "7.1.0", optional = true } diff --git a/derive/Cargo.toml b/derive/Cargo.toml index f238f10..f06023c 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "gotham_restful_derive" -version = "0.1.0" +version = "0.1.1" authors = ["Dominic Meiser "] edition = "2018" description = "RESTful additions for the gotham web framework - Derive" From 71961268c469b2889d61523c31ecab59273f970e Mon Sep 17 00:00:00 2001 From: Dominic Date: Mon, 28 Dec 2020 18:57:47 +0100 Subject: [PATCH 112/170] add include directive to Cargo.toml to reduce crate size --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 37d2bac..7bd596b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ keywords = ["gotham", "rest", "restful", "web", "http"] license = "EPL-2.0 OR Apache-2.0" readme = "README.md" repository = "https://gitlab.com/msrd0/gotham-restful" +include = ["src/**/*", "LICENSE.md", "LICENSE-*", "README.md", "CHANGELOG.md"] [badges] gitlab = { repository = "msrd0/gotham-restful", branch = "master" } From 2b8796b9c96a3d6afd944f8aea90ff04a01b9771 Mon Sep 17 00:00:00 2001 From: Dominic Date: Fri, 1 Jan 2021 15:43:40 +0100 Subject: [PATCH 113/170] update readme --- README.md | 23 +++++++++++++---------- README.tpl | 17 +++++++++++++---- src/lib.rs | 6 ------ 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 99cf632..bd8225d 100644 --- a/README.md +++ b/README.md @@ -26,10 +26,6 @@
-**Note:** The `stable` branch contains some bugfixes against the last release. The `master` -branch currently tracks gotham's master branch and the next release will use gotham 0.5.0 and be -compatible with the new future / async stuff. - This crate is an extension to the popular [gotham web framework][gotham] for Rust. It allows you to create resources with assigned methods that aim to be a more convenient way of creating handlers for requests. @@ -325,12 +321,6 @@ struct Foo; There is a lack of good examples, but there is currently a collection of code in the [example] directory, that might help you. Any help writing more examples is highly appreciated. -## License - -Licensed under your option of: - - [Apache License Version 2.0](https://gitlab.com/msrd0/gotham-restful/blob/master/LICENSE-Apache) - - [Eclipse Public License Version 2.0](https://gitlab.com/msrd0/gotham-restful/blob/master/LICENSE-EPL) - [diesel]: https://diesel.rs/ [example]: https://gitlab.com/msrd0/gotham-restful/tree/master/example @@ -340,3 +330,16 @@ Licensed under your option of: [`QueryStringExtractor`]: ../gotham/extractor/trait.QueryStringExtractor.html [`RequestBody`]: trait.RequestBody.html [`State`]: ../gotham/state/struct.State.html + +## Versioning + +Like all rust crates, this crate will follow semantic versioning guidelines. However, changing +the MSRV (minimum supported rust version) is not considered a breaking change. + +## License + +Copyright (C) 2020-2021 Dominic Meiser and [contributors](https://gitlab.com/msrd0/gotham-restful/-/graphs/master). + +Licensed under your option of: + - [Apache License Version 2.0](https://gitlab.com/msrd0/gotham-restful/blob/master/LICENSE-Apache) + - [Eclipse Public License Version 2.0](https://gitlab.com/msrd0/gotham-restful/blob/master/LICENSE-EPL) diff --git a/README.tpl b/README.tpl index c87a454..bc1ef71 100644 --- a/README.tpl +++ b/README.tpl @@ -26,8 +26,17 @@
-**Note:** The `stable` branch contains some bugfixes against the last release. The `master` -branch currently tracks gotham's master branch and the next release will use gotham 0.5.0 and be -compatible with the new future / async stuff. - {{readme}} + +## Versioning + +Like all rust crates, this crate will follow semantic versioning guidelines. However, changing +the MSRV (minimum supported rust version) is not considered a breaking change. + +## License + +Copyright (C) 2020-2021 Dominic Meiser and [contributors](https://gitlab.com/msrd0/gotham-restful/-/graphs/master). + +Licensed under your option of: + - [Apache License Version 2.0](https://gitlab.com/msrd0/gotham-restful/blob/master/LICENSE-Apache) + - [Eclipse Public License Version 2.0](https://gitlab.com/msrd0/gotham-restful/blob/master/LICENSE-EPL) diff --git a/src/lib.rs b/src/lib.rs index 918e944..1d234e9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -362,12 +362,6 @@ struct Foo; There is a lack of good examples, but there is currently a collection of code in the [example] directory, that might help you. Any help writing more examples is highly appreciated. -# License - -Licensed under your option of: - - [Apache License Version 2.0](https://gitlab.com/msrd0/gotham-restful/blob/master/LICENSE-Apache) - - [Eclipse Public License Version 2.0](https://gitlab.com/msrd0/gotham-restful/blob/master/LICENSE-EPL) - [diesel]: https://diesel.rs/ [example]: https://gitlab.com/msrd0/gotham-restful/tree/master/example From 141e5ac2d705cfa09f82a1f4c59e979a1509bc8e Mon Sep 17 00:00:00 2001 From: Dominic Date: Fri, 1 Jan 2021 15:56:47 +0100 Subject: [PATCH 114/170] bump version to 0.2.0-dev and change license to Apache-2.0 only --- Cargo.toml | 11 +- LICENSE-Apache => LICENSE | 0 LICENSE-EPL | 277 -------------------------------------- LICENSE.md | 6 - README.md | 16 ++- derive/Cargo.toml | 7 +- derive/LICENSE | 1 + derive/LICENSE-Apache | 1 - derive/LICENSE-EPL | 1 - derive/LICENSE.md | 1 - example/Cargo.toml | 11 +- 11 files changed, 30 insertions(+), 302 deletions(-) rename LICENSE-Apache => LICENSE (100%) delete mode 100644 LICENSE-EPL delete mode 100644 LICENSE.md create mode 120000 derive/LICENSE delete mode 120000 derive/LICENSE-Apache delete mode 120000 derive/LICENSE-EPL delete mode 120000 derive/LICENSE.md diff --git a/Cargo.toml b/Cargo.toml index 7bd596b..b78e7c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,19 +1,20 @@ # -*- eval: (cargo-minor-mode 1) -*- [workspace] -members = ["derive", "example"] +members = [".", "./derive", "./example"] [package] name = "gotham_restful" -version = "0.1.1" +version = "0.2.0-dev" authors = ["Dominic Meiser "] edition = "2018" description = "RESTful additions for the gotham web framework" keywords = ["gotham", "rest", "restful", "web", "http"] -license = "EPL-2.0 OR Apache-2.0" +categories = ["web-programming", "web-programming::http-server"] +license = "Apache-2.0" readme = "README.md" repository = "https://gitlab.com/msrd0/gotham-restful" -include = ["src/**/*", "LICENSE.md", "LICENSE-*", "README.md", "CHANGELOG.md"] +include = ["src/**/*", "LICENSE", "README.md", "CHANGELOG.md"] [badges] gitlab = { repository = "msrd0/gotham-restful", branch = "master" } @@ -27,7 +28,7 @@ futures-util = "0.3.7" gotham = { version = "0.5.0", default-features = false } gotham_derive = "0.5.0" gotham_middleware_diesel = { version = "0.2.0", optional = true } -gotham_restful_derive = { version = "0.1.1" } +gotham_restful_derive = "0.2.0-dev" indexmap = { version = "1.3.2", optional = true } itertools = { version = "0.10.0", optional = true } jsonwebtoken = { version = "7.1.0", optional = true } diff --git a/LICENSE-Apache b/LICENSE similarity index 100% rename from LICENSE-Apache rename to LICENSE diff --git a/LICENSE-EPL b/LICENSE-EPL deleted file mode 100644 index d3087e4..0000000 --- a/LICENSE-EPL +++ /dev/null @@ -1,277 +0,0 @@ -Eclipse Public License - v 2.0 - - THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE - PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION - OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. - -1. DEFINITIONS - -"Contribution" means: - - a) in the case of the initial Contributor, the initial content - Distributed under this Agreement, and - - b) in the case of each subsequent Contributor: - i) changes to the Program, and - ii) additions to the Program; - where such changes and/or additions to the Program originate from - and are Distributed by that particular Contributor. A Contribution - "originates" from a Contributor if it was added to the Program by - such Contributor itself or anyone acting on such Contributor's behalf. - Contributions do not include changes or additions to the Program that - are not Modified Works. - -"Contributor" means any person or entity that Distributes the Program. - -"Licensed Patents" mean patent claims licensable by a Contributor which -are necessarily infringed by the use or sale of its Contribution alone -or when combined with the Program. - -"Program" means the Contributions Distributed in accordance with this -Agreement. - -"Recipient" means anyone who receives the Program under this Agreement -or any Secondary License (as applicable), including Contributors. - -"Derivative Works" shall mean any work, whether in Source Code or other -form, that is based on (or derived from) the Program and for which the -editorial revisions, annotations, elaborations, or other modifications -represent, as a whole, an original work of authorship. - -"Modified Works" shall mean any work in Source Code or other form that -results from an addition to, deletion from, or modification of the -contents of the Program, including, for purposes of clarity any new file -in Source Code form that contains any contents of the Program. Modified -Works shall not include works that contain only declarations, -interfaces, types, classes, structures, or files of the Program solely -in each case in order to link to, bind by name, or subclass the Program -or Modified Works thereof. - -"Distribute" means the acts of a) distributing or b) making available -in any manner that enables the transfer of a copy. - -"Source Code" means the form of a Program preferred for making -modifications, including but not limited to software source code, -documentation source, and configuration files. - -"Secondary License" means either the GNU General Public License, -Version 2.0, or any later versions of that license, including any -exceptions or additional permissions as identified by the initial -Contributor. - -2. GRANT OF RIGHTS - - a) Subject to the terms of this Agreement, each Contributor hereby - grants Recipient a non-exclusive, worldwide, royalty-free copyright - license to reproduce, prepare Derivative Works of, publicly display, - publicly perform, Distribute and sublicense the Contribution of such - Contributor, if any, and such Derivative Works. - - b) Subject to the terms of this Agreement, each Contributor hereby - grants Recipient a non-exclusive, worldwide, royalty-free patent - license under Licensed Patents to make, use, sell, offer to sell, - import and otherwise transfer the Contribution of such Contributor, - if any, in Source Code or other form. This patent license shall - apply to the combination of the Contribution and the Program if, at - the time the Contribution is added by the Contributor, such addition - of the Contribution causes such combination to be covered by the - Licensed Patents. The patent license shall not apply to any other - combinations which include the Contribution. No hardware per se is - licensed hereunder. - - c) Recipient understands that although each Contributor grants the - licenses to its Contributions set forth herein, no assurances are - provided by any Contributor that the Program does not infringe the - patent or other intellectual property rights of any other entity. - Each Contributor disclaims any liability to Recipient for claims - brought by any other entity based on infringement of intellectual - property rights or otherwise. As a condition to exercising the - rights and licenses granted hereunder, each Recipient hereby - assumes sole responsibility to secure any other intellectual - property rights needed, if any. For example, if a third party - patent license is required to allow Recipient to Distribute the - Program, it is Recipient's responsibility to acquire that license - before distributing the Program. - - d) Each Contributor represents that to its knowledge it has - sufficient copyright rights in its Contribution, if any, to grant - the copyright license set forth in this Agreement. - - e) Notwithstanding the terms of any Secondary License, no - Contributor makes additional grants to any Recipient (other than - those set forth in this Agreement) as a result of such Recipient's - receipt of the Program under the terms of a Secondary License - (if permitted under the terms of Section 3). - -3. REQUIREMENTS - -3.1 If a Contributor Distributes the Program in any form, then: - - a) the Program must also be made available as Source Code, in - accordance with section 3.2, and the Contributor must accompany - the Program with a statement that the Source Code for the Program - is available under this Agreement, and informs Recipients how to - obtain it in a reasonable manner on or through a medium customarily - used for software exchange; and - - b) the Contributor may Distribute the Program under a license - different than this Agreement, provided that such license: - i) effectively disclaims on behalf of all other Contributors all - warranties and conditions, express and implied, including - warranties or conditions of title and non-infringement, and - implied warranties or conditions of merchantability and fitness - for a particular purpose; - - ii) effectively excludes on behalf of all other Contributors all - liability for damages, including direct, indirect, special, - incidental and consequential damages, such as lost profits; - - iii) does not attempt to limit or alter the recipients' rights - in the Source Code under section 3.2; and - - iv) requires any subsequent distribution of the Program by any - party to be under a license that satisfies the requirements - of this section 3. - -3.2 When the Program is Distributed as Source Code: - - a) it must be made available under this Agreement, or if the - Program (i) is combined with other material in a separate file or - files made available under a Secondary License, and (ii) the initial - Contributor attached to the Source Code the notice described in - Exhibit A of this Agreement, then the Program may be made available - under the terms of such Secondary Licenses, and - - b) a copy of this Agreement must be included with each copy of - the Program. - -3.3 Contributors may not remove or alter any copyright, patent, -trademark, attribution notices, disclaimers of warranty, or limitations -of liability ("notices") contained within the Program from any copy of -the Program which they Distribute, provided that Contributors may add -their own appropriate notices. - -4. COMMERCIAL DISTRIBUTION - -Commercial distributors of software may accept certain responsibilities -with respect to end users, business partners and the like. While this -license is intended to facilitate the commercial use of the Program, -the Contributor who includes the Program in a commercial product -offering should do so in a manner which does not create potential -liability for other Contributors. Therefore, if a Contributor includes -the Program in a commercial product offering, such Contributor -("Commercial Contributor") hereby agrees to defend and indemnify every -other Contributor ("Indemnified Contributor") against any losses, -damages and costs (collectively "Losses") arising from claims, lawsuits -and other legal actions brought by a third party against the Indemnified -Contributor to the extent caused by the acts or omissions of such -Commercial Contributor in connection with its distribution of the Program -in a commercial product offering. The obligations in this section do not -apply to any claims or Losses relating to any actual or alleged -intellectual property infringement. In order to qualify, an Indemnified -Contributor must: a) promptly notify the Commercial Contributor in -writing of such claim, and b) allow the Commercial Contributor to control, -and cooperate with the Commercial Contributor in, the defense and any -related settlement negotiations. The Indemnified Contributor may -participate in any such claim at its own expense. - -For example, a Contributor might include the Program in a commercial -product offering, Product X. That Contributor is then a Commercial -Contributor. If that Commercial Contributor then makes performance -claims, or offers warranties related to Product X, those performance -claims and warranties are such Commercial Contributor's responsibility -alone. Under this section, the Commercial Contributor would have to -defend claims against the other Contributors related to those performance -claims and warranties, and if a court requires any other Contributor to -pay any damages as a result, the Commercial Contributor must pay -those damages. - -5. NO WARRANTY - -EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT -PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" -BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR -IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF -TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR -PURPOSE. Each Recipient is solely responsible for determining the -appropriateness of using and distributing the Program and assumes all -risks associated with its exercise of rights under this Agreement, -including but not limited to the risks and costs of program errors, -compliance with applicable laws, damage to or loss of data, programs -or equipment, and unavailability or interruption of operations. - -6. DISCLAIMER OF LIABILITY - -EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT -PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS -SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST -PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE -EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - -7. GENERAL - -If any provision of this Agreement is invalid or unenforceable under -applicable law, it shall not affect the validity or enforceability of -the remainder of the terms of this Agreement, and without further -action by the parties hereto, such provision shall be reformed to the -minimum extent necessary to make such provision valid and enforceable. - -If Recipient institutes patent litigation against any entity -(including a cross-claim or counterclaim in a lawsuit) alleging that the -Program itself (excluding combinations of the Program with other software -or hardware) infringes such Recipient's patent(s), then such Recipient's -rights granted under Section 2(b) shall terminate as of the date such -litigation is filed. - -All Recipient's rights under this Agreement shall terminate if it -fails to comply with any of the material terms or conditions of this -Agreement and does not cure such failure in a reasonable period of -time after becoming aware of such noncompliance. If all Recipient's -rights under this Agreement terminate, Recipient agrees to cease use -and distribution of the Program as soon as reasonably practicable. -However, Recipient's obligations under this Agreement and any licenses -granted by Recipient relating to the Program shall continue and survive. - -Everyone is permitted to copy and distribute copies of this Agreement, -but in order to avoid inconsistency the Agreement is copyrighted and -may only be modified in the following manner. The Agreement Steward -reserves the right to publish new versions (including revisions) of -this Agreement from time to time. No one other than the Agreement -Steward has the right to modify this Agreement. The Eclipse Foundation -is the initial Agreement Steward. The Eclipse Foundation may assign the -responsibility to serve as the Agreement Steward to a suitable separate -entity. Each new version of the Agreement will be given a distinguishing -version number. The Program (including Contributions) may always be -Distributed subject to the version of the Agreement under which it was -received. In addition, after a new version of the Agreement is published, -Contributor may elect to Distribute the Program (including its -Contributions) under the new version. - -Except as expressly stated in Sections 2(a) and 2(b) above, Recipient -receives no rights or licenses to the intellectual property of any -Contributor under this Agreement, whether expressly, by implication, -estoppel or otherwise. All rights in the Program not expressly granted -under this Agreement are reserved. Nothing in this Agreement is intended -to be enforceable by any entity that is not a Contributor or Recipient. -No third-party beneficiary rights are created under this Agreement. - -Exhibit A - Form of Secondary Licenses Notice - -"This Source Code may also be made available under the following -Secondary Licenses when the conditions for such availability set forth -in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), -version(s), and exceptions or additional permissions here}." - - Simply including a copy of this Agreement, including this Exhibit A - is not sufficient to license the Source Code under Secondary Licenses. - - If it is not possible or desirable to put the notice in a particular - file, then You may include the notice in a location (such as a LICENSE - file in a relevant directory) where a recipient would be likely to - look for such a notice. - - You may add additional accurate notices of copyright ownership. diff --git a/LICENSE.md b/LICENSE.md deleted file mode 100644 index 50a1376..0000000 --- a/LICENSE.md +++ /dev/null @@ -1,6 +0,0 @@ -Copyright 2019 Dominic Meiser - -The Gotham-Restful project is licensed under your option of: - - [Apache License Version 2.0](https://gitlab.com/msrd0/gotham-restful/blob/master/LICENSE-Apache) - - [Eclipse Public License Version 2.0](https://gitlab.com/msrd0/gotham-restful/blob/master/LICENSE-EPL) - \ No newline at end of file diff --git a/README.md b/README.md index bd8225d..3b5c495 100644 --- a/README.md +++ b/README.md @@ -340,6 +340,16 @@ the MSRV (minimum supported rust version) is not considered a breaking change. Copyright (C) 2020-2021 Dominic Meiser and [contributors](https://gitlab.com/msrd0/gotham-restful/-/graphs/master). -Licensed under your option of: - - [Apache License Version 2.0](https://gitlab.com/msrd0/gotham-restful/blob/master/LICENSE-Apache) - - [Eclipse Public License Version 2.0](https://gitlab.com/msrd0/gotham-restful/blob/master/LICENSE-EPL) +``` +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +``` diff --git a/derive/Cargo.toml b/derive/Cargo.toml index f06023c..8e0fb87 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -2,13 +2,14 @@ [package] name = "gotham_restful_derive" -version = "0.1.1" +version = "0.2.0-dev" authors = ["Dominic Meiser "] edition = "2018" -description = "RESTful additions for the gotham web framework - Derive" +description = "Derive macros for gotham_restful" keywords = ["gotham", "rest", "restful", "web", "http"] -license = "EPL-2.0 OR Apache-2.0" +license = "Apache-2.0" repository = "https://gitlab.com/msrd0/gotham-restful" +workspace = ".." [lib] proc-macro = true diff --git a/derive/LICENSE b/derive/LICENSE new file mode 120000 index 0000000..ea5b606 --- /dev/null +++ b/derive/LICENSE @@ -0,0 +1 @@ +../LICENSE \ No newline at end of file diff --git a/derive/LICENSE-Apache b/derive/LICENSE-Apache deleted file mode 120000 index 0cd69a3..0000000 --- a/derive/LICENSE-Apache +++ /dev/null @@ -1 +0,0 @@ -../LICENSE-Apache \ No newline at end of file diff --git a/derive/LICENSE-EPL b/derive/LICENSE-EPL deleted file mode 120000 index 2004d06..0000000 --- a/derive/LICENSE-EPL +++ /dev/null @@ -1 +0,0 @@ -../LICENSE-EPL \ No newline at end of file diff --git a/derive/LICENSE.md b/derive/LICENSE.md deleted file mode 120000 index 7eabdb1..0000000 --- a/derive/LICENSE.md +++ /dev/null @@ -1 +0,0 @@ -../LICENSE.md \ No newline at end of file diff --git a/example/Cargo.toml b/example/Cargo.toml index 96c8e62..f9fdab1 100644 --- a/example/Cargo.toml +++ b/example/Cargo.toml @@ -2,22 +2,23 @@ [package] name = "example" -version = "0.0.1" +version = "0.0.0" authors = ["Dominic Meiser "] edition = "2018" license = "Unlicense" readme = "README.md" -include = ["src/**/*", "Cargo.toml", "LICENSE"] repository = "https://gitlab.com/msrd0/gotham-restful" +publish = false +workspace = ".." [badges] gitlab = { repository = "msrd0/gotham-restful", branch = "master" } [dependencies] fake = "2.2.2" -gotham = { version = "0.5.0-rc.1", default-features = false } -gotham_derive = "0.5.0-rc.1" -gotham_restful = { version = "0.1.0-rc0", features = ["auth", "openapi"] } +gotham = { version = "0.5.0", default-features = false } +gotham_derive = "0.5.0" +gotham_restful = { version = "0.2.0-dev", features = ["auth", "openapi"] } log = "0.4.8" pretty_env_logger = "0.4" serde = "1.0.110" From b005346e541df9fc4542ee4b1f3ba2590dca1b26 Mon Sep 17 00:00:00 2001 From: Dominic Date: Fri, 1 Jan 2021 16:18:50 +0100 Subject: [PATCH 115/170] fix the readme template --- README.tpl | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/README.tpl b/README.tpl index bc1ef71..42ce8d6 100644 --- a/README.tpl +++ b/README.tpl @@ -37,6 +37,16 @@ the MSRV (minimum supported rust version) is not considered a breaking change. Copyright (C) 2020-2021 Dominic Meiser and [contributors](https://gitlab.com/msrd0/gotham-restful/-/graphs/master). -Licensed under your option of: - - [Apache License Version 2.0](https://gitlab.com/msrd0/gotham-restful/blob/master/LICENSE-Apache) - - [Eclipse Public License Version 2.0](https://gitlab.com/msrd0/gotham-restful/blob/master/LICENSE-EPL) +``` +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +``` From 766bc9d17d8481966c435dc5e19d97f3e1642933 Mon Sep 17 00:00:00 2001 From: Dominic Date: Fri, 1 Jan 2021 16:44:55 +0100 Subject: [PATCH 116/170] support copying headers in cors preflight requests --- CHANGELOG.md | 2 + Cargo.toml | 3 +- src/cors.rs | 111 ++++++++++++++++++++++++++++++----------- src/lib.rs | 8 +-- tests/cors_handling.rs | 111 ++++++++++++++++++++++++++++++++++++----- 5 files changed, 188 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a561d6f..bf19e66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Changed + - The cors handler can now copy headers from the request if desired ## [0.1.1] - 2020-12-28 ### Added diff --git a/Cargo.toml b/Cargo.toml index b78e7c3..2d7d073 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,6 @@ gotham_derive = "0.5.0" gotham_middleware_diesel = { version = "0.2.0", optional = true } gotham_restful_derive = "0.2.0-dev" indexmap = { version = "1.3.2", optional = true } -itertools = { version = "0.10.0", optional = true } jsonwebtoken = { version = "7.1.0", optional = true } log = "0.4.8" mime = "0.3.16" @@ -49,7 +48,7 @@ trybuild = "1.0.27" [features] default = ["cors", "errorlog"] auth = ["gotham_restful_derive/auth", "base64", "cookie", "jsonwebtoken"] -cors = ["itertools"] +cors = [] errorlog = [] database = ["gotham_restful_derive/database", "gotham_middleware_diesel"] openapi = ["gotham_restful_derive/openapi", "indexmap", "openapiv3"] diff --git a/src/cors.rs b/src/cors.rs index 5463de1..d8f2988 100644 --- a/src/cors.rs +++ b/src/cors.rs @@ -5,7 +5,7 @@ use gotham::{ header::{ HeaderMap, HeaderName, HeaderValue, ACCESS_CONTROL_ALLOW_CREDENTIALS, ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_MAX_AGE, - ACCESS_CONTROL_REQUEST_METHOD, ORIGIN, VARY + ACCESS_CONTROL_REQUEST_HEADERS, ACCESS_CONTROL_REQUEST_METHOD, ORIGIN, VARY }, Body, Method, Response, StatusCode }, @@ -14,7 +14,6 @@ use gotham::{ router::{builder::*, route::matcher::AccessControlRequestMethodMatcher}, state::{FromState, State} }; -use itertools::Itertools; use std::{panic::RefUnwindSafe, pin::Pin}; /** @@ -53,6 +52,52 @@ impl Origin { } } } + + /// Returns true if the `Vary` header has to include `Origin`. + fn varies(&self) -> bool { + matches!(self, Self::Copy) + } +} + +/** +Specify the allowed headers of the request. It is up to the browser to check that only the allowed +headers are sent with the request. +*/ +#[derive(Clone, Debug)] +pub enum Headers { + /// Do not send any `Access-Control-Allow-Headers` headers. + None, + /// Set the `Access-Control-Allow-Headers` header to the following header list. If empty, this + /// is treated as if it was [None]. + List(Vec), + /// Copy the `Access-Control-Request-Headers` header into the `Access-Control-Allow-Header` + /// header. + Copy +} + +impl Default for Headers { + fn default() -> Self { + Self::None + } +} + +impl Headers { + /// Get the header value for the `Access-Control-Allow-Headers` header. + fn header_value(&self, state: &State) -> Option { + match self { + Self::None => None, + Self::List(list) => Some(list.join(",").parse().unwrap()), + Self::Copy => { + let headers = HeaderMap::borrow_from(state); + headers.get(ACCESS_CONTROL_REQUEST_HEADERS).map(Clone::clone) + } + } + } + + /// Returns true if the `Vary` header has to include `Origin`. + fn varies(&self) -> bool { + matches!(self, Self::Copy) + } } /** @@ -63,7 +108,7 @@ To change settings, you need to put this type into gotham's [State]: ```rust,no_run # use gotham::{router::builder::*, pipeline::{new_pipeline, single::single_pipeline}, state::State}; -# use gotham_restful::*; +# use gotham_restful::{*, cors::Origin}; fn main() { let cors = CorsConfig { origin: Origin::Star, @@ -81,7 +126,7 @@ configurations for different scopes, you need to register the middleware inside ```rust,no_run # use gotham::{router::builder::*, pipeline::*, pipeline::set::*, state::State}; -# use gotham_restful::*; +# use gotham_restful::{*, cors::Origin}; let pipelines = new_pipeline_set(); // The first cors configuration @@ -119,7 +164,7 @@ pub struct CorsConfig { /// The allowed origins. pub origin: Origin, /// The allowed headers. - pub headers: Vec, + pub headers: Headers, /// The amount of seconds that the preflight request can be cached. pub max_age: u64, /// Whether or not the request may be made with supplying credentials. @@ -149,22 +194,24 @@ For further information on CORS, read */ pub fn handle_cors(state: &State, res: &mut Response) { let config = CorsConfig::try_borrow_from(state); - let headers = res.headers_mut(); + if let Some(cfg) = config { + let headers = res.headers_mut(); - // non-preflight requests require the Access-Control-Allow-Origin header - if let Some(header) = config.and_then(|cfg| cfg.origin.header_value(state)) { - headers.insert(ACCESS_CONTROL_ALLOW_ORIGIN, header); - } + // non-preflight requests require the Access-Control-Allow-Origin header + if let Some(header) = cfg.origin.header_value(state) { + headers.insert(ACCESS_CONTROL_ALLOW_ORIGIN, 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)) { - let vary = headers.get(VARY).map(|vary| format!("{},Origin", vary.to_str().unwrap())); - headers.insert(VARY, vary.as_deref().unwrap_or("Origin").parse().unwrap()); - } + // if the origin is copied over, we should tell the browser by specifying the Vary header + if cfg.origin.varies() { + let vary = headers.get(VARY).map(|vary| format!("{},origin", vary.to_str().unwrap())); + headers.insert(VARY, vary.as_deref().unwrap_or("origin").parse().unwrap()); + } - // if we allow credentials, tell the browser - if config.map(|cfg| cfg.credentials).unwrap_or(false) { - headers.insert(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true".parse().unwrap()); + // if we allow credentials, tell the browser + if cfg.credentials { + headers.insert(ACCESS_CONTROL_ALLOW_CREDENTIALS, HeaderValue::from_static("true")); + } } } @@ -202,6 +249,7 @@ fn cors_preflight_handler(state: State) -> (State, Response) { // prepare the response let mut res = create_empty_response(&state, StatusCode::NO_CONTENT); let headers = res.headers_mut(); + let mut vary: Vec = Vec::new(); // copy the request method over to the response let method = HeaderMap::borrow_from(&state) @@ -209,22 +257,27 @@ fn cors_preflight_handler(state: State) -> (State, Response) { .unwrap() .clone(); headers.insert(ACCESS_CONTROL_ALLOW_METHODS, method); + vary.push(ACCESS_CONTROL_REQUEST_METHOD); - // if we allow any headers, put them in - if let Some(hdrs) = config.map(|cfg| &cfg.headers) { - if hdrs.len() > 0 { - // 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()); + if let Some(cfg) = config { + // if we allow any headers, copy them over + if let Some(header) = cfg.headers.header_value(&state) { + headers.insert(ACCESS_CONTROL_ALLOW_HEADERS, header); + } + + // if the headers are copied over, we should tell the browser by specifying the Vary header + if cfg.headers.varies() { + vary.push(ACCESS_CONTROL_REQUEST_HEADERS); + } + + // set the max age for the preflight cache + if let Some(age) = config.map(|cfg| cfg.max_age) { + headers.insert(ACCESS_CONTROL_MAX_AGE, age.into()); } } - // set the max age for the preflight cache - if let Some(age) = config.map(|cfg| cfg.max_age) { - headers.insert(ACCESS_CONTROL_MAX_AGE, age.into()); - } - // make sure the browser knows that this request was based on the method - headers.insert(VARY, "Access-Control-Request-Method".parse().unwrap()); + headers.insert(VARY, vary.join(",").parse().unwrap()); handle_cors(&state, &mut res); (state, res) diff --git a/src/lib.rs b/src/lib.rs index 1d234e9..0ca5549 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -196,7 +196,7 @@ authentication), and every content type, could look like this: # #[cfg(feature = "cors")] # mod cors_feature_enabled { # use gotham::{hyper::header::*, router::builder::*, pipeline::{new_pipeline, single::single_pipeline}, state::State}; -# use gotham_restful::*; +# use gotham_restful::{*, cors::*}; # use serde::{Deserialize, Serialize}; #[derive(Resource)] #[resource(read_all)] @@ -210,7 +210,7 @@ fn read_all() { fn main() { let cors = CorsConfig { origin: Origin::Copy, - headers: vec![CONTENT_TYPE], + headers: Headers::List(vec![CONTENT_TYPE]), max_age: 0, credentials: true }; @@ -417,9 +417,9 @@ mod auth; pub use auth::{AuthHandler, AuthMiddleware, AuthSource, AuthStatus, AuthValidation, StaticAuthHandler}; #[cfg(feature = "cors")] -mod cors; +pub mod cors; #[cfg(feature = "cors")] -pub use cors::{handle_cors, CorsConfig, CorsRoute, Origin}; +pub use cors::{handle_cors, CorsConfig, CorsRoute}; #[cfg(feature = "openapi")] mod openapi; diff --git a/tests/cors_handling.rs b/tests/cors_handling.rs index 35d5841..226b3d2 100644 --- a/tests/cors_handling.rs +++ b/tests/cors_handling.rs @@ -5,8 +5,11 @@ use gotham::{ router::builder::*, test::{Server, TestRequest, TestServer} }; -use gotham_restful::{change_all, read_all, CorsConfig, DrawResources, Origin, Raw, Resource}; -use itertools::Itertools; +use gotham_restful::{ + change_all, + cors::{Headers, Origin}, + read_all, CorsConfig, DrawResources, Raw, Resource +}; use mime::TEXT_PLAIN; #[derive(Resource)] @@ -35,7 +38,7 @@ where .unwrap(); assert_eq!(res.status(), StatusCode::NO_CONTENT); let headers = res.headers(); - println!("{}", headers.keys().join(",")); + println!("{}", headers.keys().map(|name| name.as_str()).collect::>().join(",")); assert_eq!( headers .get(ACCESS_CONTROL_ALLOW_ORIGIN) @@ -65,7 +68,7 @@ fn test_preflight(server: &TestServer, method: &str, origin: Option<&str>, vary: .unwrap(); assert_eq!(res.status(), StatusCode::NO_CONTENT); let headers = res.headers(); - println!("{}", headers.keys().join(",")); + println!("{}", headers.keys().map(|name| name.as_str()).collect::>().join(",")); assert_eq!( headers .get(ACCESS_CONTROL_ALLOW_METHODS) @@ -98,12 +101,45 @@ fn test_preflight(server: &TestServer, method: &str, origin: Option<&str>, vary: ); } +fn test_preflight_headers( + server: &TestServer, + method: &str, + request_headers: Option<&str>, + allowed_headers: Option<&str>, + vary: &str +) { + let client = server.client(); + let mut res = client + .options("http://example.org/foo") + .with_header(ACCESS_CONTROL_REQUEST_METHOD, method.parse().unwrap()) + .with_header(ORIGIN, "http://example.org".parse().unwrap()); + if let Some(hdr) = request_headers { + res = res.with_header(ACCESS_CONTROL_REQUEST_HEADERS, hdr.parse().unwrap()); + } + let res = res.perform().unwrap(); + assert_eq!(res.status(), StatusCode::NO_CONTENT); + let headers = res.headers(); + println!("{}", headers.keys().map(|name| name.as_str()).collect::>().join(",")); + if let Some(hdr) = allowed_headers { + assert_eq!( + headers + .get(ACCESS_CONTROL_ALLOW_HEADERS) + .and_then(|value| value.to_str().ok()) + .as_deref(), + Some(hdr) + ) + } else { + assert!(!headers.contains_key(ACCESS_CONTROL_ALLOW_HEADERS)); + } + assert_eq!(headers.get(VARY).and_then(|value| value.to_str().ok()).as_deref(), Some(vary)); +} + #[test] fn cors_origin_none() { let cfg = Default::default(); 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( @@ -122,7 +158,7 @@ fn cors_origin_star() { }; let server = test_server(cfg); - 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( @@ -145,7 +181,7 @@ fn cors_origin_single() { &server, "PUT", Some("https://foo.com"), - "Access-Control-Request-Method", + "access-control-request-method", false, 0 ); @@ -176,7 +212,7 @@ fn cors_origin_copy() { &server, "PUT", Some("http://example.org"), - "Access-Control-Request-Method,Origin", + "access-control-request-method,origin", false, 0 ); @@ -184,17 +220,68 @@ fn cors_origin_copy() { test_response( server.client().get("http://example.org/foo"), Some("http://example.org"), - Some("Origin"), + Some("origin"), false ); test_response( server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN), Some("http://example.org"), - Some("Origin"), + Some("origin"), false ); } +#[test] +fn cors_headers_none() { + let cfg = Default::default(); + let server = test_server(cfg); + + test_preflight_headers(&server, "PUT", None, None, "access-control-request-method"); + test_preflight_headers(&server, "PUT", Some("Content-Type"), None, "access-control-request-method"); +} + +#[test] +fn cors_headers_list() { + let cfg = CorsConfig { + headers: Headers::List(vec![CONTENT_TYPE]), + ..Default::default() + }; + let server = test_server(cfg); + + test_preflight_headers(&server, "PUT", None, Some("content-type"), "access-control-request-method"); + test_preflight_headers( + &server, + "PUT", + Some("content-type"), + Some("content-type"), + "access-control-request-method" + ); +} + +#[test] +fn cors_headers_copy() { + let cfg = CorsConfig { + headers: Headers::Copy, + ..Default::default() + }; + let server = test_server(cfg); + + test_preflight_headers( + &server, + "PUT", + None, + None, + "access-control-request-method,access-control-request-headers" + ); + test_preflight_headers( + &server, + "PUT", + Some("content-type"), + Some("content-type"), + "access-control-request-method,access-control-request-headers" + ); +} + #[test] fn cors_credentials() { let cfg = CorsConfig { @@ -204,7 +291,7 @@ fn cors_credentials() { }; let server = test_server(cfg); - 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( @@ -224,7 +311,7 @@ fn cors_max_age() { }; let server = test_server(cfg); - 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( From 813c12614f13d71177377df93cc89be82f51128c Mon Sep 17 00:00:00 2001 From: Dominic Date: Fri, 1 Jan 2021 16:49:35 +0100 Subject: [PATCH 117/170] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3b5c495..9ed0bee 100644 --- a/README.md +++ b/README.md @@ -200,7 +200,7 @@ fn read_all() { fn main() { let cors = CorsConfig { origin: Origin::Copy, - headers: vec![CONTENT_TYPE], + headers: Headers::List(vec![CONTENT_TYPE]), max_age: 0, credentials: true }; From 6ee382242b270068595fa252d4c499df185d8797 Mon Sep 17 00:00:00 2001 From: Dominic Date: Fri, 1 Jan 2021 17:43:43 +0100 Subject: [PATCH 118/170] fix example --- example/src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example/src/main.rs b/example/src/main.rs index f2d03d0..db880d7 100644 --- a/example/src/main.rs +++ b/example/src/main.rs @@ -11,7 +11,7 @@ use gotham::{ router::builder::*, state::State }; -use gotham_restful::*; +use gotham_restful::{cors::*, *}; use serde::{Deserialize, Serialize}; #[derive(Resource)] @@ -101,7 +101,7 @@ fn main() { let cors = CorsConfig { origin: Origin::Copy, - headers: vec![CONTENT_TYPE], + headers: Headers::List(vec![CONTENT_TYPE]), credentials: true, ..Default::default() }; From 388bf8b49c72d4f79478b7767541f9ef7c497da2 Mon Sep 17 00:00:00 2001 From: Dominic Date: Fri, 1 Jan 2021 18:03:31 +0100 Subject: [PATCH 119/170] apply some clippy suggestions --- src/cors.rs | 5 ++++- src/lib.rs | 7 ++++++- src/response.rs | 4 ++++ src/result/mod.rs | 2 +- src/result/raw.rs | 2 +- src/result/result.rs | 2 +- src/result/success.rs | 2 +- src/routing.rs | 7 +++++-- 8 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/cors.rs b/src/cors.rs index d8f2988..7f1fc31 100644 --- a/src/cors.rs +++ b/src/cors.rs @@ -11,7 +11,10 @@ use gotham::{ }, middleware::Middleware, pipeline::chain::PipelineHandleChain, - router::{builder::*, route::matcher::AccessControlRequestMethodMatcher}, + router::{ + builder::{DefineSingleRoute, DrawRoutes, ExtendRouteMatcher}, + route::matcher::AccessControlRequestMethodMatcher + }, state::{FromState, State} }; use std::{panic::RefUnwindSafe, pin::Pin}; diff --git a/src/lib.rs b/src/lib.rs index 0ca5549..24e8297 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,10 @@ #![allow(clippy::tabs_in_doc_comments)] -#![warn(missing_debug_implementations, rust_2018_idioms)] +#![warn( + missing_debug_implementations, + rust_2018_idioms, + clippy::wildcard_imports, + clippy::redundant_closure_for_method_calls +)] #![deny(broken_intra_doc_links)] #![forbid(unsafe_code)] /*! diff --git a/src/response.rs b/src/response.rs index 5fe1d32..cc50325 100644 --- a/src/response.rs +++ b/src/response.rs @@ -11,6 +11,7 @@ pub struct Response { impl Response { /// Create a new [Response] from raw data. + #[must_use = "Creating a response is pointless if you don't use it"] pub fn new>(status: StatusCode, body: B, mime: Option) -> Self { Self { status, @@ -20,6 +21,7 @@ impl Response { } /// Create a [Response] with mime type json from already serialized data. + #[must_use = "Creating a response is pointless if you don't use it"] pub fn json>(status: StatusCode, body: B) -> Self { Self { status, @@ -29,6 +31,7 @@ impl Response { } /// Create a _204 No Content_ [Response]. + #[must_use = "Creating a response is pointless if you don't use it"] pub fn no_content() -> Self { Self { status: StatusCode::NO_CONTENT, @@ -38,6 +41,7 @@ impl Response { } /// Create an empty _403 Forbidden_ [Response]. + #[must_use = "Creating a response is pointless if you don't use it"] pub fn forbidden() -> Self { Self { status: StatusCode::FORBIDDEN, diff --git a/src/result/mod.rs b/src/result/mod.rs index 2c79d37..526af33 100644 --- a/src/result/mod.rs +++ b/src/result/mod.rs @@ -125,7 +125,7 @@ where type Err = Res::Err; fn into_response(self) -> Pin> + Send>> { - self.then(|result| result.into_response()).boxed() + self.then(ResourceResult::into_response).boxed() } fn accepted_types() -> Option> { diff --git a/src/result/raw.rs b/src/result/raw.rs index dd174d0..6b3b520 100644 --- a/src/result/raw.rs +++ b/src/result/raw.rs @@ -92,7 +92,7 @@ where type Err = SerdeJsonError; // just for easier handling of `Result, E>` fn into_response(self) -> Pin> + 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))).boxed() } #[cfg(feature = "openapi")] diff --git a/src/result/result.rs b/src/result/result.rs index 71c969a..f22d756 100644 --- a/src/result/result.rs +++ b/src/result/result.rs @@ -71,7 +71,7 @@ mod test { let res = block_on(ok.into_response()).expect("didn't expect error response"); assert_eq!(res.status, StatusCode::OK); assert_eq!(res.mime, Some(APPLICATION_JSON)); - assert_eq!(res.full_body().unwrap(), r#"{"msg":""}"#.as_bytes()); + assert_eq!(res.full_body().unwrap(), br#"{"msg":""}"#); } #[test] diff --git a/src/result/success.rs b/src/result/success.rs index 7ed7cf7..a3816ea 100644 --- a/src/result/success.rs +++ b/src/result/success.rs @@ -129,7 +129,7 @@ mod test { let res = block_on(success.into_response()).expect("didn't expect error response"); assert_eq!(res.status, StatusCode::OK); assert_eq!(res.mime, Some(APPLICATION_JSON)); - assert_eq!(res.full_body().unwrap(), r#"{"msg":""}"#.as_bytes()); + assert_eq!(res.full_body().unwrap(), br#"{"msg":""}"#); } #[test] diff --git a/src/routing.rs b/src/routing.rs index ea4d5dd..940a51b 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -6,7 +6,10 @@ use crate::openapi::{ #[cfg(feature = "cors")] use crate::CorsRoute; use crate::{ - resource::*, + resource::{ + Resource, ResourceChange, ResourceChangeAll, ResourceCreate, ResourceRead, ResourceReadAll, ResourceRemove, + ResourceRemoveAll, ResourceSearch + }, result::{ResourceError, ResourceResult}, RequestBody, Response, StatusCode }; @@ -18,7 +21,7 @@ use gotham::{ hyper::{body::to_bytes, header::CONTENT_TYPE, Body, HeaderMap, Method}, pipeline::chain::PipelineHandleChain, router::{ - builder::*, + builder::{DefineSingleRoute, DrawRoutes, ExtendRouteMatcher, RouterBuilder, ScopeBuilder}, non_match::RouteNonMatch, route::matcher::{AcceptHeaderRouteMatcher, ContentTypeHeaderRouteMatcher, RouteMatcher} }, From 3600a115d0b5e7d5a6b872fac7f9fcdb80f30e1c Mon Sep 17 00:00:00 2001 From: Dominic Date: Fri, 1 Jan 2021 19:30:49 +0100 Subject: [PATCH 120/170] use rust 1.49 for trybuild ui testing --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f4e847f..e88eed3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -58,7 +58,7 @@ test-tarpaulin: test-trybuild-ui: stage: test - image: rust:1.48-slim + image: rust:1.49-slim before_script: - apt update -y - apt install -y --no-install-recommends libpq-dev From 44f3c9fe84717a38385826627f88a38755712877 Mon Sep 17 00:00:00 2001 From: Dominic Date: Thu, 14 Jan 2021 18:37:51 +0100 Subject: [PATCH 121/170] add headers to the response (#27) --- derive/src/resource_error.rs | 10 +++++----- src/response.rs | 30 ++++++++++++++++++++++++------ src/result/mod.rs | 2 ++ src/result/no_content.rs | 1 + src/result/raw.rs | 1 + src/result/result.rs | 1 + src/result/success.rs | 1 + src/routing.rs | 13 ++++++++++++- tests/resource_error.rs | 1 + 9 files changed, 48 insertions(+), 12 deletions(-) diff --git a/derive/src/resource_error.rs b/derive/src/resource_error.rs index e2b78d0..a2b0955 100644 --- a/derive/src/resource_error.rs +++ b/derive/src/resource_error.rs @@ -196,11 +196,11 @@ impl ErrorVariant { quote!(#from_field.into_response_error()) }, (Some(_), Some(_)) => return Err(Error::new(ident.span(), "When #[from] is used, #[status] must not be used!")), - (None, Some(status)) => quote!(Ok(#krate::Response { - status: { #status }.into(), - body: #krate::gotham::hyper::Body::empty(), - mime: None - })), + (None, Some(status)) => quote!(Ok(#krate::Response::new( + { #status }.into(), + #krate::gotham::hyper::Body::empty(), + None + ))), (None, None) => return Err(Error::new(ident.span(), "Missing #[status(code)] for this variant")) }; diff --git a/src/response.rs b/src/response.rs index cc50325..b29aa4e 100644 --- a/src/response.rs +++ b/src/response.rs @@ -1,14 +1,23 @@ -use gotham::hyper::{Body, StatusCode}; +use gotham::hyper::{ + header::{HeaderMap, HeaderName, HeaderValue}, + Body, StatusCode +}; use mime::{Mime, APPLICATION_JSON}; /// A response, used to create the final gotham response from. #[derive(Debug)] pub struct Response { + #[deprecated(since = "0.1.2", note = "This field will be private in an upcomming release")] pub status: StatusCode, + #[deprecated(since = "0.1.2", note = "This field will be private in an upcomming release")] pub body: Body, - pub mime: Option + #[deprecated(since = "0.1.2", note = "This field will be private in an upcomming release")] + pub mime: Option, + #[deprecated(since = "0.1.2", note = "This field will be private in an upcomming release")] + pub headers: HeaderMap } +#[allow(deprecated)] impl Response { /// Create a new [Response] from raw data. #[must_use = "Creating a response is pointless if you don't use it"] @@ -16,7 +25,8 @@ impl Response { Self { status, body: body.into(), - mime + mime, + headers: Default::default() } } @@ -26,7 +36,8 @@ impl Response { Self { status, body: body.into(), - mime: Some(APPLICATION_JSON) + mime: Some(APPLICATION_JSON), + headers: Default::default() } } @@ -36,7 +47,8 @@ impl Response { Self { status: StatusCode::NO_CONTENT, body: Body::empty(), - mime: None + mime: None, + headers: Default::default() } } @@ -46,10 +58,16 @@ impl Response { Self { status: StatusCode::FORBIDDEN, body: Body::empty(), - mime: None + mime: None, + headers: Default::default() } } + /// Add an HTTP header to the [Response]. + pub fn add_header(&mut self, name: HeaderName, value: HeaderValue) { + self.headers.insert(name, value); + } + #[cfg(test)] pub(crate) fn full_body(mut self) -> Result, ::Error> { use futures_executor::block_on; diff --git a/src/result/mod.rs b/src/result/mod.rs index 526af33..7531dd1 100644 --- a/src/result/mod.rs +++ b/src/result/mod.rs @@ -99,6 +99,7 @@ fn errorlog(e: E) { #[cfg(not(feature = "errorlog"))] fn errorlog(_e: E) {} +#[allow(deprecated)] fn handle_error(e: E) -> Pin> + Send>> where E: Display + IntoResponseError @@ -160,6 +161,7 @@ mod test { struct MsgError; #[test] + #[allow(deprecated)] fn result_from_future() { let nc = NoContent::default(); let res = block_on(nc.into_response()).unwrap(); diff --git a/src/result/no_content.rs b/src/result/no_content.rs index 0c4fe05..e4ab932 100644 --- a/src/result/no_content.rs +++ b/src/result/no_content.rs @@ -92,6 +92,7 @@ where } #[cfg(test)] +#[allow(deprecated)] mod test { use super::*; use futures_executor::block_on; diff --git a/src/result/raw.rs b/src/result/raw.rs index 6b3b520..27d59c0 100644 --- a/src/result/raw.rs +++ b/src/result/raw.rs @@ -131,6 +131,7 @@ mod test { use mime::TEXT_PLAIN; #[test] + #[allow(deprecated)] fn raw_response() { let msg = "Test"; let raw = Raw::new(msg, TEXT_PLAIN); diff --git a/src/result/result.rs b/src/result/result.rs index f22d756..971689b 100644 --- a/src/result/result.rs +++ b/src/result/result.rs @@ -49,6 +49,7 @@ where } #[cfg(test)] +#[allow(deprecated)] mod test { use super::*; use crate::result::OrAllTypes; diff --git a/src/result/success.rs b/src/result/success.rs index a3816ea..40922a3 100644 --- a/src/result/success.rs +++ b/src/result/success.rs @@ -124,6 +124,7 @@ mod test { } #[test] + #[allow(deprecated)] fn success_always_successfull() { let success: Success = Msg::default().into(); let res = block_on(success.into_response()).expect("didn't expect error response"); diff --git a/src/routing.rs b/src/routing.rs index 940a51b..9076b2c 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -81,10 +81,21 @@ pub trait DrawResourceRoutes { fn remove(&mut self); } +#[allow(deprecated)] fn response_from(res: Response, state: &State) -> gotham::hyper::Response { let mut r = create_empty_response(state, res.status); + let headers = r.headers_mut(); if let Some(mime) = res.mime { - r.headers_mut().insert(CONTENT_TYPE, mime.as_ref().parse().unwrap()); + headers.insert(CONTENT_TYPE, mime.as_ref().parse().unwrap()); + } + let mut last_name = None; + for (name, value) in res.headers { + if name.is_some() { + last_name = name; + } + // this unwrap is safe: the first item will always be Some + let name = last_name.clone().unwrap(); + headers.insert(name, value); } let method = Method::borrow_from(state); diff --git a/tests/resource_error.rs b/tests/resource_error.rs index a00b08a..c850175 100644 --- a/tests/resource_error.rs +++ b/tests/resource_error.rs @@ -10,6 +10,7 @@ enum Error { InternalServerError(String) } +#[allow(deprecated)] mod resource_error { use super::Error; use gotham::hyper::StatusCode; From b7a11933335792418b51a0d166bda080b8cb0678 Mon Sep 17 00:00:00 2001 From: Dominic Date: Thu, 14 Jan 2021 18:45:32 +0100 Subject: [PATCH 122/170] make all fields of response private, we're breaking change anyways Closes #34 Related to #27 --- CHANGELOG.md | 4 ++++ src/response.rs | 25 +++++++++++++++---------- src/result/mod.rs | 2 -- src/result/no_content.rs | 1 - src/result/raw.rs | 1 - src/result/result.rs | 1 - src/result/success.rs | 1 - src/routing.rs | 1 - tests/resource_error.rs | 8 ++++---- 9 files changed, 23 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf19e66..6a0929d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added + - Support custom HTTP response headers + ### Changed - The cors handler can now copy headers from the request if desired + - All fields of `Response` are now private ## [0.1.1] - 2020-12-28 ### Added diff --git a/src/response.rs b/src/response.rs index b29aa4e..8f378ca 100644 --- a/src/response.rs +++ b/src/response.rs @@ -7,17 +7,12 @@ use mime::{Mime, APPLICATION_JSON}; /// A response, used to create the final gotham response from. #[derive(Debug)] pub struct Response { - #[deprecated(since = "0.1.2", note = "This field will be private in an upcomming release")] - pub status: StatusCode, - #[deprecated(since = "0.1.2", note = "This field will be private in an upcomming release")] - pub body: Body, - #[deprecated(since = "0.1.2", note = "This field will be private in an upcomming release")] - pub mime: Option, - #[deprecated(since = "0.1.2", note = "This field will be private in an upcomming release")] - pub headers: HeaderMap + pub(crate) status: StatusCode, + pub(crate) body: Body, + pub(crate) mime: Option, + pub(crate) headers: HeaderMap } -#[allow(deprecated)] impl Response { /// Create a new [Response] from raw data. #[must_use = "Creating a response is pointless if you don't use it"] @@ -63,8 +58,18 @@ impl Response { } } + /// Return the status code of this [Response]. + pub fn status(&self) -> StatusCode { + self.status + } + + /// Return the mime type of this [Response]. + pub fn mime(&self) -> Option<&Mime> { + self.mime.as_ref() + } + /// Add an HTTP header to the [Response]. - pub fn add_header(&mut self, name: HeaderName, value: HeaderValue) { + pub fn header(&mut self, name: HeaderName, value: HeaderValue) { self.headers.insert(name, value); } diff --git a/src/result/mod.rs b/src/result/mod.rs index 7531dd1..526af33 100644 --- a/src/result/mod.rs +++ b/src/result/mod.rs @@ -99,7 +99,6 @@ fn errorlog(e: E) { #[cfg(not(feature = "errorlog"))] fn errorlog(_e: E) {} -#[allow(deprecated)] fn handle_error(e: E) -> Pin> + Send>> where E: Display + IntoResponseError @@ -161,7 +160,6 @@ mod test { struct MsgError; #[test] - #[allow(deprecated)] fn result_from_future() { let nc = NoContent::default(); let res = block_on(nc.into_response()).unwrap(); diff --git a/src/result/no_content.rs b/src/result/no_content.rs index e4ab932..0c4fe05 100644 --- a/src/result/no_content.rs +++ b/src/result/no_content.rs @@ -92,7 +92,6 @@ where } #[cfg(test)] -#[allow(deprecated)] mod test { use super::*; use futures_executor::block_on; diff --git a/src/result/raw.rs b/src/result/raw.rs index 27d59c0..6b3b520 100644 --- a/src/result/raw.rs +++ b/src/result/raw.rs @@ -131,7 +131,6 @@ mod test { use mime::TEXT_PLAIN; #[test] - #[allow(deprecated)] fn raw_response() { let msg = "Test"; let raw = Raw::new(msg, TEXT_PLAIN); diff --git a/src/result/result.rs b/src/result/result.rs index 971689b..f22d756 100644 --- a/src/result/result.rs +++ b/src/result/result.rs @@ -49,7 +49,6 @@ where } #[cfg(test)] -#[allow(deprecated)] mod test { use super::*; use crate::result::OrAllTypes; diff --git a/src/result/success.rs b/src/result/success.rs index 40922a3..a3816ea 100644 --- a/src/result/success.rs +++ b/src/result/success.rs @@ -124,7 +124,6 @@ mod test { } #[test] - #[allow(deprecated)] fn success_always_successfull() { let success: Success = Msg::default().into(); let res = block_on(success.into_response()).expect("didn't expect error response"); diff --git a/src/routing.rs b/src/routing.rs index 9076b2c..70d2369 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -81,7 +81,6 @@ pub trait DrawResourceRoutes { fn remove(&mut self); } -#[allow(deprecated)] fn response_from(res: Response, state: &State) -> gotham::hyper::Response { let mut r = create_empty_response(state, res.status); let headers = r.headers_mut(); diff --git a/tests/resource_error.rs b/tests/resource_error.rs index c850175..9325dc5 100644 --- a/tests/resource_error.rs +++ b/tests/resource_error.rs @@ -21,8 +21,8 @@ mod resource_error { fn io_error() { let err = Error::IoError(std::io::Error::last_os_error()); let res = err.into_response_error().unwrap(); - assert_eq!(res.status, StatusCode::INTERNAL_SERVER_ERROR); - assert_eq!(res.mime, Some(APPLICATION_JSON)); + assert_eq!(res.status(), StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!(res.mime(), Some(&APPLICATION_JSON)); } #[test] @@ -31,7 +31,7 @@ mod resource_error { assert_eq!(&format!("{}", err), "Internal Server Error: Brocken"); let res = err.into_response_error().unwrap(); - assert_eq!(res.status, StatusCode::INTERNAL_SERVER_ERROR); - assert_eq!(res.mime, None); // TODO shouldn't this be a json error message? + assert_eq!(res.status(), StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!(res.mime(), None); // TODO shouldn't this be a json error message? } } From daea3ba9ecd0a92bf83bcf91729a8b27ec0e1a86 Mon Sep 17 00:00:00 2001 From: Dominic Date: Thu, 14 Jan 2021 18:55:44 +0100 Subject: [PATCH 123/170] introduce without-openapi feature to improve #4 --- .gitlab-ci.yml | 6 +++--- CHANGELOG.md | 1 + Cargo.toml | 9 +++++++-- src/lib.rs | 6 ++++++ 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e88eed3..714c865 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -20,7 +20,7 @@ test-default: - cargo/ - target/ -test-all: +test-full: stage: test image: rust:1.42-slim before_script: @@ -28,7 +28,7 @@ test-all: - apt install -y --no-install-recommends libpq-dev - cargo -V script: - - cargo test --workspace --all-features + - cargo test --no-default-features --features full cache: key: cargo-1-42-all paths: @@ -64,7 +64,7 @@ test-trybuild-ui: - apt install -y --no-install-recommends libpq-dev - cargo -V script: - - cargo test --workspace --all-features --tests -- --ignored + - cargo test --no-default-features --features full --tests -- --ignored cache: key: cargo-1-48-all paths: diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a0929d..7248bc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - The cors handler can now copy headers from the request if desired - All fields of `Response` are now private + - If not enabling the `openapi` feature, `without-openapi` has to be enabled ## [0.1.1] - 2020-12-28 ### Added diff --git a/Cargo.toml b/Cargo.toml index 2d7d073..c7ad1b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,11 +46,16 @@ thiserror = "1.0.18" trybuild = "1.0.27" [features] -default = ["cors", "errorlog"] +default = ["cors", "errorlog", "without-openapi"] +full = ["auth", "cors", "database", "errorlog", "openapi"] + auth = ["gotham_restful_derive/auth", "base64", "cookie", "jsonwebtoken"] cors = [] -errorlog = [] database = ["gotham_restful_derive/database", "gotham_middleware_diesel"] +errorlog = [] + +# These features are exclusive - https://gitlab.com/msrd0/gotham-restful/-/issues/4 +without-openapi = [] openapi = ["gotham_restful_derive/openapi", "indexmap", "openapiv3"] [package.metadata.docs.rs] diff --git a/src/lib.rs b/src/lib.rs index 24e8297..9096bbe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -378,6 +378,12 @@ directory, that might help you. Any help writing more examples is highly appreci [`State`]: ../gotham/state/struct.State.html */ +#[cfg(all(feature = "openapi", feature = "without-openapi"))] +compile_error!("The 'openapi' and 'without-openapi' features cannot be combined"); + +#[cfg(all(not(feature = "openapi"), not(feature = "without-openapi")))] +compile_error!("Either the 'openapi' or 'without-openapi' feature needs to be enabled"); + // weird proc macro issue extern crate self as gotham_restful; From edd8bb618dc3dd9fed8d8687187ae99bf0e94251 Mon Sep 17 00:00:00 2001 From: Dominic Date: Fri, 15 Jan 2021 03:49:35 +0100 Subject: [PATCH 124/170] ci: --workspace applies features from the example which breaks stuff --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 714c865..deaa470 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -13,7 +13,7 @@ test-default: before_script: - cargo -V script: - - cargo test --workspace + - cargo test cache: key: cargo-1-42-default paths: From 75cd7e2c9685c7f57355dcee96f22119148c8e57 Mon Sep 17 00:00:00 2001 From: Dominic Date: Fri, 15 Jan 2021 03:55:11 +0100 Subject: [PATCH 125/170] ci: the same applies to tarpaulin --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index deaa470..672e080 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -44,7 +44,7 @@ test-tarpaulin: - cargo -V - cargo install cargo-tarpaulin script: - - cargo tarpaulin --target-dir target/tarpaulin --all --all-features --exclude-files 'cargo/*' --exclude-files 'derive/*' --exclude-files 'example/*' --exclude-files 'target/*' --ignore-panics --ignore-tests --out Html --out Xml -v + - cargo tarpaulin --target-dir target/tarpaulin --all-features --exclude-files 'cargo/*' --exclude-files 'derive/*' --exclude-files 'example/*' --exclude-files 'target/*' --ignore-panics --ignore-tests --out Html --out Xml -v artifacts: paths: - tarpaulin-report.html From 0251c03ceb3f69fd88ffe19ba5be6aa88f11a9bb Mon Sep 17 00:00:00 2001 From: Dominic Date: Fri, 15 Jan 2021 15:16:09 +0100 Subject: [PATCH 126/170] ci: fix tarpaulin features --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 672e080..7f4f016 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -44,7 +44,7 @@ test-tarpaulin: - cargo -V - cargo install cargo-tarpaulin script: - - cargo tarpaulin --target-dir target/tarpaulin --all-features --exclude-files 'cargo/*' --exclude-files 'derive/*' --exclude-files 'example/*' --exclude-files 'target/*' --ignore-panics --ignore-tests --out Html --out Xml -v + - cargo tarpaulin --target-dir target/tarpaulin --no-default-features --features full --exclude-files 'cargo/*' --exclude-files 'derive/*' --exclude-files 'example/*' --exclude-files 'target/*' --ignore-panics --ignore-tests --out Html --out Xml -v artifacts: paths: - tarpaulin-report.html From 0ac0f0f50425c8648739eed31044877262075f85 Mon Sep 17 00:00:00 2001 From: Dominic Date: Fri, 15 Jan 2021 17:33:40 +0100 Subject: [PATCH 127/170] fix features for doc --- .gitlab-ci.yml | 2 +- Cargo.toml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7f4f016..d6ad02d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -93,7 +93,7 @@ doc: before_script: - cargo -V script: - - cargo doc --all-features + - cargo doc --no-default-features --features full artifacts: paths: - target/doc/ diff --git a/Cargo.toml b/Cargo.toml index c7ad1b4..695ae6d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,7 +59,8 @@ without-openapi = [] openapi = ["gotham_restful_derive/openapi", "indexmap", "openapiv3"] [package.metadata.docs.rs] -all-features = true +no-default-features = true +features = ["full"] [patch.crates-io] gotham_restful = { path = "." } From b807ae279649ee2eabe76b1e406929c949c9d33e Mon Sep 17 00:00:00 2001 From: msrd0 <1182023-msrd0@users.noreply.gitlab.com> Date: Mon, 18 Jan 2021 00:05:30 +0000 Subject: [PATCH 128/170] Replace methods with more flexible endpoints --- .gitlab-ci.yml | 10 +- CHANGELOG.md | 9 + Cargo.toml | 7 +- README.md | 113 ++--- README.tpl | 4 +- derive/src/endpoint.rs | 469 ++++++++++++++++++ derive/src/lib.rs | 37 +- derive/src/method.rs | 466 ----------------- derive/src/openapi_type.rs | 65 ++- derive/src/private_openapi_trait.rs | 165 ++++++ derive/src/resource.rs | 61 ++- derive/src/util.rs | 12 +- example/Cargo.toml | 2 +- src/auth.rs | 2 +- src/cors.rs | 2 +- src/endpoint.rs | 105 ++++ src/lib.rs | 147 +++--- src/openapi/operation.rs | 64 +-- src/openapi/router.rs | 174 ++----- src/openapi/types.rs | 15 + src/resource.rs | 99 ---- src/result/auth_result.rs | 4 +- src/result/no_content.rs | 4 +- src/result/raw.rs | 2 +- src/result/success.rs | 4 +- src/routing.rs | 358 +++---------- src/types.rs | 11 +- tests/async_methods.rs | 18 +- tests/cors_handling.rs | 4 +- tests/custom_request_body.rs | 2 +- tests/openapi_specification.rs | 14 +- tests/openapi_supports_scope.rs | 2 +- tests/sync_methods.rs | 18 +- tests/trybuild_ui.rs | 19 +- tests/ui/endpoint/async_state.rs | 12 + tests/ui/endpoint/async_state.stderr | 5 + .../invalid_attribute.rs} | 11 +- tests/ui/endpoint/invalid_attribute.stderr | 11 + .../self.rs} | 11 +- tests/ui/endpoint/self.stderr | 19 + tests/ui/endpoint/too_few_args.rs | 11 + tests/ui/endpoint/too_few_args.stderr | 11 + tests/ui/endpoint/too_many_args.rs | 11 + tests/ui/endpoint/too_many_args.stderr | 11 + tests/ui/endpoint/unknown_attribute.rs | 11 + tests/ui/endpoint/unknown_attribute.stderr | 11 + tests/ui/endpoint/unsafe.rs | 11 + tests/ui/endpoint/unsafe.stderr | 11 + tests/ui/from_body/enum.rs | 10 + .../enum.stderr} | 4 +- tests/ui/from_body_enum.rs | 12 - tests/ui/method_async_state.rs | 15 - tests/ui/method_async_state.stderr | 5 - tests/ui/method_for_unknown_resource.rs | 10 - tests/ui/method_for_unknown_resource.stderr | 5 - tests/ui/method_no_resource.stderr | 15 - tests/ui/method_self.stderr | 13 - tests/ui/method_too_few_args.rs | 14 - tests/ui/method_too_few_args.stderr | 5 - tests/ui/method_too_many_args.rs | 14 - tests/ui/method_too_many_args.stderr | 5 - tests/ui/method_unsafe.rs | 14 - tests/ui/method_unsafe.stderr | 5 - tests/ui/openapi_type/enum_with_fields.rs | 12 + .../enum_with_fields.stderr} | 6 +- tests/ui/openapi_type/nullable_non_bool.rs | 10 + .../nullable_non_bool.stderr} | 2 +- tests/ui/openapi_type/rename_non_string.rs | 10 + .../rename_non_string.stderr} | 2 +- tests/ui/openapi_type/tuple_struct.rs | 7 + .../tuple_struct.stderr} | 4 +- tests/ui/openapi_type/union.rs | 10 + .../union.stderr} | 4 +- tests/ui/openapi_type/unknown_key.rs | 10 + .../unknown_key.stderr} | 2 +- tests/ui/openapi_type_enum_with_fields.rs | 14 - tests/ui/openapi_type_nullable_non_bool.rs | 12 - tests/ui/openapi_type_rename_non_string.rs | 12 - tests/ui/openapi_type_tuple_struct.rs | 8 - tests/ui/openapi_type_union.rs | 12 - tests/ui/openapi_type_unknown_key.rs | 12 - tests/ui/resource/unknown_method.rs | 11 + tests/ui/resource/unknown_method.stderr | 8 + tests/ui/resource_unknown_method.rs | 14 - tests/ui/resource_unknown_method.stderr | 14 - tests/ui/rustfmt.sh | 15 + tests/util/mod.rs | 7 + 87 files changed, 1497 insertions(+), 1512 deletions(-) create mode 100644 derive/src/endpoint.rs delete mode 100644 derive/src/method.rs create mode 100644 derive/src/private_openapi_trait.rs create mode 100644 src/endpoint.rs delete mode 100644 src/resource.rs create mode 100644 tests/ui/endpoint/async_state.rs create mode 100644 tests/ui/endpoint/async_state.stderr rename tests/ui/{method_self.rs => endpoint/invalid_attribute.rs} (53%) create mode 100644 tests/ui/endpoint/invalid_attribute.stderr rename tests/ui/{method_no_resource.rs => endpoint/self.rs} (50%) create mode 100644 tests/ui/endpoint/self.stderr create mode 100644 tests/ui/endpoint/too_few_args.rs create mode 100644 tests/ui/endpoint/too_few_args.stderr create mode 100644 tests/ui/endpoint/too_many_args.rs create mode 100644 tests/ui/endpoint/too_many_args.stderr create mode 100644 tests/ui/endpoint/unknown_attribute.rs create mode 100644 tests/ui/endpoint/unknown_attribute.stderr create mode 100644 tests/ui/endpoint/unsafe.rs create mode 100644 tests/ui/endpoint/unsafe.stderr create mode 100644 tests/ui/from_body/enum.rs rename tests/ui/{from_body_enum.stderr => from_body/enum.stderr} (53%) delete mode 100644 tests/ui/from_body_enum.rs delete mode 100644 tests/ui/method_async_state.rs delete mode 100644 tests/ui/method_async_state.stderr delete mode 100644 tests/ui/method_for_unknown_resource.rs delete mode 100644 tests/ui/method_for_unknown_resource.stderr delete mode 100644 tests/ui/method_no_resource.stderr delete mode 100644 tests/ui/method_self.stderr delete mode 100644 tests/ui/method_too_few_args.rs delete mode 100644 tests/ui/method_too_few_args.stderr delete mode 100644 tests/ui/method_too_many_args.rs delete mode 100644 tests/ui/method_too_many_args.stderr delete mode 100644 tests/ui/method_unsafe.rs delete mode 100644 tests/ui/method_unsafe.stderr create mode 100644 tests/ui/openapi_type/enum_with_fields.rs rename tests/ui/{openapi_type_enum_with_fields.stderr => openapi_type/enum_with_fields.stderr} (61%) create mode 100644 tests/ui/openapi_type/nullable_non_bool.rs rename tests/ui/{openapi_type_nullable_non_bool.stderr => openapi_type/nullable_non_bool.stderr} (69%) create mode 100644 tests/ui/openapi_type/rename_non_string.rs rename tests/ui/{openapi_type_rename_non_string.stderr => openapi_type/rename_non_string.stderr} (66%) create mode 100644 tests/ui/openapi_type/tuple_struct.rs rename tests/ui/{openapi_type_tuple_struct.stderr => openapi_type/tuple_struct.stderr} (56%) create mode 100644 tests/ui/openapi_type/union.rs rename tests/ui/{openapi_type_union.stderr => openapi_type/union.stderr} (56%) create mode 100644 tests/ui/openapi_type/unknown_key.rs rename tests/ui/{openapi_type_unknown_key.stderr => openapi_type/unknown_key.stderr} (65%) delete mode 100644 tests/ui/openapi_type_enum_with_fields.rs delete mode 100644 tests/ui/openapi_type_nullable_non_bool.rs delete mode 100644 tests/ui/openapi_type_rename_non_string.rs delete mode 100644 tests/ui/openapi_type_tuple_struct.rs delete mode 100644 tests/ui/openapi_type_union.rs delete mode 100644 tests/ui/openapi_type_unknown_key.rs create mode 100644 tests/ui/resource/unknown_method.rs create mode 100644 tests/ui/resource/unknown_method.stderr delete mode 100644 tests/ui/resource_unknown_method.rs delete mode 100644 tests/ui/resource_unknown_method.stderr create mode 100755 tests/ui/rustfmt.sh diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d6ad02d..18df473 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,23 +6,24 @@ stages: variables: CARGO_HOME: $CI_PROJECT_DIR/cargo + RUST_LOG: info,gotham=debug,gotham_restful=trace test-default: stage: test - image: rust:1.42-slim + image: rust:1.43-slim before_script: - cargo -V script: - cargo test cache: - key: cargo-1-42-default + key: cargo-1-43-default paths: - cargo/ - target/ test-full: stage: test - image: rust:1.42-slim + image: rust:1.43-slim before_script: - apt update -y - apt install -y --no-install-recommends libpq-dev @@ -30,7 +31,7 @@ test-full: script: - cargo test --no-default-features --features full cache: - key: cargo-1-42-all + key: cargo-1-43-all paths: - cargo/ - target/ @@ -86,6 +87,7 @@ rustfmt: - cargo fmt --version script: - cargo fmt -- --check + - ./tests/ui/rustfmt.sh --check doc: stage: build diff --git a/CHANGELOG.md b/CHANGELOG.md index 7248bc2..af8a451 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added - Support custom HTTP response headers + - New `endpoint` router extension with associated `Endpoint` trait ([!18]) ### Changed - The cors handler can now copy headers from the request if desired - All fields of `Response` are now private - If not enabling the `openapi` feature, `without-openapi` has to be enabled + - The endpoint macro attributes (`read`, `create`, ...) no longer take the resource ident and reject all unknown attributes ([!18]) + +### Removed + - All pre-defined methods (`read`, `create`, ...) from our router extensions ([!18]) + - All pre-defined method traits (`ResourceRead`, ...) ([!18]) ## [0.1.1] - 2020-12-28 ### Added @@ -25,3 +31,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.1.0] - 2020-10-02 Previous changes are not tracked by this changelog file. Refer to the [releases](https://gitlab.com/msrd0/gotham-restful/-/releases) for the changelog. + + + [!18]: https://gitlab.com/msrd0/gotham-restful/-/merge_requests/18 diff --git a/Cargo.toml b/Cargo.toml index 695ae6d..65d8f49 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,9 @@ indexmap = { version = "1.3.2", optional = true } jsonwebtoken = { version = "7.1.0", optional = true } log = "0.4.8" mime = "0.3.16" +once_cell = { version = "1.5", optional = true } openapiv3 = { version = "0.3.2", optional = true } +regex = { version = "1.4", optional = true } serde = { version = "1.0.110", features = ["derive"] } serde_json = "1.0.58" uuid = { version = "0.8.1", optional = true } @@ -42,12 +44,13 @@ uuid = { version = "0.8.1", optional = true } diesel = { version = "1.4.4", features = ["postgres"] } futures-executor = "0.3.5" paste = "1.0" +pretty_env_logger = "0.4" thiserror = "1.0.18" trybuild = "1.0.27" [features] default = ["cors", "errorlog", "without-openapi"] -full = ["auth", "cors", "database", "errorlog", "openapi"] +full = ["auth", "chrono", "cors", "database", "errorlog", "openapi", "uuid"] auth = ["gotham_restful_derive/auth", "base64", "cookie", "jsonwebtoken"] cors = [] @@ -56,7 +59,7 @@ errorlog = [] # These features are exclusive - https://gitlab.com/msrd0/gotham-restful/-/issues/4 without-openapi = [] -openapi = ["gotham_restful_derive/openapi", "indexmap", "openapiv3"] +openapi = ["gotham_restful_derive/openapi", "indexmap", "once_cell", "openapiv3", "regex"] [package.metadata.docs.rs] no-default-features = true diff --git a/README.md b/README.md index 9ed0bee..f683381 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,8 @@ rustdoc - - Minimum Rust Version + + Minimum Rust Version dependencies @@ -27,7 +27,7 @@
This crate is an extension to the popular [gotham web framework][gotham] for Rust. It allows you to -create resources with assigned methods that aim to be a more convenient way of creating handlers +create resources with assigned endpoints that aim to be a more convenient way of creating handlers for requests. ## Features @@ -45,23 +45,23 @@ for requests. This crate is just as safe as you'd expect from anything written in safe Rust - and `#![forbid(unsafe_code)]` ensures that no unsafe was used. -## Methods +## Endpoints -Assuming you assign `/foobar` to your resource, you can implement the following methods: +Assuming you assign `/foobar` to your resource, the following pre-defined endpoints exist: -| Method Name | Required Arguments | HTTP Verb | HTTP Path | -| ----------- | ------------------ | --------- | ----------- | -| read_all | | GET | /foobar | -| read | id | GET | /foobar/:id | -| search | query | GET | /foobar/search | -| create | body | POST | /foobar | -| change_all | body | PUT | /foobar | -| change | id, body | PUT | /foobar/:id | -| remove_all | | DELETE | /foobar | -| remove | id | DELETE | /foobar/:id | +| Endpoint Name | Required Arguments | HTTP Verb | HTTP Path | +| ------------- | ------------------ | --------- | -------------- | +| read_all | | GET | /foobar | +| read | id | GET | /foobar/:id | +| search | query | GET | /foobar/search | +| create | body | POST | /foobar | +| change_all | body | PUT | /foobar | +| change | id, body | PUT | /foobar/:id | +| remove_all | | DELETE | /foobar | +| remove | id | DELETE | /foobar/:id | -Each of those methods has a macro that creates the neccessary boilerplate for the Resource. A -simple example could look like this: +Each of those endpoints has a macro that creates the neccessary boilerplate for the Resource. A +simple example looks like this: ```rust /// Our RESTful resource. @@ -69,14 +69,14 @@ simple example could look like this: #[resource(read)] struct FooResource; -/// The return type of the foo read method. +/// The return type of the foo read endpoint. #[derive(Serialize)] struct Foo { id: u64 } -/// The foo read method handler. -#[read(FooResource)] +/// The foo read endpoint. +#[read] fn read(id: u64) -> Success { Foo { id }.into() } @@ -84,17 +84,16 @@ fn read(id: u64) -> Success { ## Arguments -Some methods require arguments. Those should be - * **id** Should be a deserializable json-primitive like `i64` or `String`. +Some endpoints require arguments. Those should be + * **id** Should be a deserializable json-primitive like [`i64`] or [`String`]. * **body** Should be any deserializable object, or any type implementing [`RequestBody`]. * **query** Should be any deserializable object whose variables are json-primitives. It will however not be parsed from json, but from HTTP GET parameters like in `search?id=1`. The - type needs to implement [`QueryStringExtractor`]. + type needs to implement [`QueryStringExtractor`](gotham::extractor::QueryStringExtractor). -Additionally, non-async handlers may take a reference to gotham's [`State`]. If you need to -have an async handler (that is, the function that the method macro is invoked on is declared -as `async fn`), consider returning the boxed future instead. Since [`State`] does not implement -`Sync` there is unfortunately no more convenient way. +Additionally, all handlers may take a reference to gotham's [`State`]. Please note that for async +handlers, it needs to be a mutable reference until rustc's lifetime checks across await bounds +improve. ## Uploads and Downloads @@ -114,7 +113,7 @@ struct RawImage { content_type: Mime } -#[create(ImageResource)] +#[create] fn create(body : RawImage) -> Raw> { Raw::new(body.content, body.content_type) } @@ -126,21 +125,23 @@ To make life easier for common use-cases, this create offers a few features that when you implement your web server. The complete feature list is - [`auth`](#authentication-feature) Advanced JWT middleware - `chrono` openapi support for chrono types - - [`cors`](#cors-feature) CORS handling for all method handlers + - `full` enables all features except `without-openapi` + - [`cors`](#cors-feature) CORS handling for all endpoint handlers - [`database`](#database-feature) diesel middleware support - - `errorlog` log errors returned from method handlers + - `errorlog` log errors returned from endpoint handlers - [`openapi`](#openapi-feature) router additions to generate an openapi spec - `uuid` openapi support for uuid + - `without-openapi` (**default**) disables `openapi` support. ### Authentication Feature In order to enable authentication support, enable the `auth` feature gate. This allows you to register a middleware that can automatically check for the existence of an JWT authentication -token. Besides being supported by the method macros, it supports to lookup the required JWT secret +token. Besides being supported by the endpoint macros, it supports to lookup the required JWT secret with the JWT data, hence you can use several JWT secrets and decide on the fly which secret to use. None of this is currently supported by gotham's own JWT middleware. -A simple example that uses only a single secret could look like this: +A simple example that uses only a single secret looks like this: ```rust #[derive(Resource)] @@ -159,7 +160,7 @@ struct AuthData { exp: u64 } -#[read(SecretResource)] +#[read] fn read(auth: AuthStatus, id: u64) -> AuthSuccess { let intended_for = auth.ok()?.sub; Ok(Secret { id, intended_for }) @@ -185,14 +186,14 @@ the `Access-Control-Allow-Methods` header is touched. To change the behaviour, a configuration as a middleware. A simple example that allows authentication from every origin (note that `*` always disallows -authentication), and every content type, could look like this: +authentication), and every content type, looks like this: ```rust #[derive(Resource)] #[resource(read_all)] struct FooResource; -#[read_all(FooResource)] +#[read_all] fn read_all() { // your handler } @@ -221,7 +222,7 @@ note however that due to the way gotham's diesel middleware implementation, it i to run async code while holding a database connection. If you need to combine async and database, you'll need to borrow the connection from the [`State`] yourself and return a boxed future. -A simple non-async example could look like this: +A simple non-async example looks like this: ```rust #[derive(Resource)] @@ -234,7 +235,7 @@ struct Foo { value: String } -#[read_all(FooResource)] +#[read_all] fn read_all(conn: &PgConnection) -> QueryResult> { foo::table.load(conn) } @@ -261,7 +262,7 @@ In order to automatically create an openapi specification, gotham-restful needs all routes and the types returned. `serde` does a great job at serialization but doesn't give enough type information, so all types used in the router need to implement `OpenapiType`. This can be derived for almoust any type and there should be no need to implement it manually. A simple -example could look like this: +example looks like this: ```rust #[derive(Resource)] @@ -273,7 +274,7 @@ struct Foo { bar: String } -#[read_all(FooResource)] +#[read_all] fn read_all() -> Success { Foo { bar: "Hello World".to_owned() }.into() } @@ -293,22 +294,17 @@ fn main() { } ``` -Above example adds the resource as before, but adds another endpoint that we specified as `/openapi` -that will return the generated openapi specification. This allows you to easily write clients -in different languages without worying to exactly replicate your api in each of those languages. +Above example adds the resource as before, but adds another endpoint that we specified as `/openapi`. +It will return the generated openapi specification in JSON format. This allows you to easily write +clients in different languages without worying to exactly replicate your api in each of those +languages. -However, as of right now there is one caveat. If you wrote code before enabling the openapi feature, -it is likely to break. This is because of the new requirement of `OpenapiType` for all types used -with resources, even outside of the `with_openapi` scope. This issue will eventually be resolved. -If you are writing a library that uses gotham-restful, make sure that you expose an openapi feature. -In other words, put - -```toml -[features] -openapi = ["gotham-restful/openapi"] -``` - -into your libraries `Cargo.toml` and use the following for all types used with handlers: +However, please note that by default, the `without-openapi` feature of this crate is enabled. +Disabling it in favour of the `openapi` feature will add an additional type bound, [`OpenapiType`], +on some of the types in [`Endpoint`] and related traits. This means that some code might only +compile on either feature, but not on both. If you are writing a library that uses gotham-restful, +it is strongly recommended to pass both features through and conditionally enable the openapi +code, like this: ```rust #[derive(Deserialize, Serialize)] @@ -318,18 +314,15 @@ struct Foo; ## Examples -There is a lack of good examples, but there is currently a collection of code in the [example] -directory, that might help you. Any help writing more examples is highly appreciated. +This readme and the crate documentation contain some of example. In addition to that, there is +a collection of code in the [example] directory that might help you. Any help writing more +examples is highly appreciated. [diesel]: https://diesel.rs/ [example]: https://gitlab.com/msrd0/gotham-restful/tree/master/example [gotham]: https://gotham.rs/ [serde_json]: https://github.com/serde-rs/json#serde-json---- - [`CorsRoute`]: trait.CorsRoute.html - [`QueryStringExtractor`]: ../gotham/extractor/trait.QueryStringExtractor.html - [`RequestBody`]: trait.RequestBody.html - [`State`]: ../gotham/state/struct.State.html ## Versioning diff --git a/README.tpl b/README.tpl index 42ce8d6..36373ae 100644 --- a/README.tpl +++ b/README.tpl @@ -17,8 +17,8 @@
rustdoc - - Minimum Rust Version + + Minimum Rust Version dependencies diff --git a/derive/src/endpoint.rs b/derive/src/endpoint.rs new file mode 100644 index 0000000..a70145b --- /dev/null +++ b/derive/src/endpoint.rs @@ -0,0 +1,469 @@ +use crate::util::{CollectToResult, PathEndsWith}; +use proc_macro2::{Ident, Span, TokenStream}; +use quote::{format_ident, quote}; +use std::str::FromStr; +use syn::{ + spanned::Spanned, Attribute, AttributeArgs, Error, FnArg, ItemFn, Lit, LitBool, Meta, NestedMeta, PatType, Result, + ReturnType, Type +}; + +pub enum EndpointType { + ReadAll, + Read, + Search, + Create, + UpdateAll, + Update, + DeleteAll, + Delete +} + +impl FromStr for EndpointType { + type Err = Error; + + fn from_str(str: &str) -> Result { + match str { + "ReadAll" | "read_all" => Ok(Self::ReadAll), + "Read" | "read" => Ok(Self::Read), + "Search" | "search" => Ok(Self::Search), + "Create" | "create" => Ok(Self::Create), + "ChangeAll" | "change_all" => Ok(Self::UpdateAll), + "Change" | "change" => Ok(Self::Update), + "RemoveAll" | "remove_all" => Ok(Self::DeleteAll), + "Remove" | "remove" => Ok(Self::Delete), + _ => Err(Error::new(Span::call_site(), format!("Unknown method: `{}'", str))) + } + } +} + +impl EndpointType { + fn http_method(&self) -> TokenStream { + match self { + Self::ReadAll | Self::Read | Self::Search => quote!(::gotham_restful::gotham::hyper::Method::GET), + Self::Create => quote!(::gotham_restful::gotham::hyper::Method::POST), + Self::UpdateAll | Self::Update => quote!(::gotham_restful::gotham::hyper::Method::PUT), + Self::DeleteAll | Self::Delete => quote!(::gotham_restful::gotham::hyper::Method::DELETE) + } + } + + fn uri(&self) -> TokenStream { + match self { + Self::ReadAll | Self::Create | Self::UpdateAll | Self::DeleteAll => quote!(""), + Self::Read | Self::Update | Self::Delete => quote!(":id"), + Self::Search => quote!("search") + } + } + + fn has_placeholders(&self) -> LitBool { + match self { + Self::ReadAll | Self::Search | Self::Create | Self::UpdateAll | Self::DeleteAll => LitBool { + value: false, + span: Span::call_site() + }, + Self::Read | Self::Update | Self::Delete => LitBool { + value: true, + span: Span::call_site() + } + } + } + + fn placeholders_ty(&self, arg_ty: Option<&Type>) -> TokenStream { + match self { + Self::ReadAll | Self::Search | Self::Create | Self::UpdateAll | Self::DeleteAll => { + quote!(::gotham_restful::gotham::extractor::NoopPathExtractor) + }, + Self::Read | Self::Update | Self::Delete => quote!(::gotham_restful::export::IdPlaceholder::<#arg_ty>) + } + } + + fn needs_params(&self) -> LitBool { + match self { + Self::ReadAll | Self::Read | Self::Create | Self::UpdateAll | Self::Update | Self::DeleteAll | Self::Delete => { + LitBool { + value: false, + span: Span::call_site() + } + }, + Self::Search => LitBool { + value: true, + span: Span::call_site() + } + } + } + + fn params_ty(&self, arg_ty: Option<&Type>) -> TokenStream { + match self { + Self::ReadAll | Self::Read | Self::Create | Self::UpdateAll | Self::Update | Self::DeleteAll | Self::Delete => { + quote!(::gotham_restful::gotham::extractor::NoopQueryStringExtractor) + }, + Self::Search => quote!(#arg_ty) + } + } + + fn needs_body(&self) -> LitBool { + match self { + Self::ReadAll | Self::Read | Self::Search | Self::DeleteAll | Self::Delete => LitBool { + value: false, + span: Span::call_site() + }, + Self::Create | Self::UpdateAll | Self::Update => LitBool { + value: true, + span: Span::call_site() + } + } + } + + fn body_ty(&self, arg_ty: Option<&Type>) -> TokenStream { + match self { + Self::ReadAll | Self::Read | Self::Search | Self::DeleteAll | Self::Delete => quote!(()), + Self::Create | Self::UpdateAll | Self::Update => quote!(#arg_ty) + } + } +} + +#[allow(clippy::large_enum_variant)] +enum HandlerArgType { + StateRef, + StateMutRef, + MethodArg(Type), + DatabaseConnection(Type), + AuthStatus(Type), + AuthStatusRef(Type) +} + +impl HandlerArgType { + fn is_method_arg(&self) -> bool { + matches!(self, Self::MethodArg(_)) + } + + fn is_database_conn(&self) -> bool { + matches!(self, Self::DatabaseConnection(_)) + } + + fn is_auth_status(&self) -> bool { + matches!(self, Self::AuthStatus(_) | Self::AuthStatusRef(_)) + } + + fn ty(&self) -> Option<&Type> { + match self { + Self::MethodArg(ty) | Self::DatabaseConnection(ty) | Self::AuthStatus(ty) | Self::AuthStatusRef(ty) => Some(ty), + _ => None + } + } + + fn quote_ty(&self) -> Option { + self.ty().map(|ty| quote!(#ty)) + } +} + +struct HandlerArg { + ident_span: Span, + ty: HandlerArgType +} + +impl Spanned for HandlerArg { + fn span(&self) -> Span { + self.ident_span + } +} + +fn interpret_arg_ty(attrs: &[Attribute], name: &str, ty: Type) -> Result { + let attr = attrs + .iter() + .find(|arg| arg.path.segments.iter().any(|path| &path.ident.to_string() == "rest_arg")) + .map(|arg| arg.tokens.to_string()); + + // TODO issue a warning for _state usage once diagnostics become stable + if attr.as_deref() == Some("state") || (attr.is_none() && (name == "state" || name == "_state")) { + return match ty { + Type::Reference(ty) => Ok(if ty.mutability.is_none() { + HandlerArgType::StateRef + } else { + HandlerArgType::StateMutRef + }), + _ => Err(Error::new( + ty.span(), + "The state parameter has to be a (mutable) reference to gotham_restful::State" + )) + }; + } + + if cfg!(feature = "auth") && (attr.as_deref() == Some("auth") || (attr.is_none() && name == "auth")) { + return Ok(match ty { + Type::Reference(ty) => HandlerArgType::AuthStatusRef(*ty.elem), + ty => HandlerArgType::AuthStatus(ty) + }); + } + + if cfg!(feature = "database") + && (attr.as_deref() == Some("connection") || attr.as_deref() == Some("conn") || (attr.is_none() && name == "conn")) + { + return Ok(HandlerArgType::DatabaseConnection(match ty { + Type::Reference(ty) => *ty.elem, + ty => ty + })); + } + + Ok(HandlerArgType::MethodArg(ty)) +} + +fn interpret_arg(_index: usize, arg: &PatType) -> Result { + let pat = &arg.pat; + let orig_name = quote!(#pat); + let ty = interpret_arg_ty(&arg.attrs, &orig_name.to_string(), *arg.ty.clone())?; + + Ok(HandlerArg { + ident_span: arg.pat.span(), + ty + }) +} + +#[cfg(feature = "openapi")] +fn expand_operation_id(operation_id: Option) -> Option { + match operation_id { + Some(operation_id) => Some(quote! { + fn operation_id() -> Option { + Some(#operation_id.to_string()) + } + }), + None => None + } +} + +#[cfg(not(feature = "openapi"))] +fn expand_operation_id(_: Option) -> Option { + None +} + +fn expand_wants_auth(wants_auth: Option, default: bool) -> TokenStream { + let wants_auth = wants_auth.unwrap_or_else(|| { + Lit::Bool(LitBool { + value: default, + span: Span::call_site() + }) + }); + + quote! { + fn wants_auth() -> bool { + #wants_auth + } + } +} + +pub fn endpoint_ident(fn_ident: &Ident) -> Ident { + format_ident!("{}___gotham_restful_endpoint", fn_ident) +} + +fn expand_endpoint_type(ty: EndpointType, attrs: AttributeArgs, fun: &ItemFn) -> Result { + // reject unsafe functions + if let Some(unsafety) = fun.sig.unsafety { + return Err(Error::new(unsafety.span(), "Endpoint handler methods must not be unsafe")); + } + + // parse arguments + let mut operation_id: Option = None; + let mut wants_auth: Option = None; + for meta in attrs { + match meta { + NestedMeta::Meta(Meta::NameValue(kv)) => { + if kv.path.ends_with("operation_id") { + operation_id = Some(kv.lit); + } else if kv.path.ends_with("wants_auth") { + wants_auth = Some(kv.lit); + } else { + return Err(Error::new(kv.path.span(), "Unknown attribute")); + } + }, + _ => return Err(Error::new(meta.span(), "Invalid attribute syntax")) + } + } + #[cfg(not(feature = "openapi"))] + if let Some(operation_id) = operation_id { + return Err(Error::new( + operation_id.span(), + "`operation_id` is only supported with the openapi feature" + )); + } + + // extract arguments into pattern, ident and type + let args = fun + .sig + .inputs + .iter() + .enumerate() + .map(|(i, arg)| match arg { + FnArg::Typed(arg) => interpret_arg(i, arg), + FnArg::Receiver(_) => Err(Error::new(arg.span(), "Didn't expect self parameter")) + }) + .collect_to_result()?; + + let fun_vis = &fun.vis; + let fun_ident = &fun.sig.ident; + let fun_is_async = fun.sig.asyncness.is_some(); + + let ident = endpoint_ident(fun_ident); + let dummy_ident = format_ident!("_IMPL_Endpoint_for_{}", ident); + let (output_ty, is_no_content) = match &fun.sig.output { + ReturnType::Default => (quote!(::gotham_restful::NoContent), true), + ReturnType::Type(_, ty) => (quote!(#ty), false) + }; + + let arg_tys = args.iter().filter(|arg| arg.ty.is_method_arg()).collect::>(); + let mut arg_ty_idx = 0; + let mut next_arg_ty = |return_none: bool| { + if return_none { + return Ok(None); + } + if arg_ty_idx >= arg_tys.len() { + return Err(Error::new(fun_ident.span(), "Too few arguments")); + } + let ty = arg_tys[arg_ty_idx].ty.ty().unwrap(); + arg_ty_idx += 1; + Ok(Some(ty)) + }; + + let http_method = ty.http_method(); + let uri = ty.uri(); + let has_placeholders = ty.has_placeholders(); + let placeholder_ty = ty.placeholders_ty(next_arg_ty(!has_placeholders.value)?); + let needs_params = ty.needs_params(); + let params_ty = ty.params_ty(next_arg_ty(!needs_params.value)?); + let needs_body = ty.needs_body(); + let body_ty = ty.body_ty(next_arg_ty(!needs_body.value)?); + + if arg_ty_idx < arg_tys.len() { + return Err(Error::new(fun_ident.span(), "Too many arguments")); + } + + let mut handle_args: Vec = Vec::new(); + if has_placeholders.value { + handle_args.push(quote!(placeholders.id)); + } + if needs_params.value { + handle_args.push(quote!(params)); + } + if needs_body.value { + handle_args.push(quote!(body.unwrap())); + } + let handle_args = args.iter().map(|arg| match arg.ty { + HandlerArgType::StateRef | HandlerArgType::StateMutRef => quote!(state), + HandlerArgType::MethodArg(_) => handle_args.remove(0), + HandlerArgType::DatabaseConnection(_) => quote!(&conn), + HandlerArgType::AuthStatus(_) => quote!(auth), + HandlerArgType::AuthStatusRef(_) => quote!(&auth) + }); + + let expand_handle_content = || { + let mut state_block = quote!(); + if let Some(arg) = args.iter().find(|arg| arg.ty.is_auth_status()) { + let auth_ty = arg.ty.quote_ty(); + state_block = quote! { + #state_block + let auth: #auth_ty = state.borrow::<#auth_ty>().clone(); + } + } + + let mut handle_content = quote!(#fun_ident(#(#handle_args),*)); + if fun_is_async { + if let Some(arg) = args.iter().find(|arg| matches!(arg.ty, HandlerArgType::StateRef)) { + return Err(Error::new(arg.span(), "Endpoint handler functions that are async must not take `&State` as an argument, consider taking `&mut State`")); + } + handle_content = quote!(#handle_content.await); + } + if is_no_content { + handle_content = quote!(#handle_content; ::gotham_restful::NoContent) + } + + if let Some(arg) = args.iter().find(|arg| arg.ty.is_database_conn()) { + let conn_ty = arg.ty.quote_ty(); + state_block = quote! { + #state_block + let repo = <::gotham_restful::export::Repo<#conn_ty>>::borrow_from(state).clone(); + }; + handle_content = quote! { + repo.run::<_, _, ()>(move |conn| { + Ok({ #handle_content }) + }).await.unwrap() + }; + } + + Ok(quote! { + use ::gotham_restful::export::FutureExt as _; + #state_block + async move { + #handle_content + }.boxed() + }) + }; + let handle_content = match expand_handle_content() { + Ok(content) => content, + Err(err) => err.to_compile_error() + }; + + let tr8 = if cfg!(feature = "openapi") { + quote!(::gotham_restful::EndpointWithSchema) + } else { + quote!(::gotham_restful::Endpoint) + }; + let operation_id = expand_operation_id(operation_id); + let wants_auth = expand_wants_auth(wants_auth, args.iter().any(|arg| arg.ty.is_auth_status())); + Ok(quote! { + #[doc(hidden)] + /// `gotham_restful` implementation detail + #[allow(non_camel_case_types)] + #fun_vis struct #ident; + + #[allow(non_upper_case_globals)] + static #dummy_ident: () = { + impl #tr8 for #ident { + fn http_method() -> ::gotham_restful::gotham::hyper::Method { + #http_method + } + + fn uri() -> ::std::borrow::Cow<'static, str> { + { #uri }.into() + } + + type Output = #output_ty; + + fn has_placeholders() -> bool { + #has_placeholders + } + type Placeholders = #placeholder_ty; + + fn needs_params() -> bool { + #needs_params + } + type Params = #params_ty; + + fn needs_body() -> bool { + #needs_body + } + type Body = #body_ty; + + fn handle( + state: &mut ::gotham_restful::gotham::state::State, + placeholders: Self::Placeholders, + params: Self::Params, + body: ::std::option::Option + ) -> ::gotham_restful::export::BoxFuture<'static, Self::Output> { + #handle_content + } + + #operation_id + #wants_auth + } + }; + }) +} + +pub fn expand_endpoint(ty: EndpointType, attrs: AttributeArgs, fun: ItemFn) -> Result { + let endpoint_type = match expand_endpoint_type(ty, attrs, &fun) { + Ok(code) => code, + Err(err) => err.to_compile_error() + }; + Ok(quote! { + #fun + #endpoint_type + }) +} diff --git a/derive/src/lib.rs b/derive/src/lib.rs index c3f313a..15f13cd 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -5,24 +5,32 @@ use syn::{parse_macro_input, parse_macro_input::ParseMacroInput, DeriveInput, Re mod util; +mod endpoint; +use endpoint::{expand_endpoint, EndpointType}; + mod from_body; use from_body::expand_from_body; -mod method; -use method::{expand_method, Method}; + mod request_body; use request_body::expand_request_body; + mod resource; use resource::expand_resource; + mod resource_error; use resource_error::expand_resource_error; + #[cfg(feature = "openapi")] mod openapi_type; #[cfg(feature = "openapi")] use openapi_type::expand_openapi_type; +mod private_openapi_trait; +use private_openapi_trait::expand_private_openapi_trait; + #[inline] fn print_tokens(tokens: TokenStream2) -> TokenStream { - //eprintln!("{}", tokens); + // eprintln!("{}", tokens); tokens.into() } @@ -77,40 +85,47 @@ pub fn derive_resource_error(input: TokenStream) -> TokenStream { #[proc_macro_attribute] pub fn read_all(attr: TokenStream, item: TokenStream) -> TokenStream { - expand_macro(attr, item, |attr, item| expand_method(Method::ReadAll, attr, item)) + expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::ReadAll, attr, item)) } #[proc_macro_attribute] pub fn read(attr: TokenStream, item: TokenStream) -> TokenStream { - expand_macro(attr, item, |attr, item| expand_method(Method::Read, attr, item)) + expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::Read, attr, item)) } #[proc_macro_attribute] pub fn search(attr: TokenStream, item: TokenStream) -> TokenStream { - expand_macro(attr, item, |attr, item| expand_method(Method::Search, attr, item)) + expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::Search, attr, item)) } #[proc_macro_attribute] pub fn create(attr: TokenStream, item: TokenStream) -> TokenStream { - expand_macro(attr, item, |attr, item| expand_method(Method::Create, attr, item)) + expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::Create, attr, item)) } #[proc_macro_attribute] pub fn change_all(attr: TokenStream, item: TokenStream) -> TokenStream { - expand_macro(attr, item, |attr, item| expand_method(Method::ChangeAll, attr, item)) + expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::UpdateAll, attr, item)) } #[proc_macro_attribute] pub fn change(attr: TokenStream, item: TokenStream) -> TokenStream { - expand_macro(attr, item, |attr, item| expand_method(Method::Change, attr, item)) + expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::Update, attr, item)) } #[proc_macro_attribute] pub fn remove_all(attr: TokenStream, item: TokenStream) -> TokenStream { - expand_macro(attr, item, |attr, item| expand_method(Method::RemoveAll, attr, item)) + expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::DeleteAll, attr, item)) } #[proc_macro_attribute] pub fn remove(attr: TokenStream, item: TokenStream) -> TokenStream { - expand_macro(attr, item, |attr, item| expand_method(Method::Remove, attr, item)) + expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::Delete, attr, item)) +} + +/// PRIVATE MACRO - DO NOT USE +#[doc(hidden)] +#[proc_macro_attribute] +pub fn _private_openapi_trait(attr: TokenStream, item: TokenStream) -> TokenStream { + expand_macro(attr, item, expand_private_openapi_trait) } diff --git a/derive/src/method.rs b/derive/src/method.rs deleted file mode 100644 index c9e5056..0000000 --- a/derive/src/method.rs +++ /dev/null @@ -1,466 +0,0 @@ -use crate::util::CollectToResult; -use heck::{CamelCase, SnakeCase}; -use proc_macro2::{Ident, Span, TokenStream}; -use quote::{format_ident, quote}; -use std::str::FromStr; -use syn::{ - spanned::Spanned, Attribute, AttributeArgs, Error, FnArg, ItemFn, Lit, LitBool, Meta, NestedMeta, PatType, Path, Result, - ReturnType, Type -}; - -pub enum Method { - ReadAll, - Read, - Search, - Create, - ChangeAll, - Change, - RemoveAll, - Remove -} - -impl FromStr for Method { - type Err = Error; - - fn from_str(str: &str) -> Result { - match str { - "ReadAll" | "read_all" => Ok(Self::ReadAll), - "Read" | "read" => Ok(Self::Read), - "Search" | "search" => Ok(Self::Search), - "Create" | "create" => Ok(Self::Create), - "ChangeAll" | "change_all" => Ok(Self::ChangeAll), - "Change" | "change" => Ok(Self::Change), - "RemoveAll" | "remove_all" => Ok(Self::RemoveAll), - "Remove" | "remove" => Ok(Self::Remove), - _ => Err(Error::new(Span::call_site(), format!("Unknown method: `{}'", str))) - } - } -} - -impl Method { - pub fn type_names(&self) -> Vec<&'static str> { - use Method::*; - - match self { - ReadAll | RemoveAll => vec![], - Read | Remove => vec!["ID"], - Search => vec!["Query"], - Create | ChangeAll => vec!["Body"], - Change => vec!["ID", "Body"] - } - } - - pub fn trait_ident(&self) -> Ident { - use Method::*; - - let name = match self { - ReadAll => "ReadAll", - Read => "Read", - Search => "Search", - Create => "Create", - ChangeAll => "ChangeAll", - Change => "Change", - RemoveAll => "RemoveAll", - Remove => "Remove" - }; - format_ident!("Resource{}", name) - } - - pub fn fn_ident(&self) -> Ident { - use Method::*; - - let name = match self { - ReadAll => "read_all", - Read => "read", - Search => "search", - Create => "create", - ChangeAll => "change_all", - Change => "change", - RemoveAll => "remove_all", - Remove => "remove" - }; - format_ident!("{}", name) - } - - pub fn handler_struct_ident(&self, resource: &str) -> Ident { - format_ident!("{}{}Handler", resource.to_camel_case(), self.trait_ident()) - } - - pub fn setup_ident(&self, resource: &str) -> Ident { - format_ident!("_gotham_restful_{}_{}_setup_impl", resource.to_snake_case(), self.fn_ident()) - } -} - -#[allow(clippy::large_enum_variant)] -enum MethodArgumentType { - StateRef, - StateMutRef, - MethodArg(Type), - DatabaseConnection(Type), - AuthStatus(Type), - AuthStatusRef(Type) -} - -impl MethodArgumentType { - fn is_method_arg(&self) -> bool { - matches!(self, Self::MethodArg(_)) - } - - fn is_database_conn(&self) -> bool { - matches!(self, Self::DatabaseConnection(_)) - } - - fn is_auth_status(&self) -> bool { - matches!(self, Self::AuthStatus(_) | Self::AuthStatusRef(_)) - } - - fn ty(&self) -> Option<&Type> { - match self { - Self::MethodArg(ty) | Self::DatabaseConnection(ty) | Self::AuthStatus(ty) | Self::AuthStatusRef(ty) => Some(ty), - _ => None - } - } - - fn quote_ty(&self) -> Option { - self.ty().map(|ty| quote!(#ty)) - } -} - -struct MethodArgument { - ident: Ident, - ident_span: Span, - ty: MethodArgumentType -} - -impl Spanned for MethodArgument { - fn span(&self) -> Span { - self.ident_span - } -} - -fn interpret_arg_ty(attrs: &[Attribute], name: &str, ty: Type) -> Result { - let attr = attrs - .iter() - .find(|arg| arg.path.segments.iter().any(|path| &path.ident.to_string() == "rest_arg")) - .map(|arg| arg.tokens.to_string()); - - // TODO issue a warning for _state usage once diagnostics become stable - if attr.as_deref() == Some("state") || (attr.is_none() && (name == "state" || name == "_state")) { - return match ty { - Type::Reference(ty) => Ok(if ty.mutability.is_none() { - MethodArgumentType::StateRef - } else { - MethodArgumentType::StateMutRef - }), - _ => Err(Error::new( - ty.span(), - "The state parameter has to be a (mutable) reference to gotham_restful::State" - )) - }; - } - - if cfg!(feature = "auth") && (attr.as_deref() == Some("auth") || (attr.is_none() && name == "auth")) { - return Ok(match ty { - Type::Reference(ty) => MethodArgumentType::AuthStatusRef(*ty.elem), - ty => MethodArgumentType::AuthStatus(ty) - }); - } - - if cfg!(feature = "database") - && (attr.as_deref() == Some("connection") || attr.as_deref() == Some("conn") || (attr.is_none() && name == "conn")) - { - return Ok(MethodArgumentType::DatabaseConnection(match ty { - Type::Reference(ty) => *ty.elem, - ty => ty - })); - } - - Ok(MethodArgumentType::MethodArg(ty)) -} - -fn interpret_arg(index: usize, arg: &PatType) -> Result { - let pat = &arg.pat; - let ident = format_ident!("arg{}", index); - let orig_name = quote!(#pat); - let ty = interpret_arg_ty(&arg.attrs, &orig_name.to_string(), *arg.ty.clone())?; - - Ok(MethodArgument { - ident, - ident_span: arg.pat.span(), - ty - }) -} - -#[cfg(feature = "openapi")] -fn expand_operation_id(attrs: &[NestedMeta]) -> TokenStream { - let mut operation_id: Option<&Lit> = None; - for meta in attrs { - if let NestedMeta::Meta(Meta::NameValue(kv)) = meta { - if kv.path.segments.last().map(|p| p.ident.to_string()) == Some("operation_id".to_owned()) { - operation_id = Some(&kv.lit) - } - } - } - - match operation_id { - Some(operation_id) => quote! { - fn operation_id() -> Option - { - Some(#operation_id.to_string()) - } - }, - None => quote!() - } -} - -#[cfg(not(feature = "openapi"))] -fn expand_operation_id(_: &[NestedMeta]) -> TokenStream { - quote!() -} - -fn expand_wants_auth(attrs: &[NestedMeta], default: bool) -> TokenStream { - let default_lit = Lit::Bool(LitBool { - value: default, - span: Span::call_site() - }); - let mut wants_auth = &default_lit; - for meta in attrs { - if let NestedMeta::Meta(Meta::NameValue(kv)) = meta { - if kv.path.segments.last().map(|p| p.ident.to_string()) == Some("wants_auth".to_owned()) { - wants_auth = &kv.lit - } - } - } - - quote! { - fn wants_auth() -> bool - { - #wants_auth - } - } -} - -#[allow(clippy::comparison_chain)] -fn setup_body( - method: &Method, - fun: &ItemFn, - attrs: &[NestedMeta], - resource_name: &str, - resource_path: &Path -) -> Result { - let krate = super::krate(); - - let fun_ident = &fun.sig.ident; - let fun_is_async = fun.sig.asyncness.is_some(); - - if let Some(unsafety) = fun.sig.unsafety { - return Err(Error::new(unsafety.span(), "Resource methods must not be unsafe")); - } - - let trait_ident = method.trait_ident(); - let method_ident = method.fn_ident(); - let handler_ident = method.handler_struct_ident(resource_name); - - let (ret, is_no_content) = match &fun.sig.output { - ReturnType::Default => (quote!(#krate::NoContent), true), - ReturnType::Type(_, ty) => (quote!(#ty), false) - }; - - // some default idents we'll need - let state_ident = format_ident!("state"); - let repo_ident = format_ident!("repo"); - let conn_ident = format_ident!("conn"); - let auth_ident = format_ident!("auth"); - let res_ident = format_ident!("res"); - - // extract arguments into pattern, ident and type - let args = fun - .sig - .inputs - .iter() - .enumerate() - .map(|(i, arg)| match arg { - FnArg::Typed(arg) => interpret_arg(i, arg), - FnArg::Receiver(_) => Err(Error::new(arg.span(), "Didn't expect self parameter")) - }) - .collect_to_result()?; - - // extract the generic parameters to use - let ty_names = method.type_names(); - let ty_len = ty_names.len(); - let generics_args: Vec<&MethodArgument> = args.iter().filter(|arg| (*arg).ty.is_method_arg()).collect(); - if generics_args.len() > ty_len { - return Err(Error::new(generics_args[ty_len].span(), "Too many arguments")); - } else if generics_args.len() < ty_len { - return Err(Error::new(fun_ident.span(), "Too few arguments")); - } - let generics: Vec = generics_args - .iter() - .map(|arg| arg.ty.quote_ty().unwrap()) - .zip(ty_names) - .map(|(arg, name)| { - let ident = format_ident!("{}", name); - quote!(type #ident = #arg;) - }) - .collect(); - - // extract the definition of our method - let mut args_def: Vec = args - .iter() - .filter(|arg| (*arg).ty.is_method_arg()) - .map(|arg| { - let ident = &arg.ident; - let ty = arg.ty.quote_ty(); - quote!(#ident : #ty) - }) - .collect(); - args_def.insert(0, quote!(mut #state_ident : #krate::State)); - - // extract the arguments to pass over to the supplied method - let args_pass: Vec = args - .iter() - .map(|arg| match (&arg.ty, &arg.ident) { - (MethodArgumentType::StateRef, _) => quote!(&#state_ident), - (MethodArgumentType::StateMutRef, _) => quote!(&mut #state_ident), - (MethodArgumentType::MethodArg(_), ident) => quote!(#ident), - (MethodArgumentType::DatabaseConnection(_), _) => quote!(&#conn_ident), - (MethodArgumentType::AuthStatus(_), _) => quote!(#auth_ident), - (MethodArgumentType::AuthStatusRef(_), _) => quote!(&#auth_ident) - }) - .collect(); - - // prepare the method block - let mut block = quote!(#fun_ident(#(#args_pass),*)); - let mut state_block = quote!(); - if fun_is_async { - if let Some(arg) = args.iter().find(|arg| matches!((*arg).ty, MethodArgumentType::StateRef)) { - return Err(Error::new( - arg.span(), - "async fn must not take &State as an argument as State is not Sync, consider taking &mut State" - )); - } - block = quote!(#block.await); - } - if is_no_content { - block = quote!(#block; Default::default()) - } - if let Some(arg) = args.iter().find(|arg| (*arg).ty.is_database_conn()) { - if fun_is_async { - return Err(Error::new( - arg.span(), - "async fn is not supported when database support is required, consider boxing" - )); - } - let conn_ty = arg.ty.quote_ty(); - state_block = quote! { - #state_block - let #repo_ident = <#krate::export::Repo<#conn_ty>>::borrow_from(&#state_ident).clone(); - }; - block = quote! { - { - let #res_ident = #repo_ident.run::<_, (#krate::State, #ret), ()>(move |#conn_ident| { - let #res_ident = { #block }; - Ok((#state_ident, #res_ident)) - }).await.unwrap(); - #state_ident = #res_ident.0; - #res_ident.1 - } - }; - } - if let Some(arg) = args.iter().find(|arg| (*arg).ty.is_auth_status()) { - let auth_ty = arg.ty.quote_ty(); - state_block = quote! { - #state_block - let #auth_ident : #auth_ty = <#auth_ty>::borrow_from(&#state_ident).clone(); - }; - } - - // prepare the where clause - let mut where_clause = quote!(#resource_path : #krate::Resource,); - for arg in args.iter().filter(|arg| (*arg).ty.is_auth_status()) { - let auth_ty = arg.ty.quote_ty(); - where_clause = quote!(#where_clause #auth_ty : Clone,); - } - - // attribute generated code - let operation_id = expand_operation_id(attrs); - let wants_auth = expand_wants_auth(attrs, args.iter().any(|arg| (*arg).ty.is_auth_status())); - - // put everything together - let mut dummy = format_ident!("_IMPL_RESOURCEMETHOD_FOR_{}", fun_ident); - dummy.set_span(Span::call_site()); - Ok(quote! { - struct #handler_ident; - - impl #krate::ResourceMethod for #handler_ident { - type Res = #ret; - - #operation_id - #wants_auth - } - - impl #krate::#trait_ident for #handler_ident - where #where_clause - { - #(#generics)* - - fn #method_ident(#(#args_def),*) -> std::pin::Pin + Send>> { - #[allow(unused_imports)] - use #krate::{export::FutureExt, FromState}; - - #state_block - - async move { - let #res_ident = { #block }; - (#state_ident, #res_ident) - }.boxed() - } - } - - route.#method_ident::<#handler_ident>(); - }) -} - -pub fn expand_method(method: Method, mut attrs: AttributeArgs, fun: ItemFn) -> Result { - let krate = super::krate(); - - // parse attributes - if attrs.len() < 1 { - return Err(Error::new( - Span::call_site(), - "Missing Resource struct. Example: #[read_all(MyResource)]" - )); - } - let resource_path = match attrs.remove(0) { - NestedMeta::Meta(Meta::Path(path)) => path, - p => { - return Err(Error::new( - p.span(), - "Expected name of the Resource struct this method belongs to" - )) - }, - }; - let resource_name = resource_path - .segments - .last() - .map(|s| s.ident.to_string()) - .ok_or_else(|| Error::new(resource_path.span(), "Resource name must not be empty"))?; - - let fun_vis = &fun.vis; - let setup_ident = method.setup_ident(&resource_name); - let setup_body = match setup_body(&method, &fun, &attrs, &resource_name, &resource_path) { - Ok(body) => body, - Err(err) => err.to_compile_error() - }; - - Ok(quote! { - #fun - - #[deny(dead_code)] - #[doc(hidden)] - /// `gotham_restful` implementation detail. - #fun_vis fn #setup_ident(route : &mut D) { - #setup_body - } - }) -} diff --git a/derive/src/openapi_type.rs b/derive/src/openapi_type.rs index b3f2d36..ae14678 100644 --- a/derive/src/openapi_type.rs +++ b/derive/src/openapi_type.rs @@ -3,7 +3,8 @@ use proc_macro2::{Ident, TokenStream}; use quote::quote; use syn::{ parse_macro_input, spanned::Spanned, Attribute, AttributeArgs, Data, DataEnum, DataStruct, DeriveInput, Error, Field, - Fields, GenericParam, Generics, Lit, LitStr, Meta, NestedMeta, Result, Variant + Fields, GenericParam, Generics, Lit, LitStr, Meta, NestedMeta, Path, PathSegment, PredicateType, Result, TraitBound, + TraitBoundModifier, Type, TypeParamBound, TypePath, Variant, WhereClause, WherePredicate }; pub fn expand_openapi_type(input: DeriveInput) -> Result { @@ -17,24 +18,46 @@ pub fn expand_openapi_type(input: DeriveInput) -> Result { } } -fn expand_where(generics: &Generics) -> TokenStream { +fn update_generics(generics: &Generics, where_clause: &mut Option) { if generics.params.is_empty() { - return quote!(); + return; } - let krate = super::krate(); - let idents = generics - .params - .iter() - .map(|param| match param { - GenericParam::Type(ty) => Some(ty.ident.clone()), - _ => None - }) - .filter(|param| param.is_some()) - .map(|param| param.unwrap()); + if where_clause.is_none() { + *where_clause = Some(WhereClause { + where_token: Default::default(), + predicates: Default::default() + }); + } + let where_clause = where_clause.as_mut().unwrap(); - quote! { - where #(#idents : #krate::OpenapiType),* + for param in &generics.params { + if let GenericParam::Type(ty_param) = param { + where_clause.predicates.push(WherePredicate::Type(PredicateType { + lifetimes: None, + bounded_ty: Type::Path(TypePath { + qself: None, + path: Path { + leading_colon: None, + segments: vec![PathSegment { + ident: ty_param.ident.clone(), + arguments: Default::default() + }] + .into_iter() + .collect() + } + }), + colon_token: Default::default(), + bounds: vec![TypeParamBound::Trait(TraitBound { + paren_token: None, + modifier: TraitBoundModifier::None, + lifetimes: None, + path: syn::parse_str("::gotham_restful::OpenapiType").unwrap() + })] + .into_iter() + .collect() + })); + } } } @@ -106,7 +129,9 @@ fn expand_variant(variant: &Variant) -> Result { fn expand_enum(ident: Ident, generics: Generics, attrs: Vec, input: DataEnum) -> Result { let krate = super::krate(); - let where_clause = expand_where(&generics); + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + let mut where_clause = where_clause.cloned(); + update_generics(&generics, &mut where_clause); let attrs = parse_attributes(&attrs)?; let nullable = attrs.nullable; @@ -118,7 +143,7 @@ fn expand_enum(ident: Ident, generics: Generics, attrs: Vec, input: D let variants = input.variants.iter().map(expand_variant).collect_to_result()?; Ok(quote! { - impl #generics #krate::OpenapiType for #ident #generics + impl #impl_generics #krate::OpenapiType for #ident #ty_generics #where_clause { fn schema() -> #krate::OpenapiSchema @@ -208,7 +233,9 @@ fn expand_field(field: &Field) -> Result { fn expand_struct(ident: Ident, generics: Generics, attrs: Vec, input: DataStruct) -> Result { let krate = super::krate(); - let where_clause = expand_where(&generics); + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + let mut where_clause = where_clause.cloned(); + update_generics(&generics, &mut where_clause); let attrs = parse_attributes(&attrs)?; let nullable = attrs.nullable; @@ -229,7 +256,7 @@ fn expand_struct(ident: Ident, generics: Generics, attrs: Vec, input: }; Ok(quote! { - impl #generics #krate::OpenapiType for #ident #generics + impl #impl_generics #krate::OpenapiType for #ident #ty_generics #where_clause { fn schema() -> #krate::OpenapiSchema diff --git a/derive/src/private_openapi_trait.rs b/derive/src/private_openapi_trait.rs new file mode 100644 index 0000000..1486119 --- /dev/null +++ b/derive/src/private_openapi_trait.rs @@ -0,0 +1,165 @@ +use crate::util::{remove_parens, CollectToResult, PathEndsWith}; +use proc_macro2::{Span, TokenStream}; +use quote::{quote, ToTokens}; +use syn::{ + parse::Parse, spanned::Spanned, Attribute, AttributeArgs, Error, ItemTrait, LitStr, Meta, NestedMeta, PredicateType, + Result, TraitItem, WherePredicate +}; + +struct TraitItemAttrs { + openapi_only: bool, + openapi_bound: Vec, + non_openapi_bound: Vec, + other_attrs: Vec +} + +impl TraitItemAttrs { + fn parse(attrs: Vec) -> Result { + let mut openapi_only = false; + let mut openapi_bound = Vec::new(); + let mut non_openapi_bound = Vec::new(); + let mut other = Vec::new(); + + for attr in attrs { + if attr.path.ends_with("openapi_only") { + openapi_only = true; + } else if attr.path.ends_with("openapi_bound") { + let attr_arg: LitStr = syn::parse2(remove_parens(attr.tokens))?; + let predicate = attr_arg.parse_with(WherePredicate::parse)?; + openapi_bound.push(match predicate { + WherePredicate::Type(ty) => ty, + _ => return Err(Error::new(predicate.span(), "Expected type bound")) + }); + } else if attr.path.ends_with("non_openapi_bound") { + let attr_arg: LitStr = syn::parse2(remove_parens(attr.tokens))?; + let predicate = attr_arg.parse_with(WherePredicate::parse)?; + non_openapi_bound.push(match predicate { + WherePredicate::Type(ty) => ty, + _ => return Err(Error::new(predicate.span(), "Expected type bound")) + }); + } else { + other.push(attr); + } + } + + Ok(Self { + openapi_only, + openapi_bound, + non_openapi_bound, + other_attrs: other + }) + } +} + +pub(crate) fn expand_private_openapi_trait(mut attrs: AttributeArgs, tr8: ItemTrait) -> Result { + let tr8_attrs = &tr8.attrs; + let vis = &tr8.vis; + let ident = &tr8.ident; + let generics = &tr8.generics; + let colon_token = &tr8.colon_token; + let supertraits = &tr8.supertraits; + + if attrs.len() != 1 { + return Err(Error::new( + Span::call_site(), + "Expected one argument. Example: #[_private_openapi_trait(OpenapiTraitName)]" + )); + } + let openapi_ident = match attrs.remove(0) { + NestedMeta::Meta(Meta::Path(path)) => path, + p => { + return Err(Error::new( + p.span(), + "Expected name of the Resource struct this method belongs to" + )) + }, + }; + + let orig_trait = { + let items = tr8 + .items + .clone() + .into_iter() + .map(|item| { + Ok(match item { + TraitItem::Method(mut method) => { + let attrs = TraitItemAttrs::parse(method.attrs)?; + method.attrs = attrs.other_attrs; + for bound in attrs.non_openapi_bound { + method + .sig + .generics + .type_params_mut() + .filter(|param| param.ident.to_string() == bound.bounded_ty.to_token_stream().to_string()) + .for_each(|param| param.bounds.extend(bound.bounds.clone())); + } + if attrs.openapi_only { + None + } else { + Some(TraitItem::Method(method)) + } + }, + TraitItem::Type(mut ty) => { + let attrs = TraitItemAttrs::parse(ty.attrs)?; + ty.attrs = attrs.other_attrs; + Some(TraitItem::Type(ty)) + }, + item => Some(item) + }) + }) + .collect_to_result()?; + quote! { + #(#tr8_attrs)* + #vis trait #ident #generics #colon_token #supertraits { + #(#items)* + } + } + }; + + let openapi_trait = if !cfg!(feature = "openapi") { + None + } else { + let items = tr8 + .items + .clone() + .into_iter() + .map(|item| { + Ok(match item { + TraitItem::Method(mut method) => { + let attrs = TraitItemAttrs::parse(method.attrs)?; + method.attrs = attrs.other_attrs; + for bound in attrs.openapi_bound { + method + .sig + .generics + .type_params_mut() + .filter(|param| param.ident.to_string() == bound.bounded_ty.to_token_stream().to_string()) + .for_each(|param| param.bounds.extend(bound.bounds.clone())); + } + TraitItem::Method(method) + }, + TraitItem::Type(mut ty) => { + let attrs = TraitItemAttrs::parse(ty.attrs)?; + ty.attrs = attrs.other_attrs; + for bound in attrs.openapi_bound { + ty.bounds.extend(bound.bounds.clone()); + } + TraitItem::Type(ty) + }, + item => item + }) + }) + .collect_to_result()?; + Some(quote! { + #(#tr8_attrs)* + #vis trait #openapi_ident #generics #colon_token #supertraits { + #(#items)* + } + }) + }; + + Ok(quote! { + #orig_trait + #openapi_trait + }) +} diff --git a/derive/src/resource.rs b/derive/src/resource.rs index 60ae159..c2a2e01 100644 --- a/derive/src/resource.rs +++ b/derive/src/resource.rs @@ -1,12 +1,15 @@ -use crate::{method::Method, util::CollectToResult}; +use crate::{ + endpoint::endpoint_ident, + util::{CollectToResult, PathEndsWith} +}; use proc_macro2::{Ident, TokenStream}; use quote::quote; -use std::{iter, str::FromStr}; +use std::iter; use syn::{ parenthesized, parse::{Parse, ParseStream}, punctuated::Punctuated, - DeriveInput, Error, Result, Token + DeriveInput, Result, Token }; struct MethodList(Punctuated); @@ -23,29 +26,22 @@ impl Parse for MethodList { pub fn expand_resource(input: DeriveInput) -> Result { let krate = super::krate(); let ident = input.ident; - let name = ident.to_string(); - let methods = - input - .attrs - .into_iter() - .filter( - |attr| { - attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("resource".to_string()) - } // TODO wtf - ) - .map(|attr| syn::parse2(attr.tokens).map(|m: MethodList| m.0.into_iter())) - .flat_map(|list| match list { - Ok(iter) => Box::new(iter.map(|method| { - let method = Method::from_str(&method.to_string()).map_err(|err| Error::new(method.span(), err))?; - let ident = method.setup_ident(&name); - Ok(quote!(#ident(&mut route);)) - })) as Box>>, - Err(err) => Box::new(iter::once(Err(err))) - }) - .collect_to_result()?; + let methods = input + .attrs + .into_iter() + .filter(|attr| attr.path.ends_with("resource")) + .map(|attr| syn::parse2(attr.tokens).map(|m: MethodList| m.0.into_iter())) + .flat_map(|list| match list { + Ok(iter) => Box::new(iter.map(|method| { + let ident = endpoint_ident(&method); + Ok(quote!(route.endpoint::<#ident>();)) + })) as Box>>, + Err(err) => Box::new(iter::once(Err(err))) + }) + .collect_to_result()?; - Ok(quote! { + let non_openapi_impl = quote! { impl #krate::Resource for #ident { fn setup(mut route : D) @@ -53,5 +49,22 @@ pub fn expand_resource(input: DeriveInput) -> Result { #(#methods)* } } + }; + let openapi_impl = if !cfg!(feature = "openapi") { + None + } else { + Some(quote! { + impl #krate::ResourceWithSchema for #ident + { + fn setup(mut route : D) + { + #(#methods)* + } + } + }) + }; + Ok(quote! { + #non_openapi_impl + #openapi_impl }) } diff --git a/derive/src/util.rs b/derive/src/util.rs index aedb9e6..aa94ce5 100644 --- a/derive/src/util.rs +++ b/derive/src/util.rs @@ -1,6 +1,6 @@ use proc_macro2::{Delimiter, TokenStream, TokenTree}; use std::iter; -use syn::Error; +use syn::{Error, Path}; pub trait CollectToResult { type Item; @@ -30,6 +30,16 @@ where } } +pub(crate) trait PathEndsWith { + fn ends_with(&self, s: &str) -> bool; +} + +impl PathEndsWith for Path { + fn ends_with(&self, s: &str) -> bool { + self.segments.last().map(|segment| segment.ident.to_string()).as_deref() == Some(s) + } +} + pub fn remove_parens(input: TokenStream) -> TokenStream { let iter = input.into_iter().flat_map(|tt| { if let TokenTree::Group(group) = &tt { diff --git a/example/Cargo.toml b/example/Cargo.toml index f9fdab1..d049d98 100644 --- a/example/Cargo.toml +++ b/example/Cargo.toml @@ -18,7 +18,7 @@ gitlab = { repository = "msrd0/gotham-restful", branch = "master" } fake = "2.2.2" gotham = { version = "0.5.0", default-features = false } gotham_derive = "0.5.0" -gotham_restful = { version = "0.2.0-dev", features = ["auth", "openapi"] } +gotham_restful = { version = "0.2.0-dev", features = ["auth", "openapi"], default-features = false } log = "0.4.8" pretty_env_logger = "0.4" serde = "1.0.110" diff --git a/src/auth.rs b/src/auth.rs index 15b5afe..935c7bd 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -136,7 +136,7 @@ struct AuthData { exp: u64 } -#[read_all(AuthResource)] +#[read_all] fn read_all(auth : &AuthStatus) -> Success { format!("{:?}", auth).into() } diff --git a/src/cors.rs b/src/cors.rs index 7f1fc31..b563ea5 100644 --- a/src/cors.rs +++ b/src/cors.rs @@ -246,7 +246,7 @@ where fn cors(&mut self, path: &str, method: Method); } -fn cors_preflight_handler(state: State) -> (State, Response) { +pub(crate) fn cors_preflight_handler(state: State) -> (State, Response) { let config = CorsConfig::try_borrow_from(&state); // prepare the response diff --git a/src/endpoint.rs b/src/endpoint.rs new file mode 100644 index 0000000..90bc952 --- /dev/null +++ b/src/endpoint.rs @@ -0,0 +1,105 @@ +use crate::{RequestBody, ResourceResult}; +use futures_util::future::BoxFuture; +use gotham::{ + extractor::{PathExtractor, QueryStringExtractor}, + hyper::{Body, Method}, + state::State +}; +use std::borrow::Cow; + +// TODO: Specify default types once https://github.com/rust-lang/rust/issues/29661 lands. +#[_private_openapi_trait(EndpointWithSchema)] +pub trait Endpoint { + /// The HTTP Verb of this endpoint. + fn http_method() -> Method; + /// The URI that this endpoint listens on in gotham's format. + fn uri() -> Cow<'static, str>; + + /// The output type that provides the response. + type Output: ResourceResult + Send; + + /// Returns `true` _iff_ the URI contains placeholders. `false` by default. + fn has_placeholders() -> bool { + false + } + /// The type that parses the URI placeholders. Use [gotham::extractor::NoopPathExtractor] + /// if `has_placeholders()` returns `false`. + #[openapi_bound("Placeholders: crate::OpenapiType")] + type Placeholders: PathExtractor + Sync; + + /// Returns `true` _iff_ the request parameters should be parsed. `false` by default. + fn needs_params() -> bool { + false + } + /// The type that parses the request parameters. Use [gotham::extractor::NoopQueryStringExtractor] + /// if `needs_params()` returns `false`. + #[openapi_bound("Params: crate::OpenapiType")] + type Params: QueryStringExtractor + Sync; + + /// Returns `true` _iff_ the request body should be parsed. `false` by default. + fn needs_body() -> bool { + false + } + /// The type to parse the body into. Use `()` if `needs_body()` returns `false`. + type Body: RequestBody + Send; + + /// Returns `true` if the request wants to know the auth status of the client. `false` by default. + fn wants_auth() -> bool { + false + } + + /// Replace the automatically generated operation id with a custom one. Only relevant for the + /// OpenAPI Specification. + #[openapi_only] + fn operation_id() -> Option { + None + } + + /// The handler for this endpoint. + fn handle( + state: &mut State, + placeholders: Self::Placeholders, + params: Self::Params, + body: Option + ) -> BoxFuture<'static, Self::Output>; +} + +#[cfg(feature = "openapi")] +impl Endpoint for E { + fn http_method() -> Method { + E::http_method() + } + fn uri() -> Cow<'static, str> { + E::uri() + } + + type Output = E::Output; + + fn has_placeholders() -> bool { + E::has_placeholders() + } + type Placeholders = E::Placeholders; + + fn needs_params() -> bool { + E::needs_params() + } + type Params = E::Params; + + fn needs_body() -> bool { + E::needs_body() + } + type Body = E::Body; + + fn wants_auth() -> bool { + E::wants_auth() + } + + fn handle( + state: &mut State, + placeholders: Self::Placeholders, + params: Self::Params, + body: Option + ) -> BoxFuture<'static, Self::Output> { + E::handle(state, placeholders, params, body) + } +} diff --git a/src/lib.rs b/src/lib.rs index 9096bbe..7f9a234 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,7 @@ #![forbid(unsafe_code)] /*! This crate is an extension to the popular [gotham web framework][gotham] for Rust. It allows you to -create resources with assigned methods that aim to be a more convenient way of creating handlers +create resources with assigned endpoints that aim to be a more convenient way of creating handlers for requests. # Features @@ -27,23 +27,23 @@ for requests. This crate is just as safe as you'd expect from anything written in safe Rust - and `#![forbid(unsafe_code)]` ensures that no unsafe was used. -# Methods +# Endpoints -Assuming you assign `/foobar` to your resource, you can implement the following methods: +Assuming you assign `/foobar` to your resource, the following pre-defined endpoints exist: -| Method Name | Required Arguments | HTTP Verb | HTTP Path | -| ----------- | ------------------ | --------- | ----------- | -| read_all | | GET | /foobar | -| read | id | GET | /foobar/:id | -| search | query | GET | /foobar/search | -| create | body | POST | /foobar | -| change_all | body | PUT | /foobar | -| change | id, body | PUT | /foobar/:id | -| remove_all | | DELETE | /foobar | -| remove | id | DELETE | /foobar/:id | +| Endpoint Name | Required Arguments | HTTP Verb | HTTP Path | +| ------------- | ------------------ | --------- | -------------- | +| read_all | | GET | /foobar | +| read | id | GET | /foobar/:id | +| search | query | GET | /foobar/search | +| create | body | POST | /foobar | +| change_all | body | PUT | /foobar | +| change | id, body | PUT | /foobar/:id | +| remove_all | | DELETE | /foobar | +| remove | id | DELETE | /foobar/:id | -Each of those methods has a macro that creates the neccessary boilerplate for the Resource. A -simple example could look like this: +Each of those endpoints has a macro that creates the neccessary boilerplate for the Resource. A +simple example looks like this: ```rust,no_run # #[macro_use] extern crate gotham_restful_derive; @@ -55,15 +55,15 @@ simple example could look like this: #[resource(read)] struct FooResource; -/// The return type of the foo read method. +/// The return type of the foo read endpoint. #[derive(Serialize)] # #[cfg_attr(feature = "openapi", derive(OpenapiType))] struct Foo { id: u64 } -/// The foo read method handler. -#[read(FooResource)] +/// The foo read endpoint. +#[read] fn read(id: u64) -> Success { Foo { id }.into() } @@ -76,17 +76,16 @@ fn read(id: u64) -> Success { # Arguments -Some methods require arguments. Those should be - * **id** Should be a deserializable json-primitive like `i64` or `String`. +Some endpoints require arguments. Those should be + * **id** Should be a deserializable json-primitive like [`i64`] or [`String`]. * **body** Should be any deserializable object, or any type implementing [`RequestBody`]. * **query** Should be any deserializable object whose variables are json-primitives. It will however not be parsed from json, but from HTTP GET parameters like in `search?id=1`. The - type needs to implement [`QueryStringExtractor`]. + type needs to implement [`QueryStringExtractor`](gotham::extractor::QueryStringExtractor). -Additionally, non-async handlers may take a reference to gotham's [`State`]. If you need to -have an async handler (that is, the function that the method macro is invoked on is declared -as `async fn`), consider returning the boxed future instead. Since [`State`] does not implement -`Sync` there is unfortunately no more convenient way. +Additionally, all handlers may take a reference to gotham's [`State`]. Please note that for async +handlers, it needs to be a mutable reference until rustc's lifetime checks across await bounds +improve. # Uploads and Downloads @@ -110,7 +109,7 @@ struct RawImage { content_type: Mime } -#[create(ImageResource)] +#[create] fn create(body : RawImage) -> Raw> { Raw::new(body.content, body.content_type) } @@ -127,21 +126,23 @@ To make life easier for common use-cases, this create offers a few features that when you implement your web server. The complete feature list is - [`auth`](#authentication-feature) Advanced JWT middleware - `chrono` openapi support for chrono types - - [`cors`](#cors-feature) CORS handling for all method handlers + - `full` enables all features except `without-openapi` + - [`cors`](#cors-feature) CORS handling for all endpoint handlers - [`database`](#database-feature) diesel middleware support - - `errorlog` log errors returned from method handlers + - `errorlog` log errors returned from endpoint handlers - [`openapi`](#openapi-feature) router additions to generate an openapi spec - `uuid` openapi support for uuid + - `without-openapi` (**default**) disables `openapi` support. ## Authentication Feature In order to enable authentication support, enable the `auth` feature gate. This allows you to register a middleware that can automatically check for the existence of an JWT authentication -token. Besides being supported by the method macros, it supports to lookup the required JWT secret +token. Besides being supported by the endpoint macros, it supports to lookup the required JWT secret with the JWT data, hence you can use several JWT secrets and decide on the fly which secret to use. None of this is currently supported by gotham's own JWT middleware. -A simple example that uses only a single secret could look like this: +A simple example that uses only a single secret looks like this: ```rust,no_run # #[macro_use] extern crate gotham_restful_derive; @@ -167,7 +168,7 @@ struct AuthData { exp: u64 } -#[read(SecretResource)] +#[read] fn read(auth: AuthStatus, id: u64) -> AuthSuccess { let intended_for = auth.ok()?.sub; Ok(Secret { id, intended_for }) @@ -194,7 +195,7 @@ the `Access-Control-Allow-Methods` header is touched. To change the behaviour, a configuration as a middleware. A simple example that allows authentication from every origin (note that `*` always disallows -authentication), and every content type, could look like this: +authentication), and every content type, looks like this: ```rust,no_run # #[macro_use] extern crate gotham_restful_derive; @@ -207,7 +208,7 @@ authentication), and every content type, could look like this: #[resource(read_all)] struct FooResource; -#[read_all(FooResource)] +#[read_all] fn read_all() { // your handler } @@ -237,7 +238,7 @@ note however that due to the way gotham's diesel middleware implementation, it i to run async code while holding a database connection. If you need to combine async and database, you'll need to borrow the connection from the [`State`] yourself and return a boxed future. -A simple non-async example could look like this: +A simple non-async example looks like this: ```rust,no_run # #[macro_use] extern crate diesel; @@ -267,7 +268,7 @@ struct Foo { value: String } -#[read_all(FooResource)] +#[read_all] fn read_all(conn: &PgConnection) -> QueryResult> { foo::table.load(conn) } @@ -295,7 +296,7 @@ In order to automatically create an openapi specification, gotham-restful needs all routes and the types returned. `serde` does a great job at serialization but doesn't give enough type information, so all types used in the router need to implement `OpenapiType`. This can be derived for almoust any type and there should be no need to implement it manually. A simple -example could look like this: +example looks like this: ```rust,no_run # #[macro_use] extern crate gotham_restful_derive; @@ -313,7 +314,7 @@ struct Foo { bar: String } -#[read_all(FooResource)] +#[read_all] fn read_all() -> Success { Foo { bar: "Hello World".to_owned() }.into() } @@ -334,48 +335,37 @@ fn main() { # } ``` -Above example adds the resource as before, but adds another endpoint that we specified as `/openapi` -that will return the generated openapi specification. This allows you to easily write clients -in different languages without worying to exactly replicate your api in each of those languages. +Above example adds the resource as before, but adds another endpoint that we specified as `/openapi`. +It will return the generated openapi specification in JSON format. This allows you to easily write +clients in different languages without worying to exactly replicate your api in each of those +languages. -However, as of right now there is one caveat. If you wrote code before enabling the openapi feature, -it is likely to break. This is because of the new requirement of `OpenapiType` for all types used -with resources, even outside of the `with_openapi` scope. This issue will eventually be resolved. -If you are writing a library that uses gotham-restful, make sure that you expose an openapi feature. -In other words, put +However, please note that by default, the `without-openapi` feature of this crate is enabled. +Disabling it in favour of the `openapi` feature will add an additional type bound, [`OpenapiType`], +on some of the types in [`Endpoint`] and related traits. This means that some code might only +compile on either feature, but not on both. If you are writing a library that uses gotham-restful, +it is strongly recommended to pass both features through and conditionally enable the openapi +code, like this: -```toml -[features] -openapi = ["gotham-restful/openapi"] -``` - -into your libraries `Cargo.toml` and use the following for all types used with handlers: - -``` -# #[cfg(feature = "openapi")] -# mod openapi_feature_enabled { -# use gotham_restful::OpenapiType; +```rust +# #[macro_use] extern crate gotham_restful; # use serde::{Deserialize, Serialize}; #[derive(Deserialize, Serialize)] #[cfg_attr(feature = "openapi", derive(OpenapiType))] struct Foo; -# } ``` # Examples -There is a lack of good examples, but there is currently a collection of code in the [example] -directory, that might help you. Any help writing more examples is highly appreciated. +This readme and the crate documentation contain some of example. In addition to that, there is +a collection of code in the [example] directory that might help you. Any help writing more +examples is highly appreciated. [diesel]: https://diesel.rs/ [example]: https://gitlab.com/msrd0/gotham-restful/tree/master/example [gotham]: https://gotham.rs/ [serde_json]: https://github.com/serde-rs/json#serde-json---- - [`CorsRoute`]: trait.CorsRoute.html - [`QueryStringExtractor`]: ../gotham/extractor/trait.QueryStringExtractor.html - [`RequestBody`]: trait.RequestBody.html - [`State`]: ../gotham/state/struct.State.html */ #[cfg(all(feature = "openapi", feature = "without-openapi"))] @@ -390,6 +380,8 @@ extern crate self as gotham_restful; #[macro_use] extern crate gotham_derive; #[macro_use] +extern crate gotham_restful_derive; +#[macro_use] extern crate log; #[macro_use] extern crate serde; @@ -409,7 +401,9 @@ pub use gotham_restful_derive::*; /// Not public API #[doc(hidden)] pub mod export { - pub use futures_util::future::FutureExt; + pub use crate::routing::PathExtractor as IdPlaceholder; + + pub use futures_util::future::{BoxFuture, FutureExt}; pub use serde_json; @@ -441,11 +435,10 @@ pub use openapi::{ types::{OpenapiSchema, OpenapiType} }; -mod resource; -pub use resource::{ - Resource, ResourceChange, ResourceChangeAll, ResourceCreate, ResourceMethod, ResourceRead, ResourceReadAll, - ResourceRemove, ResourceRemoveAll, ResourceSearch -}; +mod endpoint; +pub use endpoint::Endpoint; +#[cfg(feature = "openapi")] +pub use endpoint::EndpointWithSchema; mod response; pub use response::Response; @@ -457,9 +450,21 @@ pub use result::{ }; mod routing; -#[cfg(feature = "openapi")] -pub use routing::WithOpenapi; pub use routing::{DrawResourceRoutes, DrawResources}; +#[cfg(feature = "openapi")] +pub use routing::{DrawResourceRoutesWithSchema, DrawResourcesWithSchema, WithOpenapi}; mod types; pub use types::*; + +/// This trait must be implemented for every resource. It allows you to register the different +/// endpoints 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)]`. +#[_private_openapi_trait(ResourceWithSchema)] +pub trait Resource { + /// Register all methods handled by this resource with the underlying router. + #[openapi_bound("D: crate::DrawResourceRoutesWithSchema")] + #[non_openapi_bound("D: crate::DrawResourceRoutes")] + fn setup(route: D); +} diff --git a/src/openapi/operation.rs b/src/openapi/operation.rs index a70e258..8c83af5 100644 --- a/src/openapi/operation.rs +++ b/src/openapi/operation.rs @@ -1,5 +1,5 @@ use super::SECURITY_NAME; -use crate::{resource::*, result::*, OpenapiSchema, RequestBody}; +use crate::{result::*, EndpointWithSchema, OpenapiSchema, RequestBody}; use indexmap::IndexMap; use mime::Mime; use openapiv3::{ @@ -8,31 +8,40 @@ use openapiv3::{ }; #[derive(Default)] -struct OperationParams<'a> { - path_params: Vec<(&'a str, ReferenceOr)>, +struct OperationParams { + path_params: Option, query_params: Option } -impl<'a> OperationParams<'a> { - fn add_path_params(&self, params: &mut Vec>) { - for param in &self.path_params { +impl OperationParams { + fn add_path_params(path_params: Option, params: &mut Vec>) { + let path_params = match path_params { + Some(pp) => pp.schema, + None => return + }; + let path_params = match path_params { + SchemaKind::Type(Type::Object(ty)) => ty, + _ => panic!("Path Parameters needs to be a plain struct") + }; + for (name, schema) in path_params.properties { + let required = path_params.required.contains(&name); params.push(Item(Parameter::Path { parameter_data: ParameterData { - name: (*param).0.to_string(), + name, description: None, - required: true, + required, deprecated: None, - format: ParameterSchemaOrContent::Schema((*param).1.clone()), + format: ParameterSchemaOrContent::Schema(schema.unbox()), example: None, examples: IndexMap::new() }, style: Default::default() - })); + })) } } - fn add_query_params(self, params: &mut Vec>) { - let query_params = match self.query_params { + fn add_query_params(query_params: Option, params: &mut Vec>) { + let query_params = match query_params { Some(qp) => qp.schema, None => return }; @@ -61,51 +70,48 @@ impl<'a> OperationParams<'a> { fn into_params(self) -> Vec> { let mut params: Vec> = Vec::new(); - self.add_path_params(&mut params); - self.add_query_params(&mut params); + Self::add_path_params(self.path_params, &mut params); + Self::add_query_params(self.query_params, &mut params); params } } -pub struct OperationDescription<'a> { +pub struct OperationDescription { operation_id: Option, default_status: crate::StatusCode, accepted_types: Option>, schema: ReferenceOr, - params: OperationParams<'a>, + params: OperationParams, body_schema: Option>, supported_types: Option>, requires_auth: bool } -impl<'a> OperationDescription<'a> { - pub fn new(schema: ReferenceOr) -> Self { +impl OperationDescription { + pub fn new(schema: ReferenceOr) -> Self { Self { - operation_id: Handler::operation_id(), - default_status: Handler::Res::default_status(), - accepted_types: Handler::Res::accepted_types(), + operation_id: E::operation_id(), + default_status: E::Output::default_status(), + accepted_types: E::Output::accepted_types(), schema, params: Default::default(), body_schema: None, supported_types: None, - requires_auth: Handler::wants_auth() + requires_auth: E::wants_auth() } } - pub fn add_path_param(mut self, name: &'a str, schema: ReferenceOr) -> Self { - self.params.path_params.push((name, schema)); - self + pub fn set_path_params(&mut self, params: OpenapiSchema) { + self.params.path_params = Some(params); } - pub fn with_query_params(mut self, params: OpenapiSchema) -> Self { + pub fn set_query_params(&mut self, params: OpenapiSchema) { self.params.query_params = Some(params); - self } - pub fn with_body(mut self, schema: ReferenceOr) -> Self { + pub fn set_body(&mut self, schema: ReferenceOr) { self.body_schema = Some(schema); self.supported_types = Body::supported_types(); - self } fn schema_to_content(types: Vec, schema: ReferenceOr) -> IndexMap { diff --git a/src/openapi/router.rs b/src/openapi/router.rs index 19ce1a6..a836a40 100644 --- a/src/openapi/router.rs +++ b/src/openapi/router.rs @@ -1,6 +1,8 @@ use super::{builder::OpenapiBuilder, handler::OpenapiHandler, operation::OperationDescription}; -use crate::{resource::*, routing::*, OpenapiType}; -use gotham::{pipeline::chain::PipelineHandleChain, router::builder::*}; +use crate::{routing::*, EndpointWithSchema, OpenapiType, ResourceWithSchema}; +use gotham::{hyper::Method, pipeline::chain::PipelineHandleChain, router::builder::*}; +use once_cell::sync::Lazy; +use regex::{Captures, Regex}; use std::panic::RefUnwindSafe; /// This trait adds the `get_openapi` method to an OpenAPI-aware router. @@ -51,150 +53,62 @@ macro_rules! implOpenapiRouter { } } - impl<'a, 'b, C, P> DrawResources for OpenapiRouter<'a, $implType<'b, C, P>> + impl<'a, 'b, C, P> DrawResourcesWithSchema for OpenapiRouter<'a, $implType<'b, C, P>> where C: PipelineHandleChain

+ Copy + Send + Sync + 'static, P: RefUnwindSafe + Send + Sync + 'static { - fn resource(&mut self, path: &str) { + fn resource(&mut self, path: &str) { R::setup((self, path)); } } - impl<'a, 'b, C, P> DrawResourceRoutes for (&mut OpenapiRouter<'a, $implType<'b, C, P>>, &str) + impl<'a, 'b, C, P> DrawResourceRoutesWithSchema for (&mut OpenapiRouter<'a, $implType<'b, C, P>>, &str) where C: PipelineHandleChain

+ Copy + Send + Sync + 'static, P: RefUnwindSafe + Send + Sync + 'static { - fn read_all(&mut self) { - let schema = (self.0).openapi_builder.add_schema::(); + fn endpoint(&mut self) { + let schema = (self.0).openapi_builder.add_schema::(); + let mut descr = OperationDescription::new::(schema); + if E::has_placeholders() { + descr.set_path_params(E::Placeholders::schema()); + } + if E::needs_params() { + descr.set_query_params(E::Params::schema()); + } + if E::needs_body() { + let body_schema = (self.0).openapi_builder.add_schema::(); + descr.set_body::(body_schema); + } - let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1); + static URI_PLACEHOLDER_REGEX: Lazy = + Lazy::new(|| Regex::new(r#"(^|/):(?P[^/]+)(/|$)"#).unwrap()); + let uri: &str = &E::uri(); + let uri = + URI_PLACEHOLDER_REGEX.replace_all(uri, |captures: &Captures<'_>| format!("{{{}}}", &captures["name"])); + let path = if uri.is_empty() { + format!("{}/{}", self.0.scope.unwrap_or_default(), self.1) + } else { + format!("{}/{}/{}", self.0.scope.unwrap_or_default(), self.1, uri) + }; + + let op = descr.into_operation(); let mut item = (self.0).openapi_builder.remove_path(&path); - item.get = Some(OperationDescription::new::(schema).into_operation()); + match E::http_method() { + Method::GET => item.get = Some(op), + Method::PUT => item.put = Some(op), + Method::POST => item.post = Some(op), + Method::DELETE => item.delete = Some(op), + Method::OPTIONS => item.options = Some(op), + Method::HEAD => item.head = Some(op), + Method::PATCH => item.patch = Some(op), + Method::TRACE => item.trace = Some(op), + method => warn!("Ignoring unsupported method '{}' in OpenAPI Specification", method) + }; (self.0).openapi_builder.add_path(path, item); - (&mut *(self.0).router, self.1).read_all::() - } - - fn read(&mut self) { - let schema = (self.0).openapi_builder.add_schema::(); - let id_schema = (self.0).openapi_builder.add_schema::(); - - let path = format!("{}/{}/{{id}}", self.0.scope.unwrap_or_default(), self.1); - let mut item = (self.0).openapi_builder.remove_path(&path); - item.get = Some( - OperationDescription::new::(schema) - .add_path_param("id", id_schema) - .into_operation() - ); - (self.0).openapi_builder.add_path(path, item); - - (&mut *(self.0).router, self.1).read::() - } - - fn search(&mut self) { - let schema = (self.0).openapi_builder.add_schema::(); - - let path = format!("{}/{}/search", self.0.scope.unwrap_or_default(), self.1); - let mut item = (self.0).openapi_builder.remove_path(&path); - item.get = Some( - OperationDescription::new::(schema) - .with_query_params(Handler::Query::schema()) - .into_operation() - ); - (self.0).openapi_builder.add_path(path, item); - - (&mut *(self.0).router, self.1).search::() - } - - fn create(&mut self) - where - Handler::Res: 'static, - Handler::Body: 'static - { - let schema = (self.0).openapi_builder.add_schema::(); - let body_schema = (self.0).openapi_builder.add_schema::(); - - let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1); - let mut item = (self.0).openapi_builder.remove_path(&path); - item.post = Some( - OperationDescription::new::(schema) - .with_body::(body_schema) - .into_operation() - ); - (self.0).openapi_builder.add_path(path, item); - - (&mut *(self.0).router, self.1).create::() - } - - fn change_all(&mut self) - where - Handler::Res: 'static, - Handler::Body: 'static - { - let schema = (self.0).openapi_builder.add_schema::(); - let body_schema = (self.0).openapi_builder.add_schema::(); - - let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1); - let mut item = (self.0).openapi_builder.remove_path(&path); - item.put = Some( - OperationDescription::new::(schema) - .with_body::(body_schema) - .into_operation() - ); - (self.0).openapi_builder.add_path(path, item); - - (&mut *(self.0).router, self.1).change_all::() - } - - fn change(&mut self) - where - Handler::Res: 'static, - Handler::Body: 'static - { - let schema = (self.0).openapi_builder.add_schema::(); - let id_schema = (self.0).openapi_builder.add_schema::(); - let body_schema = (self.0).openapi_builder.add_schema::(); - - let path = format!("{}/{}/{{id}}", self.0.scope.unwrap_or_default(), self.1); - let mut item = (self.0).openapi_builder.remove_path(&path); - item.put = Some( - OperationDescription::new::(schema) - .add_path_param("id", id_schema) - .with_body::(body_schema) - .into_operation() - ); - (self.0).openapi_builder.add_path(path, item); - - (&mut *(self.0).router, self.1).change::() - } - - fn remove_all(&mut self) { - let schema = (self.0).openapi_builder.add_schema::(); - - let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1); - let mut item = (self.0).openapi_builder.remove_path(&path); - item.delete = Some(OperationDescription::new::(schema).into_operation()); - (self.0).openapi_builder.add_path(path, item); - - (&mut *(self.0).router, self.1).remove_all::() - } - - fn remove(&mut self) { - let schema = (self.0).openapi_builder.add_schema::(); - let id_schema = (self.0).openapi_builder.add_schema::(); - - let path = format!("{}/{}/{{id}}", self.0.scope.unwrap_or_default(), self.1); - let mut item = (self.0).openapi_builder.remove_path(&path); - item.delete = Some( - OperationDescription::new::(schema) - .add_path_param("id", id_schema) - .into_operation() - ); - (self.0).openapi_builder.add_path(path, item); - - (&mut *(self.0).router, self.1).remove::() + (&mut *(self.0).router, self.1).endpoint::() } } }; diff --git a/src/openapi/types.rs b/src/openapi/types.rs index 416e8c0..e15a6b5 100644 --- a/src/openapi/types.rs +++ b/src/openapi/types.rs @@ -1,5 +1,6 @@ #[cfg(feature = "chrono")] use chrono::{Date, DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, Utc}; +use gotham::extractor::{NoopPathExtractor, NoopQueryStringExtractor}; use indexmap::IndexMap; use openapiv3::{ AdditionalProperties, ArrayType, IntegerType, NumberFormat, NumberType, ObjectType, @@ -86,6 +87,20 @@ impl OpenapiType for () { } } +impl OpenapiType for NoopPathExtractor { + fn schema() -> OpenapiSchema { + warn!("You're asking for the OpenAPI Schema for gotham::extractor::NoopPathExtractor. This is probably not what you want."); + <()>::schema() + } +} + +impl OpenapiType for NoopQueryStringExtractor { + fn schema() -> OpenapiSchema { + warn!("You're asking for the OpenAPI Schema for gotham::extractor::NoopQueryStringExtractor. This is probably not what you want."); + <()>::schema() + } +} + impl OpenapiType for bool { fn schema() -> OpenapiSchema { OpenapiSchema::new(SchemaKind::Type(Type::Boolean {})) diff --git a/src/resource.rs b/src/resource.rs deleted file mode 100644 index 8c6bec5..0000000 --- a/src/resource.rs +++ /dev/null @@ -1,99 +0,0 @@ -use crate::{DrawResourceRoutes, RequestBody, ResourceID, ResourceResult, ResourceType}; -use gotham::{extractor::QueryStringExtractor, 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 -/// 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)]`. -pub trait Resource { - /// Register all methods handled by this resource with the underlying router. - fn setup(route: D); -} - -/// A common trait for every resource method. It defines the return type as well as some general -/// information about a resource method. -/// -/// It is not recommended to implement this yourself. Rather, just write your handler method and -/// annotate it with `#[(YourResource)]`, where `` is one of the supported -/// resource methods. -pub trait ResourceMethod { - type Res: ResourceResult + Send + 'static; - - #[cfg(feature = "openapi")] - fn operation_id() -> Option { - None - } - - fn wants_auth() -> bool { - false - } -} - -/// The read_all [ResourceMethod]. -pub trait ResourceReadAll: ResourceMethod { - /// Handle a GET request on the Resource root. - fn read_all(state: State) -> Pin + Send>>; -} - -/// The read [ResourceMethod]. -pub trait ResourceRead: ResourceMethod { - /// The ID type to be parsed from the request path. - type ID: ResourceID + 'static; - - /// Handle a GET request on the Resource with an id. - fn read(state: State, id: Self::ID) -> Pin + Send>>; -} - -/// The search [ResourceMethod]. -pub trait ResourceSearch: ResourceMethod { - /// The Query type to be parsed from the request parameters. - type Query: ResourceType + QueryStringExtractor + Sync; - - /// Handle a GET request on the Resource with additional search parameters. - fn search(state: State, query: Self::Query) -> Pin + Send>>; -} - -/// The create [ResourceMethod]. -pub trait ResourceCreate: ResourceMethod { - /// The Body type to be parsed from the request body. - type Body: RequestBody; - - /// Handle a POST request on the Resource root. - fn create(state: State, body: Self::Body) -> Pin + Send>>; -} - -/// The change_all [ResourceMethod]. -pub trait ResourceChangeAll: ResourceMethod { - /// The Body type to be parsed from the request body. - type Body: RequestBody; - - /// Handle a PUT request on the Resource root. - fn change_all(state: State, body: Self::Body) -> Pin + Send>>; -} - -/// The change [ResourceMethod]. -pub trait ResourceChange: ResourceMethod { - /// The Body type to be parsed from the request body. - type Body: RequestBody; - /// The ID type to be parsed from the request path. - type ID: ResourceID + 'static; - - /// Handle a PUT request on the Resource with an id. - fn change(state: State, id: Self::ID, body: Self::Body) -> Pin + Send>>; -} - -/// The remove_all [ResourceMethod]. -pub trait ResourceRemoveAll: ResourceMethod { - /// Handle a DELETE request on the Resource root. - fn remove_all(state: State) -> Pin + Send>>; -} - -/// The remove [ResourceMethod]. -pub trait ResourceRemove: ResourceMethod { - /// The ID type to be parsed from the request path. - type ID: ResourceID + 'static; - - /// Handle a DELETE request on the Resource with an id. - fn remove(state: State, id: Self::ID) -> Pin + Send>>; -} diff --git a/src/result/auth_result.rs b/src/result/auth_result.rs index cd56a84..677053e 100644 --- a/src/result/auth_result.rs +++ b/src/result/auth_result.rs @@ -33,7 +33,7 @@ Use can look something like this (assuming the `auth` feature is enabled): # #[derive(Clone, Deserialize)] # struct MyAuthData { exp : u64 } # -#[read_all(MyResource)] +#[read_all] fn read_all(auth : AuthStatus) -> AuthSuccess { let auth_data = match auth { AuthStatus::Authenticated(data) => data, @@ -102,7 +102,7 @@ Use can look something like this (assuming the `auth` feature is enabled): # #[derive(Clone, Deserialize)] # struct MyAuthData { exp : u64 } # -#[read_all(MyResource)] +#[read_all] fn read_all(auth : AuthStatus) -> AuthResult { let auth_data = match auth { AuthStatus::Authenticated(data) => data, diff --git a/src/result/no_content.rs b/src/result/no_content.rs index 0c4fe05..88d624e 100644 --- a/src/result/no_content.rs +++ b/src/result/no_content.rs @@ -21,8 +21,8 @@ the function attributes: # #[resource(read_all)] # struct MyResource; # -#[read_all(MyResource)] -fn read_all(_state: &mut State) { +#[read_all] +fn read_all() { // do something } # } diff --git a/src/result/raw.rs b/src/result/raw.rs index 6b3b520..de0f82e 100644 --- a/src/result/raw.rs +++ b/src/result/raw.rs @@ -25,7 +25,7 @@ example that simply returns its body: #[resource(create)] struct ImageResource; -#[create(ImageResource)] +#[create] fn create(body : Raw>) -> Raw> { body } diff --git a/src/result/success.rs b/src/result/success.rs index a3816ea..bd899bf 100644 --- a/src/result/success.rs +++ b/src/result/success.rs @@ -34,8 +34,8 @@ struct MyResponse { message: &'static str } -#[read_all(MyResource)] -fn read_all(_state: &mut State) -> Success { +#[read_all] +fn read_all() -> Success { let res = MyResponse { message: "I'm always happy" }; res.into() } diff --git a/src/routing.rs b/src/routing.rs index 70d2369..afbd857 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -3,37 +3,33 @@ use crate::openapi::{ builder::{OpenapiBuilder, OpenapiInfo}, router::OpenapiRouter }; -#[cfg(feature = "cors")] -use crate::CorsRoute; use crate::{ - resource::{ - Resource, ResourceChange, ResourceChangeAll, ResourceCreate, ResourceRead, ResourceReadAll, ResourceRemove, - ResourceRemoveAll, ResourceSearch - }, result::{ResourceError, ResourceResult}, - RequestBody, Response, StatusCode + Endpoint, FromBody, Resource, Response, StatusCode }; -use futures_util::{future, future::FutureExt}; use gotham::{ - handler::{HandlerError, HandlerFuture}, + handler::HandlerError, helpers::http::response::{create_empty_response, create_response}, hyper::{body::to_bytes, header::CONTENT_TYPE, Body, HeaderMap, Method}, pipeline::chain::PipelineHandleChain, router::{ - builder::{DefineSingleRoute, DrawRoutes, ExtendRouteMatcher, RouterBuilder, ScopeBuilder}, + builder::{DefineSingleRoute, DrawRoutes, RouterBuilder, ScopeBuilder}, non_match::RouteNonMatch, - route::matcher::{AcceptHeaderRouteMatcher, ContentTypeHeaderRouteMatcher, RouteMatcher} + route::matcher::{ + AcceptHeaderRouteMatcher, AccessControlRequestMethodMatcher, ContentTypeHeaderRouteMatcher, RouteMatcher + } }, state::{FromState, State} }; use mime::{Mime, APPLICATION_JSON}; -use std::{future::Future, panic::RefUnwindSafe, pin::Pin}; +use std::panic::RefUnwindSafe; /// Allow us to extract an id from a path. -#[derive(Deserialize, StateData, StaticResponseExtender)] -struct PathExtractor { - id: ID +#[derive(Debug, Deserialize, StateData, StaticResponseExtender)] +#[cfg_attr(feature = "openapi", derive(OpenapiType))] +pub struct PathExtractor { + pub id: ID } /// This trait adds the `with_openapi` method to gotham's routing. It turns the default @@ -48,37 +44,20 @@ pub trait WithOpenapi { /// This trait adds the `resource` method to gotham's routing. It allows you to register /// any RESTful [Resource] with a path. +#[_private_openapi_trait(DrawResourcesWithSchema)] pub trait DrawResources { - fn resource(&mut self, path: &str); + #[openapi_bound("R: crate::ResourceWithSchema")] + #[non_openapi_bound("R: crate::Resource")] + fn resource(&mut self, path: &str); } /// This trait allows to draw routes within an resource. Use this only inside the /// [Resource::setup] method. +#[_private_openapi_trait(DrawResourceRoutesWithSchema)] pub trait DrawResourceRoutes { - fn read_all(&mut self); - - fn read(&mut self); - - fn search(&mut self); - - fn create(&mut self) - where - Handler::Res: 'static, - Handler::Body: 'static; - - fn change_all(&mut self) - where - Handler::Res: 'static, - Handler::Body: 'static; - - fn change(&mut self) - where - Handler::Res: 'static, - Handler::Body: 'static; - - fn remove_all(&mut self); - - fn remove(&mut self); + #[openapi_bound("E: crate::EndpointWithSchema")] + #[non_openapi_bound("E: crate::Endpoint")] + fn endpoint(&mut self); } fn response_from(res: Response, state: &State) -> gotham::hyper::Response { @@ -108,149 +87,42 @@ fn response_from(res: Response, state: &State) -> gotham::hyper::Response r } -async fn to_handler_future( - state: State, - get_result: F -) -> Result<(State, gotham::hyper::Response), (State, HandlerError)> -where - F: FnOnce(State) -> Pin + Send>>, - R: ResourceResult -{ - let (state, res) = get_result(state).await; - let res = res.into_response().await; - match res { - Ok(res) => { - let r = response_from(res, &state); - Ok((state, r)) - }, - Err(e) => Err((state, e.into())) - } -} +async fn endpoint_handler(state: &mut State) -> Result, HandlerError> { + trace!("entering endpoint_handler"); + let placeholders = E::Placeholders::take_from(state); + let params = E::Params::take_from(state); -async fn body_to_res( - mut state: State, - get_result: F -) -> (State, Result, HandlerError>) -where - B: RequestBody, - F: FnOnce(State, B) -> Pin + Send>>, - R: ResourceResult -{ - let body = to_bytes(Body::take_from(&mut state)).await; + let body = match E::needs_body() { + true => { + let body = to_bytes(Body::take_from(state)).await?; - let body = match body { - Ok(body) => body, - Err(e) => return (state, Err(e.into())) - }; + let content_type: Mime = match HeaderMap::borrow_from(state).get(CONTENT_TYPE) { + Some(content_type) => content_type.to_str().unwrap().parse().unwrap(), + None => { + debug!("Missing Content-Type: Returning 415 Response"); + let res = create_empty_response(state, StatusCode::UNSUPPORTED_MEDIA_TYPE); + return Ok(res); + } + }; - let content_type: Mime = match HeaderMap::borrow_from(&state).get(CONTENT_TYPE) { - Some(content_type) => content_type.to_str().unwrap().parse().unwrap(), - None => { - let res = create_empty_response(&state, StatusCode::UNSUPPORTED_MEDIA_TYPE); - return (state, Ok(res)); - } - }; - - let res = { - let body = match B::from_body(body, content_type) { - Ok(body) => body, - Err(e) => { - let error: ResourceError = e.into(); - let res = match serde_json::to_string(&error) { - Ok(json) => { - let res = create_response(&state, StatusCode::BAD_REQUEST, APPLICATION_JSON, json); - Ok(res) - }, - Err(e) => Err(e.into()) - }; - return (state, res); + match E::Body::from_body(body, content_type) { + Ok(body) => Some(body), + Err(e) => { + debug!("Invalid Body: Returning 400 Response"); + let error: ResourceError = e.into(); + let json = serde_json::to_string(&error)?; + let res = create_response(state, StatusCode::BAD_REQUEST, APPLICATION_JSON, json); + return Ok(res); + } } - }; - get_result(state, body) - }; - - let (state, res) = res.await; - let res = res.into_response().await; - - let res = match res { - Ok(res) => { - let r = response_from(res, &state); - Ok(r) }, - Err(e) => Err(e.into()) + false => None }; - (state, res) -} -fn handle_with_body(state: State, get_result: F) -> Pin> -where - B: RequestBody + 'static, - F: FnOnce(State, B) -> Pin + Send>> + Send + 'static, - R: ResourceResult + Send + 'static -{ - body_to_res(state, get_result) - .then(|(state, res)| match res { - Ok(ok) => future::ok((state, ok)), - Err(err) => future::err((state, err)) - }) - .boxed() -} - -fn read_all_handler(state: State) -> Pin> { - to_handler_future(state, |state| Handler::read_all(state)).boxed() -} - -fn read_handler(state: State) -> Pin> { - let id = { - let path: &PathExtractor = PathExtractor::borrow_from(&state); - path.id.clone() - }; - to_handler_future(state, |state| Handler::read(state, id)).boxed() -} - -fn search_handler(mut state: State) -> Pin> { - let query = Handler::Query::take_from(&mut state); - to_handler_future(state, |state| Handler::search(state, query)).boxed() -} - -fn create_handler(state: State) -> Pin> -where - Handler::Res: 'static, - Handler::Body: 'static -{ - handle_with_body::(state, |state, body| Handler::create(state, body)) -} - -fn change_all_handler(state: State) -> Pin> -where - Handler::Res: 'static, - Handler::Body: 'static -{ - handle_with_body::(state, |state, body| Handler::change_all(state, body)) -} - -fn change_handler(state: State) -> Pin> -where - Handler::Res: 'static, - Handler::Body: 'static -{ - let id = { - let path: &PathExtractor = PathExtractor::borrow_from(&state); - path.id.clone() - }; - handle_with_body::(state, |state, body| Handler::change(state, id, body)) -} - -fn remove_all_handler(state: State) -> Pin> { - to_handler_future(state, |state| Handler::remove_all(state)).boxed() -} - -fn remove_handler(state: State) -> Pin> { - let id = { - let path: &PathExtractor = PathExtractor::borrow_from(&state); - path.id.clone() - }; - to_handler_future(state, |state| Handler::remove(state, id)).boxed() + let out = E::handle(state, placeholders, params, body).await; + let res = out.into_response().await?; + debug!("Returning response {:?}", res); + Ok(response_from(res, state)) } #[derive(Clone)] @@ -267,8 +139,8 @@ impl RouteMatcher for MaybeMatchAcceptHeader { } } -impl From>> for MaybeMatchAcceptHeader { - fn from(types: Option>) -> Self { +impl MaybeMatchAcceptHeader { + fn new(types: Option>) -> Self { let types = match types { Some(types) if types.is_empty() => None, types => types @@ -279,6 +151,12 @@ impl From>> for MaybeMatchAcceptHeader { } } +impl From>> for MaybeMatchAcceptHeader { + fn from(types: Option>) -> Self { + Self::new(types) + } +} + #[derive(Clone)] struct MaybeMatchContentTypeHeader { matcher: Option @@ -293,14 +171,20 @@ impl RouteMatcher for MaybeMatchContentTypeHeader { } } -impl From>> for MaybeMatchContentTypeHeader { - fn from(types: Option>) -> Self { +impl MaybeMatchContentTypeHeader { + fn new(types: Option>) -> Self { Self { matcher: types.map(|types| ContentTypeHeaderRouteMatcher::new(types).allow_no_type()) } } } +impl From>> for MaybeMatchContentTypeHeader { + fn from(types: Option>) -> Self { + Self::new(types) + } +} + macro_rules! implDrawResourceRoutes { ($implType:ident) => { #[cfg(feature = "openapi")] @@ -332,108 +216,30 @@ macro_rules! implDrawResourceRoutes { } } - #[allow(clippy::redundant_closure)] // doesn't work because of type parameters impl<'a, C, P> DrawResourceRoutes for (&mut $implType<'a, C, P>, &str) where C: PipelineHandleChain

+ Copy + Send + Sync + 'static, P: RefUnwindSafe + Send + Sync + 'static { - fn read_all(&mut self) { - let matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); - self.0 - .get(&self.1) - .extend_route_matcher(matcher) - .to(|state| read_all_handler::(state)); - } + fn endpoint(&mut self) { + let uri = format!("{}/{}", self.1, E::uri()); + debug!("Registering endpoint for {}", uri); + self.0.associate(&uri, |assoc| { + assoc + .request(vec![E::http_method()]) + .add_route_matcher(MaybeMatchAcceptHeader::new(E::Output::accepted_types())) + .with_path_extractor::() + .with_query_string_extractor::() + .to_async_borrowing(endpoint_handler::); - fn read(&mut self) { - let matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); - self.0 - .get(&format!("{}/:id", self.1)) - .extend_route_matcher(matcher) - .with_path_extractor::>() - .to(|state| read_handler::(state)); - } - - fn search(&mut self) { - let matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); - self.0 - .get(&format!("{}/search", self.1)) - .extend_route_matcher(matcher) - .with_query_string_extractor::() - .to(|state| search_handler::(state)); - } - - fn create(&mut self) - where - Handler::Res: Send + 'static, - Handler::Body: 'static - { - let accept_matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); - let content_matcher: MaybeMatchContentTypeHeader = Handler::Body::supported_types().into(); - self.0 - .post(&self.1) - .extend_route_matcher(accept_matcher) - .extend_route_matcher(content_matcher) - .to(|state| create_handler::(state)); - #[cfg(feature = "cors")] - self.0.cors(&self.1, Method::POST); - } - - fn change_all(&mut self) - where - Handler::Res: Send + 'static, - Handler::Body: 'static - { - let accept_matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); - let content_matcher: MaybeMatchContentTypeHeader = Handler::Body::supported_types().into(); - self.0 - .put(&self.1) - .extend_route_matcher(accept_matcher) - .extend_route_matcher(content_matcher) - .to(|state| change_all_handler::(state)); - #[cfg(feature = "cors")] - self.0.cors(&self.1, Method::PUT); - } - - fn change(&mut self) - where - Handler::Res: Send + 'static, - Handler::Body: 'static - { - let accept_matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); - let content_matcher: MaybeMatchContentTypeHeader = Handler::Body::supported_types().into(); - let path = format!("{}/:id", self.1); - self.0 - .put(&path) - .extend_route_matcher(accept_matcher) - .extend_route_matcher(content_matcher) - .with_path_extractor::>() - .to(|state| change_handler::(state)); - #[cfg(feature = "cors")] - self.0.cors(&path, Method::PUT); - } - - fn remove_all(&mut self) { - let matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); - self.0 - .delete(&self.1) - .extend_route_matcher(matcher) - .to(|state| remove_all_handler::(state)); - #[cfg(feature = "cors")] - self.0.cors(&self.1, Method::DELETE); - } - - fn remove(&mut self) { - let matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); - let path = format!("{}/:id", self.1); - self.0 - .delete(&path) - .extend_route_matcher(matcher) - .with_path_extractor::>() - .to(|state| remove_handler::(state)); - #[cfg(feature = "cors")] - self.0.cors(&path, Method::POST); + #[cfg(feature = "cors")] + if E::http_method() != Method::GET { + assoc + .options() + .add_route_matcher(AccessControlRequestMethodMatcher::new(E::http_method())) + .to(crate::cors::cors_preflight_handler); + } + }); } } }; diff --git a/src/types.rs b/src/types.rs index 5d7f255..ca08bec 100644 --- a/src/types.rs +++ b/src/types.rs @@ -4,7 +4,7 @@ use crate::OpenapiType; use gotham::hyper::body::Bytes; use mime::{Mime, APPLICATION_JSON}; use serde::{de::DeserializeOwned, Serialize}; -use std::{error::Error, panic::RefUnwindSafe}; +use std::error::Error; #[cfg(not(feature = "openapi"))] pub trait ResourceType {} @@ -98,12 +98,3 @@ impl RequestBody for T { Some(vec![APPLICATION_JSON]) } } - -/// 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 -/// type [OpenapiType]. -/// -/// [OpenapiType]: trait.OpenapiType.html -pub trait ResourceID: ResourceType + DeserializeOwned + Clone + RefUnwindSafe + Send + Sync {} - -impl ResourceID for T {} diff --git a/tests/async_methods.rs b/tests/async_methods.rs index 9de8465..be6a406 100644 --- a/tests/async_methods.rs +++ b/tests/async_methods.rs @@ -30,55 +30,57 @@ struct FooSearch { } const READ_ALL_RESPONSE: &[u8] = b"1ARwwSPVyOKpJKrYwqGgECPVWDl1BqajAAj7g7WJ3e"; -#[read_all(FooResource)] +#[read_all] async fn read_all() -> Raw<&'static [u8]> { Raw::new(READ_ALL_RESPONSE, TEXT_PLAIN) } const READ_RESPONSE: &[u8] = b"FEReHoeBKU17X2bBpVAd1iUvktFL43CDu0cFYHdaP9"; -#[read(FooResource)] +#[read] async fn read(_id: u64) -> Raw<&'static [u8]> { Raw::new(READ_RESPONSE, TEXT_PLAIN) } const SEARCH_RESPONSE: &[u8] = b"AWqcQUdBRHXKh3at4u79mdupOAfEbnTcx71ogCVF0E"; -#[search(FooResource)] +#[search] async fn search(_body: FooSearch) -> Raw<&'static [u8]> { Raw::new(SEARCH_RESPONSE, TEXT_PLAIN) } const CREATE_RESPONSE: &[u8] = b"y6POY7wOMAB0jBRBw0FJT7DOpUNbhmT8KdpQPLkI83"; -#[create(FooResource)] +#[create] async fn create(_body: FooBody) -> Raw<&'static [u8]> { Raw::new(CREATE_RESPONSE, TEXT_PLAIN) } const CHANGE_ALL_RESPONSE: &[u8] = b"QlbYg8gHE9OQvvk3yKjXJLTSXlIrg9mcqhfMXJmQkv"; -#[change_all(FooResource)] +#[change_all] async fn change_all(_body: FooBody) -> Raw<&'static [u8]> { Raw::new(CHANGE_ALL_RESPONSE, TEXT_PLAIN) } const CHANGE_RESPONSE: &[u8] = b"qGod55RUXkT1lgPO8h0uVM6l368O2S0GrwENZFFuRu"; -#[change(FooResource)] +#[change] async fn change(_id: u64, _body: FooBody) -> Raw<&'static [u8]> { Raw::new(CHANGE_RESPONSE, TEXT_PLAIN) } const REMOVE_ALL_RESPONSE: &[u8] = b"Y36kZ749MRk2Nem4BedJABOZiZWPLOtiwLfJlGTwm5"; -#[remove_all(FooResource)] +#[remove_all] async fn remove_all() -> Raw<&'static [u8]> { Raw::new(REMOVE_ALL_RESPONSE, TEXT_PLAIN) } const REMOVE_RESPONSE: &[u8] = b"CwRzBrKErsVZ1N7yeNfjZuUn1MacvgBqk4uPOFfDDq"; -#[remove(FooResource)] +#[remove] async fn remove(_id: u64) -> Raw<&'static [u8]> { Raw::new(REMOVE_RESPONSE, TEXT_PLAIN) } #[test] fn async_methods() { + let _ = pretty_env_logger::try_init_timed(); + let server = TestServer::new(build_simple_router(|router| { router.resource::("foo"); })) diff --git a/tests/cors_handling.rs b/tests/cors_handling.rs index 226b3d2..b74e7b3 100644 --- a/tests/cors_handling.rs +++ b/tests/cors_handling.rs @@ -16,10 +16,10 @@ use mime::TEXT_PLAIN; #[resource(read_all, change_all)] struct FooResource; -#[read_all(FooResource)] +#[read_all] fn read_all() {} -#[change_all(FooResource)] +#[change_all] fn change_all(_body: Raw>) {} fn test_server(cfg: CorsConfig) -> TestServer { diff --git a/tests/custom_request_body.rs b/tests/custom_request_body.rs index 2a5baeb..66156b6 100644 --- a/tests/custom_request_body.rs +++ b/tests/custom_request_body.rs @@ -15,7 +15,7 @@ struct Foo { content_type: Mime } -#[create(FooResource)] +#[create] fn create(body: Foo) -> Raw> { Raw::new(body.content, body.content_type) } diff --git a/tests/openapi_specification.rs b/tests/openapi_specification.rs index c5411af..faa9fb1 100644 --- a/tests/openapi_specification.rs +++ b/tests/openapi_specification.rs @@ -22,23 +22,23 @@ use util::{test_get_response, test_openapi_response}; const IMAGE_RESPONSE : &[u8] = b"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUA/wA0XsCoAAAAAXRSTlN/gFy0ywAAAApJREFUeJxjYgAAAAYAAzY3fKgAAAAASUVORK5CYII="; #[derive(Resource)] -#[resource(read, change)] +#[resource(get_image, set_image)] struct ImageResource; #[derive(FromBody, RequestBody)] #[supported_types(IMAGE_PNG)] struct Image(Vec); -#[read(ImageResource, operation_id = "getImage")] +#[read(operation_id = "getImage")] fn get_image(_id: u64) -> Raw<&'static [u8]> { Raw::new(IMAGE_RESPONSE, "image/png;base64".parse().unwrap()) } -#[change(ImageResource, operation_id = "setImage")] +#[change(operation_id = "setImage")] fn set_image(_id: u64, _image: Image) {} #[derive(Resource)] -#[resource(read, search)] +#[resource(read_secret, search_secret)] struct SecretResource; #[derive(Deserialize, Clone)] @@ -67,13 +67,13 @@ struct SecretQuery { minute: Option } -#[read(SecretResource)] +#[read] fn read_secret(auth: AuthStatus, _id: NaiveDateTime) -> AuthSuccess { auth.ok()?; Ok(Secret { code: 4.2 }) } -#[search(SecretResource)] +#[search] fn search_secret(auth: AuthStatus, _query: SecretQuery) -> AuthSuccess { auth.ok()?; Ok(Secrets { @@ -82,7 +82,7 @@ fn search_secret(auth: AuthStatus, _query: SecretQuery) -> AuthSuccess } #[test] -fn openapi_supports_scope() { +fn openapi_specification() { let info = OpenapiInfo { title: "This is just a test".to_owned(), version: "1.2.3".to_owned(), diff --git a/tests/openapi_supports_scope.rs b/tests/openapi_supports_scope.rs index 8d8e298..33f240a 100644 --- a/tests/openapi_supports_scope.rs +++ b/tests/openapi_supports_scope.rs @@ -15,7 +15,7 @@ const RESPONSE: &[u8] = b"This is the only valid response."; #[resource(read_all)] struct FooResource; -#[read_all(FooResource)] +#[read_all] fn read_all() -> Raw<&'static [u8]> { Raw::new(RESPONSE, TEXT_PLAIN) } diff --git a/tests/sync_methods.rs b/tests/sync_methods.rs index 211f5f3..4e07259 100644 --- a/tests/sync_methods.rs +++ b/tests/sync_methods.rs @@ -30,55 +30,57 @@ struct FooSearch { } const READ_ALL_RESPONSE: &[u8] = b"1ARwwSPVyOKpJKrYwqGgECPVWDl1BqajAAj7g7WJ3e"; -#[read_all(FooResource)] +#[read_all] fn read_all() -> Raw<&'static [u8]> { Raw::new(READ_ALL_RESPONSE, TEXT_PLAIN) } const READ_RESPONSE: &[u8] = b"FEReHoeBKU17X2bBpVAd1iUvktFL43CDu0cFYHdaP9"; -#[read(FooResource)] +#[read] fn read(_id: u64) -> Raw<&'static [u8]> { Raw::new(READ_RESPONSE, TEXT_PLAIN) } const SEARCH_RESPONSE: &[u8] = b"AWqcQUdBRHXKh3at4u79mdupOAfEbnTcx71ogCVF0E"; -#[search(FooResource)] +#[search] fn search(_body: FooSearch) -> Raw<&'static [u8]> { Raw::new(SEARCH_RESPONSE, TEXT_PLAIN) } const CREATE_RESPONSE: &[u8] = b"y6POY7wOMAB0jBRBw0FJT7DOpUNbhmT8KdpQPLkI83"; -#[create(FooResource)] +#[create] fn create(_body: FooBody) -> Raw<&'static [u8]> { Raw::new(CREATE_RESPONSE, TEXT_PLAIN) } const CHANGE_ALL_RESPONSE: &[u8] = b"QlbYg8gHE9OQvvk3yKjXJLTSXlIrg9mcqhfMXJmQkv"; -#[change_all(FooResource)] +#[change_all] fn change_all(_body: FooBody) -> Raw<&'static [u8]> { Raw::new(CHANGE_ALL_RESPONSE, TEXT_PLAIN) } const CHANGE_RESPONSE: &[u8] = b"qGod55RUXkT1lgPO8h0uVM6l368O2S0GrwENZFFuRu"; -#[change(FooResource)] +#[change] fn change(_id: u64, _body: FooBody) -> Raw<&'static [u8]> { Raw::new(CHANGE_RESPONSE, TEXT_PLAIN) } const REMOVE_ALL_RESPONSE: &[u8] = b"Y36kZ749MRk2Nem4BedJABOZiZWPLOtiwLfJlGTwm5"; -#[remove_all(FooResource)] +#[remove_all] fn remove_all() -> Raw<&'static [u8]> { Raw::new(REMOVE_ALL_RESPONSE, TEXT_PLAIN) } const REMOVE_RESPONSE: &[u8] = b"CwRzBrKErsVZ1N7yeNfjZuUn1MacvgBqk4uPOFfDDq"; -#[remove(FooResource)] +#[remove] fn remove(_id: u64) -> Raw<&'static [u8]> { Raw::new(REMOVE_RESPONSE, TEXT_PLAIN) } #[test] fn sync_methods() { + let _ = pretty_env_logger::try_init_timed(); + let server = TestServer::new(build_simple_router(|router| { router.resource::("foo"); })) diff --git a/tests/trybuild_ui.rs b/tests/trybuild_ui.rs index 8f2d20b..2317215 100644 --- a/tests/trybuild_ui.rs +++ b/tests/trybuild_ui.rs @@ -6,23 +6,12 @@ fn trybuild_ui() { let t = TestCases::new(); // always enabled - t.compile_fail("tests/ui/from_body_enum.rs"); - t.compile_fail("tests/ui/method_async_state.rs"); - t.compile_fail("tests/ui/method_for_unknown_resource.rs"); - t.compile_fail("tests/ui/method_no_resource.rs"); - t.compile_fail("tests/ui/method_self.rs"); - t.compile_fail("tests/ui/method_too_few_args.rs"); - t.compile_fail("tests/ui/method_too_many_args.rs"); - t.compile_fail("tests/ui/method_unsafe.rs"); - t.compile_fail("tests/ui/resource_unknown_method.rs"); + t.compile_fail("tests/ui/endpoint/*.rs"); + t.compile_fail("tests/ui/from_body/*.rs"); + t.compile_fail("tests/ui/resource/*.rs"); // require the openapi feature if cfg!(feature = "openapi") { - 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_rename_non_string.rs"); - t.compile_fail("tests/ui/openapi_type_tuple_struct.rs"); - t.compile_fail("tests/ui/openapi_type_union.rs"); - t.compile_fail("tests/ui/openapi_type_unknown_key.rs"); + t.compile_fail("tests/ui/openapi_type/*.rs"); } } diff --git a/tests/ui/endpoint/async_state.rs b/tests/ui/endpoint/async_state.rs new file mode 100644 index 0000000..85a23a4 --- /dev/null +++ b/tests/ui/endpoint/async_state.rs @@ -0,0 +1,12 @@ +#[macro_use] +extern crate gotham_restful; +use gotham_restful::State; + +#[derive(Resource)] +#[resource(read_all)] +struct FooResource; + +#[read_all] +async fn read_all(state: &State) {} + +fn main() {} diff --git a/tests/ui/endpoint/async_state.stderr b/tests/ui/endpoint/async_state.stderr new file mode 100644 index 0000000..2ca92b4 --- /dev/null +++ b/tests/ui/endpoint/async_state.stderr @@ -0,0 +1,5 @@ +error: Endpoint handler functions that are async must not take `&State` as an argument, consider taking `&mut State` + --> $DIR/async_state.rs:10:19 + | +10 | async fn read_all(state: &State) {} + | ^^^^^ diff --git a/tests/ui/method_self.rs b/tests/ui/endpoint/invalid_attribute.rs similarity index 53% rename from tests/ui/method_self.rs rename to tests/ui/endpoint/invalid_attribute.rs index 3b19b11..3c321a5 100644 --- a/tests/ui/method_self.rs +++ b/tests/ui/endpoint/invalid_attribute.rs @@ -1,14 +1,11 @@ -#[macro_use] extern crate gotham_restful; +#[macro_use] +extern crate gotham_restful; #[derive(Resource)] #[resource(read_all)] struct FooResource; #[read_all(FooResource)] -fn read_all(self) -{ -} +fn read_all() {} -fn main() -{ -} +fn main() {} diff --git a/tests/ui/endpoint/invalid_attribute.stderr b/tests/ui/endpoint/invalid_attribute.stderr new file mode 100644 index 0000000..9c5c86f --- /dev/null +++ b/tests/ui/endpoint/invalid_attribute.stderr @@ -0,0 +1,11 @@ +error: Invalid attribute syntax + --> $DIR/invalid_attribute.rs:8:12 + | +8 | #[read_all(FooResource)] + | ^^^^^^^^^^^ + +error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope + --> $DIR/invalid_attribute.rs:5:12 + | +5 | #[resource(read_all)] + | ^^^^^^^^ not found in this scope diff --git a/tests/ui/method_no_resource.rs b/tests/ui/endpoint/self.rs similarity index 50% rename from tests/ui/method_no_resource.rs rename to tests/ui/endpoint/self.rs index f0232b7..17591d0 100644 --- a/tests/ui/method_no_resource.rs +++ b/tests/ui/endpoint/self.rs @@ -1,14 +1,11 @@ -#[macro_use] extern crate gotham_restful; +#[macro_use] +extern crate gotham_restful; #[derive(Resource)] #[resource(read_all)] struct FooResource; #[read_all] -fn read_all() -{ -} +fn read_all(self) {} -fn main() -{ -} +fn main() {} diff --git a/tests/ui/endpoint/self.stderr b/tests/ui/endpoint/self.stderr new file mode 100644 index 0000000..51fd32b --- /dev/null +++ b/tests/ui/endpoint/self.stderr @@ -0,0 +1,19 @@ +error: Didn't expect self parameter + --> $DIR/self.rs:9:13 + | +9 | fn read_all(self) {} + | ^^^^ + +error: `self` parameter is only allowed in associated functions + --> $DIR/self.rs:9:13 + | +9 | fn read_all(self) {} + | ^^^^ not semantically valid as function parameter + | + = note: associated functions are those in `impl` or `trait` definitions + +error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope + --> $DIR/self.rs:5:12 + | +5 | #[resource(read_all)] + | ^^^^^^^^ not found in this scope diff --git a/tests/ui/endpoint/too_few_args.rs b/tests/ui/endpoint/too_few_args.rs new file mode 100644 index 0000000..963689b --- /dev/null +++ b/tests/ui/endpoint/too_few_args.rs @@ -0,0 +1,11 @@ +#[macro_use] +extern crate gotham_restful; + +#[derive(Resource)] +#[resource(read)] +struct FooResource; + +#[read] +fn read() {} + +fn main() {} diff --git a/tests/ui/endpoint/too_few_args.stderr b/tests/ui/endpoint/too_few_args.stderr new file mode 100644 index 0000000..5a19643 --- /dev/null +++ b/tests/ui/endpoint/too_few_args.stderr @@ -0,0 +1,11 @@ +error: Too few arguments + --> $DIR/too_few_args.rs:9:4 + | +9 | fn read() {} + | ^^^^ + +error[E0412]: cannot find type `read___gotham_restful_endpoint` in this scope + --> $DIR/too_few_args.rs:5:12 + | +5 | #[resource(read)] + | ^^^^ not found in this scope diff --git a/tests/ui/endpoint/too_many_args.rs b/tests/ui/endpoint/too_many_args.rs new file mode 100644 index 0000000..f334c3e --- /dev/null +++ b/tests/ui/endpoint/too_many_args.rs @@ -0,0 +1,11 @@ +#[macro_use] +extern crate gotham_restful; + +#[derive(Resource)] +#[resource(read_all)] +struct FooResource; + +#[read_all] +fn read_all(_id: u64) {} + +fn main() {} diff --git a/tests/ui/endpoint/too_many_args.stderr b/tests/ui/endpoint/too_many_args.stderr new file mode 100644 index 0000000..e781a97 --- /dev/null +++ b/tests/ui/endpoint/too_many_args.stderr @@ -0,0 +1,11 @@ +error: Too many arguments + --> $DIR/too_many_args.rs:9:4 + | +9 | fn read_all(_id: u64) {} + | ^^^^^^^^ + +error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope + --> $DIR/too_many_args.rs:5:12 + | +5 | #[resource(read_all)] + | ^^^^^^^^ not found in this scope diff --git a/tests/ui/endpoint/unknown_attribute.rs b/tests/ui/endpoint/unknown_attribute.rs new file mode 100644 index 0000000..d0410ba --- /dev/null +++ b/tests/ui/endpoint/unknown_attribute.rs @@ -0,0 +1,11 @@ +#[macro_use] +extern crate gotham_restful; + +#[derive(Resource)] +#[resource(read_all)] +struct FooResource; + +#[read_all(pineapple = "on pizza")] +fn read_all() {} + +fn main() {} diff --git a/tests/ui/endpoint/unknown_attribute.stderr b/tests/ui/endpoint/unknown_attribute.stderr new file mode 100644 index 0000000..ac0bafe --- /dev/null +++ b/tests/ui/endpoint/unknown_attribute.stderr @@ -0,0 +1,11 @@ +error: Unknown attribute + --> $DIR/unknown_attribute.rs:8:12 + | +8 | #[read_all(pineapple = "on pizza")] + | ^^^^^^^^^ + +error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope + --> $DIR/unknown_attribute.rs:5:12 + | +5 | #[resource(read_all)] + | ^^^^^^^^ not found in this scope diff --git a/tests/ui/endpoint/unsafe.rs b/tests/ui/endpoint/unsafe.rs new file mode 100644 index 0000000..943b54a --- /dev/null +++ b/tests/ui/endpoint/unsafe.rs @@ -0,0 +1,11 @@ +#[macro_use] +extern crate gotham_restful; + +#[derive(Resource)] +#[resource(read_all)] +struct FooResource; + +#[read_all] +unsafe fn read_all() {} + +fn main() {} diff --git a/tests/ui/endpoint/unsafe.stderr b/tests/ui/endpoint/unsafe.stderr new file mode 100644 index 0000000..d7366e0 --- /dev/null +++ b/tests/ui/endpoint/unsafe.stderr @@ -0,0 +1,11 @@ +error: Endpoint handler methods must not be unsafe + --> $DIR/unsafe.rs:9:1 + | +9 | unsafe fn read_all() {} + | ^^^^^^ + +error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope + --> $DIR/unsafe.rs:5:12 + | +5 | #[resource(read_all)] + | ^^^^^^^^ not found in this scope diff --git a/tests/ui/from_body/enum.rs b/tests/ui/from_body/enum.rs new file mode 100644 index 0000000..6e10178 --- /dev/null +++ b/tests/ui/from_body/enum.rs @@ -0,0 +1,10 @@ +#[macro_use] +extern crate gotham_restful; + +#[derive(FromBody)] +enum FromBodyEnum { + SomeVariant(Vec), + OtherVariant(String) +} + +fn main() {} diff --git a/tests/ui/from_body_enum.stderr b/tests/ui/from_body/enum.stderr similarity index 53% rename from tests/ui/from_body_enum.stderr rename to tests/ui/from_body/enum.stderr index 26cab8b..f10c2c8 100644 --- a/tests/ui/from_body_enum.stderr +++ b/tests/ui/from_body/enum.stderr @@ -1,5 +1,5 @@ error: #[derive(FromBody)] only works for structs - --> $DIR/from_body_enum.rs:4:1 + --> $DIR/enum.rs:5:1 | -4 | enum FromBodyEnum +5 | enum FromBodyEnum { | ^^^^ diff --git a/tests/ui/from_body_enum.rs b/tests/ui/from_body_enum.rs deleted file mode 100644 index 24eb9db..0000000 --- a/tests/ui/from_body_enum.rs +++ /dev/null @@ -1,12 +0,0 @@ -#[macro_use] extern crate gotham_restful; - -#[derive(FromBody)] -enum FromBodyEnum -{ - SomeVariant(Vec), - OtherVariant(String) -} - -fn main() -{ -} diff --git a/tests/ui/method_async_state.rs b/tests/ui/method_async_state.rs deleted file mode 100644 index 66b9fc7..0000000 --- a/tests/ui/method_async_state.rs +++ /dev/null @@ -1,15 +0,0 @@ -#[macro_use] extern crate gotham_restful; -use gotham_restful::State; - -#[derive(Resource)] -#[resource(read_all)] -struct FooResource; - -#[read_all(FooResource)] -async fn read_all(state : &State) -{ -} - -fn main() -{ -} diff --git a/tests/ui/method_async_state.stderr b/tests/ui/method_async_state.stderr deleted file mode 100644 index 571c334..0000000 --- a/tests/ui/method_async_state.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: async fn must not take &State as an argument as State is not Sync, consider taking &mut State - --> $DIR/method_async_state.rs:9:19 - | -9 | async fn read_all(state : &State) - | ^^^^^ diff --git a/tests/ui/method_for_unknown_resource.rs b/tests/ui/method_for_unknown_resource.rs deleted file mode 100644 index 162dc94..0000000 --- a/tests/ui/method_for_unknown_resource.rs +++ /dev/null @@ -1,10 +0,0 @@ -#[macro_use] extern crate gotham_restful; - -#[read_all(UnknownResource)] -fn read_all() -{ -} - -fn main() -{ -} diff --git a/tests/ui/method_for_unknown_resource.stderr b/tests/ui/method_for_unknown_resource.stderr deleted file mode 100644 index 1e10d24..0000000 --- a/tests/ui/method_for_unknown_resource.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error[E0412]: cannot find type `UnknownResource` in this scope - --> $DIR/method_for_unknown_resource.rs:3:12 - | -3 | #[read_all(UnknownResource)] - | ^^^^^^^^^^^^^^^ not found in this scope diff --git a/tests/ui/method_no_resource.stderr b/tests/ui/method_no_resource.stderr deleted file mode 100644 index 1cddd9e..0000000 --- a/tests/ui/method_no_resource.stderr +++ /dev/null @@ -1,15 +0,0 @@ -error: Missing Resource struct. Example: #[read_all(MyResource)] - --> $DIR/method_no_resource.rs:7:1 - | -7 | #[read_all] - | ^^^^^^^^^^^ - | - = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info) - -error[E0425]: cannot find function `_gotham_restful_foo_resource_read_all_setup_impl` in this scope - --> $DIR/method_no_resource.rs:3:10 - | -3 | #[derive(Resource)] - | ^^^^^^^^ not found in this scope - | - = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/method_self.stderr b/tests/ui/method_self.stderr deleted file mode 100644 index 1ad62ca..0000000 --- a/tests/ui/method_self.stderr +++ /dev/null @@ -1,13 +0,0 @@ -error: Didn't expect self parameter - --> $DIR/method_self.rs:8:13 - | -8 | fn read_all(self) - | ^^^^ - -error: `self` parameter is only allowed in associated functions - --> $DIR/method_self.rs:8:13 - | -8 | fn read_all(self) - | ^^^^ not semantically valid as function parameter - | - = note: associated functions are those in `impl` or `trait` definitions diff --git a/tests/ui/method_too_few_args.rs b/tests/ui/method_too_few_args.rs deleted file mode 100644 index 6f0309e..0000000 --- a/tests/ui/method_too_few_args.rs +++ /dev/null @@ -1,14 +0,0 @@ -#[macro_use] extern crate gotham_restful; - -#[derive(Resource)] -#[resource(read)] -struct FooResource; - -#[read(FooResource)] -fn read() -{ -} - -fn main() -{ -} diff --git a/tests/ui/method_too_few_args.stderr b/tests/ui/method_too_few_args.stderr deleted file mode 100644 index 2036d86..0000000 --- a/tests/ui/method_too_few_args.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: Too few arguments - --> $DIR/method_too_few_args.rs:8:4 - | -8 | fn read() - | ^^^^ diff --git a/tests/ui/method_too_many_args.rs b/tests/ui/method_too_many_args.rs deleted file mode 100644 index 5ae83eb..0000000 --- a/tests/ui/method_too_many_args.rs +++ /dev/null @@ -1,14 +0,0 @@ -#[macro_use] extern crate gotham_restful; - -#[derive(Resource)] -#[resource(read_all)] -struct FooResource; - -#[read_all(FooResource)] -fn read_all(_id : u64) -{ -} - -fn main() -{ -} diff --git a/tests/ui/method_too_many_args.stderr b/tests/ui/method_too_many_args.stderr deleted file mode 100644 index 0c251e4..0000000 --- a/tests/ui/method_too_many_args.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: Too many arguments - --> $DIR/method_too_many_args.rs:8:13 - | -8 | fn read_all(_id : u64) - | ^^^ diff --git a/tests/ui/method_unsafe.rs b/tests/ui/method_unsafe.rs deleted file mode 100644 index 65a76bc..0000000 --- a/tests/ui/method_unsafe.rs +++ /dev/null @@ -1,14 +0,0 @@ -#[macro_use] extern crate gotham_restful; - -#[derive(Resource)] -#[resource(read_all)] -struct FooResource; - -#[read_all(FooResource)] -unsafe fn read_all() -{ -} - -fn main() -{ -} diff --git a/tests/ui/method_unsafe.stderr b/tests/ui/method_unsafe.stderr deleted file mode 100644 index 92308bf..0000000 --- a/tests/ui/method_unsafe.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: Resource methods must not be unsafe - --> $DIR/method_unsafe.rs:8:1 - | -8 | unsafe fn read_all() - | ^^^^^^ diff --git a/tests/ui/openapi_type/enum_with_fields.rs b/tests/ui/openapi_type/enum_with_fields.rs new file mode 100644 index 0000000..b07cbfa --- /dev/null +++ b/tests/ui/openapi_type/enum_with_fields.rs @@ -0,0 +1,12 @@ +#[macro_use] +extern crate gotham_restful; + +#[derive(OpenapiType)] +enum Food { + Pasta, + Pizza { pineapple: bool }, + Rice, + Other(String) +} + +fn main() {} diff --git a/tests/ui/openapi_type_enum_with_fields.stderr b/tests/ui/openapi_type/enum_with_fields.stderr similarity index 61% rename from tests/ui/openapi_type_enum_with_fields.stderr rename to tests/ui/openapi_type/enum_with_fields.stderr index 1620970..2925a32 100644 --- a/tests/ui/openapi_type_enum_with_fields.stderr +++ b/tests/ui/openapi_type/enum_with_fields.stderr @@ -1,11 +1,11 @@ error: #[derive(OpenapiType)] does not support enum variants with fields - --> $DIR/openapi_type_enum_with_fields.rs:7:2 + --> $DIR/enum_with_fields.rs:7:2 | -7 | Pizza { pineapple : bool }, +7 | Pizza { pineapple: bool }, | ^^^^^ error: #[derive(OpenapiType)] does not support enum variants with fields - --> $DIR/openapi_type_enum_with_fields.rs:9:2 + --> $DIR/enum_with_fields.rs:9:2 | 9 | Other(String) | ^^^^^ diff --git a/tests/ui/openapi_type/nullable_non_bool.rs b/tests/ui/openapi_type/nullable_non_bool.rs new file mode 100644 index 0000000..2431e94 --- /dev/null +++ b/tests/ui/openapi_type/nullable_non_bool.rs @@ -0,0 +1,10 @@ +#[macro_use] +extern crate gotham_restful; + +#[derive(OpenapiType)] +struct Foo { + #[openapi(nullable = "yes, please")] + bar: String +} + +fn main() {} diff --git a/tests/ui/openapi_type_nullable_non_bool.stderr b/tests/ui/openapi_type/nullable_non_bool.stderr similarity index 69% rename from tests/ui/openapi_type_nullable_non_bool.stderr rename to tests/ui/openapi_type/nullable_non_bool.stderr index 0ce2be3..421d9cd 100644 --- a/tests/ui/openapi_type_nullable_non_bool.stderr +++ b/tests/ui/openapi_type/nullable_non_bool.stderr @@ -1,5 +1,5 @@ error: Expected bool - --> $DIR/openapi_type_nullable_non_bool.rs:6:23 + --> $DIR/nullable_non_bool.rs:6:23 | 6 | #[openapi(nullable = "yes, please")] | ^^^^^^^^^^^^^ diff --git a/tests/ui/openapi_type/rename_non_string.rs b/tests/ui/openapi_type/rename_non_string.rs new file mode 100644 index 0000000..83f8bd6 --- /dev/null +++ b/tests/ui/openapi_type/rename_non_string.rs @@ -0,0 +1,10 @@ +#[macro_use] +extern crate gotham_restful; + +#[derive(OpenapiType)] +struct Foo { + #[openapi(rename = 42)] + bar: String +} + +fn main() {} diff --git a/tests/ui/openapi_type_rename_non_string.stderr b/tests/ui/openapi_type/rename_non_string.stderr similarity index 66% rename from tests/ui/openapi_type_rename_non_string.stderr rename to tests/ui/openapi_type/rename_non_string.stderr index 9bb9dd2..0446b21 100644 --- a/tests/ui/openapi_type_rename_non_string.stderr +++ b/tests/ui/openapi_type/rename_non_string.stderr @@ -1,5 +1,5 @@ error: Expected string literal - --> $DIR/openapi_type_rename_non_string.rs:6:21 + --> $DIR/rename_non_string.rs:6:21 | 6 | #[openapi(rename = 42)] | ^^ diff --git a/tests/ui/openapi_type/tuple_struct.rs b/tests/ui/openapi_type/tuple_struct.rs new file mode 100644 index 0000000..7def578 --- /dev/null +++ b/tests/ui/openapi_type/tuple_struct.rs @@ -0,0 +1,7 @@ +#[macro_use] +extern crate gotham_restful; + +#[derive(OpenapiType)] +struct Foo(String); + +fn main() {} diff --git a/tests/ui/openapi_type_tuple_struct.stderr b/tests/ui/openapi_type/tuple_struct.stderr similarity index 56% rename from tests/ui/openapi_type_tuple_struct.stderr rename to tests/ui/openapi_type/tuple_struct.stderr index 2028d18..62a81c1 100644 --- a/tests/ui/openapi_type_tuple_struct.stderr +++ b/tests/ui/openapi_type/tuple_struct.stderr @@ -1,5 +1,5 @@ error: #[derive(OpenapiType)] does not support unnamed fields - --> $DIR/openapi_type_tuple_struct.rs:4:11 + --> $DIR/tuple_struct.rs:5:11 | -4 | struct Foo(String); +5 | struct Foo(String); | ^^^^^^^^ diff --git a/tests/ui/openapi_type/union.rs b/tests/ui/openapi_type/union.rs new file mode 100644 index 0000000..99efd49 --- /dev/null +++ b/tests/ui/openapi_type/union.rs @@ -0,0 +1,10 @@ +#[macro_use] +extern crate gotham_restful; + +#[derive(OpenapiType)] +union IntOrPointer { + int: u64, + pointer: *mut String +} + +fn main() {} diff --git a/tests/ui/openapi_type_union.stderr b/tests/ui/openapi_type/union.stderr similarity index 56% rename from tests/ui/openapi_type_union.stderr rename to tests/ui/openapi_type/union.stderr index 52639fe..2dbe3b6 100644 --- a/tests/ui/openapi_type_union.stderr +++ b/tests/ui/openapi_type/union.stderr @@ -1,5 +1,5 @@ error: #[derive(OpenapiType)] only works for structs and enums - --> $DIR/openapi_type_union.rs:4:1 + --> $DIR/union.rs:5:1 | -4 | union IntOrPointer +5 | union IntOrPointer { | ^^^^^ diff --git a/tests/ui/openapi_type/unknown_key.rs b/tests/ui/openapi_type/unknown_key.rs new file mode 100644 index 0000000..daab52a --- /dev/null +++ b/tests/ui/openapi_type/unknown_key.rs @@ -0,0 +1,10 @@ +#[macro_use] +extern crate gotham_restful; + +#[derive(OpenapiType)] +struct Foo { + #[openapi(like = "pizza")] + bar: String +} + +fn main() {} diff --git a/tests/ui/openapi_type_unknown_key.stderr b/tests/ui/openapi_type/unknown_key.stderr similarity index 65% rename from tests/ui/openapi_type_unknown_key.stderr rename to tests/ui/openapi_type/unknown_key.stderr index f8e78b7..b5e9ac1 100644 --- a/tests/ui/openapi_type_unknown_key.stderr +++ b/tests/ui/openapi_type/unknown_key.stderr @@ -1,5 +1,5 @@ error: Unknown key - --> $DIR/openapi_type_unknown_key.rs:6:12 + --> $DIR/unknown_key.rs:6:12 | 6 | #[openapi(like = "pizza")] | ^^^^ diff --git a/tests/ui/openapi_type_enum_with_fields.rs b/tests/ui/openapi_type_enum_with_fields.rs deleted file mode 100644 index 6bc6814..0000000 --- a/tests/ui/openapi_type_enum_with_fields.rs +++ /dev/null @@ -1,14 +0,0 @@ -#[macro_use] extern crate gotham_restful; - -#[derive(OpenapiType)] -enum Food -{ - Pasta, - Pizza { pineapple : bool }, - Rice, - Other(String) -} - -fn main() -{ -} diff --git a/tests/ui/openapi_type_nullable_non_bool.rs b/tests/ui/openapi_type_nullable_non_bool.rs deleted file mode 100644 index 1e0af28..0000000 --- a/tests/ui/openapi_type_nullable_non_bool.rs +++ /dev/null @@ -1,12 +0,0 @@ -#[macro_use] extern crate gotham_restful; - -#[derive(OpenapiType)] -struct Foo -{ - #[openapi(nullable = "yes, please")] - bar : String -} - -fn main() -{ -} diff --git a/tests/ui/openapi_type_rename_non_string.rs b/tests/ui/openapi_type_rename_non_string.rs deleted file mode 100644 index 0847f14..0000000 --- a/tests/ui/openapi_type_rename_non_string.rs +++ /dev/null @@ -1,12 +0,0 @@ -#[macro_use] extern crate gotham_restful; - -#[derive(OpenapiType)] -struct Foo -{ - #[openapi(rename = 42)] - bar : String -} - -fn main() -{ -} diff --git a/tests/ui/openapi_type_tuple_struct.rs b/tests/ui/openapi_type_tuple_struct.rs deleted file mode 100644 index 0247478..0000000 --- a/tests/ui/openapi_type_tuple_struct.rs +++ /dev/null @@ -1,8 +0,0 @@ -#[macro_use] extern crate gotham_restful; - -#[derive(OpenapiType)] -struct Foo(String); - -fn main() -{ -} diff --git a/tests/ui/openapi_type_union.rs b/tests/ui/openapi_type_union.rs deleted file mode 100644 index 4bc7355..0000000 --- a/tests/ui/openapi_type_union.rs +++ /dev/null @@ -1,12 +0,0 @@ -#[macro_use] extern crate gotham_restful; - -#[derive(OpenapiType)] -union IntOrPointer -{ - int: u64, - pointer: *mut String -} - -fn main() -{ -} diff --git a/tests/ui/openapi_type_unknown_key.rs b/tests/ui/openapi_type_unknown_key.rs deleted file mode 100644 index 9157e16..0000000 --- a/tests/ui/openapi_type_unknown_key.rs +++ /dev/null @@ -1,12 +0,0 @@ -#[macro_use] extern crate gotham_restful; - -#[derive(OpenapiType)] -struct Foo -{ - #[openapi(like = "pizza")] - bar : String -} - -fn main() -{ -} diff --git a/tests/ui/resource/unknown_method.rs b/tests/ui/resource/unknown_method.rs new file mode 100644 index 0000000..18e8f7b --- /dev/null +++ b/tests/ui/resource/unknown_method.rs @@ -0,0 +1,11 @@ +#[macro_use] +extern crate gotham_restful; + +#[derive(Resource)] +#[resource(read_any)] +struct FooResource; + +#[read_all] +fn read_all() {} + +fn main() {} diff --git a/tests/ui/resource/unknown_method.stderr b/tests/ui/resource/unknown_method.stderr new file mode 100644 index 0000000..4a2a67e --- /dev/null +++ b/tests/ui/resource/unknown_method.stderr @@ -0,0 +1,8 @@ +error[E0412]: cannot find type `read_any___gotham_restful_endpoint` in this scope + --> $DIR/unknown_method.rs:5:12 + | +5 | #[resource(read_any)] + | ^^^^^^^^ help: a struct with a similar name exists: `read_all___gotham_restful_endpoint` +... +8 | #[read_all] + | ----------- similarly named struct `read_all___gotham_restful_endpoint` defined here diff --git a/tests/ui/resource_unknown_method.rs b/tests/ui/resource_unknown_method.rs deleted file mode 100644 index f246ed1..0000000 --- a/tests/ui/resource_unknown_method.rs +++ /dev/null @@ -1,14 +0,0 @@ -#[macro_use] extern crate gotham_restful; - -#[derive(Resource)] -#[resource(read_any)] -struct FooResource; - -#[read_all(FooResource)] -fn read_all() -{ -} - -fn main() -{ -} diff --git a/tests/ui/resource_unknown_method.stderr b/tests/ui/resource_unknown_method.stderr deleted file mode 100644 index 61269c2..0000000 --- a/tests/ui/resource_unknown_method.stderr +++ /dev/null @@ -1,14 +0,0 @@ -error: Unknown method: `read_any' - --> $DIR/resource_unknown_method.rs:4:12 - | -4 | #[resource(read_any)] - | ^^^^^^^^ - -error[E0277]: the trait bound `FooResource: Resource` is not satisfied - --> $DIR/resource_unknown_method.rs:7:1 - | -7 | #[read_all(FooResource)] - | ^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Resource` is not implemented for `FooResource` - | - = help: see issue #48214 - = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/rustfmt.sh b/tests/ui/rustfmt.sh new file mode 100755 index 0000000..c33dedf --- /dev/null +++ b/tests/ui/rustfmt.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -euo pipefail + +rustfmt=${RUSTFMT:-rustfmt} +version="$($rustfmt -V)" +if [[ $version != *nightly* ]]; then + rustfmt="$rustfmt +nightly" +fi + +return=0 +find "$(dirname "$0")" -name '*.rs' -type f | while read file; do + $rustfmt --config-path "$(dirname "$0")/../../rustfmt.toml" "$@" "$file" || return=1 +done + +exit $return diff --git a/tests/util/mod.rs b/tests/util/mod.rs index 8846352..40cae26 100644 --- a/tests/util/mod.rs +++ b/tests/util/mod.rs @@ -2,12 +2,14 @@ use gotham::{ hyper::Body, test::TestServer }; +use log::info; use mime::Mime; #[allow(unused_imports)] use std::{fs::File, io::{Read, Write}, str}; pub fn test_get_response(server : &TestServer, path : &str, expected : &[u8]) { + info!("GET {}", path); let res = server.client().get(path).perform().unwrap().read_body().unwrap(); let body : &[u8] = res.as_ref(); assert_eq!(body, expected); @@ -17,6 +19,7 @@ pub fn test_post_response(server : &TestServer, path : &str, body : B, mime : where B : Into { + info!("POST {}", path); let res = server.client().post(path, body, mime).perform().unwrap().read_body().unwrap(); let body : &[u8] = res.as_ref(); assert_eq!(body, expected); @@ -26,6 +29,7 @@ pub fn test_put_response(server : &TestServer, path : &str, body : B, mime : where B : Into { + info!("PUT {}", path); let res = server.client().put(path, body, mime).perform().unwrap().read_body().unwrap(); let body : &[u8] = res.as_ref(); assert_eq!(body, expected); @@ -33,6 +37,7 @@ where pub fn test_delete_response(server : &TestServer, path : &str, expected : &[u8]) { + info!("DELETE {}", path); let res = server.client().delete(path).perform().unwrap().read_body().unwrap(); let body : &[u8] = res.as_ref(); assert_eq!(body, expected); @@ -41,12 +46,14 @@ pub fn test_delete_response(server : &TestServer, path : &str, expected : &[u8]) #[cfg(feature = "openapi")] pub fn test_openapi_response(server : &TestServer, path : &str, output_file : &str) { + info!("GET {}", path); let res = server.client().get(path).perform().unwrap().read_body().unwrap(); let body = serde_json::to_string_pretty(&serde_json::from_slice::(res.as_ref()).unwrap()).unwrap(); match File::open(output_file) { Ok(mut file) => { let mut expected = String::new(); file.read_to_string(&mut expected).unwrap(); + eprintln!("{}", body); assert_eq!(body, expected); }, Err(_) => { From 002cfb1b4d6e2b53cc9be88ca72e7bb677a57ac9 Mon Sep 17 00:00:00 2001 From: Dominic Date: Mon, 18 Jan 2021 01:05:56 +0100 Subject: [PATCH 129/170] fix some lints --- derive/src/endpoint.rs | 2 ++ derive/src/lib.rs | 4 ++++ derive/src/private_openapi_trait.rs | 6 ++++++ derive/src/request_body.rs | 2 +- derive/src/resource.rs | 2 +- src/cors.rs | 1 + src/lib.rs | 10 +++------- 7 files changed, 18 insertions(+), 9 deletions(-) diff --git a/derive/src/endpoint.rs b/derive/src/endpoint.rs index a70145b..1c8d15d 100644 --- a/derive/src/endpoint.rs +++ b/derive/src/endpoint.rs @@ -254,6 +254,8 @@ pub fn endpoint_ident(fn_ident: &Ident) -> Ident { format_ident!("{}___gotham_restful_endpoint", fn_ident) } +// clippy doesn't realize that vectors can be used in closures +#[cfg_attr(feature = "cargo-clippy", allow(clippy::needless_collect))] fn expand_endpoint_type(ty: EndpointType, attrs: AttributeArgs, fun: &ItemFn) -> Result { // reject unsafe functions if let Some(unsafety) = fun.sig.unsafety { diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 15f13cd..f42e50f 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -1,3 +1,7 @@ +#![warn(missing_debug_implementations, rust_2018_idioms)] +#![deny(broken_intra_doc_links)] +#![forbid(unsafe_code)] + use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use quote::quote; diff --git a/derive/src/private_openapi_trait.rs b/derive/src/private_openapi_trait.rs index 1486119..d590c7e 100644 --- a/derive/src/private_openapi_trait.rs +++ b/derive/src/private_openapi_trait.rs @@ -86,6 +86,9 @@ pub(crate) fn expand_private_openapi_trait(mut attrs: AttributeArgs, tr8: ItemTr let attrs = TraitItemAttrs::parse(method.attrs)?; method.attrs = attrs.other_attrs; for bound in attrs.non_openapi_bound { + // we compare two incompatible types using their `Display` implementation + // this triggers a false positive in clippy + #[cfg_attr(feature = "cargo-clippy", allow(clippy::cmp_owned))] method .sig .generics @@ -129,6 +132,9 @@ pub(crate) fn expand_private_openapi_trait(mut attrs: AttributeArgs, tr8: ItemTr let attrs = TraitItemAttrs::parse(method.attrs)?; method.attrs = attrs.other_attrs; for bound in attrs.openapi_bound { + // we compare two incompatible types using their `Display` implementation + // this triggers a false positive in clippy + #[cfg_attr(feature = "cargo-clippy", allow(clippy::cmp_owned))] method .sig .generics diff --git a/derive/src/request_body.rs b/derive/src/request_body.rs index c469a79..077d105 100644 --- a/derive/src/request_body.rs +++ b/derive/src/request_body.rs @@ -12,7 +12,7 @@ use syn::{ struct MimeList(Punctuated); impl Parse for MimeList { - fn parse(input: ParseStream) -> Result { + fn parse(input: ParseStream<'_>) -> Result { let list = Punctuated::parse_separated_nonempty(&input)?; Ok(Self(list)) } diff --git a/derive/src/resource.rs b/derive/src/resource.rs index c2a2e01..8e30275 100644 --- a/derive/src/resource.rs +++ b/derive/src/resource.rs @@ -15,7 +15,7 @@ use syn::{ struct MethodList(Punctuated); impl Parse for MethodList { - fn parse(input: ParseStream) -> Result { + fn parse(input: ParseStream<'_>) -> Result { let content; let _paren = parenthesized!(content in input); let list = Punctuated::parse_separated_nonempty(&content)?; diff --git a/src/cors.rs b/src/cors.rs index b563ea5..43ff43b 100644 --- a/src/cors.rs +++ b/src/cors.rs @@ -112,6 +112,7 @@ To change settings, you need to put this type into gotham's [State]: ```rust,no_run # use gotham::{router::builder::*, pipeline::{new_pipeline, single::single_pipeline}, state::State}; # use gotham_restful::{*, cors::Origin}; +# #[cfg_attr(feature = "cargo-clippy", allow(clippy::needless_doctest_main))] fn main() { let cors = CorsConfig { origin: Origin::Star, diff --git a/src/lib.rs b/src/lib.rs index 7f9a234..079f208 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,12 +1,8 @@ -#![allow(clippy::tabs_in_doc_comments)] -#![warn( - missing_debug_implementations, - rust_2018_idioms, - clippy::wildcard_imports, - clippy::redundant_closure_for_method_calls -)] +#![warn(missing_debug_implementations, rust_2018_idioms)] #![deny(broken_intra_doc_links)] #![forbid(unsafe_code)] +// can we have a lint for spaces in doc comments please? +#![cfg_attr(feature = "cargo-clippy", allow(clippy::tabs_in_doc_comments))] /*! This crate is an extension to the popular [gotham web framework][gotham] for Rust. It allows you to create resources with assigned endpoints that aim to be a more convenient way of creating handlers From 5261aa99311320184fc8de7f4eef04143427e943 Mon Sep 17 00:00:00 2001 From: msrd0 <1182023-msrd0@users.noreply.gitlab.com> Date: Mon, 18 Jan 2021 16:56:16 +0000 Subject: [PATCH 130/170] Custom Endpoints --- CHANGELOG.md | 2 + README.md | 29 +++ derive/Cargo.toml | 3 + derive/src/endpoint.rs | 177 ++++++++++++++---- derive/src/lib.rs | 5 + derive/src/util.rs | 35 +++- src/lib.rs | 40 ++++ src/openapi/router.rs | 10 +- tests/openapi_specification.json | 50 +++++ tests/openapi_specification.rs | 20 +- .../ui/endpoint/custom_method_invalid_expr.rs | 11 ++ .../custom_method_invalid_expr.stderr | 11 ++ .../ui/endpoint/custom_method_invalid_type.rs | 11 ++ .../custom_method_invalid_type.stderr | 8 + tests/ui/endpoint/custom_method_missing.rs | 11 ++ .../ui/endpoint/custom_method_missing.stderr | 13 ++ tests/ui/endpoint/custom_uri_missing.rs | 11 ++ tests/ui/endpoint/custom_uri_missing.stderr | 13 ++ .../ui/endpoint/non_custom_body_attribute.rs | 11 ++ .../endpoint/non_custom_body_attribute.stderr | 11 ++ .../endpoint/non_custom_method_attribute.rs | 11 ++ .../non_custom_method_attribute.stderr | 11 ++ .../endpoint/non_custom_params_attribute.rs | 11 ++ .../non_custom_params_attribute.stderr | 11 ++ tests/ui/endpoint/non_custom_uri_attribute.rs | 11 ++ .../endpoint/non_custom_uri_attribute.stderr | 11 ++ tests/ui/endpoint/wants_auth_non_bool.rs | 11 ++ tests/ui/endpoint/wants_auth_non_bool.stderr | 11 ++ 28 files changed, 524 insertions(+), 46 deletions(-) create mode 100644 tests/ui/endpoint/custom_method_invalid_expr.rs create mode 100644 tests/ui/endpoint/custom_method_invalid_expr.stderr create mode 100644 tests/ui/endpoint/custom_method_invalid_type.rs create mode 100644 tests/ui/endpoint/custom_method_invalid_type.stderr create mode 100644 tests/ui/endpoint/custom_method_missing.rs create mode 100644 tests/ui/endpoint/custom_method_missing.stderr create mode 100644 tests/ui/endpoint/custom_uri_missing.rs create mode 100644 tests/ui/endpoint/custom_uri_missing.stderr create mode 100644 tests/ui/endpoint/non_custom_body_attribute.rs create mode 100644 tests/ui/endpoint/non_custom_body_attribute.stderr create mode 100644 tests/ui/endpoint/non_custom_method_attribute.rs create mode 100644 tests/ui/endpoint/non_custom_method_attribute.stderr create mode 100644 tests/ui/endpoint/non_custom_params_attribute.rs create mode 100644 tests/ui/endpoint/non_custom_params_attribute.stderr create mode 100644 tests/ui/endpoint/non_custom_uri_attribute.rs create mode 100644 tests/ui/endpoint/non_custom_uri_attribute.stderr create mode 100644 tests/ui/endpoint/wants_auth_non_bool.rs create mode 100644 tests/ui/endpoint/wants_auth_non_bool.stderr diff --git a/CHANGELOG.md b/CHANGELOG.md index af8a451..8b9fe8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Support custom HTTP response headers - New `endpoint` router extension with associated `Endpoint` trait ([!18]) + - Support for custom endpoints using the `#[endpoint]` macro ([!19]) ### Changed - The cors handler can now copy headers from the request if desired @@ -34,3 +35,4 @@ Previous changes are not tracked by this changelog file. Refer to the [releases] [!18]: https://gitlab.com/msrd0/gotham-restful/-/merge_requests/18 + [!19]: https://gitlab.com/msrd0/gotham-restful/-/merge_requests/19 diff --git a/README.md b/README.md index f683381..083bf20 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,11 @@ This crate is just as safe as you'd expect from anything written in safe Rust - ## Endpoints +There are a set of pre-defined endpoints that should cover the majority of REST APIs. However, +it is also possible to define your own endpoints. + +### Pre-defined Endpoints + Assuming you assign `/foobar` to your resource, the following pre-defined endpoints exist: | Endpoint Name | Required Arguments | HTTP Verb | HTTP Path | @@ -82,6 +87,30 @@ fn read(id: u64) -> Success { } ``` +### Custom Endpoints + +Defining custom endpoints is done with the `#[endpoint]` macro. The syntax is similar to that +of the pre-defined endpoints, but you need to give it more context: + +```rust +use gotham_restful::gotham::hyper::Method; + +#[derive(Resource)] +#[resource(custom_endpoint)] +struct CustomResource; + +/// This type is used to parse path parameters. +#[derive(Deserialize, StateData, StaticResponseExtender)] +struct CustomPath { + name: String +} + +#[endpoint(uri = "custom/:name/read", method = "Method::GET", params = false, body = false)] +fn custom_endpoint(path: CustomPath) -> Success { + path.name.into() +} +``` + ## Arguments Some endpoints require arguments. Those should be diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 8e0fb87..adc9d37 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -19,8 +19,11 @@ gitlab = { repository = "msrd0/gotham-restful", branch = "master" } [dependencies] heck = "0.3.1" +once_cell = "1.5" +paste = "1.0" proc-macro2 = "1.0.13" quote = "1.0.6" +regex = "1.4" syn = { version = "1.0.22", features = ["full"] } [features] diff --git a/derive/src/endpoint.rs b/derive/src/endpoint.rs index 1c8d15d..cbaec3b 100644 --- a/derive/src/endpoint.rs +++ b/derive/src/endpoint.rs @@ -1,10 +1,13 @@ -use crate::util::{CollectToResult, PathEndsWith}; +use crate::util::{CollectToResult, ExpectLit, PathEndsWith}; +use once_cell::sync::Lazy; +use paste::paste; use proc_macro2::{Ident, Span, TokenStream}; -use quote::{format_ident, quote}; +use quote::{format_ident, quote, ToTokens}; +use regex::Regex; use std::str::FromStr; use syn::{ - spanned::Spanned, Attribute, AttributeArgs, Error, FnArg, ItemFn, Lit, LitBool, Meta, NestedMeta, PatType, Result, - ReturnType, Type + parse::Parse, spanned::Spanned, Attribute, AttributeArgs, Error, Expr, FnArg, ItemFn, LitBool, LitStr, Meta, NestedMeta, + PatType, Result, ReturnType, Type }; pub enum EndpointType { @@ -15,9 +18,52 @@ pub enum EndpointType { UpdateAll, Update, DeleteAll, - Delete + Delete, + Custom { + method: Option, + uri: Option, + params: Option, + body: Option + } } +impl EndpointType { + pub fn custom() -> Self { + Self::Custom { + method: None, + uri: None, + params: None, + body: None + } + } +} + +macro_rules! endpoint_type_setter { + ($name:ident : $ty:ty) => { + impl EndpointType { + paste! { + fn [](&mut self, span: Span, []: $ty) -> Result<()> { + match self { + Self::Custom { $name, .. } if $name.is_some() => { + Err(Error::new(span, concat!("`", concat!(stringify!($name), "` must not appear more than once")))) + }, + Self::Custom { $name, .. } => { + *$name = Some([]); + Ok(()) + }, + _ => Err(Error::new(span, concat!("`", concat!(stringify!($name), "` can only be used on custom endpoints")))) + } + } + } + } + }; +} + +endpoint_type_setter!(method: Expr); +endpoint_type_setter!(uri: LitStr); +endpoint_type_setter!(params: LitBool); +endpoint_type_setter!(body: LitBool); + impl FromStr for EndpointType { type Err = Error; @@ -36,21 +82,26 @@ impl FromStr for EndpointType { } } +static URI_PLACEHOLDER_REGEX: Lazy = Lazy::new(|| Regex::new(r#"(^|/):(?P[^/]+)(/|$)"#).unwrap()); + impl EndpointType { - fn http_method(&self) -> TokenStream { + fn http_method(&self) -> Option { + let hyper_method = quote!(::gotham_restful::gotham::hyper::Method); match self { - Self::ReadAll | Self::Read | Self::Search => quote!(::gotham_restful::gotham::hyper::Method::GET), - Self::Create => quote!(::gotham_restful::gotham::hyper::Method::POST), - Self::UpdateAll | Self::Update => quote!(::gotham_restful::gotham::hyper::Method::PUT), - Self::DeleteAll | Self::Delete => quote!(::gotham_restful::gotham::hyper::Method::DELETE) + Self::ReadAll | Self::Read | Self::Search => Some(quote!(#hyper_method::GET)), + Self::Create => Some(quote!(#hyper_method::POST)), + Self::UpdateAll | Self::Update => Some(quote!(#hyper_method::PUT)), + Self::DeleteAll | Self::Delete => Some(quote!(#hyper_method::DELETE)), + Self::Custom { method, .. } => method.as_ref().map(ToTokens::to_token_stream) } } - fn uri(&self) -> TokenStream { + fn uri(&self) -> Option { match self { - Self::ReadAll | Self::Create | Self::UpdateAll | Self::DeleteAll => quote!(""), - Self::Read | Self::Update | Self::Delete => quote!(":id"), - Self::Search => quote!("search") + Self::ReadAll | Self::Create | Self::UpdateAll | Self::DeleteAll => Some(quote!("")), + Self::Read | Self::Update | Self::Delete => Some(quote!(":id")), + Self::Search => Some(quote!("search")), + Self::Custom { uri, .. } => uri.as_ref().map(ToTokens::to_token_stream) } } @@ -63,6 +114,13 @@ impl EndpointType { Self::Read | Self::Update | Self::Delete => LitBool { value: true, span: Span::call_site() + }, + Self::Custom { uri, .. } => LitBool { + value: uri + .as_ref() + .map(|uri| URI_PLACEHOLDER_REGEX.is_match(&uri.value())) + .unwrap_or(false), + span: Span::call_site() } } } @@ -72,7 +130,14 @@ impl EndpointType { Self::ReadAll | Self::Search | Self::Create | Self::UpdateAll | Self::DeleteAll => { quote!(::gotham_restful::gotham::extractor::NoopPathExtractor) }, - Self::Read | Self::Update | Self::Delete => quote!(::gotham_restful::export::IdPlaceholder::<#arg_ty>) + Self::Read | Self::Update | Self::Delete => quote!(::gotham_restful::export::IdPlaceholder::<#arg_ty>), + Self::Custom { .. } => { + if self.has_placeholders().value { + arg_ty.to_token_stream() + } else { + quote!(::gotham_restful::gotham::extractor::NoopPathExtractor) + } + }, } } @@ -87,7 +152,11 @@ impl EndpointType { Self::Search => LitBool { value: true, span: Span::call_site() - } + }, + Self::Custom { params, .. } => params.clone().unwrap_or_else(|| LitBool { + value: false, + span: Span::call_site() + }) } } @@ -96,7 +165,14 @@ impl EndpointType { Self::ReadAll | Self::Read | Self::Create | Self::UpdateAll | Self::Update | Self::DeleteAll | Self::Delete => { quote!(::gotham_restful::gotham::extractor::NoopQueryStringExtractor) }, - Self::Search => quote!(#arg_ty) + Self::Search => quote!(#arg_ty), + Self::Custom { .. } => { + if self.needs_params().value { + arg_ty.to_token_stream() + } else { + quote!(::gotham_restful::gotham::extractor::NoopQueryStringExtractor) + } + }, } } @@ -109,14 +185,25 @@ impl EndpointType { Self::Create | Self::UpdateAll | Self::Update => LitBool { value: true, span: Span::call_site() - } + }, + Self::Custom { body, .. } => body.clone().unwrap_or_else(|| LitBool { + value: false, + span: Span::call_site() + }) } } fn body_ty(&self, arg_ty: Option<&Type>) -> TokenStream { match self { Self::ReadAll | Self::Read | Self::Search | Self::DeleteAll | Self::Delete => quote!(()), - Self::Create | Self::UpdateAll | Self::Update => quote!(#arg_ty) + Self::Create | Self::UpdateAll | Self::Update => quote!(#arg_ty), + Self::Custom { .. } => { + if self.needs_body().value { + arg_ty.to_token_stream() + } else { + quote!(::gotham_restful::gotham::extractor::NoopPathExtractor) + } + }, } } } @@ -219,7 +306,7 @@ fn interpret_arg(_index: usize, arg: &PatType) -> Result { } #[cfg(feature = "openapi")] -fn expand_operation_id(operation_id: Option) -> Option { +fn expand_operation_id(operation_id: Option) -> Option { match operation_id { Some(operation_id) => Some(quote! { fn operation_id() -> Option { @@ -231,16 +318,14 @@ fn expand_operation_id(operation_id: Option) -> Option { } #[cfg(not(feature = "openapi"))] -fn expand_operation_id(_: Option) -> Option { +fn expand_operation_id(_: Option) -> Option { None } -fn expand_wants_auth(wants_auth: Option, default: bool) -> TokenStream { - let wants_auth = wants_auth.unwrap_or_else(|| { - Lit::Bool(LitBool { - value: default, - span: Span::call_site() - }) +fn expand_wants_auth(wants_auth: Option, default: bool) -> TokenStream { + let wants_auth = wants_auth.unwrap_or_else(|| LitBool { + value: default, + span: Span::call_site() }); quote! { @@ -256,22 +341,30 @@ pub fn endpoint_ident(fn_ident: &Ident) -> Ident { // clippy doesn't realize that vectors can be used in closures #[cfg_attr(feature = "cargo-clippy", allow(clippy::needless_collect))] -fn expand_endpoint_type(ty: EndpointType, attrs: AttributeArgs, fun: &ItemFn) -> Result { +fn expand_endpoint_type(mut ty: EndpointType, attrs: AttributeArgs, fun: &ItemFn) -> Result { // reject unsafe functions if let Some(unsafety) = fun.sig.unsafety { return Err(Error::new(unsafety.span(), "Endpoint handler methods must not be unsafe")); } // parse arguments - let mut operation_id: Option = None; - let mut wants_auth: Option = None; + let mut operation_id: Option = None; + let mut wants_auth: Option = None; for meta in attrs { match meta { NestedMeta::Meta(Meta::NameValue(kv)) => { if kv.path.ends_with("operation_id") { - operation_id = Some(kv.lit); + operation_id = Some(kv.lit.expect_str()?); } else if kv.path.ends_with("wants_auth") { - wants_auth = Some(kv.lit); + wants_auth = Some(kv.lit.expect_bool()?); + } else if kv.path.ends_with("method") { + ty.set_method(kv.path.span(), kv.lit.expect_str()?.parse_with(Expr::parse)?)?; + } else if kv.path.ends_with("uri") { + ty.set_uri(kv.path.span(), kv.lit.expect_str()?)?; + } else if kv.path.ends_with("params") { + ty.set_params(kv.path.span(), kv.lit.expect_bool()?)?; + } else if kv.path.ends_with("body") { + ty.set_body(kv.path.span(), kv.lit.expect_bool()?)?; } else { return Err(Error::new(kv.path.span(), "Unknown attribute")); } @@ -324,8 +417,18 @@ fn expand_endpoint_type(ty: EndpointType, attrs: AttributeArgs, fun: &ItemFn) -> Ok(Some(ty)) }; - let http_method = ty.http_method(); - let uri = ty.uri(); + let http_method = ty.http_method().ok_or_else(|| { + Error::new( + Span::call_site(), + "Missing `method` attribute (e.g. `#[endpoint(method = \"gotham_restful::gotham::hyper::Method::GET\")]`)" + ) + })?; + let uri = ty.uri().ok_or_else(|| { + Error::new( + Span::call_site(), + "Missing `uri` attribute (e.g. `#[endpoint(uri = \"custom_endpoint\")]`)" + ) + })?; let has_placeholders = ty.has_placeholders(); let placeholder_ty = ty.placeholders_ty(next_arg_ty(!has_placeholders.value)?); let needs_params = ty.needs_params(); @@ -339,7 +442,11 @@ fn expand_endpoint_type(ty: EndpointType, attrs: AttributeArgs, fun: &ItemFn) -> let mut handle_args: Vec = Vec::new(); if has_placeholders.value { - handle_args.push(quote!(placeholders.id)); + if matches!(ty, EndpointType::Custom { .. }) { + handle_args.push(quote!(placeholders)); + } else { + handle_args.push(quote!(placeholders.id)); + } } if needs_params.value { handle_args.push(quote!(params)); diff --git a/derive/src/lib.rs b/derive/src/lib.rs index f42e50f..39e2855 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -87,6 +87,11 @@ pub fn derive_resource_error(input: TokenStream) -> TokenStream { expand_derive(input, expand_resource_error) } +#[proc_macro_attribute] +pub fn endpoint(attr: TokenStream, item: TokenStream) -> TokenStream { + expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::custom(), attr, item)) +} + #[proc_macro_attribute] pub fn read_all(attr: TokenStream, item: TokenStream) -> TokenStream { expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::ReadAll, attr, item)) diff --git a/derive/src/util.rs b/derive/src/util.rs index aa94ce5..ef55659 100644 --- a/derive/src/util.rs +++ b/derive/src/util.rs @@ -1,21 +1,21 @@ use proc_macro2::{Delimiter, TokenStream, TokenTree}; use std::iter; -use syn::{Error, Path}; +use syn::{Error, Lit, LitBool, LitStr, Path, Result}; -pub trait CollectToResult { +pub(crate) trait CollectToResult { type Item; - fn collect_to_result(self) -> Result, Error>; + fn collect_to_result(self) -> Result>; } impl CollectToResult for I where - I: Iterator> + I: Iterator> { type Item = Item; - fn collect_to_result(self) -> Result, Error> { - self.fold(, Error>>::Ok(Vec::new()), |res, code| match (code, res) { + fn collect_to_result(self) -> Result> { + self.fold(Ok(Vec::new()), |res, code| match (code, res) { (Ok(code), Ok(mut codes)) => { codes.push(code); Ok(codes) @@ -30,6 +30,27 @@ where } } +pub(crate) trait ExpectLit { + fn expect_bool(self) -> Result; + fn expect_str(self) -> Result; +} + +impl ExpectLit for Lit { + fn expect_bool(self) -> Result { + match self { + Self::Bool(bool) => Ok(bool), + _ => Err(Error::new(self.span(), "Expected boolean literal")) + } + } + + fn expect_str(self) -> Result { + match self { + Self::Str(str) => Ok(str), + _ => Err(Error::new(self.span(), "Expected string literal")) + } + } +} + pub(crate) trait PathEndsWith { fn ends_with(&self, s: &str) -> bool; } @@ -40,7 +61,7 @@ impl PathEndsWith for Path { } } -pub fn remove_parens(input: TokenStream) -> TokenStream { +pub(crate) fn remove_parens(input: TokenStream) -> TokenStream { let iter = input.into_iter().flat_map(|tt| { if let TokenTree::Group(group) = &tt { if group.delimiter() == Delimiter::Parenthesis { diff --git a/src/lib.rs b/src/lib.rs index 079f208..780f053 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,11 @@ This crate is just as safe as you'd expect from anything written in safe Rust - # Endpoints +There are a set of pre-defined endpoints that should cover the majority of REST APIs. However, +it is also possible to define your own endpoints. + +## Pre-defined Endpoints + Assuming you assign `/foobar` to your resource, the following pre-defined endpoints exist: | Endpoint Name | Required Arguments | HTTP Verb | HTTP Path | @@ -70,6 +75,41 @@ fn read(id: u64) -> Success { # } ``` +## Custom Endpoints + +Defining custom endpoints is done with the `#[endpoint]` macro. The syntax is similar to that +of the pre-defined endpoints, but you need to give it more context: + +```rust,no_run +# #[macro_use] extern crate gotham_derive; +# #[macro_use] extern crate gotham_restful_derive; +# use gotham::router::builder::*; +# use gotham_restful::*; +# use serde::{Deserialize, Serialize}; +use gotham_restful::gotham::hyper::Method; + +#[derive(Resource)] +#[resource(custom_endpoint)] +struct CustomResource; + +/// This type is used to parse path parameters. +#[derive(Deserialize, StateData, StaticResponseExtender)] +# #[cfg_attr(feature = "openapi", derive(OpenapiType))] +struct CustomPath { + name: String +} + +#[endpoint(uri = "custom/:name/read", method = "Method::GET", params = false, body = false)] +fn custom_endpoint(path: CustomPath) -> Success { + path.name.into() +} +# fn main() { +# gotham::start("127.0.0.1:8080", build_simple_router(|route| { +# route.resource::("custom"); +# })); +# } +``` + # Arguments Some endpoints require arguments. Those should be diff --git a/src/openapi/router.rs b/src/openapi/router.rs index a836a40..7ae71c6 100644 --- a/src/openapi/router.rs +++ b/src/openapi/router.rs @@ -83,10 +83,14 @@ macro_rules! implOpenapiRouter { } static URI_PLACEHOLDER_REGEX: Lazy = - Lazy::new(|| Regex::new(r#"(^|/):(?P[^/]+)(/|$)"#).unwrap()); + Lazy::new(|| Regex::new(r#"(?P^|/):(?P[^/]+)(?P/|$)"#).unwrap()); let uri: &str = &E::uri(); - let uri = - URI_PLACEHOLDER_REGEX.replace_all(uri, |captures: &Captures<'_>| format!("{{{}}}", &captures["name"])); + let uri = URI_PLACEHOLDER_REGEX.replace_all(uri, |captures: &Captures<'_>| { + format!( + "{}{{{}}}{}", + &captures["prefix"], &captures["name"], &captures["suffix"] + ) + }); let path = if uri.is_empty() { format!("{}/{}", self.0.scope.unwrap_or_default(), self.1) } else { diff --git a/tests/openapi_specification.json b/tests/openapi_specification.json index c9e6c53..c3297cd 100644 --- a/tests/openapi_specification.json +++ b/tests/openapi_specification.json @@ -44,6 +44,56 @@ }, "openapi": "3.0.2", "paths": { + "/custom": { + "patch": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/custom/read/{from}/with/{id}": { + "get": { + "parameters": [ + { + "in": "path", + "name": "from", + "required": true, + "schema": { + "type": "string" + }, + "style": "simple" + }, + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "int64", + "minimum": 0, + "type": "integer" + }, + "style": "simple" + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, "/img/{id}": { "get": { "operationId": "getImage", diff --git a/tests/openapi_specification.rs b/tests/openapi_specification.rs index faa9fb1..a2d33d4 100644 --- a/tests/openapi_specification.rs +++ b/tests/openapi_specification.rs @@ -5,6 +5,7 @@ extern crate gotham_derive; use chrono::{NaiveDate, NaiveDateTime}; use gotham::{ + hyper::Method, pipeline::{new_pipeline, single::single_pipeline}, router::builder::*, test::TestServer @@ -81,6 +82,22 @@ fn search_secret(auth: AuthStatus, _query: SecretQuery) -> AuthSuccess }) } +#[derive(Resource)] +#[resource(custom_read_with, custom_patch)] +struct CustomResource; + +#[derive(Deserialize, OpenapiType, StateData, StaticResponseExtender)] +struct ReadWithPath { + from: String, + id: u64 +} + +#[endpoint(method = "Method::GET", uri = "read/:from/with/:id")] +fn custom_read_with(_path: ReadWithPath) {} + +#[endpoint(method = "Method::PATCH", uri = "", body = true)] +fn custom_patch(_body: String) {} + #[test] fn openapi_specification() { let info = OpenapiInfo { @@ -97,8 +114,9 @@ fn openapi_specification() { let server = TestServer::new(build_router(chain, pipelines, |router| { router.with_openapi(info, |mut router| { router.resource::("img"); - router.get_openapi("openapi"); router.resource::("secret"); + router.resource::("custom"); + router.get_openapi("openapi"); }); })) .unwrap(); diff --git a/tests/ui/endpoint/custom_method_invalid_expr.rs b/tests/ui/endpoint/custom_method_invalid_expr.rs new file mode 100644 index 0000000..9d6450d --- /dev/null +++ b/tests/ui/endpoint/custom_method_invalid_expr.rs @@ -0,0 +1,11 @@ +#[macro_use] +extern crate gotham_restful; + +#[derive(Resource)] +#[resource(read_all)] +struct FooResource; + +#[endpoint(method = "I like pizza", uri = "custom_read")] +async fn read_all() {} + +fn main() {} diff --git a/tests/ui/endpoint/custom_method_invalid_expr.stderr b/tests/ui/endpoint/custom_method_invalid_expr.stderr new file mode 100644 index 0000000..8375cd1 --- /dev/null +++ b/tests/ui/endpoint/custom_method_invalid_expr.stderr @@ -0,0 +1,11 @@ +error: unexpected token + --> $DIR/custom_method_invalid_expr.rs:8:21 + | +8 | #[endpoint(method = "I like pizza", uri = "custom_read")] + | ^^^^^^^^^^^^^^ + +error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope + --> $DIR/custom_method_invalid_expr.rs:5:12 + | +5 | #[resource(read_all)] + | ^^^^^^^^ not found in this scope diff --git a/tests/ui/endpoint/custom_method_invalid_type.rs b/tests/ui/endpoint/custom_method_invalid_type.rs new file mode 100644 index 0000000..b5bc674 --- /dev/null +++ b/tests/ui/endpoint/custom_method_invalid_type.rs @@ -0,0 +1,11 @@ +#[macro_use] +extern crate gotham_restful; + +#[derive(Resource)] +#[resource(read_all)] +struct FooResource; + +#[endpoint(method = "String::new()", uri = "custom_read")] +async fn read_all() {} + +fn main() {} diff --git a/tests/ui/endpoint/custom_method_invalid_type.stderr b/tests/ui/endpoint/custom_method_invalid_type.stderr new file mode 100644 index 0000000..35ad1c3 --- /dev/null +++ b/tests/ui/endpoint/custom_method_invalid_type.stderr @@ -0,0 +1,8 @@ +error[E0308]: mismatched types + --> $DIR/custom_method_invalid_type.rs:8:21 + | +8 | #[endpoint(method = "String::new()", uri = "custom_read")] + | --------------------^^^^^^^^^^^^^^^----------------------- + | | | + | | expected struct `Method`, found struct `std::string::String` + | expected `Method` because of return type diff --git a/tests/ui/endpoint/custom_method_missing.rs b/tests/ui/endpoint/custom_method_missing.rs new file mode 100644 index 0000000..d0e6855 --- /dev/null +++ b/tests/ui/endpoint/custom_method_missing.rs @@ -0,0 +1,11 @@ +#[macro_use] +extern crate gotham_restful; + +#[derive(Resource)] +#[resource(read_all)] +struct FooResource; + +#[endpoint(uri = "custom_read")] +async fn read_all() {} + +fn main() {} diff --git a/tests/ui/endpoint/custom_method_missing.stderr b/tests/ui/endpoint/custom_method_missing.stderr new file mode 100644 index 0000000..df6da97 --- /dev/null +++ b/tests/ui/endpoint/custom_method_missing.stderr @@ -0,0 +1,13 @@ +error: Missing `method` attribute (e.g. `#[endpoint(method = "gotham_restful::gotham::hyper::Method::GET")]`) + --> $DIR/custom_method_missing.rs:8:1 + | +8 | #[endpoint(uri = "custom_read")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope + --> $DIR/custom_method_missing.rs:5:12 + | +5 | #[resource(read_all)] + | ^^^^^^^^ not found in this scope diff --git a/tests/ui/endpoint/custom_uri_missing.rs b/tests/ui/endpoint/custom_uri_missing.rs new file mode 100644 index 0000000..5ec5182 --- /dev/null +++ b/tests/ui/endpoint/custom_uri_missing.rs @@ -0,0 +1,11 @@ +#[macro_use] +extern crate gotham_restful; + +#[derive(Resource)] +#[resource(read_all)] +struct FooResource; + +#[endpoint(method = "gotham_restful::gotham::hyper::Method::GET")] +async fn read_all() {} + +fn main() {} diff --git a/tests/ui/endpoint/custom_uri_missing.stderr b/tests/ui/endpoint/custom_uri_missing.stderr new file mode 100644 index 0000000..b1584b8 --- /dev/null +++ b/tests/ui/endpoint/custom_uri_missing.stderr @@ -0,0 +1,13 @@ +error: Missing `uri` attribute (e.g. `#[endpoint(uri = "custom_endpoint")]`) + --> $DIR/custom_uri_missing.rs:8:1 + | +8 | #[endpoint(method = "gotham_restful::gotham::hyper::Method::GET")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope + --> $DIR/custom_uri_missing.rs:5:12 + | +5 | #[resource(read_all)] + | ^^^^^^^^ not found in this scope diff --git a/tests/ui/endpoint/non_custom_body_attribute.rs b/tests/ui/endpoint/non_custom_body_attribute.rs new file mode 100644 index 0000000..e6257d4 --- /dev/null +++ b/tests/ui/endpoint/non_custom_body_attribute.rs @@ -0,0 +1,11 @@ +#[macro_use] +extern crate gotham_restful; + +#[derive(Resource)] +#[resource(read_all)] +struct FooResource; + +#[read_all(body = false)] +async fn read_all() {} + +fn main() {} diff --git a/tests/ui/endpoint/non_custom_body_attribute.stderr b/tests/ui/endpoint/non_custom_body_attribute.stderr new file mode 100644 index 0000000..a30e540 --- /dev/null +++ b/tests/ui/endpoint/non_custom_body_attribute.stderr @@ -0,0 +1,11 @@ +error: `body` can only be used on custom endpoints + --> $DIR/non_custom_body_attribute.rs:8:12 + | +8 | #[read_all(body = false)] + | ^^^^ + +error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope + --> $DIR/non_custom_body_attribute.rs:5:12 + | +5 | #[resource(read_all)] + | ^^^^^^^^ not found in this scope diff --git a/tests/ui/endpoint/non_custom_method_attribute.rs b/tests/ui/endpoint/non_custom_method_attribute.rs new file mode 100644 index 0000000..c23423c --- /dev/null +++ b/tests/ui/endpoint/non_custom_method_attribute.rs @@ -0,0 +1,11 @@ +#[macro_use] +extern crate gotham_restful; + +#[derive(Resource)] +#[resource(read_all)] +struct FooResource; + +#[read_all(method = "gotham_restful::gotham::hyper::Method::GET")] +async fn read_all() {} + +fn main() {} diff --git a/tests/ui/endpoint/non_custom_method_attribute.stderr b/tests/ui/endpoint/non_custom_method_attribute.stderr new file mode 100644 index 0000000..42a541d --- /dev/null +++ b/tests/ui/endpoint/non_custom_method_attribute.stderr @@ -0,0 +1,11 @@ +error: `method` can only be used on custom endpoints + --> $DIR/non_custom_method_attribute.rs:8:12 + | +8 | #[read_all(method = "gotham_restful::gotham::hyper::Method::GET")] + | ^^^^^^ + +error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope + --> $DIR/non_custom_method_attribute.rs:5:12 + | +5 | #[resource(read_all)] + | ^^^^^^^^ not found in this scope diff --git a/tests/ui/endpoint/non_custom_params_attribute.rs b/tests/ui/endpoint/non_custom_params_attribute.rs new file mode 100644 index 0000000..377331b --- /dev/null +++ b/tests/ui/endpoint/non_custom_params_attribute.rs @@ -0,0 +1,11 @@ +#[macro_use] +extern crate gotham_restful; + +#[derive(Resource)] +#[resource(read_all)] +struct FooResource; + +#[read_all(params = true)] +async fn read_all() {} + +fn main() {} diff --git a/tests/ui/endpoint/non_custom_params_attribute.stderr b/tests/ui/endpoint/non_custom_params_attribute.stderr new file mode 100644 index 0000000..9ca336e --- /dev/null +++ b/tests/ui/endpoint/non_custom_params_attribute.stderr @@ -0,0 +1,11 @@ +error: `params` can only be used on custom endpoints + --> $DIR/non_custom_params_attribute.rs:8:12 + | +8 | #[read_all(params = true)] + | ^^^^^^ + +error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope + --> $DIR/non_custom_params_attribute.rs:5:12 + | +5 | #[resource(read_all)] + | ^^^^^^^^ not found in this scope diff --git a/tests/ui/endpoint/non_custom_uri_attribute.rs b/tests/ui/endpoint/non_custom_uri_attribute.rs new file mode 100644 index 0000000..1945fce --- /dev/null +++ b/tests/ui/endpoint/non_custom_uri_attribute.rs @@ -0,0 +1,11 @@ +#[macro_use] +extern crate gotham_restful; + +#[derive(Resource)] +#[resource(read_all)] +struct FooResource; + +#[read_all(uri = "custom_read")] +async fn read_all() {} + +fn main() {} diff --git a/tests/ui/endpoint/non_custom_uri_attribute.stderr b/tests/ui/endpoint/non_custom_uri_attribute.stderr new file mode 100644 index 0000000..61018a7 --- /dev/null +++ b/tests/ui/endpoint/non_custom_uri_attribute.stderr @@ -0,0 +1,11 @@ +error: `uri` can only be used on custom endpoints + --> $DIR/non_custom_uri_attribute.rs:8:12 + | +8 | #[read_all(uri = "custom_read")] + | ^^^ + +error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope + --> $DIR/non_custom_uri_attribute.rs:5:12 + | +5 | #[resource(read_all)] + | ^^^^^^^^ not found in this scope diff --git a/tests/ui/endpoint/wants_auth_non_bool.rs b/tests/ui/endpoint/wants_auth_non_bool.rs new file mode 100644 index 0000000..b341aaf --- /dev/null +++ b/tests/ui/endpoint/wants_auth_non_bool.rs @@ -0,0 +1,11 @@ +#[macro_use] +extern crate gotham_restful; + +#[derive(Resource)] +#[resource(read_all)] +struct FooResource; + +#[read_all(wants_auth = "yes, please")] +async fn read_all() {} + +fn main() {} diff --git a/tests/ui/endpoint/wants_auth_non_bool.stderr b/tests/ui/endpoint/wants_auth_non_bool.stderr new file mode 100644 index 0000000..e752c40 --- /dev/null +++ b/tests/ui/endpoint/wants_auth_non_bool.stderr @@ -0,0 +1,11 @@ +error: Expected boolean literal + --> $DIR/wants_auth_non_bool.rs:8:25 + | +8 | #[read_all(wants_auth = "yes, please")] + | ^^^^^^^^^^^^^ + +error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope + --> $DIR/wants_auth_non_bool.rs:5:12 + | +5 | #[resource(read_all)] + | ^^^^^^^^ not found in this scope From 70914d107b3f3bd4676d6b5a57fe870f02e3113f Mon Sep 17 00:00:00 2001 From: Dominic Date: Mon, 18 Jan 2021 18:38:12 +0100 Subject: [PATCH 131/170] fix missing FromState import --- derive/src/endpoint.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/derive/src/endpoint.rs b/derive/src/endpoint.rs index cbaec3b..0b5aaec 100644 --- a/derive/src/endpoint.rs +++ b/derive/src/endpoint.rs @@ -498,6 +498,7 @@ fn expand_endpoint_type(mut ty: EndpointType, attrs: AttributeArgs, fun: &ItemFn Ok(quote! { use ::gotham_restful::export::FutureExt as _; + use ::gotham_restful::gotham::state::FromState as _; #state_block async move { #handle_content From 681ef5d8949fe2cbbe7dc3e6b7d3d14d5e65cb3b Mon Sep 17 00:00:00 2001 From: Dominic Date: Mon, 18 Jan 2021 18:50:26 +0100 Subject: [PATCH 132/170] add debug option to endpoint macro --- derive/src/endpoint.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/derive/src/endpoint.rs b/derive/src/endpoint.rs index 0b5aaec..87ac3d8 100644 --- a/derive/src/endpoint.rs +++ b/derive/src/endpoint.rs @@ -348,12 +348,15 @@ fn expand_endpoint_type(mut ty: EndpointType, attrs: AttributeArgs, fun: &ItemFn } // parse arguments + let mut debug: bool = false; let mut operation_id: Option = None; let mut wants_auth: Option = None; for meta in attrs { match meta { NestedMeta::Meta(Meta::NameValue(kv)) => { - if kv.path.ends_with("operation_id") { + if kv.path.ends_with("debug") { + debug = kv.lit.expect_bool()?.value; + } else if kv.path.ends_with("operation_id") { operation_id = Some(kv.lit.expect_str()?); } else if kv.path.ends_with("wants_auth") { wants_auth = Some(kv.lit.expect_bool()?); @@ -517,7 +520,7 @@ fn expand_endpoint_type(mut ty: EndpointType, attrs: AttributeArgs, fun: &ItemFn }; let operation_id = expand_operation_id(operation_id); let wants_auth = expand_wants_auth(wants_auth, args.iter().any(|arg| arg.ty.is_auth_status())); - Ok(quote! { + let code = quote! { #[doc(hidden)] /// `gotham_restful` implementation detail #[allow(non_camel_case_types)] @@ -564,7 +567,11 @@ fn expand_endpoint_type(mut ty: EndpointType, attrs: AttributeArgs, fun: &ItemFn #wants_auth } }; - }) + }; + if debug { + eprintln!("{}", code); + } + Ok(code) } pub fn expand_endpoint(ty: EndpointType, attrs: AttributeArgs, fun: ItemFn) -> Result { From f2bcc8438f9b4c8d6c5f117ddc933be9b0998631 Mon Sep 17 00:00:00 2001 From: Dominic Date: Mon, 18 Jan 2021 19:04:06 +0100 Subject: [PATCH 133/170] fix implicit `&'static mut State` error --- Cargo.toml | 1 + derive/src/endpoint.rs | 6 +++--- src/endpoint.rs | 12 ++++++------ tests/async_methods.rs | 19 +++++++++++++++++-- 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 65d8f49..9203f31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ diesel = { version = "1.4.4", features = ["postgres"] } futures-executor = "0.3.5" paste = "1.0" pretty_env_logger = "0.4" +tokio = { version = "0.2", features = ["time"], default-features = false } thiserror = "1.0.18" trybuild = "1.0.27" diff --git a/derive/src/endpoint.rs b/derive/src/endpoint.rs index 87ac3d8..a363ec0 100644 --- a/derive/src/endpoint.rs +++ b/derive/src/endpoint.rs @@ -554,12 +554,12 @@ fn expand_endpoint_type(mut ty: EndpointType, attrs: AttributeArgs, fun: &ItemFn } type Body = #body_ty; - fn handle( - state: &mut ::gotham_restful::gotham::state::State, + fn handle<'a>( + state: &'a mut ::gotham_restful::gotham::state::State, placeholders: Self::Placeholders, params: Self::Params, body: ::std::option::Option - ) -> ::gotham_restful::export::BoxFuture<'static, Self::Output> { + ) -> ::gotham_restful::export::BoxFuture<'a, Self::Output> { #handle_content } diff --git a/src/endpoint.rs b/src/endpoint.rs index 90bc952..32dd015 100644 --- a/src/endpoint.rs +++ b/src/endpoint.rs @@ -56,12 +56,12 @@ pub trait Endpoint { } /// The handler for this endpoint. - fn handle( - state: &mut State, + fn handle<'a>( + state: &'a mut State, placeholders: Self::Placeholders, params: Self::Params, body: Option - ) -> BoxFuture<'static, Self::Output>; + ) -> BoxFuture<'a, Self::Output>; } #[cfg(feature = "openapi")] @@ -94,12 +94,12 @@ impl Endpoint for E { E::wants_auth() } - fn handle( - state: &mut State, + fn handle<'a>( + state: &'a mut State, placeholders: Self::Placeholders, params: Self::Params, body: Option - ) -> BoxFuture<'static, Self::Output> { + ) -> BoxFuture<'a, Self::Output> { E::handle(state, placeholders, params, body) } } diff --git a/tests/async_methods.rs b/tests/async_methods.rs index be6a406..21c3462 100644 --- a/tests/async_methods.rs +++ b/tests/async_methods.rs @@ -1,10 +1,15 @@ #[macro_use] extern crate gotham_derive; -use gotham::{router::builder::*, test::TestServer}; +use gotham::{ + hyper::{HeaderMap, Method}, + router::builder::*, + test::TestServer +}; use gotham_restful::*; use mime::{APPLICATION_JSON, TEXT_PLAIN}; use serde::Deserialize; +use tokio::time::{delay_for, Duration}; mod util { include!("util/mod.rs"); @@ -12,7 +17,7 @@ mod util { use util::{test_delete_response, test_get_response, test_post_response, test_put_response}; #[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, state_test)] struct FooResource; #[derive(Deserialize)] @@ -77,6 +82,15 @@ async fn remove(_id: u64) -> Raw<&'static [u8]> { Raw::new(REMOVE_RESPONSE, TEXT_PLAIN) } +const STATE_TEST_RESPONSE: &[u8] = b"xxJbxOuwioqR5DfzPuVqvaqRSfpdNQGluIvHU4n1LM"; +#[endpoint(method = "Method::GET", uri = "state_test")] +async fn state_test(state: &mut State) -> Raw<&'static [u8]> { + delay_for(Duration::from_nanos(1)).await; + state.borrow::(); + delay_for(Duration::from_nanos(1)).await; + Raw::new(STATE_TEST_RESPONSE, TEXT_PLAIN) +} + #[test] fn async_methods() { let _ = pretty_env_logger::try_init_timed(); @@ -112,4 +126,5 @@ fn async_methods() { ); test_delete_response(&server, "http://localhost/foo", REMOVE_ALL_RESPONSE); test_delete_response(&server, "http://localhost/foo/1", REMOVE_RESPONSE); + test_get_response(&server, "http://localhost/foo/state_test", STATE_TEST_RESPONSE); } From cf0223473f05a823e9cbd63741f3d7c8f21c5d9c Mon Sep 17 00:00:00 2001 From: Dominic Date: Sat, 23 Jan 2021 16:11:33 +0100 Subject: [PATCH 134/170] allow more types to appear in AuthErrorOrOther (related to #20) --- src/result/auth_result.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/result/auth_result.rs b/src/result/auth_result.rs index 677053e..90e564e 100644 --- a/src/result/auth_result.rs +++ b/src/result/auth_result.rs @@ -69,10 +69,16 @@ impl From for AuthErrorOrOther { } } +mod private { + use gotham::anyhow; + pub trait Sealed {} + impl> Sealed for E {} +} + impl From for AuthErrorOrOther where // TODO https://gitlab.com/msrd0/gotham-restful/-/issues/20 - F: std::error::Error + Into + F: private::Sealed + Into { fn from(err: F) -> Self { Self::Other(err.into()) From 441a42c75e973bfb8130b170c34320f7417ec93c Mon Sep 17 00:00:00 2001 From: msrd0 <1182023-msrd0@users.noreply.gitlab.com> Date: Tue, 26 Jan 2021 17:49:11 +0000 Subject: [PATCH 135/170] Add a Redirect type that can be returned by endpoints --- Cargo.toml | 1 + src/lib.rs | 2 +- src/result/mod.rs | 3 + src/result/redirect.rs | 143 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 src/result/redirect.rs diff --git a/Cargo.toml b/Cargo.toml index 9203f31..dff6df4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ openapiv3 = { version = "0.3.2", optional = true } regex = { version = "1.4", optional = true } serde = { version = "1.0.110", features = ["derive"] } serde_json = "1.0.58" +thiserror = "1.0" uuid = { version = "0.8.1", optional = true } [dev-dependencies] diff --git a/src/lib.rs b/src/lib.rs index 780f053..87cb87c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -481,7 +481,7 @@ pub use response::Response; mod result; pub use result::{ - AuthError, AuthError::Forbidden, AuthErrorOrOther, AuthResult, AuthSuccess, IntoResponseError, NoContent, Raw, + AuthError, AuthError::Forbidden, AuthErrorOrOther, AuthResult, AuthSuccess, IntoResponseError, NoContent, Raw, Redirect, ResourceResult, Success }; diff --git a/src/result/mod.rs b/src/result/mod.rs index 526af33..d5895aa 100644 --- a/src/result/mod.rs +++ b/src/result/mod.rs @@ -20,6 +20,9 @@ pub use no_content::NoContent; mod raw; pub use raw::Raw; +mod redirect; +pub use redirect::Redirect; + #[allow(clippy::module_inception)] mod result; pub use result::IntoResponseError; diff --git a/src/result/redirect.rs b/src/result/redirect.rs new file mode 100644 index 0000000..5754adc --- /dev/null +++ b/src/result/redirect.rs @@ -0,0 +1,143 @@ +use super::{handle_error, NoContent, ResourceResult}; +#[cfg(feature = "openapi")] +use crate::OpenapiSchema; +use crate::{IntoResponseError, Response}; +use futures_util::future::{BoxFuture, FutureExt, TryFutureExt}; +use gotham::hyper::{ + header::{InvalidHeaderValue, LOCATION}, + Body, StatusCode +}; +use std::{ + error::Error as StdError, + fmt::{Debug, Display} +}; +use thiserror::Error; + +/** +This is the return type of a resource that only returns a redirect. It will result +in a _303 See Other_ answer, meaning the redirect will always result in a GET request +on the target. + +``` +# #[macro_use] extern crate gotham_restful_derive; +# mod doc_tests_are_broken { +# use gotham::state::State; +# use gotham_restful::*; +# +# #[derive(Resource)] +# #[resource(read_all)] +# struct MyResource; +# +#[read_all] +fn read_all() -> Redirect { + Redirect { + to: "http://localhost:8080/cool/new/location".to_owned() + } +} +# } +``` +*/ +#[derive(Clone, Debug, Default)] +pub struct Redirect { + pub to: String +} + +impl ResourceResult for Redirect { + type Err = InvalidHeaderValue; + + fn into_response(self) -> BoxFuture<'static, Result> { + async move { + let mut res = Response::new(StatusCode::SEE_OTHER, Body::empty(), None); + res.header(LOCATION, self.to.parse()?); + Ok(res) + } + .boxed() + } + + #[cfg(feature = "openapi")] + fn default_status() -> StatusCode { + StatusCode::SEE_OTHER + } + + #[cfg(feature = "openapi")] + fn schema() -> OpenapiSchema { + ::schema() + } +} + +// private type due to parent mod +#[derive(Debug, Error)] +pub enum RedirectError { + #[error("{0}")] + InvalidLocation(#[from] InvalidHeaderValue), + #[error("{0}")] + Other(#[source] E) +} + +#[allow(ambiguous_associated_items)] // an enum variant is not a type. never. +impl ResourceResult for Result +where + E: Display + IntoResponseError, + ::Err: Sync +{ + type Err = RedirectError<::Err>; + + fn into_response(self) -> BoxFuture<'static, Result> { + match self { + Ok(nc) => nc.into_response().map_err(Into::into).boxed(), + Err(e) => handle_error(e).map_err(|e| RedirectError::Other(e)).boxed() + } + } + + #[cfg(feature = "openapi")] + fn default_status() -> StatusCode { + Redirect::default_status() + } + + #[cfg(feature = "openapi")] + fn schema() -> OpenapiSchema { + ::schema() + } +} + +#[cfg(test)] +mod test { + use super::*; + use futures_executor::block_on; + use gotham::hyper::StatusCode; + use thiserror::Error; + + #[derive(Debug, Default, Error)] + #[error("An Error")] + struct MsgError; + + #[test] + fn rediect_has_redirect_response() { + let redir = Redirect { + to: "http://localhost/foo".to_owned() + }; + let res = block_on(redir.into_response()).expect("didn't expect error response"); + assert_eq!(res.status, StatusCode::SEE_OTHER); + assert_eq!(res.mime, None); + assert_eq!( + res.headers.get(LOCATION).map(|hdr| hdr.to_str().unwrap()), + Some("http://localhost/foo") + ); + assert_eq!(res.full_body().unwrap(), &[] as &[u8]); + } + + #[test] + fn redirect_result() { + let redir: Result = Ok(Redirect { + to: "http://localhost/foo".to_owned() + }); + let res = block_on(redir.into_response()).expect("didn't expect error response"); + assert_eq!(res.status, StatusCode::SEE_OTHER); + assert_eq!(res.mime, None); + assert_eq!( + res.headers.get(LOCATION).map(|hdr| hdr.to_str().unwrap()), + Some("http://localhost/foo") + ); + assert_eq!(res.full_body().unwrap(), &[] as &[u8]); + } +} From af28e0d9165bce227722ad56cd364738af341c7c Mon Sep 17 00:00:00 2001 From: msrd0 <1182023-msrd0@users.noreply.gitlab.com> Date: Wed, 3 Feb 2021 21:22:46 +0000 Subject: [PATCH 136/170] Reexports --- README.md | 1 + derive/src/endpoint.rs | 8 ++++---- derive/src/openapi_type.rs | 4 ++-- derive/src/request_body.rs | 2 +- derive/src/resource_error.rs | 2 +- src/auth.rs | 8 +++++--- src/lib.rs | 12 +++++------- src/openapi/handler.rs | 8 +++++--- src/openapi/operation.rs | 2 +- src/result/mod.rs | 9 ++++++--- src/result/no_content.rs | 9 ++++++--- src/result/raw.rs | 8 ++++++-- src/result/result.rs | 4 +++- src/routing.rs | 4 ++-- tests/async_methods.rs | 1 + tests/ui/endpoint/async_state.rs | 2 +- 16 files changed, 50 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 083bf20..8298465 100644 --- a/README.md +++ b/README.md @@ -352,6 +352,7 @@ examples is highly appreciated. [example]: https://gitlab.com/msrd0/gotham-restful/tree/master/example [gotham]: https://gotham.rs/ [serde_json]: https://github.com/serde-rs/json#serde-json---- + [`State`]: gotham::state::State ## Versioning diff --git a/derive/src/endpoint.rs b/derive/src/endpoint.rs index a363ec0..5078383 100644 --- a/derive/src/endpoint.rs +++ b/derive/src/endpoint.rs @@ -130,7 +130,7 @@ impl EndpointType { Self::ReadAll | Self::Search | Self::Create | Self::UpdateAll | Self::DeleteAll => { quote!(::gotham_restful::gotham::extractor::NoopPathExtractor) }, - Self::Read | Self::Update | Self::Delete => quote!(::gotham_restful::export::IdPlaceholder::<#arg_ty>), + Self::Read | Self::Update | Self::Delete => quote!(::gotham_restful::private::IdPlaceholder::<#arg_ty>), Self::Custom { .. } => { if self.has_placeholders().value { arg_ty.to_token_stream() @@ -490,7 +490,7 @@ fn expand_endpoint_type(mut ty: EndpointType, attrs: AttributeArgs, fun: &ItemFn let conn_ty = arg.ty.quote_ty(); state_block = quote! { #state_block - let repo = <::gotham_restful::export::Repo<#conn_ty>>::borrow_from(state).clone(); + let repo = <::gotham_restful::private::Repo<#conn_ty>>::borrow_from(state).clone(); }; handle_content = quote! { repo.run::<_, _, ()>(move |conn| { @@ -500,7 +500,7 @@ fn expand_endpoint_type(mut ty: EndpointType, attrs: AttributeArgs, fun: &ItemFn } Ok(quote! { - use ::gotham_restful::export::FutureExt as _; + use ::gotham_restful::private::FutureExt as _; use ::gotham_restful::gotham::state::FromState as _; #state_block async move { @@ -559,7 +559,7 @@ fn expand_endpoint_type(mut ty: EndpointType, attrs: AttributeArgs, fun: &ItemFn placeholders: Self::Placeholders, params: Self::Params, body: ::std::option::Option - ) -> ::gotham_restful::export::BoxFuture<'a, Self::Output> { + ) -> ::gotham_restful::private::BoxFuture<'a, Self::Output> { #handle_content } diff --git a/derive/src/openapi_type.rs b/derive/src/openapi_type.rs index ae14678..4b4530d 100644 --- a/derive/src/openapi_type.rs +++ b/derive/src/openapi_type.rs @@ -148,7 +148,7 @@ fn expand_enum(ident: Ident, generics: Generics, attrs: Vec, input: D { fn schema() -> #krate::OpenapiSchema { - use #krate::{export::openapi::*, OpenapiSchema}; + use #krate::{private::openapi::*, OpenapiSchema}; let mut enumeration : Vec = Vec::new(); @@ -261,7 +261,7 @@ fn expand_struct(ident: Ident, generics: Generics, attrs: Vec, input: { fn schema() -> #krate::OpenapiSchema { - use #krate::{export::{openapi::*, IndexMap}, OpenapiSchema}; + use #krate::{private::{openapi::*, IndexMap}, OpenapiSchema}; let mut properties : IndexMap>> = IndexMap::new(); let mut required : Vec = Vec::new(); diff --git a/derive/src/request_body.rs b/derive/src/request_body.rs index 077d105..9657b21 100644 --- a/derive/src/request_body.rs +++ b/derive/src/request_body.rs @@ -32,7 +32,7 @@ fn impl_openapi_type(ident: &Ident, generics: &Generics) -> TokenStream { { fn schema() -> #krate::OpenapiSchema { - use #krate::{export::openapi::*, OpenapiSchema}; + use #krate::{private::openapi::*, OpenapiSchema}; OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { format: VariantOrUnknownOrEmpty::Item(StringFormat::Binary), diff --git a/derive/src/resource_error.rs b/derive/src/resource_error.rs index a2b0955..9239c80 100644 --- a/derive/src/resource_error.rs +++ b/derive/src/resource_error.rs @@ -316,7 +316,7 @@ pub fn expand_resource_error(input: DeriveInput) -> Result { impl #generics #krate::IntoResponseError for #ident #generics where #( #were ),* { - type Err = #krate::export::serde_json::Error; + type Err = #krate::private::serde_json::Error; fn into_response_error(self) -> Result<#krate::Response, Self::Err> { diff --git a/src/auth.rs b/src/auth.rs index 935c7bd..fdbab63 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,4 +1,5 @@ -use crate::{AuthError, Forbidden, HeaderName}; +use crate::{AuthError, Forbidden}; + use cookie::CookieJar; use futures_util::{ future, @@ -7,7 +8,7 @@ use futures_util::{ use gotham::{ anyhow, handler::HandlerFuture, - hyper::header::{HeaderMap, AUTHORIZATION}, + hyper::header::{HeaderMap, HeaderName, AUTHORIZATION}, middleware::{cookie::CookieParser, Middleware, NewMiddleware}, state::{FromState, State} }; @@ -15,6 +16,7 @@ use jsonwebtoken::{errors::ErrorKind, DecodingKey}; use serde::de::DeserializeOwned; use std::{marker::PhantomData, panic::RefUnwindSafe, pin::Pin}; +#[doc(no_inline)] pub use jsonwebtoken::Validation as AuthValidation; /// The authentication status returned by the auth middleware for each request. @@ -77,7 +79,7 @@ This trait will help the auth middleware to determine the validity of an authent A very basic implementation could look like this: ``` -# use gotham_restful::{AuthHandler, State}; +# use gotham_restful::{AuthHandler, gotham::state::State}; # const SECRET : &'static [u8; 32] = b"zlBsA2QXnkmpe0QTh8uCvtAEa4j33YAc"; diff --git a/src/lib.rs b/src/lib.rs index 87cb87c..ac58c2e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,10 @@ #![warn(missing_debug_implementations, rust_2018_idioms)] -#![deny(broken_intra_doc_links)] #![forbid(unsafe_code)] // can we have a lint for spaces in doc comments please? #![cfg_attr(feature = "cargo-clippy", allow(clippy::tabs_in_doc_comments))] +// intra-doc links only fully work when OpenAPI is enabled +#![cfg_attr(feature = "openapi", deny(broken_intra_doc_links))] +#![cfg_attr(not(feature = "openapi"), allow(broken_intra_doc_links))] /*! This crate is an extension to the popular [gotham web framework][gotham] for Rust. It allows you to create resources with assigned endpoints that aim to be a more convenient way of creating handlers @@ -402,6 +404,7 @@ examples is highly appreciated. [example]: https://gitlab.com/msrd0/gotham-restful/tree/master/example [gotham]: https://gotham.rs/ [serde_json]: https://github.com/serde-rs/json#serde-json---- + [`State`]: gotham::state::State */ #[cfg(all(feature = "openapi", feature = "without-openapi"))] @@ -425,18 +428,13 @@ extern crate serde; #[doc(no_inline)] pub use gotham; #[doc(no_inline)] -pub use gotham::{ - hyper::{header::HeaderName, StatusCode}, - state::{FromState, State} -}; -#[doc(no_inline)] pub use mime::Mime; pub use gotham_restful_derive::*; /// Not public API #[doc(hidden)] -pub mod export { +pub mod private { pub use crate::routing::PathExtractor as IdPlaceholder; pub use futures_util::future::{BoxFuture, FutureExt}; diff --git a/src/openapi/handler.rs b/src/openapi/handler.rs index 6c321b8..fc32c11 100644 --- a/src/openapi/handler.rs +++ b/src/openapi/handler.rs @@ -1,9 +1,11 @@ use super::SECURITY_NAME; + use futures_util::{future, future::FutureExt}; use gotham::{ anyhow, handler::{Handler, HandlerFuture, NewHandler}, helpers::http::response::create_response, + hyper::StatusCode, state::State }; use indexmap::IndexMap; @@ -75,7 +77,7 @@ impl Handler for OpenapiHandler { Ok(openapi) => openapi, Err(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, StatusCode::INTERNAL_SERVER_ERROR, TEXT_PLAIN, ""); return future::ok((state, res)).boxed(); } }; @@ -88,12 +90,12 @@ impl Handler for OpenapiHandler { match serde_json::to_string(&openapi) { Ok(body) => { - let res = create_response(&state, crate::StatusCode::OK, APPLICATION_JSON, body); + let res = create_response(&state, StatusCode::OK, APPLICATION_JSON, body); future::ok((state, res)).boxed() }, Err(e) => { error!("Unable to handle OpenAPI request due to error: {}", e); - let res = create_response(&state, crate::StatusCode::INTERNAL_SERVER_ERROR, TEXT_PLAIN, ""); + let res = create_response(&state, StatusCode::INTERNAL_SERVER_ERROR, TEXT_PLAIN, ""); future::ok((state, res)).boxed() } } diff --git a/src/openapi/operation.rs b/src/openapi/operation.rs index 8c83af5..06e1ce7 100644 --- a/src/openapi/operation.rs +++ b/src/openapi/operation.rs @@ -78,7 +78,7 @@ impl OperationParams { pub struct OperationDescription { operation_id: Option, - default_status: crate::StatusCode, + default_status: gotham::hyper::StatusCode, accepted_types: Option>, schema: ReferenceOr, params: OperationParams, diff --git a/src/result/mod.rs b/src/result/mod.rs index d5895aa..deb9d12 100644 --- a/src/result/mod.rs +++ b/src/result/mod.rs @@ -1,7 +1,10 @@ #[cfg(feature = "openapi")] use crate::OpenapiSchema; use crate::Response; + use futures_util::future::FutureExt; +#[cfg(feature = "openapi")] +use gotham::hyper::StatusCode; use mime::{Mime, STAR_STAR}; use serde::Serialize; use std::{ @@ -57,8 +60,8 @@ pub trait ResourceResult { fn schema() -> OpenapiSchema; #[cfg(feature = "openapi")] - fn default_status() -> crate::StatusCode { - crate::StatusCode::OK + fn default_status() -> StatusCode { + StatusCode::OK } } @@ -141,7 +144,7 @@ where } #[cfg(feature = "openapi")] - fn default_status() -> crate::StatusCode { + fn default_status() -> StatusCode { Res::default_status() } } diff --git a/src/result/no_content.rs b/src/result/no_content.rs index 88d624e..30413fd 100644 --- a/src/result/no_content.rs +++ b/src/result/no_content.rs @@ -2,7 +2,10 @@ use super::{handle_error, ResourceResult}; use crate::{IntoResponseError, Response}; #[cfg(feature = "openapi")] use crate::{OpenapiSchema, OpenapiType}; + use futures_util::{future, future::FutureExt}; +#[cfg(feature = "openapi")] +use gotham::hyper::StatusCode; use mime::Mime; use std::{fmt::Display, future::Future, pin::Pin}; @@ -58,8 +61,8 @@ impl ResourceResult for NoContent { /// This will always be a _204 No Content_ #[cfg(feature = "openapi")] - fn default_status() -> crate::StatusCode { - crate::StatusCode::NO_CONTENT + fn default_status() -> StatusCode { + StatusCode::NO_CONTENT } } @@ -86,7 +89,7 @@ where } #[cfg(feature = "openapi")] - fn default_status() -> crate::StatusCode { + fn default_status() -> StatusCode { NoContent::default_status() } } diff --git a/src/result/raw.rs b/src/result/raw.rs index de0f82e..fe35143 100644 --- a/src/result/raw.rs +++ b/src/result/raw.rs @@ -1,10 +1,14 @@ use super::{handle_error, IntoResponseError, ResourceResult}; #[cfg(feature = "openapi")] use crate::OpenapiSchema; -use crate::{FromBody, RequestBody, ResourceType, Response, StatusCode}; +use crate::{FromBody, RequestBody, ResourceType, Response}; + use futures_core::future::Future; use futures_util::{future, future::FutureExt}; -use gotham::hyper::body::{Body, Bytes}; +use gotham::hyper::{ + body::{Body, Bytes}, + StatusCode +}; use mime::Mime; #[cfg(feature = "openapi")] use openapiv3::{SchemaKind, StringFormat, StringType, Type, VariantOrUnknownOrEmpty}; diff --git a/src/result/result.rs b/src/result/result.rs index f22d756..2b8afe6 100644 --- a/src/result/result.rs +++ b/src/result/result.rs @@ -1,8 +1,10 @@ use super::{handle_error, into_response_helper, ResourceResult}; #[cfg(feature = "openapi")] use crate::OpenapiSchema; -use crate::{result::ResourceError, Response, ResponseBody, StatusCode}; +use crate::{result::ResourceError, Response, ResponseBody}; + use futures_core::future::Future; +use gotham::hyper::StatusCode; use mime::{Mime, APPLICATION_JSON}; use std::{error::Error, fmt::Display, pin::Pin}; diff --git a/src/routing.rs b/src/routing.rs index afbd857..f20bbf7 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -5,13 +5,13 @@ use crate::openapi::{ }; use crate::{ result::{ResourceError, ResourceResult}, - Endpoint, FromBody, Resource, Response, StatusCode + Endpoint, FromBody, Resource, Response }; use gotham::{ handler::HandlerError, helpers::http::response::{create_empty_response, create_response}, - hyper::{body::to_bytes, header::CONTENT_TYPE, Body, HeaderMap, Method}, + hyper::{body::to_bytes, header::CONTENT_TYPE, Body, HeaderMap, Method, StatusCode}, pipeline::chain::PipelineHandleChain, router::{ builder::{DefineSingleRoute, DrawRoutes, RouterBuilder, ScopeBuilder}, diff --git a/tests/async_methods.rs b/tests/async_methods.rs index 21c3462..35ec42a 100644 --- a/tests/async_methods.rs +++ b/tests/async_methods.rs @@ -4,6 +4,7 @@ extern crate gotham_derive; use gotham::{ hyper::{HeaderMap, Method}, router::builder::*, + state::State, test::TestServer }; use gotham_restful::*; diff --git a/tests/ui/endpoint/async_state.rs b/tests/ui/endpoint/async_state.rs index 85a23a4..d951370 100644 --- a/tests/ui/endpoint/async_state.rs +++ b/tests/ui/endpoint/async_state.rs @@ -1,6 +1,6 @@ #[macro_use] extern crate gotham_restful; -use gotham_restful::State; +use gotham::state::State; #[derive(Resource)] #[resource(read_all)] From 44e2f0317c989db1826d94cf9caa669047ef20da Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 3 Feb 2021 22:24:05 +0100 Subject: [PATCH 137/170] ci: use less broken docker image for rustfmt --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 18df473..eda1b34 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -81,7 +81,7 @@ readme: rustfmt: stage: test - image: rustlang/rust:nightly-slim + image: iamsauravsharma/rust-fmt:nightly-alpine before_script: - cargo -V - cargo fmt --version From 9e65540cd840235dae17d2d076e440f84905db8a Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 3 Feb 2021 22:37:34 +0100 Subject: [PATCH 138/170] ci: make the rustfmt.sh script ash-compatible --- tests/ui/rustfmt.sh | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/ui/rustfmt.sh b/tests/ui/rustfmt.sh index c33dedf..b5724e4 100755 --- a/tests/ui/rustfmt.sh +++ b/tests/ui/rustfmt.sh @@ -1,11 +1,17 @@ -#!/bin/bash +#!/bin/busybox ash set -euo pipefail rustfmt=${RUSTFMT:-rustfmt} version="$($rustfmt -V)" -if [[ $version != *nightly* ]]; then - rustfmt="$rustfmt +nightly" -fi +case "$version" in + *nightly*) + # all good, no additional flags required + ;; + *) + # assume we're using some sort of rustup setup + rustfmt="$rustfmt +nightly" + ;; +esac return=0 find "$(dirname "$0")" -name '*.rs' -type f | while read file; do From 8b7370140582564d950758aca3aa95e0228ca640 Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 3 Feb 2021 22:58:08 +0100 Subject: [PATCH 139/170] replace some `std::error::Error` bounds with `Into` --- CHANGELOG.md | 1 + src/openapi/handler.rs | 4 ++-- src/result/auth_result.rs | 4 ++-- src/result/mod.rs | 4 ++-- src/result/redirect.rs | 8 ++++---- src/result/result.rs | 2 +- src/routing.rs | 14 +++++++++----- 7 files changed, 21 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b9fe8b..369a09b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support custom HTTP response headers - New `endpoint` router extension with associated `Endpoint` trait ([!18]) - Support for custom endpoints using the `#[endpoint]` macro ([!19]) + - Support for `anyhow::Error` (or any type implementing `Into`) in most responses ### Changed - The cors handler can now copy headers from the request if desired diff --git a/src/openapi/handler.rs b/src/openapi/handler.rs index fc32c11..f340a6d 100644 --- a/src/openapi/handler.rs +++ b/src/openapi/handler.rs @@ -1,5 +1,5 @@ +#![cfg_attr(not(feature = "auth"), allow(unused_imports))] use super::SECURITY_NAME; - use futures_util::{future, future::FutureExt}; use gotham::{ anyhow, @@ -67,7 +67,7 @@ fn get_security(state: &mut State) -> IndexMap (Vec, IndexMap>) { +fn get_security(_state: &mut State) -> IndexMap> { Default::default() } diff --git a/src/result/auth_result.rs b/src/result/auth_result.rs index 90e564e..1b1fd7f 100644 --- a/src/result/auth_result.rs +++ b/src/result/auth_result.rs @@ -70,9 +70,9 @@ impl From for AuthErrorOrOther { } mod private { - use gotham::anyhow; + use gotham::handler::HandlerError; pub trait Sealed {} - impl> Sealed for E {} + impl> Sealed for E {} } impl From for AuthErrorOrOther diff --git a/src/result/mod.rs b/src/result/mod.rs index deb9d12..6d699bf 100644 --- a/src/result/mod.rs +++ b/src/result/mod.rs @@ -3,12 +3,12 @@ use crate::OpenapiSchema; use crate::Response; use futures_util::future::FutureExt; +use gotham::handler::HandlerError; #[cfg(feature = "openapi")] use gotham::hyper::StatusCode; use mime::{Mime, STAR_STAR}; use serde::Serialize; use std::{ - error::Error, fmt::{Debug, Display}, future::Future, pin::Pin @@ -45,7 +45,7 @@ impl OrAllTypes for Option> { /// A trait provided to convert a resource's result to json. pub trait ResourceResult { - type Err: Error + Send + Sync + 'static; + type Err: Into + Send + Sync + 'static; /// Turn this into a response that can be returned to the browser. This api will likely /// change in the future. diff --git a/src/result/redirect.rs b/src/result/redirect.rs index 5754adc..432203d 100644 --- a/src/result/redirect.rs +++ b/src/result/redirect.rs @@ -1,7 +1,7 @@ -use super::{handle_error, NoContent, ResourceResult}; -#[cfg(feature = "openapi")] -use crate::OpenapiSchema; +use super::{handle_error, ResourceResult}; use crate::{IntoResponseError, Response}; +#[cfg(feature = "openapi")] +use crate::{NoContent, OpenapiSchema}; use futures_util::future::{BoxFuture, FutureExt, TryFutureExt}; use gotham::hyper::{ header::{InvalidHeaderValue, LOCATION}, @@ -78,7 +78,7 @@ pub enum RedirectError { impl ResourceResult for Result where E: Display + IntoResponseError, - ::Err: Sync + ::Err: StdError + Sync { type Err = RedirectError<::Err>; diff --git a/src/result/result.rs b/src/result/result.rs index 2b8afe6..71c44b7 100644 --- a/src/result/result.rs +++ b/src/result/result.rs @@ -9,7 +9,7 @@ use mime::{Mime, APPLICATION_JSON}; use std::{error::Error, fmt::Display, pin::Pin}; pub trait IntoResponseError { - type Err: Error + Send + 'static; + type Err: Display + Send + 'static; fn into_response_error(self) -> Result; } diff --git a/src/routing.rs b/src/routing.rs index f20bbf7..132eed6 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -8,6 +8,8 @@ use crate::{ Endpoint, FromBody, Resource, Response }; +#[cfg(feature = "cors")] +use gotham::router::route::matcher::AccessControlRequestMethodMatcher; use gotham::{ handler::HandlerError, helpers::http::response::{create_empty_response, create_response}, @@ -16,9 +18,7 @@ use gotham::{ router::{ builder::{DefineSingleRoute, DrawRoutes, RouterBuilder, ScopeBuilder}, non_match::RouteNonMatch, - route::matcher::{ - AcceptHeaderRouteMatcher, AccessControlRequestMethodMatcher, ContentTypeHeaderRouteMatcher, RouteMatcher - } + route::matcher::{AcceptHeaderRouteMatcher, ContentTypeHeaderRouteMatcher, RouteMatcher} }, state::{FromState, State} }; @@ -87,7 +87,11 @@ fn response_from(res: Response, state: &State) -> gotham::hyper::Response r } -async fn endpoint_handler(state: &mut State) -> Result, HandlerError> { +async fn endpoint_handler(state: &mut State) -> Result, HandlerError> +where + E: Endpoint, + ::Err: Into +{ trace!("entering endpoint_handler"); let placeholders = E::Placeholders::take_from(state); let params = E::Params::take_from(state); @@ -120,7 +124,7 @@ async fn endpoint_handler(state: &mut State) -> Result Date: Thu, 4 Feb 2021 00:12:17 +0100 Subject: [PATCH 140/170] bump MSRV to 1.49+ not sure why but all older rust versions fail generic type resolution --- .gitlab-ci.yml | 10 +++++----- README.md | 4 ++-- README.tpl | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index eda1b34..ab2e9d0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -10,20 +10,20 @@ variables: test-default: stage: test - image: rust:1.43-slim + image: rust:1.49-slim before_script: - cargo -V script: - cargo test cache: - key: cargo-1-43-default + key: cargo-1-49-default paths: - cargo/ - target/ test-full: stage: test - image: rust:1.43-slim + image: rust:1.49-slim before_script: - apt update -y - apt install -y --no-install-recommends libpq-dev @@ -31,7 +31,7 @@ test-full: script: - cargo test --no-default-features --features full cache: - key: cargo-1-43-all + key: cargo-1-49-all paths: - cargo/ - target/ @@ -67,7 +67,7 @@ test-trybuild-ui: script: - cargo test --no-default-features --features full --tests -- --ignored cache: - key: cargo-1-48-all + key: cargo-1-49-all paths: - cargo/ - target/ diff --git a/README.md b/README.md index 8298465..028feea 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,8 @@ rustdoc - - Minimum Rust Version + + Minimum Rust Version dependencies diff --git a/README.tpl b/README.tpl index 36373ae..bd3e252 100644 --- a/README.tpl +++ b/README.tpl @@ -17,8 +17,8 @@ rustdoc - - Minimum Rust Version + + Minimum Rust Version dependencies From 3b95b9f4953d01844a6a1852c8b78fe4e84f3f1f Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 17 Feb 2021 03:39:49 +0100 Subject: [PATCH 141/170] update trybuild rust version and rustfmt --- .gitlab-ci.yml | 11 ++++++++--- rustfmt.toml | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ab2e9d0..01a97f6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -59,7 +59,7 @@ test-tarpaulin: test-trybuild-ui: stage: test - image: rust:1.49-slim + image: rust:1.50-slim before_script: - apt update -y - apt install -y --no-install-recommends libpq-dev @@ -67,7 +67,7 @@ test-trybuild-ui: script: - cargo test --no-default-features --features full --tests -- --ignored cache: - key: cargo-1-49-all + key: cargo-1-50-all paths: - cargo/ - target/ @@ -81,8 +81,13 @@ readme: rustfmt: stage: test - image: iamsauravsharma/rust-fmt:nightly-alpine + image: + name: alpine:3.13 before_script: + - apk add rustup + - rustup-init -qy --default-host x86_64-unknown-linux-musl --default-toolchain none Date: Wed, 17 Feb 2021 04:02:54 +0100 Subject: [PATCH 142/170] ci: update cargo-readme --- .gitlab-ci.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 01a97f6..7787130 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -74,9 +74,11 @@ test-trybuild-ui: readme: stage: test - image: msrd0/cargo-readme + image: ghcr.io/msrd0/cargo-readme + before_script: + - cargo readme -V script: - - cargo readme -t README.tpl >README.md.new + - cargo readme -t README.tpl -o README.md.new - diff README.md README.md.new rustfmt: From 30edd349edb5aad81a3eae18eb76c89165e15548 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 21 Feb 2021 18:06:50 +0100 Subject: [PATCH 143/170] improve ui on invalid types for endpoints --- derive/src/endpoint.rs | 14 +++++--- tests/ui/endpoint/invalid_body_ty.rs | 19 +++++++++++ tests/ui/endpoint/invalid_body_ty.stderr | 13 ++++++++ tests/ui/endpoint/invalid_params_ty.rs | 19 +++++++++++ tests/ui/endpoint/invalid_params_ty.stderr | 32 +++++++++++++++++++ tests/ui/endpoint/invalid_placeholders_ty.rs | 19 +++++++++++ .../endpoint/invalid_placeholders_ty.stderr | 32 +++++++++++++++++++ tests/ui/endpoint/invalid_return_type.rs | 16 ++++++++++ tests/ui/endpoint/invalid_return_type.stderr | 10 ++++++ 9 files changed, 169 insertions(+), 5 deletions(-) create mode 100644 tests/ui/endpoint/invalid_body_ty.rs create mode 100644 tests/ui/endpoint/invalid_body_ty.stderr create mode 100644 tests/ui/endpoint/invalid_params_ty.rs create mode 100644 tests/ui/endpoint/invalid_params_ty.stderr create mode 100644 tests/ui/endpoint/invalid_placeholders_ty.rs create mode 100644 tests/ui/endpoint/invalid_placeholders_ty.stderr create mode 100644 tests/ui/endpoint/invalid_return_type.rs create mode 100644 tests/ui/endpoint/invalid_return_type.stderr diff --git a/derive/src/endpoint.rs b/derive/src/endpoint.rs index 5078383..aac3757 100644 --- a/derive/src/endpoint.rs +++ b/derive/src/endpoint.rs @@ -2,7 +2,7 @@ use crate::util::{CollectToResult, ExpectLit, PathEndsWith}; use once_cell::sync::Lazy; use paste::paste; use proc_macro2::{Ident, Span, TokenStream}; -use quote::{format_ident, quote, ToTokens}; +use quote::{format_ident, quote, quote_spanned, ToTokens}; use regex::Regex; use std::str::FromStr; use syn::{ @@ -405,6 +405,7 @@ fn expand_endpoint_type(mut ty: EndpointType, attrs: AttributeArgs, fun: &ItemFn ReturnType::Default => (quote!(::gotham_restful::NoContent), true), ReturnType::Type(_, ty) => (quote!(#ty), false) }; + let output_typedef = quote_spanned!(output_ty.span() => type Output = #output_ty;); let arg_tys = args.iter().filter(|arg| arg.ty.is_method_arg()).collect::>(); let mut arg_ty_idx = 0; @@ -434,10 +435,13 @@ fn expand_endpoint_type(mut ty: EndpointType, attrs: AttributeArgs, fun: &ItemFn })?; let has_placeholders = ty.has_placeholders(); let placeholder_ty = ty.placeholders_ty(next_arg_ty(!has_placeholders.value)?); + let placeholder_typedef = quote_spanned!(placeholder_ty.span() => type Placeholders = #placeholder_ty;); let needs_params = ty.needs_params(); let params_ty = ty.params_ty(next_arg_ty(!needs_params.value)?); + let params_typedef = quote_spanned!(params_ty.span() => type Params = #params_ty;); let needs_body = ty.needs_body(); let body_ty = ty.body_ty(next_arg_ty(!needs_body.value)?); + let body_typedef = quote_spanned!(body_ty.span() => type Body = #body_ty;); if arg_ty_idx < arg_tys.len() { return Err(Error::new(fun_ident.span(), "Too many arguments")); @@ -537,22 +541,22 @@ fn expand_endpoint_type(mut ty: EndpointType, attrs: AttributeArgs, fun: &ItemFn { #uri }.into() } - type Output = #output_ty; + #output_typedef fn has_placeholders() -> bool { #has_placeholders } - type Placeholders = #placeholder_ty; + #placeholder_typedef fn needs_params() -> bool { #needs_params } - type Params = #params_ty; + #params_typedef fn needs_body() -> bool { #needs_body } - type Body = #body_ty; + #body_typedef fn handle<'a>( state: &'a mut ::gotham_restful::gotham::state::State, diff --git a/tests/ui/endpoint/invalid_body_ty.rs b/tests/ui/endpoint/invalid_body_ty.rs new file mode 100644 index 0000000..a3ab10d --- /dev/null +++ b/tests/ui/endpoint/invalid_body_ty.rs @@ -0,0 +1,19 @@ +#[macro_use] +extern crate gotham_restful; +use gotham_restful::gotham::hyper::Method; + +#[derive(Resource)] +#[resource(endpoint)] +struct FooResource; + +#[derive(Debug)] +struct FooBody { + foo: String +} + +#[endpoint(method = "Method::GET", uri = "", body = true)] +fn endpoint(_: FooBody) { + unimplemented!() +} + +fn main() {} diff --git a/tests/ui/endpoint/invalid_body_ty.stderr b/tests/ui/endpoint/invalid_body_ty.stderr new file mode 100644 index 0000000..e042ed3 --- /dev/null +++ b/tests/ui/endpoint/invalid_body_ty.stderr @@ -0,0 +1,13 @@ +error[E0277]: the trait bound `for<'de> FooBody: serde::de::Deserialize<'de>` is not satisfied + --> $DIR/invalid_body_ty.rs:15:16 + | +15 | fn endpoint(_: FooBody) { + | ^^^^^^^ the trait `for<'de> serde::de::Deserialize<'de>` is not implemented for `FooBody` + | + ::: $WORKSPACE/src/endpoint.rs + | + | type Body: RequestBody + Send; + | ----------- required by this bound in `gotham_restful::Endpoint::Body` + | + = note: required because of the requirements on the impl of `serde::de::DeserializeOwned` for `FooBody` + = note: required because of the requirements on the impl of `RequestBody` for `FooBody` diff --git a/tests/ui/endpoint/invalid_params_ty.rs b/tests/ui/endpoint/invalid_params_ty.rs new file mode 100644 index 0000000..e08ff91 --- /dev/null +++ b/tests/ui/endpoint/invalid_params_ty.rs @@ -0,0 +1,19 @@ +#[macro_use] +extern crate gotham_restful; +use gotham_restful::gotham::hyper::Method; + +#[derive(Resource)] +#[resource(endpoint)] +struct FooResource; + +#[derive(Debug)] +struct FooParams { + foo: String +} + +#[endpoint(method = "Method::GET", uri = "", params = true)] +fn endpoint(_: FooParams) { + unimplemented!() +} + +fn main() {} diff --git a/tests/ui/endpoint/invalid_params_ty.stderr b/tests/ui/endpoint/invalid_params_ty.stderr new file mode 100644 index 0000000..8f7cdb8 --- /dev/null +++ b/tests/ui/endpoint/invalid_params_ty.stderr @@ -0,0 +1,32 @@ +error[E0277]: the trait bound `for<'de> FooParams: serde::de::Deserialize<'de>` is not satisfied + --> $DIR/invalid_params_ty.rs:15:16 + | +15 | fn endpoint(_: FooParams) { + | ^^^^^^^^^ the trait `for<'de> serde::de::Deserialize<'de>` is not implemented for `FooParams` + | + ::: $WORKSPACE/src/endpoint.rs + | + | type Params: QueryStringExtractor + Sync; + | -------------------------- required by this bound in `gotham_restful::Endpoint::Params` + +error[E0277]: the trait bound `FooParams: StateData` is not satisfied + --> $DIR/invalid_params_ty.rs:15:16 + | +15 | fn endpoint(_: FooParams) { + | ^^^^^^^^^ the trait `StateData` is not implemented for `FooParams` + | + ::: $WORKSPACE/src/endpoint.rs + | + | type Params: QueryStringExtractor + Sync; + | -------------------------- required by this bound in `gotham_restful::Endpoint::Params` + +error[E0277]: the trait bound `FooParams: StaticResponseExtender` is not satisfied + --> $DIR/invalid_params_ty.rs:15:16 + | +15 | fn endpoint(_: FooParams) { + | ^^^^^^^^^ the trait `StaticResponseExtender` is not implemented for `FooParams` + | + ::: $WORKSPACE/src/endpoint.rs + | + | type Params: QueryStringExtractor + Sync; + | -------------------------- required by this bound in `gotham_restful::Endpoint::Params` diff --git a/tests/ui/endpoint/invalid_placeholders_ty.rs b/tests/ui/endpoint/invalid_placeholders_ty.rs new file mode 100644 index 0000000..3ca1d10 --- /dev/null +++ b/tests/ui/endpoint/invalid_placeholders_ty.rs @@ -0,0 +1,19 @@ +#[macro_use] +extern crate gotham_restful; +use gotham_restful::gotham::hyper::Method; + +#[derive(Resource)] +#[resource(endpoint)] +struct FooResource; + +#[derive(Debug)] +struct FooPlaceholders { + foo: String +} + +#[endpoint(method = "Method::GET", uri = ":foo")] +fn endpoint(_: FooPlaceholders) { + unimplemented!() +} + +fn main() {} diff --git a/tests/ui/endpoint/invalid_placeholders_ty.stderr b/tests/ui/endpoint/invalid_placeholders_ty.stderr new file mode 100644 index 0000000..6810ef5 --- /dev/null +++ b/tests/ui/endpoint/invalid_placeholders_ty.stderr @@ -0,0 +1,32 @@ +error[E0277]: the trait bound `for<'de> FooPlaceholders: serde::de::Deserialize<'de>` is not satisfied + --> $DIR/invalid_placeholders_ty.rs:15:16 + | +15 | fn endpoint(_: FooPlaceholders) { + | ^^^^^^^^^^^^^^^ the trait `for<'de> serde::de::Deserialize<'de>` is not implemented for `FooPlaceholders` + | + ::: $WORKSPACE/src/endpoint.rs + | + | type Placeholders: PathExtractor + Sync; + | ------------------- required by this bound in `gotham_restful::Endpoint::Placeholders` + +error[E0277]: the trait bound `FooPlaceholders: StateData` is not satisfied + --> $DIR/invalid_placeholders_ty.rs:15:16 + | +15 | fn endpoint(_: FooPlaceholders) { + | ^^^^^^^^^^^^^^^ the trait `StateData` is not implemented for `FooPlaceholders` + | + ::: $WORKSPACE/src/endpoint.rs + | + | type Placeholders: PathExtractor + Sync; + | ------------------- required by this bound in `gotham_restful::Endpoint::Placeholders` + +error[E0277]: the trait bound `FooPlaceholders: StaticResponseExtender` is not satisfied + --> $DIR/invalid_placeholders_ty.rs:15:16 + | +15 | fn endpoint(_: FooPlaceholders) { + | ^^^^^^^^^^^^^^^ the trait `StaticResponseExtender` is not implemented for `FooPlaceholders` + | + ::: $WORKSPACE/src/endpoint.rs + | + | type Placeholders: PathExtractor + Sync; + | ------------------- required by this bound in `gotham_restful::Endpoint::Placeholders` diff --git a/tests/ui/endpoint/invalid_return_type.rs b/tests/ui/endpoint/invalid_return_type.rs new file mode 100644 index 0000000..feeaf98 --- /dev/null +++ b/tests/ui/endpoint/invalid_return_type.rs @@ -0,0 +1,16 @@ +#[macro_use] +extern crate gotham_restful; +use gotham_restful::gotham::hyper::Method; + +#[derive(Resource)] +#[resource(endpoint)] +struct FooResource; + +struct FooResponse; + +#[endpoint(method = "Method::GET", uri = "")] +fn endpoint() -> FooResponse { + unimplemented!() +} + +fn main() {} diff --git a/tests/ui/endpoint/invalid_return_type.stderr b/tests/ui/endpoint/invalid_return_type.stderr new file mode 100644 index 0000000..1660b14 --- /dev/null +++ b/tests/ui/endpoint/invalid_return_type.stderr @@ -0,0 +1,10 @@ +error[E0277]: the trait bound `FooResponse: ResourceResult` is not satisfied + --> $DIR/invalid_return_type.rs:12:18 + | +12 | fn endpoint() -> FooResponse { + | ^^^^^^^^^^^ the trait `ResourceResult` is not implemented for `FooResponse` + | + ::: $WORKSPACE/src/endpoint.rs + | + | type Output: ResourceResult + Send; + | -------------- required by this bound in `gotham_restful::Endpoint::Output` From c640efcb888575c807bf8de795b8bea855277961 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 21 Feb 2021 18:31:44 +0100 Subject: [PATCH 144/170] fix ui when openapi is enabled --- src/openapi/builder.rs | 8 ++++---- src/openapi/operation.rs | 5 ++--- src/openapi/router.rs | 6 +++--- src/result/mod.rs | 7 ------- src/result/raw.rs | 19 +++++++++++++------ tests/ui/endpoint/invalid_body_ty.stderr | 15 +++++++++++++-- tests/ui/endpoint/invalid_params_ty.stderr | 17 ++++++++++++++--- .../endpoint/invalid_placeholders_ty.stderr | 17 ++++++++++++++--- tests/ui/endpoint/invalid_return_type.stderr | 2 +- 9 files changed, 64 insertions(+), 32 deletions(-) diff --git a/src/openapi/builder.rs b/src/openapi/builder.rs index f582015..11f79f8 100644 --- a/src/openapi/builder.rs +++ b/src/openapi/builder.rs @@ -1,4 +1,4 @@ -use crate::{OpenapiSchema, OpenapiType}; +use crate::OpenapiSchema; use indexmap::IndexMap; use openapiv3::{ Components, OpenAPI, PathItem, ReferenceOr, @@ -83,8 +83,7 @@ impl OpenapiBuilder { } } - pub fn add_schema(&mut self) -> ReferenceOr { - let mut schema = T::schema(); + pub fn add_schema(&mut self, mut schema: OpenapiSchema) -> ReferenceOr { match schema.name.clone() { Some(name) => { let reference = Reference { @@ -105,6 +104,7 @@ impl OpenapiBuilder { #[allow(dead_code)] mod test { use super::*; + use crate::OpenapiType; #[derive(OpenapiType)] struct Message { @@ -142,7 +142,7 @@ mod test { #[test] fn add_schema() { let mut builder = OpenapiBuilder::new(info()); - builder.add_schema::>(); + builder.add_schema(>::schema()); let openapi = openapi(builder); assert_eq!( diff --git a/src/openapi/operation.rs b/src/openapi/operation.rs index 06e1ce7..103c46c 100644 --- a/src/openapi/operation.rs +++ b/src/openapi/operation.rs @@ -184,12 +184,11 @@ impl OperationDescription { #[cfg(test)] mod test { use super::*; - use crate::{OpenapiType, ResourceResult}; #[test] fn no_content_schema_to_content() { let types = NoContent::accepted_types(); - let schema = ::schema(); + let schema = ::schema(); let content = OperationDescription::schema_to_content(types.or_all_types(), Item(schema.into_schema())); assert!(content.is_empty()); } @@ -197,7 +196,7 @@ mod test { #[test] fn raw_schema_to_content() { let types = Raw::<&str>::accepted_types(); - let schema = as OpenapiType>::schema(); + let schema = as ResourceResult>::schema(); let content = OperationDescription::schema_to_content(types.or_all_types(), Item(schema.into_schema())); assert_eq!(content.len(), 1); let json = serde_json::to_string(&content.values().nth(0).unwrap()).unwrap(); diff --git a/src/openapi/router.rs b/src/openapi/router.rs index 7ae71c6..5a13302 100644 --- a/src/openapi/router.rs +++ b/src/openapi/router.rs @@ -1,5 +1,5 @@ use super::{builder::OpenapiBuilder, handler::OpenapiHandler, operation::OperationDescription}; -use crate::{routing::*, EndpointWithSchema, OpenapiType, ResourceWithSchema}; +use crate::{routing::*, EndpointWithSchema, OpenapiType, ResourceResult, ResourceWithSchema}; use gotham::{hyper::Method, pipeline::chain::PipelineHandleChain, router::builder::*}; use once_cell::sync::Lazy; use regex::{Captures, Regex}; @@ -69,7 +69,7 @@ macro_rules! implOpenapiRouter { P: RefUnwindSafe + Send + Sync + 'static { fn endpoint(&mut self) { - let schema = (self.0).openapi_builder.add_schema::(); + let schema = (self.0).openapi_builder.add_schema(E::Output::schema()); let mut descr = OperationDescription::new::(schema); if E::has_placeholders() { descr.set_path_params(E::Placeholders::schema()); @@ -78,7 +78,7 @@ macro_rules! implOpenapiRouter { descr.set_query_params(E::Params::schema()); } if E::needs_body() { - let body_schema = (self.0).openapi_builder.add_schema::(); + let body_schema = (self.0).openapi_builder.add_schema(E::Body::schema()); descr.set_body::(body_schema); } diff --git a/src/result/mod.rs b/src/result/mod.rs index 6d699bf..87967aa 100644 --- a/src/result/mod.rs +++ b/src/result/mod.rs @@ -65,13 +65,6 @@ pub trait ResourceResult { } } -#[cfg(feature = "openapi")] -impl crate::OpenapiType for Res { - fn schema() -> OpenapiSchema { - Self::schema() - } -} - /// The default json returned on an 500 Internal Server Error. #[derive(Debug, Serialize)] pub(crate) struct ResourceError { diff --git a/src/result/raw.rs b/src/result/raw.rs index fe35143..cc30a55 100644 --- a/src/result/raw.rs +++ b/src/result/raw.rs @@ -1,7 +1,7 @@ use super::{handle_error, IntoResponseError, ResourceResult}; -#[cfg(feature = "openapi")] -use crate::OpenapiSchema; use crate::{FromBody, RequestBody, ResourceType, Response}; +#[cfg(feature = "openapi")] +use crate::{OpenapiSchema, OpenapiType}; use futures_core::future::Future; use futures_util::{future, future::FutureExt}; @@ -89,6 +89,16 @@ impl From<&'a [u8]>> FromBody for Raw { impl RequestBody for Raw where Raw: FromBody + ResourceType {} +#[cfg(feature = "openapi")] +impl OpenapiType for Raw { + fn schema() -> OpenapiSchema { + OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { + format: VariantOrUnknownOrEmpty::Item(StringFormat::Binary), + ..Default::default() + }))) + } +} + impl> ResourceResult for Raw where Self: Send @@ -101,10 +111,7 @@ where #[cfg(feature = "openapi")] fn schema() -> OpenapiSchema { - OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { - format: VariantOrUnknownOrEmpty::Item(StringFormat::Binary), - ..Default::default() - }))) + ::schema() } } diff --git a/tests/ui/endpoint/invalid_body_ty.stderr b/tests/ui/endpoint/invalid_body_ty.stderr index e042ed3..5a259e2 100644 --- a/tests/ui/endpoint/invalid_body_ty.stderr +++ b/tests/ui/endpoint/invalid_body_ty.stderr @@ -1,3 +1,14 @@ +error[E0277]: the trait bound `FooBody: OpenapiType` is not satisfied + --> $DIR/invalid_body_ty.rs:15:16 + | +15 | fn endpoint(_: FooBody) { + | ^^^^^^^ the trait `OpenapiType` is not implemented for `FooBody` + | + ::: $WORKSPACE/src/endpoint.rs + | + | type Body: RequestBody + Send; + | ----------- required by this bound in `gotham_restful::EndpointWithSchema::Body` + error[E0277]: the trait bound `for<'de> FooBody: serde::de::Deserialize<'de>` is not satisfied --> $DIR/invalid_body_ty.rs:15:16 | @@ -7,7 +18,7 @@ error[E0277]: the trait bound `for<'de> FooBody: serde::de::Deserialize<'de>` is ::: $WORKSPACE/src/endpoint.rs | | type Body: RequestBody + Send; - | ----------- required by this bound in `gotham_restful::Endpoint::Body` + | ----------- required by this bound in `gotham_restful::EndpointWithSchema::Body` | = note: required because of the requirements on the impl of `serde::de::DeserializeOwned` for `FooBody` - = note: required because of the requirements on the impl of `RequestBody` for `FooBody` + = note: required because of the requirements on the impl of `gotham_restful::RequestBody` for `FooBody` diff --git a/tests/ui/endpoint/invalid_params_ty.stderr b/tests/ui/endpoint/invalid_params_ty.stderr index 8f7cdb8..35ed700 100644 --- a/tests/ui/endpoint/invalid_params_ty.stderr +++ b/tests/ui/endpoint/invalid_params_ty.stderr @@ -7,7 +7,7 @@ error[E0277]: the trait bound `for<'de> FooParams: serde::de::Deserialize<'de>` ::: $WORKSPACE/src/endpoint.rs | | type Params: QueryStringExtractor + Sync; - | -------------------------- required by this bound in `gotham_restful::Endpoint::Params` + | -------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Params` error[E0277]: the trait bound `FooParams: StateData` is not satisfied --> $DIR/invalid_params_ty.rs:15:16 @@ -18,7 +18,7 @@ error[E0277]: the trait bound `FooParams: StateData` is not satisfied ::: $WORKSPACE/src/endpoint.rs | | type Params: QueryStringExtractor + Sync; - | -------------------------- required by this bound in `gotham_restful::Endpoint::Params` + | -------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Params` error[E0277]: the trait bound `FooParams: StaticResponseExtender` is not satisfied --> $DIR/invalid_params_ty.rs:15:16 @@ -29,4 +29,15 @@ error[E0277]: the trait bound `FooParams: StaticResponseExtender` is not satisfi ::: $WORKSPACE/src/endpoint.rs | | type Params: QueryStringExtractor + Sync; - | -------------------------- required by this bound in `gotham_restful::Endpoint::Params` + | -------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Params` + +error[E0277]: the trait bound `FooParams: OpenapiType` is not satisfied + --> $DIR/invalid_params_ty.rs:15:16 + | +15 | fn endpoint(_: FooParams) { + | ^^^^^^^^^ the trait `OpenapiType` is not implemented for `FooParams` + | + ::: $WORKSPACE/src/endpoint.rs + | + | #[openapi_bound("Params: crate::OpenapiType")] + | ---------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Params` diff --git a/tests/ui/endpoint/invalid_placeholders_ty.stderr b/tests/ui/endpoint/invalid_placeholders_ty.stderr index 6810ef5..09c9bbb 100644 --- a/tests/ui/endpoint/invalid_placeholders_ty.stderr +++ b/tests/ui/endpoint/invalid_placeholders_ty.stderr @@ -7,7 +7,7 @@ error[E0277]: the trait bound `for<'de> FooPlaceholders: serde::de::Deserialize< ::: $WORKSPACE/src/endpoint.rs | | type Placeholders: PathExtractor + Sync; - | ------------------- required by this bound in `gotham_restful::Endpoint::Placeholders` + | ------------------- required by this bound in `gotham_restful::EndpointWithSchema::Placeholders` error[E0277]: the trait bound `FooPlaceholders: StateData` is not satisfied --> $DIR/invalid_placeholders_ty.rs:15:16 @@ -18,7 +18,7 @@ error[E0277]: the trait bound `FooPlaceholders: StateData` is not satisfied ::: $WORKSPACE/src/endpoint.rs | | type Placeholders: PathExtractor + Sync; - | ------------------- required by this bound in `gotham_restful::Endpoint::Placeholders` + | ------------------- required by this bound in `gotham_restful::EndpointWithSchema::Placeholders` error[E0277]: the trait bound `FooPlaceholders: StaticResponseExtender` is not satisfied --> $DIR/invalid_placeholders_ty.rs:15:16 @@ -29,4 +29,15 @@ error[E0277]: the trait bound `FooPlaceholders: StaticResponseExtender` is not s ::: $WORKSPACE/src/endpoint.rs | | type Placeholders: PathExtractor + Sync; - | ------------------- required by this bound in `gotham_restful::Endpoint::Placeholders` + | ------------------- required by this bound in `gotham_restful::EndpointWithSchema::Placeholders` + +error[E0277]: the trait bound `FooPlaceholders: OpenapiType` is not satisfied + --> $DIR/invalid_placeholders_ty.rs:15:16 + | +15 | fn endpoint(_: FooPlaceholders) { + | ^^^^^^^^^^^^^^^ the trait `OpenapiType` is not implemented for `FooPlaceholders` + | + ::: $WORKSPACE/src/endpoint.rs + | + | #[openapi_bound("Placeholders: crate::OpenapiType")] + | ---------------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Placeholders` diff --git a/tests/ui/endpoint/invalid_return_type.stderr b/tests/ui/endpoint/invalid_return_type.stderr index 1660b14..bece7da 100644 --- a/tests/ui/endpoint/invalid_return_type.stderr +++ b/tests/ui/endpoint/invalid_return_type.stderr @@ -7,4 +7,4 @@ error[E0277]: the trait bound `FooResponse: ResourceResult` is not satisfied ::: $WORKSPACE/src/endpoint.rs | | type Output: ResourceResult + Send; - | -------------- required by this bound in `gotham_restful::Endpoint::Output` + | -------------- required by this bound in `gotham_restful::EndpointWithSchema::Output` From 7de11cdae196214e907707a1ee5237362f0d0674 Mon Sep 17 00:00:00 2001 From: msrd0 <1182023-msrd0@users.noreply.gitlab.com> Date: Sun, 21 Feb 2021 18:21:09 +0000 Subject: [PATCH 145/170] split the ResourceResult trait --- CHANGELOG.md | 1 + src/endpoint.rs | 1 + src/lib.rs | 2 ++ src/openapi/operation.rs | 4 +-- src/openapi/router.rs | 2 +- src/result/mod.rs | 34 +++++++++++++++++--- src/result/no_content.rs | 16 ++++++--- src/result/raw.rs | 19 ++++++++--- src/result/redirect.rs | 20 ++++++++---- src/result/result.rs | 12 +++++-- src/result/success.rs | 9 ++++-- tests/ui/endpoint/invalid_return_type.stderr | 11 +++++++ 12 files changed, 102 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 369a09b..372120c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - All fields of `Response` are now private - If not enabling the `openapi` feature, `without-openapi` has to be enabled - The endpoint macro attributes (`read`, `create`, ...) no longer take the resource ident and reject all unknown attributes ([!18]) + - The `ResourceResult` trait has been split into `ResourceResult` and `ResourceResultSchema` ### Removed - All pre-defined methods (`read`, `create`, ...) from our router extensions ([!18]) diff --git a/src/endpoint.rs b/src/endpoint.rs index 32dd015..f85fdb2 100644 --- a/src/endpoint.rs +++ b/src/endpoint.rs @@ -16,6 +16,7 @@ pub trait Endpoint { fn uri() -> Cow<'static, str>; /// The output type that provides the response. + #[openapi_bound("Output: crate::ResourceResultSchema")] type Output: ResourceResult + Send; /// Returns `true` _iff_ the URI contains placeholders. `false` by default. diff --git a/src/lib.rs b/src/lib.rs index ac58c2e..1f50701 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -482,6 +482,8 @@ pub use result::{ AuthError, AuthError::Forbidden, AuthErrorOrOther, AuthResult, AuthSuccess, IntoResponseError, NoContent, Raw, Redirect, ResourceResult, Success }; +#[cfg(feature = "openapi")] +pub use result::{ResourceResultSchema, ResourceResultWithSchema}; mod routing; pub use routing::{DrawResourceRoutes, DrawResources}; diff --git a/src/openapi/operation.rs b/src/openapi/operation.rs index 103c46c..85f5e6c 100644 --- a/src/openapi/operation.rs +++ b/src/openapi/operation.rs @@ -188,7 +188,7 @@ mod test { #[test] fn no_content_schema_to_content() { let types = NoContent::accepted_types(); - let schema = ::schema(); + let schema = ::schema(); let content = OperationDescription::schema_to_content(types.or_all_types(), Item(schema.into_schema())); assert!(content.is_empty()); } @@ -196,7 +196,7 @@ mod test { #[test] fn raw_schema_to_content() { let types = Raw::<&str>::accepted_types(); - let schema = as ResourceResult>::schema(); + let schema = as ResourceResultSchema>::schema(); let content = OperationDescription::schema_to_content(types.or_all_types(), Item(schema.into_schema())); assert_eq!(content.len(), 1); let json = serde_json::to_string(&content.values().nth(0).unwrap()).unwrap(); diff --git a/src/openapi/router.rs b/src/openapi/router.rs index 5a13302..2fd8caa 100644 --- a/src/openapi/router.rs +++ b/src/openapi/router.rs @@ -1,5 +1,5 @@ use super::{builder::OpenapiBuilder, handler::OpenapiHandler, operation::OperationDescription}; -use crate::{routing::*, EndpointWithSchema, OpenapiType, ResourceResult, ResourceWithSchema}; +use crate::{routing::*, EndpointWithSchema, OpenapiType, ResourceResultSchema, ResourceWithSchema}; use gotham::{hyper::Method, pipeline::chain::PipelineHandleChain, router::builder::*}; use once_cell::sync::Lazy; use regex::{Captures, Regex}; diff --git a/src/result/mod.rs b/src/result/mod.rs index 87967aa..a119a82 100644 --- a/src/result/mod.rs +++ b/src/result/mod.rs @@ -2,7 +2,7 @@ use crate::OpenapiSchema; use crate::Response; -use futures_util::future::FutureExt; +use futures_util::future::{BoxFuture, FutureExt}; use gotham::handler::HandlerError; #[cfg(feature = "openapi")] use gotham::hyper::StatusCode; @@ -49,22 +49,41 @@ pub trait ResourceResult { /// Turn this into a response that can be returned to the browser. This api will likely /// change in the future. - fn into_response(self) -> Pin> + Send>>; + fn into_response(self) -> BoxFuture<'static, Result>; /// Return a list of supported mime types. fn accepted_types() -> Option> { None } +} - #[cfg(feature = "openapi")] +/// Additional details for [ResourceResult] to be used with an OpenAPI-aware router. +#[cfg(feature = "openapi")] +pub trait ResourceResultSchema { fn schema() -> OpenapiSchema; - #[cfg(feature = "openapi")] fn default_status() -> StatusCode { StatusCode::OK } } +#[cfg(feature = "openapi")] +mod private { + pub trait Sealed {} +} + +/// A trait provided to convert a resource's result to json, and provide an OpenAPI schema to the +/// router. This trait is implemented for all types that implement [ResourceResult] and +/// [ResourceResultSchema]. +#[cfg(feature = "openapi")] +pub trait ResourceResultWithSchema: ResourceResult + ResourceResultSchema + private::Sealed {} + +#[cfg(feature = "openapi")] +impl private::Sealed for R {} + +#[cfg(feature = "openapi")] +impl ResourceResultWithSchema for R {} + /// The default json returned on an 500 Internal Server Error. #[derive(Debug, Serialize)] pub(crate) struct ResourceError { @@ -130,8 +149,13 @@ where fn accepted_types() -> Option> { Res::accepted_types() } +} - #[cfg(feature = "openapi")] +#[cfg(feature = "openapi")] +impl ResourceResultSchema for Pin + Send>> +where + Res: ResourceResultSchema +{ fn schema() -> OpenapiSchema { Res::schema() } diff --git a/src/result/no_content.rs b/src/result/no_content.rs index 30413fd..e6072ed 100644 --- a/src/result/no_content.rs +++ b/src/result/no_content.rs @@ -1,7 +1,7 @@ use super::{handle_error, ResourceResult}; use crate::{IntoResponseError, Response}; #[cfg(feature = "openapi")] -use crate::{OpenapiSchema, OpenapiType}; +use crate::{OpenapiSchema, OpenapiType, ResourceResultSchema}; use futures_util::{future, future::FutureExt}; #[cfg(feature = "openapi")] @@ -52,15 +52,16 @@ impl ResourceResult for NoContent { fn accepted_types() -> Option> { Some(Vec::new()) } +} +#[cfg(feature = "openapi")] +impl ResourceResultSchema for NoContent { /// Returns the schema of the `()` type. - #[cfg(feature = "openapi")] fn schema() -> OpenapiSchema { <()>::schema() } /// This will always be a _204 No Content_ - #[cfg(feature = "openapi")] fn default_status() -> StatusCode { StatusCode::NO_CONTENT } @@ -82,10 +83,15 @@ where fn accepted_types() -> Option> { NoContent::accepted_types() } +} - #[cfg(feature = "openapi")] +#[cfg(feature = "openapi")] +impl ResourceResultSchema for Result +where + E: Display + IntoResponseError +{ fn schema() -> OpenapiSchema { - ::schema() + ::schema() } #[cfg(feature = "openapi")] diff --git a/src/result/raw.rs b/src/result/raw.rs index cc30a55..759adbb 100644 --- a/src/result/raw.rs +++ b/src/result/raw.rs @@ -1,7 +1,7 @@ use super::{handle_error, IntoResponseError, ResourceResult}; use crate::{FromBody, RequestBody, ResourceType, Response}; #[cfg(feature = "openapi")] -use crate::{OpenapiSchema, OpenapiType}; +use crate::{OpenapiSchema, OpenapiType, ResourceResultSchema}; use futures_core::future::Future; use futures_util::{future, future::FutureExt}; @@ -108,8 +108,13 @@ where fn into_response(self) -> Pin> + Send>> { future::ok(Response::new(StatusCode::OK, self.raw, Some(self.mime))).boxed() } +} - #[cfg(feature = "openapi")] +#[cfg(feature = "openapi")] +impl> ResourceResultSchema for Raw +where + Self: Send +{ fn schema() -> OpenapiSchema { ::schema() } @@ -128,10 +133,16 @@ where Err(e) => handle_error(e) } } +} - #[cfg(feature = "openapi")] +#[cfg(feature = "openapi")] +impl ResourceResultSchema for Result, E> +where + Raw: ResourceResult + ResourceResultSchema, + E: Display + IntoResponseError as ResourceResult>::Err> +{ fn schema() -> OpenapiSchema { - as ResourceResult>::schema() + as ResourceResultSchema>::schema() } } diff --git a/src/result/redirect.rs b/src/result/redirect.rs index 432203d..0a1933c 100644 --- a/src/result/redirect.rs +++ b/src/result/redirect.rs @@ -1,7 +1,7 @@ use super::{handle_error, ResourceResult}; use crate::{IntoResponseError, Response}; #[cfg(feature = "openapi")] -use crate::{NoContent, OpenapiSchema}; +use crate::{NoContent, OpenapiSchema, ResourceResultSchema}; use futures_util::future::{BoxFuture, FutureExt, TryFutureExt}; use gotham::hyper::{ header::{InvalidHeaderValue, LOCATION}, @@ -53,15 +53,16 @@ impl ResourceResult for Redirect { } .boxed() } +} - #[cfg(feature = "openapi")] +#[cfg(feature = "openapi")] +impl ResourceResultSchema for Redirect { fn default_status() -> StatusCode { StatusCode::SEE_OTHER } - #[cfg(feature = "openapi")] fn schema() -> OpenapiSchema { - ::schema() + ::schema() } } @@ -88,15 +89,20 @@ where Err(e) => handle_error(e).map_err(|e| RedirectError::Other(e)).boxed() } } +} - #[cfg(feature = "openapi")] +#[cfg(feature = "openapi")] +impl ResourceResultSchema for Result +where + E: Display + IntoResponseError, + ::Err: StdError + Sync +{ fn default_status() -> StatusCode { Redirect::default_status() } - #[cfg(feature = "openapi")] fn schema() -> OpenapiSchema { - ::schema() + ::schema() } } diff --git a/src/result/result.rs b/src/result/result.rs index 71c44b7..deaa45b 100644 --- a/src/result/result.rs +++ b/src/result/result.rs @@ -1,7 +1,7 @@ use super::{handle_error, into_response_helper, ResourceResult}; -#[cfg(feature = "openapi")] -use crate::OpenapiSchema; use crate::{result::ResourceError, Response, ResponseBody}; +#[cfg(feature = "openapi")] +use crate::{OpenapiSchema, ResourceResultSchema}; use futures_core::future::Future; use gotham::hyper::StatusCode; @@ -43,8 +43,14 @@ where fn accepted_types() -> Option> { Some(vec![APPLICATION_JSON]) } +} - #[cfg(feature = "openapi")] +#[cfg(feature = "openapi")] +impl ResourceResultSchema for Result +where + R: ResponseBody, + E: Display + IntoResponseError +{ fn schema() -> OpenapiSchema { R::schema() } diff --git a/src/result/success.rs b/src/result/success.rs index bd899bf..3c1bcaa 100644 --- a/src/result/success.rs +++ b/src/result/success.rs @@ -1,6 +1,6 @@ use super::{into_response_helper, ResourceResult}; #[cfg(feature = "openapi")] -use crate::OpenapiSchema; +use crate::{OpenapiSchema, ResourceResultSchema}; use crate::{Response, ResponseBody}; use gotham::hyper::StatusCode; use mime::{Mime, APPLICATION_JSON}; @@ -104,8 +104,13 @@ where fn accepted_types() -> Option> { Some(vec![APPLICATION_JSON]) } +} - #[cfg(feature = "openapi")] +#[cfg(feature = "openapi")] +impl ResourceResultSchema for Success +where + Self: Send +{ fn schema() -> OpenapiSchema { T::schema() } diff --git a/tests/ui/endpoint/invalid_return_type.stderr b/tests/ui/endpoint/invalid_return_type.stderr index bece7da..2879898 100644 --- a/tests/ui/endpoint/invalid_return_type.stderr +++ b/tests/ui/endpoint/invalid_return_type.stderr @@ -1,3 +1,14 @@ +error[E0277]: the trait bound `FooResponse: ResourceResultSchema` is not satisfied + --> $DIR/invalid_return_type.rs:12:18 + | +12 | fn endpoint() -> FooResponse { + | ^^^^^^^^^^^ the trait `ResourceResultSchema` is not implemented for `FooResponse` + | + ::: $WORKSPACE/src/endpoint.rs + | + | #[openapi_bound("Output: crate::ResourceResultSchema")] + | ------------------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Output` + error[E0277]: the trait bound `FooResponse: ResourceResult` is not satisfied --> $DIR/invalid_return_type.rs:12:18 | From 666514c8e2f9767d91acdf64727498bbee2c1d94 Mon Sep 17 00:00:00 2001 From: Dominic Date: Mon, 22 Feb 2021 09:58:01 +0100 Subject: [PATCH 146/170] fix the example --- .gitlab-ci.yml | 13 +++++++++++++ example/Cargo.toml | 2 +- example/src/main.rs | 22 +++++++++++----------- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7787130..bc141b1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -8,6 +8,19 @@ variables: CARGO_HOME: $CI_PROJECT_DIR/cargo RUST_LOG: info,gotham=debug,gotham_restful=trace +check-example: + stage: test + image: rust:slim + before_script: + - cargo -V + script: + - cargo check -p example + cache: + key: cargo-stable-example + paths: + - cargo/ + - target/ + test-default: stage: test image: rust:1.49-slim diff --git a/example/Cargo.toml b/example/Cargo.toml index d049d98..4ddcd5f 100644 --- a/example/Cargo.toml +++ b/example/Cargo.toml @@ -18,7 +18,7 @@ gitlab = { repository = "msrd0/gotham-restful", branch = "master" } fake = "2.2.2" gotham = { version = "0.5.0", default-features = false } gotham_derive = "0.5.0" -gotham_restful = { version = "0.2.0-dev", features = ["auth", "openapi"], default-features = false } +gotham_restful = { version = "0.2.0-dev", features = ["auth", "cors", "openapi"], default-features = false } log = "0.4.8" pretty_env_logger = "0.4" serde = "1.0.110" diff --git a/example/src/main.rs b/example/src/main.rs index db880d7..d95795c 100644 --- a/example/src/main.rs +++ b/example/src/main.rs @@ -15,11 +15,11 @@ use gotham_restful::{cors::*, *}; use serde::{Deserialize, Serialize}; #[derive(Resource)] -#[resource(read_all, read, search, create, change_all, change, remove, remove_all)] +#[resource(read_all, read, search, create, update_all, update, remove, remove_all)] struct Users {} #[derive(Resource)] -#[resource(ReadAll)] +#[resource(auth_read_all)] struct Auth {} #[derive(Deserialize, OpenapiType, Serialize, StateData, StaticResponseExtender)] @@ -27,7 +27,7 @@ struct User { username: String } -#[read_all(Users)] +#[read_all] fn read_all() -> Success>> { vec![Username().fake(), Username().fake()] .into_iter() @@ -36,7 +36,7 @@ fn read_all() -> Success>> { .into() } -#[read(Users)] +#[read] fn read(id: u64) -> Success { let username: String = Username().fake(); User { @@ -45,17 +45,17 @@ fn read(id: u64) -> Success { .into() } -#[search(Users)] +#[search] fn search(query: User) -> Success { query.into() } -#[create(Users)] +#[create] fn create(body: User) { info!("Created User: {}", body.username); } -#[change_all(Users)] +#[change_all] fn update_all(body: Vec) { info!( "Changing all Users to {:?}", @@ -63,22 +63,22 @@ fn update_all(body: Vec) { ); } -#[change(Users)] +#[change] fn update(id: u64, body: User) { info!("Change User {} to {}", id, body.username); } -#[remove_all(Users)] +#[remove_all] fn remove_all() { info!("Delete all Users"); } -#[remove(Users)] +#[remove] fn remove(id: u64) { info!("Delete User {}", id); } -#[read_all(Auth)] +#[read_all] fn auth_read_all(auth: AuthStatus<()>) -> AuthSuccess { match auth { AuthStatus::Authenticated(data) => Ok(format!("{:?}", data)), From e7ef6bdf5adffeeb6ece1c79c6cd0cda3c3750b0 Mon Sep 17 00:00:00 2001 From: Dominic Date: Mon, 22 Feb 2021 10:01:43 +0100 Subject: [PATCH 147/170] cargo is dumb --- .gitlab-ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bc141b1..b98380d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -14,7 +14,8 @@ check-example: before_script: - cargo -V script: - - cargo check -p example + - cd example + - cargo check cache: key: cargo-stable-example paths: From 7ed98c82e820899cd0dd57cd2f0df62a826a252a Mon Sep 17 00:00:00 2001 From: msrd0 <1182023-msrd0@users.noreply.gitlab.com> Date: Wed, 24 Feb 2021 18:53:44 +0000 Subject: [PATCH 148/170] OpenAPI: Key Schema for HashMap's --- CHANGELOG.md | 1 + src/openapi/types.rs | 25 ++++++++++++++++++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 372120c..3349897 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - If not enabling the `openapi` feature, `without-openapi` has to be enabled - The endpoint macro attributes (`read`, `create`, ...) no longer take the resource ident and reject all unknown attributes ([!18]) - The `ResourceResult` trait has been split into `ResourceResult` and `ResourceResultSchema` + - `HashMap`'s keys are included in the generated OpenAPI spec (they defaulted to `type: string` previously) ### Removed - All pre-defined methods (`read`, `create`, ...) from our router extensions ([!18]) diff --git a/src/openapi/types.rs b/src/openapi/types.rs index e15a6b5..18f5be6 100644 --- a/src/openapi/types.rs +++ b/src/openapi/types.rs @@ -344,10 +344,24 @@ impl OpenapiType for HashSet { } } -impl OpenapiType for HashMap { +impl OpenapiType for HashMap { fn schema() -> OpenapiSchema { + let key_schema = K::schema(); + let mut dependencies = key_schema.dependencies.clone(); + + let keys = match key_schema.name.clone() { + Some(name) => { + let reference = Reference { + reference: format!("#/components/schemas/{}", name) + }; + dependencies.insert(name, key_schema); + reference + }, + None => Item(Box::new(key_schema.into_schema())) + }; + let schema = T::schema(); - let mut dependencies = schema.dependencies.clone(); + dependencies.extend(schema.dependencies.iter().map(|(k, v)| (k.clone(), v.clone()))); let items = Box::new(match schema.name.clone() { Some(name) => { @@ -360,10 +374,15 @@ impl OpenapiType for HashMap { None => Item(schema.into_schema()) }); + let mut properties = IndexMap::new(); + properties.insert("default".to_owned(), keys); + OpenapiSchema { nullable: false, name: None, schema: SchemaKind::Type(Type::Object(ObjectType { + properties, + required: vec!["default".to_owned()], additional_properties: Some(AdditionalProperties::Schema(items)), ..Default::default() })), @@ -453,6 +472,6 @@ mod test { assert_schema!(Vec => r#"{"type":"array","items":{"type":"string"}}"#); assert_schema!(BTreeSet => r#"{"type":"array","items":{"type":"string"}}"#); assert_schema!(HashSet => r#"{"type":"array","items":{"type":"string"}}"#); - assert_schema!(HashMap => r#"{"type":"object","additionalProperties":{"type":"string"}}"#); + assert_schema!(HashMap => r#"{"type":"object","properties":{"default":{"type":"integer","format":"int64"}},"required":["default"],"additionalProperties":{"type":"string"}}"#); assert_schema!(Value => r#"{"nullable":true}"#); } From 28ae4dfdeeff6c9b291c489877fe597c08fafb30 Mon Sep 17 00:00:00 2001 From: Dominic Date: Thu, 25 Feb 2021 00:37:55 +0100 Subject: [PATCH 149/170] add swagger_ui to the router --- CHANGELOG.md | 1 + Cargo.toml | 26 +++-- example/src/main.rs | 1 + src/openapi/handler.rs | 240 ++++++++++++++++++++++++++++++++++------- src/openapi/router.rs | 15 ++- 5 files changed, 230 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3349897..26ed987 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - New `endpoint` router extension with associated `Endpoint` trait ([!18]) - Support for custom endpoints using the `#[endpoint]` macro ([!19]) - Support for `anyhow::Error` (or any type implementing `Into`) in most responses + - `swagger_ui` method to the OpenAPI router to render the specification using Swagger UI ### Changed - The cors handler can now copy headers from the request if desired diff --git a/Cargo.toml b/Cargo.toml index dff6df4..bf96d7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,27 +20,33 @@ include = ["src/**/*", "LICENSE", "README.md", "CHANGELOG.md"] gitlab = { repository = "msrd0/gotham-restful", branch = "master" } [dependencies] -base64 = { version = "0.13.0", optional = true } -chrono = { version = "0.4.19", features = ["serde"], optional = true } -cookie = { version = "0.14", optional = true } futures-core = "0.3.7" futures-util = "0.3.7" gotham = { version = "0.5.0", default-features = false } gotham_derive = "0.5.0" -gotham_middleware_diesel = { version = "0.2.0", optional = true } gotham_restful_derive = "0.2.0-dev" -indexmap = { version = "1.3.2", optional = true } -jsonwebtoken = { version = "7.1.0", optional = true } log = "0.4.8" mime = "0.3.16" -once_cell = { version = "1.5", optional = true } -openapiv3 = { version = "0.3.2", optional = true } -regex = { version = "1.4", optional = true } serde = { version = "1.0.110", features = ["derive"] } serde_json = "1.0.58" thiserror = "1.0" + +# features +chrono = { version = "0.4.19", features = ["serde"], optional = true } uuid = { version = "0.8.1", optional = true } +# non-feature optional dependencies +base64 = { version = "0.13.0", optional = true } +cookie = { version = "0.14", optional = true } +gotham_middleware_diesel = { version = "0.2.0", optional = true } +indexmap = { version = "1.3.2", optional = true } +indoc = { version = "1.0", optional = true } +jsonwebtoken = { version = "7.1.0", optional = true } +once_cell = { version = "1.5", optional = true } +openapiv3 = { version = "0.3.2", optional = true } +regex = { version = "1.4", optional = true } +sha2 = { version = "0.9.3", optional = true } + [dev-dependencies] diesel = { version = "1.4.4", features = ["postgres"] } futures-executor = "0.3.5" @@ -61,7 +67,7 @@ errorlog = [] # These features are exclusive - https://gitlab.com/msrd0/gotham-restful/-/issues/4 without-openapi = [] -openapi = ["gotham_restful_derive/openapi", "indexmap", "once_cell", "openapiv3", "regex"] +openapi = ["gotham_restful_derive/openapi", "base64", "indexmap", "indoc", "once_cell", "openapiv3", "regex", "sha2"] [package.metadata.docs.rs] no-default-features = true diff --git a/example/src/main.rs b/example/src/main.rs index d95795c..e85f911 100644 --- a/example/src/main.rs +++ b/example/src/main.rs @@ -122,6 +122,7 @@ fn main() { route.resource::("users"); route.resource::("auth"); route.get_openapi("openapi"); + route.swagger_ui(""); }); }) ); diff --git a/src/openapi/handler.rs b/src/openapi/handler.rs index f340a6d..9762ca6 100644 --- a/src/openapi/handler.rs +++ b/src/openapi/handler.rs @@ -4,37 +4,26 @@ use futures_util::{future, future::FutureExt}; use gotham::{ anyhow, handler::{Handler, HandlerFuture, NewHandler}, - helpers::http::response::create_response, - hyper::StatusCode, + helpers::http::response::{create_empty_response, create_response}, + hyper::{ + header::{ + HeaderMap, HeaderValue, CACHE_CONTROL, CONTENT_SECURITY_POLICY, ETAG, IF_NONE_MATCH, REFERRER_POLICY, + X_CONTENT_TYPE_OPTIONS + }, + Body, Response, StatusCode, Uri + }, state::State }; use indexmap::IndexMap; -use mime::{APPLICATION_JSON, TEXT_PLAIN}; +use mime::{APPLICATION_JSON, TEXT_HTML, TEXT_PLAIN}; +use once_cell::sync::Lazy; use openapiv3::{APIKeyLocation, OpenAPI, ReferenceOr, SecurityScheme}; +use sha2::{Digest, Sha256}; use std::{ pin::Pin, sync::{Arc, RwLock} }; -#[derive(Clone)] -pub struct OpenapiHandler { - openapi: Arc> -} - -impl OpenapiHandler { - pub fn new(openapi: Arc>) -> Self { - Self { openapi } - } -} - -impl NewHandler for OpenapiHandler { - type Instance = Self; - - fn new_handler(&self) -> anyhow::Result { - Ok(self.clone()) - } -} - #[cfg(feature = "auth")] fn get_security(state: &mut State) -> IndexMap> { use crate::AuthSource; @@ -71,33 +60,202 @@ fn get_security(_state: &mut State) -> IndexMap>) -> Response { + let openapi = match openapi.read() { + Ok(openapi) => openapi, + Err(e) => { + error!("Unable to acquire read lock for the OpenAPI specification: {}", e); + return create_response(&state, StatusCode::INTERNAL_SERVER_ERROR, TEXT_PLAIN, ""); + } + }; + + let mut openapi = openapi.clone(); + let security_schemes = get_security(state); + let mut components = openapi.components.unwrap_or_default(); + components.security_schemes = security_schemes; + openapi.components = Some(components); + + match serde_json::to_string(&openapi) { + Ok(body) => { + let mut res = create_response(&state, StatusCode::OK, APPLICATION_JSON, body); + let headers = res.headers_mut(); + headers.insert(X_CONTENT_TYPE_OPTIONS, HeaderValue::from_static("nosniff")); + res + }, + Err(e) => { + error!("Unable to handle OpenAPI request due to error: {}", e); + create_response(&state, StatusCode::INTERNAL_SERVER_ERROR, TEXT_PLAIN, "") + } + } +} + +#[derive(Clone)] +pub struct OpenapiHandler { + openapi: Arc> +} + +impl OpenapiHandler { + pub fn new(openapi: Arc>) -> Self { + Self { openapi } + } +} + +impl NewHandler for OpenapiHandler { + type Instance = Self; + + fn new_handler(&self) -> anyhow::Result { + Ok(self.clone()) + } +} + impl Handler for OpenapiHandler { fn handle(self, mut state: State) -> Pin> { - let openapi = match self.openapi.read() { - Ok(openapi) => openapi, - Err(e) => { - error!("Unable to acquire read lock for the OpenAPI specification: {}", e); - let res = create_response(&state, StatusCode::INTERNAL_SERVER_ERROR, TEXT_PLAIN, ""); - return future::ok((state, res)).boxed(); - } - }; + let res = create_openapi_response(&mut state, &self.openapi); + future::ok((state, res)).boxed() + } +} - let mut openapi = openapi.clone(); - let security_schemes = get_security(&mut state); - let mut components = openapi.components.unwrap_or_default(); - components.security_schemes = security_schemes; - openapi.components = Some(components); +#[derive(Clone)] +pub struct SwaggerUiHandler { + openapi: Arc> +} - match serde_json::to_string(&openapi) { - Ok(body) => { - let res = create_response(&state, StatusCode::OK, APPLICATION_JSON, body); +impl SwaggerUiHandler { + pub fn new(openapi: Arc>) -> Self { + Self { openapi } + } +} + +impl NewHandler for SwaggerUiHandler { + type Instance = Self; + + fn new_handler(&self) -> anyhow::Result { + Ok(self.clone()) + } +} + +impl Handler for SwaggerUiHandler { + fn handle(self, mut state: State) -> Pin> { + let uri: &Uri = state.borrow(); + let query = uri.query(); + match query { + // TODO this is hacky + Some(q) if q.contains("spec") => { + let res = create_openapi_response(&mut state, &self.openapi); future::ok((state, res)).boxed() }, - Err(e) => { - error!("Unable to handle OpenAPI request due to error: {}", e); - let res = create_response(&state, StatusCode::INTERNAL_SERVER_ERROR, TEXT_PLAIN, ""); + _ => { + { + let headers: &HeaderMap = state.borrow(); + if headers + .get(IF_NONE_MATCH) + .map_or(false, |etag| etag.as_bytes() == SWAGGER_UI_HTML_ETAG.as_bytes()) + { + let res = create_empty_response(&state, StatusCode::NOT_MODIFIED); + return future::ok((state, res)).boxed(); + } + } + + let mut res = create_response(&state, StatusCode::OK, TEXT_HTML, SWAGGER_UI_HTML.as_bytes()); + let headers = res.headers_mut(); + headers.insert(CACHE_CONTROL, HeaderValue::from_static("public,max-age=2592000")); + headers.insert(CONTENT_SECURITY_POLICY, format!("default-src 'none'; script-src 'unsafe-inline' 'sha256-{}' 'strict-dynamic'; style-src 'unsafe-inline' https://cdnjs.cloudflare.com; connect-src 'self'; img-src data:;", SWAGGER_UI_SCRIPT_HASH.as_str()).parse().unwrap()); + headers.insert(ETAG, SWAGGER_UI_HTML_ETAG.parse().unwrap()); + headers.insert(REFERRER_POLICY, HeaderValue::from_static("strict-origin-when-cross-origin")); + headers.insert(X_CONTENT_TYPE_OPTIONS, HeaderValue::from_static("nosniff")); future::ok((state, res)).boxed() } } } } + +// inspired by https://github.com/swagger-api/swagger-ui/blob/master/dist/index.html +const SWAGGER_UI_HTML: Lazy<&'static String> = Lazy::new(|| { + let template = indoc::indoc! { + r#" + + + + + + + + +

+ + + + "# + }; + Box::leak(Box::new(template.replace("{{script}}", SWAGGER_UI_SCRIPT))) +}); +static SWAGGER_UI_HTML_ETAG: Lazy = Lazy::new(|| { + let mut hash = Sha256::new(); + hash.update(SWAGGER_UI_HTML.as_bytes()); + let hash = hash.finalize(); + let hash = base64::encode(hash); + format!("\"{}\"", hash) +}); +const SWAGGER_UI_SCRIPT: &str = r#" +let s0rdy = false; +let s1rdy = false; + +window.onload = function() { + const cb = function() { + if (!s0rdy || !s1rdy) + return; + const ui = SwaggerUIBundle({ + url: window.location.origin + window.location.pathname + '?spec', + dom_id: '#swagger-ui', + deepLinking: true, + presets: [ + SwaggerUIBundle.presets.apis, + SwaggerUIStandalonePreset + ], + plugins: [ + SwaggerUIBundle.plugins.DownloadUrl + ], + layout: 'StandaloneLayout' + }); + window.ui = ui; + }; + + const s0 = document.createElement('script'); + s0.setAttribute('src', 'https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.43.0/swagger-ui-bundle.js'); + s0.setAttribute('integrity', 'sha512-EfK//grBlevo9MrtEDyNvf4SkBA0avHZoVLEuSR2Yl6ymnjcIwClgZ7FXdr/42yGqnhEHxb+Sv/bJeUp26YPRw=='); + s0.setAttribute('crossorigin', 'anonymous'); + s0.onload = function() { + s0rdy = true; + cb(); + }; + document.head.appendChild(s0); + + const s1 = document.createElement('script'); + s1.setAttribute('src', 'https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.43.0/swagger-ui-standalone-preset.js'); + s1.setAttribute('integrity', 'sha512-Hbx9NyAhG+P6YNBU9mp6hc6ntRjGYHqo/qae4OHhzPA69xbNmF8n2aJxpzUwdbXbYICO6eor4IhgSfiSQm9OYg=='); + s1.setAttribute('crossorigin', 'anonymous'); + s1.onload = function() { + s1rdy = true; + cb(); + }; + document.head.appendChild(s1); +}; +"#; +static SWAGGER_UI_SCRIPT_HASH: Lazy = Lazy::new(|| { + let mut hash = Sha256::new(); + hash.update(SWAGGER_UI_SCRIPT); + let hash = hash.finalize(); + base64::encode(hash) +}); diff --git a/src/openapi/router.rs b/src/openapi/router.rs index 2fd8caa..7097cfd 100644 --- a/src/openapi/router.rs +++ b/src/openapi/router.rs @@ -1,13 +1,18 @@ -use super::{builder::OpenapiBuilder, handler::OpenapiHandler, operation::OperationDescription}; +use super::{ + builder::OpenapiBuilder, + handler::{OpenapiHandler, SwaggerUiHandler}, + operation::OperationDescription +}; use crate::{routing::*, EndpointWithSchema, OpenapiType, ResourceResultSchema, ResourceWithSchema}; use gotham::{hyper::Method, pipeline::chain::PipelineHandleChain, router::builder::*}; use once_cell::sync::Lazy; use regex::{Captures, Regex}; use std::panic::RefUnwindSafe; -/// This trait adds the `get_openapi` method to an OpenAPI-aware router. +/// This trait adds the `get_openapi` and `swagger_ui` method to an OpenAPI-aware router. pub trait GetOpenapi { fn get_openapi(&mut self, path: &str); + fn swagger_ui(&mut self, path: &str); } #[derive(Debug)] @@ -51,6 +56,12 @@ macro_rules! implOpenapiRouter { .get(path) .to_new_handler(OpenapiHandler::new(self.openapi_builder.openapi.clone())); } + + fn swagger_ui(&mut self, path: &str) { + self.router + .get(path) + .to_new_handler(SwaggerUiHandler::new(self.openapi_builder.openapi.clone())); + } } impl<'a, 'b, C, P> DrawResourcesWithSchema for OpenapiRouter<'a, $implType<'b, C, P>> From 31f92c07cdccd4c55a75f70d89eecc3de9aa899a Mon Sep 17 00:00:00 2001 From: msrd0 <1182023-msrd0@users.noreply.gitlab.com> Date: Sat, 27 Feb 2021 15:40:34 +0000 Subject: [PATCH 150/170] Custom HTTP Headers --- CHANGELOG.md | 2 +- README.md | 21 ++ derive/src/endpoint.rs | 2 +- src/endpoint.rs | 6 +- src/lib.rs | 44 ++- src/openapi/operation.rs | 7 +- src/openapi/router.rs | 2 +- src/response.rs | 84 ------ src/{result => response}/auth_result.rs | 12 +- src/response/mod.rs | 283 +++++++++++++++++++ src/{result => response}/no_content.rs | 52 +++- src/{result => response}/raw.rs | 22 +- src/{result => response}/redirect.rs | 16 +- src/{result => response}/result.rs | 14 +- src/{result => response}/success.rs | 104 +++---- src/result/mod.rs | 197 ------------- src/routing.rs | 7 +- tests/ui/endpoint/invalid_return_type.stderr | 16 +- 18 files changed, 475 insertions(+), 416 deletions(-) delete mode 100644 src/response.rs rename src/{result => response}/auth_result.rs (85%) create mode 100644 src/response/mod.rs rename src/{result => response}/no_content.rs (66%) rename src/{result => response}/raw.rs (85%) rename src/{result => response}/redirect.rs (89%) rename src/{result => response}/result.rs (85%) rename src/{result => response}/success.rs (52%) delete mode 100644 src/result/mod.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 26ed987..ec4e9a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - All fields of `Response` are now private - If not enabling the `openapi` feature, `without-openapi` has to be enabled - The endpoint macro attributes (`read`, `create`, ...) no longer take the resource ident and reject all unknown attributes ([!18]) - - The `ResourceResult` trait has been split into `ResourceResult` and `ResourceResultSchema` + - The `ResourceResult` trait has been split into `IntoResponse` and `ResponseSchema` - `HashMap`'s keys are included in the generated OpenAPI spec (they defaulted to `type: string` previously) ### Removed diff --git a/README.md b/README.md index 028feea..76da543 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,27 @@ fn create(body : RawImage) -> Raw> { } ``` +## Custom HTTP Headers + +You can read request headers from the state as you would in any other gotham handler, and specify +custom response headers using [Response::header]. + +```rust +#[derive(Resource)] +#[resource(read_all)] +struct FooResource; + +#[read_all] +async fn read_all(state: &mut State) -> NoContent { + let headers: &HeaderMap = state.borrow(); + let accept = &headers[ACCEPT]; + + let mut res = NoContent::default(); + res.header(VARY, "accept".parse().unwrap()); + res +} +``` + ## Features To make life easier for common use-cases, this create offers a few features that might be helpful diff --git a/derive/src/endpoint.rs b/derive/src/endpoint.rs index aac3757..f6f143b 100644 --- a/derive/src/endpoint.rs +++ b/derive/src/endpoint.rs @@ -487,7 +487,7 @@ fn expand_endpoint_type(mut ty: EndpointType, attrs: AttributeArgs, fun: &ItemFn handle_content = quote!(#handle_content.await); } if is_no_content { - handle_content = quote!(#handle_content; ::gotham_restful::NoContent) + handle_content = quote!(#handle_content; <::gotham_restful::NoContent as ::std::default::Default>::default()) } if let Some(arg) = args.iter().find(|arg| arg.ty.is_database_conn()) { diff --git a/src/endpoint.rs b/src/endpoint.rs index f85fdb2..d8da412 100644 --- a/src/endpoint.rs +++ b/src/endpoint.rs @@ -1,4 +1,4 @@ -use crate::{RequestBody, ResourceResult}; +use crate::{IntoResponse, RequestBody}; use futures_util::future::BoxFuture; use gotham::{ extractor::{PathExtractor, QueryStringExtractor}, @@ -16,8 +16,8 @@ pub trait Endpoint { fn uri() -> Cow<'static, str>; /// The output type that provides the response. - #[openapi_bound("Output: crate::ResourceResultSchema")] - type Output: ResourceResult + Send; + #[openapi_bound("Output: crate::ResponseSchema")] + type Output: IntoResponse + Send; /// Returns `true` _iff_ the URI contains placeholders. `false` by default. fn has_placeholders() -> bool { diff --git a/src/lib.rs b/src/lib.rs index 1f50701..36674c3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -158,6 +158,37 @@ fn create(body : RawImage) -> Raw> { # } ``` +# Custom HTTP Headers + +You can read request headers from the state as you would in any other gotham handler, and specify +custom response headers using [Response::header]. + +```rust,no_run +# #[macro_use] extern crate gotham_restful_derive; +# use gotham::hyper::header::{ACCEPT, HeaderMap, VARY}; +# use gotham::{router::builder::*, state::State}; +# use gotham_restful::*; +#[derive(Resource)] +#[resource(read_all)] +struct FooResource; + +#[read_all] +async fn read_all(state: &mut State) -> NoContent { + let headers: &HeaderMap = state.borrow(); + let accept = &headers[ACCEPT]; +# drop(accept); + + let mut res = NoContent::default(); + res.header(VARY, "accept".parse().unwrap()); + res +} +# fn main() { +# gotham::start("127.0.0.1:8080", build_simple_router(|route| { +# route.resource::("foo"); +# })); +# } +``` + # Features To make life easier for common use-cases, this create offers a few features that might be helpful @@ -475,15 +506,12 @@ pub use endpoint::Endpoint; pub use endpoint::EndpointWithSchema; mod response; -pub use response::Response; - -mod result; -pub use result::{ - AuthError, AuthError::Forbidden, AuthErrorOrOther, AuthResult, AuthSuccess, IntoResponseError, NoContent, Raw, Redirect, - ResourceResult, Success +pub use response::{ + AuthError, AuthError::Forbidden, AuthErrorOrOther, AuthResult, AuthSuccess, IntoResponse, IntoResponseError, NoContent, + Raw, Redirect, Response, Success }; #[cfg(feature = "openapi")] -pub use result::{ResourceResultSchema, ResourceResultWithSchema}; +pub use response::{IntoResponseWithSchema, ResponseSchema}; mod routing; pub use routing::{DrawResourceRoutes, DrawResources}; @@ -496,7 +524,7 @@ pub use types::*; /// This trait must be implemented for every resource. It allows you to register the different /// endpoints 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, just use `#[derive(Resource)]`. #[_private_openapi_trait(ResourceWithSchema)] pub trait Resource { /// Register all methods handled by this resource with the underlying router. diff --git a/src/openapi/operation.rs b/src/openapi/operation.rs index 85f5e6c..62d06d5 100644 --- a/src/openapi/operation.rs +++ b/src/openapi/operation.rs @@ -1,5 +1,5 @@ use super::SECURITY_NAME; -use crate::{result::*, EndpointWithSchema, OpenapiSchema, RequestBody}; +use crate::{response::OrAllTypes, EndpointWithSchema, IntoResponse, OpenapiSchema, RequestBody, ResponseSchema}; use indexmap::IndexMap; use mime::Mime; use openapiv3::{ @@ -184,11 +184,12 @@ impl OperationDescription { #[cfg(test)] mod test { use super::*; + use crate::{NoContent, Raw, ResponseSchema}; #[test] fn no_content_schema_to_content() { let types = NoContent::accepted_types(); - let schema = ::schema(); + let schema = ::schema(); let content = OperationDescription::schema_to_content(types.or_all_types(), Item(schema.into_schema())); assert!(content.is_empty()); } @@ -196,7 +197,7 @@ mod test { #[test] fn raw_schema_to_content() { let types = Raw::<&str>::accepted_types(); - let schema = as ResourceResultSchema>::schema(); + let schema = as ResponseSchema>::schema(); let content = OperationDescription::schema_to_content(types.or_all_types(), Item(schema.into_schema())); assert_eq!(content.len(), 1); let json = serde_json::to_string(&content.values().nth(0).unwrap()).unwrap(); diff --git a/src/openapi/router.rs b/src/openapi/router.rs index 7097cfd..3ced31b 100644 --- a/src/openapi/router.rs +++ b/src/openapi/router.rs @@ -3,7 +3,7 @@ use super::{ handler::{OpenapiHandler, SwaggerUiHandler}, operation::OperationDescription }; -use crate::{routing::*, EndpointWithSchema, OpenapiType, ResourceResultSchema, ResourceWithSchema}; +use crate::{routing::*, EndpointWithSchema, OpenapiType, ResourceWithSchema, ResponseSchema}; use gotham::{hyper::Method, pipeline::chain::PipelineHandleChain, router::builder::*}; use once_cell::sync::Lazy; use regex::{Captures, Regex}; diff --git a/src/response.rs b/src/response.rs deleted file mode 100644 index 8f378ca..0000000 --- a/src/response.rs +++ /dev/null @@ -1,84 +0,0 @@ -use gotham::hyper::{ - header::{HeaderMap, HeaderName, HeaderValue}, - Body, StatusCode -}; -use mime::{Mime, APPLICATION_JSON}; - -/// A response, used to create the final gotham response from. -#[derive(Debug)] -pub struct Response { - pub(crate) status: StatusCode, - pub(crate) body: Body, - pub(crate) mime: Option, - pub(crate) headers: HeaderMap -} - -impl Response { - /// Create a new [Response] from raw data. - #[must_use = "Creating a response is pointless if you don't use it"] - pub fn new>(status: StatusCode, body: B, mime: Option) -> Self { - Self { - status, - body: body.into(), - mime, - headers: Default::default() - } - } - - /// Create a [Response] with mime type json from already serialized data. - #[must_use = "Creating a response is pointless if you don't use it"] - pub fn json>(status: StatusCode, body: B) -> Self { - Self { - status, - body: body.into(), - mime: Some(APPLICATION_JSON), - headers: Default::default() - } - } - - /// Create a _204 No Content_ [Response]. - #[must_use = "Creating a response is pointless if you don't use it"] - pub fn no_content() -> Self { - Self { - status: StatusCode::NO_CONTENT, - body: Body::empty(), - mime: None, - headers: Default::default() - } - } - - /// Create an empty _403 Forbidden_ [Response]. - #[must_use = "Creating a response is pointless if you don't use it"] - pub fn forbidden() -> Self { - Self { - status: StatusCode::FORBIDDEN, - body: Body::empty(), - mime: None, - headers: Default::default() - } - } - - /// Return the status code of this [Response]. - pub fn status(&self) -> StatusCode { - self.status - } - - /// Return the mime type of this [Response]. - pub fn mime(&self) -> Option<&Mime> { - self.mime.as_ref() - } - - /// Add an HTTP header to the [Response]. - pub fn header(&mut self, name: HeaderName, value: HeaderValue) { - self.headers.insert(name, value); - } - - #[cfg(test)] - pub(crate) fn full_body(mut self) -> Result, ::Error> { - use futures_executor::block_on; - use gotham::hyper::body::to_bytes; - - let bytes: &[u8] = &block_on(to_bytes(&mut self.body))?; - Ok(bytes.to_vec()) - } -} diff --git a/src/result/auth_result.rs b/src/response/auth_result.rs similarity index 85% rename from src/result/auth_result.rs rename to src/response/auth_result.rs index 1b1fd7f..d8b3300 100644 --- a/src/result/auth_result.rs +++ b/src/response/auth_result.rs @@ -12,9 +12,9 @@ pub enum AuthError { } /** -This return type can be used to map another [ResourceResult](crate::ResourceResult) that can -only be returned if the client is authenticated. Otherwise, an empty _403 Forbidden_ response -will be issued. +This return type can be used to wrap any type implementing [IntoResponse](crate::IntoResponse) +that can only be returned if the client is authenticated. Otherwise, an empty _403 Forbidden_ +response will be issued. Use can look something like this (assuming the `auth` feature is enabled): @@ -86,9 +86,9 @@ where } /** -This return type can be used to map another [ResourceResult](crate::ResourceResult) that can -only be returned if the client is authenticated. Otherwise, an empty _403 Forbidden_ response -will be issued. +This return type can be used to wrap any type implementing [IntoResponse](crate::IntoResponse) +that can only be returned if the client is authenticated. Otherwise, an empty _403 Forbidden_ +response will be issued. Use can look something like this (assuming the `auth` feature is enabled): diff --git a/src/response/mod.rs b/src/response/mod.rs new file mode 100644 index 0000000..b2796dc --- /dev/null +++ b/src/response/mod.rs @@ -0,0 +1,283 @@ +#[cfg(feature = "openapi")] +use crate::OpenapiSchema; + +use futures_util::future::{self, BoxFuture, FutureExt}; +use gotham::{ + handler::HandlerError, + hyper::{ + header::{HeaderMap, HeaderName, HeaderValue}, + Body, StatusCode + } +}; +use mime::{Mime, APPLICATION_JSON, STAR_STAR}; +use serde::Serialize; +use std::{ + convert::Infallible, + fmt::{Debug, Display}, + future::Future, + pin::Pin +}; + +mod auth_result; +pub use auth_result::{AuthError, AuthErrorOrOther, AuthResult, AuthSuccess}; + +mod no_content; +pub use no_content::NoContent; + +mod raw; +pub use raw::Raw; + +mod redirect; +pub use redirect::Redirect; + +#[allow(clippy::module_inception)] +mod result; +pub use result::IntoResponseError; + +mod success; +pub use success::Success; + +pub(crate) trait OrAllTypes { + fn or_all_types(self) -> Vec; +} + +impl OrAllTypes for Option> { + fn or_all_types(self) -> Vec { + self.unwrap_or_else(|| vec![STAR_STAR]) + } +} + +/// A response, used to create the final gotham response from. +#[derive(Debug)] +pub struct Response { + pub(crate) status: StatusCode, + pub(crate) body: Body, + pub(crate) mime: Option, + pub(crate) headers: HeaderMap +} + +impl Response { + /// Create a new [Response] from raw data. + #[must_use = "Creating a response is pointless if you don't use it"] + pub fn new>(status: StatusCode, body: B, mime: Option) -> Self { + Self { + status, + body: body.into(), + mime, + headers: Default::default() + } + } + + /// Create a [Response] with mime type json from already serialized data. + #[must_use = "Creating a response is pointless if you don't use it"] + pub fn json>(status: StatusCode, body: B) -> Self { + Self { + status, + body: body.into(), + mime: Some(APPLICATION_JSON), + headers: Default::default() + } + } + + /// Create a _204 No Content_ [Response]. + #[must_use = "Creating a response is pointless if you don't use it"] + pub fn no_content() -> Self { + Self { + status: StatusCode::NO_CONTENT, + body: Body::empty(), + mime: None, + headers: Default::default() + } + } + + /// Create an empty _403 Forbidden_ [Response]. + #[must_use = "Creating a response is pointless if you don't use it"] + pub fn forbidden() -> Self { + Self { + status: StatusCode::FORBIDDEN, + body: Body::empty(), + mime: None, + headers: Default::default() + } + } + + /// Return the status code of this [Response]. + pub fn status(&self) -> StatusCode { + self.status + } + + /// Return the mime type of this [Response]. + pub fn mime(&self) -> Option<&Mime> { + self.mime.as_ref() + } + + /// Add an HTTP header to the [Response]. + pub fn header(&mut self, name: HeaderName, value: HeaderValue) { + self.headers.insert(name, value); + } + + pub(crate) fn with_headers(mut self, headers: HeaderMap) -> Self { + self.headers = headers; + self + } + + #[cfg(test)] + pub(crate) fn full_body(mut self) -> Result, ::Error> { + use futures_executor::block_on; + use gotham::hyper::body::to_bytes; + + let bytes: &[u8] = &block_on(to_bytes(&mut self.body))?; + Ok(bytes.to_vec()) + } +} + +impl IntoResponse for Response { + type Err = Infallible; + + fn into_response(self) -> BoxFuture<'static, Result> { + future::ok(self).boxed() + } +} + +/// This trait needs to be implemented by every type returned from an endpoint to +/// to provide the response. +pub trait IntoResponse { + type Err: Into + Send + Sync + 'static; + + /// Turn this into a response that can be returned to the browser. This api will likely + /// change in the future. + fn into_response(self) -> BoxFuture<'static, Result>; + + /// Return a list of supported mime types. + fn accepted_types() -> Option> { + None + } +} + +/// Additional details for [IntoResponse] to be used with an OpenAPI-aware router. +#[cfg(feature = "openapi")] +pub trait ResponseSchema { + fn schema() -> OpenapiSchema; + + fn default_status() -> StatusCode { + StatusCode::OK + } +} + +#[cfg(feature = "openapi")] +mod private { + pub trait Sealed {} +} + +/// A trait provided to convert a resource's result to json, and provide an OpenAPI schema to the +/// router. This trait is implemented for all types that implement [IntoResponse] and +/// [ResponseSchema]. +#[cfg(feature = "openapi")] +pub trait IntoResponseWithSchema: IntoResponse + ResponseSchema + private::Sealed {} + +#[cfg(feature = "openapi")] +impl private::Sealed for R {} + +#[cfg(feature = "openapi")] +impl IntoResponseWithSchema for R {} + +/// The default json returned on an 500 Internal Server Error. +#[derive(Debug, Serialize)] +pub(crate) struct ResourceError { + error: bool, + message: String +} + +impl From for ResourceError { + fn from(message: T) -> Self { + Self { + error: true, + message: message.to_string() + } + } +} + +#[cfg(feature = "errorlog")] +fn errorlog(e: E) { + error!("The handler encountered an error: {}", e); +} + +#[cfg(not(feature = "errorlog"))] +fn errorlog(_e: E) {} + +fn handle_error(e: E) -> Pin> + Send>> +where + E: Display + IntoResponseError +{ + let msg = e.to_string(); + let res = e.into_response_error(); + match &res { + Ok(res) if res.status.is_server_error() => errorlog(msg), + Err(err) => { + errorlog(msg); + errorlog(&err); + }, + _ => {} + }; + future::ready(res).boxed() +} + +impl IntoResponse for Pin + Send>> +where + Res: IntoResponse + 'static +{ + type Err = Res::Err; + + fn into_response(self) -> Pin> + Send>> { + self.then(IntoResponse::into_response).boxed() + } + + fn accepted_types() -> Option> { + Res::accepted_types() + } +} + +#[cfg(feature = "openapi")] +impl ResponseSchema for Pin + Send>> +where + Res: ResponseSchema +{ + fn schema() -> OpenapiSchema { + Res::schema() + } + + #[cfg(feature = "openapi")] + fn default_status() -> StatusCode { + Res::default_status() + } +} + +#[cfg(test)] +mod test { + use super::*; + use futures_executor::block_on; + use thiserror::Error; + + #[derive(Debug, Default, Deserialize, Serialize)] + #[cfg_attr(feature = "openapi", derive(crate::OpenapiType))] + struct Msg { + msg: String + } + + #[derive(Debug, Default, Error)] + #[error("An Error")] + struct MsgError; + + #[test] + fn result_from_future() { + let nc = NoContent::default(); + let res = block_on(nc.into_response()).unwrap(); + + let fut_nc = async move { NoContent::default() }.boxed(); + let fut_res = block_on(fut_nc.into_response()).unwrap(); + + assert_eq!(res.status, fut_res.status); + assert_eq!(res.mime, fut_res.mime); + assert_eq!(res.full_body().unwrap(), fut_res.full_body().unwrap()); + } +} diff --git a/src/result/no_content.rs b/src/response/no_content.rs similarity index 66% rename from src/result/no_content.rs rename to src/response/no_content.rs index e6072ed..73159c1 100644 --- a/src/result/no_content.rs +++ b/src/response/no_content.rs @@ -1,9 +1,9 @@ -use super::{handle_error, ResourceResult}; +use super::{handle_error, IntoResponse}; use crate::{IntoResponseError, Response}; #[cfg(feature = "openapi")] -use crate::{OpenapiSchema, OpenapiType, ResourceResultSchema}; - +use crate::{OpenapiSchema, OpenapiType, ResponseSchema}; use futures_util::{future, future::FutureExt}; +use gotham::hyper::header::{HeaderMap, HeaderValue, IntoHeaderName}; #[cfg(feature = "openapi")] use gotham::hyper::StatusCode; use mime::Mime; @@ -31,22 +31,36 @@ fn read_all() { # } ``` */ -#[derive(Clone, Copy, Debug, Default)] -pub struct NoContent; +#[derive(Clone, Debug, Default)] +pub struct NoContent { + headers: HeaderMap +} impl From<()> for NoContent { fn from(_: ()) -> Self { - Self {} + Self::default() } } -impl ResourceResult for NoContent { +impl NoContent { + /// Set a custom HTTP header. If a header with this name was set before, its value is being updated. + pub fn header(&mut self, name: K, value: HeaderValue) { + self.headers.insert(name, value); + } + + /// Allow manipulating HTTP headers. + pub fn headers_mut(&mut self) -> &mut HeaderMap { + &mut self.headers + } +} + +impl IntoResponse for NoContent { // TODO this shouldn't be a serde_json::Error type Err = serde_json::Error; // just for easier handling of `Result` /// This will always be a _204 No Content_ together with an empty string. fn into_response(self) -> Pin> + Send>> { - future::ok(Response::no_content()).boxed() + future::ok(Response::no_content().with_headers(self.headers)).boxed() } fn accepted_types() -> Option> { @@ -55,7 +69,7 @@ impl ResourceResult for NoContent { } #[cfg(feature = "openapi")] -impl ResourceResultSchema for NoContent { +impl ResponseSchema for NoContent { /// Returns the schema of the `()` type. fn schema() -> OpenapiSchema { <()>::schema() @@ -67,7 +81,7 @@ impl ResourceResultSchema for NoContent { } } -impl ResourceResult for Result +impl IntoResponse for Result where E: Display + IntoResponseError { @@ -86,12 +100,12 @@ where } #[cfg(feature = "openapi")] -impl ResourceResultSchema for Result +impl ResponseSchema for Result where E: Display + IntoResponseError { fn schema() -> OpenapiSchema { - ::schema() + ::schema() } #[cfg(feature = "openapi")] @@ -104,7 +118,7 @@ where mod test { use super::*; use futures_executor::block_on; - use gotham::hyper::StatusCode; + use gotham::hyper::{header::ACCESS_CONTROL_ALLOW_ORIGIN, StatusCode}; use thiserror::Error; #[derive(Debug, Default, Error)] @@ -118,6 +132,9 @@ mod test { assert_eq!(res.status, StatusCode::NO_CONTENT); assert_eq!(res.mime, None); assert_eq!(res.full_body().unwrap(), &[] as &[u8]); + + #[cfg(feature = "openapi")] + assert_eq!(NoContent::default_status(), StatusCode::NO_CONTENT); } #[test] @@ -128,4 +145,13 @@ mod test { assert_eq!(res.mime, None); assert_eq!(res.full_body().unwrap(), &[] as &[u8]); } + + #[test] + fn no_content_custom_headers() { + let mut no_content = NoContent::default(); + no_content.header(ACCESS_CONTROL_ALLOW_ORIGIN, HeaderValue::from_static("*")); + let res = block_on(no_content.into_response()).expect("didn't expect error response"); + let cors = res.headers.get(ACCESS_CONTROL_ALLOW_ORIGIN); + assert_eq!(cors.map(|value| value.to_str().unwrap()), Some("*")); + } } diff --git a/src/result/raw.rs b/src/response/raw.rs similarity index 85% rename from src/result/raw.rs rename to src/response/raw.rs index 759adbb..6c003dc 100644 --- a/src/result/raw.rs +++ b/src/response/raw.rs @@ -1,7 +1,7 @@ -use super::{handle_error, IntoResponseError, ResourceResult}; +use super::{handle_error, IntoResponse, IntoResponseError}; use crate::{FromBody, RequestBody, ResourceType, Response}; #[cfg(feature = "openapi")] -use crate::{OpenapiSchema, OpenapiType, ResourceResultSchema}; +use crate::{IntoResponseWithSchema, OpenapiSchema, OpenapiType, ResponseSchema}; use futures_core::future::Future; use futures_util::{future, future::FutureExt}; @@ -99,7 +99,7 @@ impl OpenapiType for Raw { } } -impl> ResourceResult for Raw +impl> IntoResponse for Raw where Self: Send { @@ -111,7 +111,7 @@ where } #[cfg(feature = "openapi")] -impl> ResourceResultSchema for Raw +impl> ResponseSchema for Raw where Self: Send { @@ -120,10 +120,10 @@ where } } -impl ResourceResult for Result, E> +impl IntoResponse for Result, E> where - Raw: ResourceResult, - E: Display + IntoResponseError as ResourceResult>::Err> + Raw: IntoResponse, + E: Display + IntoResponseError as IntoResponse>::Err> { type Err = E::Err; @@ -136,13 +136,13 @@ where } #[cfg(feature = "openapi")] -impl ResourceResultSchema for Result, E> +impl ResponseSchema for Result, E> where - Raw: ResourceResult + ResourceResultSchema, - E: Display + IntoResponseError as ResourceResult>::Err> + Raw: IntoResponseWithSchema, + E: Display + IntoResponseError as IntoResponse>::Err> { fn schema() -> OpenapiSchema { - as ResourceResultSchema>::schema() + as ResponseSchema>::schema() } } diff --git a/src/result/redirect.rs b/src/response/redirect.rs similarity index 89% rename from src/result/redirect.rs rename to src/response/redirect.rs index 0a1933c..8b6e854 100644 --- a/src/result/redirect.rs +++ b/src/response/redirect.rs @@ -1,7 +1,7 @@ -use super::{handle_error, ResourceResult}; +use super::{handle_error, IntoResponse}; use crate::{IntoResponseError, Response}; #[cfg(feature = "openapi")] -use crate::{NoContent, OpenapiSchema, ResourceResultSchema}; +use crate::{NoContent, OpenapiSchema, ResponseSchema}; use futures_util::future::{BoxFuture, FutureExt, TryFutureExt}; use gotham::hyper::{ header::{InvalidHeaderValue, LOCATION}, @@ -42,7 +42,7 @@ pub struct Redirect { pub to: String } -impl ResourceResult for Redirect { +impl IntoResponse for Redirect { type Err = InvalidHeaderValue; fn into_response(self) -> BoxFuture<'static, Result> { @@ -56,13 +56,13 @@ impl ResourceResult for Redirect { } #[cfg(feature = "openapi")] -impl ResourceResultSchema for Redirect { +impl ResponseSchema for Redirect { fn default_status() -> StatusCode { StatusCode::SEE_OTHER } fn schema() -> OpenapiSchema { - ::schema() + ::schema() } } @@ -76,7 +76,7 @@ pub enum RedirectError { } #[allow(ambiguous_associated_items)] // an enum variant is not a type. never. -impl ResourceResult for Result +impl IntoResponse for Result where E: Display + IntoResponseError, ::Err: StdError + Sync @@ -92,7 +92,7 @@ where } #[cfg(feature = "openapi")] -impl ResourceResultSchema for Result +impl ResponseSchema for Result where E: Display + IntoResponseError, ::Err: StdError + Sync @@ -102,7 +102,7 @@ where } fn schema() -> OpenapiSchema { - ::schema() + ::schema() } } diff --git a/src/result/result.rs b/src/response/result.rs similarity index 85% rename from src/result/result.rs rename to src/response/result.rs index deaa45b..a28803f 100644 --- a/src/result/result.rs +++ b/src/response/result.rs @@ -1,7 +1,7 @@ -use super::{handle_error, into_response_helper, ResourceResult}; -use crate::{result::ResourceError, Response, ResponseBody}; +use super::{handle_error, IntoResponse, ResourceError}; #[cfg(feature = "openapi")] -use crate::{OpenapiSchema, ResourceResultSchema}; +use crate::{OpenapiSchema, ResponseSchema}; +use crate::{Response, ResponseBody, Success}; use futures_core::future::Future; use gotham::hyper::StatusCode; @@ -26,7 +26,7 @@ impl IntoResponseError for E { } } -impl ResourceResult for Result +impl IntoResponse for Result where R: ResponseBody, E: Display + IntoResponseError @@ -35,7 +35,7 @@ where fn into_response(self) -> Pin> + Send>> { match self { - Ok(r) => into_response_helper(|| Ok(Response::json(StatusCode::OK, serde_json::to_string(&r)?))), + Ok(r) => Success::from(r).into_response(), Err(e) => handle_error(e) } } @@ -46,7 +46,7 @@ where } #[cfg(feature = "openapi")] -impl ResourceResultSchema for Result +impl ResponseSchema for Result where R: ResponseBody, E: Display + IntoResponseError @@ -59,7 +59,7 @@ where #[cfg(test)] mod test { use super::*; - use crate::result::OrAllTypes; + use crate::response::OrAllTypes; use futures_executor::block_on; use thiserror::Error; diff --git a/src/result/success.rs b/src/response/success.rs similarity index 52% rename from src/result/success.rs rename to src/response/success.rs index 3c1bcaa..24d6b3c 100644 --- a/src/result/success.rs +++ b/src/response/success.rs @@ -1,19 +1,17 @@ -use super::{into_response_helper, ResourceResult}; +use super::IntoResponse; #[cfg(feature = "openapi")] -use crate::{OpenapiSchema, ResourceResultSchema}; +use crate::{OpenapiSchema, ResponseSchema}; use crate::{Response, ResponseBody}; -use gotham::hyper::StatusCode; -use mime::{Mime, APPLICATION_JSON}; -use std::{ - fmt::Debug, - future::Future, - ops::{Deref, DerefMut}, - pin::Pin +use futures_util::future::{self, FutureExt}; +use gotham::hyper::{ + header::{HeaderMap, HeaderValue, IntoHeaderName}, + StatusCode }; +use mime::{Mime, APPLICATION_JSON}; +use std::{fmt::Debug, future::Future, pin::Pin}; /** -This can be returned from a resource when there is no cause of an error. It behaves similar to a -smart pointer like box, it that it implements [AsRef], [Deref] and the likes. +This can be returned from a resource when there is no cause of an error. Usage example: @@ -42,63 +40,40 @@ fn read_all() -> Success { # } ``` */ -#[derive(Debug)] -pub struct Success(T); - -impl AsMut for Success { - fn as_mut(&mut self) -> &mut T { - &mut self.0 - } -} - -impl AsRef for Success { - fn as_ref(&self) -> &T { - &self.0 - } -} - -impl Deref for Success { - type Target = T; - - fn deref(&self) -> &T { - &self.0 - } -} - -impl DerefMut for Success { - fn deref_mut(&mut self) -> &mut T { - &mut self.0 - } +#[derive(Clone, Debug, Default)] +pub struct Success { + value: T, + headers: HeaderMap } impl From for Success { fn from(t: T) -> Self { - Self(t) + Self { + value: t, + headers: HeaderMap::new() + } } } -impl Clone for Success { - fn clone(&self) -> Self { - Self(self.0.clone()) +impl Success { + /// Set a custom HTTP header. If a header with this name was set before, its value is being updated. + pub fn header(&mut self, name: K, value: HeaderValue) { + self.headers.insert(name, value); + } + + /// Allow manipulating HTTP headers. + pub fn headers_mut(&mut self) -> &mut HeaderMap { + &mut self.headers } } -impl Copy for Success {} - -impl Default for Success { - fn default() -> Self { - Self(T::default()) - } -} - -impl ResourceResult for Success -where - Self: Send -{ +impl IntoResponse for Success { type Err = serde_json::Error; fn into_response(self) -> Pin> + Send>> { - into_response_helper(|| Ok(Response::json(StatusCode::OK, serde_json::to_string(self.as_ref())?))) + let res = + serde_json::to_string(&self.value).map(|body| Response::json(StatusCode::OK, body).with_headers(self.headers)); + future::ready(res).boxed() } fn accepted_types() -> Option> { @@ -107,10 +82,7 @@ where } #[cfg(feature = "openapi")] -impl ResourceResultSchema for Success -where - Self: Send -{ +impl ResponseSchema for Success { fn schema() -> OpenapiSchema { T::schema() } @@ -119,8 +91,9 @@ where #[cfg(test)] mod test { use super::*; - use crate::result::OrAllTypes; + use crate::response::OrAllTypes; use futures_executor::block_on; + use gotham::hyper::header::ACCESS_CONTROL_ALLOW_ORIGIN; #[derive(Debug, Default, Serialize)] #[cfg_attr(feature = "openapi", derive(crate::OpenapiType))] @@ -135,6 +108,17 @@ mod test { assert_eq!(res.status, StatusCode::OK); assert_eq!(res.mime, Some(APPLICATION_JSON)); assert_eq!(res.full_body().unwrap(), br#"{"msg":""}"#); + #[cfg(feature = "openapi")] + assert_eq!(>::default_status(), StatusCode::OK); + } + + #[test] + fn success_custom_headers() { + let mut success: Success = Msg::default().into(); + success.header(ACCESS_CONTROL_ALLOW_ORIGIN, HeaderValue::from_static("*")); + let res = block_on(success.into_response()).expect("didn't expect error response"); + let cors = res.headers.get(ACCESS_CONTROL_ALLOW_ORIGIN); + assert_eq!(cors.map(|value| value.to_str().unwrap()), Some("*")); } #[test] diff --git a/src/result/mod.rs b/src/result/mod.rs deleted file mode 100644 index a119a82..0000000 --- a/src/result/mod.rs +++ /dev/null @@ -1,197 +0,0 @@ -#[cfg(feature = "openapi")] -use crate::OpenapiSchema; -use crate::Response; - -use futures_util::future::{BoxFuture, FutureExt}; -use gotham::handler::HandlerError; -#[cfg(feature = "openapi")] -use gotham::hyper::StatusCode; -use mime::{Mime, STAR_STAR}; -use serde::Serialize; -use std::{ - fmt::{Debug, Display}, - future::Future, - pin::Pin -}; - -mod auth_result; -pub use auth_result::{AuthError, AuthErrorOrOther, AuthResult, AuthSuccess}; - -mod no_content; -pub use no_content::NoContent; - -mod raw; -pub use raw::Raw; - -mod redirect; -pub use redirect::Redirect; - -#[allow(clippy::module_inception)] -mod result; -pub use result::IntoResponseError; - -mod success; -pub use success::Success; - -pub(crate) trait OrAllTypes { - fn or_all_types(self) -> Vec; -} - -impl OrAllTypes for Option> { - fn or_all_types(self) -> Vec { - self.unwrap_or_else(|| vec![STAR_STAR]) - } -} - -/// A trait provided to convert a resource's result to json. -pub trait ResourceResult { - type Err: Into + Send + Sync + 'static; - - /// Turn this into a response that can be returned to the browser. This api will likely - /// change in the future. - fn into_response(self) -> BoxFuture<'static, Result>; - - /// Return a list of supported mime types. - fn accepted_types() -> Option> { - None - } -} - -/// Additional details for [ResourceResult] to be used with an OpenAPI-aware router. -#[cfg(feature = "openapi")] -pub trait ResourceResultSchema { - fn schema() -> OpenapiSchema; - - fn default_status() -> StatusCode { - StatusCode::OK - } -} - -#[cfg(feature = "openapi")] -mod private { - pub trait Sealed {} -} - -/// A trait provided to convert a resource's result to json, and provide an OpenAPI schema to the -/// router. This trait is implemented for all types that implement [ResourceResult] and -/// [ResourceResultSchema]. -#[cfg(feature = "openapi")] -pub trait ResourceResultWithSchema: ResourceResult + ResourceResultSchema + private::Sealed {} - -#[cfg(feature = "openapi")] -impl private::Sealed for R {} - -#[cfg(feature = "openapi")] -impl ResourceResultWithSchema for R {} - -/// The default json returned on an 500 Internal Server Error. -#[derive(Debug, Serialize)] -pub(crate) struct ResourceError { - error: bool, - message: String -} - -impl From for ResourceError { - fn from(message: T) -> Self { - Self { - error: true, - message: message.to_string() - } - } -} - -fn into_response_helper(create_response: F) -> Pin> + Send>> -where - Err: Send + 'static, - F: FnOnce() -> Result -{ - let res = create_response(); - async move { res }.boxed() -} - -#[cfg(feature = "errorlog")] -fn errorlog(e: E) { - error!("The handler encountered an error: {}", e); -} - -#[cfg(not(feature = "errorlog"))] -fn errorlog(_e: E) {} - -fn handle_error(e: E) -> Pin> + Send>> -where - E: Display + IntoResponseError -{ - into_response_helper(|| { - let msg = e.to_string(); - let res = e.into_response_error(); - match &res { - Ok(res) if res.status.is_server_error() => errorlog(msg), - Err(err) => { - errorlog(msg); - errorlog(&err); - }, - _ => {} - }; - res - }) -} - -impl ResourceResult for Pin + Send>> -where - Res: ResourceResult + 'static -{ - type Err = Res::Err; - - fn into_response(self) -> Pin> + Send>> { - self.then(ResourceResult::into_response).boxed() - } - - fn accepted_types() -> Option> { - Res::accepted_types() - } -} - -#[cfg(feature = "openapi")] -impl ResourceResultSchema for Pin + Send>> -where - Res: ResourceResultSchema -{ - fn schema() -> OpenapiSchema { - Res::schema() - } - - #[cfg(feature = "openapi")] - fn default_status() -> StatusCode { - Res::default_status() - } -} - -#[cfg(test)] -mod test { - use super::*; - use futures_executor::block_on; - use thiserror::Error; - - #[derive(Debug, Default, Deserialize, Serialize)] - #[cfg_attr(feature = "openapi", derive(crate::OpenapiType))] - struct Msg { - msg: String - } - - #[derive(Debug, Default, Error)] - #[error("An Error")] - struct MsgError; - - #[test] - fn result_from_future() { - let nc = NoContent::default(); - let res = block_on(nc.into_response()).unwrap(); - - let fut_nc = async move { NoContent::default() }.boxed(); - let fut_res = block_on(fut_nc.into_response()).unwrap(); - - assert_eq!(res.status, fut_res.status); - assert_eq!(res.mime, fut_res.mime); - assert_eq!(res.full_body().unwrap(), fut_res.full_body().unwrap()); - } -} diff --git a/src/routing.rs b/src/routing.rs index 132eed6..f41dc93 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -3,10 +3,7 @@ use crate::openapi::{ builder::{OpenapiBuilder, OpenapiInfo}, router::OpenapiRouter }; -use crate::{ - result::{ResourceError, ResourceResult}, - Endpoint, FromBody, Resource, Response -}; +use crate::{response::ResourceError, Endpoint, FromBody, IntoResponse, Resource, Response}; #[cfg(feature = "cors")] use gotham::router::route::matcher::AccessControlRequestMethodMatcher; @@ -90,7 +87,7 @@ fn response_from(res: Response, state: &State) -> gotham::hyper::Response async fn endpoint_handler(state: &mut State) -> Result, HandlerError> where E: Endpoint, - ::Err: Into + ::Err: Into { trace!("entering endpoint_handler"); let placeholders = E::Placeholders::take_from(state); diff --git a/tests/ui/endpoint/invalid_return_type.stderr b/tests/ui/endpoint/invalid_return_type.stderr index 2879898..69d5f39 100644 --- a/tests/ui/endpoint/invalid_return_type.stderr +++ b/tests/ui/endpoint/invalid_return_type.stderr @@ -1,21 +1,21 @@ -error[E0277]: the trait bound `FooResponse: ResourceResultSchema` is not satisfied +error[E0277]: the trait bound `FooResponse: ResponseSchema` is not satisfied --> $DIR/invalid_return_type.rs:12:18 | 12 | fn endpoint() -> FooResponse { - | ^^^^^^^^^^^ the trait `ResourceResultSchema` is not implemented for `FooResponse` + | ^^^^^^^^^^^ the trait `ResponseSchema` is not implemented for `FooResponse` | ::: $WORKSPACE/src/endpoint.rs | - | #[openapi_bound("Output: crate::ResourceResultSchema")] - | ------------------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Output` + | #[openapi_bound("Output: crate::ResponseSchema")] + | ------------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Output` -error[E0277]: the trait bound `FooResponse: ResourceResult` is not satisfied +error[E0277]: the trait bound `FooResponse: gotham_restful::IntoResponse` is not satisfied --> $DIR/invalid_return_type.rs:12:18 | 12 | fn endpoint() -> FooResponse { - | ^^^^^^^^^^^ the trait `ResourceResult` is not implemented for `FooResponse` + | ^^^^^^^^^^^ the trait `gotham_restful::IntoResponse` is not implemented for `FooResponse` | ::: $WORKSPACE/src/endpoint.rs | - | type Output: ResourceResult + Send; - | -------------- required by this bound in `gotham_restful::EndpointWithSchema::Output` + | type Output: IntoResponse + Send; + | ------------ required by this bound in `gotham_restful::EndpointWithSchema::Output` From bb6f5b0fddb326079a6ace7b3b27abd7629cdb1d Mon Sep 17 00:00:00 2001 From: Dominic Date: Sat, 27 Feb 2021 17:16:04 +0100 Subject: [PATCH 151/170] release 0.2.0 --- CHANGELOG.md | 2 ++ Cargo.toml | 4 ++-- derive/Cargo.toml | 3 +-- example/Cargo.toml | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec4e9a3..0aa0e81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + +## [0.2.0] - 2021-02-27 ### Added - Support custom HTTP response headers - New `endpoint` router extension with associated `Endpoint` trait ([!18]) diff --git a/Cargo.toml b/Cargo.toml index bf96d7c..6f81fe0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ members = [".", "./derive", "./example"] [package] name = "gotham_restful" -version = "0.2.0-dev" +version = "0.2.0" authors = ["Dominic Meiser "] edition = "2018" description = "RESTful additions for the gotham web framework" @@ -24,7 +24,7 @@ futures-core = "0.3.7" futures-util = "0.3.7" gotham = { version = "0.5.0", default-features = false } gotham_derive = "0.5.0" -gotham_restful_derive = "0.2.0-dev" +gotham_restful_derive = "0.2.0" log = "0.4.8" mime = "0.3.16" serde = { version = "1.0.110", features = ["derive"] } diff --git a/derive/Cargo.toml b/derive/Cargo.toml index adc9d37..e06f2b0 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "gotham_restful_derive" -version = "0.2.0-dev" +version = "0.2.0" authors = ["Dominic Meiser "] edition = "2018" description = "Derive macros for gotham_restful" @@ -18,7 +18,6 @@ proc-macro = true gitlab = { repository = "msrd0/gotham-restful", branch = "master" } [dependencies] -heck = "0.3.1" once_cell = "1.5" paste = "1.0" proc-macro2 = "1.0.13" diff --git a/example/Cargo.toml b/example/Cargo.toml index 4ddcd5f..398a5ef 100644 --- a/example/Cargo.toml +++ b/example/Cargo.toml @@ -18,7 +18,7 @@ gitlab = { repository = "msrd0/gotham-restful", branch = "master" } fake = "2.2.2" gotham = { version = "0.5.0", default-features = false } gotham_derive = "0.5.0" -gotham_restful = { version = "0.2.0-dev", features = ["auth", "cors", "openapi"], default-features = false } +gotham_restful = { version = "0.2.0", features = ["auth", "cors", "openapi"], default-features = false } log = "0.4.8" pretty_env_logger = "0.4" serde = "1.0.110" From 960ba0e8bcc5bcc8bcce8e6c847766b5a9b10771 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 28 Feb 2021 01:59:59 +0100 Subject: [PATCH 152/170] track gotham master --- Cargo.toml | 8 ++++---- tests/async_methods.rs | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6f81fe0..0f4621c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ members = [".", "./derive", "./example"] [package] name = "gotham_restful" -version = "0.2.0" +version = "0.3.0-dev" authors = ["Dominic Meiser "] edition = "2018" description = "RESTful additions for the gotham web framework" @@ -22,7 +22,7 @@ gitlab = { repository = "msrd0/gotham-restful", branch = "master" } [dependencies] futures-core = "0.3.7" futures-util = "0.3.7" -gotham = { version = "0.5.0", default-features = false } +gotham = { git = "https://github.com/gotham-rs/gotham", default-features = false } gotham_derive = "0.5.0" gotham_restful_derive = "0.2.0" log = "0.4.8" @@ -37,7 +37,7 @@ uuid = { version = "0.8.1", optional = true } # non-feature optional dependencies base64 = { version = "0.13.0", optional = true } -cookie = { version = "0.14", optional = true } +cookie = { version = "0.15", optional = true } gotham_middleware_diesel = { version = "0.2.0", optional = true } indexmap = { version = "1.3.2", optional = true } indoc = { version = "1.0", optional = true } @@ -52,7 +52,7 @@ diesel = { version = "1.4.4", features = ["postgres"] } futures-executor = "0.3.5" paste = "1.0" pretty_env_logger = "0.4" -tokio = { version = "0.2", features = ["time"], default-features = false } +tokio = { version = "1.0", features = ["time"], default-features = false } thiserror = "1.0.18" trybuild = "1.0.27" diff --git a/tests/async_methods.rs b/tests/async_methods.rs index 35ec42a..21a74b5 100644 --- a/tests/async_methods.rs +++ b/tests/async_methods.rs @@ -10,7 +10,7 @@ use gotham::{ use gotham_restful::*; use mime::{APPLICATION_JSON, TEXT_PLAIN}; use serde::Deserialize; -use tokio::time::{delay_for, Duration}; +use tokio::time::{sleep, Duration}; mod util { include!("util/mod.rs"); @@ -86,9 +86,9 @@ async fn remove(_id: u64) -> Raw<&'static [u8]> { const STATE_TEST_RESPONSE: &[u8] = b"xxJbxOuwioqR5DfzPuVqvaqRSfpdNQGluIvHU4n1LM"; #[endpoint(method = "Method::GET", uri = "state_test")] async fn state_test(state: &mut State) -> Raw<&'static [u8]> { - delay_for(Duration::from_nanos(1)).await; + sleep(Duration::from_nanos(1)).await; state.borrow::(); - delay_for(Duration::from_nanos(1)).await; + sleep(Duration::from_nanos(1)).await; Raw::new(STATE_TEST_RESPONSE, TEXT_PLAIN) } From fabcbc4e78e75bfd9f297a255abb67b387ec2b51 Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 3 Mar 2021 23:44:53 +0100 Subject: [PATCH 153/170] pin version of openapiv3 they introduced breaking changes in patch release 0.3.3 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 6f81fe0..f9d03c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,7 @@ indexmap = { version = "1.3.2", optional = true } indoc = { version = "1.0", optional = true } jsonwebtoken = { version = "7.1.0", optional = true } once_cell = { version = "1.5", optional = true } -openapiv3 = { version = "0.3.2", optional = true } +openapiv3 = { version = "=0.3.2", optional = true } regex = { version = "1.4", optional = true } sha2 = { version = "0.9.3", optional = true } From 09dee5a673ac29ec38ab6f1442f403b20a776959 Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 3 Mar 2021 23:46:13 +0100 Subject: [PATCH 154/170] update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0aa0e81..dc51188 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.2.1] - 2021-03-04 +### Changed + - Pin version of `openapiv3` dependency to `0.3.2` + ## [0.2.0] - 2021-02-27 ### Added - Support custom HTTP response headers From 90870e3b6a16bcd41859d3491bb05c16faa4afd4 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 7 Mar 2021 19:05:25 +0100 Subject: [PATCH 155/170] basic structure for openapi_type crate --- Cargo.toml | 4 +- openapi_type/Cargo.toml | 20 ++++ openapi_type/src/lib.rs | 76 +++++++++++++++ .../tests/fail/generics_not_openapitype.rs | 12 +++ .../fail/generics_not_openapitype.stderr | 21 +++++ openapi_type/tests/pass/unit_struct.rs | 6 ++ openapi_type/tests/trybuild.rs | 8 ++ openapi_type_derive/Cargo.toml | 22 +++++ openapi_type_derive/src/lib.rs | 93 +++++++++++++++++++ 9 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 openapi_type/Cargo.toml create mode 100644 openapi_type/src/lib.rs create mode 100644 openapi_type/tests/fail/generics_not_openapitype.rs create mode 100644 openapi_type/tests/fail/generics_not_openapitype.stderr create mode 100644 openapi_type/tests/pass/unit_struct.rs create mode 100644 openapi_type/tests/trybuild.rs create mode 100644 openapi_type_derive/Cargo.toml create mode 100644 openapi_type_derive/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 08bf06d..7c0baee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ # -*- eval: (cargo-minor-mode 1) -*- [workspace] -members = [".", "./derive", "./example"] +members = [".", "./derive", "./example", "./openapi_type", "./openapi_type_derive"] [package] name = "gotham_restful" @@ -76,3 +76,5 @@ features = ["full"] [patch.crates-io] gotham_restful = { path = "." } gotham_restful_derive = { path = "./derive" } +openapi_type = { path = "./openapi_type" } +openapi_type_derive = { path = "./openapi_type_derive" } diff --git a/openapi_type/Cargo.toml b/openapi_type/Cargo.toml new file mode 100644 index 0000000..5e2972a --- /dev/null +++ b/openapi_type/Cargo.toml @@ -0,0 +1,20 @@ +# -*- eval: (cargo-minor-mode 1) -*- + +[package] +workspace = ".." +name = "openapi_type" +version = "0.1.0-dev" +authors = ["Dominic Meiser "] +edition = "2018" +description = "OpenAPI type information for Rust structs and enums" +keywords = ["openapi", "type"] +license = "Apache-2.0" +repository = "https://gitlab.com/msrd0/gotham-restful/-/tree/master/openapi_type" + +[dependencies] +indexmap = "1.6" +openapi_type_derive = "0.1.0-dev" +openapiv3 = "=0.3.2" + +[dev-dependencies] +trybuild = "1.0" diff --git a/openapi_type/src/lib.rs b/openapi_type/src/lib.rs new file mode 100644 index 0000000..b5c0341 --- /dev/null +++ b/openapi_type/src/lib.rs @@ -0,0 +1,76 @@ +#![warn(missing_debug_implementations, rust_2018_idioms)] +#![forbid(unsafe_code)] +#![cfg_attr(feature = "cargo-clippy", allow(clippy::tabs_in_doc_comments))] +/*! +TODO +*/ + +pub use indexmap; +pub use openapi_type_derive::OpenapiType; +pub use openapiv3 as openapi; + +use indexmap::IndexMap; +use openapi::{Schema, SchemaData, SchemaKind}; + +// TODO update the documentation +/** +This struct needs to be available for every type that can be part of an OpenAPI Spec. It is +already implemented for primitive types, String, Vec, Option and the like. To have it available +for your type, simply derive from [OpenapiType]. +*/ +#[derive(Debug, Clone, PartialEq)] +pub struct OpenapiSchema { + /// The name of this schema. If it is None, the schema will be inlined. + pub name: Option, + /// 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 + /// an optional parameter. + pub nullable: bool, + /// The actual OpenAPI schema. + pub schema: SchemaKind, + /// Other schemas that this schema depends on. They will be included in the final OpenAPI Spec + /// along with this schema. + pub dependencies: IndexMap +} + +impl OpenapiSchema { + /// Create a new schema that has no name. + pub fn new(schema: SchemaKind) -> Self { + Self { + name: None, + nullable: false, + schema, + dependencies: IndexMap::new() + } + } + + /// Convert this schema to a [Schema] that can be serialized to the OpenAPI Spec. + pub fn into_schema(self) -> Schema { + Schema { + schema_data: SchemaData { + nullable: self.nullable, + title: self.name, + ..Default::default() + }, + schema_kind: self.schema + } + } +} + +/** +This trait needs to be implemented by every type that is being used in the OpenAPI Spec. It gives +access to the [OpenapiSchema] of this type. It is provided for primitive types, String and the +like. For use on your own types, there is a derive macro: + +``` +# #[macro_use] extern crate openapi_type_derive; +# +#[derive(OpenapiType)] +struct MyResponse { + message: String +} +``` +*/ +pub trait OpenapiType { + fn schema() -> OpenapiSchema; +} diff --git a/openapi_type/tests/fail/generics_not_openapitype.rs b/openapi_type/tests/fail/generics_not_openapitype.rs new file mode 100644 index 0000000..3d2a09d --- /dev/null +++ b/openapi_type/tests/fail/generics_not_openapitype.rs @@ -0,0 +1,12 @@ +use openapi_type::OpenapiType; + +#[derive(OpenapiType)] +struct Foo { + bar: T +} + +struct Bar; + +fn main() { + >::schema(); +} diff --git a/openapi_type/tests/fail/generics_not_openapitype.stderr b/openapi_type/tests/fail/generics_not_openapitype.stderr new file mode 100644 index 0000000..193eadb --- /dev/null +++ b/openapi_type/tests/fail/generics_not_openapitype.stderr @@ -0,0 +1,21 @@ +error[E0599]: no function or associated item named `schema` found for struct `Foo` in the current scope + --> $DIR/generics_not_openapitype.rs:11:14 + | +4 | struct Foo { + | ------------- + | | + | function or associated item `schema` not found for this + | doesn't satisfy `Foo: OpenapiType` +... +8 | struct Bar; + | ----------- doesn't satisfy `Bar: OpenapiType` +... +11 | >::schema(); + | ^^^^^^ function or associated item not found in `Foo` + | + = note: the method `schema` exists but the following trait bounds were not satisfied: + `Bar: OpenapiType` + which is required by `Foo: OpenapiType` + = help: items from traits can only be used if the trait is implemented and in scope + = note: the following trait defines an item `schema`, perhaps you need to implement it: + candidate #1: `OpenapiType` diff --git a/openapi_type/tests/pass/unit_struct.rs b/openapi_type/tests/pass/unit_struct.rs new file mode 100644 index 0000000..79f6443 --- /dev/null +++ b/openapi_type/tests/pass/unit_struct.rs @@ -0,0 +1,6 @@ +use openapi_type_derive::OpenapiType; + +#[derive(OpenapiType)] +struct Foo; + +fn main() {} diff --git a/openapi_type/tests/trybuild.rs b/openapi_type/tests/trybuild.rs new file mode 100644 index 0000000..28574f1 --- /dev/null +++ b/openapi_type/tests/trybuild.rs @@ -0,0 +1,8 @@ +use trybuild::TestCases; + +#[test] +fn trybuild() { + let t = TestCases::new(); + t.pass("tests/pass/*.rs"); + t.compile_fail("tests/fail/*.rs"); +} diff --git a/openapi_type_derive/Cargo.toml b/openapi_type_derive/Cargo.toml new file mode 100644 index 0000000..c1cbedc --- /dev/null +++ b/openapi_type_derive/Cargo.toml @@ -0,0 +1,22 @@ +# -*- eval: (cargo-minor-mode 1) -*- + +[package] +workspace = ".." +name = "openapi_type_derive" +version = "0.1.0-dev" +authors = ["Dominic Meiser "] +edition = "2018" +description = "Implementation detail of the openapi_type crate" +license = "Apache-2.0" +repository = "https://gitlab.com/msrd0/gotham-restful/-/tree/master/openapi_type_derive" + +# tests are done using trybuild exclusively +autotests = false + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0" +quote = "1.0" +syn = "1.0" diff --git a/openapi_type_derive/src/lib.rs b/openapi_type_derive/src/lib.rs new file mode 100644 index 0000000..bc049b3 --- /dev/null +++ b/openapi_type_derive/src/lib.rs @@ -0,0 +1,93 @@ +#![warn(missing_debug_implementations, rust_2018_idioms)] +#![deny(broken_intra_doc_links)] +#![forbid(unsafe_code)] +//! This crate defines the macros for `#[derive(OpenapiType)]`. + +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::{ + parse_macro_input, spanned::Spanned as _, Data, DataEnum, DataStruct, DataUnion, DeriveInput, TraitBound, + TraitBoundModifier, TypeParamBound +}; + +#[proc_macro_derive(OpenapiType)] +pub fn derive_openapi_type(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input); + expand_openapi_type(input).unwrap_or_else(|err| err.to_compile_error()).into() +} + +macro_rules! path { + (:: $($segment:ident)::*) => { + path!(@private Some(Default::default()), $($segment),*) + }; + ($($segment:ident)::*) => { + path!(@private None, $($segment),*) + }; + (@private $leading_colon:expr, $($segment:ident),*) => { + { + #[allow(unused_mut)] + let mut segments: ::syn::punctuated::Punctuated<::syn::PathSegment, _> = Default::default(); + $( + segments.push(::syn::PathSegment { + ident: ::proc_macro2::Ident::new(stringify!($segment), ::proc_macro2::Span::call_site()), + arguments: Default::default() + }); + )* + ::syn::Path { + leading_colon: $leading_colon, + segments + } + } + }; +} + +fn expand_openapi_type(mut input: DeriveInput) -> syn::Result { + let ident = &input.ident; + + // prepare the generics - all impl generics will get `OpenapiType` requirement + let (impl_generics, ty_generics, where_clause) = { + let generics = &mut input.generics; + generics.type_params_mut().for_each(|param| { + param.colon_token.get_or_insert_with(Default::default); + param.bounds.push(TypeParamBound::Trait(TraitBound { + paren_token: None, + modifier: TraitBoundModifier::None, + lifetimes: None, + path: path!(::openapi_type::OpenapiType) + })) + }); + generics.split_for_impl() + }; + + // parse the input data + match &input.data { + Data::Struct(strukt) => parse_struct(strukt)?, + Data::Enum(inum) => parse_enum(inum)?, + Data::Union(union) => parse_union(union)? + }; + + // generate the impl code + Ok(quote! { + impl #impl_generics ::openapi_type::OpenapiType for #ident #ty_generics #where_clause { + fn schema() -> ::openapi_type::OpenapiSchema { + unimplemented!() + } + } + }) +} + +fn parse_struct(_strukt: &DataStruct) -> syn::Result<()> { + Ok(()) +} + +fn parse_enum(_inum: &DataEnum) -> syn::Result<()> { + unimplemented!() +} + +fn parse_union(union: &DataUnion) -> syn::Result<()> { + Err(syn::Error::new( + union.union_token.span(), + "#[derive(OpenapiType)] cannot be used on unions" + )) +} From d9c7f4135f1c314f5f483f5314214806c7d32db9 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 7 Mar 2021 23:09:50 +0100 Subject: [PATCH 156/170] start openapi-type codegen --- openapi_type/tests/fail/not_openapitype.rs | 12 +++ .../tests/fail/not_openapitype.stderr | 8 ++ ...apitype.rs => not_openapitype_generics.rs} | 0 ...stderr => not_openapitype_generics.stderr} | 2 +- openapi_type_derive/Cargo.toml | 3 - openapi_type_derive/src/codegen.rs | 95 +++++++++++++++++++ openapi_type_derive/src/lib.rs | 94 +++++++++--------- openapi_type_derive/src/parser.rs | 40 ++++++++ openapi_type_derive/src/util.rs | 38 ++++++++ 9 files changed, 241 insertions(+), 51 deletions(-) create mode 100644 openapi_type/tests/fail/not_openapitype.rs create mode 100644 openapi_type/tests/fail/not_openapitype.stderr rename openapi_type/tests/fail/{generics_not_openapitype.rs => not_openapitype_generics.rs} (100%) rename openapi_type/tests/fail/{generics_not_openapitype.stderr => not_openapitype_generics.stderr} (94%) create mode 100644 openapi_type_derive/src/codegen.rs create mode 100644 openapi_type_derive/src/parser.rs create mode 100644 openapi_type_derive/src/util.rs diff --git a/openapi_type/tests/fail/not_openapitype.rs b/openapi_type/tests/fail/not_openapitype.rs new file mode 100644 index 0000000..2b5b23c --- /dev/null +++ b/openapi_type/tests/fail/not_openapitype.rs @@ -0,0 +1,12 @@ +use openapi_type::OpenapiType; + +#[derive(OpenapiType)] +struct Foo { + bar: Bar +} + +struct Bar; + +fn main() { + Foo::schema(); +} diff --git a/openapi_type/tests/fail/not_openapitype.stderr b/openapi_type/tests/fail/not_openapitype.stderr new file mode 100644 index 0000000..f089b15 --- /dev/null +++ b/openapi_type/tests/fail/not_openapitype.stderr @@ -0,0 +1,8 @@ +error[E0277]: the trait bound `Bar: OpenapiType` is not satisfied + --> $DIR/not_openapitype.rs:3:10 + | +3 | #[derive(OpenapiType)] + | ^^^^^^^^^^^ the trait `OpenapiType` is not implemented for `Bar` + | + = note: required by `schema` + = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/openapi_type/tests/fail/generics_not_openapitype.rs b/openapi_type/tests/fail/not_openapitype_generics.rs similarity index 100% rename from openapi_type/tests/fail/generics_not_openapitype.rs rename to openapi_type/tests/fail/not_openapitype_generics.rs diff --git a/openapi_type/tests/fail/generics_not_openapitype.stderr b/openapi_type/tests/fail/not_openapitype_generics.stderr similarity index 94% rename from openapi_type/tests/fail/generics_not_openapitype.stderr rename to openapi_type/tests/fail/not_openapitype_generics.stderr index 193eadb..d41098b 100644 --- a/openapi_type/tests/fail/generics_not_openapitype.stderr +++ b/openapi_type/tests/fail/not_openapitype_generics.stderr @@ -1,5 +1,5 @@ error[E0599]: no function or associated item named `schema` found for struct `Foo` in the current scope - --> $DIR/generics_not_openapitype.rs:11:14 + --> $DIR/not_openapitype_generics.rs:11:14 | 4 | struct Foo { | ------------- diff --git a/openapi_type_derive/Cargo.toml b/openapi_type_derive/Cargo.toml index c1cbedc..ab8e932 100644 --- a/openapi_type_derive/Cargo.toml +++ b/openapi_type_derive/Cargo.toml @@ -10,9 +10,6 @@ description = "Implementation detail of the openapi_type crate" license = "Apache-2.0" repository = "https://gitlab.com/msrd0/gotham-restful/-/tree/master/openapi_type_derive" -# tests are done using trybuild exclusively -autotests = false - [lib] proc-macro = true diff --git a/openapi_type_derive/src/codegen.rs b/openapi_type_derive/src/codegen.rs new file mode 100644 index 0000000..9ae9db7 --- /dev/null +++ b/openapi_type_derive/src/codegen.rs @@ -0,0 +1,95 @@ +use crate::parser::ParseData; +use proc_macro2::TokenStream; +use quote::quote; +use syn::{LitStr, Type}; + +impl ParseData { + pub(super) fn gen_schema(self) -> syn::Result { + match self { + Self::Struct(fields) => gen_struct(fields), + Self::Unit => gen_unit(), + _ => unimplemented!() + } + } +} + +fn gen_struct(fields: Vec<(LitStr, Type)>) -> syn::Result { + let field_name = fields.iter().map(|(name, _)| name); + let field_ty = fields.iter().map(|(_, ty)| ty); + + let openapi = path!(::openapi_type::openapi); + Ok(quote! { + { + let mut properties = <::openapi_type::indexmap::IndexMap< + ::std::string::String, + #openapi::ReferenceOr<::std::boxed::Box<#openapi::Schema>> + >>::new(); + let mut required = <::std::vec::Vec<::std::string::String>>::new(); + + #({ + const FIELD_NAME: &::core::primitive::str = #field_name; + let mut field_schema = <#field_ty as ::openapi_type::OpenapiType>::schema(); + add_dependencies(&mut field_schema.dependencies); + + // fields in OpenAPI are nullable by default + match field_schema.nullable { + true => field_schema.nullable = false, + false => required.push(::std::string::String::from(FIELD_NAME)) + }; + + match field_schema.name.as_ref() { + // include the field schema as reference + ::std::option::Option::Some(schema_name) => { + let mut reference = ::std::string::String::from("#/components/schemas/"); + reference.push_str(schema_name); + properties.insert( + ::std::string::String::from(FIELD_NAME), + #openapi::ReferenceOr::Reference { reference } + ); + dependencies.insert( + ::std::string::String::from(schema_name), + field_schema + ); + }, + // inline the field schema + ::std::option::Option::None => { + properties.insert( + ::std::string::String::from(FIELD_NAME), + #openapi::ReferenceOr::Item( + ::std::boxed::Box::new( + field_schema.into_schema() + ) + ) + ); + } + } + })* + + #openapi::SchemaKind::Type( + #openapi::Type::Object( + #openapi::ObjectType { + properties, + required, + .. ::std::default::Default::default() + } + ) + ) + } + }) +} + +fn gen_unit() -> syn::Result { + let openapi = path!(::openapi_type::openapi); + Ok(quote! { + #openapi::SchemaKind::Type( + #openapi::Type::Object( + #openapi::ObjectType { + additional_properties: ::std::option::Option::Some( + #openapi::AdditionalProperties::Any(false) + ), + .. ::std::default::Default::default() + } + ) + ) + }) +} diff --git a/openapi_type_derive/src/lib.rs b/openapi_type_derive/src/lib.rs index bc049b3..af4f9be 100644 --- a/openapi_type_derive/src/lib.rs +++ b/openapi_type_derive/src/lib.rs @@ -6,44 +6,27 @@ use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use quote::quote; -use syn::{ - parse_macro_input, spanned::Spanned as _, Data, DataEnum, DataStruct, DataUnion, DeriveInput, TraitBound, - TraitBoundModifier, TypeParamBound -}; +use syn::{parse_macro_input, Data, DeriveInput, LitStr, TraitBound, TraitBoundModifier, TypeParamBound}; +#[macro_use] +mod util; +//use util::*; + +mod codegen; +mod parser; +use parser::*; + +/// The derive macro for [OpenapiType][openapi_type::OpenapiType]. #[proc_macro_derive(OpenapiType)] pub fn derive_openapi_type(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input); expand_openapi_type(input).unwrap_or_else(|err| err.to_compile_error()).into() } -macro_rules! path { - (:: $($segment:ident)::*) => { - path!(@private Some(Default::default()), $($segment),*) - }; - ($($segment:ident)::*) => { - path!(@private None, $($segment),*) - }; - (@private $leading_colon:expr, $($segment:ident),*) => { - { - #[allow(unused_mut)] - let mut segments: ::syn::punctuated::Punctuated<::syn::PathSegment, _> = Default::default(); - $( - segments.push(::syn::PathSegment { - ident: ::proc_macro2::Ident::new(stringify!($segment), ::proc_macro2::Span::call_site()), - arguments: Default::default() - }); - )* - ::syn::Path { - leading_colon: $leading_colon, - segments - } - } - }; -} - fn expand_openapi_type(mut input: DeriveInput) -> syn::Result { let ident = &input.ident; + let name = ident.to_string(); + let name = LitStr::new(&name, ident.span()); // prepare the generics - all impl generics will get `OpenapiType` requirement let (impl_generics, ty_generics, where_clause) = { @@ -61,33 +44,50 @@ fn expand_openapi_type(mut input: DeriveInput) -> syn::Result { }; // parse the input data - match &input.data { + let parsed = match &input.data { Data::Struct(strukt) => parse_struct(strukt)?, Data::Enum(inum) => parse_enum(inum)?, Data::Union(union) => parse_union(union)? }; - // generate the impl code + // run the codegen + let schema_code = parsed.gen_schema()?; + + // put the code together Ok(quote! { + #[allow(unused_mut)] impl #impl_generics ::openapi_type::OpenapiType for #ident #ty_generics #where_clause { fn schema() -> ::openapi_type::OpenapiSchema { - unimplemented!() + // prepare the dependencies + let mut dependencies = <::openapi_type::indexmap::IndexMap< + ::std::string::String, + ::openapi_type::OpenapiSchema + >>::new(); + + // this function can be used to include dependencies of dependencies + let add_dependencies = |deps: &mut ::openapi_type::indexmap::IndexMap< + ::std::string::String, + ::openapi_type::OpenapiSchema + >| { + while let ::std::option::Option::Some((dep_name, dep_schema)) = deps.pop() { + if !dependencies.contains_key(&dep_name) { + dependencies.insert(dep_name, dep_schema); + } + } + }; + + // create the schema + let schema = #schema_code; + + // return everything + const NAME: &::core::primitive::str = #name; + ::openapi_type::OpenapiSchema { + name: ::std::option::Option::Some(::std::string::String::from(NAME)), + nullable: false, + schema, + dependencies + } } } }) } - -fn parse_struct(_strukt: &DataStruct) -> syn::Result<()> { - Ok(()) -} - -fn parse_enum(_inum: &DataEnum) -> syn::Result<()> { - unimplemented!() -} - -fn parse_union(union: &DataUnion) -> syn::Result<()> { - Err(syn::Error::new( - union.union_token.span(), - "#[derive(OpenapiType)] cannot be used on unions" - )) -} diff --git a/openapi_type_derive/src/parser.rs b/openapi_type_derive/src/parser.rs new file mode 100644 index 0000000..4a71c0a --- /dev/null +++ b/openapi_type_derive/src/parser.rs @@ -0,0 +1,40 @@ +use crate::util::ToLitStr; +use syn::{spanned::Spanned as _, DataEnum, DataStruct, DataUnion, Fields, LitStr, Type}; + +#[allow(dead_code)] +pub(super) enum ParseData { + Struct(Vec<(LitStr, Type)>), + Enum(Vec), + Alternatives(Vec), + Unit +} + +pub(super) fn parse_struct(strukt: &DataStruct) -> syn::Result { + match &strukt.fields { + Fields::Named(named_fields) => { + let mut fields: Vec<(LitStr, Type)> = Vec::new(); + for f in &named_fields.named { + let ident = f.ident.as_ref().ok_or_else(|| { + syn::Error::new(f.span(), "#[derive(OpenapiType)] does not support fields without an ident") + })?; + let name = ident.to_lit_str(); + let ty = f.ty.to_owned(); + fields.push((name, ty)); + } + Ok(ParseData::Struct(fields)) + }, + Fields::Unnamed(_) => unimplemented!(), + Fields::Unit => Ok(ParseData::Unit) + } +} + +pub(super) fn parse_enum(_inum: &DataEnum) -> syn::Result { + unimplemented!() +} + +pub(super) fn parse_union(union: &DataUnion) -> syn::Result { + Err(syn::Error::new( + union.union_token.span(), + "#[derive(OpenapiType)] cannot be used on unions" + )) +} diff --git a/openapi_type_derive/src/util.rs b/openapi_type_derive/src/util.rs new file mode 100644 index 0000000..9470a04 --- /dev/null +++ b/openapi_type_derive/src/util.rs @@ -0,0 +1,38 @@ +use proc_macro2::Ident; +use syn::LitStr; + +/// Convert any literal path into a [syn::Path]. +macro_rules! path { + (:: $($segment:ident)::*) => { + path!(@private Some(Default::default()), $($segment),*) + }; + ($($segment:ident)::*) => { + path!(@private None, $($segment),*) + }; + (@private $leading_colon:expr, $($segment:ident),*) => { + { + #[allow(unused_mut)] + let mut segments: ::syn::punctuated::Punctuated<::syn::PathSegment, _> = Default::default(); + $( + segments.push(::syn::PathSegment { + ident: ::proc_macro2::Ident::new(stringify!($segment), ::proc_macro2::Span::call_site()), + arguments: Default::default() + }); + )* + ::syn::Path { + leading_colon: $leading_colon, + segments + } + } + }; +} + +/// Convert any [Ident] into a [LitStr]. Basically `stringify!`. +pub(super) trait ToLitStr { + fn to_lit_str(&self) -> LitStr; +} +impl ToLitStr for Ident { + fn to_lit_str(&self) -> LitStr { + LitStr::new(&self.to_string(), self.span()) + } +} From 667009bd228dbee456b44d2f41342b9e4dec684e Mon Sep 17 00:00:00 2001 From: Dominic Date: Mon, 8 Mar 2021 16:33:38 +0100 Subject: [PATCH 157/170] copy OpenapiType implementations and fix codegen reference --- openapi_type/Cargo.toml | 2 + openapi_type/src/impls.rs | 321 +++++++++++++++++++++++++++++ openapi_type/src/lib.rs | 4 + openapi_type/src/private.rs | 12 ++ openapi_type/tests/custom_types.rs | 44 ++++ openapi_type_derive/src/codegen.rs | 5 +- openapi_type_derive/src/lib.rs | 17 +- 7 files changed, 388 insertions(+), 17 deletions(-) create mode 100644 openapi_type/src/impls.rs create mode 100644 openapi_type/src/private.rs create mode 100644 openapi_type/tests/custom_types.rs diff --git a/openapi_type/Cargo.toml b/openapi_type/Cargo.toml index 5e2972a..028c753 100644 --- a/openapi_type/Cargo.toml +++ b/openapi_type/Cargo.toml @@ -15,6 +15,8 @@ repository = "https://gitlab.com/msrd0/gotham-restful/-/tree/master/openapi_type indexmap = "1.6" openapi_type_derive = "0.1.0-dev" openapiv3 = "=0.3.2" +serde_json = "1.0" [dev-dependencies] +paste = "1.0" trybuild = "1.0" diff --git a/openapi_type/src/impls.rs b/openapi_type/src/impls.rs new file mode 100644 index 0000000..f363818 --- /dev/null +++ b/openapi_type/src/impls.rs @@ -0,0 +1,321 @@ +use crate::{OpenapiSchema, OpenapiType}; +use indexmap::IndexMap; +use openapiv3::{ + AdditionalProperties, ArrayType, IntegerType, NumberFormat, NumberType, ObjectType, ReferenceOr, SchemaKind, StringType, + Type, VariantOrUnknownOrEmpty +}; +use std::{ + collections::{BTreeSet, HashMap, HashSet}, + hash::BuildHasher, + num::{NonZeroU128, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize} +}; + +impl OpenapiType for () { + fn schema() -> OpenapiSchema { + OpenapiSchema::new(SchemaKind::Type(Type::Object(ObjectType { + additional_properties: Some(AdditionalProperties::Any(false)), + ..Default::default() + }))) + } +} + +impl OpenapiType for bool { + fn schema() -> OpenapiSchema { + OpenapiSchema::new(SchemaKind::Type(Type::Boolean {})) + } +} + +macro_rules! int_types { + ($($int_ty:ty),*) => {$( + impl OpenapiType for $int_ty + { + fn schema() -> OpenapiSchema + { + OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType::default()))) + } + } + )*}; + + (unsigned $($int_ty:ty),*) => {$( + impl OpenapiType for $int_ty + { + fn schema() -> OpenapiSchema + { + OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { + minimum: Some(0), + ..Default::default() + }))) + } + } + )*}; + + (gtzero $($int_ty:ty),*) => {$( + impl OpenapiType for $int_ty + { + fn schema() -> OpenapiSchema + { + OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { + minimum: Some(1), + ..Default::default() + }))) + } + } + )*}; + + (bits = $bits:expr, $($int_ty:ty),*) => {$( + impl OpenapiType for $int_ty + { + fn schema() -> OpenapiSchema + { + OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { + format: VariantOrUnknownOrEmpty::Unknown(format!("int{}", $bits)), + ..Default::default() + }))) + } + } + )*}; + + (unsigned bits = $bits:expr, $($int_ty:ty),*) => {$( + impl OpenapiType for $int_ty + { + fn schema() -> OpenapiSchema + { + OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { + format: VariantOrUnknownOrEmpty::Unknown(format!("int{}", $bits)), + minimum: Some(0), + ..Default::default() + }))) + } + } + )*}; + + (gtzero bits = $bits:expr, $($int_ty:ty),*) => {$( + impl OpenapiType for $int_ty + { + fn schema() -> OpenapiSchema + { + OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { + format: VariantOrUnknownOrEmpty::Unknown(format!("int{}", $bits)), + minimum: Some(1), + ..Default::default() + }))) + } + } + )*}; +} + +int_types!(isize); +int_types!(unsigned usize); +int_types!(gtzero NonZeroUsize); +int_types!(bits = 8, i8); +int_types!(unsigned bits = 8, u8); +int_types!(gtzero bits = 8, NonZeroU8); +int_types!(bits = 16, i16); +int_types!(unsigned bits = 16, u16); +int_types!(gtzero bits = 16, NonZeroU16); +int_types!(bits = 32, i32); +int_types!(unsigned bits = 32, u32); +int_types!(gtzero bits = 32, NonZeroU32); +int_types!(bits = 64, i64); +int_types!(unsigned bits = 64, u64); +int_types!(gtzero bits = 64, NonZeroU64); +int_types!(bits = 128, i128); +int_types!(unsigned bits = 128, u128); +int_types!(gtzero bits = 128, NonZeroU128); + +macro_rules! num_types { + ($($num_ty:ty = $num_fmt:ident),*) => {$( + impl OpenapiType for $num_ty + { + fn schema() -> OpenapiSchema + { + OpenapiSchema::new(SchemaKind::Type(Type::Number(NumberType { + format: VariantOrUnknownOrEmpty::Item(NumberFormat::$num_fmt), + ..Default::default() + }))) + } + } + )*} +} + +num_types!(f32 = Float, f64 = Double); + +macro_rules! str_types { + ($($str_ty:ty),*) => {$( + impl OpenapiType for $str_ty + { + fn schema() -> OpenapiSchema + { + OpenapiSchema::new(SchemaKind::Type(Type::String(StringType::default()))) + } + } + )*}; + + (format = $format:ident, $($str_ty:ty),*) => {$( + impl OpenapiType for $str_ty + { + fn schema() -> OpenapiSchema + { + use openapiv3::StringFormat; + + OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { + format: VariantOrUnknownOrEmpty::Item(StringFormat::$format), + ..Default::default() + }))) + } + } + )*}; + + (format_str = $format:expr, $($str_ty:ty),*) => {$( + impl OpenapiType for $str_ty + { + fn schema() -> OpenapiSchema + { + OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { + format: VariantOrUnknownOrEmpty::Unknown($format.to_string()), + ..Default::default() + }))) + } + } + )*}; +} + +str_types!(String, &str); + +#[cfg(feature = "chrono")] +str_types!(format = Date, Date, Date, Date, NaiveDate); +#[cfg(feature = "chrono")] +str_types!( + format = DateTime, + DateTime, + DateTime, + DateTime, + NaiveDateTime +); + +#[cfg(feature = "uuid")] +str_types!(format_str = "uuid", Uuid); + +impl OpenapiType for Option { + fn schema() -> OpenapiSchema { + let schema = T::schema(); + let mut dependencies = schema.dependencies.clone(); + let schema = match schema.name.clone() { + Some(name) => { + let reference = ReferenceOr::Reference { + reference: format!("#/components/schemas/{}", name) + }; + dependencies.insert(name, schema); + SchemaKind::AllOf { all_of: vec![reference] } + }, + None => schema.schema + }; + + OpenapiSchema { + nullable: true, + name: None, + schema, + dependencies + } + } +} + +impl OpenapiType for Vec { + fn schema() -> OpenapiSchema { + let schema = T::schema(); + let mut dependencies = schema.dependencies.clone(); + + let items = match schema.name.clone() { + Some(name) => { + let reference = ReferenceOr::Reference { + reference: format!("#/components/schemas/{}", name) + }; + dependencies.insert(name, schema); + reference + }, + None => ReferenceOr::Item(Box::new(schema.into_schema())) + }; + + OpenapiSchema { + nullable: false, + name: None, + schema: SchemaKind::Type(Type::Array(ArrayType { + items, + min_items: None, + max_items: None, + unique_items: false + })), + dependencies + } + } +} + +impl OpenapiType for BTreeSet { + fn schema() -> OpenapiSchema { + as OpenapiType>::schema() + } +} + +impl OpenapiType for HashSet { + fn schema() -> OpenapiSchema { + as OpenapiType>::schema() + } +} + +impl OpenapiType for HashMap { + fn schema() -> OpenapiSchema { + let key_schema = K::schema(); + let mut dependencies = key_schema.dependencies.clone(); + + let keys = match key_schema.name.clone() { + Some(name) => { + let reference = ReferenceOr::Reference { + reference: format!("#/components/schemas/{}", name) + }; + dependencies.insert(name, key_schema); + reference + }, + None => ReferenceOr::Item(Box::new(key_schema.into_schema())) + }; + + let schema = T::schema(); + dependencies.extend(schema.dependencies.iter().map(|(k, v)| (k.clone(), v.clone()))); + + let items = Box::new(match schema.name.clone() { + Some(name) => { + let reference = ReferenceOr::Reference { + reference: format!("#/components/schemas/{}", name) + }; + dependencies.insert(name, schema); + reference + }, + None => ReferenceOr::Item(schema.into_schema()) + }); + + let mut properties = IndexMap::new(); + properties.insert("default".to_owned(), keys); + + OpenapiSchema { + nullable: false, + name: None, + schema: SchemaKind::Type(Type::Object(ObjectType { + properties, + required: vec!["default".to_owned()], + additional_properties: Some(AdditionalProperties::Schema(items)), + ..Default::default() + })), + dependencies + } + } +} + +impl OpenapiType for serde_json::Value { + fn schema() -> OpenapiSchema { + OpenapiSchema { + nullable: true, + name: None, + schema: SchemaKind::Any(Default::default()), + dependencies: Default::default() + } + } +} diff --git a/openapi_type/src/lib.rs b/openapi_type/src/lib.rs index b5c0341..590800b 100644 --- a/openapi_type/src/lib.rs +++ b/openapi_type/src/lib.rs @@ -9,6 +9,10 @@ pub use indexmap; pub use openapi_type_derive::OpenapiType; pub use openapiv3 as openapi; +mod impls; +#[doc(hidden)] +pub mod private; + use indexmap::IndexMap; use openapi::{Schema, SchemaData, SchemaKind}; diff --git a/openapi_type/src/private.rs b/openapi_type/src/private.rs new file mode 100644 index 0000000..892b8e3 --- /dev/null +++ b/openapi_type/src/private.rs @@ -0,0 +1,12 @@ +use crate::OpenapiSchema; +use indexmap::IndexMap; + +pub type Dependencies = IndexMap; + +pub fn add_dependencies(dependencies: &mut Dependencies, other: &mut Dependencies) { + while let Some((dep_name, dep_schema)) = other.pop() { + if !dependencies.contains_key(&dep_name) { + dependencies.insert(dep_name, dep_schema); + } + } +} diff --git a/openapi_type/tests/custom_types.rs b/openapi_type/tests/custom_types.rs new file mode 100644 index 0000000..ba52b41 --- /dev/null +++ b/openapi_type/tests/custom_types.rs @@ -0,0 +1,44 @@ +#![allow(dead_code)] +use openapi_type::OpenapiType; + +macro_rules! test_type { + ($ty:ty = $json:tt) => { + paste::paste! { + #[test] + fn [< $ty:lower >]() { + let schema = <$ty as OpenapiType>::schema(); + let schema = openapi_type::OpenapiSchema::into_schema(schema); + let schema_json = serde_json::to_value(&schema).unwrap(); + let expected = serde_json::json!($json); + assert_eq!(schema_json, expected); + } + } + }; +} + +#[derive(OpenapiType)] +struct UnitStruct; +test_type!(UnitStruct = { + "type": "object", + "title": "UnitStruct", + "additionalProperties": false +}); + +#[derive(OpenapiType)] +struct SimpleStruct { + foo: String, + bar: isize +} +test_type!(SimpleStruct = { + "type": "object", + "title": "SimpleStruct", + "properties": { + "foo": { + "type": "string" + }, + "bar": { + "type": "integer" + } + }, + "required": ["foo", "bar"] +}); diff --git a/openapi_type_derive/src/codegen.rs b/openapi_type_derive/src/codegen.rs index 9ae9db7..4aae6a3 100644 --- a/openapi_type_derive/src/codegen.rs +++ b/openapi_type_derive/src/codegen.rs @@ -29,7 +29,10 @@ fn gen_struct(fields: Vec<(LitStr, Type)>) -> syn::Result { #({ const FIELD_NAME: &::core::primitive::str = #field_name; let mut field_schema = <#field_ty as ::openapi_type::OpenapiType>::schema(); - add_dependencies(&mut field_schema.dependencies); + ::openapi_type::private::add_dependencies( + &mut dependencies, + &mut field_schema.dependencies + ); // fields in OpenAPI are nullable by default match field_schema.nullable { diff --git a/openapi_type_derive/src/lib.rs b/openapi_type_derive/src/lib.rs index af4f9be..cdbdbf3 100644 --- a/openapi_type_derive/src/lib.rs +++ b/openapi_type_derive/src/lib.rs @@ -59,22 +59,7 @@ fn expand_openapi_type(mut input: DeriveInput) -> syn::Result { impl #impl_generics ::openapi_type::OpenapiType for #ident #ty_generics #where_clause { fn schema() -> ::openapi_type::OpenapiSchema { // prepare the dependencies - let mut dependencies = <::openapi_type::indexmap::IndexMap< - ::std::string::String, - ::openapi_type::OpenapiSchema - >>::new(); - - // this function can be used to include dependencies of dependencies - let add_dependencies = |deps: &mut ::openapi_type::indexmap::IndexMap< - ::std::string::String, - ::openapi_type::OpenapiSchema - >| { - while let ::std::option::Option::Some((dep_name, dep_schema)) = deps.pop() { - if !dependencies.contains_key(&dep_name) { - dependencies.insert(dep_name, dep_schema); - } - } - }; + let mut dependencies = ::openapi_type::private::Dependencies::new(); // create the schema let schema = #schema_code; From 43d3a1cd89bccd482d62399f3de47918be443dff Mon Sep 17 00:00:00 2001 From: Dominic Date: Mon, 8 Mar 2021 17:20:41 +0100 Subject: [PATCH 158/170] start implementing enums --- openapi_type/tests/custom_types.rs | 35 ++++++++ .../tests/fail/enum_with_no_variants.rs | 6 ++ .../tests/fail/enum_with_no_variants.stderr | 5 ++ openapi_type/tests/pass/unit_struct.rs | 6 -- openapi_type/tests/trybuild.rs | 1 - openapi_type_derive/src/codegen.rs | 49 +++++++--- openapi_type_derive/src/lib.rs | 2 +- openapi_type_derive/src/parser.rs | 90 +++++++++++++++---- 8 files changed, 159 insertions(+), 35 deletions(-) create mode 100644 openapi_type/tests/fail/enum_with_no_variants.rs create mode 100644 openapi_type/tests/fail/enum_with_no_variants.stderr delete mode 100644 openapi_type/tests/pass/unit_struct.rs diff --git a/openapi_type/tests/custom_types.rs b/openapi_type/tests/custom_types.rs index ba52b41..fce5e57 100644 --- a/openapi_type/tests/custom_types.rs +++ b/openapi_type/tests/custom_types.rs @@ -42,3 +42,38 @@ test_type!(SimpleStruct = { }, "required": ["foo", "bar"] }); + +#[derive(OpenapiType)] +enum EnumWithoutFields { + Success, + Error +} +test_type!(EnumWithoutFields = { + "type": "string", + "title": "EnumWithoutFields", + "enum": [ + "Success", + "Error" + ] +}); + +#[derive(OpenapiType)] +enum EnumWithOneField { + Success { value: isize } +} +test_type!(EnumWithOneField = { + "type": "object", + "title": "EnumWithOneField", + "properties": { + "Success": { + "type": "object", + "properties": { + "value": { + "type": "integer" + } + }, + "required": ["value"] + } + }, + "required": ["Success"] +}); diff --git a/openapi_type/tests/fail/enum_with_no_variants.rs b/openapi_type/tests/fail/enum_with_no_variants.rs new file mode 100644 index 0000000..d08e223 --- /dev/null +++ b/openapi_type/tests/fail/enum_with_no_variants.rs @@ -0,0 +1,6 @@ +use openapi_type::OpenapiType; + +#[derive(OpenapiType)] +enum Foo {} + +fn main() {} diff --git a/openapi_type/tests/fail/enum_with_no_variants.stderr b/openapi_type/tests/fail/enum_with_no_variants.stderr new file mode 100644 index 0000000..5c6b1d1 --- /dev/null +++ b/openapi_type/tests/fail/enum_with_no_variants.stderr @@ -0,0 +1,5 @@ +error: #[derive(OpenapiType)] does not support enums with no variants + --> $DIR/enum_with_no_variants.rs:4:10 + | +4 | enum Foo {} + | ^^ diff --git a/openapi_type/tests/pass/unit_struct.rs b/openapi_type/tests/pass/unit_struct.rs deleted file mode 100644 index 79f6443..0000000 --- a/openapi_type/tests/pass/unit_struct.rs +++ /dev/null @@ -1,6 +0,0 @@ -use openapi_type_derive::OpenapiType; - -#[derive(OpenapiType)] -struct Foo; - -fn main() {} diff --git a/openapi_type/tests/trybuild.rs b/openapi_type/tests/trybuild.rs index 28574f1..b76b676 100644 --- a/openapi_type/tests/trybuild.rs +++ b/openapi_type/tests/trybuild.rs @@ -3,6 +3,5 @@ use trybuild::TestCases; #[test] fn trybuild() { let t = TestCases::new(); - t.pass("tests/pass/*.rs"); t.compile_fail("tests/fail/*.rs"); } diff --git a/openapi_type_derive/src/codegen.rs b/openapi_type_derive/src/codegen.rs index 4aae6a3..1ad7d02 100644 --- a/openapi_type_derive/src/codegen.rs +++ b/openapi_type_derive/src/codegen.rs @@ -1,24 +1,33 @@ -use crate::parser::ParseData; +use crate::parser::{ParseData, ParseDataType}; use proc_macro2::TokenStream; use quote::quote; -use syn::{LitStr, Type}; +use syn::LitStr; impl ParseData { - pub(super) fn gen_schema(self) -> syn::Result { + pub(super) fn gen_schema(&self) -> TokenStream { match self { Self::Struct(fields) => gen_struct(fields), + Self::Enum(variants) => gen_enum(variants), Self::Unit => gen_unit(), _ => unimplemented!() } } } -fn gen_struct(fields: Vec<(LitStr, Type)>) -> syn::Result { +fn gen_struct(fields: &[(LitStr, ParseDataType)]) -> TokenStream { let field_name = fields.iter().map(|(name, _)| name); - let field_ty = fields.iter().map(|(_, ty)| ty); + let field_schema = fields.iter().map(|(_, ty)| match ty { + ParseDataType::Type(ty) => { + quote!(<#ty as ::openapi_type::OpenapiType>::schema()) + }, + ParseDataType::Inline(data) => { + let code = data.gen_schema(); + quote!(::openapi_type::OpenapiSchema::new(#code)) + } + }); let openapi = path!(::openapi_type::openapi); - Ok(quote! { + quote! { { let mut properties = <::openapi_type::indexmap::IndexMap< ::std::string::String, @@ -28,7 +37,7 @@ fn gen_struct(fields: Vec<(LitStr, Type)>) -> syn::Result { #({ const FIELD_NAME: &::core::primitive::str = #field_name; - let mut field_schema = <#field_ty as ::openapi_type::OpenapiType>::schema(); + let mut field_schema = #field_schema; ::openapi_type::private::add_dependencies( &mut dependencies, &mut field_schema.dependencies @@ -78,12 +87,30 @@ fn gen_struct(fields: Vec<(LitStr, Type)>) -> syn::Result { ) ) } - }) + } } -fn gen_unit() -> syn::Result { +fn gen_enum(variants: &[LitStr]) -> TokenStream { let openapi = path!(::openapi_type::openapi); - Ok(quote! { + quote! { + { + let mut enumeration = <::std::vec::Vec<::std::string::String>>::new(); + #(enumeration.push(::std::string::String::from(#variants));)* + #openapi::SchemaKind::Type( + #openapi::Type::String( + #openapi::StringType { + enumeration, + .. ::std::default::Default::default() + } + ) + ) + } + } +} + +fn gen_unit() -> TokenStream { + let openapi = path!(::openapi_type::openapi); + quote! { #openapi::SchemaKind::Type( #openapi::Type::Object( #openapi::ObjectType { @@ -94,5 +121,5 @@ fn gen_unit() -> syn::Result { } ) ) - }) + } } diff --git a/openapi_type_derive/src/lib.rs b/openapi_type_derive/src/lib.rs index cdbdbf3..e3bc9fc 100644 --- a/openapi_type_derive/src/lib.rs +++ b/openapi_type_derive/src/lib.rs @@ -51,7 +51,7 @@ fn expand_openapi_type(mut input: DeriveInput) -> syn::Result { }; // run the codegen - let schema_code = parsed.gen_schema()?; + let schema_code = parsed.gen_schema(); // put the code together Ok(quote! { diff --git a/openapi_type_derive/src/parser.rs b/openapi_type_derive/src/parser.rs index 4a71c0a..8d0c15f 100644 --- a/openapi_type_derive/src/parser.rs +++ b/openapi_type_derive/src/parser.rs @@ -1,35 +1,93 @@ use crate::util::ToLitStr; -use syn::{spanned::Spanned as _, DataEnum, DataStruct, DataUnion, Fields, LitStr, Type}; +use syn::{spanned::Spanned as _, DataEnum, DataStruct, DataUnion, Fields, FieldsNamed, LitStr, Type}; + +pub(super) enum ParseDataType { + Type(Type), + Inline(ParseData) +} #[allow(dead_code)] pub(super) enum ParseData { - Struct(Vec<(LitStr, Type)>), + Struct(Vec<(LitStr, ParseDataType)>), Enum(Vec), Alternatives(Vec), Unit } +fn parse_named_fields(named_fields: &FieldsNamed) -> syn::Result { + let mut fields: Vec<(LitStr, ParseDataType)> = Vec::new(); + for f in &named_fields.named { + let ident = f + .ident + .as_ref() + .ok_or_else(|| syn::Error::new(f.span(), "#[derive(OpenapiType)] does not support fields without an ident"))?; + let name = ident.to_lit_str(); + let ty = f.ty.to_owned(); + fields.push((name, ParseDataType::Type(ty))); + } + Ok(ParseData::Struct(fields)) +} + pub(super) fn parse_struct(strukt: &DataStruct) -> syn::Result { match &strukt.fields { - Fields::Named(named_fields) => { - let mut fields: Vec<(LitStr, Type)> = Vec::new(); - for f in &named_fields.named { - let ident = f.ident.as_ref().ok_or_else(|| { - syn::Error::new(f.span(), "#[derive(OpenapiType)] does not support fields without an ident") - })?; - let name = ident.to_lit_str(); - let ty = f.ty.to_owned(); - fields.push((name, ty)); - } - Ok(ParseData::Struct(fields)) - }, + Fields::Named(named_fields) => parse_named_fields(named_fields), Fields::Unnamed(_) => unimplemented!(), Fields::Unit => Ok(ParseData::Unit) } } -pub(super) fn parse_enum(_inum: &DataEnum) -> syn::Result { - unimplemented!() +pub(super) fn parse_enum(inum: &DataEnum) -> syn::Result { + let mut strings: Vec = Vec::new(); + let mut types: Vec<(LitStr, ParseData)> = Vec::new(); + + for v in &inum.variants { + let name = v.ident.to_lit_str(); + match &v.fields { + Fields::Named(named_fields) => { + types.push((name, parse_named_fields(named_fields)?)); + }, + Fields::Unnamed(_unnamed_fields) => unimplemented!(), + Fields::Unit => strings.push(name) + } + } + + let data_strings = if strings.is_empty() { + None + } else { + Some(ParseData::Enum(strings)) + }; + + let data_types = if types.is_empty() { + None + } else { + Some(ParseData::Alternatives( + types + .into_iter() + .map(|(name, data)| ParseData::Struct(vec![(name, ParseDataType::Inline(data))])) + .collect() + )) + }; + + match (data_strings, data_types) { + // only variants without fields + (Some(data), None) => Ok(data), + // only one variant with fields + (None, Some(ParseData::Alternatives(mut alt))) if alt.len() == 1 => Ok(alt.remove(0)), + // only variants with fields + (None, Some(data)) => Ok(data), + // variants with and without fields + (Some(data), Some(ParseData::Alternatives(mut alt))) => { + alt.push(data); + Ok(ParseData::Alternatives(alt)) + }, + // no variants + (None, None) => Err(syn::Error::new( + inum.brace_token.span, + "#[derive(OpenapiType)] does not support enums with no variants" + )), + // data_types always produces Alternatives + _ => unreachable!() + } } pub(super) fn parse_union(union: &DataUnion) -> syn::Result { From 5f60599c412b4dc71f8cbd5a066bda0938bc158c Mon Sep 17 00:00:00 2001 From: Dominic Date: Mon, 8 Mar 2021 17:33:49 +0100 Subject: [PATCH 159/170] more enum stuff [skip ci] --- openapi_type/tests/custom_types.rs | 65 ++++++++++++++++++++++++++++++ openapi_type_derive/src/codegen.rs | 22 +++++++++- 2 files changed, 85 insertions(+), 2 deletions(-) diff --git a/openapi_type/tests/custom_types.rs b/openapi_type/tests/custom_types.rs index fce5e57..5119033 100644 --- a/openapi_type/tests/custom_types.rs +++ b/openapi_type/tests/custom_types.rs @@ -77,3 +77,68 @@ test_type!(EnumWithOneField = { }, "required": ["Success"] }); + +#[derive(OpenapiType)] +enum EnumWithFields { + Success { value: isize }, + Error { msg: String } +} +test_type!(EnumWithFields = { + "title": "EnumWithFields", + "oneOf": [{ + "type": "object", + "properties": { + "Success": { + "type": "object", + "properties": { + "value": { + "type": "integer" + } + }, + "required": ["value"] + } + }, + "required": ["Success"] + }, { + "type": "object", + "properties": { + "Error": { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + }, + "required": ["msg"] + } + }, + "required": ["Error"] + }] +}); + +#[derive(OpenapiType)] +enum EnumExternallyTagged { + Success { value: isize }, + Error +} +test_type!(EnumExternallyTagged = { + "title": "EnumExternallyTagged", + "oneOf": [{ + "type": "object", + "properties": { + "Success": { + "type": "object", + "properties": { + "value": { + "type": "integer" + } + }, + "required": ["value"] + } + }, + "required": ["Success"] + }, { + "type": "string", + "enum": ["Error"] + }] +}); diff --git a/openapi_type_derive/src/codegen.rs b/openapi_type_derive/src/codegen.rs index 1ad7d02..a56c97c 100644 --- a/openapi_type_derive/src/codegen.rs +++ b/openapi_type_derive/src/codegen.rs @@ -8,8 +8,8 @@ impl ParseData { match self { Self::Struct(fields) => gen_struct(fields), Self::Enum(variants) => gen_enum(variants), - Self::Unit => gen_unit(), - _ => unimplemented!() + Self::Alternatives(alt) => gen_alt(alt), + Self::Unit => gen_unit() } } } @@ -108,6 +108,24 @@ fn gen_enum(variants: &[LitStr]) -> TokenStream { } } +fn gen_alt(alt: &[ParseData]) -> TokenStream { + let openapi = path!(::openapi_type::openapi); + let schema = alt.iter().map(|data| data.gen_schema()); + quote! { + { + let mut alternatives = <::std::vec::Vec< + #openapi::ReferenceOr<#openapi::Schema> + >>::new(); + #(alternatives.push(#openapi::ReferenceOr::Item( + ::openapi_type::OpenapiSchema::new(#schema).into_schema() + ));)* + #openapi::SchemaKind::OneOf { + one_of: alternatives + } + } + } +} + fn gen_unit() -> TokenStream { let openapi = path!(::openapi_type::openapi); quote! { From a57f1c097da1f79038a27ac04849bd1f1da82270 Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 9 Mar 2021 00:17:13 +0100 Subject: [PATCH 160/170] enum representations [skip ci] --- openapi_type/Cargo.toml | 1 + openapi_type/tests/custom_types.rs | 107 +++++++++++++++++++++++++- openapi_type_derive/src/lib.rs | 23 +++++- openapi_type_derive/src/parser.rs | 118 +++++++++++++++++++++++++---- openapi_type_derive/src/util.rs | 16 +++- 5 files changed, 246 insertions(+), 19 deletions(-) diff --git a/openapi_type/Cargo.toml b/openapi_type/Cargo.toml index 028c753..0513c0b 100644 --- a/openapi_type/Cargo.toml +++ b/openapi_type/Cargo.toml @@ -19,4 +19,5 @@ serde_json = "1.0" [dev-dependencies] paste = "1.0" +serde = "1.0" trybuild = "1.0" diff --git a/openapi_type/tests/custom_types.rs b/openapi_type/tests/custom_types.rs index 5119033..18cca88 100644 --- a/openapi_type/tests/custom_types.rs +++ b/openapi_type/tests/custom_types.rs @@ -43,6 +43,15 @@ test_type!(SimpleStruct = { "required": ["foo", "bar"] }); +#[derive(OpenapiType)] +#[openapi(rename = "FooBar")] +struct StructRename; +test_type!(StructRename = { + "type": "object", + "title": "FooBar", + "additionalProperties": false +}); + #[derive(OpenapiType)] enum EnumWithoutFields { Success, @@ -119,6 +128,7 @@ test_type!(EnumWithFields = { #[derive(OpenapiType)] enum EnumExternallyTagged { Success { value: isize }, + Empty, Error } test_type!(EnumExternallyTagged = { @@ -139,6 +149,101 @@ test_type!(EnumExternallyTagged = { "required": ["Success"] }, { "type": "string", - "enum": ["Error"] + "enum": ["Empty", "Error"] + }] +}); + +#[derive(OpenapiType)] +#[openapi(tag = "ty")] +enum EnumInternallyTagged { + Success { value: isize }, + Empty, + Error +} +test_type!(EnumInternallyTagged = { + "title": "EnumInternallyTagged", + "oneOf": [{ + "type": "object", + "properties": { + "value": { + "type": "integer" + }, + "ty": { + "type": "string", + "enum": ["Success"] + } + }, + "required": ["value", "ty"] + }, { + "type": "object", + "properties": { + "ty": { + "type": "string", + "enum": ["Empty", "Error"] + } + }, + "required": ["ty"] + }] +}); + +#[derive(OpenapiType)] +#[openapi(tag = "ty", content = "ct")] +enum EnumAdjacentlyTagged { + Success { value: isize }, + Empty, + Error +} +test_type!(EnumAdjacentlyTagged = { + "title": "EnumAdjacentlyTagged", + "oneOf": [{ + "type": "object", + "properties": { + "ty": { + "type": "string", + "enum": ["Success"] + }, + "ct": { + "type": "object", + "properties": { + "value": { + "type": "integer" + } + }, + "required": ["value"] + } + }, + "required": ["ty", "ct"] + }, { + "type": "object", + "properties": { + "ty": { + "type": "string", + "enum": ["Empty", "Error"] + } + }, + "required": ["ty"] + }] +}); + +#[derive(OpenapiType)] +#[openapi(untagged)] +enum EnumUntagged { + Success { value: isize }, + Empty, + Error +} +test_type!(EnumUntagged = { + "title": "EnumUntagged", + "oneOf": [{ + "type": "object", + "properties": { + "value": { + "type": "integer" + } + }, + "required": ["value"] + }, { + "type": "object", + "additionalProperties": false }] }); diff --git a/openapi_type_derive/src/lib.rs b/openapi_type_derive/src/lib.rs index e3bc9fc..6d82331 100644 --- a/openapi_type_derive/src/lib.rs +++ b/openapi_type_derive/src/lib.rs @@ -17,16 +17,33 @@ mod parser; use parser::*; /// The derive macro for [OpenapiType][openapi_type::OpenapiType]. -#[proc_macro_derive(OpenapiType)] +#[proc_macro_derive(OpenapiType, attributes(openapi))] pub fn derive_openapi_type(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input); expand_openapi_type(input).unwrap_or_else(|err| err.to_compile_error()).into() } fn expand_openapi_type(mut input: DeriveInput) -> syn::Result { + // parse #[serde] and #[openapi] attributes + let mut attrs = ContainerAttributes::default(); + for attr in &input.attrs { + if attr.path.is_ident("serde") { + parse_container_attrs(attr, &mut attrs, false)?; + } + } + for attr in &input.attrs { + if attr.path.is_ident("openapi") { + parse_container_attrs(attr, &mut attrs, true)?; + } + } + + // prepare impl block for codegen let ident = &input.ident; let name = ident.to_string(); - let name = LitStr::new(&name, ident.span()); + let mut name = LitStr::new(&name, ident.span()); + if let Some(rename) = &attrs.rename { + name = rename.clone(); + } // prepare the generics - all impl generics will get `OpenapiType` requirement let (impl_generics, ty_generics, where_clause) = { @@ -46,7 +63,7 @@ fn expand_openapi_type(mut input: DeriveInput) -> syn::Result { // parse the input data let parsed = match &input.data { Data::Struct(strukt) => parse_struct(strukt)?, - Data::Enum(inum) => parse_enum(inum)?, + Data::Enum(inum) => parse_enum(inum, &attrs)?, Data::Union(union) => parse_union(union)? }; diff --git a/openapi_type_derive/src/parser.rs b/openapi_type_derive/src/parser.rs index 8d0c15f..5e57736 100644 --- a/openapi_type_derive/src/parser.rs +++ b/openapi_type_derive/src/parser.rs @@ -1,5 +1,9 @@ -use crate::util::ToLitStr; -use syn::{spanned::Spanned as _, DataEnum, DataStruct, DataUnion, Fields, FieldsNamed, LitStr, Type}; +use crate::util::{ExpectLit, ToLitStr}; +use proc_macro2::Span; +use syn::{ + punctuated::Punctuated, spanned::Spanned as _, Attribute, DataEnum, DataStruct, DataUnion, Fields, FieldsNamed, LitStr, + Meta, Token, Type +}; pub(super) enum ParseDataType { Type(Type), @@ -36,7 +40,7 @@ pub(super) fn parse_struct(strukt: &DataStruct) -> syn::Result { } } -pub(super) fn parse_enum(inum: &DataEnum) -> syn::Result { +pub(super) fn parse_enum(inum: &DataEnum, attrs: &ContainerAttributes) -> syn::Result { let mut strings: Vec = Vec::new(); let mut types: Vec<(LitStr, ParseData)> = Vec::new(); @@ -54,19 +58,59 @@ pub(super) fn parse_enum(inum: &DataEnum) -> syn::Result { let data_strings = if strings.is_empty() { None } else { - Some(ParseData::Enum(strings)) + match (&attrs.tag, &attrs.content, attrs.untagged) { + // externally tagged (default) + (None, None, false) => Some(ParseData::Enum(strings)), + // internally tagged or adjacently tagged + (Some(tag), _, false) => Some(ParseData::Struct(vec![( + tag.clone(), + ParseDataType::Inline(ParseData::Enum(strings)) + )])), + // untagged + (None, None, true) => Some(ParseData::Unit), + // unknown + _ => return Err(syn::Error::new(Span::call_site(), "Unknown enum representation")) + } }; - let data_types = if types.is_empty() { - None - } else { - Some(ParseData::Alternatives( - types - .into_iter() - .map(|(name, data)| ParseData::Struct(vec![(name, ParseDataType::Inline(data))])) - .collect() - )) - }; + let data_types = + if types.is_empty() { + None + } else { + Some(ParseData::Alternatives( + types + .into_iter() + .map(|(name, mut data)| { + Ok(match (&attrs.tag, &attrs.content, attrs.untagged) { + // externally tagged (default) + (None, None, false) => ParseData::Struct(vec![(name, ParseDataType::Inline(data))]), + // internally tagged + (Some(tag), None, false) => { + match &mut data { + ParseData::Struct(fields) => { + fields.push((tag.clone(), ParseDataType::Inline(ParseData::Enum(vec![name])))) + }, + _ => return Err(syn::Error::new( + tag.span(), + "#[derive(OpenapiType)] does not support tuple variants on internally tagged enums" + )) + }; + data + }, + // adjacently tagged + (Some(tag), Some(content), false) => ParseData::Struct(vec![ + (tag.clone(), ParseDataType::Inline(ParseData::Enum(vec![name]))), + (content.clone(), ParseDataType::Inline(data)), + ]), + // untagged + (None, None, true) => data, + // unknown + _ => return Err(syn::Error::new(Span::call_site(), "Unknown enum representation")) + }) + }) + .collect::>>()? + )) + }; match (data_strings, data_types) { // only variants without fields @@ -96,3 +140,49 @@ pub(super) fn parse_union(union: &DataUnion) -> syn::Result { "#[derive(OpenapiType)] cannot be used on unions" )) } + +#[derive(Default)] +pub(super) struct ContainerAttributes { + pub(super) rename: Option, + pub(super) rename_all: Option, + pub(super) tag: Option, + pub(super) content: Option, + pub(super) untagged: bool +} + +pub(super) fn parse_container_attrs( + input: &Attribute, + attrs: &mut ContainerAttributes, + error_on_unknown: bool +) -> syn::Result<()> { + let tokens: Punctuated = input.parse_args_with(Punctuated::parse_terminated)?; + for token in tokens { + match token { + Meta::NameValue(kv) if kv.path.is_ident("rename") => { + attrs.rename = Some(kv.lit.expect_str()?); + }, + + Meta::NameValue(kv) if kv.path.is_ident("rename_all") => { + attrs.rename_all = Some(kv.lit.expect_str()?); + }, + + Meta::NameValue(kv) if kv.path.is_ident("tag") => { + attrs.tag = Some(kv.lit.expect_str()?); + }, + + Meta::NameValue(kv) if kv.path.is_ident("content") => { + attrs.content = Some(kv.lit.expect_str()?); + }, + + Meta::Path(path) if path.is_ident("untagged") => { + attrs.untagged = true; + }, + + Meta::Path(path) if error_on_unknown => return Err(syn::Error::new(path.span(), "Unexpected token")), + Meta::List(list) if error_on_unknown => return Err(syn::Error::new(list.span(), "Unexpected token")), + Meta::NameValue(kv) if error_on_unknown => return Err(syn::Error::new(kv.path.span(), "Unexpected token")), + _ => {} + } + } + Ok(()) +} diff --git a/openapi_type_derive/src/util.rs b/openapi_type_derive/src/util.rs index 9470a04..2a752e0 100644 --- a/openapi_type_derive/src/util.rs +++ b/openapi_type_derive/src/util.rs @@ -1,5 +1,5 @@ use proc_macro2::Ident; -use syn::LitStr; +use syn::{Lit, LitStr}; /// Convert any literal path into a [syn::Path]. macro_rules! path { @@ -36,3 +36,17 @@ impl ToLitStr for Ident { LitStr::new(&self.to_string(), self.span()) } } + +/// Convert a [Lit] to one specific literal type. +pub(crate) trait ExpectLit { + fn expect_str(self) -> syn::Result; +} + +impl ExpectLit for Lit { + fn expect_str(self) -> syn::Result { + match self { + Self::Str(str) => Ok(str), + _ => Err(syn::Error::new(self.span(), "Expected string literal")) + } + } +} From 2a35e044dbc4a433f6dcded23d3a41e8898b6a03 Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 9 Mar 2021 16:17:11 +0100 Subject: [PATCH 161/170] more error messages [skip ci] --- openapi_type/tests/fail/tuple_struct.rs | 6 ++++++ openapi_type/tests/fail/tuple_struct.stderr | 5 +++++ openapi_type/tests/fail/tuple_variant.rs | 8 ++++++++ openapi_type/tests/fail/tuple_variant.stderr | 5 +++++ openapi_type/tests/fail/union.rs | 9 +++++++++ openapi_type/tests/fail/union.stderr | 5 +++++ openapi_type/tests/fail/unknown_attribute.rs | 7 +++++++ openapi_type/tests/fail/unknown_attribute.stderr | 5 +++++ openapi_type_derive/src/parser.rs | 14 ++++++++++++-- 9 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 openapi_type/tests/fail/tuple_struct.rs create mode 100644 openapi_type/tests/fail/tuple_struct.stderr create mode 100644 openapi_type/tests/fail/tuple_variant.rs create mode 100644 openapi_type/tests/fail/tuple_variant.stderr create mode 100644 openapi_type/tests/fail/union.rs create mode 100644 openapi_type/tests/fail/union.stderr create mode 100644 openapi_type/tests/fail/unknown_attribute.rs create mode 100644 openapi_type/tests/fail/unknown_attribute.stderr diff --git a/openapi_type/tests/fail/tuple_struct.rs b/openapi_type/tests/fail/tuple_struct.rs new file mode 100644 index 0000000..146a236 --- /dev/null +++ b/openapi_type/tests/fail/tuple_struct.rs @@ -0,0 +1,6 @@ +use openapi_type::OpenapiType; + +#[derive(OpenapiType)] +struct Foo(i64, i64); + +fn main() {} diff --git a/openapi_type/tests/fail/tuple_struct.stderr b/openapi_type/tests/fail/tuple_struct.stderr new file mode 100644 index 0000000..b5ceb01 --- /dev/null +++ b/openapi_type/tests/fail/tuple_struct.stderr @@ -0,0 +1,5 @@ +error: #[derive(OpenapiType)] does not support tuple structs + --> $DIR/tuple_struct.rs:4:11 + | +4 | struct Foo(i64, i64); + | ^^^^^^^^^^ diff --git a/openapi_type/tests/fail/tuple_variant.rs b/openapi_type/tests/fail/tuple_variant.rs new file mode 100644 index 0000000..92aa8d7 --- /dev/null +++ b/openapi_type/tests/fail/tuple_variant.rs @@ -0,0 +1,8 @@ +use openapi_type::OpenapiType; + +#[derive(OpenapiType)] +enum Foo { + Pair(i64, i64) +} + +fn main() {} diff --git a/openapi_type/tests/fail/tuple_variant.stderr b/openapi_type/tests/fail/tuple_variant.stderr new file mode 100644 index 0000000..05573cb --- /dev/null +++ b/openapi_type/tests/fail/tuple_variant.stderr @@ -0,0 +1,5 @@ +error: #[derive(OpenapiType)] does not support tuple variants + --> $DIR/tuple_variant.rs:5:6 + | +5 | Pair(i64, i64) + | ^^^^^^^^^^ diff --git a/openapi_type/tests/fail/union.rs b/openapi_type/tests/fail/union.rs new file mode 100644 index 0000000..d011109 --- /dev/null +++ b/openapi_type/tests/fail/union.rs @@ -0,0 +1,9 @@ +use openapi_type::OpenapiType; + +#[derive(OpenapiType)] +union Foo { + signed: i64, + unsigned: u64 +} + +fn main() {} diff --git a/openapi_type/tests/fail/union.stderr b/openapi_type/tests/fail/union.stderr new file mode 100644 index 0000000..f0feb48 --- /dev/null +++ b/openapi_type/tests/fail/union.stderr @@ -0,0 +1,5 @@ +error: #[derive(OpenapiType)] cannot be used on unions + --> $DIR/union.rs:4:1 + | +4 | union Foo { + | ^^^^^ diff --git a/openapi_type/tests/fail/unknown_attribute.rs b/openapi_type/tests/fail/unknown_attribute.rs new file mode 100644 index 0000000..70a4785 --- /dev/null +++ b/openapi_type/tests/fail/unknown_attribute.rs @@ -0,0 +1,7 @@ +use openapi_type::OpenapiType; + +#[derive(OpenapiType)] +#[openapi(pizza)] +struct Foo; + +fn main() {} diff --git a/openapi_type/tests/fail/unknown_attribute.stderr b/openapi_type/tests/fail/unknown_attribute.stderr new file mode 100644 index 0000000..2558768 --- /dev/null +++ b/openapi_type/tests/fail/unknown_attribute.stderr @@ -0,0 +1,5 @@ +error: Unexpected token + --> $DIR/unknown_attribute.rs:4:11 + | +4 | #[openapi(pizza)] + | ^^^^^ diff --git a/openapi_type_derive/src/parser.rs b/openapi_type_derive/src/parser.rs index 5e57736..350fee2 100644 --- a/openapi_type_derive/src/parser.rs +++ b/openapi_type_derive/src/parser.rs @@ -35,7 +35,12 @@ fn parse_named_fields(named_fields: &FieldsNamed) -> syn::Result { pub(super) fn parse_struct(strukt: &DataStruct) -> syn::Result { match &strukt.fields { Fields::Named(named_fields) => parse_named_fields(named_fields), - Fields::Unnamed(_) => unimplemented!(), + Fields::Unnamed(unnamed_fields) => { + return Err(syn::Error::new( + unnamed_fields.span(), + "#[derive(OpenapiType)] does not support tuple structs" + )) + }, Fields::Unit => Ok(ParseData::Unit) } } @@ -50,7 +55,12 @@ pub(super) fn parse_enum(inum: &DataEnum, attrs: &ContainerAttributes) -> syn::R Fields::Named(named_fields) => { types.push((name, parse_named_fields(named_fields)?)); }, - Fields::Unnamed(_unnamed_fields) => unimplemented!(), + Fields::Unnamed(unnamed_fields) => { + return Err(syn::Error::new( + unnamed_fields.span(), + "#[derive(OpenapiType)] does not support tuple variants" + )) + }, Fields::Unit => strings.push(name) } } From eecd1924580d958233ea8a3dd7ba32b6000572ab Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 9 Mar 2021 17:07:16 +0100 Subject: [PATCH 162/170] redo and test openapi type implementations --- openapi_type/Cargo.toml | 4 + openapi_type/src/impls.rs | 483 +++++++++++++------------------- openapi_type/tests/std_types.rs | 216 ++++++++++++++ src/openapi/types.rs | 1 - 4 files changed, 413 insertions(+), 291 deletions(-) create mode 100644 openapi_type/tests/std_types.rs diff --git a/openapi_type/Cargo.toml b/openapi_type/Cargo.toml index 0513c0b..782f798 100644 --- a/openapi_type/Cargo.toml +++ b/openapi_type/Cargo.toml @@ -17,6 +17,10 @@ openapi_type_derive = "0.1.0-dev" openapiv3 = "=0.3.2" serde_json = "1.0" +# optional dependencies / features +chrono = { version = "0.4.19", optional = true } +uuid = { version = "0.8.2" , optional = true } + [dev-dependencies] paste = "1.0" serde = "1.0" diff --git a/openapi_type/src/impls.rs b/openapi_type/src/impls.rs index f363818..d9396fd 100644 --- a/openapi_type/src/impls.rs +++ b/openapi_type/src/impls.rs @@ -1,321 +1,224 @@ use crate::{OpenapiSchema, OpenapiType}; -use indexmap::IndexMap; +#[cfg(feature = "chrono")] +use chrono::{offset::TimeZone, Date, DateTime, NaiveDate, NaiveDateTime}; +use indexmap::{IndexMap, IndexSet}; use openapiv3::{ - AdditionalProperties, ArrayType, IntegerType, NumberFormat, NumberType, ObjectType, ReferenceOr, SchemaKind, StringType, - Type, VariantOrUnknownOrEmpty + AdditionalProperties, ArrayType, IntegerType, NumberFormat, NumberType, ObjectType, ReferenceOr, SchemaKind, + StringFormat, StringType, Type, VariantOrUnknownOrEmpty }; +use serde_json::Value; use std::{ - collections::{BTreeSet, HashMap, HashSet}, + collections::{BTreeMap, BTreeSet, HashMap, HashSet}, hash::BuildHasher, num::{NonZeroU128, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize} }; +#[cfg(feature = "uuid")] +use uuid::Uuid; -impl OpenapiType for () { - fn schema() -> OpenapiSchema { - OpenapiSchema::new(SchemaKind::Type(Type::Object(ObjectType { - additional_properties: Some(AdditionalProperties::Any(false)), - ..Default::default() - }))) +macro_rules! impl_openapi_type { + ($($ty:ident $(<$($generic:ident : $bound:path),+>)*),* => $schema:expr) => { + $( + impl $(<$($generic : $bound),+>)* OpenapiType for $ty $(<$($generic),+>)* { + fn schema() -> OpenapiSchema { + $schema + } + } + )* + }; +} + +type Unit = (); +impl_openapi_type!(Unit => { + OpenapiSchema::new(SchemaKind::Type(Type::Object(ObjectType { + additional_properties: Some(AdditionalProperties::Any(false)), + ..Default::default() + }))) +}); + +impl_openapi_type!(Value => { + OpenapiSchema { + nullable: true, + name: None, + schema: SchemaKind::Any(Default::default()), + dependencies: Default::default() } +}); + +impl_openapi_type!(bool => OpenapiSchema::new(SchemaKind::Type(Type::Boolean {}))); + +#[inline] +fn int_schema(minimum: Option, bits: Option) -> OpenapiSchema { + OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { + minimum, + format: bits + .map(|bits| VariantOrUnknownOrEmpty::Unknown(format!("int{}", bits))) + .unwrap_or(VariantOrUnknownOrEmpty::Empty), + ..Default::default() + }))) } -impl OpenapiType for bool { - fn schema() -> OpenapiSchema { - OpenapiSchema::new(SchemaKind::Type(Type::Boolean {})) - } +impl_openapi_type!(isize => int_schema(None, None)); +impl_openapi_type!(i8 => int_schema(None, Some(8))); +impl_openapi_type!(i16 => int_schema(None, Some(16))); +impl_openapi_type!(i32 => int_schema(None, Some(32))); +impl_openapi_type!(i64 => int_schema(None, Some(64))); +impl_openapi_type!(i128 => int_schema(None, Some(128))); + +impl_openapi_type!(usize => int_schema(Some(0), None)); +impl_openapi_type!(u8 => int_schema(Some(0), Some(8))); +impl_openapi_type!(u16 => int_schema(Some(0), Some(16))); +impl_openapi_type!(u32 => int_schema(Some(0), Some(32))); +impl_openapi_type!(u64 => int_schema(Some(0), Some(64))); +impl_openapi_type!(u128 => int_schema(Some(0), Some(128))); + +impl_openapi_type!(NonZeroUsize => int_schema(Some(1), None)); +impl_openapi_type!(NonZeroU8 => int_schema(Some(1), Some(8))); +impl_openapi_type!(NonZeroU16 => int_schema(Some(1), Some(16))); +impl_openapi_type!(NonZeroU32 => int_schema(Some(1), Some(32))); +impl_openapi_type!(NonZeroU64 => int_schema(Some(1), Some(64))); +impl_openapi_type!(NonZeroU128 => int_schema(Some(1), Some(128))); + +#[inline] +fn float_schema(format: NumberFormat) -> OpenapiSchema { + OpenapiSchema::new(SchemaKind::Type(Type::Number(NumberType { + format: VariantOrUnknownOrEmpty::Item(format), + ..Default::default() + }))) } -macro_rules! int_types { - ($($int_ty:ty),*) => {$( - impl OpenapiType for $int_ty - { - fn schema() -> OpenapiSchema - { - OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType::default()))) - } - } - )*}; +impl_openapi_type!(f32 => float_schema(NumberFormat::Float)); +impl_openapi_type!(f64 => float_schema(NumberFormat::Double)); - (unsigned $($int_ty:ty),*) => {$( - impl OpenapiType for $int_ty - { - fn schema() -> OpenapiSchema - { - OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { - minimum: Some(0), - ..Default::default() - }))) - } - } - )*}; - - (gtzero $($int_ty:ty),*) => {$( - impl OpenapiType for $int_ty - { - fn schema() -> OpenapiSchema - { - OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { - minimum: Some(1), - ..Default::default() - }))) - } - } - )*}; - - (bits = $bits:expr, $($int_ty:ty),*) => {$( - impl OpenapiType for $int_ty - { - fn schema() -> OpenapiSchema - { - OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { - format: VariantOrUnknownOrEmpty::Unknown(format!("int{}", $bits)), - ..Default::default() - }))) - } - } - )*}; - - (unsigned bits = $bits:expr, $($int_ty:ty),*) => {$( - impl OpenapiType for $int_ty - { - fn schema() -> OpenapiSchema - { - OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { - format: VariantOrUnknownOrEmpty::Unknown(format!("int{}", $bits)), - minimum: Some(0), - ..Default::default() - }))) - } - } - )*}; - - (gtzero bits = $bits:expr, $($int_ty:ty),*) => {$( - impl OpenapiType for $int_ty - { - fn schema() -> OpenapiSchema - { - OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { - format: VariantOrUnknownOrEmpty::Unknown(format!("int{}", $bits)), - minimum: Some(1), - ..Default::default() - }))) - } - } - )*}; +#[inline] +fn str_schema(format: VariantOrUnknownOrEmpty) -> OpenapiSchema { + OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { + format, + ..Default::default() + }))) } -int_types!(isize); -int_types!(unsigned usize); -int_types!(gtzero NonZeroUsize); -int_types!(bits = 8, i8); -int_types!(unsigned bits = 8, u8); -int_types!(gtzero bits = 8, NonZeroU8); -int_types!(bits = 16, i16); -int_types!(unsigned bits = 16, u16); -int_types!(gtzero bits = 16, NonZeroU16); -int_types!(bits = 32, i32); -int_types!(unsigned bits = 32, u32); -int_types!(gtzero bits = 32, NonZeroU32); -int_types!(bits = 64, i64); -int_types!(unsigned bits = 64, u64); -int_types!(gtzero bits = 64, NonZeroU64); -int_types!(bits = 128, i128); -int_types!(unsigned bits = 128, u128); -int_types!(gtzero bits = 128, NonZeroU128); - -macro_rules! num_types { - ($($num_ty:ty = $num_fmt:ident),*) => {$( - impl OpenapiType for $num_ty - { - fn schema() -> OpenapiSchema - { - OpenapiSchema::new(SchemaKind::Type(Type::Number(NumberType { - format: VariantOrUnknownOrEmpty::Item(NumberFormat::$num_fmt), - ..Default::default() - }))) - } - } - )*} -} - -num_types!(f32 = Float, f64 = Double); - -macro_rules! str_types { - ($($str_ty:ty),*) => {$( - impl OpenapiType for $str_ty - { - fn schema() -> OpenapiSchema - { - OpenapiSchema::new(SchemaKind::Type(Type::String(StringType::default()))) - } - } - )*}; - - (format = $format:ident, $($str_ty:ty),*) => {$( - impl OpenapiType for $str_ty - { - fn schema() -> OpenapiSchema - { - use openapiv3::StringFormat; - - OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { - format: VariantOrUnknownOrEmpty::Item(StringFormat::$format), - ..Default::default() - }))) - } - } - )*}; - - (format_str = $format:expr, $($str_ty:ty),*) => {$( - impl OpenapiType for $str_ty - { - fn schema() -> OpenapiSchema - { - OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { - format: VariantOrUnknownOrEmpty::Unknown($format.to_string()), - ..Default::default() - }))) - } - } - )*}; -} - -str_types!(String, &str); +impl_openapi_type!(String => str_schema(VariantOrUnknownOrEmpty::Empty)); #[cfg(feature = "chrono")] -str_types!(format = Date, Date, Date, Date, NaiveDate); +impl_openapi_type!(Date, NaiveDate => { + str_schema(VariantOrUnknownOrEmpty::Item(StringFormat::Date)) +}); + #[cfg(feature = "chrono")] -str_types!( - format = DateTime, - DateTime, - DateTime, - DateTime, - NaiveDateTime -); +impl_openapi_type!(DateTime, NaiveDateTime => { + str_schema(VariantOrUnknownOrEmpty::Item(StringFormat::DateTime)) +}); #[cfg(feature = "uuid")] -str_types!(format_str = "uuid", Uuid); +impl_openapi_type!(Uuid => { + str_schema(VariantOrUnknownOrEmpty::Unknown("uuid".to_owned())) +}); -impl OpenapiType for Option { - fn schema() -> OpenapiSchema { - let schema = T::schema(); - let mut dependencies = schema.dependencies.clone(); - let schema = match schema.name.clone() { - Some(name) => { - let reference = ReferenceOr::Reference { - reference: format!("#/components/schemas/{}", name) - }; - dependencies.insert(name, schema); - SchemaKind::AllOf { all_of: vec![reference] } - }, - None => schema.schema - }; +impl_openapi_type!(Option => { + let schema = T::schema(); + let mut dependencies = schema.dependencies.clone(); + let schema = match schema.name.clone() { + Some(name) => { + let reference = ReferenceOr::Reference { + reference: format!("#/components/schemas/{}", name) + }; + dependencies.insert(name, schema); + SchemaKind::AllOf { all_of: vec![reference] } + }, + None => schema.schema + }; - OpenapiSchema { - nullable: true, - name: None, - schema, - dependencies - } + OpenapiSchema { + nullable: true, + name: None, + schema, + dependencies + } +}); + +#[inline] +fn array_schema(unique_items: bool) -> OpenapiSchema { + let schema = T::schema(); + let mut dependencies = schema.dependencies.clone(); + + let items = match schema.name.clone() { + Some(name) => { + let reference = ReferenceOr::Reference { + reference: format!("#/components/schemas/{}", name) + }; + dependencies.insert(name, schema); + reference + }, + None => ReferenceOr::Item(Box::new(schema.into_schema())) + }; + + OpenapiSchema { + nullable: false, + name: None, + schema: SchemaKind::Type(Type::Array(ArrayType { + items, + min_items: None, + max_items: None, + unique_items + })), + dependencies } } -impl OpenapiType for Vec { - fn schema() -> OpenapiSchema { - let schema = T::schema(); - let mut dependencies = schema.dependencies.clone(); +impl_openapi_type!(Vec => array_schema::(false)); +impl_openapi_type!(BTreeSet, IndexSet, HashSet => { + array_schema::(true) +}); - let items = match schema.name.clone() { - Some(name) => { - let reference = ReferenceOr::Reference { - reference: format!("#/components/schemas/{}", name) - }; - dependencies.insert(name, schema); - reference - }, - None => ReferenceOr::Item(Box::new(schema.into_schema())) - }; +#[inline] +fn map_schema() -> OpenapiSchema { + let key_schema = K::schema(); + let mut dependencies = key_schema.dependencies.clone(); - OpenapiSchema { - nullable: false, - name: None, - schema: SchemaKind::Type(Type::Array(ArrayType { - items, - min_items: None, - max_items: None, - unique_items: false - })), - dependencies - } + let keys = match key_schema.name.clone() { + Some(name) => { + let reference = ReferenceOr::Reference { + reference: format!("#/components/schemas/{}", name) + }; + dependencies.insert(name, key_schema); + reference + }, + None => ReferenceOr::Item(Box::new(key_schema.into_schema())) + }; + + let schema = T::schema(); + dependencies.extend(schema.dependencies.iter().map(|(k, v)| (k.clone(), v.clone()))); + + let items = Box::new(match schema.name.clone() { + Some(name) => { + let reference = ReferenceOr::Reference { + reference: format!("#/components/schemas/{}", name) + }; + dependencies.insert(name, schema); + reference + }, + None => ReferenceOr::Item(schema.into_schema()) + }); + + let mut properties = IndexMap::new(); + properties.insert("default".to_owned(), keys); + + OpenapiSchema { + nullable: false, + name: None, + schema: SchemaKind::Type(Type::Object(ObjectType { + properties, + required: vec!["default".to_owned()], + additional_properties: Some(AdditionalProperties::Schema(items)), + ..Default::default() + })), + dependencies } } -impl OpenapiType for BTreeSet { - fn schema() -> OpenapiSchema { - as OpenapiType>::schema() - } -} - -impl OpenapiType for HashSet { - fn schema() -> OpenapiSchema { - as OpenapiType>::schema() - } -} - -impl OpenapiType for HashMap { - fn schema() -> OpenapiSchema { - let key_schema = K::schema(); - let mut dependencies = key_schema.dependencies.clone(); - - let keys = match key_schema.name.clone() { - Some(name) => { - let reference = ReferenceOr::Reference { - reference: format!("#/components/schemas/{}", name) - }; - dependencies.insert(name, key_schema); - reference - }, - None => ReferenceOr::Item(Box::new(key_schema.into_schema())) - }; - - let schema = T::schema(); - dependencies.extend(schema.dependencies.iter().map(|(k, v)| (k.clone(), v.clone()))); - - let items = Box::new(match schema.name.clone() { - Some(name) => { - let reference = ReferenceOr::Reference { - reference: format!("#/components/schemas/{}", name) - }; - dependencies.insert(name, schema); - reference - }, - None => ReferenceOr::Item(schema.into_schema()) - }); - - let mut properties = IndexMap::new(); - properties.insert("default".to_owned(), keys); - - OpenapiSchema { - nullable: false, - name: None, - schema: SchemaKind::Type(Type::Object(ObjectType { - properties, - required: vec!["default".to_owned()], - additional_properties: Some(AdditionalProperties::Schema(items)), - ..Default::default() - })), - dependencies - } - } -} - -impl OpenapiType for serde_json::Value { - fn schema() -> OpenapiSchema { - OpenapiSchema { - nullable: true, - name: None, - schema: SchemaKind::Any(Default::default()), - dependencies: Default::default() - } - } -} +impl_openapi_type!( + BTreeMap, + IndexMap, + HashMap + => map_schema::() +); diff --git a/openapi_type/tests/std_types.rs b/openapi_type/tests/std_types.rs new file mode 100644 index 0000000..e10fb89 --- /dev/null +++ b/openapi_type/tests/std_types.rs @@ -0,0 +1,216 @@ +#[cfg(feature = "chrono")] +use chrono::{Date, DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, Utc}; +use indexmap::{IndexMap, IndexSet}; +use openapi_type::OpenapiType; +use serde_json::Value; +use std::{ + collections::{BTreeMap, BTreeSet, HashMap, HashSet}, + num::{NonZeroU128, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize} +}; +#[cfg(feature = "uuid")] +use uuid::Uuid; + +macro_rules! test_type { + ($($ty:ident $(<$($generic:ident),+>)*),* = $json:tt) => { + paste::paste! { $( + #[test] + fn [< $ty:lower $($(_ $generic:lower)+)* >]() { + let schema = <$ty $(<$($generic),+>)* as OpenapiType>::schema(); + let schema = openapi_type::OpenapiSchema::into_schema(schema); + let schema_json = serde_json::to_value(&schema).unwrap(); + let expected = serde_json::json!($json); + assert_eq!(schema_json, expected); + } + )* } + }; +} + +type Unit = (); +test_type!(Unit = { + "type": "object", + "additionalProperties": false +}); + +test_type!(Value = { + "nullable": true +}); + +test_type!(bool = { + "type": "boolean" +}); + +// ### integer types + +test_type!(isize = { + "type": "integer" +}); + +test_type!(usize = { + "type": "integer", + "minimum": 0 +}); + +test_type!(i8 = { + "type": "integer", + "format": "int8" +}); + +test_type!(u8 = { + "type": "integer", + "format": "int8", + "minimum": 0 +}); + +test_type!(i16 = { + "type": "integer", + "format": "int16" +}); + +test_type!(u16 = { + "type": "integer", + "format": "int16", + "minimum": 0 +}); + +test_type!(i32 = { + "type": "integer", + "format": "int32" +}); + +test_type!(u32 = { + "type": "integer", + "format": "int32", + "minimum": 0 +}); + +test_type!(i64 = { + "type": "integer", + "format": "int64" +}); + +test_type!(u64 = { + "type": "integer", + "format": "int64", + "minimum": 0 +}); + +test_type!(i128 = { + "type": "integer", + "format": "int128" +}); + +test_type!(u128 = { + "type": "integer", + "format": "int128", + "minimum": 0 +}); + +// ### non-zero integer types + +test_type!(NonZeroUsize = { + "type": "integer", + "minimum": 1 +}); + +test_type!(NonZeroU8 = { + "type": "integer", + "format": "int8", + "minimum": 1 +}); + +test_type!(NonZeroU16 = { + "type": "integer", + "format": "int16", + "minimum": 1 +}); + +test_type!(NonZeroU32 = { + "type": "integer", + "format": "int32", + "minimum": 1 +}); + +test_type!(NonZeroU64 = { + "type": "integer", + "format": "int64", + "minimum": 1 +}); + +test_type!(NonZeroU128 = { + "type": "integer", + "format": "int128", + "minimum": 1 +}); + +// ### floats + +test_type!(f32 = { + "type": "number", + "format": "float" +}); + +test_type!(f64 = { + "type": "number", + "format": "double" +}); + +// ### string + +test_type!(String = { + "type": "string" +}); + +#[cfg(feature = "uuid")] +test_type!(Uuid = { + "type": "string", + "format": "uuid" +}); + +// ### date/time + +#[cfg(feature = "chrono")] +test_type!(Date, Date, Date, NaiveDate = { + "type": "string", + "format": "date" +}); + +#[cfg(feature = "chrono")] +test_type!(DateTime, DateTime, DateTime, NaiveDateTime = { + "type": "string", + "format": "date-time" +}); + +// ### some std types + +test_type!(Option = { + "type": "string", + "nullable": true +}); + +test_type!(Vec = { + "type": "array", + "items": { + "type": "string" + } +}); + +test_type!(BTreeSet, IndexSet, HashSet = { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true +}); + +test_type!(BTreeMap, IndexMap, HashMap = { + "type": "object", + "properties": { + "default": { + "type": "integer" + } + }, + "required": ["default"], + "additionalProperties": { + "type": "string" + } +}); diff --git a/src/openapi/types.rs b/src/openapi/types.rs index 18f5be6..66bf059 100644 --- a/src/openapi/types.rs +++ b/src/openapi/types.rs @@ -7,7 +7,6 @@ use openapiv3::{ ReferenceOr::{Item, Reference}, Schema, SchemaData, SchemaKind, StringType, Type, VariantOrUnknownOrEmpty }; - use std::{ collections::{BTreeSet, HashMap, HashSet}, hash::BuildHasher, From ebea39fe0d5f9ae446dfb42bbff70a38096376ab Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 9 Mar 2021 19:46:11 +0100 Subject: [PATCH 163/170] use openapi_type::OpenapiType for gotham_restful --- Cargo.toml | 13 +- README.md | 20 +- derive/Cargo.toml | 2 +- derive/src/endpoint.rs | 10 +- derive/src/lib.rs | 11 - derive/src/openapi_type.rs | 289 -------------------- derive/src/request_body.rs | 23 +- openapi_type/src/impls.rs | 2 +- openapi_type/src/lib.rs | 6 + openapi_type_derive/src/lib.rs | 2 +- src/endpoint.rs | 50 +++- src/lib.rs | 39 ++- src/openapi/builder.rs | 4 +- src/openapi/mod.rs | 1 - src/openapi/operation.rs | 3 +- src/openapi/router.rs | 3 +- src/openapi/types.rs | 476 --------------------------------- src/response/mod.rs | 7 +- src/response/no_content.rs | 6 +- src/response/raw.rs | 4 +- src/response/redirect.rs | 4 +- src/response/result.rs | 6 +- src/response/success.rs | 8 +- src/routing.rs | 12 +- src/types.rs | 5 +- tests/async_methods.rs | 4 +- tests/sync_methods.rs | 4 +- 27 files changed, 148 insertions(+), 866 deletions(-) delete mode 100644 derive/src/openapi_type.rs delete mode 100644 src/openapi/types.rs diff --git a/Cargo.toml b/Cargo.toml index 7c0baee..787b6da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,26 +24,23 @@ futures-core = "0.3.7" futures-util = "0.3.7" gotham = { git = "https://github.com/gotham-rs/gotham", default-features = false } gotham_derive = "0.5.0" -gotham_restful_derive = "0.2.0" +gotham_restful_derive = "0.3.0-dev" log = "0.4.8" mime = "0.3.16" serde = { version = "1.0.110", features = ["derive"] } serde_json = "1.0.58" thiserror = "1.0" -# features -chrono = { version = "0.4.19", features = ["serde"], optional = true } -uuid = { version = "0.8.1", optional = true } - # non-feature optional dependencies base64 = { version = "0.13.0", optional = true } cookie = { version = "0.15", optional = true } -gotham_middleware_diesel = { version = "0.2.0", optional = true } +gotham_middleware_diesel = { git = "https://github.com/gotham-rs/gotham", optional = true } indexmap = { version = "1.3.2", optional = true } indoc = { version = "1.0", optional = true } jsonwebtoken = { version = "7.1.0", optional = true } once_cell = { version = "1.5", optional = true } openapiv3 = { version = "=0.3.2", optional = true } +openapi_type = { version = "0.1.0-dev", optional = true } regex = { version = "1.4", optional = true } sha2 = { version = "0.9.3", optional = true } @@ -58,7 +55,7 @@ trybuild = "1.0.27" [features] default = ["cors", "errorlog", "without-openapi"] -full = ["auth", "chrono", "cors", "database", "errorlog", "openapi", "uuid"] +full = ["auth", "cors", "database", "errorlog", "openapi"] auth = ["gotham_restful_derive/auth", "base64", "cookie", "jsonwebtoken"] cors = [] @@ -67,7 +64,7 @@ errorlog = [] # These features are exclusive - https://gitlab.com/msrd0/gotham-restful/-/issues/4 without-openapi = [] -openapi = ["gotham_restful_derive/openapi", "base64", "indexmap", "indoc", "once_cell", "openapiv3", "regex", "sha2"] +openapi = ["gotham_restful_derive/openapi", "base64", "indexmap", "indoc", "once_cell", "openapiv3", "openapi_type", "regex", "sha2"] [package.metadata.docs.rs] no-default-features = true diff --git a/README.md b/README.md index 76da543..1cb82c1 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ use gotham_restful::gotham::hyper::Method; struct CustomResource; /// This type is used to parse path parameters. -#[derive(Deserialize, StateData, StaticResponseExtender)] +#[derive(Clone, Deserialize, StateData, StaticResponseExtender)] struct CustomPath { name: String } @@ -310,9 +310,9 @@ carefully both as a binary as well as a library author to avoid unwanted suprise In order to automatically create an openapi specification, gotham-restful needs knowledge over all routes and the types returned. `serde` does a great job at serialization but doesn't give -enough type information, so all types used in the router need to implement `OpenapiType`. This -can be derived for almoust any type and there should be no need to implement it manually. A simple -example looks like this: +enough type information, so all types used in the router need to implement +`OpenapiType`[openapi_type::OpenapiType]. This can be derived for almoust any type and there +should be no need to implement it manually. A simple example looks like this: ```rust #[derive(Resource)] @@ -350,15 +350,15 @@ clients in different languages without worying to exactly replicate your api in languages. However, please note that by default, the `without-openapi` feature of this crate is enabled. -Disabling it in favour of the `openapi` feature will add an additional type bound, [`OpenapiType`], -on some of the types in [`Endpoint`] and related traits. This means that some code might only -compile on either feature, but not on both. If you are writing a library that uses gotham-restful, -it is strongly recommended to pass both features through and conditionally enable the openapi -code, like this: +Disabling it in favour of the `openapi` feature will add an additional type bound, +[`OpenapiType`][openapi_type::OpenapiType], on some of the types in [`Endpoint`] and related +traits. This means that some code might only compile on either feature, but not on both. If you +are writing a library that uses gotham-restful, it is strongly recommended to pass both features +through and conditionally enable the openapi code, like this: ```rust #[derive(Deserialize, Serialize)] -#[cfg_attr(feature = "openapi", derive(OpenapiType))] +#[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))] struct Foo; ``` diff --git a/derive/Cargo.toml b/derive/Cargo.toml index e06f2b0..58c877e 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "gotham_restful_derive" -version = "0.2.0" +version = "0.3.0-dev" authors = ["Dominic Meiser "] edition = "2018" description = "Derive macros for gotham_restful" diff --git a/derive/src/endpoint.rs b/derive/src/endpoint.rs index f6f143b..457f8ee 100644 --- a/derive/src/endpoint.rs +++ b/derive/src/endpoint.rs @@ -128,14 +128,14 @@ impl EndpointType { fn placeholders_ty(&self, arg_ty: Option<&Type>) -> TokenStream { match self { Self::ReadAll | Self::Search | Self::Create | Self::UpdateAll | Self::DeleteAll => { - quote!(::gotham_restful::gotham::extractor::NoopPathExtractor) + quote!(::gotham_restful::NoopExtractor) }, Self::Read | Self::Update | Self::Delete => quote!(::gotham_restful::private::IdPlaceholder::<#arg_ty>), Self::Custom { .. } => { if self.has_placeholders().value { arg_ty.to_token_stream() } else { - quote!(::gotham_restful::gotham::extractor::NoopPathExtractor) + quote!(::gotham_restful::NoopExtractor) } }, } @@ -163,14 +163,14 @@ impl EndpointType { fn params_ty(&self, arg_ty: Option<&Type>) -> TokenStream { match self { Self::ReadAll | Self::Read | Self::Create | Self::UpdateAll | Self::Update | Self::DeleteAll | Self::Delete => { - quote!(::gotham_restful::gotham::extractor::NoopQueryStringExtractor) + quote!(::gotham_restful::NoopExtractor) }, Self::Search => quote!(#arg_ty), Self::Custom { .. } => { if self.needs_params().value { arg_ty.to_token_stream() } else { - quote!(::gotham_restful::gotham::extractor::NoopQueryStringExtractor) + quote!(::gotham_restful::NoopExtractor) } }, } @@ -201,7 +201,7 @@ impl EndpointType { if self.needs_body().value { arg_ty.to_token_stream() } else { - quote!(::gotham_restful::gotham::extractor::NoopPathExtractor) + quote!(()) } }, } diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 39e2855..59ee8b6 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -24,11 +24,6 @@ use resource::expand_resource; mod resource_error; use resource_error::expand_resource_error; -#[cfg(feature = "openapi")] -mod openapi_type; -#[cfg(feature = "openapi")] -use openapi_type::expand_openapi_type; - mod private_openapi_trait; use private_openapi_trait::expand_private_openapi_trait; @@ -66,12 +61,6 @@ pub fn derive_from_body(input: TokenStream) -> TokenStream { expand_derive(input, expand_from_body) } -#[cfg(feature = "openapi")] -#[proc_macro_derive(OpenapiType, attributes(openapi))] -pub fn derive_openapi_type(input: TokenStream) -> TokenStream { - expand_derive(input, expand_openapi_type) -} - #[proc_macro_derive(RequestBody, attributes(supported_types))] pub fn derive_request_body(input: TokenStream) -> TokenStream { expand_derive(input, expand_request_body) diff --git a/derive/src/openapi_type.rs b/derive/src/openapi_type.rs deleted file mode 100644 index 4b4530d..0000000 --- a/derive/src/openapi_type.rs +++ /dev/null @@ -1,289 +0,0 @@ -use crate::util::{remove_parens, CollectToResult}; -use proc_macro2::{Ident, TokenStream}; -use quote::quote; -use syn::{ - parse_macro_input, spanned::Spanned, Attribute, AttributeArgs, Data, DataEnum, DataStruct, DeriveInput, Error, Field, - Fields, GenericParam, Generics, Lit, LitStr, Meta, NestedMeta, Path, PathSegment, PredicateType, Result, TraitBound, - TraitBoundModifier, Type, TypeParamBound, TypePath, Variant, WhereClause, WherePredicate -}; - -pub fn expand_openapi_type(input: DeriveInput) -> Result { - match (input.ident, input.generics, input.attrs, input.data) { - (ident, generics, attrs, Data::Enum(inum)) => expand_enum(ident, generics, attrs, inum), - (ident, generics, attrs, Data::Struct(strukt)) => expand_struct(ident, generics, attrs, strukt), - (_, _, _, Data::Union(uni)) => Err(Error::new( - uni.union_token.span(), - "#[derive(OpenapiType)] only works for structs and enums" - )) - } -} - -fn update_generics(generics: &Generics, where_clause: &mut Option) { - if generics.params.is_empty() { - return; - } - - if where_clause.is_none() { - *where_clause = Some(WhereClause { - where_token: Default::default(), - predicates: Default::default() - }); - } - let where_clause = where_clause.as_mut().unwrap(); - - for param in &generics.params { - if let GenericParam::Type(ty_param) = param { - where_clause.predicates.push(WherePredicate::Type(PredicateType { - lifetimes: None, - bounded_ty: Type::Path(TypePath { - qself: None, - path: Path { - leading_colon: None, - segments: vec![PathSegment { - ident: ty_param.ident.clone(), - arguments: Default::default() - }] - .into_iter() - .collect() - } - }), - colon_token: Default::default(), - bounds: vec![TypeParamBound::Trait(TraitBound { - paren_token: None, - modifier: TraitBoundModifier::None, - lifetimes: None, - path: syn::parse_str("::gotham_restful::OpenapiType").unwrap() - })] - .into_iter() - .collect() - })); - } - } -} - -#[derive(Debug, Default)] -struct Attrs { - nullable: bool, - rename: Option -} - -fn to_string(lit: &Lit) -> Result { - match lit { - Lit::Str(str) => Ok(str.value()), - _ => Err(Error::new(lit.span(), "Expected string literal")) - } -} - -fn to_bool(lit: &Lit) -> Result { - match lit { - Lit::Bool(bool) => Ok(bool.value), - _ => Err(Error::new(lit.span(), "Expected bool")) - } -} - -fn parse_attributes(input: &[Attribute]) -> Result { - let mut parsed = Attrs::default(); - for attr in input { - if attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("openapi".to_owned()) { - let tokens = remove_parens(attr.tokens.clone()); - // TODO this is not public api but syn currently doesn't offer another convenient way to parse AttributeArgs - let nested = parse_macro_input::parse::(tokens.into())?; - for meta in nested { - match &meta { - NestedMeta::Meta(Meta::NameValue(kv)) => match kv.path.segments.last().map(|s| s.ident.to_string()) { - Some(key) => match key.as_ref() { - "nullable" => parsed.nullable = to_bool(&kv.lit)?, - "rename" => parsed.rename = Some(to_string(&kv.lit)?), - _ => return Err(Error::new(kv.path.span(), "Unknown key")) - }, - _ => return Err(Error::new(meta.span(), "Unexpected token")) - }, - _ => return Err(Error::new(meta.span(), "Unexpected token")) - } - } - } - } - Ok(parsed) -} - -fn expand_variant(variant: &Variant) -> Result { - if !matches!(variant.fields, Fields::Unit) { - return Err(Error::new( - variant.span(), - "#[derive(OpenapiType)] does not support enum variants with fields" - )); - } - - let ident = &variant.ident; - - let attrs = parse_attributes(&variant.attrs)?; - let name = match attrs.rename { - Some(rename) => rename, - None => ident.to_string() - }; - - Ok(quote! { - enumeration.push(#name.to_string()); - }) -} - -fn expand_enum(ident: Ident, generics: Generics, attrs: Vec, input: DataEnum) -> Result { - let krate = super::krate(); - let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - let mut where_clause = where_clause.cloned(); - update_generics(&generics, &mut where_clause); - - let attrs = parse_attributes(&attrs)?; - let nullable = attrs.nullable; - let name = match attrs.rename { - Some(rename) => rename, - None => ident.to_string() - }; - - let variants = input.variants.iter().map(expand_variant).collect_to_result()?; - - Ok(quote! { - impl #impl_generics #krate::OpenapiType for #ident #ty_generics - #where_clause - { - fn schema() -> #krate::OpenapiSchema - { - use #krate::{private::openapi::*, OpenapiSchema}; - - let mut enumeration : Vec = Vec::new(); - - #(#variants)* - - let schema = SchemaKind::Type(Type::String(StringType { - format: VariantOrUnknownOrEmpty::Empty, - enumeration, - ..Default::default() - })); - - OpenapiSchema { - name: Some(#name.to_string()), - nullable: #nullable, - schema, - dependencies: Default::default() - } - } - } - }) -} - -fn expand_field(field: &Field) -> Result { - let ident = match &field.ident { - Some(ident) => 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 ty = &field.ty; - - let attrs = parse_attributes(&field.attrs)?; - let nullable = attrs.nullable; - let name = match attrs.rename { - Some(rename) => rename, - None => ident.to_string() - }; - - Ok(quote! {{ - let mut schema = <#ty>::schema(); - - if schema.nullable - { - schema.nullable = false; - } - else if !#nullable - { - required.push(#ident_str.to_string()); - } - - let keys : Vec = schema.dependencies.keys().map(|k| k.to_string()).collect(); - for dep in keys - { - let dep_schema = schema.dependencies.swap_remove(&dep); - if let Some(dep_schema) = dep_schema - { - dependencies.insert(dep, dep_schema); - } - } - - match schema.name.clone() { - Some(schema_name) => { - properties.insert( - #name.to_string(), - ReferenceOr::Reference { reference: format!("#/components/schemas/{}", schema_name) } - ); - dependencies.insert(schema_name, schema); - }, - None => { - properties.insert( - #name.to_string(), - ReferenceOr::Item(Box::new(schema.into_schema())) - ); - } - } - }}) -} - -fn expand_struct(ident: Ident, generics: Generics, attrs: Vec, input: DataStruct) -> Result { - let krate = super::krate(); - let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - let mut where_clause = where_clause.cloned(); - update_generics(&generics, &mut where_clause); - - let attrs = parse_attributes(&attrs)?; - let nullable = attrs.nullable; - let name = match attrs.rename { - Some(rename) => rename, - None => ident.to_string() - }; - - let fields: Vec = match input.fields { - Fields::Named(named_fields) => named_fields.named.iter().map(expand_field).collect_to_result()?, - Fields::Unnamed(fields) => { - return Err(Error::new( - fields.span(), - "#[derive(OpenapiType)] does not support unnamed fields" - )) - }, - Fields::Unit => Vec::new() - }; - - Ok(quote! { - impl #impl_generics #krate::OpenapiType for #ident #ty_generics - #where_clause - { - fn schema() -> #krate::OpenapiSchema - { - use #krate::{private::{openapi::*, IndexMap}, OpenapiSchema}; - - let mut properties : IndexMap>> = IndexMap::new(); - let mut required : Vec = Vec::new(); - let mut dependencies : IndexMap = IndexMap::new(); - - #(#fields)* - - let schema = SchemaKind::Type(Type::Object(ObjectType { - properties, - required, - additional_properties: None, - min_properties: None, - max_properties: None - })); - - OpenapiSchema { - name: Some(#name.to_string()), - nullable: #nullable, - schema, - dependencies - } - } - } - }) -} diff --git a/derive/src/request_body.rs b/derive/src/request_body.rs index 9657b21..c543dfa 100644 --- a/derive/src/request_body.rs +++ b/derive/src/request_body.rs @@ -26,18 +26,25 @@ fn impl_openapi_type(_ident: &Ident, _generics: &Generics) -> TokenStream { #[cfg(feature = "openapi")] fn impl_openapi_type(ident: &Ident, generics: &Generics) -> TokenStream { let krate = super::krate(); + let openapi = quote!(#krate::private::openapi); quote! { - impl #generics #krate::OpenapiType for #ident #generics + impl #generics #krate::private::OpenapiType for #ident #generics { - fn schema() -> #krate::OpenapiSchema + fn schema() -> #krate::private::OpenapiSchema { - use #krate::{private::openapi::*, OpenapiSchema}; - - OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { - format: VariantOrUnknownOrEmpty::Item(StringFormat::Binary), - ..Default::default() - }))) + #krate::private::OpenapiSchema::new( + #openapi::SchemaKind::Type( + #openapi::Type::String( + #openapi::StringType { + format: #openapi::VariantOrUnknownOrEmpty::Item( + #openapi::StringFormat::Binary + ), + .. ::std::default::Default::default() + } + ) + ) + ) } } } diff --git a/openapi_type/src/impls.rs b/openapi_type/src/impls.rs index d9396fd..d46a922 100644 --- a/openapi_type/src/impls.rs +++ b/openapi_type/src/impls.rs @@ -97,7 +97,7 @@ fn str_schema(format: VariantOrUnknownOrEmpty) -> OpenapiSchema { }))) } -impl_openapi_type!(String => str_schema(VariantOrUnknownOrEmpty::Empty)); +impl_openapi_type!(String, str => str_schema(VariantOrUnknownOrEmpty::Empty)); #[cfg(feature = "chrono")] impl_openapi_type!(Date, NaiveDate => { diff --git a/openapi_type/src/lib.rs b/openapi_type/src/lib.rs index 590800b..2933027 100644 --- a/openapi_type/src/lib.rs +++ b/openapi_type/src/lib.rs @@ -78,3 +78,9 @@ struct MyResponse { pub trait OpenapiType { fn schema() -> OpenapiSchema; } + +impl<'a, T: ?Sized + OpenapiType> OpenapiType for &'a T { + fn schema() -> OpenapiSchema { + T::schema() + } +} diff --git a/openapi_type_derive/src/lib.rs b/openapi_type_derive/src/lib.rs index 6d82331..10e6c0a 100644 --- a/openapi_type_derive/src/lib.rs +++ b/openapi_type_derive/src/lib.rs @@ -55,7 +55,7 @@ fn expand_openapi_type(mut input: DeriveInput) -> syn::Result { modifier: TraitBoundModifier::None, lifetimes: None, path: path!(::openapi_type::OpenapiType) - })) + })); }); generics.split_for_impl() }; diff --git a/src/endpoint.rs b/src/endpoint.rs index d8da412..2095948 100644 --- a/src/endpoint.rs +++ b/src/endpoint.rs @@ -2,11 +2,41 @@ use crate::{IntoResponse, RequestBody}; use futures_util::future::BoxFuture; use gotham::{ extractor::{PathExtractor, QueryStringExtractor}, - hyper::{Body, Method}, - state::State + hyper::{Body, Method, Response}, + router::response::extender::StaticResponseExtender, + state::{State, StateData} }; +#[cfg(feature = "openapi")] +use openapi_type::{OpenapiSchema, OpenapiType}; +use serde::{Deserialize, Deserializer}; use std::borrow::Cow; +/// A no-op extractor that can be used as a default type for [Endpoint::Placeholders] and +/// [Endpoint::Params]. +#[derive(Debug, Clone, Copy)] +pub struct NoopExtractor; + +impl<'de> Deserialize<'de> for NoopExtractor { + fn deserialize>(_: D) -> Result { + Ok(Self) + } +} + +#[cfg(feature = "openapi")] +impl OpenapiType for NoopExtractor { + fn schema() -> OpenapiSchema { + warn!("You're asking for the OpenAPI Schema for gotham_restful::NoopExtractor. This is probably not what you want."); + <() as OpenapiType>::schema() + } +} + +impl StateData for NoopExtractor {} + +impl StaticResponseExtender for NoopExtractor { + type ResBody = Body; + fn extend(_: &mut State, _: &mut Response) {} +} + // TODO: Specify default types once https://github.com/rust-lang/rust/issues/29661 lands. #[_private_openapi_trait(EndpointWithSchema)] pub trait Endpoint { @@ -23,19 +53,19 @@ pub trait Endpoint { fn has_placeholders() -> bool { false } - /// The type that parses the URI placeholders. Use [gotham::extractor::NoopPathExtractor] - /// if `has_placeholders()` returns `false`. - #[openapi_bound("Placeholders: crate::OpenapiType")] - type Placeholders: PathExtractor + Sync; + /// The type that parses the URI placeholders. Use [NoopExtractor] if `has_placeholders()` + /// returns `false`. + #[openapi_bound("Placeholders: OpenapiType")] + type Placeholders: PathExtractor + Clone + Sync; /// Returns `true` _iff_ the request parameters should be parsed. `false` by default. fn needs_params() -> bool { false } - /// The type that parses the request parameters. Use [gotham::extractor::NoopQueryStringExtractor] - /// if `needs_params()` returns `false`. - #[openapi_bound("Params: crate::OpenapiType")] - type Params: QueryStringExtractor + Sync; + /// The type that parses the request parameters. Use [NoopExtractor] if `needs_params()` + /// returns `false`. + #[openapi_bound("Params: OpenapiType")] + type Params: QueryStringExtractor + Clone + Sync; /// Returns `true` _iff_ the request body should be parsed. `false` by default. fn needs_body() -> bool { diff --git a/src/lib.rs b/src/lib.rs index 36674c3..aea56a4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -60,7 +60,7 @@ struct FooResource; /// The return type of the foo read endpoint. #[derive(Serialize)] -# #[cfg_attr(feature = "openapi", derive(OpenapiType))] +# #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))] struct Foo { id: u64 } @@ -95,8 +95,8 @@ use gotham_restful::gotham::hyper::Method; struct CustomResource; /// This type is used to parse path parameters. -#[derive(Deserialize, StateData, StaticResponseExtender)] -# #[cfg_attr(feature = "openapi", derive(OpenapiType))] +#[derive(Clone, Deserialize, StateData, StaticResponseExtender)] +# #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))] struct CustomPath { name: String } @@ -225,7 +225,7 @@ A simple example that uses only a single secret looks like this: struct SecretResource; #[derive(Serialize)] -# #[cfg_attr(feature = "openapi", derive(OpenapiType))] +# #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))] struct Secret { id: u64, intended_for: String @@ -331,7 +331,7 @@ A simple non-async example looks like this: struct FooResource; #[derive(Queryable, Serialize)] -# #[cfg_attr(feature = "openapi", derive(OpenapiType))] +# #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))] struct Foo { id: i64, value: String @@ -363,9 +363,9 @@ carefully both as a binary as well as a library author to avoid unwanted suprise In order to automatically create an openapi specification, gotham-restful needs knowledge over all routes and the types returned. `serde` does a great job at serialization but doesn't give -enough type information, so all types used in the router need to implement `OpenapiType`. This -can be derived for almoust any type and there should be no need to implement it manually. A simple -example looks like this: +enough type information, so all types used in the router need to implement +`OpenapiType`[openapi_type::OpenapiType]. This can be derived for almoust any type and there +should be no need to implement it manually. A simple example looks like this: ```rust,no_run # #[macro_use] extern crate gotham_restful_derive; @@ -373,6 +373,7 @@ example looks like this: # mod openapi_feature_enabled { # use gotham::{router::builder::*, state::State}; # use gotham_restful::*; +# use openapi_type::OpenapiType; # use serde::{Deserialize, Serialize}; #[derive(Resource)] #[resource(read_all)] @@ -410,17 +411,17 @@ clients in different languages without worying to exactly replicate your api in languages. However, please note that by default, the `without-openapi` feature of this crate is enabled. -Disabling it in favour of the `openapi` feature will add an additional type bound, [`OpenapiType`], -on some of the types in [`Endpoint`] and related traits. This means that some code might only -compile on either feature, but not on both. If you are writing a library that uses gotham-restful, -it is strongly recommended to pass both features through and conditionally enable the openapi -code, like this: +Disabling it in favour of the `openapi` feature will add an additional type bound, +[`OpenapiType`][openapi_type::OpenapiType], on some of the types in [`Endpoint`] and related +traits. This means that some code might only compile on either feature, but not on both. If you +are writing a library that uses gotham-restful, it is strongly recommended to pass both features +through and conditionally enable the openapi code, like this: ```rust # #[macro_use] extern crate gotham_restful; # use serde::{Deserialize, Serialize}; #[derive(Deserialize, Serialize)] -#[cfg_attr(feature = "openapi", derive(OpenapiType))] +#[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))] struct Foo; ``` @@ -478,6 +479,8 @@ pub mod private { #[cfg(feature = "openapi")] pub use indexmap::IndexMap; #[cfg(feature = "openapi")] + pub use openapi_type::{OpenapiSchema, OpenapiType}; + #[cfg(feature = "openapi")] pub use openapiv3 as openapi; } @@ -494,16 +497,12 @@ pub use cors::{handle_cors, CorsConfig, CorsRoute}; #[cfg(feature = "openapi")] mod openapi; #[cfg(feature = "openapi")] -pub use openapi::{ - builder::OpenapiInfo, - router::GetOpenapi, - types::{OpenapiSchema, OpenapiType} -}; +pub use openapi::{builder::OpenapiInfo, router::GetOpenapi}; mod endpoint; -pub use endpoint::Endpoint; #[cfg(feature = "openapi")] pub use endpoint::EndpointWithSchema; +pub use endpoint::{Endpoint, NoopExtractor}; mod response; pub use response::{ diff --git a/src/openapi/builder.rs b/src/openapi/builder.rs index 11f79f8..4fa6a0d 100644 --- a/src/openapi/builder.rs +++ b/src/openapi/builder.rs @@ -1,5 +1,5 @@ -use crate::OpenapiSchema; use indexmap::IndexMap; +use openapi_type::OpenapiSchema; use openapiv3::{ Components, OpenAPI, PathItem, ReferenceOr, ReferenceOr::{Item, Reference}, @@ -104,7 +104,7 @@ impl OpenapiBuilder { #[allow(dead_code)] mod test { use super::*; - use crate::OpenapiType; + use openapi_type::OpenapiType; #[derive(OpenapiType)] struct Message { diff --git a/src/openapi/mod.rs b/src/openapi/mod.rs index 500d190..5eefc1f 100644 --- a/src/openapi/mod.rs +++ b/src/openapi/mod.rs @@ -4,4 +4,3 @@ pub mod builder; pub mod handler; pub mod operation; pub mod router; -pub mod types; diff --git a/src/openapi/operation.rs b/src/openapi/operation.rs index 62d06d5..1823b3c 100644 --- a/src/openapi/operation.rs +++ b/src/openapi/operation.rs @@ -1,7 +1,8 @@ use super::SECURITY_NAME; -use crate::{response::OrAllTypes, EndpointWithSchema, IntoResponse, OpenapiSchema, RequestBody, ResponseSchema}; +use crate::{response::OrAllTypes, EndpointWithSchema, IntoResponse, RequestBody, ResponseSchema}; use indexmap::IndexMap; use mime::Mime; +use openapi_type::OpenapiSchema; use openapiv3::{ MediaType, Operation, Parameter, ParameterData, ParameterSchemaOrContent, ReferenceOr, ReferenceOr::Item, RequestBody as OARequestBody, Response, Responses, Schema, SchemaKind, StatusCode, Type diff --git a/src/openapi/router.rs b/src/openapi/router.rs index 3ced31b..e6b3187 100644 --- a/src/openapi/router.rs +++ b/src/openapi/router.rs @@ -3,9 +3,10 @@ use super::{ handler::{OpenapiHandler, SwaggerUiHandler}, operation::OperationDescription }; -use crate::{routing::*, EndpointWithSchema, OpenapiType, ResourceWithSchema, ResponseSchema}; +use crate::{routing::*, EndpointWithSchema, ResourceWithSchema, ResponseSchema}; use gotham::{hyper::Method, pipeline::chain::PipelineHandleChain, router::builder::*}; use once_cell::sync::Lazy; +use openapi_type::OpenapiType; use regex::{Captures, Regex}; use std::panic::RefUnwindSafe; diff --git a/src/openapi/types.rs b/src/openapi/types.rs deleted file mode 100644 index 66bf059..0000000 --- a/src/openapi/types.rs +++ /dev/null @@ -1,476 +0,0 @@ -#[cfg(feature = "chrono")] -use chrono::{Date, DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, Utc}; -use gotham::extractor::{NoopPathExtractor, NoopQueryStringExtractor}; -use indexmap::IndexMap; -use openapiv3::{ - AdditionalProperties, ArrayType, IntegerType, NumberFormat, NumberType, ObjectType, - ReferenceOr::{Item, Reference}, - Schema, SchemaData, SchemaKind, StringType, Type, VariantOrUnknownOrEmpty -}; -use std::{ - collections::{BTreeSet, HashMap, HashSet}, - hash::BuildHasher, - num::{NonZeroU128, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize} -}; -#[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 -already implemented for primitive types, String, Vec, Option and the like. To have it available -for your type, simply derive from [OpenapiType]. -*/ -#[derive(Debug, Clone, PartialEq)] -pub struct OpenapiSchema { - /// The name of this schema. If it is None, the schema will be inlined. - pub name: Option, - /// 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 - /// an optional parameter. - pub nullable: bool, - /// The actual OpenAPI schema. - pub schema: SchemaKind, - /// Other schemas that this schema depends on. They will be included in the final OpenAPI Spec - /// along with this schema. - pub dependencies: IndexMap -} - -impl OpenapiSchema { - /// Create a new schema that has no name. - pub fn new(schema: SchemaKind) -> Self { - Self { - name: None, - nullable: false, - schema, - dependencies: IndexMap::new() - } - } - - /// Convert this schema to an [openapiv3::Schema] that can be serialized to the OpenAPI Spec. - pub fn into_schema(self) -> Schema { - Schema { - schema_data: SchemaData { - nullable: self.nullable, - title: self.name, - ..Default::default() - }, - schema_kind: self.schema - } - } -} - -/** -This trait needs to be implemented by every type that is being used in the OpenAPI Spec. It gives -access to the [OpenapiSchema] of this type. It is provided for primitive types, String and the -like. For use on your own types, there is a derive macro: - -``` -# #[macro_use] extern crate gotham_restful_derive; -# -#[derive(OpenapiType)] -struct MyResponse { - message: String -} -``` -*/ -pub trait OpenapiType { - fn schema() -> OpenapiSchema; -} - -impl OpenapiType for () { - fn schema() -> OpenapiSchema { - OpenapiSchema::new(SchemaKind::Type(Type::Object(ObjectType { - additional_properties: Some(AdditionalProperties::Any(false)), - ..Default::default() - }))) - } -} - -impl OpenapiType for NoopPathExtractor { - fn schema() -> OpenapiSchema { - warn!("You're asking for the OpenAPI Schema for gotham::extractor::NoopPathExtractor. This is probably not what you want."); - <()>::schema() - } -} - -impl OpenapiType for NoopQueryStringExtractor { - fn schema() -> OpenapiSchema { - warn!("You're asking for the OpenAPI Schema for gotham::extractor::NoopQueryStringExtractor. This is probably not what you want."); - <()>::schema() - } -} - -impl OpenapiType for bool { - fn schema() -> OpenapiSchema { - OpenapiSchema::new(SchemaKind::Type(Type::Boolean {})) - } -} - -macro_rules! int_types { - ($($int_ty:ty),*) => {$( - impl OpenapiType for $int_ty - { - fn schema() -> OpenapiSchema - { - OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType::default()))) - } - } - )*}; - - (unsigned $($int_ty:ty),*) => {$( - impl OpenapiType for $int_ty - { - fn schema() -> OpenapiSchema - { - OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { - minimum: Some(0), - ..Default::default() - }))) - } - } - )*}; - - (gtzero $($int_ty:ty),*) => {$( - impl OpenapiType for $int_ty - { - fn schema() -> OpenapiSchema - { - OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { - minimum: Some(1), - ..Default::default() - }))) - } - } - )*}; - - (bits = $bits:expr, $($int_ty:ty),*) => {$( - impl OpenapiType for $int_ty - { - fn schema() -> OpenapiSchema - { - OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { - format: VariantOrUnknownOrEmpty::Unknown(format!("int{}", $bits)), - ..Default::default() - }))) - } - } - )*}; - - (unsigned bits = $bits:expr, $($int_ty:ty),*) => {$( - impl OpenapiType for $int_ty - { - fn schema() -> OpenapiSchema - { - OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { - format: VariantOrUnknownOrEmpty::Unknown(format!("int{}", $bits)), - minimum: Some(0), - ..Default::default() - }))) - } - } - )*}; - - (gtzero bits = $bits:expr, $($int_ty:ty),*) => {$( - impl OpenapiType for $int_ty - { - fn schema() -> OpenapiSchema - { - OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { - format: VariantOrUnknownOrEmpty::Unknown(format!("int{}", $bits)), - minimum: Some(1), - ..Default::default() - }))) - } - } - )*}; -} - -int_types!(isize); -int_types!(unsigned usize); -int_types!(gtzero NonZeroUsize); -int_types!(bits = 8, i8); -int_types!(unsigned bits = 8, u8); -int_types!(gtzero bits = 8, NonZeroU8); -int_types!(bits = 16, i16); -int_types!(unsigned bits = 16, u16); -int_types!(gtzero bits = 16, NonZeroU16); -int_types!(bits = 32, i32); -int_types!(unsigned bits = 32, u32); -int_types!(gtzero bits = 32, NonZeroU32); -int_types!(bits = 64, i64); -int_types!(unsigned bits = 64, u64); -int_types!(gtzero bits = 64, NonZeroU64); -int_types!(bits = 128, i128); -int_types!(unsigned bits = 128, u128); -int_types!(gtzero bits = 128, NonZeroU128); - -macro_rules! num_types { - ($($num_ty:ty = $num_fmt:ident),*) => {$( - impl OpenapiType for $num_ty - { - fn schema() -> OpenapiSchema - { - OpenapiSchema::new(SchemaKind::Type(Type::Number(NumberType { - format: VariantOrUnknownOrEmpty::Item(NumberFormat::$num_fmt), - ..Default::default() - }))) - } - } - )*} -} - -num_types!(f32 = Float, f64 = Double); - -macro_rules! str_types { - ($($str_ty:ty),*) => {$( - impl OpenapiType for $str_ty - { - fn schema() -> OpenapiSchema - { - OpenapiSchema::new(SchemaKind::Type(Type::String(StringType::default()))) - } - } - )*}; - - (format = $format:ident, $($str_ty:ty),*) => {$( - impl OpenapiType for $str_ty - { - fn schema() -> OpenapiSchema - { - use openapiv3::StringFormat; - - OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { - format: VariantOrUnknownOrEmpty::Item(StringFormat::$format), - ..Default::default() - }))) - } - } - )*}; - - (format_str = $format:expr, $($str_ty:ty),*) => {$( - impl OpenapiType for $str_ty - { - fn schema() -> OpenapiSchema - { - OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { - format: VariantOrUnknownOrEmpty::Unknown($format.to_string()), - ..Default::default() - }))) - } - } - )*}; -} - -str_types!(String, &str); - -#[cfg(feature = "chrono")] -str_types!(format = Date, Date, Date, Date, NaiveDate); -#[cfg(feature = "chrono")] -str_types!( - format = DateTime, - DateTime, - DateTime, - DateTime, - NaiveDateTime -); - -#[cfg(feature = "uuid")] -str_types!(format_str = "uuid", Uuid); - -impl OpenapiType for Option { - fn schema() -> OpenapiSchema { - let schema = T::schema(); - let mut dependencies = schema.dependencies.clone(); - let schema = match schema.name.clone() { - Some(name) => { - let reference = Reference { - reference: format!("#/components/schemas/{}", name) - }; - dependencies.insert(name, schema); - SchemaKind::AllOf { all_of: vec![reference] } - }, - None => schema.schema - }; - - OpenapiSchema { - nullable: true, - name: None, - schema, - dependencies - } - } -} - -impl OpenapiType for Vec { - fn schema() -> OpenapiSchema { - let schema = T::schema(); - let mut dependencies = schema.dependencies.clone(); - - let items = match schema.name.clone() { - Some(name) => { - let reference = Reference { - reference: format!("#/components/schemas/{}", name) - }; - dependencies.insert(name, schema); - reference - }, - None => Item(Box::new(schema.into_schema())) - }; - - OpenapiSchema { - nullable: false, - name: None, - schema: SchemaKind::Type(Type::Array(ArrayType { - items, - min_items: None, - max_items: None, - unique_items: false - })), - dependencies - } - } -} - -impl OpenapiType for BTreeSet { - fn schema() -> OpenapiSchema { - as OpenapiType>::schema() - } -} - -impl OpenapiType for HashSet { - fn schema() -> OpenapiSchema { - as OpenapiType>::schema() - } -} - -impl OpenapiType for HashMap { - fn schema() -> OpenapiSchema { - let key_schema = K::schema(); - let mut dependencies = key_schema.dependencies.clone(); - - let keys = match key_schema.name.clone() { - Some(name) => { - let reference = Reference { - reference: format!("#/components/schemas/{}", name) - }; - dependencies.insert(name, key_schema); - reference - }, - None => Item(Box::new(key_schema.into_schema())) - }; - - let schema = T::schema(); - dependencies.extend(schema.dependencies.iter().map(|(k, v)| (k.clone(), v.clone()))); - - let items = Box::new(match schema.name.clone() { - Some(name) => { - let reference = Reference { - reference: format!("#/components/schemas/{}", name) - }; - dependencies.insert(name, schema); - reference - }, - None => Item(schema.into_schema()) - }); - - let mut properties = IndexMap::new(); - properties.insert("default".to_owned(), keys); - - OpenapiSchema { - nullable: false, - name: None, - schema: SchemaKind::Type(Type::Object(ObjectType { - properties, - required: vec!["default".to_owned()], - additional_properties: Some(AdditionalProperties::Schema(items)), - ..Default::default() - })), - dependencies - } - } -} - -impl OpenapiType for serde_json::Value { - fn schema() -> OpenapiSchema { - OpenapiSchema { - nullable: true, - name: None, - schema: SchemaKind::Any(Default::default()), - dependencies: Default::default() - } - } -} - -#[cfg(test)] -mod test { - use super::*; - use serde_json::Value; - - type Unit = (); - - macro_rules! assert_schema { - ($ty:ident $(<$($generic:ident),+>)* => $json:expr) => { - paste::item! { - #[test] - fn []() - { - let schema = <$ty $(<$($generic),+>)* as OpenapiType>::schema().into_schema(); - let schema_json = serde_json::to_string(&schema).expect(&format!("Unable to serialize schema for {}", stringify!($ty))); - assert_eq!(schema_json, $json); - } - } - }; - } - - assert_schema!(Unit => r#"{"type":"object","additionalProperties":false}"#); - assert_schema!(bool => r#"{"type":"boolean"}"#); - - assert_schema!(isize => r#"{"type":"integer"}"#); - assert_schema!(usize => r#"{"type":"integer","minimum":0}"#); - assert_schema!(i8 => r#"{"type":"integer","format":"int8"}"#); - assert_schema!(u8 => r#"{"type":"integer","format":"int8","minimum":0}"#); - assert_schema!(i16 => r#"{"type":"integer","format":"int16"}"#); - assert_schema!(u16 => r#"{"type":"integer","format":"int16","minimum":0}"#); - assert_schema!(i32 => r#"{"type":"integer","format":"int32"}"#); - assert_schema!(u32 => r#"{"type":"integer","format":"int32","minimum":0}"#); - assert_schema!(i64 => r#"{"type":"integer","format":"int64"}"#); - assert_schema!(u64 => r#"{"type":"integer","format":"int64","minimum":0}"#); - assert_schema!(i128 => r#"{"type":"integer","format":"int128"}"#); - assert_schema!(u128 => r#"{"type":"integer","format":"int128","minimum":0}"#); - - assert_schema!(NonZeroUsize => r#"{"type":"integer","minimum":1}"#); - assert_schema!(NonZeroU8 => r#"{"type":"integer","format":"int8","minimum":1}"#); - assert_schema!(NonZeroU16 => r#"{"type":"integer","format":"int16","minimum":1}"#); - assert_schema!(NonZeroU32 => r#"{"type":"integer","format":"int32","minimum":1}"#); - assert_schema!(NonZeroU64 => r#"{"type":"integer","format":"int64","minimum":1}"#); - assert_schema!(NonZeroU128 => r#"{"type":"integer","format":"int128","minimum":1}"#); - - assert_schema!(f32 => r#"{"type":"number","format":"float"}"#); - assert_schema!(f64 => r#"{"type":"number","format":"double"}"#); - - assert_schema!(String => r#"{"type":"string"}"#); - - #[cfg(feature = "uuid")] - assert_schema!(Uuid => r#"{"type":"string","format":"uuid"}"#); - - #[cfg(feature = "chrono")] - mod chrono { - use super::*; - - assert_schema!(Date => r#"{"type":"string","format":"date"}"#); - assert_schema!(Date => r#"{"type":"string","format":"date"}"#); - assert_schema!(Date => r#"{"type":"string","format":"date"}"#); - assert_schema!(NaiveDate => r#"{"type":"string","format":"date"}"#); - assert_schema!(DateTime => r#"{"type":"string","format":"date-time"}"#); - assert_schema!(DateTime => r#"{"type":"string","format":"date-time"}"#); - assert_schema!(DateTime => r#"{"type":"string","format":"date-time"}"#); - assert_schema!(NaiveDateTime => r#"{"type":"string","format":"date-time"}"#); - } - - assert_schema!(Option => r#"{"nullable":true,"type":"string"}"#); - assert_schema!(Vec => r#"{"type":"array","items":{"type":"string"}}"#); - assert_schema!(BTreeSet => r#"{"type":"array","items":{"type":"string"}}"#); - assert_schema!(HashSet => r#"{"type":"array","items":{"type":"string"}}"#); - assert_schema!(HashMap => r#"{"type":"object","properties":{"default":{"type":"integer","format":"int64"}},"required":["default"],"additionalProperties":{"type":"string"}}"#); - assert_schema!(Value => r#"{"nullable":true}"#); -} diff --git a/src/response/mod.rs b/src/response/mod.rs index b2796dc..bdf7c66 100644 --- a/src/response/mod.rs +++ b/src/response/mod.rs @@ -1,6 +1,3 @@ -#[cfg(feature = "openapi")] -use crate::OpenapiSchema; - use futures_util::future::{self, BoxFuture, FutureExt}; use gotham::{ handler::HandlerError, @@ -10,6 +7,8 @@ use gotham::{ } }; use mime::{Mime, APPLICATION_JSON, STAR_STAR}; +#[cfg(feature = "openapi")] +use openapi_type::OpenapiSchema; use serde::Serialize; use std::{ convert::Infallible, @@ -259,7 +258,7 @@ mod test { use thiserror::Error; #[derive(Debug, Default, Deserialize, Serialize)] - #[cfg_attr(feature = "openapi", derive(crate::OpenapiType))] + #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))] struct Msg { msg: String } diff --git a/src/response/no_content.rs b/src/response/no_content.rs index 73159c1..3a10b3b 100644 --- a/src/response/no_content.rs +++ b/src/response/no_content.rs @@ -1,12 +1,14 @@ use super::{handle_error, IntoResponse}; -use crate::{IntoResponseError, Response}; #[cfg(feature = "openapi")] -use crate::{OpenapiSchema, OpenapiType, ResponseSchema}; +use crate::ResponseSchema; +use crate::{IntoResponseError, Response}; use futures_util::{future, future::FutureExt}; use gotham::hyper::header::{HeaderMap, HeaderValue, IntoHeaderName}; #[cfg(feature = "openapi")] use gotham::hyper::StatusCode; use mime::Mime; +#[cfg(feature = "openapi")] +use openapi_type::{OpenapiSchema, OpenapiType}; use std::{fmt::Display, future::Future, pin::Pin}; /** diff --git a/src/response/raw.rs b/src/response/raw.rs index 6c003dc..3722146 100644 --- a/src/response/raw.rs +++ b/src/response/raw.rs @@ -1,7 +1,9 @@ use super::{handle_error, IntoResponse, IntoResponseError}; use crate::{FromBody, RequestBody, ResourceType, Response}; #[cfg(feature = "openapi")] -use crate::{IntoResponseWithSchema, OpenapiSchema, OpenapiType, ResponseSchema}; +use crate::{IntoResponseWithSchema, ResponseSchema}; +#[cfg(feature = "openapi")] +use openapi_type::{OpenapiSchema, OpenapiType}; use futures_core::future::Future; use futures_util::{future, future::FutureExt}; diff --git a/src/response/redirect.rs b/src/response/redirect.rs index 8b6e854..f1edd82 100644 --- a/src/response/redirect.rs +++ b/src/response/redirect.rs @@ -1,12 +1,14 @@ use super::{handle_error, IntoResponse}; use crate::{IntoResponseError, Response}; #[cfg(feature = "openapi")] -use crate::{NoContent, OpenapiSchema, ResponseSchema}; +use crate::{NoContent, ResponseSchema}; use futures_util::future::{BoxFuture, FutureExt, TryFutureExt}; use gotham::hyper::{ header::{InvalidHeaderValue, LOCATION}, Body, StatusCode }; +#[cfg(feature = "openapi")] +use openapi_type::OpenapiSchema; use std::{ error::Error as StdError, fmt::{Debug, Display} diff --git a/src/response/result.rs b/src/response/result.rs index a28803f..f0ddc91 100644 --- a/src/response/result.rs +++ b/src/response/result.rs @@ -1,7 +1,9 @@ use super::{handle_error, IntoResponse, ResourceError}; #[cfg(feature = "openapi")] -use crate::{OpenapiSchema, ResponseSchema}; +use crate::ResponseSchema; use crate::{Response, ResponseBody, Success}; +#[cfg(feature = "openapi")] +use openapi_type::OpenapiSchema; use futures_core::future::Future; use gotham::hyper::StatusCode; @@ -64,7 +66,7 @@ mod test { use thiserror::Error; #[derive(Debug, Default, Deserialize, Serialize)] - #[cfg_attr(feature = "openapi", derive(crate::OpenapiType))] + #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))] struct Msg { msg: String } diff --git a/src/response/success.rs b/src/response/success.rs index 24d6b3c..31f9374 100644 --- a/src/response/success.rs +++ b/src/response/success.rs @@ -1,6 +1,6 @@ use super::IntoResponse; #[cfg(feature = "openapi")] -use crate::{OpenapiSchema, ResponseSchema}; +use crate::ResponseSchema; use crate::{Response, ResponseBody}; use futures_util::future::{self, FutureExt}; use gotham::hyper::{ @@ -8,6 +8,8 @@ use gotham::hyper::{ StatusCode }; use mime::{Mime, APPLICATION_JSON}; +#[cfg(feature = "openapi")] +use openapi_type::OpenapiSchema; use std::{fmt::Debug, future::Future, pin::Pin}; /** @@ -27,7 +29,7 @@ Usage example: # struct MyResource; # #[derive(Deserialize, Serialize)] -# #[cfg_attr(feature = "openapi", derive(OpenapiType))] +# #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))] struct MyResponse { message: &'static str } @@ -96,7 +98,7 @@ mod test { use gotham::hyper::header::ACCESS_CONTROL_ALLOW_ORIGIN; #[derive(Debug, Default, Serialize)] - #[cfg_attr(feature = "openapi", derive(crate::OpenapiType))] + #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))] struct Msg { msg: String } diff --git a/src/routing.rs b/src/routing.rs index f41dc93..c7cd5a6 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -4,7 +4,6 @@ use crate::openapi::{ router::OpenapiRouter }; use crate::{response::ResourceError, Endpoint, FromBody, IntoResponse, Resource, Response}; - #[cfg(feature = "cors")] use gotham::router::route::matcher::AccessControlRequestMethodMatcher; use gotham::{ @@ -20,10 +19,12 @@ use gotham::{ state::{FromState, State} }; use mime::{Mime, APPLICATION_JSON}; -use std::panic::RefUnwindSafe; +#[cfg(feature = "openapi")] +use openapi_type::OpenapiType; +use std::{any::TypeId, panic::RefUnwindSafe}; /// Allow us to extract an id from a path. -#[derive(Debug, Deserialize, StateData, StaticResponseExtender)] +#[derive(Clone, Copy, Debug, Deserialize, StateData, StaticResponseExtender)] #[cfg_attr(feature = "openapi", derive(OpenapiType))] pub struct PathExtractor { pub id: ID @@ -91,6 +92,11 @@ where { trace!("entering endpoint_handler"); let placeholders = E::Placeholders::take_from(state); + // workaround for E::Placeholders and E::Param being the same type + // when fixed remove `Clone` requirement on endpoint + if TypeId::of::() == TypeId::of::() { + state.put(placeholders.clone()); + } let params = E::Params::take_from(state); let body = match E::needs_body() { diff --git a/src/types.rs b/src/types.rs index ca08bec..20be58d 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,8 +1,7 @@ -#[cfg(feature = "openapi")] -use crate::OpenapiType; - use gotham::hyper::body::Bytes; use mime::{Mime, APPLICATION_JSON}; +#[cfg(feature = "openapi")] +use openapi_type::OpenapiType; use serde::{de::DeserializeOwned, Serialize}; use std::error::Error; diff --git a/tests/async_methods.rs b/tests/async_methods.rs index 21a74b5..9a1669b 100644 --- a/tests/async_methods.rs +++ b/tests/async_methods.rs @@ -9,6 +9,8 @@ use gotham::{ }; use gotham_restful::*; use mime::{APPLICATION_JSON, TEXT_PLAIN}; +#[cfg(feature = "openapi")] +use openapi_type::OpenapiType; use serde::Deserialize; use tokio::time::{sleep, Duration}; @@ -28,7 +30,7 @@ struct FooBody { data: String } -#[derive(Deserialize, StateData, StaticResponseExtender)] +#[derive(Clone, Deserialize, StateData, StaticResponseExtender)] #[cfg_attr(feature = "openapi", derive(OpenapiType))] #[allow(dead_code)] struct FooSearch { diff --git a/tests/sync_methods.rs b/tests/sync_methods.rs index 4e07259..2b440fa 100644 --- a/tests/sync_methods.rs +++ b/tests/sync_methods.rs @@ -4,6 +4,8 @@ extern crate gotham_derive; use gotham::{router::builder::*, test::TestServer}; use gotham_restful::*; use mime::{APPLICATION_JSON, TEXT_PLAIN}; +#[cfg(feature = "openapi")] +use openapi_type::OpenapiType; use serde::Deserialize; mod util { @@ -22,7 +24,7 @@ struct FooBody { data: String } -#[derive(Deserialize, StateData, StaticResponseExtender)] +#[derive(Clone, Deserialize, StateData, StaticResponseExtender)] #[cfg_attr(feature = "openapi", derive(OpenapiType))] #[allow(dead_code)] struct FooSearch { From 3a3f74336935f222e1c2bf0cb4b21f9ea2c77bc3 Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 9 Mar 2021 19:55:04 +0100 Subject: [PATCH 164/170] ci: include openapi_type crate --- .gitlab-ci.yml | 11 ++++++---- .../fail/not_openapitype_generics.stderr | 2 ++ openapi_type/tests/fail/rustfmt.sh | 21 +++++++++++++++++++ 3 files changed, 30 insertions(+), 4 deletions(-) create mode 100755 openapi_type/tests/fail/rustfmt.sh diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b98380d..27a6250 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -14,20 +14,20 @@ check-example: before_script: - cargo -V script: - - cd example - - cargo check + - cargo check --manifest-path example/Cargo.toml cache: key: cargo-stable-example paths: - cargo/ - target/ - + test-default: stage: test image: rust:1.49-slim before_script: - cargo -V script: + - cargo test --manifest-path openapi_type/Cargo.toml -- --skip trybuild - cargo test cache: key: cargo-1-49-default @@ -43,6 +43,7 @@ test-full: - apt install -y --no-install-recommends libpq-dev - cargo -V script: + - cargo test --manifest-path openapi_type/Cargo.toml --all-features -- --skip trybuild - cargo test --no-default-features --features full cache: key: cargo-1-49-all @@ -79,6 +80,7 @@ test-trybuild-ui: - apt install -y --no-install-recommends libpq-dev - cargo -V script: + - cargo test --manifest-path openapi_type/Cargo.toml --all-features -- trybuild - cargo test --no-default-features --features full --tests -- --ignored cache: key: cargo-1-50-all @@ -107,8 +109,9 @@ rustfmt: - cargo -V - cargo fmt --version script: - - cargo fmt -- --check + - cargo fmt --all -- --check - ./tests/ui/rustfmt.sh --check + - ./openapi-type/tests/fail/rustfmt.sh --check doc: stage: build diff --git a/openapi_type/tests/fail/not_openapitype_generics.stderr b/openapi_type/tests/fail/not_openapitype_generics.stderr index d41098b..d33bafe 100644 --- a/openapi_type/tests/fail/not_openapitype_generics.stderr +++ b/openapi_type/tests/fail/not_openapitype_generics.stderr @@ -16,6 +16,8 @@ error[E0599]: no function or associated item named `schema` found for struct `Fo = note: the method `schema` exists but the following trait bounds were not satisfied: `Bar: OpenapiType` which is required by `Foo: OpenapiType` + `Foo: OpenapiType` + which is required by `&Foo: OpenapiType` = help: items from traits can only be used if the trait is implemented and in scope = note: the following trait defines an item `schema`, perhaps you need to implement it: candidate #1: `OpenapiType` diff --git a/openapi_type/tests/fail/rustfmt.sh b/openapi_type/tests/fail/rustfmt.sh new file mode 100755 index 0000000..a93f958 --- /dev/null +++ b/openapi_type/tests/fail/rustfmt.sh @@ -0,0 +1,21 @@ +#!/bin/busybox ash +set -euo pipefail + +rustfmt=${RUSTFMT:-rustfmt} +version="$($rustfmt -V)" +case "$version" in + *nightly*) + # all good, no additional flags required + ;; + *) + # assume we're using some sort of rustup setup + rustfmt="$rustfmt +nightly" + ;; +esac + +return=0 +find "$(dirname "$0")" -name '*.rs' -type f | while read file; do + $rustfmt --config-path "$(dirname "$0")/../../../rustfmt.toml" "$@" "$file" || return=1 +done + +exit $return From 63567f54806cad4fdc18afe34c7d31bd1593b3e6 Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 9 Mar 2021 20:08:12 +0100 Subject: [PATCH 165/170] ci: fix typo --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 27a6250..3db33c0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -111,7 +111,7 @@ rustfmt: script: - cargo fmt --all -- --check - ./tests/ui/rustfmt.sh --check - - ./openapi-type/tests/fail/rustfmt.sh --check + - ./openapi_type/tests/fail/rustfmt.sh --check doc: stage: build From 9c7f681e3dd2d8f530cb9484f2406826d01f0d54 Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 9 Mar 2021 20:51:44 +0100 Subject: [PATCH 166/170] remove outdated tests --- tests/trybuild_ui.rs | 7 ------- tests/ui/openapi_type/enum_with_fields.rs | 12 ------------ tests/ui/openapi_type/enum_with_fields.stderr | 11 ----------- tests/ui/openapi_type/nullable_non_bool.rs | 10 ---------- tests/ui/openapi_type/nullable_non_bool.stderr | 5 ----- tests/ui/openapi_type/rename_non_string.rs | 10 ---------- tests/ui/openapi_type/rename_non_string.stderr | 5 ----- tests/ui/openapi_type/tuple_struct.rs | 7 ------- tests/ui/openapi_type/tuple_struct.stderr | 5 ----- tests/ui/openapi_type/union.rs | 10 ---------- tests/ui/openapi_type/union.stderr | 5 ----- tests/ui/openapi_type/unknown_key.rs | 10 ---------- tests/ui/openapi_type/unknown_key.stderr | 5 ----- 13 files changed, 102 deletions(-) delete mode 100644 tests/ui/openapi_type/enum_with_fields.rs delete mode 100644 tests/ui/openapi_type/enum_with_fields.stderr delete mode 100644 tests/ui/openapi_type/nullable_non_bool.rs delete mode 100644 tests/ui/openapi_type/nullable_non_bool.stderr delete mode 100644 tests/ui/openapi_type/rename_non_string.rs delete mode 100644 tests/ui/openapi_type/rename_non_string.stderr delete mode 100644 tests/ui/openapi_type/tuple_struct.rs delete mode 100644 tests/ui/openapi_type/tuple_struct.stderr delete mode 100644 tests/ui/openapi_type/union.rs delete mode 100644 tests/ui/openapi_type/union.stderr delete mode 100644 tests/ui/openapi_type/unknown_key.rs delete mode 100644 tests/ui/openapi_type/unknown_key.stderr diff --git a/tests/trybuild_ui.rs b/tests/trybuild_ui.rs index 2317215..406ae6a 100644 --- a/tests/trybuild_ui.rs +++ b/tests/trybuild_ui.rs @@ -4,14 +4,7 @@ use trybuild::TestCases; #[ignore] fn trybuild_ui() { let t = TestCases::new(); - - // always enabled t.compile_fail("tests/ui/endpoint/*.rs"); t.compile_fail("tests/ui/from_body/*.rs"); t.compile_fail("tests/ui/resource/*.rs"); - - // require the openapi feature - if cfg!(feature = "openapi") { - t.compile_fail("tests/ui/openapi_type/*.rs"); - } } diff --git a/tests/ui/openapi_type/enum_with_fields.rs b/tests/ui/openapi_type/enum_with_fields.rs deleted file mode 100644 index b07cbfa..0000000 --- a/tests/ui/openapi_type/enum_with_fields.rs +++ /dev/null @@ -1,12 +0,0 @@ -#[macro_use] -extern crate gotham_restful; - -#[derive(OpenapiType)] -enum Food { - Pasta, - Pizza { pineapple: bool }, - Rice, - Other(String) -} - -fn main() {} diff --git a/tests/ui/openapi_type/enum_with_fields.stderr b/tests/ui/openapi_type/enum_with_fields.stderr deleted file mode 100644 index 2925a32..0000000 --- a/tests/ui/openapi_type/enum_with_fields.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: #[derive(OpenapiType)] does not support enum variants with fields - --> $DIR/enum_with_fields.rs:7:2 - | -7 | Pizza { pineapple: bool }, - | ^^^^^ - -error: #[derive(OpenapiType)] does not support enum variants with fields - --> $DIR/enum_with_fields.rs:9:2 - | -9 | Other(String) - | ^^^^^ diff --git a/tests/ui/openapi_type/nullable_non_bool.rs b/tests/ui/openapi_type/nullable_non_bool.rs deleted file mode 100644 index 2431e94..0000000 --- a/tests/ui/openapi_type/nullable_non_bool.rs +++ /dev/null @@ -1,10 +0,0 @@ -#[macro_use] -extern crate gotham_restful; - -#[derive(OpenapiType)] -struct Foo { - #[openapi(nullable = "yes, please")] - bar: String -} - -fn main() {} diff --git a/tests/ui/openapi_type/nullable_non_bool.stderr b/tests/ui/openapi_type/nullable_non_bool.stderr deleted file mode 100644 index 421d9cd..0000000 --- a/tests/ui/openapi_type/nullable_non_bool.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: Expected bool - --> $DIR/nullable_non_bool.rs:6:23 - | -6 | #[openapi(nullable = "yes, please")] - | ^^^^^^^^^^^^^ diff --git a/tests/ui/openapi_type/rename_non_string.rs b/tests/ui/openapi_type/rename_non_string.rs deleted file mode 100644 index 83f8bd6..0000000 --- a/tests/ui/openapi_type/rename_non_string.rs +++ /dev/null @@ -1,10 +0,0 @@ -#[macro_use] -extern crate gotham_restful; - -#[derive(OpenapiType)] -struct Foo { - #[openapi(rename = 42)] - bar: String -} - -fn main() {} diff --git a/tests/ui/openapi_type/rename_non_string.stderr b/tests/ui/openapi_type/rename_non_string.stderr deleted file mode 100644 index 0446b21..0000000 --- a/tests/ui/openapi_type/rename_non_string.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: Expected string literal - --> $DIR/rename_non_string.rs:6:21 - | -6 | #[openapi(rename = 42)] - | ^^ diff --git a/tests/ui/openapi_type/tuple_struct.rs b/tests/ui/openapi_type/tuple_struct.rs deleted file mode 100644 index 7def578..0000000 --- a/tests/ui/openapi_type/tuple_struct.rs +++ /dev/null @@ -1,7 +0,0 @@ -#[macro_use] -extern crate gotham_restful; - -#[derive(OpenapiType)] -struct Foo(String); - -fn main() {} diff --git a/tests/ui/openapi_type/tuple_struct.stderr b/tests/ui/openapi_type/tuple_struct.stderr deleted file mode 100644 index 62a81c1..0000000 --- a/tests/ui/openapi_type/tuple_struct.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: #[derive(OpenapiType)] does not support unnamed fields - --> $DIR/tuple_struct.rs:5:11 - | -5 | struct Foo(String); - | ^^^^^^^^ diff --git a/tests/ui/openapi_type/union.rs b/tests/ui/openapi_type/union.rs deleted file mode 100644 index 99efd49..0000000 --- a/tests/ui/openapi_type/union.rs +++ /dev/null @@ -1,10 +0,0 @@ -#[macro_use] -extern crate gotham_restful; - -#[derive(OpenapiType)] -union IntOrPointer { - int: u64, - pointer: *mut String -} - -fn main() {} diff --git a/tests/ui/openapi_type/union.stderr b/tests/ui/openapi_type/union.stderr deleted file mode 100644 index 2dbe3b6..0000000 --- a/tests/ui/openapi_type/union.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: #[derive(OpenapiType)] only works for structs and enums - --> $DIR/union.rs:5:1 - | -5 | union IntOrPointer { - | ^^^^^ diff --git a/tests/ui/openapi_type/unknown_key.rs b/tests/ui/openapi_type/unknown_key.rs deleted file mode 100644 index daab52a..0000000 --- a/tests/ui/openapi_type/unknown_key.rs +++ /dev/null @@ -1,10 +0,0 @@ -#[macro_use] -extern crate gotham_restful; - -#[derive(OpenapiType)] -struct Foo { - #[openapi(like = "pizza")] - bar: String -} - -fn main() {} diff --git a/tests/ui/openapi_type/unknown_key.stderr b/tests/ui/openapi_type/unknown_key.stderr deleted file mode 100644 index b5e9ac1..0000000 --- a/tests/ui/openapi_type/unknown_key.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: Unknown key - --> $DIR/unknown_key.rs:6:12 - | -6 | #[openapi(like = "pizza")] - | ^^^^ From a5257608e38a7f97a8694de7650f1538eac88479 Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 9 Mar 2021 21:00:35 +0100 Subject: [PATCH 167/170] update readme --- README.md | 29 +++++++++++++++++++---------- README.tpl | 29 +++++++++++++++++++---------- 2 files changed, 38 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 1cb82c1..ecf0ae8 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,11 @@ -
-

gotham-restful

-
-
+
+
pipeline status coverage report - - crates.io - - - docs.rs - rustdoc @@ -26,6 +18,23 @@

+This repository contains the following crates: + + - **gotham_restful** + [![gotham_restful on crates.io](https://img.shields.io/crates/v/gotham_restful.svg)](https://crates.io/crates/gotham_restful) + [![gotham_restful on docs.rs](https://docs.rs/gotham_restful/badge.svg)](https://docs.rs/gotham_restful) + - **gotham_restful_derive** + [![gotham_restful_derive on crates.io](https://img.shields.io/crates/v/gotham_restful_derive.svg)](https://crates.io/crates/gotham_restful_derive) + [![gotham_restful_derive on docs.rs](https://docs.rs/gotham_restful_derive/badge.svg)](https://docs.rs/gotham_restful_derive) + - **openapi_type** + [![openapi_type on crates.io](https://img.shields.io/crates/v/openapi_type.svg)](https://crates.io/crates/openapi_type) + [![openapi_type on docs.rs](https://docs.rs/openapi_type/badge.svg)](https://docs.rs/crate/openapi_type) + - **openapi_type_derive** + [![openapi_type_derive on crates.io](https://img.shields.io/crates/v/openapi_type_derive.svg)](https://crates.io/crates/openapi_type_derive) + [![openapi_type_derive on docs.rs](https://docs.rs/openapi_type_derive/badge.svg)](https://docs.rs/crate/openapi_type_derive) + +# gotham-restful + This crate is an extension to the popular [gotham web framework][gotham] for Rust. It allows you to create resources with assigned endpoints that aim to be a more convenient way of creating handlers for requests. diff --git a/README.tpl b/README.tpl index bd3e252..1769315 100644 --- a/README.tpl +++ b/README.tpl @@ -1,19 +1,11 @@ -
-

gotham-restful

-
-
+
+
pipeline status coverage report - - crates.io - - - docs.rs - rustdoc @@ -26,6 +18,23 @@

+This repository contains the following crates: + + - **gotham_restful** + [![gotham_restful on crates.io](https://img.shields.io/crates/v/gotham_restful.svg)](https://crates.io/crates/gotham_restful) + [![gotham_restful on docs.rs](https://docs.rs/gotham_restful/badge.svg)](https://docs.rs/gotham_restful) + - **gotham_restful_derive** + [![gotham_restful_derive on crates.io](https://img.shields.io/crates/v/gotham_restful_derive.svg)](https://crates.io/crates/gotham_restful_derive) + [![gotham_restful_derive on docs.rs](https://docs.rs/gotham_restful_derive/badge.svg)](https://docs.rs/gotham_restful_derive) + - **openapi_type** + [![openapi_type on crates.io](https://img.shields.io/crates/v/openapi_type.svg)](https://crates.io/crates/openapi_type) + [![openapi_type on docs.rs](https://docs.rs/openapi_type/badge.svg)](https://docs.rs/crate/openapi_type) + - **openapi_type_derive** + [![openapi_type_derive on crates.io](https://img.shields.io/crates/v/openapi_type_derive.svg)](https://crates.io/crates/openapi_type_derive) + [![openapi_type_derive on docs.rs](https://docs.rs/openapi_type_derive/badge.svg)](https://docs.rs/crate/openapi_type_derive) + +# gotham-restful + {{readme}} ## Versioning From e206ab10eb1bb4eacfe418fd166cfcb55f7dd3b4 Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 9 Mar 2021 21:23:44 +0100 Subject: [PATCH 168/170] update trybuild tests --- tests/ui/endpoint/invalid_params_ty.stderr | 25 +++++++++++++------ .../endpoint/invalid_placeholders_ty.stderr | 25 +++++++++++++------ 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/tests/ui/endpoint/invalid_params_ty.stderr b/tests/ui/endpoint/invalid_params_ty.stderr index 35ed700..de1597a 100644 --- a/tests/ui/endpoint/invalid_params_ty.stderr +++ b/tests/ui/endpoint/invalid_params_ty.stderr @@ -1,3 +1,14 @@ +error[E0277]: the trait bound `FooParams: OpenapiType` is not satisfied + --> $DIR/invalid_params_ty.rs:15:16 + | +15 | fn endpoint(_: FooParams) { + | ^^^^^^^^^ the trait `OpenapiType` is not implemented for `FooParams` + | + ::: $WORKSPACE/src/endpoint.rs + | + | #[openapi_bound("Params: OpenapiType")] + | --------------------- required by this bound in `gotham_restful::EndpointWithSchema::Params` + error[E0277]: the trait bound `for<'de> FooParams: serde::de::Deserialize<'de>` is not satisfied --> $DIR/invalid_params_ty.rs:15:16 | @@ -6,7 +17,7 @@ error[E0277]: the trait bound `for<'de> FooParams: serde::de::Deserialize<'de>` | ::: $WORKSPACE/src/endpoint.rs | - | type Params: QueryStringExtractor + Sync; + | type Params: QueryStringExtractor + Clone + Sync; | -------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Params` error[E0277]: the trait bound `FooParams: StateData` is not satisfied @@ -17,7 +28,7 @@ error[E0277]: the trait bound `FooParams: StateData` is not satisfied | ::: $WORKSPACE/src/endpoint.rs | - | type Params: QueryStringExtractor + Sync; + | type Params: QueryStringExtractor + Clone + Sync; | -------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Params` error[E0277]: the trait bound `FooParams: StaticResponseExtender` is not satisfied @@ -28,16 +39,16 @@ error[E0277]: the trait bound `FooParams: StaticResponseExtender` is not satisfi | ::: $WORKSPACE/src/endpoint.rs | - | type Params: QueryStringExtractor + Sync; + | type Params: QueryStringExtractor + Clone + Sync; | -------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Params` -error[E0277]: the trait bound `FooParams: OpenapiType` is not satisfied +error[E0277]: the trait bound `FooParams: Clone` is not satisfied --> $DIR/invalid_params_ty.rs:15:16 | 15 | fn endpoint(_: FooParams) { - | ^^^^^^^^^ the trait `OpenapiType` is not implemented for `FooParams` + | ^^^^^^^^^ the trait `Clone` is not implemented for `FooParams` | ::: $WORKSPACE/src/endpoint.rs | - | #[openapi_bound("Params: crate::OpenapiType")] - | ---------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Params` + | type Params: QueryStringExtractor + Clone + Sync; + | ----- required by this bound in `gotham_restful::EndpointWithSchema::Params` diff --git a/tests/ui/endpoint/invalid_placeholders_ty.stderr b/tests/ui/endpoint/invalid_placeholders_ty.stderr index 09c9bbb..58c8014 100644 --- a/tests/ui/endpoint/invalid_placeholders_ty.stderr +++ b/tests/ui/endpoint/invalid_placeholders_ty.stderr @@ -1,3 +1,14 @@ +error[E0277]: the trait bound `FooPlaceholders: OpenapiType` is not satisfied + --> $DIR/invalid_placeholders_ty.rs:15:16 + | +15 | fn endpoint(_: FooPlaceholders) { + | ^^^^^^^^^^^^^^^ the trait `OpenapiType` is not implemented for `FooPlaceholders` + | + ::: $WORKSPACE/src/endpoint.rs + | + | #[openapi_bound("Placeholders: OpenapiType")] + | --------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Placeholders` + error[E0277]: the trait bound `for<'de> FooPlaceholders: serde::de::Deserialize<'de>` is not satisfied --> $DIR/invalid_placeholders_ty.rs:15:16 | @@ -6,7 +17,7 @@ error[E0277]: the trait bound `for<'de> FooPlaceholders: serde::de::Deserialize< | ::: $WORKSPACE/src/endpoint.rs | - | type Placeholders: PathExtractor + Sync; + | type Placeholders: PathExtractor + Clone + Sync; | ------------------- required by this bound in `gotham_restful::EndpointWithSchema::Placeholders` error[E0277]: the trait bound `FooPlaceholders: StateData` is not satisfied @@ -17,7 +28,7 @@ error[E0277]: the trait bound `FooPlaceholders: StateData` is not satisfied | ::: $WORKSPACE/src/endpoint.rs | - | type Placeholders: PathExtractor + Sync; + | type Placeholders: PathExtractor + Clone + Sync; | ------------------- required by this bound in `gotham_restful::EndpointWithSchema::Placeholders` error[E0277]: the trait bound `FooPlaceholders: StaticResponseExtender` is not satisfied @@ -28,16 +39,16 @@ error[E0277]: the trait bound `FooPlaceholders: StaticResponseExtender` is not s | ::: $WORKSPACE/src/endpoint.rs | - | type Placeholders: PathExtractor + Sync; + | type Placeholders: PathExtractor + Clone + Sync; | ------------------- required by this bound in `gotham_restful::EndpointWithSchema::Placeholders` -error[E0277]: the trait bound `FooPlaceholders: OpenapiType` is not satisfied +error[E0277]: the trait bound `FooPlaceholders: Clone` is not satisfied --> $DIR/invalid_placeholders_ty.rs:15:16 | 15 | fn endpoint(_: FooPlaceholders) { - | ^^^^^^^^^^^^^^^ the trait `OpenapiType` is not implemented for `FooPlaceholders` + | ^^^^^^^^^^^^^^^ the trait `Clone` is not implemented for `FooPlaceholders` | ::: $WORKSPACE/src/endpoint.rs | - | #[openapi_bound("Placeholders: crate::OpenapiType")] - | ---------------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Placeholders` + | type Placeholders: PathExtractor + Clone + Sync; + | ----- required by this bound in `gotham_restful::EndpointWithSchema::Placeholders` From 2dd3f3e21a6f0cd15ffd74c5142935c7ea54947f Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 9 Mar 2021 22:35:22 +0100 Subject: [PATCH 169/170] fix broken doc link --- openapi_type_derive/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openapi_type_derive/src/lib.rs b/openapi_type_derive/src/lib.rs index 10e6c0a..0a81bec 100644 --- a/openapi_type_derive/src/lib.rs +++ b/openapi_type_derive/src/lib.rs @@ -16,7 +16,7 @@ mod codegen; mod parser; use parser::*; -/// The derive macro for [OpenapiType][openapi_type::OpenapiType]. +/// The derive macro for [OpenapiType](https://docs.rs/openapi_type/*/openapi_type/trait.OpenapiType.html). #[proc_macro_derive(OpenapiType, attributes(openapi))] pub fn derive_openapi_type(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input); From 7bac379e057c26daf7b7a266d72f78a37109af2b Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 10 Mar 2021 19:03:25 +0100 Subject: [PATCH 170/170] update readme: moved to github [skip ci] --- README.md | 408 +----------------------------------------------------- 1 file changed, 2 insertions(+), 406 deletions(-) diff --git a/README.md b/README.md index ecf0ae8..9c1e534 100644 --- a/README.md +++ b/README.md @@ -1,408 +1,4 @@ -
- -
+# Moved to GitHub -This repository contains the following crates: +This project has moved to GitHub: https://github.com/msrd0/gotham_restful - - **gotham_restful** - [![gotham_restful on crates.io](https://img.shields.io/crates/v/gotham_restful.svg)](https://crates.io/crates/gotham_restful) - [![gotham_restful on docs.rs](https://docs.rs/gotham_restful/badge.svg)](https://docs.rs/gotham_restful) - - **gotham_restful_derive** - [![gotham_restful_derive on crates.io](https://img.shields.io/crates/v/gotham_restful_derive.svg)](https://crates.io/crates/gotham_restful_derive) - [![gotham_restful_derive on docs.rs](https://docs.rs/gotham_restful_derive/badge.svg)](https://docs.rs/gotham_restful_derive) - - **openapi_type** - [![openapi_type on crates.io](https://img.shields.io/crates/v/openapi_type.svg)](https://crates.io/crates/openapi_type) - [![openapi_type on docs.rs](https://docs.rs/openapi_type/badge.svg)](https://docs.rs/crate/openapi_type) - - **openapi_type_derive** - [![openapi_type_derive on crates.io](https://img.shields.io/crates/v/openapi_type_derive.svg)](https://crates.io/crates/openapi_type_derive) - [![openapi_type_derive on docs.rs](https://docs.rs/openapi_type_derive/badge.svg)](https://docs.rs/crate/openapi_type_derive) - -# gotham-restful - -This crate is an extension to the popular [gotham web framework][gotham] for Rust. It allows you to -create resources with assigned endpoints that aim to be a more convenient way of creating handlers -for requests. - -## Features - - - Automatically parse **JSON** request and produce response bodies - - Allow using **raw** request and response bodies - - Convenient **macros** to create responses that can be registered with gotham's router - - Auto-Generate an **OpenAPI** specification for your API - - Manage **CORS** headers so you don't have to - - Manage **Authentication** with JWT - - Integrate diesel connection pools for easy **database** integration - -## Safety - -This crate is just as safe as you'd expect from anything written in safe Rust - and -`#![forbid(unsafe_code)]` ensures that no unsafe was used. - -## Endpoints - -There are a set of pre-defined endpoints that should cover the majority of REST APIs. However, -it is also possible to define your own endpoints. - -### Pre-defined Endpoints - -Assuming you assign `/foobar` to your resource, the following pre-defined endpoints exist: - -| Endpoint Name | Required Arguments | HTTP Verb | HTTP Path | -| ------------- | ------------------ | --------- | -------------- | -| read_all | | GET | /foobar | -| read | id | GET | /foobar/:id | -| search | query | GET | /foobar/search | -| create | body | POST | /foobar | -| change_all | body | PUT | /foobar | -| change | id, body | PUT | /foobar/:id | -| remove_all | | DELETE | /foobar | -| remove | id | DELETE | /foobar/:id | - -Each of those endpoints has a macro that creates the neccessary boilerplate for the Resource. A -simple example looks like this: - -```rust -/// Our RESTful resource. -#[derive(Resource)] -#[resource(read)] -struct FooResource; - -/// The return type of the foo read endpoint. -#[derive(Serialize)] -struct Foo { - id: u64 -} - -/// The foo read endpoint. -#[read] -fn read(id: u64) -> Success { - Foo { id }.into() -} -``` - -### Custom Endpoints - -Defining custom endpoints is done with the `#[endpoint]` macro. The syntax is similar to that -of the pre-defined endpoints, but you need to give it more context: - -```rust -use gotham_restful::gotham::hyper::Method; - -#[derive(Resource)] -#[resource(custom_endpoint)] -struct CustomResource; - -/// This type is used to parse path parameters. -#[derive(Clone, Deserialize, StateData, StaticResponseExtender)] -struct CustomPath { - name: String -} - -#[endpoint(uri = "custom/:name/read", method = "Method::GET", params = false, body = false)] -fn custom_endpoint(path: CustomPath) -> Success { - path.name.into() -} -``` - -## Arguments - -Some endpoints require arguments. Those should be - * **id** Should be a deserializable json-primitive like [`i64`] or [`String`]. - * **body** Should be any deserializable object, or any type implementing [`RequestBody`]. - * **query** Should be any deserializable object whose variables are json-primitives. It will - however not be parsed from json, but from HTTP GET parameters like in `search?id=1`. The - type needs to implement [`QueryStringExtractor`](gotham::extractor::QueryStringExtractor). - -Additionally, all handlers may take a reference to gotham's [`State`]. Please note that for async -handlers, it needs to be a mutable reference until rustc's lifetime checks across await bounds -improve. - -## Uploads and Downloads - -By default, every request body is parsed from json, and every respone is converted to json using -[serde_json]. However, you may also use raw bodies. This is an example where the request body -is simply returned as the response again, no json parsing involved: - -```rust -#[derive(Resource)] -#[resource(create)] -struct ImageResource; - -#[derive(FromBody, RequestBody)] -#[supported_types(mime::IMAGE_GIF, mime::IMAGE_JPEG, mime::IMAGE_PNG)] -struct RawImage { - content: Vec, - content_type: Mime -} - -#[create] -fn create(body : RawImage) -> Raw> { - Raw::new(body.content, body.content_type) -} -``` - -## Custom HTTP Headers - -You can read request headers from the state as you would in any other gotham handler, and specify -custom response headers using [Response::header]. - -```rust -#[derive(Resource)] -#[resource(read_all)] -struct FooResource; - -#[read_all] -async fn read_all(state: &mut State) -> NoContent { - let headers: &HeaderMap = state.borrow(); - let accept = &headers[ACCEPT]; - - let mut res = NoContent::default(); - res.header(VARY, "accept".parse().unwrap()); - res -} -``` - -## Features - -To make life easier for common use-cases, this create offers a few features that might be helpful -when you implement your web server. The complete feature list is - - [`auth`](#authentication-feature) Advanced JWT middleware - - `chrono` openapi support for chrono types - - `full` enables all features except `without-openapi` - - [`cors`](#cors-feature) CORS handling for all endpoint handlers - - [`database`](#database-feature) diesel middleware support - - `errorlog` log errors returned from endpoint handlers - - [`openapi`](#openapi-feature) router additions to generate an openapi spec - - `uuid` openapi support for uuid - - `without-openapi` (**default**) disables `openapi` support. - -### Authentication Feature - -In order to enable authentication support, enable the `auth` feature gate. This allows you to -register a middleware that can automatically check for the existence of an JWT authentication -token. Besides being supported by the endpoint macros, it supports to lookup the required JWT secret -with the JWT data, hence you can use several JWT secrets and decide on the fly which secret to use. -None of this is currently supported by gotham's own JWT middleware. - -A simple example that uses only a single secret looks like this: - -```rust -#[derive(Resource)] -#[resource(read)] -struct SecretResource; - -#[derive(Serialize)] -struct Secret { - id: u64, - intended_for: String -} - -#[derive(Deserialize, Clone)] -struct AuthData { - sub: String, - exp: u64 -} - -#[read] -fn read(auth: AuthStatus, id: u64) -> AuthSuccess { - let intended_for = auth.ok()?.sub; - Ok(Secret { id, intended_for }) -} - -fn main() { - let auth: AuthMiddleware = AuthMiddleware::new( - AuthSource::AuthorizationHeader, - AuthValidation::default(), - StaticAuthHandler::from_array(b"zlBsA2QXnkmpe0QTh8uCvtAEa4j33YAc") - ); - let (chain, pipelines) = single_pipeline(new_pipeline().add(auth).build()); - gotham::start("127.0.0.1:8080", build_router(chain, pipelines, |route| { - route.resource::("secret"); - })); -} -``` - -### CORS Feature - -The cors feature allows an easy usage of this web server from other origins. By default, only -the `Access-Control-Allow-Methods` header is touched. To change the behaviour, add your desired -configuration as a middleware. - -A simple example that allows authentication from every origin (note that `*` always disallows -authentication), and every content type, looks like this: - -```rust -#[derive(Resource)] -#[resource(read_all)] -struct FooResource; - -#[read_all] -fn read_all() { - // your handler -} - -fn main() { - let cors = CorsConfig { - origin: Origin::Copy, - headers: Headers::List(vec![CONTENT_TYPE]), - max_age: 0, - credentials: true - }; - let (chain, pipelines) = single_pipeline(new_pipeline().add(cors).build()); - gotham::start("127.0.0.1:8080", build_router(chain, pipelines, |route| { - route.resource::("foo"); - })); -} -``` - -The cors feature can also be used for non-resource handlers. Take a look at [`CorsRoute`] -for an example. - -### Database Feature - -The database feature allows an easy integration of [diesel] into your handler functions. Please -note however that due to the way gotham's diesel middleware implementation, it is not possible -to run async code while holding a database connection. If you need to combine async and database, -you'll need to borrow the connection from the [`State`] yourself and return a boxed future. - -A simple non-async example looks like this: - -```rust -#[derive(Resource)] -#[resource(read_all)] -struct FooResource; - -#[derive(Queryable, Serialize)] -struct Foo { - id: i64, - value: String -} - -#[read_all] -fn read_all(conn: &PgConnection) -> QueryResult> { - foo::table.load(conn) -} - -type Repo = gotham_middleware_diesel::Repo; - -fn main() { - let repo = Repo::new(&env::var("DATABASE_URL").unwrap()); - let diesel = DieselMiddleware::new(repo); - - let (chain, pipelines) = single_pipeline(new_pipeline().add(diesel).build()); - gotham::start("127.0.0.1:8080", build_router(chain, pipelines, |route| { - route.resource::("foo"); - })); -} -``` - -### OpenAPI Feature - -The OpenAPI feature is probably the most powerful one of this crate. Definitely read this section -carefully both as a binary as well as a library author to avoid unwanted suprises. - -In order to automatically create an openapi specification, gotham-restful needs knowledge over -all routes and the types returned. `serde` does a great job at serialization but doesn't give -enough type information, so all types used in the router need to implement -`OpenapiType`[openapi_type::OpenapiType]. This can be derived for almoust any type and there -should be no need to implement it manually. A simple example looks like this: - -```rust -#[derive(Resource)] -#[resource(read_all)] -struct FooResource; - -#[derive(OpenapiType, Serialize)] -struct Foo { - bar: String -} - -#[read_all] -fn read_all() -> Success { - Foo { bar: "Hello World".to_owned() }.into() -} - -fn main() { - gotham::start("127.0.0.1:8080", build_simple_router(|route| { - let info = OpenapiInfo { - title: "My Foo API".to_owned(), - version: "0.1.0".to_owned(), - urls: vec!["https://example.org/foo/api/v1".to_owned()] - }; - route.with_openapi(info, |mut route| { - route.resource::("foo"); - route.get_openapi("openapi"); - }); - })); -} -``` - -Above example adds the resource as before, but adds another endpoint that we specified as `/openapi`. -It will return the generated openapi specification in JSON format. This allows you to easily write -clients in different languages without worying to exactly replicate your api in each of those -languages. - -However, please note that by default, the `without-openapi` feature of this crate is enabled. -Disabling it in favour of the `openapi` feature will add an additional type bound, -[`OpenapiType`][openapi_type::OpenapiType], on some of the types in [`Endpoint`] and related -traits. This means that some code might only compile on either feature, but not on both. If you -are writing a library that uses gotham-restful, it is strongly recommended to pass both features -through and conditionally enable the openapi code, like this: - -```rust -#[derive(Deserialize, Serialize)] -#[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))] -struct Foo; -``` - -## Examples - -This readme and the crate documentation contain some of example. In addition to that, there is -a collection of code in the [example] directory that might help you. Any help writing more -examples is highly appreciated. - - - [diesel]: https://diesel.rs/ - [example]: https://gitlab.com/msrd0/gotham-restful/tree/master/example - [gotham]: https://gotham.rs/ - [serde_json]: https://github.com/serde-rs/json#serde-json---- - [`State`]: gotham::state::State - -## Versioning - -Like all rust crates, this crate will follow semantic versioning guidelines. However, changing -the MSRV (minimum supported rust version) is not considered a breaking change. - -## License - -Copyright (C) 2020-2021 Dominic Meiser and [contributors](https://gitlab.com/msrd0/gotham-restful/-/graphs/master). - -``` -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -```