Zero2Prod: Day 2 – Form, FromRequest, serde

A blog visitor can subscribe to the newsletter

  • She can submit a form with name and email.
  • If both name and email are valid, we should return 200 OK
  • Otherwise, return 400 Bad Request

Writing tests

Build a multipart form:

let form = multipart::Form::new()
    .text("name", "Oliver Nguyen")
    .text("email", "oliver@example.com");

Create a new client and send the request:

let addr = spawn_app();

let client = reqwest::Client::new();
let resp = client
	.post(&format!("{}/subscribe", addr))
	.multipart(form)
	.send()
	.await
	.expect("Failed to send request");

Define FormData with #[derive(Serialize)]

#[derive(Serialize)]
struct FormData {
    name: String,
    email: String,
}

Add some test cases:

async fn subscribe_returns_a_400_for_invalid_form_data() {
    struct TestCase {
        name: String,
        email: String,
        expected_error: String,
    }

    let addr = spawn_app();
    let client = reqwest::Client::new();
    let testcases = vec![
        TestCase {
            name: "".to_string(),
            email: "oliver@example.com".to_string(),
            expected_error: "missing name".to_string(),
        },
        TestCase {
            name: "Oliver Nguyen".to_string(),
            email: "".to_string(),
            expected_error: "missing email".to_string(),
        },
        TestCase {
            name: "".to_string(),
            email: "".to_string(),
            expected_error: "missing name and email".to_string(),
        },
    ];

    for tc in testcases {
        let form = FormData {
            name: tc.name,
            email: tc.email,
        };

        let resp = client
            .post(&format!("{}/subscribe", addr))
            // 👇 .form() will set Content-Type: x-www-form-urlencoded
            .form(&form)
            .send()
            .await
            .expect("Failed to send request");

        assert_eq!(
            resp.status().as_u16(),
            400,
            "expected 400, got {}",
            resp.status()
        );
        // ... more checks
    }
}

Implement /subscribe

  • Add a new route /subscribe
  • Declare a struct SubscribeRequest to store the form data
  • Use #[derive(serde::Deserialize)]
  • web::Form<SubscribeRequest> implements Handler<FromRequest>
App::new().route("/subscribe", web::post().to(subscribe))

#[derive(serde::Deserialize)]
pub struct SubscribeForm {
    pub email: String,
    pub name: String,
}

// 👇 web::Form<...> expects Content-Type: x-www-form-urlencoded
pub async fn subscribe(form: web::Form<SubscribeForm>) -> impl Responder {
match (form.name.is_empty(), form.email.is_empty()) {
        (true, true) => {
            return HttpResponse::BadRequest().json(json!({ "error": "missing name and email" }));
        }
        (false, true) => {
            return HttpResponse::BadRequest().json(json!({ "error": "missing email" }));
        }
        (true, false) => {
            return HttpResponse::BadRequest().json(json!({ "error": "missing name" }));
        }
        (false, false) => HttpResponse::Ok()
            .json(json!({ "name": form.name, "email": form.email, "status": "ok" })),
    }
}

An example implement of Serialize trait for Vec<T>

use serde::ser::{Serialize, Serializer, SerializeSeq};
impl<T> Serialize for Vec<T>
where
    T: Serialize,
{
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let mut seq = serializer.serialize_seq(Some(self.len()))?;
        for element in self {
            seq.serialize_element(element)?;
        }
        seq.end()
    }
}

Form and FromRequest

#[derive(PartialEq, Eq, PartialOrd, Ord)]
pub struct Form<T>(pub T);

pub trait FromRequest: Sized {
    type Error: Into<Error>;
    type Future: Future<Output = Result<Self, Self::Error>>;
    
    fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future;

	fn extract(req: &HttpRequest) -> Self::Future {
        Self::from_request(req, &mut Payload::None)
    }
}

impl<T> FromRequest for Form<T>
where
    T: DeserializeOwned + 'static,
{
    type Error = Error;
    type Future = FormExtractFut<T>;

    #[inline]
    fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
        let FormConfig { limit, err_handler } = FormConfig::from_req(req).clone();

        FormExtractFut {
            fut: UrlEncoded::new(req, payload).limit(limit),
            req: req.clone(),
            err_handler,
        }
    }
}

We can think about fn from_request() as async fn from_request():

fn from_request(
    req: &HttpRequest, 
    payload: &mut Payload
) -> Self::Future

// similar to this pseudo code (async is not supported in trait yet)
async fn from_request(
	req: &HttpRequest, 
    payload: &mut Payload
) -> Result<Self, Self::Error>

FormExtractFut<T> implements Future<T> with fn poll():

impl<T> Future for FormExtractFut<T>
where
    T: DeserializeOwned + 'static,
{
    type Output = Result<Form<T>, Error>;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        let this = self.get_mut();
        let res = ready!(Pin::new(&mut this.fut).poll(cx));
        let res = match res {
            Err(err) => match &this.err_handler {
                Some(err_handler) => Err((err_handler)(err, &this.req)),
                None => Err(err.into()),
            },
            Ok(item) => Ok(Form(item)),
        };
        Poll::Ready(res)
    }
}

Notes

  • Using web::Form<SubscribeForm> in handler will verify that the request header is x-www-form-urlencoded.
  • Using .form() in client request will set request header as x-www-form-urlencoded.

Parse JSON with serde and write assertions

Using serde_json::Value and access as resp_body["error"]:

let resp_body: serde_json::Value = resp.json().await.expect("Failed to parse JSON");
let error_message = resp_body["error"]
	.as_str()
	.expect("Failed to get error message");
assert_eq!(
	error_message, tc.expected_error,
	"expected error message '{}', got '{}'",
	tc.expected_error, error_message
);

Define and parse with a struct:

#[derive(Deserialize)]
struct SubscribeError {
    error: String,
}

// Parse the response body as JSON
let resp_body: SubscribeError = resp.json().await.expect("Failed to parse JSON");
assert_eq!(
	resp_body.error, tc.expected_error,
	"expected error message '{}', got '{}'",
	tc.expected_error, resp_body.error
);

Notes

  • serde is a framework for (de)serialization, providing a set of interfaces: Serialize, Serializer, etc.
  • serde_json provides implementation for JSON
  • It’s efficient due to monomorphization, which generate different code for each concrete type.
    • Benefits: zero-cost abstraction - we do not pay any runtime code for generic.
    • Downside: the binary will be bigger, compile time is longer.
  • Understanding Serde by Joshua Mcguigan

Author

I'm Oliver Nguyen. A software maker working mostly in Go and JavaScript. I enjoy learning and seeing a better version of myself each day. Occasionally spin off new open source projects. Share knowledge and thoughts during my journey. Connect with me on , , , and .

Back Back