Skip to content

Commit

Permalink
Merge pull request #1 from wacker-dev/init
Browse files Browse the repository at this point in the history
Initial version of waki
  • Loading branch information
iawia002 authored Jun 5, 2024
2 parents 32ec03c + b08acca commit 53d9d01
Show file tree
Hide file tree
Showing 48 changed files with 3,290 additions and 0 deletions.
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: CI
on:
push:
branches:
- 'main'
- 'release-**'
pull_request:
jobs:
ci:
name: Lint and test
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/[email protected]
with:
components: clippy, rustfmt
- name: cargo fmt
run: cargo fmt --all -- --check
- name: cargo clippy
run: cargo clippy --all-targets --all-features -- -D warnings
22 changes: 22 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: release
on:
push:
tags:
- "v*"
permissions:
contents: write
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Install Rust
uses: dtolnay/[email protected]
- name: cargo publish
run: |
cargo publish -p waki-macros
cargo publish -p waki
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
20 changes: 20 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[workspace]
resolver = "2"
members = [
"waki",
"waki-macros",
]

[workspace.package]
version = "0.1.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"
readme = "README.md"

[workspace.dependencies]
waki-macros = { path = "waki-macros", version = "0.1.0" }
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,12 @@
# waki

An HTTP library for building Web apps with WASI API.

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

#[handler]
fn hello(req: Request) -> Response {
Response::new().body(b"Hello, WASI!")
}
```
19 changes: 19 additions & 0 deletions waki-macros/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "waki-macros"
readme.workspace = true
description.workspace = true
version.workspace = true
authors.workspace = true
edition.workspace = true
categories.workspace = true
keywords.workspace = true
repository.workspace = true
license.workspace = true

[lib]
proc-macro = true

[dependencies]
syn = { version = "2.0.66", features = ["full"] }
quote = "1.0.36"
proc-macro2 = "1.0.85"
11 changes: 11 additions & 0 deletions waki-macros/src/dummy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use proc_macro2::TokenStream;
use quote::quote;

pub fn wrap_in_const(code: TokenStream) -> TokenStream {
quote! {
#[doc(hidden)]
const _: () = {
#code
};
}
}
23 changes: 23 additions & 0 deletions waki-macros/src/export.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use crate::dummy;

use proc_macro2::TokenStream;
use quote::quote;
use syn::{ItemFn, Result};

pub fn handler(input: ItemFn) -> Result<TokenStream> {
let fn_name = &input.sig.ident;

Ok(dummy::wrap_in_const(quote! {
#input

struct Component;

::waki::bindings::export!(Component with_types_in ::waki::bindings);

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()))
}
}
}))
}
12 changes: 12 additions & 0 deletions waki-macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
mod dummy;
mod export;

use proc_macro::TokenStream;
use syn::{parse_macro_input, ItemFn};

#[proc_macro_attribute]
pub fn handler(_: TokenStream, input: TokenStream) -> TokenStream {
export::handler(parse_macro_input!(input as ItemFn))
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
22 changes: 22 additions & 0 deletions waki/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "waki"
readme.workspace = true
description.workspace = true
version.workspace = true
authors.workspace = true
edition.workspace = true
categories.workspace = true
keywords.workspace = true
repository.workspace = true
license.workspace = true

[dependencies]
waki-macros.workspace = true

anyhow = "1.0.86"
wit-bindgen = "0.26.0"
url = "2.5.0"

[build-dependencies]
wit-deps = "0.3.1"
anyhow = "1.0.86"
6 changes: 6 additions & 0 deletions waki/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
use anyhow::Result;

fn main() -> Result<()> {
wit_deps::lock_sync!()?;
Ok(())
}
44 changes: 44 additions & 0 deletions waki/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//! # waki
//!
//! An HTTP library for building Web apps with WASI API.
//!
//! ```
//! use waki::{handler, Request, Response};
//!
//! #[handler]
//! fn hello(req: Request) -> Response {
//! Response::new().body(b"Hello, WASI!")
//! }
//! ```

mod request;
mod response;

#[doc(hidden)]
pub mod bindings {
wit_bindgen::generate!({
path: "wit",
world: "http",
pub_export_macro: true,
});
}

#[doc(hidden)]
pub use self::response::handle_response;
pub use self::{bindings::wasi::http::types::Method, request::Request, response::Response};

/// Export the annotated function as entrypoint of the WASI HTTP component.
///
/// The function needs to have one [`Request`] parameter and one [`Response`] return value.
///
/// For example:
///
/// ```
/// use waki::{handler, Request, Response};
///
/// #[handler]
/// fn hello(req: Request) -> Response {
/// Response::new().body(b"Hello, WASI!")
/// }
/// ```
pub use waki_macros::handler;
110 changes: 110 additions & 0 deletions waki/src/request.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
use crate::{
bindings::wasi::{
http::types::{IncomingBody, IncomingRequest, InputStream, Scheme},
io::streams::StreamError,
},
Method,
};

use anyhow::{anyhow, Result};
use std::collections::HashMap;
use url::Url;

pub struct Request {
url: Url,
method: Method,
headers: HashMap<String, String>,
// 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<IncomingRequest> for Request {
fn from(req: IncomingRequest) -> Self {
let scheme = match req.scheme().unwrap_or(Scheme::Http) {
Scheme::Http => "http".into(),
Scheme::Https => "https".into(),
Scheme::Other(s) => s,
};
let method = req.method();
let url = Url::parse(&format!(
"{}://{}{}",
scheme,
req.authority().unwrap_or("localhost".into()),
req.path_with_query().unwrap_or("/".into())
))
.unwrap();

let headers_handle = req.headers();
let headers: HashMap<String, String> = headers_handle
.entries()
.into_iter()
.map(|(key, value)| (key, String::from_utf8_lossy(&value).to_string()))
.collect();
drop(headers_handle);

// The consume() method can only be called once
let incoming_body = req.consume().unwrap();
drop(req);

// The stream() method can only be called once
let input_stream = incoming_body.stream().unwrap();
Self {
url,
method,
headers,
input_stream,
_incoming_body: incoming_body,
}
}
}

impl Request {
/// Get the full URL of the request.
pub fn url(&self) -> Url {
self.url.clone()
}

/// Get the HTTP method of the request.
pub fn method(&self) -> Method {
self.method.clone()
}

/// Get the path of the request.
pub fn path(&self) -> String {
self.url.path().to_string()
}

/// Get the query string of the request.
pub fn query(&self) -> HashMap<String, String> {
let query_pairs = self.url.query_pairs();
query_pairs.into_owned().collect()
}

/// Get the headers of the request.
pub fn headers(&self) -> HashMap<String, String> {
self.headers.clone()
}

/// Get a chunk of the request body.
///
/// It will block until at least one byte can be read or the stream is closed.
pub fn chunk(&self, len: u64) -> Result<Option<Vec<u8>>> {
match self.input_stream.blocking_read(len) {
Ok(c) => Ok(Some(c)),
Err(StreamError::Closed) => Ok(None),
Err(e) => Err(anyhow!("input_stream read failed: {e:?}"))?,
}
}

/// Get the full request body.
///
/// It will block until the stream is closed.
pub fn body(self) -> Result<Vec<u8>> {
let mut body = Vec::new();
while let Some(mut chunk) = self.chunk(1024 * 1024)? {
body.append(&mut chunk);
}
Ok(body)
}
}
Loading

0 comments on commit 53d9d01

Please sign in to comment.