From 097822e441ab4d5939ec5faf99daba66a063c79c Mon Sep 17 00:00:00 2001 From: AngelicosPhosphoros Date: Wed, 10 Nov 2021 16:23:51 +0300 Subject: [PATCH] Add example of middleware with async-await usage (#577) --- Cargo.toml | 1 + .../middleware/introduction_await/Cargo.toml | 9 + .../middleware/introduction_await/README.md | 53 ++++++ .../middleware/introduction_await/src/main.rs | 173 ++++++++++++++++++ 4 files changed, 236 insertions(+) create mode 100644 examples/middleware/introduction_await/Cargo.toml create mode 100644 examples/middleware/introduction_await/README.md create mode 100644 examples/middleware/introduction_await/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index 97abdc3a..49df805e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ members = [ # middleware "examples/middleware/introduction", + "examples/middleware/introduction_await", "examples/middleware/multiple_pipelines", # into_response diff --git a/examples/middleware/introduction_await/Cargo.toml b/examples/middleware/introduction_await/Cargo.toml new file mode 100644 index 00000000..14f0ff44 --- /dev/null +++ b/examples/middleware/introduction_await/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "gotham_examples_middleware_introduction_await" +description = "Introduces the Middleware and Pipeline concepts provided by the Gotham web framework with async-await" +version = "0.0.0" +publish = false +edition = "2018" + +[dependencies] +gotham = { path = "../../../gotham" } diff --git a/examples/middleware/introduction_await/README.md b/examples/middleware/introduction_await/README.md new file mode 100644 index 00000000..b43ea8e2 --- /dev/null +++ b/examples/middleware/introduction_await/README.md @@ -0,0 +1,53 @@ +# Middleware and Pipelines introduction + +Introduces the `Middleware` and `Pipelines` concepts provided by the +Gotham web framework. Shows how to use async-await syntax in middlewares. + +## Running + +From the `examples/middleware/introduction_await` directory: + +``` +Terminal 1: + $ cargo run + Compiling gotham_examples_middleware_introduction_await v0.0.0 (file://.../gotham/examples/middleware/introduction_await) + Finished dev [unoptimized + debuginfo] target(s) in 3.57s + Running `.../gotham/target/debug/gotham_examples_middleware_introduction_await` + Listening for requests at http://127.0.0.1:7878 + +Terminal 2: + $ curl -v http://localhost:7878/ + * Trying 127.0.0.1... + * TCP_NODELAY set + * Connected to localhost (127.0.0.1) port 7878 (#0) + > GET / HTTP/1.1 + > Host: localhost:7878 + > User-Agent: curl/7.54.1 + > Accept: */* + > + < HTTP/1.1 200 OK + < Content-Length: 0 + < X-Request-ID: 22d760ad-a7d2-4cf1-accf-fb192230d0ea + < X-Frame-Options: DENY + < X-XSS-Protection: 1; mode=block + < X-Content-Type-Options: nosniff + < X-User-Agent: Supplied: curl/7.54.1, Supported: true + < X-Runtime-Microseconds: 147 + < Date: Mon, 19 Feb 2018 10:29:41 GMT + < + * Connection #0 to host localhost left intact +``` + +## License + +Licensed under your option of: + +* [MIT License](../../LICENSE-MIT) +* [Apache License, Version 2.0](../../LICENSE-APACHE) + +## Community + +The following policies guide participation in our project and our community: + +* [Code of conduct](../../CODE_OF_CONDUCT.md) +* [Contributing](../../CONTRIBUTING.md) diff --git a/examples/middleware/introduction_await/src/main.rs b/examples/middleware/introduction_await/src/main.rs new file mode 100644 index 00000000..b7f9838b --- /dev/null +++ b/examples/middleware/introduction_await/src/main.rs @@ -0,0 +1,173 @@ +//! Introduces the Middleware and Pipeline concepts provided by the Gotham web framework. + +use std::pin::Pin; + +use gotham::handler::HandlerFuture; +use gotham::helpers::http::response::create_empty_response; +use gotham::hyper::header::{HeaderMap, USER_AGENT}; +use gotham::hyper::{Body, Response, StatusCode}; +use gotham::middleware::{Middleware, NewMiddleware}; +use gotham::pipeline::{new_pipeline, single_pipeline}; +use gotham::router::builder::*; +use gotham::router::Router; +use gotham::state::{FromState, State, StateData}; + +/// A simple struct which holds an identifier for the user agent which made the request. +/// +/// It is created by our Middleware and then accessed via `state` by both our Middleware and Handler. +#[derive(StateData)] +pub struct ExampleMiddlewareData { + pub user_agent: String, + pub supported: bool, +} + +/// A struct that can act as a Gotham web framework middleware. +/// +/// The key requirements for struct to act as a Middleware are: +/// +/// 1. That the struct implements the `gotham::middleware::NewMiddleware` trait which allows +/// the Gotham web framework to create a new instance of your middleware to service every +/// request. In many cases, as we're doing here, this can simply be derived. +/// 2. That the struct implements the `gotham::middleware::Middleware` trait as we're doing +/// next. +#[derive(Clone, NewMiddleware)] +pub struct ExampleMiddleware; + +/// Implementing `gotham::middleware::Middleware` allows the logic that you want your Middleware to +/// provided to be correctly executed by the Gotham web framework Router. +/// +/// As shown here Middleware can make changes to the environment both before and after the handler^ +/// for the route is executed. +/// +/// ^Later examples will show how Middlewares in a pipeline can work with each other in a similar +/// manner. +impl Middleware for ExampleMiddleware { + fn call(self, mut state: State, chain: Chain) -> Pin> + where + Chain: FnOnce(State) -> Pin>, + { + let user_agent = match HeaderMap::borrow_from(&state).get(USER_AGENT) { + Some(ua) => ua.to_str().unwrap().to_string(), + None => "None".to_string(), + }; + + // Prior to letting Request handling proceed our middleware creates some new data and adds + // it to `state`. + state.put(ExampleMiddlewareData { + user_agent, + supported: false, + }); + + // We're finished working on the Request, so allow other components to continue processing + // the Request. + // + // Alternatively we could elect to not call chain and return a Response we've created if we + // want to prevent any further processing from occuring on the Request. + let inner_future = chain(state); + + // Once a Response is generated by another part of the application, in this example's case + // the middleware_reliant_handler function, we want to do some more work. + // + // Here we use async-await syntax to add new actions after handling request. + let f = async move { + let (state, mut response) = inner_future.await?; + let headers = response.headers_mut(); + let data = ExampleMiddlewareData::borrow_from(&state); + + // All our middleware does is add a header to the Response generated by our handler. + headers.insert( + "X-User-Agent", + format!( + "Supplied: {}, Supported: {}", + data.user_agent, data.supported + ) + .parse() + .unwrap(), + ); + + Ok((state, response)) + }; + + Box::pin(f) + } +} + +/// The handler which is invoked for all requests to "/". +/// +/// This handler expects that `ExampleMiddleware` has already been executed by Gotham before +/// it is invoked. As a result of that middleware being run our handler trusts that it must +/// have placed data into state that we can perform operations on. +pub fn middleware_reliant_handler(mut state: State) -> (State, Response) { + { + let data = ExampleMiddlewareData::borrow_mut_from(&mut state); + + // Mark any kind of web client as supported. A trival example but it highlights the + // interaction that is possible between Middleware and Handlers via state. + data.supported = true; + }; + + // Finally we create a basic Response to complete our handling of the Request. + let res = create_empty_response(&state, StatusCode::OK); + (state, res) +} + +/// Create a `Router` +fn router() -> Router { + // Within the Gotham web framework Middleware is added to and referenced from a Pipeline. + // + // A pipeline can consist of multiple Middleware types and guarantees to call them all in the + // ordering which is established by successive calls to the `add` method. + // + // A pipeline is considered complete once the build method is called and can no longer + // be modified. + // + // The Gotham web framework supports multiple Pipelines and even Pipelines containing Pipelines. + // However, as shown here, many applications will get sufficent power and flexibility + // from a `single_pipeline` which we've provided specific API assitance for. + let (chain, pipelines) = single_pipeline(new_pipeline().add(ExampleMiddleware).build()); + + // Notice we've switched from build_simple_router which has been present in all our examples up + // until this point. Under the hood build_simple_router has simply been creating an empty + // set of Pipelines on your behalf. + // + // Now that we're creating and populating our own Pipelines we'll switch to using + // build_router directly. + // + // Tip: Use build_simple_router for as long as you can. Switching to build_router is simple once + // do need to introduce Pipelines and Middleware. + build_router(chain, pipelines, |route| { + route.get("/").to(middleware_reliant_handler); + }) +} + +/// Start a server and use a `Router` to dispatch requests +pub fn main() { + let addr = "127.0.0.1:7878"; + println!("Listening for requests at http://{}", addr); + gotham::start(addr, router()).unwrap(); +} + +#[cfg(test)] +mod tests { + use super::*; + use gotham::test::TestServer; + + #[test] + fn ensure_middleware_and_handler_collaborate() { + let test_server = TestServer::new(router()).unwrap(); + let response = test_server + .client() + .get("http://localhost") + .with_header(USER_AGENT, "TestServer/0.0.0".parse().unwrap()) + .perform() + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + // Ensure Middleware has set a header after our handler generated the Response. + assert_eq!( + response.headers().get("X-User-Agent").unwrap(), + "Supplied: TestServer/0.0.0, Supported: true" + ); + } +}