Compare commits
2 Commits
8b1fbec3b2
...
a89cbe5bb0
Author | SHA1 | Date |
---|---|---|
Elf M. Sternberg | a89cbe5bb0 | |
Elf M. Sternberg | 89fb8188b7 |
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
commands-show-output: false
|
||||
no-inline-html: false
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
# 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,6 +452,20 @@ name = "serde"
|
|||
version = "1.0.158"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "serde_json"
|
||||
|
@ -826,6 +840,7 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"axum",
|
||||
"hyper",
|
||||
"serde",
|
||||
"tokio",
|
||||
"tower",
|
||||
"tracing",
|
||||
|
|
|
@ -13,8 +13,13 @@ name = "ztp"
|
|||
[dependencies]
|
||||
axum = "0.6.11"
|
||||
hyper = { version = "0.14.25", features = ["full"] }
|
||||
serde = { version = "1.0.158", features = ["derive"] }
|
||||
tokio = { version = "1.26.0", features = ["full"] }
|
||||
tower = "0.4.13"
|
||||
tracing = "0.1.35"
|
||||
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
|
||||
local account:
|
||||
|
||||
```
|
||||
``` sh
|
||||
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
```
|
||||
|
||||
Once installed, you can install Rust itself:
|
||||
|
||||
```
|
||||
``` sh
|
||||
$ rustup install toolchain stable
|
||||
```
|
||||
|
||||
You should now have Rust compiler and the Rust build and packaging tool, known
|
||||
as Cargo:
|
||||
|
||||
```
|
||||
``` sh
|
||||
$ rustc --version
|
||||
rustc 1.68.0 (2c8cc3432 2023-03-06)
|
||||
$ cargo --version
|
||||
|
@ -34,8 +34,8 @@ cargo 1.68.0 (115f34552 2023-02-26)
|
|||
|
||||
I also installed the following tools:
|
||||
|
||||
```
|
||||
$ rustup component add clippy rust-src rust-docs
|
||||
``` sh
|
||||
$ rustup component add clippy rust-src rust-docs
|
||||
$ cargo install rustfmt rust-analyzer
|
||||
```
|
||||
|
||||
|
@ -45,5 +45,3 @@ $ cargo install rustfmt rust-analyzer
|
|||
- rust-analyzer: For your IDE, rust-analyzer provides the LSP (Language Server
|
||||
Protocol) for Rust, giving you code completion, on-the-fly error definition,
|
||||
and other luxuries.
|
||||
|
||||
|
||||
|
|
|
@ -16,12 +16,11 @@ 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
|
||||
commands installed the tools I'll be using:
|
||||
|
||||
```
|
||||
``` sh
|
||||
cargo add --features tokio/full --features hyper/full tokio hyper \
|
||||
axum tracing tracing-subscriber
|
||||
```
|
||||
|
||||
|
||||
- axum: The web server framework for Tokio.
|
||||
- tokio: The Rust asynchronous runtime. Has single-threaded (select) and
|
||||
multi-threaded variants.
|
||||
|
@ -39,7 +38,7 @@ converting data from one format to another.
|
|||
|
||||
All of these go into `src/lib.rs`:
|
||||
|
||||
```
|
||||
``` rust
|
||||
async fn health_check() -> impl IntoResponse {
|
||||
(StatusCode::OK, ())
|
||||
}
|
||||
|
@ -65,7 +64,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
|
||||
straightforward and familiar territory:
|
||||
|
||||
```
|
||||
``` rust
|
||||
fn app() -> Router {
|
||||
Router::new()
|
||||
.route("/", get(anon_greet))
|
||||
|
@ -76,7 +75,7 @@ fn app() -> Router {
|
|||
|
||||
We then define a function to *run* the core server:
|
||||
|
||||
```
|
||||
``` rust
|
||||
pub async fn run() {
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
|
||||
tracing::info!("listening on {}", addr);
|
||||
|
@ -85,11 +84,11 @@ pub async fn run() {
|
|||
.await
|
||||
.unwrap()
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
And finally, in a file named `src/main.rs`, we instantiate the server:
|
||||
|
||||
```
|
||||
``` rust
|
||||
use ztp::run;
|
||||
|
||||
#[tokio::main]
|
||||
|
@ -104,7 +103,7 @@ between the library and the CLI program.
|
|||
In the project root's `Cargo.toml` file, the first three sections are needed to
|
||||
define these relationships:
|
||||
|
||||
```
|
||||
``` toml
|
||||
[package]
|
||||
name = "ztp"
|
||||
version = "0.1.0"
|
||||
|
@ -127,13 +126,13 @@ project to have more than one package, called "crates," per project. </aside>
|
|||
|
||||
This project should now be runnable. In one window, type:
|
||||
|
||||
```
|
||||
``` sh
|
||||
$ cargo run
|
||||
```
|
||||
|
||||
And in another, type and see the replies:
|
||||
|
||||
```
|
||||
``` sh
|
||||
$ curl http://localhost:3000/
|
||||
Hello, World!
|
||||
$ curl http://localhost:3000/Jim
|
||||
|
@ -159,7 +158,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
|
||||
test asserts to check that we got what we expected.
|
||||
|
||||
```
|
||||
``` rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
+++
|
||||
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,4 +17,3 @@ book. Those changes include:
|
|||
- Neglecting CI/CD, since I'm a sole developer
|
||||
- Developing entirely in a laptop environment
|
||||
- Using Axum instead of Actix-Web for the server framework
|
||||
|
||||
|
|
10
src/lib.rs
10
src/lib.rs
|
@ -1,10 +1,8 @@
|
|||
use axum::{extract::Path, http::StatusCode, response::IntoResponse, routing::get, Router};
|
||||
|
||||
use std::net::SocketAddr;
|
||||
|
||||
async fn anon_greet() -> &'static str {
|
||||
"Hello World!\n"
|
||||
}
|
||||
mod user;
|
||||
use user::index;
|
||||
|
||||
async fn greet(Path(name): Path<String>) -> impl IntoResponse {
|
||||
let greeting = String::from("He's dead, ") + name.as_str();
|
||||
|
@ -16,9 +14,9 @@ async fn health_check() -> impl IntoResponse {
|
|||
(StatusCode::OK, ())
|
||||
}
|
||||
|
||||
fn app() -> Router {
|
||||
pub fn app() -> Router {
|
||||
Router::new()
|
||||
.route("/", get(anon_greet))
|
||||
.route("/", get(index))
|
||||
.route("/:name", get(greet))
|
||||
.route("/health_check", get(health_check))
|
||||
}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
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))
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
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