Skip to content

Commit

Permalink
Merge pull request #7 from wacker-dev/multipart
Browse files Browse the repository at this point in the history
Add initial support for handling multipart/form-data body
  • Loading branch information
iawia002 authored Jun 13, 2024
2 parents f5bb417 + 2297d87 commit ae533a1
Show file tree
Hide file tree
Showing 11 changed files with 457 additions and 36 deletions.
3 changes: 2 additions & 1 deletion test-programs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ edition = "2021"
publish = false

[dependencies]
waki = { path = "../waki", features = ["json"] }
waki = { path = "../waki", features = ["json", "multipart"] }
serde = { workspace = true, features = ["derive"] }
mime = "0.3.17"
21 changes: 21 additions & 0 deletions test-programs/src/bin/client_post_with_body.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
use serde::Deserialize;
use std::time::Duration;
use waki::Client;

#[derive(Deserialize)]
struct Data {
data: String,
}

fn main() {
let resp = Client::new()
.post("https://httpbin.org/post")
.body("hello")
.connect_timeout(Duration::from_secs(5))
.send()
.unwrap();
assert_eq!(resp.status_code(), 200);

let data = resp.json::<Data>().unwrap();
assert_eq!(data.data, "hello");
}
31 changes: 16 additions & 15 deletions test-programs/src/bin/client_post_with_multipart_form_data.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use serde::Deserialize;
use std::collections::HashMap;
use std::time::Duration;
use waki::Client;
use waki::{
multipart::{Form, Part},
Client,
};

#[derive(Deserialize)]
struct Data {
Expand All @@ -12,19 +15,16 @@ struct Data {
fn main() {
let resp = Client::new()
.post("https://httpbin.org/post")
.header("Content-Type", "multipart/form-data; boundary=boundary")
.body(
"--boundary
Content-Disposition: form-data; name=field1
value1
--boundary
Content-Disposition: form-data; name=field2; filename=file.txt
Content-Type: text/plain
hello
--boundary--"
.as_bytes(),
.multipart(
Form::new()
.text("field1", "value1")
.file("field2", "file.txt")
.unwrap()
.part(
Part::new("field3", "hello")
.filename("file.txt")
.mime(mime::TEXT_PLAIN),
),
)
.connect_timeout(Duration::from_secs(5))
.send()
Expand All @@ -33,5 +33,6 @@ hello

let data = resp.json::<Data>().unwrap();
assert_eq!(data.form.get("field1").unwrap(), "value1");
assert_eq!(data.files.get("field2").unwrap(), "hello");
assert_eq!(data.files.get("field2").unwrap(), "hello\n");
assert_eq!(data.files.get("field3").unwrap(), "hello");
}
7 changes: 7 additions & 0 deletions waki/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,16 @@ wit-bindgen = "0.26.0"
url = "2.5.0"
serde_urlencoded = "0.7.1"
serde_json = { version = "1.0.117", optional = true }
mime = { version = "0.3.17", optional = true }
mime_guess = { version = "2.0.4", optional = true }
rand = { version = "0.8.5", optional = true }
memchr = { version = "2.7.2", optional = true }
bytes = { version = "1.6.0", optional = true }
httparse = { version = "1.9.3", optional = true }

[features]
json = ["dep:serde_json"]
multipart = ["dep:mime", "dep:mime_guess", "dep:rand", "dep:memchr", "dep:bytes", "dep:httparse"]

[build-dependencies]
anyhow.workspace = true
Expand Down
98 changes: 80 additions & 18 deletions waki/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@
mod body;
mod client;
mod header;
#[cfg(feature = "multipart")]
#[cfg_attr(docsrs, doc(cfg(feature = "multipart")))]
pub mod multipart;
mod request;
mod response;

Expand Down Expand Up @@ -73,7 +76,9 @@ pub use self::{
pub use waki_macros::handler;

use crate::body::Body;
use anyhow::Result;
#[cfg(feature = "multipart")]
use crate::multipart::{parser::parse, Form, Part};
use anyhow::{anyhow, Result};
use serde::Serialize;
use std::collections::HashMap;

Expand Down Expand Up @@ -129,6 +134,33 @@ macro_rules! impl_common_get_methods {
pub fn json<T: serde::de::DeserializeOwned>(self) -> Result<T> {
Ok(serde_json::from_slice(self.body()?.as_ref())?)
}

/// Parse the body as multipart/form-data.
///
/// # Optional
///
/// This requires the `multipart` feature enabled.
#[cfg(feature = "multipart")]
#[cfg_attr(docsrs, doc(cfg(feature = "multipart")))]
pub fn multipart(self) -> Result<HashMap<String, Part>> {
match self.headers.get("Content-Type") {
Some(header) => {
let mime = header.parse::<mime::Mime>()?;
let boundary = match mime.get_param(mime::BOUNDARY) {
Some(v) => v.as_str(),
None => {
return Err(anyhow!(
"unable to find the boundary value in the Content-Type header"
))
}
};
parse(self.body()?.as_ref(), boundary)
}
None => Err(anyhow!(
"parse body as multipart failed, unable to find the Content-Type header"
)),
}
}
}
)+)
}
Expand All @@ -141,12 +173,10 @@ macro_rules! impl_common_set_methods {
/// Add a header.
///
/// ```
/// # use anyhow::Result;
/// # use waki::ResponseBuilder;
/// # fn run() -> Result<()> {
/// # fn run() {
/// # let r = ResponseBuilder::new();
/// r.header("Content-Type", "application/json");
/// # Ok(())
/// # }
/// ```
pub fn header<S: Into<String>>(mut self, key: S, value: S) -> Self {
Expand All @@ -159,12 +189,10 @@ macro_rules! impl_common_set_methods {
/// Add a set of headers.
///
/// ```
/// # use anyhow::Result;
/// # use waki::ResponseBuilder;
/// # fn run() -> Result<()> {
/// # fn run() {
/// # let r = ResponseBuilder::new();
/// r.headers([("Content-Type", "application/json"), ("Accept", "*/*")]);
/// # Ok(())
/// # }
/// ```
pub fn headers<S, I>(mut self, headers: I) -> Self
Expand All @@ -182,15 +210,13 @@ macro_rules! impl_common_set_methods {
/// Set the body.
///
/// ```
/// # use anyhow::Result;
/// # use waki::ResponseBuilder;
/// # fn run() -> Result<()> {
/// # fn run() {
/// # let r = ResponseBuilder::new();
/// r.body("hello".as_bytes());
/// # Ok(())
/// r.body("hello");
/// # }
/// ```
pub fn body(mut self, body: &[u8]) -> Self {
pub fn body<V: Into<Vec<u8>>>(mut self, body: V) -> Self {
if let Ok(ref mut inner) = self.inner {
inner.body = Body::Bytes(body.into());
}
Expand All @@ -204,13 +230,11 @@ macro_rules! impl_common_set_methods {
/// This requires the `json` feature enabled.
///
/// ```
/// # use anyhow::Result;
/// # use std::collections::HashMap;
/// # use waki::ResponseBuilder;
/// # fn run() -> Result<()> {
/// # fn run() {
/// # let r = ResponseBuilder::new();
/// r.json(&HashMap::from([("data", "hello")]));
/// # Ok(())
/// # }
/// ```
#[cfg(feature = "json")]
Expand All @@ -234,12 +258,10 @@ macro_rules! impl_common_set_methods {
/// Set a form body.
///
/// ```
/// # use anyhow::Result;
/// # use waki::ResponseBuilder;
/// # fn run() -> Result<()> {
/// # fn run() {
/// # let r = ResponseBuilder::new();
/// r.form(&[("a", "b"), ("c", "d")]);
/// # Ok(())
/// # }
/// ```
pub fn form<T: Serialize + ?Sized>(mut self, form: &T) -> Self {
Expand All @@ -259,6 +281,46 @@ macro_rules! impl_common_set_methods {
}
self
}

/// Set a multipart/form-data body.
///
/// # Optional
///
/// This requires the `multipart` feature enabled.
///
/// ```
/// # use anyhow::Result;
/// # use waki::ResponseBuilder;
/// # fn run() -> Result<()> {
/// # let r = ResponseBuilder::new();
/// let form = waki::multipart::Form::new()
/// // Add a text field
/// .text("key", "value")
/// // And a file
/// .file("file", "/path/to/file.txt")?
/// // And a custom part
/// .part(
/// waki::multipart::Part::new("key2", "value2")
/// .filename("file.txt")
/// .mime_str("text/plain")?,
/// );
///
/// r.multipart(form);
/// # Ok(())
/// # }
/// ```
#[cfg(feature = "multipart")]
#[cfg_attr(docsrs, doc(cfg(feature = "multipart")))]
pub fn multipart(mut self, form: Form) -> Self {
if let Ok(ref mut inner) = self.inner {
inner.headers.insert(
"Content-Type".into(),
format!("multipart/form-data; boundary={}", form.boundary()),
);
inner.body = Body::Bytes(form.build());
}
self
}
}
)+)
}
Expand Down
4 changes: 4 additions & 0 deletions waki/src/multipart/constants.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub const MAX_HEADERS: usize = 32;
pub const BOUNDARY_EXT: &str = "--";
pub const CRLF: &str = "\r\n";
pub const CRLF_CRLF: &str = "\r\n\r\n";
Loading

0 comments on commit ae533a1

Please sign in to comment.