305 lines
11 KiB
Markdown
305 lines
11 KiB
Markdown
+++
|
|
title = "Forms and Serializers"
|
|
date = 2023-03-20T17:38:12Z
|
|
weight = 3
|
|
+++
|
|
|
|
## Chapter 3.7 (Sort-of): Forms and Serializations
|
|
|
|
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
|
|
#[derive(serde::Deserialize)]
|
|
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.
|
|
|
|
## Chapter 3.8.4: Initializing the database
|
|
|
|
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?
|
|
|
|
## Section 3.8.5: Reading in the Configuration
|
|
|
|
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;
|
|
|
|
#[derive(serde::Deserialize)]
|
|
#[serde(default)]
|
|
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, so the presence of a valid
|
|
configuration isn't a show-stopper and should not be required.
|
|
|
|
``` 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
|
|
#[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);
|
|
}
|
|
}
|
|
```
|