Parse data from incoming request
This commit is contained in:
7
src/domain.rs
Normal file
7
src/domain.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
mod new_subscriber;
|
||||
mod subscriber_email;
|
||||
mod subscriber_name;
|
||||
|
||||
pub use new_subscriber::NewSubscriber;
|
||||
pub use subscriber_email::SubscriberEmail;
|
||||
pub use subscriber_name::SubscriberName;
|
||||
6
src/domain/new_subscriber.rs
Normal file
6
src/domain/new_subscriber.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
use crate::domain::{SubscriberName, subscriber_email::SubscriberEmail};
|
||||
|
||||
pub struct NewSubscriber {
|
||||
pub email: SubscriberEmail,
|
||||
pub name: SubscriberName,
|
||||
}
|
||||
67
src/domain/subscriber_email.rs
Normal file
67
src/domain/subscriber_email.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use validator::Validate;
|
||||
|
||||
#[derive(Debug, Validate)]
|
||||
pub struct SubscriberEmail {
|
||||
#[validate(email)]
|
||||
email: String,
|
||||
}
|
||||
|
||||
impl SubscriberEmail {
|
||||
pub fn parse(email: String) -> Result<Self, String> {
|
||||
let subscriber_email = SubscriberEmail { email };
|
||||
subscriber_email
|
||||
.validate()
|
||||
.map_err(|_| format!("{} is not a valid email.", subscriber_email.email))?;
|
||||
Ok(subscriber_email)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for SubscriberEmail {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.email.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::SubscriberEmail;
|
||||
use claims::assert_err;
|
||||
use fake::Fake;
|
||||
use fake::faker::internet::en::SafeEmail;
|
||||
use fake::rand::SeedableRng;
|
||||
use fake::rand::rngs::StdRng;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct ValidEmailFixture(pub String);
|
||||
|
||||
impl quickcheck::Arbitrary for ValidEmailFixture {
|
||||
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
|
||||
let mut rng = StdRng::seed_from_u64(u64::arbitrary(g));
|
||||
let email = SafeEmail().fake_with_rng(&mut rng);
|
||||
Self(email)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_string_is_rejected() {
|
||||
let email = "".to_string();
|
||||
assert_err!(SubscriberEmail::parse(email));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn email_missing_at_symbol_is_rejected() {
|
||||
let email = "alphonse.paixoutlook.com".to_string();
|
||||
assert_err!(SubscriberEmail::parse(email));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn email_missing_subject_is_rejected() {
|
||||
let email = "@outlook.com".to_string();
|
||||
assert_err!(SubscriberEmail::parse(email));
|
||||
}
|
||||
|
||||
#[quickcheck_macros::quickcheck]
|
||||
fn valid_emails_are_parsed_successfully(valid_email: ValidEmailFixture) -> bool {
|
||||
SubscriberEmail::parse(dbg!(valid_email.0)).is_ok()
|
||||
}
|
||||
}
|
||||
69
src/domain/subscriber_name.rs
Normal file
69
src/domain/subscriber_name.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SubscriberName(String);
|
||||
|
||||
impl SubscriberName {
|
||||
pub fn parse(s: String) -> Result<Self, String> {
|
||||
let is_empty_or_whitespace = s.trim().is_empty();
|
||||
let is_too_long = s.graphemes(true).count() > 256;
|
||||
let forbidden_characters = ['/', '(', ')', '"', '<', '>', '\\', '{', '}'];
|
||||
let contains_forbidden_characters = s.chars().any(|g| forbidden_characters.contains(&g));
|
||||
|
||||
if is_empty_or_whitespace || is_too_long || contains_forbidden_characters {
|
||||
Err(format!("{} is not a valid subscriber name.", s))
|
||||
} else {
|
||||
Ok(Self(s))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for SubscriberName {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::domain::SubscriberName;
|
||||
use claims::{assert_err, assert_ok};
|
||||
|
||||
#[test]
|
||||
fn a_256_grapheme_long_name_is_valid() {
|
||||
let name = "ê".repeat(256);
|
||||
assert_ok!(SubscriberName::parse(name));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_name_longer_than_256_graphemes_is_rejected() {
|
||||
let name = "ê".repeat(257);
|
||||
assert_err!(SubscriberName::parse(name));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_whitespace_only_name_is_rejected() {
|
||||
let name = "\n \t ".to_string();
|
||||
assert_err!(SubscriberName::parse(name));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_string_is_rejected() {
|
||||
let name = "".to_string();
|
||||
assert_err!(SubscriberName::parse(name));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_name_containing_invalid_character_is_rejected() {
|
||||
for name in ['/', '(', ')', '"', '<', '>', '\\', '{', '}'] {
|
||||
let name = name.to_string();
|
||||
assert_err!(SubscriberName::parse(name));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_valid_name_is_parsed_successfully() {
|
||||
let name = "Alphonse".to_string();
|
||||
assert_ok!(SubscriberName::parse(name));
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod configuration;
|
||||
pub mod domain;
|
||||
pub mod routes;
|
||||
pub mod startup;
|
||||
pub mod telemetry;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::domain::{NewSubscriber, SubscriberEmail, SubscriberName};
|
||||
use axum::{Form, extract::State, http::StatusCode, response::IntoResponse};
|
||||
use chrono::Utc;
|
||||
use serde::Deserialize;
|
||||
@@ -14,9 +15,16 @@ use uuid::Uuid;
|
||||
)]
|
||||
pub async fn subscribe(
|
||||
State(connection): State<PgPool>,
|
||||
form: Form<FormData>,
|
||||
Form(form): Form<FormData>,
|
||||
) -> impl IntoResponse {
|
||||
if insert_subscriber(&connection, &form).await.is_err() {
|
||||
let new_subscriber = match form.try_into() {
|
||||
Ok(subscriber) => subscriber,
|
||||
Err(_) => return StatusCode::BAD_REQUEST,
|
||||
};
|
||||
if insert_subscriber(&connection, &new_subscriber)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
} else {
|
||||
StatusCode::OK
|
||||
@@ -25,11 +33,11 @@ pub async fn subscribe(
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "Saving new subscriber details in the database",
|
||||
skip(connection, form)
|
||||
skip(connection, new_subscriber)
|
||||
)]
|
||||
pub async fn insert_subscriber(
|
||||
connection: &PgPool,
|
||||
form: &Form<FormData>,
|
||||
new_subscriber: &NewSubscriber,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
@@ -37,8 +45,8 @@ pub async fn insert_subscriber(
|
||||
VALUES ($1, $2, $3, $4);
|
||||
"#,
|
||||
Uuid::new_v4(),
|
||||
form.email,
|
||||
form.name,
|
||||
new_subscriber.email.as_ref(),
|
||||
new_subscriber.name.as_ref(),
|
||||
Utc::now()
|
||||
)
|
||||
.execute(connection)
|
||||
@@ -56,3 +64,13 @@ pub struct FormData {
|
||||
name: String,
|
||||
email: String,
|
||||
}
|
||||
|
||||
impl TryFrom<FormData> for NewSubscriber {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: FormData) -> Result<Self, Self::Error> {
|
||||
let name = SubscriberName::parse(value.name)?;
|
||||
let email = SubscriberEmail::parse(value.email)?;
|
||||
Ok(Self { name, email })
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user