ztp/src/configuration.rs

76 lines
1.7 KiB
Rust
Raw Permalink Normal View History

Implemented the forms reader, config, and database migrations. This chapter introduces the Actix "Extractors" for retrieving form data. I've added tests to the `./tests` folder to attempt to interact with those extractors; as of this commit, a89cbe5b, they fail because the example code isn't there. What is there is a variant of the "Hello, World!" code from the previous exercises (section 3.5), which uses the Actix extractor: ``` rust // Actix, *not* Axum. Does not work with the current framework. fn index(form: web::Form<FormData>) -> String { format!('Welcome {}!', form.username) } ``` Translated and polished into Axum, it translates to: ``` rust 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, {}!\n", &username)) } ``` The Axum version is a little smarter, providing a default "World!" if you don't specify a name. That's what `.map_or` does, although the `or` part actually comes first in the function. So the result is: ``` sh $ curl http://localhost:3000/ Hello, World! $ curl 'http://localhost:3000/?username=Spock' Hello, Spock! ``` Which is more or less the version we want. **Section 3.7.3** then goes into some detail on the implementation of a Trait. A Trait in Rust is like an Interface in other languages; it describes a collection of functions for manipulating the values found in a defined Type. **Types**: A Type is just a description of the value: `u16` is a sixteen-bit unsigned integer; `char` is a Unicode character, and it's size is always 32bits, but a `String` is a bunch of things: it's an array of characters (which are not always `char`!), a length for that string and a capacity. If the String is manipulated to exceed the capacity, the array is re-allocated to a new capacity and the old array copied into it. A `Vec<String>` is an array of Strings; as a Type, it is considered to have a single value: whatever is in it at the moment you use it. **Trait**: A Trait defines a collection of one or more functions that can operate on a value. The truly nifty thing about Traits is that they can be implemented after the fact. By importing a Trait and an implementation of that trait specific to a type into a module containing that type, you can extend the behavior of a type in a deterministic way without having to modify or inherit the code, as you would in an object-oriented language. Axum has a valuable trait, `FromRequest`. For any structure you can imagine passing from the client to the server, you can implement `FromRequest` for that object and any content in the body of the message will be transformed into that structure. We've seen a trait before: `IntoResponse`, written as `impl IntoResponse`, and is the output (the return type) of many of the functions that produce return values for our application server. In this case the return type instructs Rust to look in the current lexical scope and, for the value returned by that function, determine if an `IntoResponse` trait has been defined for it. If it has, the value will be returned because Axum has now been assured that there exists a function to convert that value into something streamable and usable as an HTTP response. Fortunately for us, Axum has already implemented `FromRequest` for all the native data types, as well as some structures and arrays. Even better, it has implemented `FromRequest` for the Serde serialization/deserialization library. So in this example: ``` rust pub struct FormData { username: String, } pub async fn index(payload: Option<Form<FormData>>) -> impl IntoResponse { ... ``` A `Form` (something using the `application/x-www-form-urlencoded` protocol) of `FormData` will automatically be converted into a `payload` object of `{ username: "Spock" )`, and in this case wrapped in a `Some()` handler. (Or `None`, if there was no form included.) <aside>So far, there's not too much bloat in this product; with all the debugging symbols, it's 60MB or so, but stripped to the bone it's only 3.1MB, tolerable for modern deployments.</aside> First, though, we must adjust our `valid_subscription` test: ``` rust let body = "name=le%20guin&email=ursula_le_guin%40gmail.com"; let response = hyper::Client::new() .request( Request::builder() .method("POST") .header("content-type", "application/x-www-form-urlencoded") .uri(format!("http://{}/subscriptions", addr)) .body(Body::from(body)) .unwrap(), ) .await .expect("Failed to execute request."); ``` Two updates from the book: first, we're sending it via POST instead of GET. This is the correct way to do things; a GET should never (and I mean *never*) cause a change of state on the back-end. To send something new that the server will process and store, you use a POST. (To update something, or to send something to a known *and unique* URI, PUT is better.) Secondly, since we're using a generic form-data object, we need to set the content-type on the client so that the server is informed of how to unpack this payload. The '%20' and '%40' markers in the `body` are the space and the `@` respectively. I completely ignored the advice in the book and went instead with [Dbmate](https://github.com/amacneil/dbmate); Dbmate is a bit cranky; your SQL must be very much nestled against the 'up' and 'down' markers in the migration file, and it seems to be quite opinionated about everything being lowercase. That said, it was trivial to create a database with it: ``` sh $ dbmate new create_subscriptions_table ``` This will create the folder `db/migrations/20230322174957_create_subscriptions_table.sql`, (The timestamp will be different, obviously), and in this file you put the following, as specified in the book: ``` sql -- migrate:up create table subscriptions ( id uuid not null, primary key (id), email text not null unique, name text not null, subscribed_at timestamptz not null ); -- migrate:down drop table subscriptions; ``` To use Dbmate, you have to specify how to connect. I'm using Postgres, so let's start with creating a database and a user for it: ``` sh $ sudo -u postgres psql [sudo] possword for user: ................... postgres=# create database newsletter; CREATE DATABASE postgres=# create user newletter with encrypted password 'redacted'; CREATE USER postgres=# grant all privileges on database newsletter to newsletter; GRANT postgres=# exit ``` In your project root, create a `.env` file to specify your connection: ``` sh DATABASE_URL="postgres://newsletter:redacted@127.0.0.1:5432/newsletter?sslmode=disable" ``` The `sslmode` flag there is necessary for localhost connections, as Dbmate assumes an encrypted connection by default, but we're isolating to a local connection that is, usually, safe. With the new entry in your `.env` file, you can now run a migration: ``` sh $ dbmate up Writing: ./db/schema.sql ``` Running `dbmate up` will automatically create the database for you if it hasn't already; `dbmate migrate` also performs migrations, but it will not create the database. Now you can re-connect to Postgres as the newsletter user and see what you've got: ``` sh $ psql --user newsletter -h localhost --password Password: psql (14.7 (Ubuntu 14.7-0ubuntu0.22.04.1), server 11.7 (Ubuntu 11.7-0ubuntu0.19.10.1)) newsletter=> \d List of relations Schema | Name | Type | Owner --------+-------------------+-------+------------ public | schema_migrations | table | newsletter public | subscriptions | table | newsletter (2 rows) newsletter=> \d subscriptions Table "public.subscriptions" Column | Type | Collation | Nullable | Default ---------------+--------------------------+-----------+----------+--------- id | uuid | | not null | email | text | | not null | name | text | | not null | subscribed_at | timestamp with time zone | | not null | Indexes: "subscriptions_pkey" PRIMARY KEY, btree (id) "subscriptions_email_key" UNIQUE CONSTRAINT, btree (email) ``` Note that Dbmate has allocated a table to itself, `schema_migrations`, for tracking what it's done to your system and when. Try not to conflict with it, okay? Every complex app has a configuration, and there are plenty of different ways the configuration can be specified. Environment variables, internal defaults, and configuration files-- the last of which comes in so many different flavors. Rust has a well-known [config](https://docs.rs/config/latest/config/index.html) crate that supports all the most common configurations: YAML, JSON, TOML; you can even add your own by writing something that implements the `config::Format` trait. Add it to Cargo.toml: ``` sh $ cargo add config ``` For the meantime, we're just going to create a new file, called `configuration.rs`, and put our configuration details in there. Right now we have a single configuration detail: the port. I'm going to go above and beyond Lucas here and configure some internal defaults for my code. It will have expectations. First, you have to tell Serde that there will be default values: ``` rust use config::Config; pub struct Settings { pub port: u16, } ``` Then, you have to set those default values. Fortunately, Rust provides a "set default values" trait named, sensibly enough, Default: ``` rust impl Default for Settings { fn default() -> Self { Settings { port: 3001 } } } ``` Again, exceeding the book's parameters, I'm going to say that if the file is missing the default parameters should hold: ``` rust pub(crate) fn get_configuration() -> Result<Settings, config::ConfigError> { Config::builder() .add_source(config::File::with_name("./ztd.config").required(false)) .build()? .try_deserialize() } ``` And since this is the first time I'm doing this, I'm going to write a test to assert that my understanding of how this all works is correct: ``` rust mod tests { use super::*; #[test] fn test_for_defaults() { let maybe_config = get_configuration(); assert!(!maybe_config.is_err()); let config = maybe_config.unwrap(); assert_eq!(config.port, 3001); } } ```
2023-03-24 14:51:19 +00:00
use config::Config;
2023-03-24 19:22:28 +00:00
#[derive(serde::Deserialize, Debug)]
#[serde(default)]
Implemented the forms reader, config, and database migrations. This chapter introduces the Actix "Extractors" for retrieving form data. I've added tests to the `./tests` folder to attempt to interact with those extractors; as of this commit, a89cbe5b, they fail because the example code isn't there. What is there is a variant of the "Hello, World!" code from the previous exercises (section 3.5), which uses the Actix extractor: ``` rust // Actix, *not* Axum. Does not work with the current framework. fn index(form: web::Form<FormData>) -> String { format!('Welcome {}!', form.username) } ``` Translated and polished into Axum, it translates to: ``` rust 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, {}!\n", &username)) } ``` The Axum version is a little smarter, providing a default "World!" if you don't specify a name. That's what `.map_or` does, although the `or` part actually comes first in the function. So the result is: ``` sh $ curl http://localhost:3000/ Hello, World! $ curl 'http://localhost:3000/?username=Spock' Hello, Spock! ``` Which is more or less the version we want. **Section 3.7.3** then goes into some detail on the implementation of a Trait. A Trait in Rust is like an Interface in other languages; it describes a collection of functions for manipulating the values found in a defined Type. **Types**: A Type is just a description of the value: `u16` is a sixteen-bit unsigned integer; `char` is a Unicode character, and it's size is always 32bits, but a `String` is a bunch of things: it's an array of characters (which are not always `char`!), a length for that string and a capacity. If the String is manipulated to exceed the capacity, the array is re-allocated to a new capacity and the old array copied into it. A `Vec<String>` is an array of Strings; as a Type, it is considered to have a single value: whatever is in it at the moment you use it. **Trait**: A Trait defines a collection of one or more functions that can operate on a value. The truly nifty thing about Traits is that they can be implemented after the fact. By importing a Trait and an implementation of that trait specific to a type into a module containing that type, you can extend the behavior of a type in a deterministic way without having to modify or inherit the code, as you would in an object-oriented language. Axum has a valuable trait, `FromRequest`. For any structure you can imagine passing from the client to the server, you can implement `FromRequest` for that object and any content in the body of the message will be transformed into that structure. We've seen a trait before: `IntoResponse`, written as `impl IntoResponse`, and is the output (the return type) of many of the functions that produce return values for our application server. In this case the return type instructs Rust to look in the current lexical scope and, for the value returned by that function, determine if an `IntoResponse` trait has been defined for it. If it has, the value will be returned because Axum has now been assured that there exists a function to convert that value into something streamable and usable as an HTTP response. Fortunately for us, Axum has already implemented `FromRequest` for all the native data types, as well as some structures and arrays. Even better, it has implemented `FromRequest` for the Serde serialization/deserialization library. So in this example: ``` rust pub struct FormData { username: String, } pub async fn index(payload: Option<Form<FormData>>) -> impl IntoResponse { ... ``` A `Form` (something using the `application/x-www-form-urlencoded` protocol) of `FormData` will automatically be converted into a `payload` object of `{ username: "Spock" )`, and in this case wrapped in a `Some()` handler. (Or `None`, if there was no form included.) <aside>So far, there's not too much bloat in this product; with all the debugging symbols, it's 60MB or so, but stripped to the bone it's only 3.1MB, tolerable for modern deployments.</aside> First, though, we must adjust our `valid_subscription` test: ``` rust let body = "name=le%20guin&email=ursula_le_guin%40gmail.com"; let response = hyper::Client::new() .request( Request::builder() .method("POST") .header("content-type", "application/x-www-form-urlencoded") .uri(format!("http://{}/subscriptions", addr)) .body(Body::from(body)) .unwrap(), ) .await .expect("Failed to execute request."); ``` Two updates from the book: first, we're sending it via POST instead of GET. This is the correct way to do things; a GET should never (and I mean *never*) cause a change of state on the back-end. To send something new that the server will process and store, you use a POST. (To update something, or to send something to a known *and unique* URI, PUT is better.) Secondly, since we're using a generic form-data object, we need to set the content-type on the client so that the server is informed of how to unpack this payload. The '%20' and '%40' markers in the `body` are the space and the `@` respectively. I completely ignored the advice in the book and went instead with [Dbmate](https://github.com/amacneil/dbmate); Dbmate is a bit cranky; your SQL must be very much nestled against the 'up' and 'down' markers in the migration file, and it seems to be quite opinionated about everything being lowercase. That said, it was trivial to create a database with it: ``` sh $ dbmate new create_subscriptions_table ``` This will create the folder `db/migrations/20230322174957_create_subscriptions_table.sql`, (The timestamp will be different, obviously), and in this file you put the following, as specified in the book: ``` sql -- migrate:up create table subscriptions ( id uuid not null, primary key (id), email text not null unique, name text not null, subscribed_at timestamptz not null ); -- migrate:down drop table subscriptions; ``` To use Dbmate, you have to specify how to connect. I'm using Postgres, so let's start with creating a database and a user for it: ``` sh $ sudo -u postgres psql [sudo] possword for user: ................... postgres=# create database newsletter; CREATE DATABASE postgres=# create user newletter with encrypted password 'redacted'; CREATE USER postgres=# grant all privileges on database newsletter to newsletter; GRANT postgres=# exit ``` In your project root, create a `.env` file to specify your connection: ``` sh DATABASE_URL="postgres://newsletter:redacted@127.0.0.1:5432/newsletter?sslmode=disable" ``` The `sslmode` flag there is necessary for localhost connections, as Dbmate assumes an encrypted connection by default, but we're isolating to a local connection that is, usually, safe. With the new entry in your `.env` file, you can now run a migration: ``` sh $ dbmate up Writing: ./db/schema.sql ``` Running `dbmate up` will automatically create the database for you if it hasn't already; `dbmate migrate` also performs migrations, but it will not create the database. Now you can re-connect to Postgres as the newsletter user and see what you've got: ``` sh $ psql --user newsletter -h localhost --password Password: psql (14.7 (Ubuntu 14.7-0ubuntu0.22.04.1), server 11.7 (Ubuntu 11.7-0ubuntu0.19.10.1)) newsletter=> \d List of relations Schema | Name | Type | Owner --------+-------------------+-------+------------ public | schema_migrations | table | newsletter public | subscriptions | table | newsletter (2 rows) newsletter=> \d subscriptions Table "public.subscriptions" Column | Type | Collation | Nullable | Default ---------------+--------------------------+-----------+----------+--------- id | uuid | | not null | email | text | | not null | name | text | | not null | subscribed_at | timestamp with time zone | | not null | Indexes: "subscriptions_pkey" PRIMARY KEY, btree (id) "subscriptions_email_key" UNIQUE CONSTRAINT, btree (email) ``` Note that Dbmate has allocated a table to itself, `schema_migrations`, for tracking what it's done to your system and when. Try not to conflict with it, okay? Every complex app has a configuration, and there are plenty of different ways the configuration can be specified. Environment variables, internal defaults, and configuration files-- the last of which comes in so many different flavors. Rust has a well-known [config](https://docs.rs/config/latest/config/index.html) crate that supports all the most common configurations: YAML, JSON, TOML; you can even add your own by writing something that implements the `config::Format` trait. Add it to Cargo.toml: ``` sh $ cargo add config ``` For the meantime, we're just going to create a new file, called `configuration.rs`, and put our configuration details in there. Right now we have a single configuration detail: the port. I'm going to go above and beyond Lucas here and configure some internal defaults for my code. It will have expectations. First, you have to tell Serde that there will be default values: ``` rust use config::Config; pub struct Settings { pub port: u16, } ``` Then, you have to set those default values. Fortunately, Rust provides a "set default values" trait named, sensibly enough, Default: ``` rust impl Default for Settings { fn default() -> Self { Settings { port: 3001 } } } ``` Again, exceeding the book's parameters, I'm going to say that if the file is missing the default parameters should hold: ``` rust pub(crate) fn get_configuration() -> Result<Settings, config::ConfigError> { Config::builder() .add_source(config::File::with_name("./ztd.config").required(false)) .build()? .try_deserialize() } ``` And since this is the first time I'm doing this, I'm going to write a test to assert that my understanding of how this all works is correct: ``` rust mod tests { use super::*; #[test] fn test_for_defaults() { let maybe_config = get_configuration(); assert!(!maybe_config.is_err()); let config = maybe_config.unwrap(); assert_eq!(config.port, 3001); } } ```
2023-03-24 14:51:19 +00:00
pub struct DatabaseSettings {
pub username: String,
pub password: String,
pub host: String,
pub port: u16,
pub database: String,
}
2023-03-24 19:22:28 +00:00
impl DatabaseSettings {
pub fn url(&self) -> String {
Databases, Connecting, Using, and Checking Errors First, we're gonna expand the configuration we defined previously. The one thing we will require is the password, although eventually I'm going to make that a command-line option using Clap. ``` rust pub struct DatabaseSettings { pub username: String, pub password: String, pub host: String, pub port: u16, pub database: String, } pub struct Settings { pub database: DatabaseSettings, pub port: u16, } impl Default for DatabaseSettings { fn default() -> Self { DatabaseSettings { username: "newsletter".to_string(), password: "".to_string(), host: "localhost".to_string(), port: 5432, database: "newsletter".to_string(), } } } impl Default for Settings { fn default() -> Self { Settings { port: 3001, database: DatabaseSettings::default(), } } } ``` The code's beginning to get a bit messsy. So let's re-arrange. I already have the configuration handler in its own file, but let's clean up the routes. I'm going to take the functions I've already defined and put them in a subfolder, `src/routes`, and then for each one I'll make a new file. For example: ``` rust use axum::{http::StatusCode, response::IntoResponse}; pub(crate) async fn health_check() -> impl IntoResponse { (StatusCode::OK, ()) } ``` Just a note: *ignore* your IDE's "advice" to remove the unneeded `async`; Axum will not compile this module correctly, `IntoResponse` requires that it be an async function. I don't know why rust-analyzer failed to pick that up. Now make a `src/routes.rs` (yes, the same name as the folder). You can activate and de-activate routes at will: ``` rust use axum::{ routing::{get, post}, Router, }; mod greet; use greet::greet; mod health_check; use health_check::health_check; mod index; use index::index; mod subscribe; use subscribe::subscribe; pub(crate) fn app() -> Router { Router::new() .route("/", get(index)) .route("/subscriptions", post(subscribe)) .route("/:name", get(greet)) .route("/health_check", get(health_check)) } ``` You don't need the `use` clauses, you could just say `index::index`, but I kinda like the way this looks, even with the repetition. It makes it clear what I'm doing and what I'm looking at. This is Rust 2021, where the presence of a file and a folder with the same name indicate a module with submodules. In prior editions of rust, instead of `routes.rs`, we would have used `routes/mod.rs`. I'm not entirely sure which I like better. Now that that's done, in our `lib.rs`, we use the `routes`: ``` rust use std::net::SocketAddr; mod configuration; use configuration::get_configuration; mod routes; use routes::app; pub async fn run() { let configuration = get_configuration().unwrap(); let addr = SocketAddr::from(([127, 0, 0, 1], configuration.port)); tracing::info!("listening on {}", addr); axum::Server::bind(&addr) .serve(app().into_make_service()) .await .unwrap() } ``` All of the Axum imports have been removed, since they're part of the routes. Nice, clean and clear code. At this point, even `cargo clippy` is happy. Connecting to a database these days involves a URL. Let's make it easy to generate one. Now, we could implement the trait `std::string::ToString`, or `Display`, but that would conflict with any debugging information. Instead, let's define a new method in `configuration.rs`: ``` rust impl DatabaseSettings { pub fn url(&self) -> String { if self.password.len() == 0 { format!( "postgres://{}@{}:{}/{}", self.username, self.host, self.port, self.database ) } else { format!( "postgres://{}:{}@{}:{}/{}", self.username, self.password, self.host, self.port, self.database ) } } } ``` It is possible (unwise, but possible) to connect to a Postgres database with no password. In a dynamic language this would have been less wordy, but I can't complain about Rust being a little extra verbose in exchange for being a lot more precise. Since we're using Postgres, in `lib.rs` we'll add `use sqlx::postgres::PgPoolOptions;`, which will allow us to create a pool of workers and limit how many there are. In the `run()` function, we're going to extract the `app()` definition and use it: ``` rust use axum::Extension; use sqlx::postgres::PgPoolOptions; // in: fn run() let pool = PgPoolOptions::new() .max_connections(50) .connect(&configuration.database.url()) .await .expect("could not connect to database_url"); let routes = app().layer(Extension(pool)); tracing::info!("listening on {}", addr); axum::Server::bind(&addr) .serve(routes.into_make_service()) .await .unwrap() ``` Now we have a configuration that is runnable. If you run it as-is, it will fail because, if you set up your Postgres database correctly, there is no password. Create a `ztp.config.yaml` file (or JSON, or TOML, or whatever floats your boat) and establish the password: ``` yaml database: password: redacted ``` At one point, I had this file named only `ztp.config`, and unfortunately it crashed. Adding `#[derive(Debug)]` to my configuration structs helped show that the password wasn't getting set, but it took me a few minutes to realize that I'd forgotten the extension so `Config` didn't know how to read the file. Naming it `ztp.config.yaml` fixed it, and this illustrates just how useful TOML can be in circumstances like this. This turned out to be a bit of a beast, and I'm incredibly indebted to Carlos Armando Marcano Vargas and [his blog](https://github.com/carlosm27/blog) where he documented much of what I needed to make this work with Axum. In the migrations file, we specified that the primary key of our subscriptions table would be a UUID, and that we were going to record when the subscription happened. To do this, first we have to make sure that we have the `Uuid` and `Chrono` crates, and that they're all hooked up for proper serialization: ``` toml chrono = { version = "0.4.24", features = ["serde"] } uuid = { version = "1.3.0", features = ["v4", "serde"] } sqlx = { version = "0.6.3", features = ["runtime-tokio-native-tls", "macros", "postgres", "uuid", "chrono"] } ``` Both of the new creates need `serde` as a feature and we'll be using `UUID4` for our primary key. We'll also need a full subscription object to store, and then a way to generate it from a new subscription: ```rust pub(crate) struct NewSubscription { pub email: String, pub name: String, } struct Subscription { pub id: Uuid, pub email: String, pub name: String, pub subscribed_at: DateTime<Utc>, } impl From<&NewSubscription> for Subscription { fn from(s: &NewSubscription) -> Self { Subscription { id: Uuid::new_v4(), email: s.email.clone(), name: s.name.clone(), subscribed_at: Utc::now(), } } } ``` I've renamed "Subscription" to "NewSubscription" to distinguish it from the thing we're going to reliably keep in our database, which has all the fields. And now we're going to re-write the `subscribe()` function to actually talk to the database. All of those parens around the `payload` are necessary to help Rust understand what we want to borrow. You'll note that it's `payload.0`, not just `payload`; Form objects can have multiple instances of their content. ``` rust pub(crate) async fn subscribe(Extension(pool): Extension<PgPool>, payload: Form<NewSubscription>) -> impl IntoResponse { let sql = r#"INSERT INTO subscriptions (id, email, name, subscribed_at) VALUES ($1, $2, $3, $4);"#.to_string(); let subscription: Subscription = (&(payload.0)).into(); let _ = sqlx::query(&sql) .bind(subscription.id) .bind(subscription.email) .bind(subscription.name) .bind(subscription.subscribed_at) .execute(&pool) .await .expect("Failed for some reason?"); (StatusCode::OK, ()) } ``` Alternatively, I could have written: ``` rust let subscription = Subscription::from(&(payload.0)); ``` Either way works. That unused `.expect()` has Tokio panicking if I give it a duplicate email address, as one of the constraints in our original migration reads: ``` sql email text not null unique, ``` We want to catch and handle that error. We also want to fix something that's been sitting in the "TODO" list for awhile: make sure that when the subscription form isn't fully filled out, we return a straight 400. That's actually the easier part: we make the payload optional and return our default `BAD_REQUEST` if it's not there: ``` rust pub(crate) async fn subscribe( Extension(pool): Extension<PgPool>, payload: Option<Form<NewSubscription>>, ) -> impl IntoResponse { if let Some(payload) = payload { ... (StatusCode::OK, ()) } else { (StatusCode::BAD_REQUEST, ()) } ``` So, handling the errors is going to be interesting. Going above and beyond, it's time to use `thiserror`. Rust has an excellent error handling story, with the `Result<T, impl Error>`, although mostly this is just a way of distinguishing and annotating errors in a robust, type-safe manner; it's still up to the programmer to handle errors. A library has a set of error states that it can return, and `thiserror` is a set of macros that automatically annotate your list of errors with messages and values. Right now we have two possible errors: the form data passed in was incomplete, or the email address submitted already exists. (We're not even checking the email address for validity, just existence.) So let's create those errors and provide them with labels. We'll just put this in a file named `errors.rs`, in the root of the crate; that seems to be where everyone puts it, and it's a nice convention. ``` rust pub enum ZTPError { #[error("Form data was incomplete")] FormIncomplete, #[error("Email address already subscribed")] DuplicateEmail, } ``` And now the error will always be "400: Bad Response", but the text of the message will be much more descriptive of what went wrong. We have to edit the `subscribe` function to handle these changes: first, the return type must change, and then the errors must be included: ``` rust pub(crate) async fn subscribe( Extension(pool): Extension<PgPool>, payload: Option<Form<NewSubscription>>, ) -> Result<(StatusCode, ()), ZTPError> { // ... .map_or(Ok((StatusCode::OK, ())), |_| Err(ZTPError::DuplicateEmail)) } else { Err(ZTPError::FormIncomplete) } ``` We've changed the function to take an `Option<Form<>>`; this way, if we don't get a form that deserialized properly we'll be able to say "That wasn't a good form," and we've changed the result so that we can return our error. Then we changed our two edge cases to report the errors. Axum doesn't know what a ZTPError is, but we can easily make our errors to something Axum does understand. We'll just have to implement our own `IntoResponse`, and we'll put it in `errors.rs`: ``` rust impl IntoResponse for ZTPError { fn into_response(self) -> axum::response::Response { let (status, error_message) = match self { Self::FormIncomplete => (StatusCode::BAD_REQUEST, self.to_string()), Self::DuplicateEmail => (StatusCode::BAD_REQUEST, self.to_string()), }; (status, Json(json!({ "error": error_message }))).into_response() } } ``` ... and with that, we can now change all of the tests to watch for `400` codes, as that ancient TODO said, and it will all just work. *Holy Chao*, we've made it through the "writing the application" stuff, and a lot of what was in the book is... no longer quite so relevant. There's whole sections on getting Actix to type-match with your code that Axum and 2021 Rust make more or less irrelevant, and by using features like `thiserror` and `dbmate` we've managed to route around many of the bulkier difficulties in the book. The next chapter is about logging and monitoring. ɡɡ
2023-03-24 22:09:39 +00:00
if self.password.is_empty() {
2023-03-24 19:22:28 +00:00
format!(
"postgres://{}@{}:{}/{}",
self.username, self.host, self.port, self.database
)
} else {
format!(
"postgres://{}:{}@{}:{}/{}",
self.username, self.password, self.host, self.port, self.database
)
}
}
}
#[derive(serde::Deserialize, Debug)]
Implemented the forms reader, config, and database migrations. This chapter introduces the Actix "Extractors" for retrieving form data. I've added tests to the `./tests` folder to attempt to interact with those extractors; as of this commit, a89cbe5b, they fail because the example code isn't there. What is there is a variant of the "Hello, World!" code from the previous exercises (section 3.5), which uses the Actix extractor: ``` rust // Actix, *not* Axum. Does not work with the current framework. fn index(form: web::Form<FormData>) -> String { format!('Welcome {}!', form.username) } ``` Translated and polished into Axum, it translates to: ``` rust 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, {}!\n", &username)) } ``` The Axum version is a little smarter, providing a default "World!" if you don't specify a name. That's what `.map_or` does, although the `or` part actually comes first in the function. So the result is: ``` sh $ curl http://localhost:3000/ Hello, World! $ curl 'http://localhost:3000/?username=Spock' Hello, Spock! ``` Which is more or less the version we want. **Section 3.7.3** then goes into some detail on the implementation of a Trait. A Trait in Rust is like an Interface in other languages; it describes a collection of functions for manipulating the values found in a defined Type. **Types**: A Type is just a description of the value: `u16` is a sixteen-bit unsigned integer; `char` is a Unicode character, and it's size is always 32bits, but a `String` is a bunch of things: it's an array of characters (which are not always `char`!), a length for that string and a capacity. If the String is manipulated to exceed the capacity, the array is re-allocated to a new capacity and the old array copied into it. A `Vec<String>` is an array of Strings; as a Type, it is considered to have a single value: whatever is in it at the moment you use it. **Trait**: A Trait defines a collection of one or more functions that can operate on a value. The truly nifty thing about Traits is that they can be implemented after the fact. By importing a Trait and an implementation of that trait specific to a type into a module containing that type, you can extend the behavior of a type in a deterministic way without having to modify or inherit the code, as you would in an object-oriented language. Axum has a valuable trait, `FromRequest`. For any structure you can imagine passing from the client to the server, you can implement `FromRequest` for that object and any content in the body of the message will be transformed into that structure. We've seen a trait before: `IntoResponse`, written as `impl IntoResponse`, and is the output (the return type) of many of the functions that produce return values for our application server. In this case the return type instructs Rust to look in the current lexical scope and, for the value returned by that function, determine if an `IntoResponse` trait has been defined for it. If it has, the value will be returned because Axum has now been assured that there exists a function to convert that value into something streamable and usable as an HTTP response. Fortunately for us, Axum has already implemented `FromRequest` for all the native data types, as well as some structures and arrays. Even better, it has implemented `FromRequest` for the Serde serialization/deserialization library. So in this example: ``` rust pub struct FormData { username: String, } pub async fn index(payload: Option<Form<FormData>>) -> impl IntoResponse { ... ``` A `Form` (something using the `application/x-www-form-urlencoded` protocol) of `FormData` will automatically be converted into a `payload` object of `{ username: "Spock" )`, and in this case wrapped in a `Some()` handler. (Or `None`, if there was no form included.) <aside>So far, there's not too much bloat in this product; with all the debugging symbols, it's 60MB or so, but stripped to the bone it's only 3.1MB, tolerable for modern deployments.</aside> First, though, we must adjust our `valid_subscription` test: ``` rust let body = "name=le%20guin&email=ursula_le_guin%40gmail.com"; let response = hyper::Client::new() .request( Request::builder() .method("POST") .header("content-type", "application/x-www-form-urlencoded") .uri(format!("http://{}/subscriptions", addr)) .body(Body::from(body)) .unwrap(), ) .await .expect("Failed to execute request."); ``` Two updates from the book: first, we're sending it via POST instead of GET. This is the correct way to do things; a GET should never (and I mean *never*) cause a change of state on the back-end. To send something new that the server will process and store, you use a POST. (To update something, or to send something to a known *and unique* URI, PUT is better.) Secondly, since we're using a generic form-data object, we need to set the content-type on the client so that the server is informed of how to unpack this payload. The '%20' and '%40' markers in the `body` are the space and the `@` respectively. I completely ignored the advice in the book and went instead with [Dbmate](https://github.com/amacneil/dbmate); Dbmate is a bit cranky; your SQL must be very much nestled against the 'up' and 'down' markers in the migration file, and it seems to be quite opinionated about everything being lowercase. That said, it was trivial to create a database with it: ``` sh $ dbmate new create_subscriptions_table ``` This will create the folder `db/migrations/20230322174957_create_subscriptions_table.sql`, (The timestamp will be different, obviously), and in this file you put the following, as specified in the book: ``` sql -- migrate:up create table subscriptions ( id uuid not null, primary key (id), email text not null unique, name text not null, subscribed_at timestamptz not null ); -- migrate:down drop table subscriptions; ``` To use Dbmate, you have to specify how to connect. I'm using Postgres, so let's start with creating a database and a user for it: ``` sh $ sudo -u postgres psql [sudo] possword for user: ................... postgres=# create database newsletter; CREATE DATABASE postgres=# create user newletter with encrypted password 'redacted'; CREATE USER postgres=# grant all privileges on database newsletter to newsletter; GRANT postgres=# exit ``` In your project root, create a `.env` file to specify your connection: ``` sh DATABASE_URL="postgres://newsletter:redacted@127.0.0.1:5432/newsletter?sslmode=disable" ``` The `sslmode` flag there is necessary for localhost connections, as Dbmate assumes an encrypted connection by default, but we're isolating to a local connection that is, usually, safe. With the new entry in your `.env` file, you can now run a migration: ``` sh $ dbmate up Writing: ./db/schema.sql ``` Running `dbmate up` will automatically create the database for you if it hasn't already; `dbmate migrate` also performs migrations, but it will not create the database. Now you can re-connect to Postgres as the newsletter user and see what you've got: ``` sh $ psql --user newsletter -h localhost --password Password: psql (14.7 (Ubuntu 14.7-0ubuntu0.22.04.1), server 11.7 (Ubuntu 11.7-0ubuntu0.19.10.1)) newsletter=> \d List of relations Schema | Name | Type | Owner --------+-------------------+-------+------------ public | schema_migrations | table | newsletter public | subscriptions | table | newsletter (2 rows) newsletter=> \d subscriptions Table "public.subscriptions" Column | Type | Collation | Nullable | Default ---------------+--------------------------+-----------+----------+--------- id | uuid | | not null | email | text | | not null | name | text | | not null | subscribed_at | timestamp with time zone | | not null | Indexes: "subscriptions_pkey" PRIMARY KEY, btree (id) "subscriptions_email_key" UNIQUE CONSTRAINT, btree (email) ``` Note that Dbmate has allocated a table to itself, `schema_migrations`, for tracking what it's done to your system and when. Try not to conflict with it, okay? Every complex app has a configuration, and there are plenty of different ways the configuration can be specified. Environment variables, internal defaults, and configuration files-- the last of which comes in so many different flavors. Rust has a well-known [config](https://docs.rs/config/latest/config/index.html) crate that supports all the most common configurations: YAML, JSON, TOML; you can even add your own by writing something that implements the `config::Format` trait. Add it to Cargo.toml: ``` sh $ cargo add config ``` For the meantime, we're just going to create a new file, called `configuration.rs`, and put our configuration details in there. Right now we have a single configuration detail: the port. I'm going to go above and beyond Lucas here and configure some internal defaults for my code. It will have expectations. First, you have to tell Serde that there will be default values: ``` rust use config::Config; pub struct Settings { pub port: u16, } ``` Then, you have to set those default values. Fortunately, Rust provides a "set default values" trait named, sensibly enough, Default: ``` rust impl Default for Settings { fn default() -> Self { Settings { port: 3001 } } } ``` Again, exceeding the book's parameters, I'm going to say that if the file is missing the default parameters should hold: ``` rust pub(crate) fn get_configuration() -> Result<Settings, config::ConfigError> { Config::builder() .add_source(config::File::with_name("./ztd.config").required(false)) .build()? .try_deserialize() } ``` And since this is the first time I'm doing this, I'm going to write a test to assert that my understanding of how this all works is correct: ``` rust mod tests { use super::*; #[test] fn test_for_defaults() { let maybe_config = get_configuration(); assert!(!maybe_config.is_err()); let config = maybe_config.unwrap(); assert_eq!(config.port, 3001); } } ```
2023-03-24 14:51:19 +00:00
#[serde(default)]
pub struct Settings {
2023-03-24 19:22:28 +00:00
pub database: DatabaseSettings,
Implemented the forms reader, config, and database migrations. This chapter introduces the Actix "Extractors" for retrieving form data. I've added tests to the `./tests` folder to attempt to interact with those extractors; as of this commit, a89cbe5b, they fail because the example code isn't there. What is there is a variant of the "Hello, World!" code from the previous exercises (section 3.5), which uses the Actix extractor: ``` rust // Actix, *not* Axum. Does not work with the current framework. fn index(form: web::Form<FormData>) -> String { format!('Welcome {}!', form.username) } ``` Translated and polished into Axum, it translates to: ``` rust 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, {}!\n", &username)) } ``` The Axum version is a little smarter, providing a default "World!" if you don't specify a name. That's what `.map_or` does, although the `or` part actually comes first in the function. So the result is: ``` sh $ curl http://localhost:3000/ Hello, World! $ curl 'http://localhost:3000/?username=Spock' Hello, Spock! ``` Which is more or less the version we want. **Section 3.7.3** then goes into some detail on the implementation of a Trait. A Trait in Rust is like an Interface in other languages; it describes a collection of functions for manipulating the values found in a defined Type. **Types**: A Type is just a description of the value: `u16` is a sixteen-bit unsigned integer; `char` is a Unicode character, and it's size is always 32bits, but a `String` is a bunch of things: it's an array of characters (which are not always `char`!), a length for that string and a capacity. If the String is manipulated to exceed the capacity, the array is re-allocated to a new capacity and the old array copied into it. A `Vec<String>` is an array of Strings; as a Type, it is considered to have a single value: whatever is in it at the moment you use it. **Trait**: A Trait defines a collection of one or more functions that can operate on a value. The truly nifty thing about Traits is that they can be implemented after the fact. By importing a Trait and an implementation of that trait specific to a type into a module containing that type, you can extend the behavior of a type in a deterministic way without having to modify or inherit the code, as you would in an object-oriented language. Axum has a valuable trait, `FromRequest`. For any structure you can imagine passing from the client to the server, you can implement `FromRequest` for that object and any content in the body of the message will be transformed into that structure. We've seen a trait before: `IntoResponse`, written as `impl IntoResponse`, and is the output (the return type) of many of the functions that produce return values for our application server. In this case the return type instructs Rust to look in the current lexical scope and, for the value returned by that function, determine if an `IntoResponse` trait has been defined for it. If it has, the value will be returned because Axum has now been assured that there exists a function to convert that value into something streamable and usable as an HTTP response. Fortunately for us, Axum has already implemented `FromRequest` for all the native data types, as well as some structures and arrays. Even better, it has implemented `FromRequest` for the Serde serialization/deserialization library. So in this example: ``` rust pub struct FormData { username: String, } pub async fn index(payload: Option<Form<FormData>>) -> impl IntoResponse { ... ``` A `Form` (something using the `application/x-www-form-urlencoded` protocol) of `FormData` will automatically be converted into a `payload` object of `{ username: "Spock" )`, and in this case wrapped in a `Some()` handler. (Or `None`, if there was no form included.) <aside>So far, there's not too much bloat in this product; with all the debugging symbols, it's 60MB or so, but stripped to the bone it's only 3.1MB, tolerable for modern deployments.</aside> First, though, we must adjust our `valid_subscription` test: ``` rust let body = "name=le%20guin&email=ursula_le_guin%40gmail.com"; let response = hyper::Client::new() .request( Request::builder() .method("POST") .header("content-type", "application/x-www-form-urlencoded") .uri(format!("http://{}/subscriptions", addr)) .body(Body::from(body)) .unwrap(), ) .await .expect("Failed to execute request."); ``` Two updates from the book: first, we're sending it via POST instead of GET. This is the correct way to do things; a GET should never (and I mean *never*) cause a change of state on the back-end. To send something new that the server will process and store, you use a POST. (To update something, or to send something to a known *and unique* URI, PUT is better.) Secondly, since we're using a generic form-data object, we need to set the content-type on the client so that the server is informed of how to unpack this payload. The '%20' and '%40' markers in the `body` are the space and the `@` respectively. I completely ignored the advice in the book and went instead with [Dbmate](https://github.com/amacneil/dbmate); Dbmate is a bit cranky; your SQL must be very much nestled against the 'up' and 'down' markers in the migration file, and it seems to be quite opinionated about everything being lowercase. That said, it was trivial to create a database with it: ``` sh $ dbmate new create_subscriptions_table ``` This will create the folder `db/migrations/20230322174957_create_subscriptions_table.sql`, (The timestamp will be different, obviously), and in this file you put the following, as specified in the book: ``` sql -- migrate:up create table subscriptions ( id uuid not null, primary key (id), email text not null unique, name text not null, subscribed_at timestamptz not null ); -- migrate:down drop table subscriptions; ``` To use Dbmate, you have to specify how to connect. I'm using Postgres, so let's start with creating a database and a user for it: ``` sh $ sudo -u postgres psql [sudo] possword for user: ................... postgres=# create database newsletter; CREATE DATABASE postgres=# create user newletter with encrypted password 'redacted'; CREATE USER postgres=# grant all privileges on database newsletter to newsletter; GRANT postgres=# exit ``` In your project root, create a `.env` file to specify your connection: ``` sh DATABASE_URL="postgres://newsletter:redacted@127.0.0.1:5432/newsletter?sslmode=disable" ``` The `sslmode` flag there is necessary for localhost connections, as Dbmate assumes an encrypted connection by default, but we're isolating to a local connection that is, usually, safe. With the new entry in your `.env` file, you can now run a migration: ``` sh $ dbmate up Writing: ./db/schema.sql ``` Running `dbmate up` will automatically create the database for you if it hasn't already; `dbmate migrate` also performs migrations, but it will not create the database. Now you can re-connect to Postgres as the newsletter user and see what you've got: ``` sh $ psql --user newsletter -h localhost --password Password: psql (14.7 (Ubuntu 14.7-0ubuntu0.22.04.1), server 11.7 (Ubuntu 11.7-0ubuntu0.19.10.1)) newsletter=> \d List of relations Schema | Name | Type | Owner --------+-------------------+-------+------------ public | schema_migrations | table | newsletter public | subscriptions | table | newsletter (2 rows) newsletter=> \d subscriptions Table "public.subscriptions" Column | Type | Collation | Nullable | Default ---------------+--------------------------+-----------+----------+--------- id | uuid | | not null | email | text | | not null | name | text | | not null | subscribed_at | timestamp with time zone | | not null | Indexes: "subscriptions_pkey" PRIMARY KEY, btree (id) "subscriptions_email_key" UNIQUE CONSTRAINT, btree (email) ``` Note that Dbmate has allocated a table to itself, `schema_migrations`, for tracking what it's done to your system and when. Try not to conflict with it, okay? Every complex app has a configuration, and there are plenty of different ways the configuration can be specified. Environment variables, internal defaults, and configuration files-- the last of which comes in so many different flavors. Rust has a well-known [config](https://docs.rs/config/latest/config/index.html) crate that supports all the most common configurations: YAML, JSON, TOML; you can even add your own by writing something that implements the `config::Format` trait. Add it to Cargo.toml: ``` sh $ cargo add config ``` For the meantime, we're just going to create a new file, called `configuration.rs`, and put our configuration details in there. Right now we have a single configuration detail: the port. I'm going to go above and beyond Lucas here and configure some internal defaults for my code. It will have expectations. First, you have to tell Serde that there will be default values: ``` rust use config::Config; pub struct Settings { pub port: u16, } ``` Then, you have to set those default values. Fortunately, Rust provides a "set default values" trait named, sensibly enough, Default: ``` rust impl Default for Settings { fn default() -> Self { Settings { port: 3001 } } } ``` Again, exceeding the book's parameters, I'm going to say that if the file is missing the default parameters should hold: ``` rust pub(crate) fn get_configuration() -> Result<Settings, config::ConfigError> { Config::builder() .add_source(config::File::with_name("./ztd.config").required(false)) .build()? .try_deserialize() } ``` And since this is the first time I'm doing this, I'm going to write a test to assert that my understanding of how this all works is correct: ``` rust mod tests { use super::*; #[test] fn test_for_defaults() { let maybe_config = get_configuration(); assert!(!maybe_config.is_err()); let config = maybe_config.unwrap(); assert_eq!(config.port, 3001); } } ```
2023-03-24 14:51:19 +00:00
pub port: u16,
}
2023-03-24 19:22:28 +00:00
impl Default for DatabaseSettings {
fn default() -> Self {
DatabaseSettings {
username: "newsletter".to_string(),
password: "".to_string(),
host: "localhost".to_string(),
port: 5432,
database: "newsletter".to_string(),
}
}
}
Implemented the forms reader, config, and database migrations. This chapter introduces the Actix "Extractors" for retrieving form data. I've added tests to the `./tests` folder to attempt to interact with those extractors; as of this commit, a89cbe5b, they fail because the example code isn't there. What is there is a variant of the "Hello, World!" code from the previous exercises (section 3.5), which uses the Actix extractor: ``` rust // Actix, *not* Axum. Does not work with the current framework. fn index(form: web::Form<FormData>) -> String { format!('Welcome {}!', form.username) } ``` Translated and polished into Axum, it translates to: ``` rust 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, {}!\n", &username)) } ``` The Axum version is a little smarter, providing a default "World!" if you don't specify a name. That's what `.map_or` does, although the `or` part actually comes first in the function. So the result is: ``` sh $ curl http://localhost:3000/ Hello, World! $ curl 'http://localhost:3000/?username=Spock' Hello, Spock! ``` Which is more or less the version we want. **Section 3.7.3** then goes into some detail on the implementation of a Trait. A Trait in Rust is like an Interface in other languages; it describes a collection of functions for manipulating the values found in a defined Type. **Types**: A Type is just a description of the value: `u16` is a sixteen-bit unsigned integer; `char` is a Unicode character, and it's size is always 32bits, but a `String` is a bunch of things: it's an array of characters (which are not always `char`!), a length for that string and a capacity. If the String is manipulated to exceed the capacity, the array is re-allocated to a new capacity and the old array copied into it. A `Vec<String>` is an array of Strings; as a Type, it is considered to have a single value: whatever is in it at the moment you use it. **Trait**: A Trait defines a collection of one or more functions that can operate on a value. The truly nifty thing about Traits is that they can be implemented after the fact. By importing a Trait and an implementation of that trait specific to a type into a module containing that type, you can extend the behavior of a type in a deterministic way without having to modify or inherit the code, as you would in an object-oriented language. Axum has a valuable trait, `FromRequest`. For any structure you can imagine passing from the client to the server, you can implement `FromRequest` for that object and any content in the body of the message will be transformed into that structure. We've seen a trait before: `IntoResponse`, written as `impl IntoResponse`, and is the output (the return type) of many of the functions that produce return values for our application server. In this case the return type instructs Rust to look in the current lexical scope and, for the value returned by that function, determine if an `IntoResponse` trait has been defined for it. If it has, the value will be returned because Axum has now been assured that there exists a function to convert that value into something streamable and usable as an HTTP response. Fortunately for us, Axum has already implemented `FromRequest` for all the native data types, as well as some structures and arrays. Even better, it has implemented `FromRequest` for the Serde serialization/deserialization library. So in this example: ``` rust pub struct FormData { username: String, } pub async fn index(payload: Option<Form<FormData>>) -> impl IntoResponse { ... ``` A `Form` (something using the `application/x-www-form-urlencoded` protocol) of `FormData` will automatically be converted into a `payload` object of `{ username: "Spock" )`, and in this case wrapped in a `Some()` handler. (Or `None`, if there was no form included.) <aside>So far, there's not too much bloat in this product; with all the debugging symbols, it's 60MB or so, but stripped to the bone it's only 3.1MB, tolerable for modern deployments.</aside> First, though, we must adjust our `valid_subscription` test: ``` rust let body = "name=le%20guin&email=ursula_le_guin%40gmail.com"; let response = hyper::Client::new() .request( Request::builder() .method("POST") .header("content-type", "application/x-www-form-urlencoded") .uri(format!("http://{}/subscriptions", addr)) .body(Body::from(body)) .unwrap(), ) .await .expect("Failed to execute request."); ``` Two updates from the book: first, we're sending it via POST instead of GET. This is the correct way to do things; a GET should never (and I mean *never*) cause a change of state on the back-end. To send something new that the server will process and store, you use a POST. (To update something, or to send something to a known *and unique* URI, PUT is better.) Secondly, since we're using a generic form-data object, we need to set the content-type on the client so that the server is informed of how to unpack this payload. The '%20' and '%40' markers in the `body` are the space and the `@` respectively. I completely ignored the advice in the book and went instead with [Dbmate](https://github.com/amacneil/dbmate); Dbmate is a bit cranky; your SQL must be very much nestled against the 'up' and 'down' markers in the migration file, and it seems to be quite opinionated about everything being lowercase. That said, it was trivial to create a database with it: ``` sh $ dbmate new create_subscriptions_table ``` This will create the folder `db/migrations/20230322174957_create_subscriptions_table.sql`, (The timestamp will be different, obviously), and in this file you put the following, as specified in the book: ``` sql -- migrate:up create table subscriptions ( id uuid not null, primary key (id), email text not null unique, name text not null, subscribed_at timestamptz not null ); -- migrate:down drop table subscriptions; ``` To use Dbmate, you have to specify how to connect. I'm using Postgres, so let's start with creating a database and a user for it: ``` sh $ sudo -u postgres psql [sudo] possword for user: ................... postgres=# create database newsletter; CREATE DATABASE postgres=# create user newletter with encrypted password 'redacted'; CREATE USER postgres=# grant all privileges on database newsletter to newsletter; GRANT postgres=# exit ``` In your project root, create a `.env` file to specify your connection: ``` sh DATABASE_URL="postgres://newsletter:redacted@127.0.0.1:5432/newsletter?sslmode=disable" ``` The `sslmode` flag there is necessary for localhost connections, as Dbmate assumes an encrypted connection by default, but we're isolating to a local connection that is, usually, safe. With the new entry in your `.env` file, you can now run a migration: ``` sh $ dbmate up Writing: ./db/schema.sql ``` Running `dbmate up` will automatically create the database for you if it hasn't already; `dbmate migrate` also performs migrations, but it will not create the database. Now you can re-connect to Postgres as the newsletter user and see what you've got: ``` sh $ psql --user newsletter -h localhost --password Password: psql (14.7 (Ubuntu 14.7-0ubuntu0.22.04.1), server 11.7 (Ubuntu 11.7-0ubuntu0.19.10.1)) newsletter=> \d List of relations Schema | Name | Type | Owner --------+-------------------+-------+------------ public | schema_migrations | table | newsletter public | subscriptions | table | newsletter (2 rows) newsletter=> \d subscriptions Table "public.subscriptions" Column | Type | Collation | Nullable | Default ---------------+--------------------------+-----------+----------+--------- id | uuid | | not null | email | text | | not null | name | text | | not null | subscribed_at | timestamp with time zone | | not null | Indexes: "subscriptions_pkey" PRIMARY KEY, btree (id) "subscriptions_email_key" UNIQUE CONSTRAINT, btree (email) ``` Note that Dbmate has allocated a table to itself, `schema_migrations`, for tracking what it's done to your system and when. Try not to conflict with it, okay? Every complex app has a configuration, and there are plenty of different ways the configuration can be specified. Environment variables, internal defaults, and configuration files-- the last of which comes in so many different flavors. Rust has a well-known [config](https://docs.rs/config/latest/config/index.html) crate that supports all the most common configurations: YAML, JSON, TOML; you can even add your own by writing something that implements the `config::Format` trait. Add it to Cargo.toml: ``` sh $ cargo add config ``` For the meantime, we're just going to create a new file, called `configuration.rs`, and put our configuration details in there. Right now we have a single configuration detail: the port. I'm going to go above and beyond Lucas here and configure some internal defaults for my code. It will have expectations. First, you have to tell Serde that there will be default values: ``` rust use config::Config; pub struct Settings { pub port: u16, } ``` Then, you have to set those default values. Fortunately, Rust provides a "set default values" trait named, sensibly enough, Default: ``` rust impl Default for Settings { fn default() -> Self { Settings { port: 3001 } } } ``` Again, exceeding the book's parameters, I'm going to say that if the file is missing the default parameters should hold: ``` rust pub(crate) fn get_configuration() -> Result<Settings, config::ConfigError> { Config::builder() .add_source(config::File::with_name("./ztd.config").required(false)) .build()? .try_deserialize() } ``` And since this is the first time I'm doing this, I'm going to write a test to assert that my understanding of how this all works is correct: ``` rust mod tests { use super::*; #[test] fn test_for_defaults() { let maybe_config = get_configuration(); assert!(!maybe_config.is_err()); let config = maybe_config.unwrap(); assert_eq!(config.port, 3001); } } ```
2023-03-24 14:51:19 +00:00
impl Default for Settings {
fn default() -> Self {
2023-03-24 19:22:28 +00:00
Settings {
port: 3001,
database: DatabaseSettings::default(),
}
Implemented the forms reader, config, and database migrations. This chapter introduces the Actix "Extractors" for retrieving form data. I've added tests to the `./tests` folder to attempt to interact with those extractors; as of this commit, a89cbe5b, they fail because the example code isn't there. What is there is a variant of the "Hello, World!" code from the previous exercises (section 3.5), which uses the Actix extractor: ``` rust // Actix, *not* Axum. Does not work with the current framework. fn index(form: web::Form<FormData>) -> String { format!('Welcome {}!', form.username) } ``` Translated and polished into Axum, it translates to: ``` rust 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, {}!\n", &username)) } ``` The Axum version is a little smarter, providing a default "World!" if you don't specify a name. That's what `.map_or` does, although the `or` part actually comes first in the function. So the result is: ``` sh $ curl http://localhost:3000/ Hello, World! $ curl 'http://localhost:3000/?username=Spock' Hello, Spock! ``` Which is more or less the version we want. **Section 3.7.3** then goes into some detail on the implementation of a Trait. A Trait in Rust is like an Interface in other languages; it describes a collection of functions for manipulating the values found in a defined Type. **Types**: A Type is just a description of the value: `u16` is a sixteen-bit unsigned integer; `char` is a Unicode character, and it's size is always 32bits, but a `String` is a bunch of things: it's an array of characters (which are not always `char`!), a length for that string and a capacity. If the String is manipulated to exceed the capacity, the array is re-allocated to a new capacity and the old array copied into it. A `Vec<String>` is an array of Strings; as a Type, it is considered to have a single value: whatever is in it at the moment you use it. **Trait**: A Trait defines a collection of one or more functions that can operate on a value. The truly nifty thing about Traits is that they can be implemented after the fact. By importing a Trait and an implementation of that trait specific to a type into a module containing that type, you can extend the behavior of a type in a deterministic way without having to modify or inherit the code, as you would in an object-oriented language. Axum has a valuable trait, `FromRequest`. For any structure you can imagine passing from the client to the server, you can implement `FromRequest` for that object and any content in the body of the message will be transformed into that structure. We've seen a trait before: `IntoResponse`, written as `impl IntoResponse`, and is the output (the return type) of many of the functions that produce return values for our application server. In this case the return type instructs Rust to look in the current lexical scope and, for the value returned by that function, determine if an `IntoResponse` trait has been defined for it. If it has, the value will be returned because Axum has now been assured that there exists a function to convert that value into something streamable and usable as an HTTP response. Fortunately for us, Axum has already implemented `FromRequest` for all the native data types, as well as some structures and arrays. Even better, it has implemented `FromRequest` for the Serde serialization/deserialization library. So in this example: ``` rust pub struct FormData { username: String, } pub async fn index(payload: Option<Form<FormData>>) -> impl IntoResponse { ... ``` A `Form` (something using the `application/x-www-form-urlencoded` protocol) of `FormData` will automatically be converted into a `payload` object of `{ username: "Spock" )`, and in this case wrapped in a `Some()` handler. (Or `None`, if there was no form included.) <aside>So far, there's not too much bloat in this product; with all the debugging symbols, it's 60MB or so, but stripped to the bone it's only 3.1MB, tolerable for modern deployments.</aside> First, though, we must adjust our `valid_subscription` test: ``` rust let body = "name=le%20guin&email=ursula_le_guin%40gmail.com"; let response = hyper::Client::new() .request( Request::builder() .method("POST") .header("content-type", "application/x-www-form-urlencoded") .uri(format!("http://{}/subscriptions", addr)) .body(Body::from(body)) .unwrap(), ) .await .expect("Failed to execute request."); ``` Two updates from the book: first, we're sending it via POST instead of GET. This is the correct way to do things; a GET should never (and I mean *never*) cause a change of state on the back-end. To send something new that the server will process and store, you use a POST. (To update something, or to send something to a known *and unique* URI, PUT is better.) Secondly, since we're using a generic form-data object, we need to set the content-type on the client so that the server is informed of how to unpack this payload. The '%20' and '%40' markers in the `body` are the space and the `@` respectively. I completely ignored the advice in the book and went instead with [Dbmate](https://github.com/amacneil/dbmate); Dbmate is a bit cranky; your SQL must be very much nestled against the 'up' and 'down' markers in the migration file, and it seems to be quite opinionated about everything being lowercase. That said, it was trivial to create a database with it: ``` sh $ dbmate new create_subscriptions_table ``` This will create the folder `db/migrations/20230322174957_create_subscriptions_table.sql`, (The timestamp will be different, obviously), and in this file you put the following, as specified in the book: ``` sql -- migrate:up create table subscriptions ( id uuid not null, primary key (id), email text not null unique, name text not null, subscribed_at timestamptz not null ); -- migrate:down drop table subscriptions; ``` To use Dbmate, you have to specify how to connect. I'm using Postgres, so let's start with creating a database and a user for it: ``` sh $ sudo -u postgres psql [sudo] possword for user: ................... postgres=# create database newsletter; CREATE DATABASE postgres=# create user newletter with encrypted password 'redacted'; CREATE USER postgres=# grant all privileges on database newsletter to newsletter; GRANT postgres=# exit ``` In your project root, create a `.env` file to specify your connection: ``` sh DATABASE_URL="postgres://newsletter:redacted@127.0.0.1:5432/newsletter?sslmode=disable" ``` The `sslmode` flag there is necessary for localhost connections, as Dbmate assumes an encrypted connection by default, but we're isolating to a local connection that is, usually, safe. With the new entry in your `.env` file, you can now run a migration: ``` sh $ dbmate up Writing: ./db/schema.sql ``` Running `dbmate up` will automatically create the database for you if it hasn't already; `dbmate migrate` also performs migrations, but it will not create the database. Now you can re-connect to Postgres as the newsletter user and see what you've got: ``` sh $ psql --user newsletter -h localhost --password Password: psql (14.7 (Ubuntu 14.7-0ubuntu0.22.04.1), server 11.7 (Ubuntu 11.7-0ubuntu0.19.10.1)) newsletter=> \d List of relations Schema | Name | Type | Owner --------+-------------------+-------+------------ public | schema_migrations | table | newsletter public | subscriptions | table | newsletter (2 rows) newsletter=> \d subscriptions Table "public.subscriptions" Column | Type | Collation | Nullable | Default ---------------+--------------------------+-----------+----------+--------- id | uuid | | not null | email | text | | not null | name | text | | not null | subscribed_at | timestamp with time zone | | not null | Indexes: "subscriptions_pkey" PRIMARY KEY, btree (id) "subscriptions_email_key" UNIQUE CONSTRAINT, btree (email) ``` Note that Dbmate has allocated a table to itself, `schema_migrations`, for tracking what it's done to your system and when. Try not to conflict with it, okay? Every complex app has a configuration, and there are plenty of different ways the configuration can be specified. Environment variables, internal defaults, and configuration files-- the last of which comes in so many different flavors. Rust has a well-known [config](https://docs.rs/config/latest/config/index.html) crate that supports all the most common configurations: YAML, JSON, TOML; you can even add your own by writing something that implements the `config::Format` trait. Add it to Cargo.toml: ``` sh $ cargo add config ``` For the meantime, we're just going to create a new file, called `configuration.rs`, and put our configuration details in there. Right now we have a single configuration detail: the port. I'm going to go above and beyond Lucas here and configure some internal defaults for my code. It will have expectations. First, you have to tell Serde that there will be default values: ``` rust use config::Config; pub struct Settings { pub port: u16, } ``` Then, you have to set those default values. Fortunately, Rust provides a "set default values" trait named, sensibly enough, Default: ``` rust impl Default for Settings { fn default() -> Self { Settings { port: 3001 } } } ``` Again, exceeding the book's parameters, I'm going to say that if the file is missing the default parameters should hold: ``` rust pub(crate) fn get_configuration() -> Result<Settings, config::ConfigError> { Config::builder() .add_source(config::File::with_name("./ztd.config").required(false)) .build()? .try_deserialize() } ``` And since this is the first time I'm doing this, I'm going to write a test to assert that my understanding of how this all works is correct: ``` rust mod tests { use super::*; #[test] fn test_for_defaults() { let maybe_config = get_configuration(); assert!(!maybe_config.is_err()); let config = maybe_config.unwrap(); assert_eq!(config.port, 3001); } } ```
2023-03-24 14:51:19 +00:00
}
}
2023-03-24 19:22:28 +00:00
pub fn get_configuration() -> Result<Settings, config::ConfigError> {
Implemented the forms reader, config, and database migrations. This chapter introduces the Actix "Extractors" for retrieving form data. I've added tests to the `./tests` folder to attempt to interact with those extractors; as of this commit, a89cbe5b, they fail because the example code isn't there. What is there is a variant of the "Hello, World!" code from the previous exercises (section 3.5), which uses the Actix extractor: ``` rust // Actix, *not* Axum. Does not work with the current framework. fn index(form: web::Form<FormData>) -> String { format!('Welcome {}!', form.username) } ``` Translated and polished into Axum, it translates to: ``` rust 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, {}!\n", &username)) } ``` The Axum version is a little smarter, providing a default "World!" if you don't specify a name. That's what `.map_or` does, although the `or` part actually comes first in the function. So the result is: ``` sh $ curl http://localhost:3000/ Hello, World! $ curl 'http://localhost:3000/?username=Spock' Hello, Spock! ``` Which is more or less the version we want. **Section 3.7.3** then goes into some detail on the implementation of a Trait. A Trait in Rust is like an Interface in other languages; it describes a collection of functions for manipulating the values found in a defined Type. **Types**: A Type is just a description of the value: `u16` is a sixteen-bit unsigned integer; `char` is a Unicode character, and it's size is always 32bits, but a `String` is a bunch of things: it's an array of characters (which are not always `char`!), a length for that string and a capacity. If the String is manipulated to exceed the capacity, the array is re-allocated to a new capacity and the old array copied into it. A `Vec<String>` is an array of Strings; as a Type, it is considered to have a single value: whatever is in it at the moment you use it. **Trait**: A Trait defines a collection of one or more functions that can operate on a value. The truly nifty thing about Traits is that they can be implemented after the fact. By importing a Trait and an implementation of that trait specific to a type into a module containing that type, you can extend the behavior of a type in a deterministic way without having to modify or inherit the code, as you would in an object-oriented language. Axum has a valuable trait, `FromRequest`. For any structure you can imagine passing from the client to the server, you can implement `FromRequest` for that object and any content in the body of the message will be transformed into that structure. We've seen a trait before: `IntoResponse`, written as `impl IntoResponse`, and is the output (the return type) of many of the functions that produce return values for our application server. In this case the return type instructs Rust to look in the current lexical scope and, for the value returned by that function, determine if an `IntoResponse` trait has been defined for it. If it has, the value will be returned because Axum has now been assured that there exists a function to convert that value into something streamable and usable as an HTTP response. Fortunately for us, Axum has already implemented `FromRequest` for all the native data types, as well as some structures and arrays. Even better, it has implemented `FromRequest` for the Serde serialization/deserialization library. So in this example: ``` rust pub struct FormData { username: String, } pub async fn index(payload: Option<Form<FormData>>) -> impl IntoResponse { ... ``` A `Form` (something using the `application/x-www-form-urlencoded` protocol) of `FormData` will automatically be converted into a `payload` object of `{ username: "Spock" )`, and in this case wrapped in a `Some()` handler. (Or `None`, if there was no form included.) <aside>So far, there's not too much bloat in this product; with all the debugging symbols, it's 60MB or so, but stripped to the bone it's only 3.1MB, tolerable for modern deployments.</aside> First, though, we must adjust our `valid_subscription` test: ``` rust let body = "name=le%20guin&email=ursula_le_guin%40gmail.com"; let response = hyper::Client::new() .request( Request::builder() .method("POST") .header("content-type", "application/x-www-form-urlencoded") .uri(format!("http://{}/subscriptions", addr)) .body(Body::from(body)) .unwrap(), ) .await .expect("Failed to execute request."); ``` Two updates from the book: first, we're sending it via POST instead of GET. This is the correct way to do things; a GET should never (and I mean *never*) cause a change of state on the back-end. To send something new that the server will process and store, you use a POST. (To update something, or to send something to a known *and unique* URI, PUT is better.) Secondly, since we're using a generic form-data object, we need to set the content-type on the client so that the server is informed of how to unpack this payload. The '%20' and '%40' markers in the `body` are the space and the `@` respectively. I completely ignored the advice in the book and went instead with [Dbmate](https://github.com/amacneil/dbmate); Dbmate is a bit cranky; your SQL must be very much nestled against the 'up' and 'down' markers in the migration file, and it seems to be quite opinionated about everything being lowercase. That said, it was trivial to create a database with it: ``` sh $ dbmate new create_subscriptions_table ``` This will create the folder `db/migrations/20230322174957_create_subscriptions_table.sql`, (The timestamp will be different, obviously), and in this file you put the following, as specified in the book: ``` sql -- migrate:up create table subscriptions ( id uuid not null, primary key (id), email text not null unique, name text not null, subscribed_at timestamptz not null ); -- migrate:down drop table subscriptions; ``` To use Dbmate, you have to specify how to connect. I'm using Postgres, so let's start with creating a database and a user for it: ``` sh $ sudo -u postgres psql [sudo] possword for user: ................... postgres=# create database newsletter; CREATE DATABASE postgres=# create user newletter with encrypted password 'redacted'; CREATE USER postgres=# grant all privileges on database newsletter to newsletter; GRANT postgres=# exit ``` In your project root, create a `.env` file to specify your connection: ``` sh DATABASE_URL="postgres://newsletter:redacted@127.0.0.1:5432/newsletter?sslmode=disable" ``` The `sslmode` flag there is necessary for localhost connections, as Dbmate assumes an encrypted connection by default, but we're isolating to a local connection that is, usually, safe. With the new entry in your `.env` file, you can now run a migration: ``` sh $ dbmate up Writing: ./db/schema.sql ``` Running `dbmate up` will automatically create the database for you if it hasn't already; `dbmate migrate` also performs migrations, but it will not create the database. Now you can re-connect to Postgres as the newsletter user and see what you've got: ``` sh $ psql --user newsletter -h localhost --password Password: psql (14.7 (Ubuntu 14.7-0ubuntu0.22.04.1), server 11.7 (Ubuntu 11.7-0ubuntu0.19.10.1)) newsletter=> \d List of relations Schema | Name | Type | Owner --------+-------------------+-------+------------ public | schema_migrations | table | newsletter public | subscriptions | table | newsletter (2 rows) newsletter=> \d subscriptions Table "public.subscriptions" Column | Type | Collation | Nullable | Default ---------------+--------------------------+-----------+----------+--------- id | uuid | | not null | email | text | | not null | name | text | | not null | subscribed_at | timestamp with time zone | | not null | Indexes: "subscriptions_pkey" PRIMARY KEY, btree (id) "subscriptions_email_key" UNIQUE CONSTRAINT, btree (email) ``` Note that Dbmate has allocated a table to itself, `schema_migrations`, for tracking what it's done to your system and when. Try not to conflict with it, okay? Every complex app has a configuration, and there are plenty of different ways the configuration can be specified. Environment variables, internal defaults, and configuration files-- the last of which comes in so many different flavors. Rust has a well-known [config](https://docs.rs/config/latest/config/index.html) crate that supports all the most common configurations: YAML, JSON, TOML; you can even add your own by writing something that implements the `config::Format` trait. Add it to Cargo.toml: ``` sh $ cargo add config ``` For the meantime, we're just going to create a new file, called `configuration.rs`, and put our configuration details in there. Right now we have a single configuration detail: the port. I'm going to go above and beyond Lucas here and configure some internal defaults for my code. It will have expectations. First, you have to tell Serde that there will be default values: ``` rust use config::Config; pub struct Settings { pub port: u16, } ``` Then, you have to set those default values. Fortunately, Rust provides a "set default values" trait named, sensibly enough, Default: ``` rust impl Default for Settings { fn default() -> Self { Settings { port: 3001 } } } ``` Again, exceeding the book's parameters, I'm going to say that if the file is missing the default parameters should hold: ``` rust pub(crate) fn get_configuration() -> Result<Settings, config::ConfigError> { Config::builder() .add_source(config::File::with_name("./ztd.config").required(false)) .build()? .try_deserialize() } ``` And since this is the first time I'm doing this, I'm going to write a test to assert that my understanding of how this all works is correct: ``` rust mod tests { use super::*; #[test] fn test_for_defaults() { let maybe_config = get_configuration(); assert!(!maybe_config.is_err()); let config = maybe_config.unwrap(); assert_eq!(config.port, 3001); } } ```
2023-03-24 14:51:19 +00:00
Config::builder()
2023-03-24 19:22:28 +00:00
.add_source(config::File::with_name("./ztp.config").required(false))
Implemented the forms reader, config, and database migrations. This chapter introduces the Actix "Extractors" for retrieving form data. I've added tests to the `./tests` folder to attempt to interact with those extractors; as of this commit, a89cbe5b, they fail because the example code isn't there. What is there is a variant of the "Hello, World!" code from the previous exercises (section 3.5), which uses the Actix extractor: ``` rust // Actix, *not* Axum. Does not work with the current framework. fn index(form: web::Form<FormData>) -> String { format!('Welcome {}!', form.username) } ``` Translated and polished into Axum, it translates to: ``` rust 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, {}!\n", &username)) } ``` The Axum version is a little smarter, providing a default "World!" if you don't specify a name. That's what `.map_or` does, although the `or` part actually comes first in the function. So the result is: ``` sh $ curl http://localhost:3000/ Hello, World! $ curl 'http://localhost:3000/?username=Spock' Hello, Spock! ``` Which is more or less the version we want. **Section 3.7.3** then goes into some detail on the implementation of a Trait. A Trait in Rust is like an Interface in other languages; it describes a collection of functions for manipulating the values found in a defined Type. **Types**: A Type is just a description of the value: `u16` is a sixteen-bit unsigned integer; `char` is a Unicode character, and it's size is always 32bits, but a `String` is a bunch of things: it's an array of characters (which are not always `char`!), a length for that string and a capacity. If the String is manipulated to exceed the capacity, the array is re-allocated to a new capacity and the old array copied into it. A `Vec<String>` is an array of Strings; as a Type, it is considered to have a single value: whatever is in it at the moment you use it. **Trait**: A Trait defines a collection of one or more functions that can operate on a value. The truly nifty thing about Traits is that they can be implemented after the fact. By importing a Trait and an implementation of that trait specific to a type into a module containing that type, you can extend the behavior of a type in a deterministic way without having to modify or inherit the code, as you would in an object-oriented language. Axum has a valuable trait, `FromRequest`. For any structure you can imagine passing from the client to the server, you can implement `FromRequest` for that object and any content in the body of the message will be transformed into that structure. We've seen a trait before: `IntoResponse`, written as `impl IntoResponse`, and is the output (the return type) of many of the functions that produce return values for our application server. In this case the return type instructs Rust to look in the current lexical scope and, for the value returned by that function, determine if an `IntoResponse` trait has been defined for it. If it has, the value will be returned because Axum has now been assured that there exists a function to convert that value into something streamable and usable as an HTTP response. Fortunately for us, Axum has already implemented `FromRequest` for all the native data types, as well as some structures and arrays. Even better, it has implemented `FromRequest` for the Serde serialization/deserialization library. So in this example: ``` rust pub struct FormData { username: String, } pub async fn index(payload: Option<Form<FormData>>) -> impl IntoResponse { ... ``` A `Form` (something using the `application/x-www-form-urlencoded` protocol) of `FormData` will automatically be converted into a `payload` object of `{ username: "Spock" )`, and in this case wrapped in a `Some()` handler. (Or `None`, if there was no form included.) <aside>So far, there's not too much bloat in this product; with all the debugging symbols, it's 60MB or so, but stripped to the bone it's only 3.1MB, tolerable for modern deployments.</aside> First, though, we must adjust our `valid_subscription` test: ``` rust let body = "name=le%20guin&email=ursula_le_guin%40gmail.com"; let response = hyper::Client::new() .request( Request::builder() .method("POST") .header("content-type", "application/x-www-form-urlencoded") .uri(format!("http://{}/subscriptions", addr)) .body(Body::from(body)) .unwrap(), ) .await .expect("Failed to execute request."); ``` Two updates from the book: first, we're sending it via POST instead of GET. This is the correct way to do things; a GET should never (and I mean *never*) cause a change of state on the back-end. To send something new that the server will process and store, you use a POST. (To update something, or to send something to a known *and unique* URI, PUT is better.) Secondly, since we're using a generic form-data object, we need to set the content-type on the client so that the server is informed of how to unpack this payload. The '%20' and '%40' markers in the `body` are the space and the `@` respectively. I completely ignored the advice in the book and went instead with [Dbmate](https://github.com/amacneil/dbmate); Dbmate is a bit cranky; your SQL must be very much nestled against the 'up' and 'down' markers in the migration file, and it seems to be quite opinionated about everything being lowercase. That said, it was trivial to create a database with it: ``` sh $ dbmate new create_subscriptions_table ``` This will create the folder `db/migrations/20230322174957_create_subscriptions_table.sql`, (The timestamp will be different, obviously), and in this file you put the following, as specified in the book: ``` sql -- migrate:up create table subscriptions ( id uuid not null, primary key (id), email text not null unique, name text not null, subscribed_at timestamptz not null ); -- migrate:down drop table subscriptions; ``` To use Dbmate, you have to specify how to connect. I'm using Postgres, so let's start with creating a database and a user for it: ``` sh $ sudo -u postgres psql [sudo] possword for user: ................... postgres=# create database newsletter; CREATE DATABASE postgres=# create user newletter with encrypted password 'redacted'; CREATE USER postgres=# grant all privileges on database newsletter to newsletter; GRANT postgres=# exit ``` In your project root, create a `.env` file to specify your connection: ``` sh DATABASE_URL="postgres://newsletter:redacted@127.0.0.1:5432/newsletter?sslmode=disable" ``` The `sslmode` flag there is necessary for localhost connections, as Dbmate assumes an encrypted connection by default, but we're isolating to a local connection that is, usually, safe. With the new entry in your `.env` file, you can now run a migration: ``` sh $ dbmate up Writing: ./db/schema.sql ``` Running `dbmate up` will automatically create the database for you if it hasn't already; `dbmate migrate` also performs migrations, but it will not create the database. Now you can re-connect to Postgres as the newsletter user and see what you've got: ``` sh $ psql --user newsletter -h localhost --password Password: psql (14.7 (Ubuntu 14.7-0ubuntu0.22.04.1), server 11.7 (Ubuntu 11.7-0ubuntu0.19.10.1)) newsletter=> \d List of relations Schema | Name | Type | Owner --------+-------------------+-------+------------ public | schema_migrations | table | newsletter public | subscriptions | table | newsletter (2 rows) newsletter=> \d subscriptions Table "public.subscriptions" Column | Type | Collation | Nullable | Default ---------------+--------------------------+-----------+----------+--------- id | uuid | | not null | email | text | | not null | name | text | | not null | subscribed_at | timestamp with time zone | | not null | Indexes: "subscriptions_pkey" PRIMARY KEY, btree (id) "subscriptions_email_key" UNIQUE CONSTRAINT, btree (email) ``` Note that Dbmate has allocated a table to itself, `schema_migrations`, for tracking what it's done to your system and when. Try not to conflict with it, okay? Every complex app has a configuration, and there are plenty of different ways the configuration can be specified. Environment variables, internal defaults, and configuration files-- the last of which comes in so many different flavors. Rust has a well-known [config](https://docs.rs/config/latest/config/index.html) crate that supports all the most common configurations: YAML, JSON, TOML; you can even add your own by writing something that implements the `config::Format` trait. Add it to Cargo.toml: ``` sh $ cargo add config ``` For the meantime, we're just going to create a new file, called `configuration.rs`, and put our configuration details in there. Right now we have a single configuration detail: the port. I'm going to go above and beyond Lucas here and configure some internal defaults for my code. It will have expectations. First, you have to tell Serde that there will be default values: ``` rust use config::Config; pub struct Settings { pub port: u16, } ``` Then, you have to set those default values. Fortunately, Rust provides a "set default values" trait named, sensibly enough, Default: ``` rust impl Default for Settings { fn default() -> Self { Settings { port: 3001 } } } ``` Again, exceeding the book's parameters, I'm going to say that if the file is missing the default parameters should hold: ``` rust pub(crate) fn get_configuration() -> Result<Settings, config::ConfigError> { Config::builder() .add_source(config::File::with_name("./ztd.config").required(false)) .build()? .try_deserialize() } ``` And since this is the first time I'm doing this, I'm going to write a test to assert that my understanding of how this all works is correct: ``` rust mod tests { use super::*; #[test] fn test_for_defaults() { let maybe_config = get_configuration(); assert!(!maybe_config.is_err()); let config = maybe_config.unwrap(); assert_eq!(config.port, 3001); } } ```
2023-03-24 14:51:19 +00:00
.build()?
.try_deserialize()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_for_defaults() {
let maybe_config = get_configuration();
assert!(!maybe_config.is_err());
let config = maybe_config.unwrap();
assert_eq!(config.port, 3001);
}
}