Constructors
Define a constructor to make a type injectable. Pavex will invoke your constructor whenever it needs to create an instance of that type.
Defining a constructor
Pavex provides three different attributes to define a constructor: #[singleton], #[request_scoped] and #[transient].
use pavex::methods;
pub enum User {
Anonymous,
Authenticated(AuthenticatedUser),
}
pub struct AuthenticatedUser {
pub id: u64,
}
#[methods] // (1)!
impl User {
#[request_scoped]
pub fn extract() -> Self {
// Business logic goes here
// [...]
}
}
- The
#[methods]attribute must be added to theimplblock if you want to annotate one of its methods with a Pavex attribute.
Lifecycles
Each attribute attaches a different lifecycle to the output type:
- Singleton.
The constructor is invoked at most once, before the application starts. The same instance is injected every time the type is needed. - Request-scoped.
The constructor is invoked at most once per request. The same instance is injected every time the type is needed when handling the same request. - Transient.
The constructor is invoked every time the type is needed. Instances are never reused.
Let's look at a few common scenarios to build some intuition around lifecycles:
| Scenario | Lifecycle | Why? |
|---|---|---|
| Database connection pool | Singleton | The entire application should use the same pool. Each request will fetch a connection from the pool when needed. |
| HTTP client | Singleton | Most HTTP clients keep, under the hood, a connection pool. You want to reuse those connections across requests to minimise latency and the number of open file descriptors. |
| Path parameters | Request-scoped | Path parameters are extracted from the incoming request. They must not be shared across requests, therefore they can't be a singleton. They could be transient, but re-parsing the parameters before every use would be expensive. Request-scoped is the optimal choice. |
| Database connection | Transient | The connection is retrieved from a shared pool. It could be request-scoped, but you might end up keeping the connection booked (i.e. outside of the pool) for longer than it's strictly necessary. Transient is the optimal choice: you only remove the connection from the pool when it's needed, put it back when idle. |
Requirements
Constructors must return, as output, the type you want to make injectable.
Constructors can fail, too. A fallible constructor will return Result<T, E>, where T is the type you want to make injectable and E is an error type.
Other than that, you have a lot of freedom in how you define your constructors:
- They can be free functions or methods.
- They can be synchronous or asynchronous.
- They can take additional input parameters, leaning (recursively!) on Pavex's dependency injection system.
Registration
Use an import to register in bulk all the constructors defined in the current crate:
use pavex::{Blueprint, blueprint::from};
pub fn blueprint() -> Blueprint {
let mut bp = Blueprint::new();
bp.import(from![crate]); // (1)!
// [...]
}
- You can also import constructors from other crates or specific modules.
Alternatively, register constructors one by one using Blueprint::constructor:
use crate::user::USER_EXTRACT;
use pavex::Blueprint;
pub fn blueprint() -> Blueprint {
let mut bp = Blueprint::new();
bp.constructor(USER_EXTRACT); // (1)!
// [...]
}
USER_EXTRACTis a strongly-typed constant generated by the#[request_scoped]attribute on theUser::extractmethod.
Check out the documentation on component ids for more details.
Recursive dependencies
Dependency injection wouldn't be very useful if all constructors were required to take no input parameters. The dependency injection framework is recursive: constructors can take advantage of dependency injection to request the data they need to do their job.
Going back to our User example: it's unlikely that you'll be able to build a User instance without
taking a look at the incoming request, or some data extracted from it.
Let's say you want to build a User instance based on the value of the Authorization header
of the incoming request. We would modify the previous constructor to inject a &RequestHead instance:
// [...]
#[methods]
impl User {
#[request_scoped]
pub fn extract(head: &RequestHead /* (1)! */) -> Self {
// [...]
}
}
RequestHeadrepresents the incoming request data, minus the body.
When Pavex examines your Blueprint, the following happens:
- The
reject_anonymousmiddleware must be invoked. Doesreject_anonymoushave any input parameters? - Yes, it needs a
Userinstance. CanUserbe injected?- Yes, we can build it with
User::extract. DoesUser::extracthave any input parameters? - Yes, it needs a reference to a
RequestHead. Can&RequestHeadbe injected?- Etc.
- Yes, we can build it with
The recursion continues until:
- The required input has a constructor with no input parameters, or
- The required input is a framework primitive, or
- The required input is a prebuilt type, or
- The required input is a configuration type.
If the required input doesn't match any of the conditions above, Pavex will complain about a missing constructor.
Constructors can fail
Constructors can return a Result<T, E>, where E is an error type.
Check out the error handling guide for more details on how to handle the error case.
Invocation order
Pavex provides no guarantees on the relative invocation order of constructors.
Consider the following request handler:
use super::{A, B};
use pavex::Response;
use pavex::get;
#[get(path = "/")]
pub fn handler(a: A, b: B) -> Response {
// [...]
}
It injects two different types as input parameters, A and B.
The way input parameters are ordered in handler's definition does not influence the invocation order
of the respective constructors. Pavex may invoke A's constructor before B's constructor,
or vice versa.
The final invocation order will be primarily determined based on:
- Dependency constraints.
IfA's constructor takesCas input andC's constructor takes&Bas input,B's constructor will certainly be invoked beforeA's. There's no other way! - Borrow-checking constraints.
IfA's constructor takes a reference toCas input, whileB's constructor takesCby value, Pavex will invokeA's constructor first to avoid a.clone().
No mutations
Constructors are not allowed to take mutable references (i.e. &mut T) as inputs.
It'd be quite difficult to reason about mutations since you can't control the
invocation order of constructors.
On the other hand, invocation order is well-defined for other types of components: routes, pre-processing middlewares and post-processing middlewares. That's why Pavex allows them to inject mutable references as input parameters.
Wrapping middlewares
Invocation order is well-defined for wrapping middlewares, but Pavex
doesn't let them manipulate mutable references.
Check their guide
to learn more about the rationale for this exception.