Skip to content

Commit

Permalink
Merge pull request #8 from wacker-dev/headers
Browse files Browse the repository at this point in the history
Use HeaderMap instead of HashMap
  • Loading branch information
iawia002 authored Jun 14, 2024
2 parents ae533a1 + 27c45c7 commit 67c450f
Show file tree
Hide file tree
Showing 14 changed files with 427 additions and 329 deletions.
27 changes: 27 additions & 0 deletions test-programs/src/bin/client_get_headers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
use serde::Deserialize;
use std::collections::HashMap;
use waki::{header::CONTENT_TYPE, Client};

#[derive(Deserialize)]
struct Data {
headers: HashMap<String, String>,
}

fn main() {
let resp = Client::new()
.get("https://httpbin.org/get")
.header("Test", "test")
.headers([("A", "b"), ("C", "d")])
.send()
.unwrap();
assert_eq!(resp.status_code(), 200);
assert_eq!(
resp.header(CONTENT_TYPE).unwrap().to_str().unwrap(),
"application/json"
);

let data = resp.json::<Data>().unwrap();
assert_eq!(data.headers.get("Test").unwrap(), "test");
assert_eq!(data.headers.get("A").unwrap(), "b");
assert_eq!(data.headers.get("C").unwrap(), "d");
}
1 change: 0 additions & 1 deletion test-programs/src/bin/client_get_with_query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ struct Data {
fn main() {
let resp = Client::new()
.get("https://httpbin.org/get?a=b")
.headers([("Content-Type", "application/json"), ("Accept", "*/*")])
.send()
.unwrap();
assert_eq!(resp.status_code(), 200);
Expand Down
7 changes: 5 additions & 2 deletions waki-macros/src/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ pub fn handler(input: ItemFn) -> Result<TokenStream> {

impl ::waki::bindings::exports::wasi::http::incoming_handler::Guest for Component {
fn handle(request: ::waki::bindings::wasi::http::types::IncomingRequest, response_out: ::waki::bindings::wasi::http::types::ResponseOutparam) {
match #fn_name(request.into()) {
Ok(resp) => ::waki::handle_response(response_out, resp),
match request.try_into() {
Ok(req) => match #fn_name(req) {
Ok(resp) => ::waki::handle_response(response_out, resp),
Err(e) => ::waki::bindings::wasi::http::types::ResponseOutparam::set(response_out, Err(e)),
}
Err(e) => ::waki::bindings::wasi::http::types::ResponseOutparam::set(response_out, Err(e)),
}
}
Expand Down
1 change: 1 addition & 0 deletions waki/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ anyhow.workspace = true
serde.workspace = true
wit-bindgen = "0.26.0"
url = "2.5.0"
http = "1.1.0"
serde_urlencoded = "0.7.1"
serde_json = { version = "1.0.117", optional = true }
mime = { version = "0.3.17", optional = true }
Expand Down
34 changes: 34 additions & 0 deletions waki/src/common/header.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use crate::{
bindings::wasi::http::types::{HeaderError, Headers, IncomingRequest, IncomingResponse},
header::HeaderMap,
};
use anyhow::Result;

macro_rules! impl_header {
($($t:ty),+ $(,)?) => ($(
impl $t {
pub fn headers_map(&self) -> Result<HeaderMap> {
let headers_handle = self.headers();
headers_handle
.entries()
.into_iter()
.map(|(key, value)| Ok((key.try_into()?, value.try_into()?)))
.collect::<Result<_, _>>()
}
}
)+)
}

impl_header!(IncomingRequest, IncomingResponse);

impl TryFrom<HeaderMap> for Headers {
type Error = HeaderError;

fn try_from(headers: HeaderMap) -> Result<Self, Self::Error> {
let entries = headers
.iter()
.map(|(k, v)| (k.to_string(), v.as_bytes().into()))
.collect::<Vec<_>>();
Headers::from_list(&entries)
}
}
2 changes: 2 additions & 0 deletions waki/src/common/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
mod header;
mod request_and_response;
290 changes: 290 additions & 0 deletions waki/src/common/request_and_response.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
#[cfg(feature = "multipart")]
use crate::multipart::{parser::parse, Form, Part};
use crate::{
body::Body,
header::{AsHeaderName, HeaderMap, HeaderValue, IntoHeaderName, CONTENT_TYPE},
Request, RequestBuilder, Response, ResponseBuilder,
};
use anyhow::{anyhow, Error, Result};
use serde::Serialize;
use std::collections::HashMap;

macro_rules! impl_common_get_methods {
($($t:ty),+ $(,)?) => ($(
impl $t {
/// Get the header.
pub fn header<K: AsHeaderName>(&self, key: K) -> Option<&HeaderValue> {
self.headers.get(key)
}

/// Get headers.
pub fn headers(&self) -> &HeaderMap {
&self.headers
}

/// Get a chunk of the body.
///
/// It will block until at least one byte can be read or the stream is closed.
///
/// NOTE: This method is only for incoming requests/responses, if you call it on an
/// outgoing request/response it will always return None.
pub fn chunk(&self, len: u64) -> Result<Option<Vec<u8>>> {
self.body.chunk(len)
}

/// Get the full body.
///
/// It will block until the stream is closed.
pub fn body(self) -> Result<Vec<u8>> {
self.body.bytes()
}

/// Deserialize the body as JSON.
///
/// # Optional
///
/// This requires the `json` feature enabled.
///
/// ```
/// # use anyhow::Result;
/// # use serde::Deserialize;
/// # use waki::Response;
/// # fn run() -> Result<()> {
/// # let r = Response::new();
/// #[derive(Deserialize)]
/// struct Data {
/// origin: String,
/// url: String,
/// }
///
/// let json_data = r.json::<Data>()?;
/// # Ok(())
/// # }
/// ```
#[cfg(feature = "json")]
#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
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.to_str()?.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"
)),
}
}
}
)+)
}

impl_common_get_methods!(Request, Response);

macro_rules! impl_common_set_methods {
($($t:ty),+ $(,)?) => ($(
impl $t {
/// Add a header.
///
/// ```
/// # use waki::ResponseBuilder;
/// # fn run() {
/// # let r = ResponseBuilder::new();
/// r.header("Content-Type", "application/json");
/// # }
/// ```
pub fn header<K, V>(mut self, key: K, value: V) -> Self
where
K: IntoHeaderName,
V: TryInto<HeaderValue>,
<V as TryInto<HeaderValue>>::Error: Into<Error>,
{
let mut err = None;
if let Ok(ref mut inner) = self.inner {
match value.try_into().map_err(|e| e.into()) {
Ok(v) => {
inner.headers.insert(key, v);
}
Err(e) => err = Some(e),
};
}
if let Some(e) = err {
self.inner = Err(e);
}
self
}

/// Add a set of headers.
///
/// ```
/// # use waki::ResponseBuilder;
/// # fn run() {
/// # let r = ResponseBuilder::new();
/// r.headers([("Content-Type", "application/json"), ("Accept", "*/*")]);
/// # }
/// ```
pub fn headers<K, V, I>(mut self, headers: I) -> Self
where
K: IntoHeaderName,
V: TryInto<HeaderValue>,
<V as TryInto<HeaderValue>>::Error: Into<Error>,
I: IntoIterator<Item = (K, V)>,
{
let mut err = None;
if let Ok(ref mut inner) = self.inner {
for (key, value) in headers.into_iter() {
match value.try_into().map_err(|e| e.into()) {
Ok(v) => {
inner.headers.insert(key, v);
}
Err(e) => {
err = Some(e);
break;
}
};
}
}
if let Some(e) = err {
self.inner = Err(e);
}
self
}

/// Set the body.
///
/// ```
/// # use waki::ResponseBuilder;
/// # fn run() {
/// # let r = ResponseBuilder::new();
/// r.body("hello");
/// # }
/// ```
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());
}
self
}

/// Set a JSON body.
///
/// # Optional
///
/// This requires the `json` feature enabled.
///
/// ```
/// # use std::collections::HashMap;
/// # use waki::ResponseBuilder;
/// # fn run() {
/// # let r = ResponseBuilder::new();
/// r.json(&HashMap::from([("data", "hello")]));
/// # }
/// ```
#[cfg(feature = "json")]
#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
pub fn json<T: Serialize + ?Sized>(mut self, json: &T) -> Self {
let mut err = None;
if let Ok(ref mut inner) = self.inner {
inner.headers.insert(CONTENT_TYPE, "application/json".parse().unwrap());
match serde_json::to_vec(json) {
Ok(data) => inner.body = Body::Bytes(data),
Err(e) => err = Some(e.into()),
}
}
if let Some(e) = err {
self.inner = Err(e);
}
self
}

/// Set a form body.
///
/// ```
/// # use waki::ResponseBuilder;
/// # fn run() {
/// # let r = ResponseBuilder::new();
/// r.form(&[("a", "b"), ("c", "d")]);
/// # }
/// ```
pub fn form<T: Serialize + ?Sized>(mut self, form: &T) -> Self {
let mut err = None;
if let Ok(ref mut inner) = self.inner {
inner.headers.insert(
CONTENT_TYPE,
"application/x-www-form-urlencoded".parse().unwrap(),
);
match serde_urlencoded::to_string(form) {
Ok(data) => inner.body = Body::Bytes(data.into()),
Err(e) => err = Some(e.into()),
}
}
if let Some(e) = err {
self.inner = Err(e);
}
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,
format!("multipart/form-data; boundary={}", form.boundary())
.parse()
.unwrap(),
);
inner.body = Body::Bytes(form.build());
}
self
}
}
)+)
}

impl_common_set_methods!(RequestBuilder, ResponseBuilder);
Loading

0 comments on commit 67c450f

Please sign in to comment.