-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
3da7e85
commit 57100c1
Showing
7 changed files
with
367 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
{{#include links.md}} | ||
|
||
# Best Practices | ||
|
||
To make it easy to test an application, it is recommended that you expose a function that configures the default set of services. This will make it simple to use the same default configuration as the application and replace only the parts that are necessary for testing. | ||
|
||
If a service can be replaced, then it should be registered using [`try_add`]. A service can still be replaced after it has been registered, but [`try_add`] will skip the process altogether if the service has already been registered. | ||
|
||
```rust | ||
use crate::*; | ||
use axum::{ | ||
async_trait, | ||
extract::Path, | ||
http::StatusCode, | ||
response::{IntoResponse, Response}, | ||
routing::{get, post}, | ||
Json, Router, | ||
}; | ||
use serde::{Deserialize, Serialize}; | ||
use serde_json::json; | ||
use tokio::net::TcpListener; | ||
use uuid::Uuid; | ||
|
||
// provide a function that can be called with the expected set of services | ||
fn add_default_services(services: &mut ServiceCollection) { | ||
services.try_add(ExampleUserRepo::scoped()); | ||
} | ||
|
||
// provide a function that can build a router representing the application | ||
fn build_app(services: ServiceCollection) -> Router { | ||
Router::new() | ||
.route("/users/:id", get(one_user)) | ||
.route("/users", post(new_user)) | ||
.with_provider(services.build_provider().unwrap()) | ||
} | ||
|
||
#[tokio::main] | ||
async fn main() { | ||
let mut services = ServiceCollection::new(); | ||
|
||
add_default_services(&mut services); | ||
|
||
let app = build_app(services); | ||
let listener = TcpListener::bind("127.0.0.1:3000").await.unwrap(); | ||
|
||
println!("listening on: {}", listener.local_addr().unwrap()); | ||
|
||
axum::serve(listener, app).await.unwrap(); | ||
} | ||
|
||
#[async_trait] | ||
trait UserRepo { | ||
async fn find(&self, user_id: Uuid) -> Result<User, UserRepoError>; | ||
async fn create(&self, params: CreateUser) -> Result<User, UserRepoError>; | ||
} | ||
|
||
#[injectable(UserRepo + Send + Sync)] | ||
struct ExampleUserRepo; | ||
|
||
#[async_trait] | ||
impl UserRepo for ExampleUserRepo { | ||
async fn find(&self, _user_id: Uuid) -> Result<User, UserRepoError> { | ||
unimplemented!() | ||
} | ||
|
||
async fn create(&self, _params: CreateUser) -> Result<User, UserRepoError> { | ||
unimplemented!() | ||
} | ||
} | ||
|
||
async fn one_user( | ||
Path(id): Path<Uuid>, | ||
Inject(repo): Inject<dyn UserRepo + Send + Sync>, | ||
) -> Result<Json<User>, AppError> { | ||
let user = repo.find(user_id).await?; | ||
Ok(user.into()) | ||
} | ||
|
||
async fn new_user( | ||
Inject(repo): Inject<dyn UserRepo + Send + Sync>, | ||
Json(params): Json<CreateUser>, | ||
) -> Result<Json<User>, AppError> { | ||
let user = repo.create(params).await?; | ||
Ok(user.into()) | ||
} | ||
``` | ||
|
||
You can now easily test your application by replacing on the only necessary services. In the following test, we: | ||
|
||
1. Create a `TestUserRepo` to simulate the behavior of a `dyn UserRepo` | ||
2. Register `TestUserRepo` in a new `ServiceCollection` | ||
3. Register all other default services | ||
- Since `dyn UserRepo` has been registered as `TestUserRepo` and [`try_add`] was used, the default registration is skipped | ||
4. Create a `Router` representing the application | ||
5. Run the application with a test client | ||
6. Invoke the HTTP `GET` method to return a single `User` | ||
|
||
```rust | ||
use super::*; | ||
use crate::*; | ||
use di::*; | ||
|
||
#[tokio::test] | ||
async fn get_should_return_user() { | ||
// arrange | ||
#[injectable(UserRepo + Send + Sync)] | ||
struct TestUserRepo; | ||
|
||
#[async_trait] | ||
impl UserRepo for TestUserRepo { | ||
async fn find(&self, _user_id: Uuid) -> Result<User, UserRepoError> { | ||
Ok(User::default()) | ||
} | ||
|
||
async fn create(&self, _params: CreateUser) -> Result<User, UserRepoError> { | ||
unimplemented!() | ||
} | ||
} | ||
|
||
let mut services = ServiceCollection::new(); | ||
|
||
services.add(TestUserRepo::scoped()); | ||
add_default_services(services); | ||
|
||
let app = build_app(services); | ||
let client = TestClient::new(app); | ||
|
||
// act | ||
let response = client.get("/user/b51565c273c04bb4ac179232c90b20af").send().await; | ||
|
||
// assert | ||
assert_eq!(response.status(), StatusCode::OK); | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
<!-- | ||
This file contains links that can be shared across pages. RustDoc links cannot currently | ||
be used by mdBook directly. Links are stable on crates.io so we can centralize what is | ||
required in this file, albeit manually. | ||
REF: https://github.com/rust-lang/mdBook/issues/1356 | ||
REF: https://github.com/rust-lang/cargo/issues/739 | ||
REF: https://github.com/tag1consulting/goose/issues/320 | ||
--> | ||
|
||
[`ServiceProvider`]: https://docs.rs/more-di/3.1.0/di/struct.ServiceProvider.html | ||
[`try_add`]: https://docs.rs/more-di/3.1.0/di/struct.ServiceCollection.html#method.try_add | ||
[`get`]: https://docs.rs/more-di/3.1.0/di/struct.ServiceProvider.html#method.get | ||
[`get_mut`]: https://docs.rs/more-di/3.1.0/di/struct.ServiceProvider.html#method.get_mut | ||
[`get_by_key`]: https://docs.rs/more-di/3.1.0/di/struct.ServiceProvider.html#method.get_by_key | ||
[`get_by_key_mut`]: https://docs.rs/more-di/3.1.0/di/struct.ServiceProvider.html#method.get_by_key_mut | ||
[`get_all`]: https://docs.rs/more-di/3.1.0/di/struct.ServiceProvider.html#method.get_all | ||
[`get_all_mut`]: https://docs.rs/more-di/3.1.0/di/struct.ServiceProvider.html#method.get_all_mut | ||
[`get_all_by_key`]: https://docs.rs/more-di/3.1.0/di/struct.ServiceProvider.html#method.get_all_by_key | ||
[`get_all_by_key_mut`]: https://docs.rs/more-di/3.1.0/di/struct.ServiceProvider.html#method.get_all_by_key_mut | ||
[`get_required`]: https://docs.rs/more-di/3.1.0/di/struct.ServiceProvider.html#method.get_required | ||
[`get_required_mut`]: https://docs.rs/more-di/3.1.0/di/struct.ServiceProvider.html#method.get_required_mut | ||
[`get_required_by_key`]: https://docs.rs/more-di/3.1.0/di/struct.ServiceProvider.html#method.get_required_by_key | ||
[`get_required_by_key_mut`]: https://docs.rs/more-di/3.1.0/di/struct.ServiceProvider.html#method.get_required_by_key_mut |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
# Service Registration | ||
|
||
`axum` handlers execute in an asynchronous context. This requires that an injected service must be thread-safe. `axum` imposes that an such service must implement `Send` and `Sync`. | ||
Most structures will already satisfy this requirement and is generated by the compiler. If it doesn't, then the struct will have to be wrapped by another struct that satisfies this | ||
requirement. A trait, on the other hand, has several options and depends on the trait definition itself. The method you chose to use is largely based on your preference. | ||
|
||
## Thread-Safe Trait | ||
|
||
A trait declares it is thread-safe if it requires implementing `Send` and `Sync`. | ||
|
||
```rust | ||
trait Service: Send + Sync {} | ||
|
||
#[injectable(Service)] | ||
struct ServiceImpl; | ||
|
||
impl Service for ServiceImpl {} | ||
|
||
async fn handler(Inject(service): Inject<dyn Service>) {} | ||
``` | ||
|
||
## Multiple Trait Implementation | ||
|
||
If the original trait does not declare thread safety with `Send` and `Sync`, then a struct implementation can directly specify that it is thread-safe. | ||
|
||
```rust | ||
trait Service {} | ||
|
||
#[injectable(Service + Send + Sync)] | ||
struct ServiceImpl; | ||
|
||
impl Service for ServiceImpl {} | ||
|
||
async fn handler(Inject(service): Inject<dyn Service + Send + Sync>) {} | ||
``` | ||
|
||
## Trait Unification | ||
|
||
If the original trait does not declare thread safety with `Send` and `Sync`, another alterative is to unify the trait with `Send` and `Sync` in a new trait. | ||
You might chose this approach for better usage ergonomics. | ||
|
||
```rust | ||
trait Service {} | ||
|
||
trait ServiceAsync: Service + Send + Sync {} | ||
|
||
#[injectable(ServiceAsync)] | ||
struct ServiceImpl; | ||
|
||
impl Service for ServiceImpl {} | ||
impl ServiceAsync for ServiceImpl {} | ||
|
||
async fn handler(Inject(service): Inject<dyn ServiceAsync>) {} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
{{#include links.md}} | ||
|
||
# Service Resolution | ||
|
||
Services are resolved and injected using the functions provided by [`ServiceProvider`]. A new _scope_ is created during each | ||
HTTP request before the handler is executed. | ||
|
||
| Extactor | Function | | ||
| ------------------ | --------------------------- | | ||
| `TryInject` | [`get`] | | ||
| `TryInjectMut` | [`get_mut`] | | ||
| `TryInjectWithKey` | [`get_by_key`] | | ||
| `InjectWithKeyMut` | [`get_by_key_mut`] | | ||
| `InjectAll` | [`get_all`] | | ||
| `InjectAllMut` | [`get_all_mut`] | | ||
| `InjectAllWithKey` | [`get_all_by_key`] | | ||
| `InjectWithKeyMut` | [`get_all_by_key_mut`] | | ||
| `Inject` | [`get_required`] | | ||
| `InjectMut` | [`get_required_mut`] | | ||
| `InjectWithKey` | [`get_required_by_key`] | | ||
| `InjectWithKeyMut` | [`get_required_by_key_mut`] | | ||
|
||
|
||
If resolution fails, the HTTP request will short-circuit with HTTP status code 500 - Internal Server Error. |