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