mirror of
https://github.com/LukeMathWalker/zero-to-production.git
synced 2025-03-06 19:31:40 +00:00
Merge chapter 6, part 1
This commit is contained in:
commit
7fdb2151e0
8 changed files with 490 additions and 164 deletions
536
Cargo.lock
generated
536
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -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"
|
||||
|
|
|
@ -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
7
src/domain/mod.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;
|
8
src/domain/new_subscriber.rs
Normal file
8
src/domain/new_subscriber.rs
Normal 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,
|
||||
}
|
61
src/domain/subscriber_email.rs
Normal file
61
src/domain/subscriber_email.rs
Normal 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()
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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()
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue