From cc86d3396c1437e30c31eda862c6c0396bdb0bf7 Mon Sep 17 00:00:00 2001 From: Dominic Date: Mon, 4 May 2020 20:45:46 +0200 Subject: [PATCH 1/2] 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 2/2] 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()