Compare commits
No commits in common. "a89cbe5bb0671e67c93e51af2a82d8298e16d3e5" and "8b1fbec3b299e744ecef6a98f7ee442d12c3b9e3" have entirely different histories.
a89cbe5bb0
...
8b1fbec3b2
|
@ -1,3 +0,0 @@
|
||||||
---
|
|
||||||
commands-show-output: false
|
|
||||||
no-inline-html: false
|
|
|
@ -1,29 +0,0 @@
|
||||||
---
|
|
||||||
# See https://pre-commit.com for more information
|
|
||||||
# See https://pre-commit.com/hooks.html for more hooks
|
|
||||||
repos:
|
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
||||||
rev: v3.1.0
|
|
||||||
hooks:
|
|
||||||
- id: check-byte-order-marker
|
|
||||||
- id: check-case-conflict
|
|
||||||
- id: check-merge-conflict
|
|
||||||
- id: check-symlinks
|
|
||||||
- id: check-yaml
|
|
||||||
- id: end-of-file-fixer
|
|
||||||
- id: mixed-line-ending
|
|
||||||
- id: trailing-whitespace
|
|
||||||
- repo: https://github.com/pre-commit/pre-commit
|
|
||||||
rev: v2.5.1
|
|
||||||
hooks:
|
|
||||||
- id: validate_manifest
|
|
||||||
- repo: https://github.com/doublify/pre-commit-rust
|
|
||||||
rev: eeee35a8
|
|
||||||
hooks:
|
|
||||||
- id: fmt
|
|
||||||
- id: cargo-check
|
|
||||||
- id: clippy
|
|
||||||
- repo: https://github.com/DavidAnson/markdownlint-cli2
|
|
||||||
rev: v0.6.0
|
|
||||||
hooks:
|
|
||||||
- id: markdownlint-cli2
|
|
|
@ -452,20 +452,6 @@ name = "serde"
|
||||||
version = "1.0.158"
|
version = "1.0.158"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "771d4d9c4163ee138805e12c710dd365e4f44be8be0503cb1bb9eb989425d9c9"
|
checksum = "771d4d9c4163ee138805e12c710dd365e4f44be8be0503cb1bb9eb989425d9c9"
|
||||||
dependencies = [
|
|
||||||
"serde_derive",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde_derive"
|
|
||||||
version = "1.0.158"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e801c1712f48475582b7696ac71e0ca34ebb30e09338425384269d9717c62cad"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.4",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
|
@ -840,7 +826,6 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"hyper",
|
"hyper",
|
||||||
"serde",
|
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower",
|
"tower",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|
|
@ -13,13 +13,8 @@ name = "ztp"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = "0.6.11"
|
axum = "0.6.11"
|
||||||
hyper = { version = "0.14.25", features = ["full"] }
|
hyper = { version = "0.14.25", features = ["full"] }
|
||||||
serde = { version = "1.0.158", features = ["derive"] }
|
|
||||||
tokio = { version = "1.26.0", features = ["full"] }
|
tokio = { version = "1.26.0", features = ["full"] }
|
||||||
tower = "0.4.13"
|
tower = "0.4.13"
|
||||||
tracing = "0.1.35"
|
tracing = "0.1.35"
|
||||||
tracing-subscriber = "0.3.14"
|
tracing-subscriber = "0.3.14"
|
||||||
|
|
||||||
[profile.release]
|
|
||||||
opt-level = 3
|
|
||||||
lto = true
|
|
||||||
codegen-units = 1
|
|
||||||
|
|
|
@ -12,20 +12,20 @@ tool. It is one of those blind-trust-in-the-safety-of-the-toolchain things. For
|
||||||
Linux and Mac users, the command is a shell script that installs to a user's
|
Linux and Mac users, the command is a shell script that installs to a user's
|
||||||
local account:
|
local account:
|
||||||
|
|
||||||
``` sh
|
```
|
||||||
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Once installed, you can install Rust itself:
|
Once installed, you can install Rust itself:
|
||||||
|
|
||||||
``` sh
|
```
|
||||||
$ rustup install toolchain stable
|
$ rustup install toolchain stable
|
||||||
```
|
```
|
||||||
|
|
||||||
You should now have Rust compiler and the Rust build and packaging tool, known
|
You should now have Rust compiler and the Rust build and packaging tool, known
|
||||||
as Cargo:
|
as Cargo:
|
||||||
|
|
||||||
``` sh
|
```
|
||||||
$ rustc --version
|
$ rustc --version
|
||||||
rustc 1.68.0 (2c8cc3432 2023-03-06)
|
rustc 1.68.0 (2c8cc3432 2023-03-06)
|
||||||
$ cargo --version
|
$ cargo --version
|
||||||
|
@ -34,7 +34,7 @@ cargo 1.68.0 (115f34552 2023-02-26)
|
||||||
|
|
||||||
I also installed the following tools:
|
I also installed the following tools:
|
||||||
|
|
||||||
``` sh
|
```
|
||||||
$ rustup component add clippy rust-src rust-docs
|
$ rustup component add clippy rust-src rust-docs
|
||||||
$ cargo install rustfmt rust-analyzer
|
$ cargo install rustfmt rust-analyzer
|
||||||
```
|
```
|
||||||
|
@ -45,3 +45,5 @@ $ cargo install rustfmt rust-analyzer
|
||||||
- rust-analyzer: For your IDE, rust-analyzer provides the LSP (Language Server
|
- rust-analyzer: For your IDE, rust-analyzer provides the LSP (Language Server
|
||||||
Protocol) for Rust, giving you code completion, on-the-fly error definition,
|
Protocol) for Rust, giving you code completion, on-the-fly error definition,
|
||||||
and other luxuries.
|
and other luxuries.
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -16,11 +16,12 @@ Although the book is only two years old, it is already out-of-date with respect
|
||||||
to some commands. `cargo add` is now provided by default. The following
|
to some commands. `cargo add` is now provided by default. The following
|
||||||
commands installed the tools I'll be using:
|
commands installed the tools I'll be using:
|
||||||
|
|
||||||
``` sh
|
```
|
||||||
cargo add --features tokio/full --features hyper/full tokio hyper \
|
cargo add --features tokio/full --features hyper/full tokio hyper \
|
||||||
axum tracing tracing-subscriber
|
axum tracing tracing-subscriber
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
- axum: The web server framework for Tokio.
|
- axum: The web server framework for Tokio.
|
||||||
- tokio: The Rust asynchronous runtime. Has single-threaded (select) and
|
- tokio: The Rust asynchronous runtime. Has single-threaded (select) and
|
||||||
multi-threaded variants.
|
multi-threaded variants.
|
||||||
|
@ -38,7 +39,7 @@ converting data from one format to another.
|
||||||
|
|
||||||
All of these go into `src/lib.rs`:
|
All of these go into `src/lib.rs`:
|
||||||
|
|
||||||
``` rust
|
```
|
||||||
async fn health_check() -> impl IntoResponse {
|
async fn health_check() -> impl IntoResponse {
|
||||||
(StatusCode::OK, ())
|
(StatusCode::OK, ())
|
||||||
}
|
}
|
||||||
|
@ -64,7 +65,7 @@ E>`](https://doc.rust-lang.org/std/result/), especially one with an error.</asid
|
||||||
We then define the routes that our server will recognize. This is
|
We then define the routes that our server will recognize. This is
|
||||||
straightforward and familiar territory:
|
straightforward and familiar territory:
|
||||||
|
|
||||||
``` rust
|
```
|
||||||
fn app() -> Router {
|
fn app() -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/", get(anon_greet))
|
.route("/", get(anon_greet))
|
||||||
|
@ -75,7 +76,7 @@ fn app() -> Router {
|
||||||
|
|
||||||
We then define a function to *run* the core server:
|
We then define a function to *run* the core server:
|
||||||
|
|
||||||
``` rust
|
```
|
||||||
pub async fn run() {
|
pub async fn run() {
|
||||||
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
|
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
|
||||||
tracing::info!("listening on {}", addr);
|
tracing::info!("listening on {}", addr);
|
||||||
|
@ -88,7 +89,7 @@ pub async fn run() {
|
||||||
|
|
||||||
And finally, in a file named `src/main.rs`, we instantiate the server:
|
And finally, in a file named `src/main.rs`, we instantiate the server:
|
||||||
|
|
||||||
``` rust
|
```
|
||||||
use ztp::run;
|
use ztp::run;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
|
@ -103,7 +104,7 @@ between the library and the CLI program.
|
||||||
In the project root's `Cargo.toml` file, the first three sections are needed to
|
In the project root's `Cargo.toml` file, the first three sections are needed to
|
||||||
define these relationships:
|
define these relationships:
|
||||||
|
|
||||||
``` toml
|
```
|
||||||
[package]
|
[package]
|
||||||
name = "ztp"
|
name = "ztp"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -126,13 +127,13 @@ project to have more than one package, called "crates," per project. </aside>
|
||||||
|
|
||||||
This project should now be runnable. In one window, type:
|
This project should now be runnable. In one window, type:
|
||||||
|
|
||||||
``` sh
|
```
|
||||||
$ cargo run
|
$ cargo run
|
||||||
```
|
```
|
||||||
|
|
||||||
And in another, type and see the replies:
|
And in another, type and see the replies:
|
||||||
|
|
||||||
``` sh
|
```
|
||||||
$ curl http://localhost:3000/
|
$ curl http://localhost:3000/
|
||||||
Hello, World!
|
Hello, World!
|
||||||
$ curl http://localhost:3000/Jim
|
$ curl http://localhost:3000/Jim
|
||||||
|
@ -158,7 +159,7 @@ exercise its functions. We'll use Tokio's `spawn` function to create a new
|
||||||
server, use hyper to request data from the server, and finally Rust's own native
|
server, use hyper to request data from the server, and finally Rust's own native
|
||||||
test asserts to check that we got what we expected.
|
test asserts to check that we got what we expected.
|
||||||
|
|
||||||
``` rust
|
```
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
@ -1,100 +0,0 @@
|
||||||
+++
|
|
||||||
title = "Refactored Tests and Pre-Commits"
|
|
||||||
date = 2023-03-20T17:38:12Z
|
|
||||||
weight = 3
|
|
||||||
+++
|
|
||||||
|
|
||||||
## Chapter 3.7 (Sort-of): A brief diversion
|
|
||||||
|
|
||||||
Re-reading the text, I made a number of changes. The first is that, while it is
|
|
||||||
nice that Rust allows us to have unit tests in the file whose functionality
|
|
||||||
we're testing, it's also nice to have the tests somewhere separate, and to have
|
|
||||||
the tests be a little more modular.
|
|
||||||
|
|
||||||
In the `./tests` folder, you can now see the same `health_check` test as the
|
|
||||||
original, but in an isolated and cleaned-up form. Most importantly, the server
|
|
||||||
startup code is now in its own function, with a correct return type that
|
|
||||||
includes a handle to the spawned thread and the address on which that server is
|
|
||||||
listening; tests can be run in parallel on many different ports and a lot of
|
|
||||||
code duplication is eliminated.
|
|
||||||
|
|
||||||
``` rust
|
|
||||||
type NullHandle = JoinHandle<()>;
|
|
||||||
|
|
||||||
async fn spawn_server() -> (SocketAddr, NullHandle) {
|
|
||||||
let listener = TcpListener::bind("127.0.0.1:0".parse::<SocketAddr>().unwrap()).unwrap();
|
|
||||||
let addr = listener.local_addr().unwrap();
|
|
||||||
|
|
||||||
let handle: NullHandle = tokio::spawn(async move {
|
|
||||||
axum::Server::from_tcp(listener)
|
|
||||||
.unwrap()
|
|
||||||
.serve(app().into_make_service())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
});
|
|
||||||
|
|
||||||
(addr, handle)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
It is also possible now to add new tests in a straightforward manner. The
|
|
||||||
Hyper API is not that much different from the Actix request API, and the Axum
|
|
||||||
extractors seem to be straightforward. I suspect that what I'm looking at here
|
|
||||||
with the handle is the idea that, when it goes out of scope, it calls a d
|
|
||||||
|
|
||||||
## Adding some checks
|
|
||||||
|
|
||||||
In the introduction I said I was going to be neglecting CI/CD, since I'm a solo
|
|
||||||
developer. That's true, but I do like my guardrails. I like not being able to
|
|
||||||
commit garbage to the repository. So I'm going to add some checks, using
|
|
||||||
[Pre-Commit](https://pre-commit.com/).
|
|
||||||
|
|
||||||
Pre-Commit is a Python program, so we'll start by installing it. I'm using a
|
|
||||||
local Python environment kickstarted with
|
|
||||||
[Pyenv](https://github.com/pyenv/pyenv).
|
|
||||||
|
|
||||||
``` sh
|
|
||||||
$ pip install pre-commit
|
|
||||||
```
|
|
||||||
|
|
||||||
And inside your project, in the project root, you hook it up with the following commands:
|
|
||||||
|
|
||||||
``` sh
|
|
||||||
$ pre-commit install
|
|
||||||
$ pre-commit sample-config > .pre-commit-config.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
I'm going with the default from the rust pre-commit collection, so my
|
|
||||||
`.pre-commit-config.yaml` file looks like this:
|
|
||||||
|
|
||||||
``` yaml
|
|
||||||
repos:
|
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
||||||
rev: v3.1.0
|
|
||||||
hooks:
|
|
||||||
- id: check-byte-order-marker
|
|
||||||
- id: check-case-conflict
|
|
||||||
- id: check-merge-conflict
|
|
||||||
- id: check-symlinks
|
|
||||||
- id: check-yaml
|
|
||||||
- id: end-of-file-fixer
|
|
||||||
- id: mixed-line-ending
|
|
||||||
- id: trailing-whitespace
|
|
||||||
- repo: https://github.com/pre-commit/pre-commit
|
|
||||||
rev: v2.5.1
|
|
||||||
hooks:
|
|
||||||
- id: validate_manifest
|
|
||||||
- repo: https://github.com/doublify/pre-commit-rust
|
|
||||||
rev: master
|
|
||||||
hooks:
|
|
||||||
- id: fmt
|
|
||||||
- id: cargo-check
|
|
||||||
- id: clippy
|
|
||||||
```
|
|
||||||
|
|
||||||
... and with that, every time I try to commit my code, it will not let me until
|
|
||||||
these tests pass. And I *like* that level of discipline. This is low-level
|
|
||||||
validation; it won't catch if I put addition where I meant subtraction, or if I
|
|
||||||
have a comparison going in the wrong direction, but at least the basics are
|
|
||||||
handled and, more importantly, the formatting and styling is consistent
|
|
||||||
throughout all of my code.
|
|
|
@ -17,3 +17,4 @@ book. Those changes include:
|
||||||
- Neglecting CI/CD, since I'm a sole developer
|
- Neglecting CI/CD, since I'm a sole developer
|
||||||
- Developing entirely in a laptop environment
|
- Developing entirely in a laptop environment
|
||||||
- Using Axum instead of Actix-Web for the server framework
|
- Using Axum instead of Actix-Web for the server framework
|
||||||
|
|
||||||
|
|
10
src/lib.rs
10
src/lib.rs
|
@ -1,8 +1,10 @@
|
||||||
use axum::{extract::Path, http::StatusCode, response::IntoResponse, routing::get, Router};
|
use axum::{extract::Path, http::StatusCode, response::IntoResponse, routing::get, Router};
|
||||||
|
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
mod user;
|
async fn anon_greet() -> &'static str {
|
||||||
use user::index;
|
"Hello World!\n"
|
||||||
|
}
|
||||||
|
|
||||||
async fn greet(Path(name): Path<String>) -> impl IntoResponse {
|
async fn greet(Path(name): Path<String>) -> impl IntoResponse {
|
||||||
let greeting = String::from("He's dead, ") + name.as_str();
|
let greeting = String::from("He's dead, ") + name.as_str();
|
||||||
|
@ -14,9 +16,9 @@ async fn health_check() -> impl IntoResponse {
|
||||||
(StatusCode::OK, ())
|
(StatusCode::OK, ())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn app() -> Router {
|
fn app() -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/", get(index))
|
.route("/", get(anon_greet))
|
||||||
.route("/:name", get(greet))
|
.route("/:name", get(greet))
|
||||||
.route("/health_check", get(health_check))
|
.route("/health_check", get(health_check))
|
||||||
}
|
}
|
||||||
|
|
13
src/user.rs
13
src/user.rs
|
@ -1,13 +0,0 @@
|
||||||
use axum::{http::StatusCode, response::IntoResponse, Form};
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
|
||||||
pub struct FormData {
|
|
||||||
username: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn index(payload: Option<Form<FormData>>) -> impl IntoResponse {
|
|
||||||
let username = payload.map_or("World !".to_string(), move |index| -> String {
|
|
||||||
String::from(&(index.username))
|
|
||||||
});
|
|
||||||
(StatusCode::OK, format!("Hello, {}", &username))
|
|
||||||
}
|
|
|
@ -1,95 +0,0 @@
|
||||||
use axum::{
|
|
||||||
body::Body,
|
|
||||||
http::{Request, StatusCode},
|
|
||||||
};
|
|
||||||
use std::net::{SocketAddr, TcpListener};
|
|
||||||
use tokio::task::JoinHandle;
|
|
||||||
use ztp::*;
|
|
||||||
|
|
||||||
type NullHandle = JoinHandle<()>;
|
|
||||||
|
|
||||||
async fn spawn_server() -> (SocketAddr, NullHandle) {
|
|
||||||
let listener = TcpListener::bind("127.0.0.1:0".parse::<SocketAddr>().unwrap()).unwrap();
|
|
||||||
let addr = listener.local_addr().unwrap();
|
|
||||||
|
|
||||||
let handle: NullHandle = tokio::spawn(async move {
|
|
||||||
axum::Server::from_tcp(listener)
|
|
||||||
.unwrap()
|
|
||||||
.serve(app().into_make_service())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
});
|
|
||||||
|
|
||||||
(addr, handle)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn the_real_deal() {
|
|
||||||
let (addr, _server_handle) = spawn_server().await;
|
|
||||||
|
|
||||||
let response = hyper::Client::new()
|
|
||||||
.request(
|
|
||||||
Request::builder()
|
|
||||||
.uri(format!("http://{}/", addr))
|
|
||||||
.body(Body::empty())
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
|
|
||||||
assert_eq!(&body[..], b"Hello World!\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn valid_subscription() {
|
|
||||||
let (addr, _server_handle) = spawn_server().await;
|
|
||||||
let body = "name=le%20guin&email=ursula_le_guin%40gmail.com";
|
|
||||||
|
|
||||||
let response = hyper::Client::new()
|
|
||||||
.request(
|
|
||||||
Request::builder()
|
|
||||||
.method("POST")
|
|
||||||
.uri(format!("http://{}/subscriptions", addr))
|
|
||||||
.body(Body::from(body))
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect("Failed to execute request.");
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
assert_eq!(200, response.status().as_u16());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn subscribe_returns_400_on_missing_data() {
|
|
||||||
let (addr, _server_handle) = spawn_server().await;
|
|
||||||
|
|
||||||
let test_cases = vec![
|
|
||||||
("name=le%20guin", "missing the email"),
|
|
||||||
("email=ursula_le_guin%40gmail.com", "missing the name"),
|
|
||||||
("", "missing both name and email"),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (invalid_body, error_message) in test_cases {
|
|
||||||
let response = hyper::Client::new()
|
|
||||||
.request(
|
|
||||||
Request::builder()
|
|
||||||
.method("POST")
|
|
||||||
.uri(format!("http://{}/subscriptions", addr))
|
|
||||||
.body(Body::from(invalid_body))
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect("Failed to execute request.");
|
|
||||||
|
|
||||||
// TODO This should be 400 "Bad Request"
|
|
||||||
assert_eq!(
|
|
||||||
405,
|
|
||||||
response.status().as_u16(),
|
|
||||||
// Additional customised error message on test failure
|
|
||||||
"The API did not fail with 400 Bad Request when the payload was {}.",
|
|
||||||
error_message
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue