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()