fusio 0.6.0

Fusio provides lean, minimal cost abstraction and extensible Read / Write trait to multiple storage on multiple poll-based / completion-based async runtime.
Documentation
use std::str::FromStr;

use bytes::Bytes;
use http::{Request, Response};
use http_body::Body;
use http_body_util::BodyExt;

use super::{HttpClient, HttpError};
use crate::{BoxedError, MaybeSync};

#[derive(Default)]
pub struct WasmClient;

impl WasmClient {
    pub fn new() -> Self {
        Default::default()
    }
}

impl HttpClient for WasmClient {
    type RespBody = http_body_util::Full<Bytes>;

    async fn send_request<B>(
        &self,
        request: Request<B>,
    ) -> Result<Response<Self::RespBody>, HttpError>
    where
        B: Body + Send + MaybeSync + 'static,
        B::Data: Into<Bytes>,
        B::Error: Into<BoxedError>,
    {
        let uri = request.uri().clone();
        let (mut parts, body) = request.into_parts();

        // Remove content-length header - browser's fetch API manages this automatically
        // and including it can cause signature mismatches in WASM environment
        parts.headers.remove(http::header::CONTENT_LENGTH);

        let url = reqwest::Url::from_str(&uri.to_string())?;
        let body = http_body_util::combinators::UnsyncBoxBody::new(body);

        match body.collect().await {
            Ok(body) => {
                let client = reqwest::Client::new();

                let mut builder = client.request(parts.method, url).headers(parts.headers);
                builder = builder.body(reqwest::Body::from(body.to_bytes()));
                let response = builder.send().await?;

                let status = response.status();
                let mut resp_builder = Response::builder().status(status.as_u16());
                let mut headers = resp_builder.headers_mut();

                for (name, value) in response.headers().iter() {
                    headers.as_mut().unwrap().append(name, value.clone());
                }

                let bytes = response.bytes().await?;

                resp_builder
                    .body(http_body_util::Full::new(bytes))
                    .map_err(HttpError::Http)
            }
            Err(err) => Err(HttpError::Other(err.into())),
        }
    }
}

#[cfg(feature = "web-http")]
#[cfg(test)]
mod tests {
    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);

    use wasm_bindgen_test::wasm_bindgen_test;

    #[wasm_bindgen_test]
    async fn test_wasm_client() {
        use bytes::Bytes;
        use http::{Request, StatusCode};
        use http_body_util::Empty;

        use super::{HttpClient, WasmClient};

        let request = Request::get("https://jsonplaceholder.typicode.com/users")
            .body(Empty::<Bytes>::new())
            .unwrap();
        let client = WasmClient::new();
        let response = client.send_request(request).await.unwrap();
        assert_eq!(response.status(), StatusCode::OK);
    }

    #[cfg(all(feature = "web-http", feature = "aws"))]
    #[wasm_bindgen_test]
    async fn list_and_remove_wasm() {
        use std::pin::pin;

        use futures_util::StreamExt;

        use crate::{
            fs::{Fs, OpenOptions},
            path::Path,
            remotes::aws::{fs::AmazonS3Builder, AwsCredential},
            Write,
        };

        if option_env!("AWS_ACCESS_KEY_ID").is_none() {
            eprintln!("skipping AWS s3 test");
            return;
        }
        let key_id = option_env!("AWS_ACCESS_KEY_ID").unwrap().to_string();
        let secret_key = option_env!("AWS_SECRET_ACCESS_KEY").unwrap().to_string();

        let bucket = std::option_env!("BUCKET_NAME")
            .expect("expected bucket not to be empty")
            .to_string();
        let region = std::option_env!("AWS_REGION")
            .expect("expected region not to be empty")
            .to_string();
        let token = std::option_env!("AWS_SESSION_TOKEN").map(|v| v.to_string());

        let s3 = AmazonS3Builder::new(bucket)
            .credential(AwsCredential {
                key_id,
                secret_key,
                token,
            })
            .region(region)
            .sign_payload(true)
            .build();

        let dir = Path::parse("wasm/list").unwrap();
        {
            let file_path = dir.child("file");
            let mut file = s3
                .open_options(
                    &file_path,
                    OpenOptions::default().create(true).truncate(true),
                )
                .await
                .unwrap();
            file.close().await.unwrap();
        }
        let mut stream = pin!(s3.list(&dir).await.unwrap());
        while let Some(meta) = stream.next().await {
            let meta = meta.unwrap();
            s3.remove(&meta.path).await.unwrap();
        }
    }
}