mirror of
https://github.com/LukeMathWalker/zero-to-production.git
synced 2024-12-18 05:56:35 +00:00
Merge
This commit is contained in:
commit
518db87ced
7 changed files with 181 additions and 31 deletions
57
Cargo.lock
generated
57
Cargo.lock
generated
|
@ -278,9 +278,9 @@ checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e"
|
|||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.5.7"
|
||||
version = "0.5.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad4243ec6feddc812c0f442d9765374d250aba94e10ecf8b632e1b1c118547e8"
|
||||
checksum = "19fac972e53443ba111b1ff866e9d3b6484df5c05030e13bc7c6a1ebc802e983"
|
||||
dependencies = [
|
||||
"getrandom 0.2.0",
|
||||
"lazy_static",
|
||||
|
@ -503,6 +503,15 @@ dependencies = [
|
|||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "claim"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68ad37958d55b29a7088909368968d2fe876a24c203f8441195130f3b15194b9"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "config"
|
||||
version = "0.10.1"
|
||||
|
@ -527,9 +536,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "const_fn"
|
||||
version = "0.4.3"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c478836e029dcef17fb47c89023448c64f781a046e0300e257ad8225ae59afab"
|
||||
checksum = "cd51eab21ab4fd6a3bf889e2d0958c0a6e3a61ad04260325e919e652a2a62826"
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
|
@ -1172,9 +1181,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.80"
|
||||
version = "0.2.81"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614"
|
||||
checksum = "1482821306169ec4d07f6aca392a4681f66c75c9918aa49641a2595db64053cb"
|
||||
|
||||
[[package]]
|
||||
name = "linked-hash-map"
|
||||
|
@ -1360,9 +1369,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "net2"
|
||||
version = "0.2.36"
|
||||
version = "0.2.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7cf75f38f16cb05ea017784dc6dbfd354f76c223dba37701734c4f5a9337d02"
|
||||
checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae"
|
||||
dependencies = [
|
||||
"cfg-if 0.1.10",
|
||||
"libc",
|
||||
|
@ -1441,12 +1450,12 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
|||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.30"
|
||||
version = "0.10.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d575eff3665419f9b83678ff2815858ad9d11567e082f5ac1814baba4e2bcb4"
|
||||
checksum = "8d008f51b1acffa0d3450a68606e6a51c123012edaacb0f4e1426bd978869187"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if 0.1.10",
|
||||
"cfg-if 1.0.0",
|
||||
"foreign-types",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
|
@ -1461,9 +1470,9 @@ checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de"
|
|||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.58"
|
||||
version = "0.9.59"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a842db4709b604f0fe5d1170ae3565899be2ad3d9cbc72dedc789ac0511f78de"
|
||||
checksum = "de52d8eabd217311538a39bba130d7dea1f1e118010fee7a033d966845e7d5fe"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"cc",
|
||||
|
@ -1867,9 +1876,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
|
|||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.117"
|
||||
version = "1.0.118"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a"
|
||||
checksum = "06c64263859d87aa2eb554587e2d23183398d617427327cf2b3d0ed8c69e4800"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
@ -1887,9 +1896,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.117"
|
||||
version = "1.0.118"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e"
|
||||
checksum = "c84d3526699cd55261af4b941e4e725444df67aa4f9e6a3564f18030d12672df"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -2190,9 +2199,9 @@ checksum = "343f3f510c2915908f155e94f17220b19ccfacf2a64a2a5d8004f2c3e311e7fd"
|
|||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.53"
|
||||
version = "1.0.54"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8833e20724c24de12bbaba5ad230ea61c3eafb05b881c7c9d3cfe8638b187e68"
|
||||
checksum = "9a2af957a63d6bd42255c359c93d9bfdb97076bd3b820897ce55ffbfbf107f44"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -2323,9 +2332,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
|
|||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "0.2.23"
|
||||
version = "0.2.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6d7ad61edd59bfcc7e80dababf0f4aed2e6d5e0ba1659356ae889752dfc12ff"
|
||||
checksum = "099837d3464c16a808060bb3f02263b412f6fafcb5d01c533d309985fbeebe48"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
|
@ -2639,9 +2648,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.10"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c"
|
||||
checksum = "b00bca6106a5e23f3eee943593759b7fcddb00554332e856d990c893966879fb"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
|
@ -2888,6 +2897,7 @@ dependencies = [
|
|||
"actix-rt",
|
||||
"actix-web",
|
||||
"chrono",
|
||||
"claim",
|
||||
"config",
|
||||
"lazy_static",
|
||||
"reqwest",
|
||||
|
@ -2901,5 +2911,6 @@ dependencies = [
|
|||
"tracing-futures",
|
||||
"tracing-log",
|
||||
"tracing-subscriber",
|
||||
"unicode-segmentation",
|
||||
"uuid",
|
||||
]
|
||||
|
|
|
@ -28,7 +28,9 @@ tracing-bunyan-formatter = "0.1.6"
|
|||
tracing-log = "0.1.1"
|
||||
tracing-actix-web = "0.2.0"
|
||||
serde-aux = "1.0.1"
|
||||
unicode-segmentation = "1.7.1"
|
||||
|
||||
[dev-dependencies]
|
||||
reqwest = { version = "0.10.7", features = ["json"] }
|
||||
lazy_static = "1.4.0"
|
||||
claim = "0.4.0"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Zero To Production / Code
|
||||
# Zero To Production / Code (Chapter 5)
|
||||
|
||||
<div align="center"><a href="https://zero2prod.com" target="_blank"><img src="https://static-2.gumroad.com/res/gumroad/3629854790655/asset_previews/bc9026cad3ece1746327c1d70218f602/retina/rsz_zero_to_production_punk.png" /></a></div>
|
||||
|
||||
|
@ -8,7 +8,7 @@ This repository serves as supplementary material for [the book](https://zero2pro
|
|||
|
||||
## Chapter snapshots
|
||||
|
||||
The `master` branch (where you are right now!) shows the project at the end of the last published chapter _(Chapter 5, right now)_.
|
||||
The `master` branch (where you are right now!) shows the project at the end of the last published chapter _(Chapter 6, right now)_.
|
||||
|
||||
You can browse the project at end of previous chapters by switching to their dedicated branches:
|
||||
|
||||
|
@ -16,6 +16,7 @@ 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 5](https://github.com/LukeMathWalker/zero-to-production/tree/root-chapter-06)
|
||||
|
||||
## Pre-requisite
|
||||
|
||||
|
|
95
src/domain.rs
Normal file
95
src/domain.rs
Normal file
|
@ -0,0 +1,95 @@
|
|||
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.
|
||||
/// It panics otherwise.
|
||||
pub fn parse(s: String) -> Result<SubscriberName, String> {
|
||||
// `.trim()` returns a view over the input `s` without trailing
|
||||
// whitespace-like characters.
|
||||
// `.is_empty` checks if the view contains any character.
|
||||
let is_empty_or_whitespace = s.trim().is_empty();
|
||||
|
||||
// A grapheme is defined by the Unicode standard as a "user-perceived"
|
||||
// character: `å` is a single grapheme, but it is composed of two characters
|
||||
// (`a` and `̊`).
|
||||
//
|
||||
// `graphemes` returns an iterator over the graphemes in the input `s`.
|
||||
// `true` specifies that we want to use the extended grapheme definition set,
|
||||
// the recommended one.
|
||||
let is_too_long = s.graphemes(true).count() > 256;
|
||||
|
||||
// Iterate over all characters in the input `s` to check if any of them matches
|
||||
// one of the characters in the forbidden array.
|
||||
let forbidden_characters = ['/', '(', ')', '"', '<', '>', '\\', '{', '}'];
|
||||
let contains_forbidden_characters = s
|
||||
.chars()
|
||||
.filter(|g| forbidden_characters.contains(g))
|
||||
.count()
|
||||
> 0;
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::domain::SubscriberName;
|
||||
use claim::{assert_err, assert_ok};
|
||||
|
||||
#[test]
|
||||
fn a_256_grapheme_long_name_is_valid() {
|
||||
let name = "a̐".repeat(256);
|
||||
assert_ok!(SubscriberName::parse(name));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_name_longer_than_256_graphemes_is_rejected() {
|
||||
let name = "a".repeat(257);
|
||||
assert_err!(SubscriberName::parse(name));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn whitespace_only_names_are_rejected() {
|
||||
let name = " ".to_string();
|
||||
assert_err!(SubscriberName::parse(name));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_string_is_rejected() {
|
||||
let name = "".to_string();
|
||||
assert_err!(SubscriberName::parse(name));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn names_containing_an_invalid_characters_are_rejected() {
|
||||
for name in vec!['/', '(', ')', '"', '<', '>', '\\', '{', '}'] {
|
||||
let name = name.to_string();
|
||||
assert_err!(SubscriberName::parse(name));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_valid_name_is_parsed_successfully() {
|
||||
let name = "Ursula Le Guin".to_string();
|
||||
assert_ok!(SubscriberName::parse(name));
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
#![allow(clippy::toplevel_ref_arg)]
|
||||
pub mod configuration;
|
||||
pub mod domain;
|
||||
pub mod routes;
|
||||
pub mod startup;
|
||||
pub mod telemetry;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use crate::domain::{NewSubscriber, SubscriberName};
|
||||
use actix_web::{web, HttpResponse};
|
||||
use chrono::Utc;
|
||||
use sqlx::PgPool;
|
||||
|
@ -21,7 +22,13 @@ pub async fn subscribe(
|
|||
form: web::Form<FormData>,
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, HttpResponse> {
|
||||
insert_subscriber(&pool, &form)
|
||||
let name =
|
||||
SubscriberName::parse(form.0.name).map_err(|_| HttpResponse::BadRequest().finish())?;
|
||||
let new_subscriber = NewSubscriber {
|
||||
email: form.0.email,
|
||||
name,
|
||||
};
|
||||
insert_subscriber(&pool, &new_subscriber)
|
||||
.await
|
||||
.map_err(|_| HttpResponse::InternalServerError().finish())?;
|
||||
Ok(HttpResponse::Ok().finish())
|
||||
|
@ -29,17 +36,20 @@ pub async fn subscribe(
|
|||
|
||||
#[tracing::instrument(
|
||||
name = "Saving new subscriber details in the database",
|
||||
skip(form, pool)
|
||||
skip(new_subscriber, pool)
|
||||
)]
|
||||
pub async fn insert_subscriber(pool: &PgPool, form: &FormData) -> Result<(), sqlx::Error> {
|
||||
pub async fn insert_subscriber(
|
||||
pool: &PgPool,
|
||||
new_subscriber: &NewSubscriber,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO subscriptions (id, email, name, subscribed_at)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
"#,
|
||||
Uuid::new_v4(),
|
||||
form.email,
|
||||
form.name,
|
||||
new_subscriber.email,
|
||||
new_subscriber.name.as_ref(),
|
||||
Utc::now()
|
||||
)
|
||||
.execute(pool)
|
||||
|
|
|
@ -141,3 +141,34 @@ async fn subscribe_returns_a_400_when_data_is_missing() {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn subscribe_returns_a_400_when_fields_are_present_but_invalid() {
|
||||
// Arrange
|
||||
let app = spawn_app().await;
|
||||
let client = reqwest::Client::new();
|
||||
let test_cases = vec![
|
||||
("name=&email=ursula_le_guin%40gmail.com", "empty name"),
|
||||
("name=Ursula&email=", "empty email"),
|
||||
("name=Ursula&email=definitely-not-an-email", "invalid email"),
|
||||
];
|
||||
|
||||
for (body, description) in test_cases {
|
||||
// Act
|
||||
let response = client
|
||||
.post(&format!("{}/subscriptions", &app.address))
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.body(body)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request.");
|
||||
|
||||
// Assert
|
||||
assert_eq!(
|
||||
400,
|
||||
response.status().as_u16(),
|
||||
"The API did not return a 400 Bad Request when the payload was {}.",
|
||||
description
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue