Cookbook
This cookbook contains a collection of reference examples for Pavex's dependency injection framework. It covers the registration syntax for common use cases (free functions, methods) as well as more advanced ones (trait methods, generics, etc.).
Use it a reference in your day-to-day Pavex development if you're not sure of the syntax for a particular use case.
More than constructors
All the examples register constructors, but the very same f!
invocations can be used to register
request handlers, error handlers, error observers and middlewares.
Unambiguous paths
In all the cases described in this cookbook, we'll talk about unambiguous paths.
They allow Pavex to identify with certainty the callable you want to register.
The format differs between local items and items imported from a dependency of your crate.
Local items
If the item you want to register is defined in the current crate, you can use three different formats for its unambiguous path:
- A path prefixed with
crate
, spelling out where the item is located with respect to the root of the crate. - A path prefixed with
self
, spelling out where the item is located with respect to the root of the current module. - A path prefixed with
super
, spelling out where the item is located with respect to the parent of the current module.
The three formats are equivalent for Pavex.
In practice,
paths prefixed with self
or super
often end up being shorter—prefer them if you want to make your registration code terser.
From a dependency
If the item is defined in a dependency of your crate, you must use its absolute path, starting the name of
the crate it is defined into. E.g. reqwest::Client
, if reqwest
is one of your dependencies.
Free functions
Free functions are the simplest case.
You pass an unambiguous path to the f!
macro as input.
use pavex::blueprint::Blueprint;
use pavex::f;
pub fn blueprint() -> Blueprint {
let mut bp = Blueprint::new();
bp.request_scoped(f!(super::extract));
// [...]
}
use crate::User;
pub async fn extract() -> User {
// The constructor logic goes here
// [...]
}
Static methods
Static methods (1) behave exactly like free functions: you provide an unambiguous path to the f!
macro.
- A static method is a method that doesn't take
self
(or one of its variants) as an input parameter.
use pavex::blueprint::Blueprint;
use pavex::f;
pub fn blueprint() -> Blueprint {
let mut bp = Blueprint::new();
bp.request_scoped(f!(crate::User::extract));
// [...]
}
use crate::User;
impl User {
pub async fn extract() -> User {
// The constructor logic goes here
// [...]
}
}
Non-static methods
On the surface, non-static methods are registered in the same way as static methods:
by passing an unambiguous path to the f!
macro.
use pavex::blueprint::Blueprint;
use pavex::f;
pub fn blueprint() -> Blueprint {
let mut bp = Blueprint::new();
bp.request_scoped(f!(super::UserStore::retrieve));
// [...]
}
use pavex::request::RequestHead;
use crate::User;
pub struct UserStore(/* ... */);
impl UserStore {
// [...]
pub async fn retrieve(&self, request_head: &RequestHead) -> User {
// The constructor logic goes here
// [...]
}
}
However, there's a catch: self
counts as a dependency!
When registering a non-static method, you need to make sure to also register a constructor
for the type of self
.
use pavex::blueprint::Blueprint;
use pavex::f;
pub fn blueprint() -> Blueprint {
let mut bp = Blueprint::new();
// [...]
bp.request_scoped(f!(super::UserStore::new));
// [...]
}
// [...]
impl UserStore {
pub fn new() -> Self {
// The constructor logic goes here
// [...]
}
// [...]
}
Trait methods
The syntax for trait methods (1) is a bit more complex: you need to use the fully qualified syntax for function calls1.
- An inherent method is defined directly on a type (e.g.
Vec::new
). A trait method is part of trait definition (e.g.Iterator::next
) and it's available on a type if that type implements the trait.
use pavex::blueprint::Blueprint;
use pavex::f;
pub fn blueprint() -> Blueprint {
let mut bp = Blueprint::new();
bp.request_scoped(f!(<crate::User as super::WithId>::id));
// [...]
}
pub trait WithId {
fn id(&self) -> u64;
}
impl WithId for crate::User {
fn id(&self) -> u64 {
// [...]
}
}
Generics
Output-driven generics
A generic parameter is output-driven if it appears in the output type of a constructor.
use pavex::request::body::errors::ExtractJsonBodyError;
use pavex::request::body::BufferedBody;
pub fn parse<T>(body: BufferedBody) -> JsonBody<T> {
// [...]
}
If the constructor is fallible, the generic parameter must appear in type of the Ok
variant to
qualify as output-driven.
// [...]
pub fn fallible_parse<T>(body: BufferedBody) -> Result<JsonBody<T>, ExtractJsonBodyError> {
// [...]
}
If all generic parameters are output-driven, you can register the constructor as if it wasn't generic. Pavex will automatically infer the generic parameters based on the scenarios where the constructor is used.
use pavex::blueprint::Blueprint;
use pavex::f;
pub fn blueprint() -> Blueprint {
let mut bp = Blueprint::new();
bp.request_scoped(f!(super::parse));
// [...]
}
Input-driven generics
A generic parameter is input-driven if it isn't output-driven, i.e. it doesn't appear in the output type of a constructor.
use pavex::request::body::JsonBody;
pub fn length<T>(body: JsonBody<T>) -> u64 {
// [...]
}
If all generic parameters are input-driven, you need to explicitly specify the concrete type for each generic parameter when registering the constructor.
use pavex::blueprint::Blueprint;
use pavex::f;
pub fn blueprint() -> Blueprint {
let mut bp = Blueprint::new();
bp.request_scoped(f!(super::length::<super::GreetBody>));
// [...]
}
Mixed generics
If a constructor has both input-driven and output-driven generic parameters, you need to explicitly specify the concrete type for all generic parameters when registering the constructor.
-
Check out the relevant RFC if you're curious. ↩