Dependency injection
You just added a new input parameter to your request handler and, somehow, the framework was able to provide its value
at runtime without you having to do anything.
How does that work?
It's all thanks to dependency injection.
Pavex automatically injects the expected input parameters when invoking your handler functions as long as
it knows how to construct them.
Constructor registration
Let's zoom in on PathParams
: how does the framework know how to construct it?
You need to go back to the Blueprint
to find out:
// [...]
pub fn blueprint() -> Blueprint {
let mut bp = Blueprint::new();
ApiKit::new().register(&mut bp);
telemetry::register(&mut bp);
// [...]
}
ApiKit
is one of Pavex's kits: it
bundles together constructors for types
that are commonly used when building APIs with Pavex.
In particular, it includes a constructor for PathParams
.
A new extractor: UserAgent
Kits give you a head start, but they're not the last stop on your journey: to leverage Pavex to
its full potential, you'll soon need to define and register your own constructors.
There's no substitute for hands-on experience: let's design together a brand-new constructor
for our demo project to get a better understanding of how it all works.
We only want to greet people who include a User-Agent
header in their request(1).
- It's an arbitrary requirement, follow along for the sake of the example!
Let's start by defining a new UserAgent
type:
pub use blueprint::blueprint;
mod blueprint;
pub mod configuration;
pub mod routes;
pub mod telemetry;
pub mod user_agent;
pub enum UserAgent {
/// No `User-Agent` header was provided.
Unknown,
/// The value of the `User-Agent` header for the incoming request.
Known(String),
}
Missing constructor
What if you tried to inject UserAgent
into your request handler straight away? Would it work?
Let's find out!
// [...]
use crate::user_agent::UserAgent;
// [...]
pub fn get(params: PathParams<GreetParams>, user_agent: UserAgent /* (1)! */) -> Response {
if let UserAgent::Unknown = user_agent {
return Response::unauthorized().set_typed_body("You must provide a `User-Agent` header");
}
// [...]
}
- New input parameter!
If you try to build the project now, you'll get an error from Pavex:
ERROR:
× I can't find a constructor for `app::user_agent::UserAgent`.
│ I need an instance of `app::user_agent::UserAgent` to invoke your request
│ handler, `app::routes::greet::get`.
│
│ ╭─[app/src/routes/mod.rs:8:1]
│ 8 │ bp.route(GET, "/api/ping", f!(self::ping::get));
│ 9 │ bp.route(GET, "/api/greet/:name", f!(self::greet::get));
│ · ──────────┬─────────
│ · The request handler was registered here ──╯
│ 10 │ }
│ ╰────
│ ╭─[app/src/routes/greet.rs:10:1]
│ 10 │
│ 11 │ pub fn get(params: PathParams<GreetParams>, user_agent: UserAgent) -> Response {
│ · ──────────┬──────────
│ · I don't know how to construct an instance of this input parameter
│ 12 │ if let UserAgent::Unknown = user_agent {
│ ╰────
│ help: Register a constructor for `app::user_agent::UserAgent`.
│ help: Alternatively, use `Blueprint::prebuilt` to add a new input
│ parameter of type `app::user_agent::UserAgent` to the (generated)
│ `build_application_state`.
The invocation of `pavex [...] generate [...]` exited with a non-zero status code: 1
error: Failed to run `bp`, the code generator for package `server_sdk`
Pavex cannot do miracles, nor does it want to: it only knows how to construct a type if you tell it how to do so.
By the way: this is also your first encounter with Pavex's error messages!
We strive to make them as helpful as possible. If you find them confusing, report it as a bug!
Add a new constructor
To inject UserAgent
into your request handler, you need to define a constructor for it.
Constructors, just like request handlers, can take advantage of dependency injection: they can request input parameters
that will be injected by the framework at runtime.
Since you need to look at headers, ask for RequestHead
as input parameter: the incoming request data,
minus the body.
use pavex::http::header::USER_AGENT;
use pavex::request::RequestHead;
// [...]
impl UserAgent {
pub fn extract(request_head: &RequestHead) -> Self {
let Some(user_agent) = request_head.headers.get(USER_AGENT) else {
return Self::Unknown;
};
match user_agent.to_str() {
Ok(s) => Self::Known(s.into()),
Err(_e) => todo!(),
}
}
}
Now register the new constructor with the Blueprint
:
// [...]
use pavex::f;
// [...]
pub fn blueprint() -> Blueprint {
// [...]
bp.request_scoped(f!(crate::user_agent::UserAgent::extract));
// [...]
}
In Blueprint::request_scoped
you must specify
an unambiguous path to the constructor method,
wrapped in the f!
macro.
Make sure that the project compiles successfully at this point.
Lifecycles
A constructor registered via Blueprint::request_scoped
has
a request-scoped lifecycle: the framework
will invoke a request-scoped constructor at most once per request.
You can register constructors with two other lifecycles: singleton
and transient.
Singletons are built once and shared across requests.
Transient constructors, instead, are invoked every time their output type is needed—potentially
multiple times for the same request.
UserAgent
wouldn't be a good fit as a singleton or a transient constructor:
UserAgent
depends on the headers of the incoming request. It would be incorrect to mark it as a singleton and share it across requests (and Pavex wouldn't allow it!)- You could register
UserAgent
as transient, but extracting (and parsing) theUser-Agent
header multiple times would be wasteful. As a request-scoped constructor, it's done once and the outcome is reused.