From f0aee87a00bd6107fec7d1ff7e3dd4665e5bb248 Mon Sep 17 00:00:00 2001 From: LukeMathWalker Date: Sun, 9 May 2021 18:36:31 +0100 Subject: [PATCH] Layering. --- src/routes/subscriptions.rs | 152 +++++++++++++++++++++++++++++++----- 1 file changed, 132 insertions(+), 20 deletions(-) diff --git a/src/routes/subscriptions.rs b/src/routes/subscriptions.rs index bacf8d0..b130154 100644 --- a/src/routes/subscriptions.rs +++ b/src/routes/subscriptions.rs @@ -1,7 +1,8 @@ use crate::domain::{NewSubscriber, SubscriberEmail, SubscriberName}; use crate::email_client::EmailClient; use crate::startup::ApplicationBaseUrl; -use actix_web::{web, HttpResponse}; +use actix_web::http::StatusCode; +use actix_web::{web, HttpResponse, ResponseError}; use chrono::Utc; use rand::distributions::Alphanumeric; use rand::{thread_rng, Rng}; @@ -25,6 +26,91 @@ impl TryInto for FormData { } } +pub enum SubscribeError { + ValidationError(String), + PoolError(sqlx::Error), + InsertSubscriberError(sqlx::Error), + StoreTokenError(StoreTokenError), + TransactionCommitError(sqlx::Error), + SendEmailError(reqwest::Error), +} + +impl From for SubscribeError { + fn from(e: reqwest::Error) -> Self { + Self::SendEmailError(e) + } +} + +impl From for SubscribeError { + fn from(e: StoreTokenError) -> Self { + Self::StoreTokenError(e) + } +} + +impl From for SubscribeError { + fn from(e: String) -> Self { + Self::ValidationError(e) + } +} + +impl std::fmt::Display for SubscribeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SubscribeError::ValidationError(e) => write!(f, "{}", e), + SubscribeError::PoolError(_) => { + write!(f, "Failed to acquire a Postgres connection from the pool") + } + SubscribeError::InsertSubscriberError(_) => { + write!(f, "Failed to insert new subscriber in the database.") + } + SubscribeError::StoreTokenError(_) => write!( + f, + "Failed to store the confirmation token for a new subscriber." + ), + SubscribeError::TransactionCommitError(_) => { + write!( + f, + "Failed to commit SQL transaction to store a new subscriber." + ) + } + SubscribeError::SendEmailError(_) => write!(f, "Failed to send a confirmation email."), + } + } +} + +impl std::fmt::Debug for SubscribeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + error_chain_fmt(self, f) + } +} + +impl std::error::Error for SubscribeError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + // &str does not implement `Error` - we consider it the root cause + SubscribeError::ValidationError(_) => None, + SubscribeError::PoolError(e) => Some(e), + SubscribeError::InsertSubscriberError(e) => Some(e), + SubscribeError::StoreTokenError(e) => Some(e), + SubscribeError::TransactionCommitError(e) => Some(e), + SubscribeError::SendEmailError(e) => Some(e), + } + } +} + +impl ResponseError for SubscribeError { + fn status_code(&self) -> StatusCode { + match self { + SubscribeError::ValidationError(_) => StatusCode::BAD_REQUEST, + SubscribeError::PoolError(_) + | SubscribeError::TransactionCommitError(_) + | SubscribeError::InsertSubscriberError(_) + | SubscribeError::StoreTokenError(_) + | SubscribeError::SendEmailError(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + #[tracing::instrument( name = "Adding a new subscriber", skip(form, pool, email_client, base_url), @@ -38,35 +124,25 @@ pub async fn subscribe( pool: web::Data, email_client: web::Data, base_url: web::Data, -) -> Result { - let new_subscriber = form - .0 - .try_into() - .map_err(|_| HttpResponse::BadRequest().finish())?; - let mut transaction = pool - .begin() - .await - .map_err(|_| HttpResponse::InternalServerError().finish())?; +) -> Result { + let new_subscriber = form.0.try_into()?; + let mut transaction = pool.begin().await.map_err(SubscribeError::PoolError)?; let subscriber_id = insert_subscriber(&mut transaction, &new_subscriber) .await - .map_err(|_| HttpResponse::InternalServerError().finish())?; - // We are swallowing the error for the time being. + .map_err(SubscribeError::InsertSubscriberError)?; let subscription_token = generate_subscription_token(); - store_token(&mut transaction, subscriber_id, &subscription_token) - .await - .map_err(|_| HttpResponse::InternalServerError().finish())?; + store_token(&mut transaction, subscriber_id, &subscription_token).await?; transaction .commit() .await - .map_err(|_| HttpResponse::InternalServerError().finish())?; + .map_err(SubscribeError::TransactionCommitError)?; send_confirmation_email( &email_client, new_subscriber, &base_url.0, &subscription_token, ) - .await - .map_err(|_| HttpResponse::InternalServerError().finish())?; + .await?; Ok(HttpResponse::Ok().finish()) } @@ -141,7 +217,7 @@ pub async fn store_token( transaction: &mut Transaction<'_, Postgres>, subscriber_id: Uuid, subscription_token: &str, -) -> Result<(), sqlx::Error> { +) -> Result<(), StoreTokenError> { sqlx::query!( r#" INSERT INTO subscription_tokens (subscription_token, subscriber_id) @@ -154,7 +230,43 @@ pub async fn store_token( .await .map_err(|e| { tracing::error!("Failed to execute query: {:?}", e); - e + StoreTokenError(e) })?; Ok(()) } + +pub struct StoreTokenError(sqlx::Error); + +impl std::error::Error for StoreTokenError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&self.0) + } +} + +impl std::fmt::Debug for StoreTokenError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + error_chain_fmt(self, f) + } +} + +impl std::fmt::Display for StoreTokenError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "A database failure was encountered while trying to store a subscription token." + ) + } +} + +fn error_chain_fmt( + e: &impl std::error::Error, + f: &mut std::fmt::Formatter<'_>, +) -> std::fmt::Result { + writeln!(f, "{}\n", e)?; + let mut current = e.source(); + while let Some(cause) = current { + writeln!(f, "Caused by:\n\t{}", cause)?; + current = cause.source(); + } + Ok(()) +}