Skip to content

Commit

Permalink
Wrapping middleware #2: Validation (#85)
Browse files Browse the repository at this point in the history
The registered middlewares are now properly validation by `pavexc`: 
- they take `Next` as input (once)
- they return a type that can be converted into a `Response` (either
directly or in the happy case)
- they don't use generic parameters that we can't reliably infer

I did some refactorings along the way, especially in the component
database (arguably the messiest part of the codebase).
  • Loading branch information
LukeMathWalker authored Aug 14, 2023
1 parent c74a941 commit 7b2481c
Show file tree
Hide file tree
Showing 49 changed files with 1,526 additions and 496 deletions.
1 change: 1 addition & 0 deletions examples/realworld/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions examples/skeleton/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions examples/skeleton/app_server_sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ app_blueprint = { version = "0.1.0", path = "../app_blueprint", package = "app_b
http = { version = "0.2.9", package = "http" }
hyper = { version = "0.14.26", package = "hyper" }
pavex = { version = "0.1.0", path = "../../../libs/pavex", package = "pavex" }
thiserror = { version = "1.0.40", package = "thiserror" }
1 change: 1 addition & 0 deletions examples/skeleton/app_server_sdk/blueprint.ron
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
error_handler: None,
),
],
middlewares: [],
routes: [
(
path: "/home",
Expand Down
107 changes: 62 additions & 45 deletions libs/pavex/src/blueprint/blueprint.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,31 @@
use super::constructor::{Constructor, Lifecycle};
use super::internals::{
NestedBlueprint, RegisteredCallable, RegisteredConstructor, RegisteredRoute,
RegisteredWrappingMiddleware,
};
use super::middleware::WrappingMiddleware;
use super::reflection::{Location, RawCallable, RawCallableIdentifiers};
use super::router::{MethodGuard, Route};

#[derive(serde::Serialize, serde::Deserialize)]
/// The starting point for building an application with Pavex.
///
/// A blueprint defines the runtime behaviour of your application.
/// It captures three types of information:
/// It keeps track of:
///
/// - route handlers, via [`Blueprint::route`].
/// - constructors, via [`Blueprint::constructor`].
/// - error handlers, via [`Constructor::error_handler`].
/// - route handlers, registered via [`Blueprint::route`]
/// - constructors, registered via [`Blueprint::constructor`]
/// - wrapping middlewares, registered via [`Blueprint::wrap`]
///
/// This information is then serialized via [`Blueprint::persist`] and passed as input to
/// Pavex's CLI to generate the application's source code.
///
/// [`Constructor::error_handler`]: Constructor::error_handler
pub struct Blueprint {
/// The location where the [`Blueprint`] was created.
pub creation_location: Location,
/// All registered constructors, in the order they were registered.
pub constructors: Vec<RegisteredConstructor>,
/// All registered middlewares, in the order they were registered.
pub middlewares: Vec<RegisteredWrappingMiddleware>,
/// All registered routes, in the order they were registered.
pub routes: Vec<RegisteredRoute>,
/// All blueprints nested under this one, in the order they were nested.
Expand All @@ -39,6 +41,7 @@ impl Blueprint {
constructors: Default::default(),
routes: Default::default(),
nested_blueprints: Default::default(),
middlewares: Default::default(),
}
}

Expand Down Expand Up @@ -255,35 +258,34 @@ impl Blueprint {

#[track_caller]
/// Register a wrapping middleware.
///
///
/// A wrapping middleware is invoked before the request handler and it is given
/// the opportunity to *wrap* the execution of the rest of the request processing
/// pipeline, including the request handler itself.
///
///
/// It is primarily useful for functionality that requires access to the [`Future`]
/// representing the rest of the request processing pipeline, such as:
///
///
/// - structured logging (e.g. attaching a `tracing` span to the request execution);
/// - timeouts;
/// - metric timers;
/// - etc.
///
///
/// # Example: a timeout wrapper
///
///
/// ```rust
/// use pavex::middleware::Next;
/// use pavex::response::Response;
/// use pavex::{f, blueprint::Blueprint, middleware::Next, response::Response};
/// use std::future::Future;
/// use std::time::Duration;
/// use tokio::time::{timeout, error::Elapsed};
///
///
/// pub async fn timeout_wrapper<C>(next: Next<C>) -> Result<Response, Elapsed>
/// where
/// C: Future<Output = Response>
/// {
/// timeout(Duration::from_secs(2), next).await
/// }
///
///
/// pub fn api() -> Blueprint {
/// let mut bp = Blueprint::new();
/// // Register the wrapping middleware against the blueprint.
Expand All @@ -292,29 +294,29 @@ impl Blueprint {
/// bp
/// }
/// ```
///
///
/// # Signature
///
/// A wrapping middleware is an asynchronous function (or a method) that takes [`Next`]
///
/// A wrapping middleware is an asynchronous function (or a method) that takes [`Next`]
/// as input and returns a [`Response`], either directly (if infallible) or wrapped in a
/// [`Result`] (if fallible).
///
///
/// ```rust
/// use pavex::{middleware::Next, response::Response};
/// use std::{future::Future, time::Duration};
/// use tokio::time::{timeout, error::Elapsed};
/// use tracing::Instrument;
///
///
/// // This is an infallible wrapping middleware. It returns a `Response` directly.
/// pub async fn logging_wrapper<C>(next: Next<C>) -> Response
/// pub async fn logging_wrapper<C>(next: Next<C>) -> Response
/// where
/// C: Future<Output = Response>
/// {
/// let span = tracing::info_span!("Incoming request");
/// next.instrument(span).await
/// }
///
/// // This is a fallible wrapping middleware.
///
/// // This is a fallible wrapping middleware.
/// // It returns a `Result<Response, Elapsed>`.
/// pub async fn timeout_wrapper<C>(next: Next<C>) -> Result<Response, Elapsed>
/// where
Expand All @@ -323,26 +325,29 @@ impl Blueprint {
/// timeout(Duration::from_secs(1), next).await
/// }
/// ```
///
///
/// ## Dependency injection
///
///
/// Wrapping middlewares can take advantage of dependency injection, like any
/// other component.
/// You list what you want to inject as function parameters (in _addition_ to [`Next`])
/// You list what you want to inject as function parameters (in _addition_ to [`Next`])
/// and Pavex will inject them for you in the generated code:
///
///
/// ```rust
/// use pavex::{middleware::Next, response::Response};
/// use pavex::{
/// blueprint::{Blueprint, constructor::Lifecycle},
/// f, middleware::Next, response::Response
/// };
/// use std::{future::Future, time::Duration};
/// use tokio::time::{timeout, error::Elapsed};
///
///
/// #[derive(Copy, Clone)]
/// pub struct TimeoutConfig {
/// request_timeout: Duration
/// }
///
///
/// pub async fn timeout_wrapper<C>(
/// next: Next<C>,
/// next: Next<C>,
/// // This parameter will be injected by the framework.
/// config: TimeoutConfig
/// ) -> Result<Response, Elapsed>
Expand All @@ -351,22 +356,22 @@ impl Blueprint {
/// {
/// timeout(config.request_timeout, next).await
/// }
///
///
/// pub fn api() -> Blueprint {
/// let mut bp = Blueprint::new();
/// // We need to register a constructor for the dependencies
/// // We need to register a constructor for the dependencies
/// // that we want to inject
/// bp.constructor(f!(crate::timeout_config), Lifecycle::RequestScoped);
/// bp.wrap(f!(crate::timeout_wrapper));
/// // [...]
/// bp
/// }
/// ```
///
///
/// # Execution order
///
///
/// Wrapping middlewares are invoked in the order they are registered.
///
///
/// ```rust
/// use pavex::{f, blueprint::{Blueprint, router::GET}};
/// # use pavex::{request::RequestHead, response::Response, middleware::Next};
Expand All @@ -380,26 +385,38 @@ impl Blueprint {
/// bp.route(GET, "/home", f!(crate::handler));
/// # }
/// ```
///
/// `first` will be invoked before `second`, which is in turn invoked before the
///
/// `first` will be invoked before `second`, which is in turn invoked before the
/// request handler.
/// Or, in other words:
///
///
/// - `second` is invoked when `first` calls `.await` on its `Next` input
/// - the request handler is invoked when `second` calls `.await` on its `Next` input
///
/// ## Nesting
///
///
/// ## Nesting
///
/// If a blueprint is nested under another blueprint, the wrapping middlewares registered
/// against the parent blueprint will be invoked before the wrapping middlewares registered
/// against the nested blueprint.
///
///
/// [`Next`]: crate::middleware::Next
/// [`Response`]: crate::response::Response
/// [`Future`]: std::future::Future
#[doc(alias = "middleware")]
pub fn wrap(&mut self, _callable: RawCallable) {
todo!()
pub fn wrap(&mut self, callable: RawCallable) -> WrappingMiddleware {
let registered = RegisteredWrappingMiddleware {
middleware: RegisteredCallable {
callable: RawCallableIdentifiers::from_raw_callable(callable),
location: std::panic::Location::caller().into(),
},
error_handler: None,
};
let middleware_id = self.middlewares.len();
self.middlewares.push(registered);
WrappingMiddleware {
blueprint: self,
middleware_id,
}
}

#[track_caller]
Expand Down
9 changes: 9 additions & 0 deletions libs/pavex/src/blueprint/internals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ pub struct RegisteredConstructor {
pub error_handler: Option<RegisteredCallable>,
}

#[derive(serde::Serialize, serde::Deserialize)]
/// A middleware registered against a [`Blueprint`] via [`Blueprint::wrap`].
pub struct RegisteredWrappingMiddleware {
/// The callable that executes the middleware's logic.
pub middleware: RegisteredCallable,
/// The callable in charge of processing errors returned by this middleware, if any.
pub error_handler: Option<RegisteredCallable>,
}

#[derive(serde::Serialize, serde::Deserialize)]
/// A "callable" registered against a [`Blueprint`]—either a free function or a method,
/// used as a request handler, error handler or constructor.
Expand Down
8 changes: 8 additions & 0 deletions libs/pavex/src/blueprint/middleware/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
//! Execute common logic across multiple routes.
//!
//! Check out [`Blueprint::wrap`] for a brief introduction to wrapping middlewares in Pavex.
//!
//! [`Blueprint::wrap`]: crate::blueprint::Blueprint::wrap
mod wrapping;

pub use wrapping::WrappingMiddleware;
71 changes: 71 additions & 0 deletions libs/pavex/src/blueprint/middleware/wrapping.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
use crate::blueprint::{Blueprint, reflection::{RawCallable, RawCallableIdentifiers}, internals::RegisteredCallable};

/// The type returned by [`Blueprint::wrap`].
///
/// It allows you to further configure the behaviour of the registered wrapping
/// middleware.
pub struct WrappingMiddleware<'a> {
#[allow(dead_code)]
pub(crate) blueprint: &'a mut Blueprint,
/// The index of the registered wrapping middleware in the
/// [`Blueprint`]'s `middlewares` vector.
pub(crate) middleware_id: usize,
}

impl<'a> WrappingMiddleware<'a> {
#[track_caller]
/// Register an error handler.
///
/// Error handlers convert the error type returned by your middleware into an HTTP response.
///
/// Error handlers **can't** consume the error type, they must take a reference to the
/// error as input.
/// Error handlers can have additional input parameters alongside the error, as long as there
/// are constructors registered for those parameter types.
///
/// ```rust
/// use pavex::{f, blueprint::Blueprint, middleware::Next};
/// use pavex::{response::Response, hyper::body::Body};
/// use std::future::Future;
/// # struct LogLevel;
/// # struct Logger;
/// # struct TimeoutError;
///
/// fn timeout_middleware<C>(next: Next<C>) -> Result<Response, TimeoutError>
/// where
/// C: Future<Output = Response>
/// {
/// // [...]
/// # todo!()
/// }
///
/// fn error_to_response(error: &TimeoutError, log_level: LogLevel) -> Response {
/// // [...]
/// # todo!()
/// }
///
/// # fn main() {
/// let mut bp = Blueprint::new();
/// bp.wrap(f!(crate::timeout_middleware))
/// .error_handler(f!(crate::error_to_response));
/// # }
/// ```
///
/// If an error handler has already been registered for the same error type, it will be
/// overwritten.
///
/// ## Common Errors
///
/// Pavex will fail to generate the runtime code for your application if you register
/// an error handler for an infallible middleware (i.e. a middleware that doesn't return
/// a `Result`).
pub fn error_handler(self, error_handler: RawCallable) -> Self {
let callable_identifiers = RawCallableIdentifiers::from_raw_callable(error_handler);
let callable = RegisteredCallable {
callable: callable_identifiers,
location: std::panic::Location::caller().into(),
};
self.blueprint.middlewares[self.middleware_id].error_handler = Some(callable);
self
}
}
1 change: 1 addition & 0 deletions libs/pavex/src/blueprint/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ pub use blueprint::Blueprint;
mod blueprint;
pub mod constructor;
pub mod internals;
pub mod middleware;
pub mod reflection;
pub mod router;
2 changes: 2 additions & 0 deletions libs/pavex_cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ guppy = "0.15"

[dev-dependencies]
pavex_test_runner = { path = "../pavex_test_runner" }
# Enable more expensive debug assertions when building for testing purposes
pavexc = { path = "../pavexc", features = ["debug_assertions"] }
Loading

0 comments on commit 7b2481c

Please sign in to comment.