Struct pavex::blueprint::Blueprint

source ·
pub struct Blueprint { /* private fields */ }
Expand description

The starting point for building an application with Pavex.

§Guide

Check out the “Project structure” section of Pavex’s guide for more details on the role of Blueprint in Pavex applications.

§Overview

A blueprint defines the runtime behaviour of your application.
It keeps track of:

You can also choose to decompose your overall application into smaller sub-components, taking advantage of Blueprint::nest and Blueprint::nest_at.

The information encoded in a blueprint can be serialized via Blueprint::persist and passed as input to Pavex’s CLI to generate the application’s server SDK.

Implementations§

source§

impl Blueprint

source

pub fn new() -> Self

Create a new Blueprint.

source

pub fn route( &mut self, method_guard: MethodGuard, path: &str, callable: RawCallable ) -> RegisteredRoute<'_>

Register a request handler to be invoked when an incoming request matches the specified route.

If a request handler has already been registered for the same route, it will be overwritten.

§Guide

Check out the “Routing” section of Pavex’s guide for a thorough introduction to routing in Pavex applications.

§Example
use pavex::{f, blueprint::{Blueprint, router::GET}};
use pavex::{request::RequestHead, response::Response};

fn my_handler(request_head: &RequestHead) -> Response {
    // [...]
}

let mut bp = Blueprint::new();
bp.route(GET, "/path", f!(crate::my_handler));
source

pub fn constructor( &mut self, callable: RawCallable, lifecycle: Lifecycle ) -> RegisteredConstructor<'_>

Register a constructor.

If a constructor for the same type has already been registered, it will be overwritten.

§Guide

Check out the “Dependency injection” section of Pavex’s guide for a thorough introduction to dependency injection in Pavex applications.

§Example
use pavex::f;
use pavex::blueprint::{Blueprint, constructor::Lifecycle};

fn logger(log_level: LogLevel) -> Logger {
    // [...]
}

let mut bp = Blueprint::new();
bp.constructor(f!(crate::logger), Lifecycle::Transient);
source

pub fn singleton(&mut self, callable: RawCallable) -> RegisteredConstructor<'_>

Register a constructor with a singleton lifecycle.

It’s a shorthand for Blueprint::constructor—refer to its documentation for more information on dependency injection in Pavex.

§Example
use pavex::f;
use pavex::blueprint::Blueprint;

fn logger(log_level: LogLevel) -> Logger {
    // [...]
}

let mut bp = Blueprint::new();
bp.singleton(f!(crate::logger));
// ^ is equivalent to:
// bp.constructor(f!(crate::logger), Lifecycle::Singleton));
source

pub fn request_scoped( &mut self, callable: RawCallable ) -> RegisteredConstructor<'_>

Register a constructor with a request-scoped lifecycle.

It’s a shorthand for Blueprint::constructor—refer to its documentation for more information on dependency injection in Pavex.

§Example
use pavex::f;
use pavex::blueprint::Blueprint;

fn logger(log_level: LogLevel) -> Logger {
    // [...]
}

let mut bp = Blueprint::new();
bp.request_scoped(f!(crate::logger));
// ^ is equivalent to:
// bp.constructor(f!(crate::logger), Lifecycle::RequestScoped));
source

pub fn transient(&mut self, callable: RawCallable) -> RegisteredConstructor<'_>

Register a constructor with a transient lifecycle.

It’s a shorthand for Blueprint::constructor—refer to its documentation for more information on dependency injection in Pavex.

§Example
use pavex::f;
use pavex::blueprint::Blueprint;

fn logger(log_level: LogLevel) -> Logger {
    // [...]
}

let mut bp = Blueprint::new();
bp.transient(f!(crate::logger));
// ^ is equivalent to:
// bp.constructor(f!(crate::logger), Lifecycle::Transient));
source

pub fn wrap( &mut self, callable: RawCallable ) -> RegisteredWrappingMiddleware<'_>

Register a wrapping middleware.

§Guide

Check out the “Middleware” section of Pavex’s guide for a thorough introduction to middlewares in Pavex applications.

§Example: a timeout wrapper
use pavex::{f, blueprint::Blueprint, middleware::Next, response::Response};
use std::future::{IntoFuture, 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.into_future()).await
}

pub fn api() -> Blueprint {
    let mut bp = Blueprint::new();
    // Register the wrapping middleware against the blueprint.
    bp.wrap(f!(crate::timeout_wrapper));
    // [...]
    bp
}
source

pub fn post_process( &mut self, callable: RawCallable ) -> RegisteredPostProcessingMiddleware<'_>

Register a post-processing middleware.

§Guide

Check out the “Middleware” section of Pavex’s guide for a thorough introduction to middlewares in Pavex applications.

§Example: a logging middleware
use pavex::{f, blueprint::Blueprint, response::Response};
use pavex_tracing::{
    RootSpan,
    fields::{http_response_status_code, HTTP_RESPONSE_STATUS_CODE}
};

pub fn response_logger(response: Response, root_span: &RootSpan) -> Response
{
    root_span.record(
        HTTP_RESPONSE_STATUS_CODE,
        http_response_status_code(&response),
    );
    response
}

pub fn api() -> Blueprint {
    let mut bp = Blueprint::new();
    // Register the post-processing middleware against the blueprint.
    bp.post_process(f!(crate::response_logger));
    // [...]
    bp
}
source

pub fn pre_process( &mut self, callable: RawCallable ) -> RegisteredPreProcessingMiddleware<'_>

Register a pre-processing middleware.

§Guide

Check out the “Middleware” section of Pavex’s guide for a thorough introduction to middlewares in Pavex applications.

§Example: path normalization
use pavex::{f, blueprint::Blueprint, response::Response};
use pavex::middleware::Processing;
use pavex::http::{HeaderValue, header::LOCATION};
use pavex::request::RequestHead;

/// If the request path ends with a `/`,
/// redirect to the same path without the trailing `/`.
pub fn redirect_to_normalized(request_head: &RequestHead) -> Processing
{
    let Some(normalized_path) = request_head.target.path().strip_suffix('/') else {
        // No need to redirect, we continue processing the request.
        return Processing::Continue;
    };
    let location = HeaderValue::from_str(normalized_path).unwrap();
    let redirect = Response::temporary_redirect().insert_header(LOCATION, location);
    // Short-circuit the request processing pipeline and return the redirect response
    // to the client without invoking downstream middlewares and the request handler.
    Processing::EarlyReturn(redirect)
}

pub fn api() -> Blueprint {
    let mut bp = Blueprint::new();
    // Register the pre-processing middleware against the blueprint.
    bp.pre_process(f!(crate::redirect_to_normalized));
    // [...]
    bp
}
source

pub fn nest_at(&mut self, prefix: &str, blueprint: Blueprint)

Nest a Blueprint under the current Blueprint (the parent), adding a common prefix to all the new routes.

§Routes

prefix will be prepended to all the routes coming from the nested blueprint.
prefix must be non-empty and it must start with a /.
If you don’t want to add a common prefix, check out Blueprint::nest.

§Trailing slashes

prefix can’t end with a trailing /.
This would result in routes with two consecutive / in their paths—e.g. /prefix//path—which is rarely desirable.
If you actually need consecutive slashes in your route, you can add them explicitly to the path of the route registered in the nested blueprint:

use pavex::f;
use pavex::blueprint::{Blueprint, router::GET};

fn app() -> Blueprint {
    let mut bp = Blueprint::new();
    bp.nest_at("/api", api_bp());
    bp
}

fn api_bp() -> Blueprint {
    let mut bp = Blueprint::new();
    // This will match `GET` requests to `/api//path`.
    bp.route(GET, "//path", f!(crate::handler));
    bp
}
§Constructors

Constructors registered against the parent blueprint will be available to the nested blueprint—they are inherited.
Constructors registered against the nested blueprint will not be available to other sibling blueprints that are nested under the same parent—they are private.

Check out the example below to better understand the implications of nesting blueprints.

§Visibility
use pavex::f;
use pavex::blueprint::{Blueprint, router::GET};
use pavex::blueprint::constructor::Lifecycle;

fn app() -> Blueprint {
    let mut bp = Blueprint::new();
    bp.constructor(f!(crate::db_connection_pool), Lifecycle::Singleton);
    bp.nest(home_bp());
    bp.nest(user_bp());
    bp
}

/// All property-related routes and constructors.
fn home_bp() -> Blueprint {
    let mut bp = Blueprint::new();
    bp.route(GET, "/home", f!(crate::v1::get_home));
    bp
}

/// All user-related routes and constructors.
fn user_bp() -> Blueprint {
    let mut bp = Blueprint::new();
    bp.constructor(f!(crate::user::get_session), Lifecycle::RequestScoped);
    bp.route(GET, "/user", f!(crate::user::get_user));
    bp
}

This example registers two routes:

  • GET /home
  • GET /user

It also registers two constructors:

  • crate::user::get_session, for Session;
  • crate::db_connection_pool, for ConnectionPool.

Since we are nesting the user_bp blueprint, the get_session constructor will only be available to the routes declared in the user_bp blueprint.
If a route declared in home_bp tries to inject a Session, Pavex will report an error at compile-time, complaining that there is no registered constructor for Session. In other words, all constructors declared against the user_bp blueprint are private and isolated from the rest of the application.

The db_connection_pool constructor, instead, is declared against the parent blueprint and will therefore be available to all routes declared in home_bp and user_bp—i.e. nested blueprints inherit all the constructors declared against their parent(s).

§Precedence

If a constructor is declared against both the parent and one of its nested blueprints, the one declared against the nested blueprint takes precedence.

use pavex::f;
use pavex::blueprint::{Blueprint, router::GET};
use pavex::blueprint::constructor::Lifecycle;

fn app() -> Blueprint {
    let mut bp = Blueprint::new();
    // This constructor is registered against the root blueprint and it's visible
    // to all nested blueprints.
    bp.constructor(f!(crate::global::get_session), Lifecycle::RequestScoped);
    bp.nest(user_bp());
    // [..]
    bp
}

fn user_bp() -> Blueprint {
    let mut bp = Blueprint::new();
    // It can be overridden by a constructor for the same type registered
    // against a nested blueprint.
    // All routes in `user_bp` will use `user::get_session` instead of `global::get_session`.
    bp.constructor(f!(crate::user::get_session), Lifecycle::RequestScoped);
    // [...]
    bp
}
§Singletons

There is one exception to the precedence rule: constructors for singletons (i.e. using Lifecycle::Singleton).
Pavex guarantees that there will be only one instance of a singleton type for the entire lifecycle of the application. What should happen if two different constructors are registered for the same Singleton type by two nested blueprints that share the same parent?
We can’t honor both constructors without ending up with two different instances of the same type, which would violate the singleton contract.

It goes one step further! Even if those two constructors are identical, what is the expected behaviour? Does the user expect the same singleton instance to be injected in both blueprints? Or does the user expect two different singleton instances to be injected in each nested blueprint?

To avoid this ambiguity, Pavex takes a conservative approach: a singleton constructor must be registered exactly once for each type.
If multiple nested blueprints need access to the singleton, the constructor must be registered against a common parent blueprint—the root blueprint, if necessary.

source

pub fn nest(&mut self, blueprint: Blueprint)

Nest a Blueprint under the current Blueprint (the parent), without adding a common prefix to all the new routes.

Check out Blueprint::nest_at for more details.

source

pub fn fallback(&mut self, callable: RawCallable) -> RegisteredFallback<'_>

Register a fallback handler to be invoked when an incoming request does not match any of the routes you registered with Blueprint::route.

If you don’t register a fallback handler, the default framework fallback will be used instead.

If a fallback handler has already been registered against this Blueprint, it will be overwritten.

§Example
use pavex::{f, blueprint::{Blueprint, router::GET}};
use pavex::response::Response;

fn handler() -> Response {
    // [...]
}
fn fallback_handler() -> Response {
    // [...]
}

let mut bp = Blueprint::new();
bp.route(GET, "/path", f!(crate::handler));
// The fallback handler will be invoked for all the requests that don't match `/path`.
// E.g. `GET /home`, `POST /home`, `GET /home/123`, etc.
bp.fallback(f!(crate::fallback_handler));
§Signature

A fallback handler is a function (or a method) that returns a Response, either directly (if infallible) or wrapped in a Result (if fallible).

Fallback handlers can take advantage of dependency injection, like any other component.
You list what you want to see injected as function parameters and Pavex will inject them for you in the generated code.

§Nesting

You can register a single fallback handler for each blueprint. If your application takes advantage of nesting, you can register a fallback against each nested blueprint in your application as well as one for the top-level blueprint.

Let’s explore how nesting affects the invocation of fallback handlers.

§Nesting without prefix

The fallback registered against a blueprint will be invoked for all the requests that match the path of a route that was directly registered against that blueprint, but don’t satisfy their method guards.

use pavex::{f, blueprint::{Blueprint, router::GET}};
use pavex::response::Response;

fn fallback_handler() -> Response {
    // [...]
}

let mut bp = Blueprint::new();
bp.route(GET, "/home", f!(crate::home_handler));
bp.nest({
    let mut bp = Blueprint::new();
    bp.route(GET, "/route", f!(crate::route_handler));
    bp.fallback(f!(crate::fallback_handler));
    bp
});

In the example above, crate::fallback_handler will be invoked for incoming POST /route requests: the path matches the path of a route registered against the nested blueprint (GET /route), but the method guard doesn’t (POST vs GET).
If the incoming requests don’t have /route as their path instead (e.g. GET /street or GET /route/123), they will be handled by the fallback registered against the parent blueprint—the top-level one in this case.
Since no fallback has been explicitly registered against the top-level blueprint, the default framework fallback will be used instead.

§Nesting with prefix

If the nested blueprint includes a nesting prefix (e.g. bp.nest_at("/api", api_bp)), its fallback will also be invoked for all the requests that start with the prefix but don’t match any of the route paths registered against the nested blueprint.

use pavex::{f, blueprint::{Blueprint, router::GET}};
use pavex::response::Response;

fn fallback_handler() -> Response {
    // [...]
}

let mut bp = Blueprint::new();
bp.route(GET, "/home", f!(crate::home_handler));
bp.nest_at("/route", {
    let mut bp = Blueprint::new();
    bp.route(GET, "/", f!(crate::route_handler));
    bp.fallback(f!(crate::fallback_handler));
    bp
});

In the example above, crate::fallback_handler will be invoked for both POST /route and POST /route/123 requests: the path of the latter doesn’t match the path of the only route registered against the nested blueprint (GET /route), but it starts with the prefix of the nested blueprint (/route).

source

pub fn error_observer( &mut self, callable: RawCallable ) -> RegisteredErrorObserver<'_>

Register an error observer to intercept and report errors that occur during request handling.

§Guide

Check out the “Error observers” section of Pavex’s guide for a thorough introduction to error observers in Pavex applications.

§Example
use pavex::f;
use pavex::blueprint::Blueprint;

pub fn error_logger(e: &pavex::Error) {
    tracing::error!(
        error.msg = %e,
        error.details = ?e,
        "An error occurred while handling a request"
    );
}

let mut bp = Blueprint::new();
bp.error_observer(f!(crate::error_logger));
source§

impl Blueprint

Methods to serialize and deserialize a Blueprint.
These are used to pass the blueprint data to Pavex’s CLI.

source

pub fn persist(&self, filepath: &Path) -> Result<(), Error>

Serialize the Blueprint to a file in RON format.

The file is only written to disk if the content of the blueprint has changed.

source

pub fn load(filepath: &Path) -> Result<Self, Error>

Read a RON-encoded Blueprint from a file.

Trait Implementations§

source§

impl Default for Blueprint

source§

fn default() -> Self

Returns the “default value” for a type. Read more

Auto Trait Implementations§

Blanket Implementations§

source§

impl<T> Any for T
where T: 'static + ?Sized,

source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
source§

impl<T> Borrow<T> for T
where T: ?Sized,

source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
source§

impl<T> From<T> for T

source§

fn from(t: T) -> T

Returns the argument unchanged.

source§

impl<T> Instrument for T

source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
source§

impl<T, U> Into<U> for T
where U: From<T>,

source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

source§

impl<T> Same for T

§

type Output = T

Should always be Self
source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

§

type Error = Infallible

The type returned in the event of a conversion error.
source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

source§

fn vzip(self) -> V

source§

impl<T> WithSubscriber for T

source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more