Skip to content

Commit

Permalink
Merge pull request #2 from wacker-dev/client
Browse files Browse the repository at this point in the history
Merge wasi-http-client into waki
  • Loading branch information
iawia002 authored Jun 7, 2024
2 parents 74d854f + 06de28d commit 2e49a9a
Show file tree
Hide file tree
Showing 22 changed files with 902 additions and 141 deletions.
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,16 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@1.78.0
with:
targets: wasm32-wasi
components: clippy, rustfmt
- name: cargo fmt
run: cargo fmt --all -- --check
- name: cargo clippy
run: cargo clippy --all-targets --all-features -- -D warnings
- name: Install wasmtime
uses: bytecodealliance/actions/wasmtime/setup@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
version: "v21.0.1"
- name: cargo test
run: cargo test
10 changes: 7 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,22 @@ resolver = "2"
members = [
"waki",
"waki-macros",
"test-programs",
]

[workspace.package]
version = "0.1.0"
version = "0.2.0"
authors = ["Xinzhao Xu"]
edition = "2021"
categories = ["wasm"]
keywords = ["webassembly", "wasm", "wasi"]
repository = "https://github.com/wacker-dev/waki"
license = "Apache-2.0"
description = "An HTTP library for building Web apps with WASI API"
description = "HTTP client and server library for WASI"
readme = "README.md"

[workspace.dependencies]
waki-macros = { path = "waki-macros", version = "0.1.0" }
waki-macros = { path = "waki-macros", version = "0.2.0" }

anyhow = "1.0.86"
serde = "1.0.201"
21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
# waki

An HTTP library for building Web apps with WASI API.
HTTP client and server library for WASI.

Send a request:

```rust
let resp = Client::new()
.post("https://httpbin.org/post")
.connect_timeout(Duration::from_secs(5))
.send()?;

println!("status code: {}", resp.status_code());
```

Writing an HTTP component:

```rust
use waki::{handler, Request, Response};
use waki::{handler, ErrorCode, Request, Response};

#[handler]
fn hello(req: Request) -> Response {
Response::new().body(b"Hello, WASI!")
fn hello(req: Request) -> Result<Response, ErrorCode> {
Response::builder().body(b"Hello, WASI!").build()
}
```
9 changes: 9 additions & 0 deletions test-programs/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[package]
name = "test-programs"
version = "0.0.0"
edition = "2021"
publish = false

[dependencies]
waki = { path = "../waki", features = ["json"] }
serde = { workspace = true, features = ["derive"] }
11 changes: 11 additions & 0 deletions test-programs/artifacts/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "test-programs-artifacts"
version = "0.0.0"
edition = "2021"
publish = false

[build-dependencies]
anyhow.workspace = true
cargo_metadata = "0.18.1"
wit-component = "0.208.1"
heck = "0.5.0"
69 changes: 69 additions & 0 deletions test-programs/artifacts/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
use anyhow::Result;
use cargo_metadata::MetadataCommand;
use heck::*;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::{env, fs};
use wit_component::ComponentEncoder;

fn main() -> Result<()> {
let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());

println!("cargo::rerun-if-changed=../src");

let status = Command::new("cargo")
.arg("build")
.arg("--package=test-programs")
.arg("--target=wasm32-wasi")
.env("CARGO_TARGET_DIR", &out_dir)
.env("CARGO_PROFILE_DEV_DEBUG", "1")
.status()?;
assert!(status.success());

let meta = MetadataCommand::new().exec()?;
let targets = meta
.packages
.iter()
.find(|p| p.name == "test-programs")
.unwrap()
.targets
.iter()
.filter(|t| t.kind == ["bin"])
.map(|t| &t.name)
.collect::<Vec<_>>();

let mut generated_code = String::new();

for target in targets {
let camel = target.to_shouty_snake_case();
let wasm = out_dir
.join("wasm32-wasi")
.join("debug")
.join(format!("{target}.wasm"));

let path = compile_component(&wasm)?;
generated_code += &format!("pub const {camel}_COMPONENT: &str = {path:?};\n");
}

fs::write(out_dir.join("gen.rs"), generated_code)?;

Ok(())
}

// Compile a component, return the path of the binary
fn compile_component(wasm: &Path) -> Result<PathBuf> {
let module = fs::read(wasm)?;
let component = ComponentEncoder::default()
.module(module.as_slice())?
.validate(true)
.adapter(
"wasi_snapshot_preview1",
include_bytes!("wasi_snapshot_preview1.command.wasm"),
)?
.encode()?;
let out_dir = wasm.parent().unwrap();
let stem = wasm.file_stem().unwrap().to_str().unwrap();
let component_path = out_dir.join(format!("{stem}.component.wasm"));
fs::write(&component_path, component)?;
Ok(component_path)
}
1 change: 1 addition & 0 deletions test-programs/artifacts/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include!(concat!(env!("OUT_DIR"), "/gen.rs"));
Binary file not shown.
14 changes: 14 additions & 0 deletions test-programs/src/bin/get_chunk.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
use waki::Client;

fn main() {
let resp = Client::new()
.get("https://httpbin.org/range/20")
.query(&[("duration", "5"), ("chunk_size", "10")])
.send()
.unwrap();
assert_eq!(resp.status_code(), 200);

while let Some(chunk) = resp.chunk(1024).unwrap() {
assert_eq!(String::from_utf8(chunk).unwrap().len(), 10);
}
}
20 changes: 20 additions & 0 deletions test-programs/src/bin/get_with_query.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use serde::Deserialize;
use std::collections::HashMap;
use waki::Client;

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

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);

let data = resp.json::<Data>().unwrap();
assert_eq!(data.args.get("a").unwrap(), "b");
}
23 changes: 23 additions & 0 deletions test-programs/src/bin/post_with_form_data.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use serde::Deserialize;
use std::collections::HashMap;
use std::time::Duration;
use waki::Client;

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

fn main() {
let resp = Client::new()
.post("https://httpbin.org/post")
.form(&[("a", "b"), ("c", "")])
.connect_timeout(Duration::from_secs(5))
.send()
.unwrap();
assert_eq!(resp.status_code(), 200);

let data = resp.json::<Data>().unwrap();
assert_eq!(data.form.get("a").unwrap(), "b");
assert_eq!(data.form.get("c").unwrap(), "");
}
22 changes: 22 additions & 0 deletions test-programs/src/bin/post_with_json_data.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use serde::Deserialize;
use std::collections::HashMap;
use std::time::Duration;
use waki::Client;

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

fn main() {
let resp = Client::new()
.post("https://httpbin.org/post")
.json(&HashMap::from([("data", "hello")]))
.connect_timeout(Duration::from_secs(5))
.send()
.unwrap();
assert_eq!(resp.status_code(), 200);

let data = resp.json::<Data>().unwrap();
assert_eq!(data.json.get("data").unwrap(), "hello");
}
37 changes: 37 additions & 0 deletions test-programs/src/bin/post_with_multipart_form_data.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use serde::Deserialize;
use std::collections::HashMap;
use std::time::Duration;
use waki::Client;

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

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(),
)
.connect_timeout(Duration::from_secs(5))
.send()
.unwrap();
assert_eq!(resp.status_code(), 200);

let data = resp.json::<Data>().unwrap();
assert_eq!(data.form.get("field1").unwrap(), "value1");
assert_eq!(data.files.get("field2").unwrap(), "hello");
}
5 changes: 4 additions & 1 deletion waki-macros/src/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ 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) {
::waki::handle_response(response_out, #fn_name(request.into()))
match #fn_name(request.into()) {
Ok(resp) => ::waki::handle_response(response_out, resp),
Err(e) => ::waki::bindings::wasi::http::types::ResponseOutparam::set(response_out, Err(e)),
}
}
}
}))
Expand Down
17 changes: 15 additions & 2 deletions waki/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,26 @@ keywords.workspace = true
repository.workspace = true
license.workspace = true

[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]

[dependencies]
waki-macros.workspace = true

anyhow = "1.0.86"
anyhow.workspace = true
serde.workspace = true
wit-bindgen = "0.26.0"
url = "2.5.0"
serde_urlencoded = "0.7.1"
serde_json = { version = "1.0.117", optional = true }

[features]
json = ["dep:serde_json"]

[build-dependencies]
anyhow.workspace = true
wit-deps = "0.3.1"
anyhow = "1.0.86"

[dev-dependencies]
test-programs-artifacts = { path = "../test-programs/artifacts" }
59 changes: 59 additions & 0 deletions waki/src/body.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use crate::bindings::wasi::{
http::types::{IncomingBody, InputStream},
io::streams::StreamError,
};

use anyhow::{anyhow, Result};

pub struct IncomingBodyStream {
// input-stream resource is a child: it must be dropped before the parent incoming-body is dropped
input_stream: InputStream,
_incoming_body: IncomingBody,
}

impl From<IncomingBody> for IncomingBodyStream {
fn from(body: IncomingBody) -> Self {
Self {
// The stream() method can only be called once
input_stream: body.stream().unwrap(),
_incoming_body: body,
}
}
}

impl InputStream {
pub fn chunk(&self, len: u64) -> Result<Option<Vec<u8>>> {
match self.blocking_read(len) {
Ok(c) => Ok(Some(c)),
Err(StreamError::Closed) => Ok(None),
Err(e) => Err(anyhow!("input_stream read failed: {e:?}"))?,
}
}
}

pub enum Body {
Bytes(Vec<u8>),
Stream(IncomingBodyStream),
}

impl Body {
pub fn chunk(&self, len: u64) -> Result<Option<Vec<u8>>> {
match &self {
Body::Bytes(_) => Ok(None),
Body::Stream(s) => s.input_stream.chunk(len),
}
}

pub fn bytes(self) -> Result<Vec<u8>> {
match self {
Body::Bytes(data) => Ok(data),
Body::Stream(s) => {
let mut body = Vec::new();
while let Some(mut chunk) = s.input_stream.chunk(1024 * 1024)? {
body.append(&mut chunk);
}
Ok(body)
}
}
}
}
Loading

0 comments on commit 2e49a9a

Please sign in to comment.