Crate gotham_restful

source ·
Expand description

This crate is an extension to the popular gotham web framework 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 NameRequired ArgumentsHTTP VerbHTTP Path
read_allGET/foobar
readidGET/foobar/:id
searchqueryGET/foobar/search
createbodyPOST/foobar
update_allbodyPUT/foobar
updateid, bodyPUT/foobar/:id
delete_allDELETE/foobar
deleteidDELETE/foobar/:id

Each of those endpoints has a macro that creates the neccessary boilerplate for the Resource. A simple example looks like this:

/// 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> {
	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:

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<String> {
	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.

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:

#[derive(Resource)]
#[resource(create)]
struct ImageResource;

#[derive(FromBody, RequestBody)]
#[supported_types(mime::IMAGE_GIF, mime::IMAGE_JPEG, mime::IMAGE_PNG)]
struct RawImage {
	content: Vec<u8>,
	content_type: Mime
}

#[create]
fn create(body: RawImage) -> Raw<Vec<u8>> {
	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.

#[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 Advanced JWT middleware
  • cors CORS handling for all endpoint handlers
  • database diesel middleware support
  • errorlog log errors returned from endpoint handlers
  • full enables all features except without-openapi
  • openapi router additions to generate an openapi spec
  • 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:

#[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<AuthData>, id: u64) -> AuthSuccess<Secret> {
	let intended_for = auth.ok()?.sub;
	Ok(Secret { id, intended_for })
}

fn main() {
	let auth: AuthMiddleware<AuthData, _> = 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::<SecretResource>("secret");
		})
	)
	.expect("Failed to start gotham");
}

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:

#[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::<FooResource>("foo");
		})
	)
	.expect("Failed to start gotham");
}

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:

#[derive(Resource)]
#[resource(read_all)]
struct FooResource;

#[derive(Queryable, Serialize)]
struct Foo {
	id: i64,
	value: String
}

#[read_all]
fn read_all(conn: &mut PgConnection) -> QueryResult<Vec<Foo>> {
	foo::table.load(conn)
}

type Repo = gotham_middleware_diesel::Repo<PgConnection>;

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::<FooResource>("foo");
		})
	)
	.expect("Failed to start gotham");
}

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 looks like this:

#[derive(Resource)]
#[resource(read_all)]
struct FooResource;

#[derive(OpenapiType, Serialize)]
struct Foo {
	bar: String
}

#[read_all]
fn read_all() -> Success<Foo> {
	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::<FooResource>("foo");
				route.openapi_spec("openapi");
				route.openapi_doc("/");
			});
		})
	)
	.expect("Failed to start gotham");
}

Above example adds the resource as before, but adds two other endpoints as well: /openapi and /. The first one will return the generated openapi specification in JSON format, allowing you to easily generate clients in different languages without worying to exactly replicate your api in each of those languages. The second one will return documentation in HTML format, so you can easily view your api and share it with other people.

Gotchas

The openapi feature has some gotchas you should be aware of.

  • The name of a struct is used as a “link” in the openapi specification. Therefore, if you have two structs with the same name in your project, the openapi specification will be invalid as only one of the two will make it into the spec.

  • By default, the without-openapi feature of this crate is enabled. Disabling it in favour of the openapi feature will add additional type bounds and method requirements to some of the traits and types in this crate, for example instead of Endpoint you now have to implement EndpointWithSchema. 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:

    #[derive(Deserialize, Serialize)]
    #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))]
    struct Foo;

Re-exports

pub use gotham;
pub use cors::handle_cors;
pub use cors::CorsConfig;
pub use cors::CorsRoute;

Modules

Structs

This is an error type that always yields a 403 Forbidden response. This type is best used in combination with AuthSuccess or AuthResult.
This is the auth middleware. To use it, first make sure you have the auth feature enabled. Then simply add it to your pipeline and request it inside your handler:
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:
A no-op extractor that can be used as a default type for Endpoint::Placeholders and Endpoint::Params.
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:
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.
A response, used to create the final gotham response from.
An AuthHandler returning always the same secret. See AuthMiddleware for a usage example.
This can be returned from a resource when there is no cause of an error.

Enums

This is an error type that either yields a 403 Forbidden response if produced from an authentication error, or delegates to another error type. This type is best used with AuthResult.
The source of the authentication token in the request.
The authentication status returned by the auth middleware for each request.

Traits

This trait will help the auth middleware to determine the validity of an authentication token.
This trait allows to draw routes within an resource. Use this only inside the Resource::setup method.
This trait allows to draw routes within an resource. Use this only inside the Resource::setup method.
This trait adds the resource method to gotham’s routing. It allows you to register any RESTful Resource with a path.
This trait adds the resource method to gotham’s routing. It allows you to register any RESTful Resource with a path.
This trait should be implemented for every type that can be built from an HTTP request body plus its media type.
This trait adds the openapi_spec and openapi_doc method to an OpenAPI-aware router.
This trait needs to be implemented by every type returned from an endpoint to to provide the response.
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.
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.
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.
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.
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.
Additional details for IntoResponse to be used with an OpenAPI-aware router.
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.

Type Definitions

This return type can be used to wrap any type implementing IntoResponse 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 that can only be returned if the client is authenticated. Otherwise, an empty 403 Forbidden response will be issued.

Attribute Macros

Derive Macros