Skip to content

Commit b08acca

Browse files
committed
Initial version of waki
1 parent 32ec03c commit b08acca

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+3290
-0
lines changed

.github/workflows/ci.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: CI
2+
on:
3+
push:
4+
branches:
5+
- 'main'
6+
- 'release-**'
7+
pull_request:
8+
jobs:
9+
ci:
10+
name: Lint and test
11+
runs-on: ubuntu-latest
12+
timeout-minutes: 30
13+
steps:
14+
- uses: actions/checkout@v4
15+
- name: Install Rust
16+
uses: dtolnay/rust-toolchain@1.78.0
17+
with:
18+
components: clippy, rustfmt
19+
- name: cargo fmt
20+
run: cargo fmt --all -- --check
21+
- name: cargo clippy
22+
run: cargo clippy --all-targets --all-features -- -D warnings

.github/workflows/release.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: release
2+
on:
3+
push:
4+
tags:
5+
- "v*"
6+
permissions:
7+
contents: write
8+
jobs:
9+
publish:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
with:
14+
fetch-depth: 1
15+
- name: Install Rust
16+
uses: dtolnay/rust-toolchain@1.78.0
17+
- name: cargo publish
18+
run: |
19+
cargo publish -p waki-macros
20+
cargo publish -p waki
21+
env:
22+
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}

Cargo.toml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[workspace]
2+
resolver = "2"
3+
members = [
4+
"waki",
5+
"waki-macros",
6+
]
7+
8+
[workspace.package]
9+
version = "0.1.0"
10+
authors = ["Xinzhao Xu"]
11+
edition = "2021"
12+
categories = ["wasm"]
13+
keywords = ["webassembly", "wasm", "wasi"]
14+
repository = "https://github.com/wacker-dev/waki"
15+
license = "Apache-2.0"
16+
description = "An HTTP library for building Web apps with WASI API"
17+
readme = "README.md"
18+
19+
[workspace.dependencies]
20+
waki-macros = { path = "waki-macros", version = "0.1.0" }

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,12 @@
11
# waki
2+
23
An HTTP library for building Web apps with WASI API.
4+
5+
```rust
6+
use waki::{handler, Request, Response};
7+
8+
#[handler]
9+
fn hello(req: Request) -> Response {
10+
Response::new().body(b"Hello, WASI!")
11+
}
12+
```

waki-macros/Cargo.toml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[package]
2+
name = "waki-macros"
3+
readme.workspace = true
4+
description.workspace = true
5+
version.workspace = true
6+
authors.workspace = true
7+
edition.workspace = true
8+
categories.workspace = true
9+
keywords.workspace = true
10+
repository.workspace = true
11+
license.workspace = true
12+
13+
[lib]
14+
proc-macro = true
15+
16+
[dependencies]
17+
syn = { version = "2.0.66", features = ["full"] }
18+
quote = "1.0.36"
19+
proc-macro2 = "1.0.85"

waki-macros/src/dummy.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
use proc_macro2::TokenStream;
2+
use quote::quote;
3+
4+
pub fn wrap_in_const(code: TokenStream) -> TokenStream {
5+
quote! {
6+
#[doc(hidden)]
7+
const _: () = {
8+
#code
9+
};
10+
}
11+
}

waki-macros/src/export.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
use crate::dummy;
2+
3+
use proc_macro2::TokenStream;
4+
use quote::quote;
5+
use syn::{ItemFn, Result};
6+
7+
pub fn handler(input: ItemFn) -> Result<TokenStream> {
8+
let fn_name = &input.sig.ident;
9+
10+
Ok(dummy::wrap_in_const(quote! {
11+
#input
12+
13+
struct Component;
14+
15+
::waki::bindings::export!(Component with_types_in ::waki::bindings);
16+
17+
impl ::waki::bindings::exports::wasi::http::incoming_handler::Guest for Component {
18+
fn handle(request: ::waki::bindings::wasi::http::types::IncomingRequest, response_out: ::waki::bindings::wasi::http::types::ResponseOutparam) {
19+
::waki::handle_response(response_out, #fn_name(request.into()))
20+
}
21+
}
22+
}))
23+
}

waki-macros/src/lib.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
mod dummy;
2+
mod export;
3+
4+
use proc_macro::TokenStream;
5+
use syn::{parse_macro_input, ItemFn};
6+
7+
#[proc_macro_attribute]
8+
pub fn handler(_: TokenStream, input: TokenStream) -> TokenStream {
9+
export::handler(parse_macro_input!(input as ItemFn))
10+
.unwrap_or_else(syn::Error::into_compile_error)
11+
.into()
12+
}

waki/Cargo.toml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[package]
2+
name = "waki"
3+
readme.workspace = true
4+
description.workspace = true
5+
version.workspace = true
6+
authors.workspace = true
7+
edition.workspace = true
8+
categories.workspace = true
9+
keywords.workspace = true
10+
repository.workspace = true
11+
license.workspace = true
12+
13+
[dependencies]
14+
waki-macros.workspace = true
15+
16+
anyhow = "1.0.86"
17+
wit-bindgen = "0.26.0"
18+
url = "2.5.0"
19+
20+
[build-dependencies]
21+
wit-deps = "0.3.1"
22+
anyhow = "1.0.86"

waki/build.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
use anyhow::Result;
2+
3+
fn main() -> Result<()> {
4+
wit_deps::lock_sync!()?;
5+
Ok(())
6+
}

waki/src/lib.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//! # waki
2+
//!
3+
//! An HTTP library for building Web apps with WASI API.
4+
//!
5+
//! ```
6+
//! use waki::{handler, Request, Response};
7+
//!
8+
//! #[handler]
9+
//! fn hello(req: Request) -> Response {
10+
//! Response::new().body(b"Hello, WASI!")
11+
//! }
12+
//! ```
13+
14+
mod request;
15+
mod response;
16+
17+
#[doc(hidden)]
18+
pub mod bindings {
19+
wit_bindgen::generate!({
20+
path: "wit",
21+
world: "http",
22+
pub_export_macro: true,
23+
});
24+
}
25+
26+
#[doc(hidden)]
27+
pub use self::response::handle_response;
28+
pub use self::{bindings::wasi::http::types::Method, request::Request, response::Response};
29+
30+
/// Export the annotated function as entrypoint of the WASI HTTP component.
31+
///
32+
/// The function needs to have one [`Request`] parameter and one [`Response`] return value.
33+
///
34+
/// For example:
35+
///
36+
/// ```
37+
/// use waki::{handler, Request, Response};
38+
///
39+
/// #[handler]
40+
/// fn hello(req: Request) -> Response {
41+
/// Response::new().body(b"Hello, WASI!")
42+
/// }
43+
/// ```
44+
pub use waki_macros::handler;

waki/src/request.rs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
use crate::{
2+
bindings::wasi::{
3+
http::types::{IncomingBody, IncomingRequest, InputStream, Scheme},
4+
io::streams::StreamError,
5+
},
6+
Method,
7+
};
8+
9+
use anyhow::{anyhow, Result};
10+
use std::collections::HashMap;
11+
use url::Url;
12+
13+
pub struct Request {
14+
url: Url,
15+
method: Method,
16+
headers: HashMap<String, String>,
17+
// input-stream resource is a child: it must be dropped before the parent incoming-body is dropped
18+
input_stream: InputStream,
19+
_incoming_body: IncomingBody,
20+
}
21+
22+
impl From<IncomingRequest> for Request {
23+
fn from(req: IncomingRequest) -> Self {
24+
let scheme = match req.scheme().unwrap_or(Scheme::Http) {
25+
Scheme::Http => "http".into(),
26+
Scheme::Https => "https".into(),
27+
Scheme::Other(s) => s,
28+
};
29+
let method = req.method();
30+
let url = Url::parse(&format!(
31+
"{}://{}{}",
32+
scheme,
33+
req.authority().unwrap_or("localhost".into()),
34+
req.path_with_query().unwrap_or("/".into())
35+
))
36+
.unwrap();
37+
38+
let headers_handle = req.headers();
39+
let headers: HashMap<String, String> = headers_handle
40+
.entries()
41+
.into_iter()
42+
.map(|(key, value)| (key, String::from_utf8_lossy(&value).to_string()))
43+
.collect();
44+
drop(headers_handle);
45+
46+
// The consume() method can only be called once
47+
let incoming_body = req.consume().unwrap();
48+
drop(req);
49+
50+
// The stream() method can only be called once
51+
let input_stream = incoming_body.stream().unwrap();
52+
Self {
53+
url,
54+
method,
55+
headers,
56+
input_stream,
57+
_incoming_body: incoming_body,
58+
}
59+
}
60+
}
61+
62+
impl Request {
63+
/// Get the full URL of the request.
64+
pub fn url(&self) -> Url {
65+
self.url.clone()
66+
}
67+
68+
/// Get the HTTP method of the request.
69+
pub fn method(&self) -> Method {
70+
self.method.clone()
71+
}
72+
73+
/// Get the path of the request.
74+
pub fn path(&self) -> String {
75+
self.url.path().to_string()
76+
}
77+
78+
/// Get the query string of the request.
79+
pub fn query(&self) -> HashMap<String, String> {
80+
let query_pairs = self.url.query_pairs();
81+
query_pairs.into_owned().collect()
82+
}
83+
84+
/// Get the headers of the request.
85+
pub fn headers(&self) -> HashMap<String, String> {
86+
self.headers.clone()
87+
}
88+
89+
/// Get a chunk of the request body.
90+
///
91+
/// It will block until at least one byte can be read or the stream is closed.
92+
pub fn chunk(&self, len: u64) -> Result<Option<Vec<u8>>> {
93+
match self.input_stream.blocking_read(len) {
94+
Ok(c) => Ok(Some(c)),
95+
Err(StreamError::Closed) => Ok(None),
96+
Err(e) => Err(anyhow!("input_stream read failed: {e:?}"))?,
97+
}
98+
}
99+
100+
/// Get the full request body.
101+
///
102+
/// It will block until the stream is closed.
103+
pub fn body(self) -> Result<Vec<u8>> {
104+
let mut body = Vec::new();
105+
while let Some(mut chunk) = self.chunk(1024 * 1024)? {
106+
body.append(&mut chunk);
107+
}
108+
Ok(body)
109+
}
110+
}

0 commit comments

Comments
 (0)