feat: prepare email client usage

This commit is contained in:
Sandro Eiler 2024-02-21 11:18:44 +01:00
parent eecab50b55
commit 13db7853bd
12 changed files with 150 additions and 208 deletions

View file

@ -4,6 +4,8 @@ use sqlx::postgres::PgConnectOptions;
use sqlx::postgres::PgSslMode;
use sqlx::ConnectOptions;
use crate::domain::SubscriberEmail;
#[derive(serde::Deserialize)]
/// The setting collection.
///
@ -12,6 +14,19 @@ use sqlx::ConnectOptions;
pub struct Settings {
pub database: DatabaseSettings,
pub application: ApplicationSettings,
pub email_client: EmailClientSettings,
}
#[derive(serde::Deserialize)]
pub struct EmailClientSettings {
pub base_url: String,
pub sender_email: String,
}
impl EmailClientSettings {
pub fn sender(&self) -> Result<SubscriberEmail, String> {
SubscriberEmail::parse(self.sender_email.clone())
}
}
#[derive(serde::Deserialize)]

View file

@ -1,6 +1,6 @@
use validator::validate_email;
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct SubscriberEmail(String);
impl SubscriberEmail {

29
src/email_client.rs Normal file
View file

@ -0,0 +1,29 @@
use crate::domain::SubscriberEmail;
use reqwest::Client;
#[derive(Clone)]
pub struct EmailClient {
http_client: Client,
base_url: String,
sender: SubscriberEmail,
}
impl EmailClient {
pub fn new(base_url: String, sender: SubscriberEmail) -> Self {
Self {
http_client: Client::new(),
base_url,
sender,
}
}
pub async fn send_email(
&self,
recipient: SubscriberEmail,
subject: &str,
html_content: &str,
text_content: &str,
) -> Result<(), String> {
todo!()
}
}

View file

@ -4,6 +4,7 @@
pub mod configuration;
pub mod domain;
pub mod email_client;
pub mod routes;
pub mod startup;
pub mod telemetry;

View file

@ -1,4 +1,5 @@
use learn_axum::configuration::get_configuration;
use learn_axum::email_client::EmailClient;
use learn_axum::startup;
use learn_axum::telemetry::{get_subscriber, init_subscriber};
use sqlx::postgres::PgPoolOptions;
@ -20,5 +21,12 @@ async fn main() {
let connection_pool = PgPoolOptions::new()
.acquire_timeout(std::time::Duration::from_secs(2))
.connect_lazy_with(configuration.database.with_db());
startup::run(listener, connection_pool).await.unwrap();
let sender_email = configuration
.email_client
.sender()
.expect("Invalid sender email address.");
let email_client = EmailClient::new(configuration.email_client.base_url, sender_email);
startup::run(listener, connection_pool, email_client)
.await
.unwrap();
}

View file

@ -1,5 +1,7 @@
use crate::domain::SubscriberEmail;
use crate::domain::{NewSubscriber, SubscriberName};
use crate::email_client;
use crate::startup::AppState;
use axum::extract::State;
use axum::routing::post;
use axum::Form;
@ -31,21 +33,27 @@ impl TryFrom<FormData> for NewSubscriber {
#[tracing::instrument(
name = "Adding a new subscriber",
skip(form, pool),
skip(form, db_pool, email_client),
fields(
request_id = %Uuid::new_v4(),
subscriber_email = %form.email,
subscriber_name = %form.name
)
)]
pub async fn subscribe(State(pool): State<PgPool>, Form(form): Form<FormData>) -> Response {
pub async fn subscribe(
State(AppState {
db_pool,
email_client,
}): State<AppState>,
Form(form): Form<FormData>,
) -> Response {
let new_subscriber = match form.try_into() {
Ok(form) => form,
Err(_) => {
return (StatusCode::BAD_REQUEST, "Invalid subscription.").into_response();
}
};
match insert_subscriber(&pool, &new_subscriber).await {
match insert_subscriber(&db_pool, &new_subscriber).await {
Ok(_) => {
return (StatusCode::OK,).into_response();
}
@ -57,10 +65,10 @@ pub async fn subscribe(State(pool): State<PgPool>, Form(form): Form<FormData>) -
#[tracing::instrument(
name = "Saving new subscriber details in the database",
skip(new_subscriber, pool)
skip(new_subscriber, db_pool)
)]
pub async fn insert_subscriber(
pool: &PgPool,
db_pool: &PgPool,
new_subscriber: &NewSubscriber,
) -> Result<(), sqlx::Error> {
let _ = sqlx::query!(
@ -75,7 +83,7 @@ pub async fn insert_subscriber(
)
// We use `get_ref` to get an immutable reference to the `PgConnection`
// wrapped by `web::Data`.
.execute(pool)
.execute(db_pool)
.await
.map_err(|e| {
tracing::error!("Failed to execute query: {:?}", e);
@ -84,8 +92,8 @@ pub async fn insert_subscriber(
Ok(())
}
pub fn routes_subscriptions(pool: PgPool) -> Router {
pub fn routes_subscriptions(state: AppState) -> Router {
Router::new()
.route("/subscriptions", post(subscribe))
.with_state(pool)
.with_state(state)
}

View file

@ -1,3 +1,4 @@
use crate::email_client::EmailClient;
use axum::http::Request;
use axum::routing::IntoMakeService;
use axum::serve::Serve;
@ -13,6 +14,12 @@ use tower_http::{
use tracing::Level;
use uuid::Uuid;
#[derive(Clone)]
pub struct AppState {
pub db_pool: PgPool,
pub email_client: EmailClient,
}
// from https://docs.rs/tower-http/0.2.5/tower_http/request_id/index.html#using-uuids
#[derive(Clone)]
struct MakeRequestUuid;
@ -28,10 +35,14 @@ impl MakeRequestId for MakeRequestUuid {
/// API routing
///
/// * `connection`: The postgres connection pool
pub fn app(connection: PgPool) -> Router {
pub fn app(db_connection: PgPool, email_client: EmailClient) -> Router {
let state = AppState {
db_pool: db_connection.clone(),
email_client: email_client.clone(),
};
Router::new()
.merge(crate::routes::routes_health_check())
.merge(crate::routes::routes_subscriptions(connection.clone()))
.merge(crate::routes::routes_subscriptions(state))
.layer(
// from https://docs.rs/tower-http/0.2.5/tower_http/request_id/index.html#using-trace
ServiceBuilder::new()
@ -53,6 +64,11 @@ pub fn app(connection: PgPool) -> Router {
///
/// * `listener`: The TCP listener
/// * `connection`: The postgres connection pool
pub fn run(listener: TcpListener, connection: PgPool) -> Serve<IntoMakeService<Router>, Router> {
axum::serve(listener, app(connection).into_make_service())
/// * `email_client`: The email client
pub fn run(
listener: TcpListener,
connection: PgPool,
email_client: EmailClient,
) -> Serve<IntoMakeService<Router>, Router> {
axum::serve(listener, app(connection, email_client).into_make_service())
}