Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(wasm): add support for wasip2, aka webassembly components, using wasi-http #2290

Closed
Prev Previous commit
Next Next commit
feat(wasm): add wasip2 component support
Signed-off-by: Brooks Townsend <brooksmtownsend@gmail.com>

wip: feat(wasm): add component feature

Signed-off-by: Brooks Townsend <brooksmtownsend@gmail.com>
  • Loading branch information
brooksmtownsend committed Aug 15, 2024
commit ea2473e04322f3d2bb173780ca50bd0ecdccdf10
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ socks = ["dep:tokio-socks"]
# Use the system's proxy configuration.
macos-system-configuration = ["dep:system-configuration"]

# Wasm features
# Build Wasm as a wasi-p2 component with wasi-http bindings.
wasm-component = ["dep:wit-bindgen"]

# Experimental HTTP/3 client.
http3 = ["rustls-tls-manual-roots", "dep:h3", "dep:h3-quinn", "dep:quinn", "dep:slab", "dep:futures-channel"]

Expand Down Expand Up @@ -194,6 +198,7 @@ serde_json = "1.0"
wasm-bindgen = "0.2.68"
wasm-bindgen-futures = "0.4.18"
wasm-streams = { version = "0.4", optional = true }
wit-bindgen = { version = "0.24.0", optional = true }

[target.'cfg(target_arch = "wasm32")'.dependencies.web-sys]
version = "0.3.28"
Expand Down
311 changes: 311 additions & 0 deletions src/wasm/component/body.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
#[cfg(feature = "multipart")]
use super::multipart::Form;
/// dox
use bytes::Bytes;
use js_sys::Uint8Array;
use std::{borrow::Cow, fmt};
use wasm_bindgen::JsValue;

/// The body of a `Request`.
///
/// In most cases, this is not needed directly, as the
/// [`RequestBuilder.body`][builder] method uses `Into<Body>`, which allows
/// passing many things (like a string or vector of bytes).
///
/// [builder]: ./struct.RequestBuilder.html#method.body
pub struct Body {
inner: Inner,
}

enum Inner {
Single(Single),
/// MultipartForm holds a multipart/form-data body.
#[cfg(feature = "multipart")]
MultipartForm(Form),
}

#[derive(Clone)]
pub(crate) enum Single {
Bytes(Bytes),
Text(Cow<'static, str>),
}

impl Single {
fn as_bytes(&self) -> &[u8] {
match self {
Single::Bytes(bytes) => bytes.as_ref(),
Single::Text(text) => text.as_bytes(),
}
}

#[allow(unused)]
pub(crate) fn to_js_value(&self) -> JsValue {
match self {
Single::Bytes(bytes) => {
let body_bytes: &[u8] = bytes.as_ref();
let body_uint8_array: Uint8Array = body_bytes.into();
let js_value: &JsValue = body_uint8_array.as_ref();
js_value.to_owned()
}
Single::Text(text) => JsValue::from_str(text),
}
}

fn is_empty(&self) -> bool {
match self {
Single::Bytes(bytes) => bytes.is_empty(),
Single::Text(text) => text.is_empty(),
}
}
}

impl Body {
/// Returns a reference to the internal data of the `Body`.
///
/// `None` is returned, if the underlying data is a multipart form.
#[inline]
pub fn as_bytes(&self) -> Option<&[u8]> {
match &self.inner {
Inner::Single(single) => Some(single.as_bytes()),
#[cfg(feature = "multipart")]
Inner::MultipartForm(_) => None,
}
}

#[allow(unused)]
pub(crate) fn to_js_value(&self) -> crate::Result<JsValue> {
match &self.inner {
Inner::Single(single) => Ok(single.to_js_value()),
#[cfg(feature = "multipart")]
Inner::MultipartForm(form) => {
let form_data = form.to_form_data()?;
let js_value: &JsValue = form_data.as_ref();
Ok(js_value.to_owned())
}
}
}

#[cfg(feature = "multipart")]
pub(crate) fn as_single(&self) -> Option<&Single> {
match &self.inner {
Inner::Single(single) => Some(single),
Inner::MultipartForm(_) => None,
}
}

#[inline]
#[cfg(feature = "multipart")]
pub(crate) fn from_form(f: Form) -> Body {
Self {
inner: Inner::MultipartForm(f),
}
}

/// into_part turns a regular body into the body of a multipart/form-data part.
#[cfg(feature = "multipart")]
pub(crate) fn into_part(self) -> Body {
match self.inner {
Inner::Single(single) => Self {
inner: Inner::Single(single),
},
Inner::MultipartForm(form) => Self {
inner: Inner::MultipartForm(form),
},
}
}

#[allow(unused)]
pub(crate) fn is_empty(&self) -> bool {
match &self.inner {
Inner::Single(single) => single.is_empty(),
#[cfg(feature = "multipart")]
Inner::MultipartForm(form) => form.is_empty(),
}
}

pub(crate) fn try_clone(&self) -> Option<Body> {
match &self.inner {
Inner::Single(single) => Some(Self {
inner: Inner::Single(single.clone()),
}),
#[cfg(feature = "multipart")]
Inner::MultipartForm(_) => None,
}
}
}

impl From<Bytes> for Body {
#[inline]
fn from(bytes: Bytes) -> Body {
Body {
inner: Inner::Single(Single::Bytes(bytes)),
}
}
}

impl From<Vec<u8>> for Body {
#[inline]
fn from(vec: Vec<u8>) -> Body {
Body {
inner: Inner::Single(Single::Bytes(vec.into())),
}
}
}

impl From<&'static [u8]> for Body {
#[inline]
fn from(s: &'static [u8]) -> Body {
Body {
inner: Inner::Single(Single::Bytes(Bytes::from_static(s))),
}
}
}

impl From<String> for Body {
#[inline]
fn from(s: String) -> Body {
Body {
inner: Inner::Single(Single::Text(s.into())),
}
}
}

impl From<&'static str> for Body {
#[inline]
fn from(s: &'static str) -> Body {
Body {
inner: Inner::Single(Single::Text(s.into())),
}
}
}

impl fmt::Debug for Body {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("Body").finish()
}
}

#[cfg(test)]
mod tests {
// use crate::Body;
// use js_sys::Uint8Array;
// use wasm_bindgen::prelude::*;
// use wasm_bindgen_test::*;

// wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);

// #[wasm_bindgen]
// extern "C" {
// // Use `js_namespace` here to bind `console.log(..)` instead of just
// // `log(..)`
// #[wasm_bindgen(js_namespace = console)]
// fn log(s: String);
// }

// #[wasm_bindgen_test]
// async fn test_body() {
// let body = Body::from("TEST");
// assert_eq!([84, 69, 83, 84], body.as_bytes().unwrap());
// }

// #[wasm_bindgen_test]
// async fn test_body_js_static_str() {
// let body_value = "TEST";
// let body = Body::from(body_value);

// let mut init = web_sys::RequestInit::new();
// init.method("POST");
// init.body(Some(
// body.to_js_value()
// .expect("could not convert body to JsValue")
// .as_ref(),
// ));

// let js_req = web_sys::Request::new_with_str_and_init("", &init)
// .expect("could not create JS request");
// let text_promise = js_req.text().expect("could not get text promise");
// let text = crate::wasm::promise::<JsValue>(text_promise)
// .await
// .expect("could not get request body as text");

// assert_eq!(text.as_string().expect("text is not a string"), body_value);
// }
// #[wasm_bindgen_test]
// async fn test_body_js_string() {
// let body_value = "TEST".to_string();
// let body = Body::from(body_value.clone());

// let mut init = web_sys::RequestInit::new();
// init.method("POST");
// init.body(Some(
// body.to_js_value()
// .expect("could not convert body to JsValue")
// .as_ref(),
// ));

// let js_req = web_sys::Request::new_with_str_and_init("", &init)
// .expect("could not create JS request");
// let text_promise = js_req.text().expect("could not get text promise");
// let text = crate::wasm::promise::<JsValue>(text_promise)
// .await
// .expect("could not get request body as text");

// assert_eq!(text.as_string().expect("text is not a string"), body_value);
// }

// #[wasm_bindgen_test]
// async fn test_body_js_static_u8_slice() {
// let body_value: &'static [u8] = b"\x00\x42";
// let body = Body::from(body_value);

// let mut init = web_sys::RequestInit::new();
// init.method("POST");
// init.body(Some(
// body.to_js_value()
// .expect("could not convert body to JsValue")
// .as_ref(),
// ));

// let js_req = web_sys::Request::new_with_str_and_init("", &init)
// .expect("could not create JS request");

// let array_buffer_promise = js_req
// .array_buffer()
// .expect("could not get array_buffer promise");
// let array_buffer = crate::wasm::promise::<JsValue>(array_buffer_promise)
// .await
// .expect("could not get request body as array buffer");

// let v = Uint8Array::new(&array_buffer).to_vec();

// assert_eq!(v, body_value);
// }

// #[wasm_bindgen_test]
// async fn test_body_js_vec_u8() {
// let body_value = vec![0u8, 42];
// let body = Body::from(body_value.clone());

// let mut init = web_sys::RequestInit::new();
// init.method("POST");
// init.body(Some(
// body.to_js_value()
// .expect("could not convert body to JsValue")
// .as_ref(),
// ));

// let js_req = web_sys::Request::new_with_str_and_init("", &init)
// .expect("could not create JS request");

// let array_buffer_promise = js_req
// .array_buffer()
// .expect("could not get array_buffer promise");
// let array_buffer = crate::wasm::promise::<JsValue>(array_buffer_promise)
// .await
// .expect("could not get request body as array buffer");

// let v = Uint8Array::new(&array_buffer).to_vec();

// assert_eq!(v, body_value);
// }
}
Loading