Merge chapter 6, part 1

This commit is contained in:
LukeMathWalker 2021-01-03 18:06:10 +00:00
commit 7fdb2151e0
8 changed files with 490 additions and 164 deletions

536
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -29,8 +29,12 @@ tracing-log = "0.1.1"
tracing-actix-web = "0.2.0"
serde-aux = "1.0.1"
unicode-segmentation = "1.7.1"
validator = "0.12.0"
quickcheck_macros = "0.9.1"
[dev-dependencies]
reqwest = { version = "0.10.7", features = ["json"] }
lazy_static = "1.4.0"
claim = "0.4.0"
quickcheck = "0.9.2"
fake = "2.3.0"

View file

@ -6,9 +6,10 @@
This repository serves as supplementary material for [the book](https://zero2prod.com/): it hosts snapshots of the codebase of our email newsletter project at end of each chapter.
<<<<<<< HEAD
## Chapter snapshots
The `master` branch (where you are right now!) shows the project at the end of the last published chapter _(Chapter 6, right now)_.
The `master` branch (where you are right now!) shows the project at the end of the last published chapter _(Chapter 6 Part 1, right now)_.
You can browse the project at end of previous chapters by switching to their dedicated branches:
@ -16,7 +17,8 @@ You can browse the project at end of previous chapters by switching to their ded
- [Chapter 3, Part 1](https://github.com/LukeMathWalker/zero-to-production/tree/root-chapter-03-part1)
- [Chapter 4](https://github.com/LukeMathWalker/zero-to-production/tree/root-chapter-04)
- [Chapter 5](https://github.com/LukeMathWalker/zero-to-production/tree/root-chapter-05)
- [Chapter 6](https://github.com/LukeMathWalker/zero-to-production/tree/root-chapter-06)
- [Chapter 6, Part 0](https://github.com/LukeMathWalker/zero-to-production/tree/root-chapter-06)
- [Chapter 6, Part 1](https://github.com/LukeMathWalker/zero-to-production/tree/root-chapter-06-part1)
## Pre-requisite

7
src/domain/mod.rs Normal file
View 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;

View file

@ -0,0 +1,8 @@
use crate::domain::subscriber_name::SubscriberName;
use crate::domain::subscriber_email::SubscriberEmail;
pub struct NewSubscriber {
// We are not using `String` anymore!
pub email: SubscriberEmail,
pub name: SubscriberName,
}

View file

@ -0,0 +1,61 @@
use validator::validate_email;
#[derive(Debug)]
pub struct SubscriberEmail(String);
impl SubscriberEmail {
pub fn parse(s: String) -> Result<SubscriberEmail, String> {
if validate_email(&s) {
Ok(Self(s))
} else {
Err(format!("{} is not a valid subscriber email.", s))
}
}
}
impl AsRef<str> for SubscriberEmail {
fn as_ref(&self) -> &str {
&self.0
}
}
#[cfg(test)]
mod tests {
use super::SubscriberEmail;
use claim::assert_err;
use fake::Fake;
use fake::faker::internet::en::SafeEmail;
#[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 = "ursuladomain.com".to_string();
assert_err!(SubscriberEmail::parse(email));
}
#[test]
fn email_missing_subject_is_rejected() {
let email = "@domain.com".to_string();
assert_err!(SubscriberEmail::parse(email));
}
#[derive(Debug, Clone)]
struct ValidEmailFixture(pub String);
impl quickcheck::Arbitrary for ValidEmailFixture {
fn arbitrary<G: quickcheck::Gen>(g: &mut G) -> Self {
let email = SafeEmail().fake_with_rng(g);
Self(email)
}
}
#[quickcheck_macros::quickcheck]
fn valid_emails_are_parsed_successfully(valid_email: ValidEmailFixture) -> bool {
SubscriberEmail::parse(valid_email.0).is_ok()
}
}

View file

@ -1,16 +1,11 @@
use unicode_segmentation::UnicodeSegmentation;
pub struct NewSubscriber {
pub email: String,
pub name: SubscriberName,
}
#[derive(Debug)]
pub struct SubscriberName(String);
impl SubscriberName {
/// Returns an instance of `SubscriberName` if the input satisfies all
/// our validation constraints on subscriber names.
/// our validation constraints on subscriber names.
/// It panics otherwise.
pub fn parse(s: String) -> Result<SubscriberName, String> {
// `.trim()` returns a view over the input `s` without trailing

View file

@ -1,7 +1,8 @@
use crate::domain::{NewSubscriber, SubscriberName};
use crate::domain::{NewSubscriber, SubscriberEmail, SubscriberName};
use actix_web::{web, HttpResponse};
use chrono::Utc;
use sqlx::PgPool;
use std::convert::TryInto;
use uuid::Uuid;
#[derive(serde::Deserialize)]
@ -10,6 +11,16 @@ pub struct FormData {
name: String,
}
impl TryInto<NewSubscriber> for FormData {
type Error = String;
fn try_into(self) -> Result<NewSubscriber, Self::Error> {
let name = SubscriberName::parse(self.name)?;
let email = SubscriberEmail::parse(self.email)?;
Ok(NewSubscriber { email, name })
}
}
#[tracing::instrument(
name = "Adding a new subscriber",
skip(form, pool),
@ -22,12 +33,10 @@ pub async fn subscribe(
form: web::Form<FormData>,
pool: web::Data<PgPool>,
) -> Result<HttpResponse, HttpResponse> {
let name =
SubscriberName::parse(form.0.name).map_err(|_| HttpResponse::BadRequest().finish())?;
let new_subscriber = NewSubscriber {
email: form.0.email,
name,
};
let new_subscriber = form
.0
.try_into()
.map_err(|_| HttpResponse::BadRequest().finish())?;
insert_subscriber(&pool, &new_subscriber)
.await
.map_err(|_| HttpResponse::InternalServerError().finish())?;
@ -48,7 +57,7 @@ pub async fn insert_subscriber(
VALUES ($1, $2, $3, $4)
"#,
Uuid::new_v4(),
new_subscriber.email,
new_subscriber.email.as_ref(),
new_subscriber.name.as_ref(),
Utc::now()
)