Skip to content

Commit

Permalink
feat: Reshape singleton dependencies (#298)
Browse files Browse the repository at this point in the history
This is a fairly big change to Pavex's dependency injection system.
Before this PR, all singleton dependencies were automatically added as
inputs to `build_application_state` if they had no registered
constructor. There was no way to explicitly tell Pavex, "Please, add
this as input to `build_application_state`, I'll build it for you".
 
After this PR, singleton dependencies are no longer special-cased. If
there's no constructor for them, you'll get an error.
But you now have two options for resolving it: register a constructor or
mark them as "prebuilt types."
Prebuilt types (if used) will be added as inputs to
`build_application_state` and then used as if they had been built by
Pavex. The same logic applies to constructed types, e.g. cloning
strategy.

This brings along a few other changes:
- You can no longer register a type with a non-'static lifetime
parameter (implicit or explicit) as a singleton.
- Singletons are no longer forced to be cloned
- We validate upfront that all types with a `CloneIfNecessary` strategy
implement `Clone`
- This surfaced a bug where `BufferedBody` was marked as
`CloneIfNecessary` but didn't implement `Clone`. This is now fixed.
  • Loading branch information
LukeMathWalker authored Jun 14, 2024
1 parent 38eb192 commit 00b1822
Show file tree
Hide file tree
Showing 175 changed files with 4,665 additions and 1,164 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
ERROR:
× I can't find a constructor for `biscotti::ProcessorConfig`.
│ I need an instance of `biscotti::ProcessorConfig` to
│ invoke your constructor, `<pavex::cookie::Processor as
│ std::convert::From::<pavex::cookie::ProcessorConfig>>::from`.
│
│ ╭─[../../../../../libs/pavex/src/cookie/kit.rs:80:1]
│ 80 │ .error_handler(f!(super::errors::InjectResponseCookiesError::into_response));
│ 81 │ ╭─▶ let processor = Constructor::singleton(f!(<super::Processor as std::convert::From<
│ 82 │ │ super::ProcessorConfig,
│ 83 │ ├─▶ >>::from))
│ · ╰──── The constructor was registered here
│ 84 │ .ignore(Lint::Unused);
│ ╰────
│ help: Register a constructor for `biscotti::ProcessorConfig`.
│  help: Alternatively, use `Blueprint::prebuilt` to add a new input
│ parameter of type `biscotti::ProcessorConfig` to the (generated)
│ `build_application_state`.

Error: NonZeroExitCode(NonZeroExitCode { code: 1, command: "pavex [...] generate [...]" })
error: Failed to run `cookie_installation`, the code generator for package `cookie_installation_server_sdk`
9 changes: 5 additions & 4 deletions doc_examples/guide/cookies/installation/tutorial.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
starter_project_folder: "project"
commands:
- command: "cargo px c"
expected_outcome: "success"
- command: "cargo px c -q"
expected_outcome: "failure"
expected_output_at: "missing_process_config.snap"
snippets:
- name: "kit"
source_path: "src/blueprint.rs"
ranges: [ "0..6", "7..8" ]
hl_lines: [ 6 ]
ranges: ["0..6", "7..8"]
hl_lines: [6]
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
```rust title="server_sdk/src/lib.rs" hl_lines="2"
// [...]
pub async fn build_application_state(v0: di_prebuilt::base::A) -> crate::ApplicationState {
// [...]
}
```
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
```rust title="src/base/blueprint.rs" hl_lines="4"
// [...]
pub fn blueprint() -> Blueprint {
let mut bp = Blueprint::new();
bp.prebuilt(t!(super::A));
// [...]
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/target
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "di_prebuilt"
version = "0.1.0"
edition = "2021"

[dependencies]
pavex = { path = "../../../../../libs/pavex" }
pavex_cli_client = { path = "../../../../../libs/pavex_cli_client" }
cargo_px_env = "0.1"

[workspace]
members = [".", "server_sdk"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
name = "di_prebuilt_server_sdk"
version = "0.1.0"
edition = "2021"

[package.metadata.px.generate]
generator_type = "cargo_workspace_binary"
generator_name = "di_prebuilt"

[dependencies]
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use pavex::blueprint::router::GET;
use pavex::blueprint::Blueprint;
use pavex::{f, t};

pub fn blueprint() -> Blueprint {
let mut bp = Blueprint::new();
bp.prebuilt(t!(super::A));
bp.route(GET, "/", f!(super::handler));
bp
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
use super::A;
use pavex::response::Response;

pub fn handler(a: &A) -> Response {
// Handler logic
todo!()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
pub use blueprint::blueprint;
pub use handler::handler;

mod blueprint;
mod handler;

pub struct A;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
use pavex::blueprint::Blueprint;

pub fn blueprint() -> Blueprint {
let mut bp = Blueprint::new();
bp.nest(crate::base::blueprint());
bp
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#![allow(dead_code)]
#![allow(unused_variables)]

pub use blueprint::blueprint;

pub mod base;
mod blueprint;
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
use std::error::Error;

use cargo_px_env::generated_pkg_manifest_path;
use pavex_cli_client::Client;

use di_prebuilt::blueprint;

fn main() -> Result<(), Box<dyn Error>> {
let generated_dir = generated_pkg_manifest_path()?.parent().unwrap().into();
Client::new()
.generate(blueprint(), generated_dir)
.execute()?;
Ok(())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
use pavex::http::StatusCode;

pub fn handler() -> StatusCode {
todo!()
}
16 changes: 16 additions & 0 deletions doc_examples/guide/dependency_injection/prebuilt/tutorial.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
starter_project_folder: "project"
commands:
- command: "cargo px c"
expected_outcome: "success"
snippets:
- name: "registration"
source_path: "src/base/blueprint.rs"
ranges: ["4..7", "9..10"]
hl_lines: [4]
steps:
- patch: "01.patch"
snippets:
- name: "build_state"
source_path: "server_sdk/src/lib.rs"
ranges: ["11..12", "13..14"]
hl_lines: [2]
11 changes: 7 additions & 4 deletions doc_examples/quickstart/05-error.snap
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
ERROR:
[31m×[0m I can't invoke your request handler, `app::routes::greet::get`, because
[31m│[0m it needs an instance of `app::user_agent::UserAgent` as input, but I can't
[31m│[0m find a constructor for that type.
[31m×[0m I can't find a constructor for `app::user_agent::UserAgent`.
[31m│[0m I need an instance of `app::user_agent::UserAgent` to invoke your request
[31m│[0m handler, `app::routes::greet::get`.
│
│ ╭─[app/src/routes/mod.rs:8:1]
│  8bp.route(GET, "/api/ping", f!(self::ping::get));
Expand All @@ -17,7 +17,10 @@
│ · I don't know how to construct an instance of this input parameter
│ 12if let UserAgent::Unknown = user_agent {
│ ╰────
│  help: Register a constructor for `app::user_agent::UserAgent`
│ 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`
8 changes: 4 additions & 4 deletions doc_examples/tutorial_generator/Cargo.lock

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

6 changes: 3 additions & 3 deletions docs/getting_started/quickstart/dependency_injection.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ You need to go back to the [`Blueprint`][Blueprint] to find out:

--8<-- "doc_examples/quickstart/04-register_common_invocation.snap"

[`ApiKit`][ApiKit] is one of Pavex's [kits](../../guide/dependency_injection/core_concepts/kits.md): it
bundles together [constructors](../../guide/dependency_injection/core_concepts/constructors.md) for types
[`ApiKit`][ApiKit] is one of Pavex's [kits](../../guide/dependency_injection/kits.md): it
bundles together [constructors](../../guide/dependency_injection/constructors.md) for types
that are commonly used when building APIs with Pavex.
In particular, it includes a constructor for [`PathParams`][PathParams].

Expand Down Expand Up @@ -103,5 +103,5 @@ multiple times for the same request.
[f!]: ../../api_reference/pavex/macro.f!.html
[PathParams]: ../../api_reference/pavex/request/path/struct.PathParams.html
[ApiKit]: ../../api_reference/pavex/kit/struct.ApiKit.html
[lifecycle]: ../../guide/dependency_injection/core_concepts/constructors.md#lifecycles
[lifecycle]: ../../guide/dependency_injection/constructors.md#lifecycles
[RequestHead]: ../../api_reference/pavex/request/struct.RequestHead.html
12 changes: 8 additions & 4 deletions docs/guide/cookies/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,21 @@ Register it with your [`Blueprint`][Blueprint] to get started:
--8<-- "doc_examples/guide/cookies/installation/project-kit.snap"

You can customize each component inside [`CookieKit`][CookieKit] to suit your needs.
Check out the ["Kits"](../dependency_injection/core_concepts/kits.md#customization)
Check out the ["Kits"](../dependency_injection/kits.md#customization)
section for reference examples.

## `ProcessorConfig`

[`ProcessorConfig`][ProcessorConfig] determines how cookies (both incoming and outgoing) are processed by your application.
Do they need to be percent-encoded? Should they be signed or encrypted? Using what key?

Once you register [`CookieKit`][CookieKit] against your [`Blueprint`][Blueprint], you'll be asked to provide an instance of
[`ProcessorConfig`][ProcessorConfig] as an input to your [`build_application_state`][build_application_state] function.
Once you register [`CookieKit`][CookieKit] against your [`Blueprint`][Blueprint], you'll get an error:

```ansi-color
--8<-- "doc_examples/guide/cookies/installation/missing_process_config.snap"
```

To fix it, you need to give Pavex a way to work with a [`ProcessorConfig`][ProcessorConfig] instance.
You have two options:

1. Add [`ProcessorConfig`][ProcessorConfig] as a field on your application's `AppConfig` struct, usually located
Expand All @@ -48,4 +52,4 @@ encrypted or signed.
[ProcessorConfig::default]: ../../api_reference/pavex/cookie/struct.ProcessorConfig.html#method.default
[ProcessorConfig::crypto_rules]: ../../api_reference/pavex/cookie/struct.ProcessorConfig.html#structfield.crypto_rules
[default settings]: ../../api_reference/pavex/cookie/struct.ProcessorConfig.html#fields
[build_application_state]: ../project_structure.md#applicationstate
[build_application_state]: ../project_structure.md#applicationstate
2 changes: 1 addition & 1 deletion docs/guide/cookies/response_cookies.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ It exposes multiple `set_*` methods to configure the cookie's properties: `Path`
You can only inject mutable references into [request handlers](../routing/request_handlers.md),
[pre-processing middlewares](../middleware/pre_processing.md), and [post-processing middlewares](../middleware/post_processing.md).
As a result, you can only set cookies in those components.
Check out ["No mutations"](../dependency_injection/core_concepts/constructors.md#no-mutations) for more information
Check out ["No mutations"](../dependency_injection/constructors.md#no-mutations) for more information
on the rationale.

## Remove a client-side cookie
Expand Down
19 changes: 19 additions & 0 deletions docs/guide/dependency_injection/application_state.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# `ApplicationState`

When generating the [server SDK crate], Pavex examines all the components you registered to determine which [singletons][Lifecycle::Singleton] will be
used at runtime to process incoming requests.
Pavex then generates a type to group them together, named `ApplicationState`.

## `build_application_state`

Inside the [server SDK crate], you'll also find a function named [`build_application_state`][build_application_state]. As the name suggest, it returns an instance of `ApplicationState`.

`build_application_state` takes as input all the types that you marked
as [prebuilt](prebuilt_types.md).
Inside its body, it'll invoke the constructors for all your [singletons][Lifecycle::Singleton] in order to build an instance of `ApplicationState`.

[Lifecycle::Singleton]: ../../api_reference/pavex/blueprint/constructor/enum.Lifecycle.html#variant.Singleton
[build_application_state]: ../project_structure.md#applicationstate
[server crate]: ../project_structure.md#the-server-crate
[ApplicationState]: ../project_structure.md#applicationstate
[server SDK crate]: ../project_structure.md#the-server-sdk
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Constructors

To make a type injectable, you need to **register a constructor** for it.
To make a type injectable, you can **register a constructor** for it.
Pavex will then invoke your constructor to create instances of that type when needed.

## Requirements

A constructor must satisfy a few requirements:

Expand All @@ -25,7 +28,7 @@ Going back to our `User` example, this would be a valid signature for a construc

Constructors can be either sync or async.
Check out
[the "Sync or async" section](../../routing/request_handlers.md#sync-or-async) in the guide on request handlers
[the "Sync or async" section](../routing/request_handlers.md#sync-or-async) in the guide on request handlers
to learn when to use one or the other.

## Registration
Expand All @@ -36,7 +39,7 @@ Once you have defined a constructor, you need to register it with the applicatio

[`Blueprint::constructor`][Blueprint::constructor] takes two arguments:

- An [unambiguous path](../cookbook.md) to the constructor, wrapped in the [`f!`][f] macro.
- An [unambiguous path](cookbook.md) to the constructor, wrapped in the [`f!`][f] macro.
- The [constructor's lifecycle](#lifecycles).

Alternatively, you could use [`Blueprint::request_scoped`][Blueprint::request_scoped] as
Expand Down Expand Up @@ -70,7 +73,7 @@ Let's look at a few common scenarios to build some intuition around lifecycles:

## Recursive dependencies

Dependency injection wouldn't be very useful if all constructors were required to take no input parameters.
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.

Expand All @@ -95,14 +98,14 @@ When Pavex examines your application [`Blueprint`][Blueprint], the following hap
The recursion continues until Pavex finds a constructor that doesn't have any input parameters or
a type that doesn't need to be constructed.
If a type needs to be constructed, but Pavex can't find a constructor for it,
[it will report an error](../../../getting_started/quickstart/dependency_injection.md#missing-constructor).
[it will report an error](../../getting_started/quickstart/dependency_injection.md#missing-constructor).

## Constructors can fail

Constructors can be fallible: they can return a `Result<T, E>`, where `E` is an error type.
If a constructor is fallible, you must specify an [**error handler**](../../errors/error_handlers.md) when registering
If a constructor is fallible, you must specify an [**error handler**](../errors/error_handlers.md) when registering
it with the application [`Blueprint`][Blueprint].
Check out the [error handling guide](../../errors/error_handlers.md) for more details.
Check out the [error handling guide](../errors/error_handlers.md) for more details.

## Invocation order

Expand Down Expand Up @@ -133,26 +136,26 @@ It'd be quite difficult to reason about mutations since you can't control the
[invocation order of constructors](#invocation-order).

On the other hand, invocation order is well-defined for other types of components:
[request handlers](../../routing/request_handlers.md),
[pre-processing middlewares](../../middleware/pre_processing.md) and
[post-processing middlewares](../../middleware/post_processing.md).
[request handlers](../routing/request_handlers.md),
[pre-processing middlewares](../middleware/pre_processing.md) and
[post-processing middlewares](../middleware/post_processing.md).
That's why Pavex allows them to inject mutable references as input parameters.

!!! note "Wrapping middlewares"

Invocation order is well-defined for wrapping middlewares, but Pavex
doesn't let them manipulate mutable references.
Check [their guide](../../middleware/wrapping.md#use-with-caution)
Check [their guide](../middleware/wrapping.md#use-with-caution)
to learn more about the rationale for this exception.


[Blueprint]: ../../../api_reference/pavex/blueprint/struct.Blueprint.html
[Blueprint::constructor]: ../../../api_reference/pavex/blueprint/struct.Blueprint.html#method.constructor
[Blueprint::singleton]: ../../../api_reference/pavex/blueprint/struct.Blueprint.html#method.singleton
[Blueprint::request_scoped]: ../../../api_reference/pavex/blueprint/struct.Blueprint.html#method.request_scoped
[Blueprint::transient]: ../../../api_reference/pavex/blueprint/struct.Blueprint.html#method.transient
[f]: ../../../api_reference/pavex/macro.f.html
[Lifecycle::Singleton]: ../../../api_reference/pavex/blueprint/constructor/enum.Lifecycle.html#variant.Singleton
[Lifecycle::RequestScoped]: ../../../api_reference/pavex/blueprint/constructor/enum.Lifecycle.html#variant.RequestScoped
[Lifecycle::Transient]: ../../../api_reference/pavex/blueprint/constructor/enum.Lifecycle.html#variant.Transient
[RequestHead]: ../../../api_reference/pavex/request/struct.RequestHead.html
[Blueprint]: ../../api_reference/pavex/blueprint/struct.Blueprint.html
[Blueprint::constructor]: ../../api_reference/pavex/blueprint/struct.Blueprint.html#method.constructor
[Blueprint::singleton]: ../../api_reference/pavex/blueprint/struct.Blueprint.html#method.singleton
[Blueprint::request_scoped]: ../../api_reference/pavex/blueprint/struct.Blueprint.html#method.request_scoped
[Blueprint::transient]: ../../api_reference/pavex/blueprint/struct.Blueprint.html#method.transient
[f]: ../../api_reference/pavex/macro.f.html
[Lifecycle::Singleton]: ../../api_reference/pavex/blueprint/constructor/enum.Lifecycle.html#variant.Singleton
[Lifecycle::RequestScoped]: ../../api_reference/pavex/blueprint/constructor/enum.Lifecycle.html#variant.RequestScoped
[Lifecycle::Transient]: ../../api_reference/pavex/blueprint/constructor/enum.Lifecycle.html#variant.Transient
[RequestHead]: ../../api_reference/pavex/request/struct.RequestHead.html
4 changes: 2 additions & 2 deletions docs/guide/dependency_injection/cookbook.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Cookbook

This cookbook contains a collection of reference examples for Pavex's dependency injection framework.
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.).

Expand Down Expand Up @@ -156,4 +156,4 @@ when registering the constructor.

[f!]: ../../api_reference/pavex/macro.f!.html
[unambiguous path]: #unambiguous-paths
[^ufcs]: Check out the [relevant RFC](https://github.com/rust-lang/rfcs/blob/master/text/0132-ufcs.md) if you're curious.
[^ufcs]: Check out the [relevant RFC](https://github.com/rust-lang/rfcs/blob/master/text/0132-ufcs.md) if you're curious.
Loading

0 comments on commit 00b1822

Please sign in to comment.