Skip to content

Commit

Permalink
fix, improve and expand http form example
Browse files Browse the repository at this point in the history
  • Loading branch information
glendc committed Apr 4, 2024
1 parent 166d62c commit 7ab08ad
Show file tree
Hide file tree
Showing 3 changed files with 156 additions and 27 deletions.
101 changes: 86 additions & 15 deletions examples/http_form.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,51 +9,122 @@
//! # Run the example
//!
//! ```sh
//! cargo run --example http_form
//! RUST_LOG=debug cargo run --example http_form
//! ```
//!
//! # Expected output
//!
//! The server will start and listen on `:8080`. You can use `curl` to check if the server is running:
//!
//! ```sh
//! curl -X POST -F 'name=John' -F 'age=32' http://127.0.0.1:8080/form
//! curl -X POST http://127.0.0.1:8080/form \
//! -H "Content-Type: application/x-www-form-urlencoded" \
//! -d "name=John&age=32"
//!
//! curl -v 'http://127.0.0.1:8080/form?name=John&age=32'
//! ```
//!
//! You should see a response with `HTTP/1.1 200 OK` and `John is 32 years old.`.
//! You should see in both cases a response with `HTTP/1.1 200 OK` and `John is 32 years old.`.
//!
//! Alternatively you can
//!
//! ```
//! open http://127.0.0.1:8080
//! ```
//!
//! and fill the form in the browser, you should see a response page after submitting the form,
//! stating your name and age.
use rama::http::layer::trace::TraceLayer;
use rama::http::service::web::extract::Form;
use rama::http::service::web::WebService;
use rama::http::matcher::HttpMatcher;
use rama::http::response::Html;
use rama::http::service::web::{extract::Form, WebService};
use rama::http::{IntoResponse, Response};
use rama::service::ServiceBuilder;
use rama::{http::server::HttpServer, rt::Executor};
use serde::{Deserialize, Serialize};
use std::time::Duration;
use tracing::level_filters::LevelFilter;
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};

#[derive(Serialize, Deserialize, Debug)]
struct Payload {
name: String,
age: i32,
html: Option<bool>,
}

#[tokio::main]
async fn main() {
let exec = Executor::default();
HttpServer::auto(exec)
.listen(
"127.0.0.1:8080",
ServiceBuilder::new()
.layer(TraceLayer::new_for_http())
.service(WebService::default().post("/form", send_form_data)),
tracing_subscriber::registry()
.with(fmt::layer())
.with(
EnvFilter::builder()
.with_default_directive(LevelFilter::DEBUG.into())
.from_env_lossy(),
)
.init();

let graceful = rama::graceful::Shutdown::default();

graceful.spawn_task_fn(|guard| async move {
let exec = Executor::graceful(guard.clone());
HttpServer::auto(exec)
.listen_graceful(
guard,
"127.0.0.1:8080",
ServiceBuilder::new()
.layer(TraceLayer::new_for_http())
.service(
WebService::default()
.get(
"/",
Html(
r##"<html>
<body>
<form action="/form" method="post">
<label for="name">Name:</label><br>
<input type="text" id="name" name="name"><br>
<label for="age">Age:</label><br>
<input type="number" id="age" name="age"><br><br>
<input type="hidden" id="html" name="html" value="true"><br>
<input type="submit" value="Submit">
</form>
</body>
</html>"##,
),
)
.on(HttpMatcher::method_get().or_method_post().and_path("/form"), send_form_data),
),
)
.await
.expect("failed to run service");
});

graceful
.shutdown_with_limit(Duration::from_secs(30))
.await
.expect("failed to run service");
.expect("graceful shutdown");
}

async fn send_form_data(Form(payload): Form<Payload>) -> String {
async fn send_form_data(Form(payload): Form<Payload>) -> Response {
tracing::info!("{:?}", payload.name);

let name = payload.name;
let age = payload.age;

format!("{} is {} years old.", name, age)
if payload.html.unwrap_or_default() {
Html(format!(
r##"<html>
<body>
<h1>Success</h1>
<p>Thank you for submitting the form {}, {} years old.</p>
</body>
</html>"##,
name, age
))
.into_response()
} else {
format!("{} is {} years old.", name, age).into_response()
}
}
62 changes: 51 additions & 11 deletions src/http/service/web/endpoint/extract/body.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use super::FromRequest;
use crate::http::{self, dep::http_body_util::BodyExt, StatusCode};
use crate::http::{self, dep::http_body_util::BodyExt, Method, StatusCode};
use crate::service::Context;
use std::convert::Infallible;
use std::ops::{Deref, DerefMut};
Expand Down Expand Up @@ -141,15 +141,31 @@ where
type Rejection = StatusCode;

async fn from_request(_ctx: Context<S>, req: http::Request) -> Result<Self, Self::Rejection> {
let body = req.into_body();
match body.collect().await {
Ok(c) => {
let b = c.to_bytes();
let value =
serde_urlencoded::from_bytes(&b).map_err(|_| StatusCode::BAD_REQUEST)?;
Ok(Form(value))
if req.method() == Method::GET {
let value = match req.uri().query() {
Some(query) => serde_urlencoded::from_bytes(query.as_bytes()),
None => serde_urlencoded::from_bytes(&[]),
}
.map_err(|_| StatusCode::BAD_REQUEST)?;
Ok(Form(value))
} else {
if !super::has_any_content_type(
req.headers(),
&[&mime::APPLICATION_WWW_FORM_URLENCODED],
) {
return Err(StatusCode::BAD_REQUEST);
}

let body = req.into_body();
match body.collect().await {
Ok(c) => {
let b = c.to_bytes();
let value =
serde_urlencoded::from_bytes(&b).map_err(|_| StatusCode::BAD_REQUEST)?;
Ok(Form(value))
}
Err(_) => Err(StatusCode::BAD_REQUEST),
}
Err(_) => Err(StatusCode::BAD_REQUEST),
}
}
}
Expand Down Expand Up @@ -227,7 +243,30 @@ mod test {
}

#[tokio::test]
async fn test_form() {
async fn test_form_post_form_urlencoded() {
#[derive(Debug, serde::Deserialize)]
struct Input {
name: String,
age: u8,
}

let service = WebService::default().post("/", |Form(body): Form<Input>| async move {
assert_eq!(body.name, "Devan");
assert_eq!(body.age, 29);
});

let req = http::Request::builder()
.uri("/")
.method(http::Method::POST)
.header("content-type", "application/x-www-form-urlencoded")
.body(r#"name=Devan&age=29"#.into())
.unwrap();
let resp = service.serve(Context::default(), req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}

#[tokio::test]
async fn test_form_get() {
#[derive(Debug, serde::Deserialize)]
struct Input {
name: String,
Expand All @@ -240,8 +279,9 @@ mod test {
});

let req = http::Request::builder()
.uri("/?name=Devan&age=29")
.method(http::Method::GET)
.body(r#"name=Devan&age=29"#.into())
.body(http::Body::empty())
.unwrap();
let resp = service.serve(Context::default(), req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
Expand Down
20 changes: 19 additions & 1 deletion src/http/service/web/endpoint/extract/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Extract utilities to develop endpoint services efortless.
use crate::http::{self, dep::http::request::Parts, IntoResponse};
use crate::http::{self, dep::http::request::Parts, dep::mime, header, HeaderMap, IntoResponse};
use crate::service::Context;
use std::future::Future;

Expand Down Expand Up @@ -94,3 +94,21 @@ where
Self::from_request_parts(&ctx, &parts).await
}
}

fn has_any_content_type(headers: &HeaderMap, expected_content_types: &[&mime::Mime]) -> bool {
let content_type = if let Some(content_type) = headers.get(header::CONTENT_TYPE) {
content_type
} else {
return false;
};

let content_type = if let Ok(content_type) = content_type.to_str() {
content_type
} else {
return false;
};

expected_content_types
.iter()
.any(|ct| content_type.starts_with(ct.as_ref()))
}

0 comments on commit 7ab08ad

Please sign in to comment.