From 6a16cd40ca1d34f3391b8fbed9a2aa976bc8dc2a Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Sat, 19 Jun 2021 12:34:42 +0200 Subject: [PATCH] Add error handling and dependency injection example (#23) --- Cargo.toml | 2 +- ...error_handling_and_dependency_injection.rs | 146 ++++++++++++++++++ src/response.rs | 12 ++ 3 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 examples/error_handling_and_dependency_injection.rs diff --git a/Cargo.toml b/Cargo.toml index c4303170..c22b22d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,7 +51,7 @@ tokio = { version = "1.6.1", features = ["macros", "rt", "rt-multi-thread"] } tokio-postgres = "0.7.2" tracing = "0.1" tracing-subscriber = "0.2" -uuid = "0.8" +uuid = { version = "0.8", features = ["serde"] } [dev-dependencies.tower] version = "0.4" diff --git a/examples/error_handling_and_dependency_injection.rs b/examples/error_handling_and_dependency_injection.rs new file mode 100644 index 00000000..9bbac60b --- /dev/null +++ b/examples/error_handling_and_dependency_injection.rs @@ -0,0 +1,146 @@ +//! Example showing how to convert errors into responses and how one might do +//! dependency injection using trait objects. + +#![allow(dead_code)] + +use awebframework::{ + async_trait, + extract::{Extension, Json, UrlParams}, + prelude::*, + response::IntoResponse, + AddExtensionLayer, +}; +use http::StatusCode; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use std::{net::SocketAddr, sync::Arc}; +use tower_http::trace::TraceLayer; +use uuid::Uuid; + +#[tokio::main] +async fn main() { + tracing_subscriber::fmt::init(); + + // Inject a `UserRepo` into our handlers via a trait object. This could be + // the live implementation or just a mock for testing. + let user_repo = Arc::new(ExampleUserRepo) as DynUserRepo; + + // Build our application with some routes + let app = route("/users/:id", get(users_show)) + .route("/users", post(users_create)) + // Add our `user_repo` to all request's extensions so handlers can access + // it. + .layer(AddExtensionLayer::new(user_repo)) + // Add tracing because why not. + .layer(TraceLayer::new_for_http()); + + // Run our application + let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); + tracing::debug!("listening on {}", addr); + app.serve(&addr).await.unwrap(); +} + +/// Handler for `GET /users/:id`. +/// +/// Extracts the user repo from request extensions and calls it. `UserRepoError`s +/// are automatically converted into `AppError` which implements `IntoResponse` +/// so it can be returned from handlers directly. +async fn users_show( + UrlParams((user_id,)): UrlParams<(Uuid,)>, + Extension(user_repo): Extension, +) -> Result, AppError> { + let user = user_repo.find(user_id).await?; + + Ok(user.into()) +} + +/// Handler for `POST /users`. +async fn users_create( + Json(params): Json, + Extension(user_repo): Extension, +) -> Result, AppError> { + let user = user_repo.create(params).await?; + + Ok(user.into()) +} + +/// Our app's top level error type. +enum AppError { + /// Something went wrong when calling the user repo. + UserRepo(UserRepoError), +} + +/// This makes it possible to use `?` to automatically convert a `UserRepoError` +/// into an `AppError`. +impl From for AppError { + fn from(inner: UserRepoError) -> Self { + AppError::UserRepo(inner) + } +} + +impl IntoResponse for AppError { + fn into_response(self) -> http::Response { + let (status, error_json) = match self { + AppError::UserRepo(UserRepoError::NotFound) => { + (StatusCode::NOT_FOUND, json!("User not found")) + } + AppError::UserRepo(UserRepoError::InvalidUsername) => { + (StatusCode::UNPROCESSABLE_ENTITY, json!("Invalid username")) + } + }; + + let mut response = response::Json(json!({ + "error": error_json, + })) + .into_response(); + + *response.status_mut() = status; + + response + } +} + +/// Example implementation of `UserRepo`. +struct ExampleUserRepo; + +#[async_trait] +impl UserRepo for ExampleUserRepo { + async fn find(&self, _user_id: Uuid) -> Result { + unimplemented!() + } + + async fn create(&self, _params: CreateUser) -> Result { + unimplemented!() + } +} + +/// Type alias that makes it easier to extract `UserRepo` trait objects. +type DynUserRepo = Arc; + +/// A trait that defines things a user repo might support. +#[async_trait] +trait UserRepo { + /// Loop up a user by their id. + async fn find(&self, user_id: Uuid) -> Result; + + /// Create a new user. + async fn create(&self, params: CreateUser) -> Result; +} + +#[derive(Debug, Serialize)] +struct User { + id: Uuid, + username: String, +} + +#[derive(Debug, Deserialize)] +struct CreateUser { + username: String, +} + +/// Errors that can happen when using the user repo. +#[derive(Debug)] +enum UserRepoError { + NotFound, + InvalidUsername, +} diff --git a/src/response.rs b/src/response.rs index 4fcd46a0..9d0a01e0 100644 --- a/src/response.rs +++ b/src/response.rs @@ -188,6 +188,12 @@ where } } +impl From for Html { + fn from(inner: T) -> Self { + Self(inner) + } +} + /// A JSON response. /// /// Can be created from any type that implements [`serde::Serialize`]. @@ -238,3 +244,9 @@ where res } } + +impl From for Json { + fn from(inner: T) -> Self { + Self(inner) + } +}