diff --git a/.github/workflows/rust_tests.yml b/.github/workflows/rust_tests.yml index 551419f..de8bd8f 100644 --- a/.github/workflows/rust_tests.yml +++ b/.github/workflows/rust_tests.yml @@ -115,6 +115,14 @@ jobs: env: CARGO_TARGET_WASM32_WASI_RUNNER: wasmedge # Build examples + - uses: actions-rs/cargo@v1 + name: "Build wasi-example" + if: matrix.target == 'wasm32-wasi' && matrix.toolchain == 'stable' + with: + command: build + toolchain: ${{ matrix.toolchain }} + args: --target ${{ matrix.target }} --manifest-path ./example-projects/wasi-example/Cargo.toml + - uses: actions-rs/cargo@v1 name: "Build reqwest-wasm-example" if: matrix.target == 'wasm32-unknown-unknown' && matrix.toolchain == 'stable' @@ -170,3 +178,4 @@ jobs: command: build toolchain: ${{ matrix.toolchain }} args: --target ${{ matrix.target }} --manifest-path ./example-projects/nats-example/Cargo.toml + diff --git a/example-projects/wasi-example/Cargo.toml b/example-projects/wasi-example/Cargo.toml new file mode 100644 index 0000000..91e863c --- /dev/null +++ b/example-projects/wasi-example/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "wasi-example" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1.0" +cloudevents-sdk = { path = "../..", features = ["http-binding", "hyper_wasi", "hyper" ] } +hyper_wasi = { version = "0.15", features = ["full"] } +log = "0.4.21" +tokio_wasi = { version = "1", features = ["io-util", "fs", "net", "time", "rt", "macros"] } +serde_json = " 1.0.116" + +[dev-dependencies] +bytes = "1.6.0" +http-body-util = "0.1.1" +chrono = "*" \ No newline at end of file diff --git a/example-projects/wasi-example/README.md b/example-projects/wasi-example/README.md new file mode 100644 index 0000000..81a1b6c --- /dev/null +++ b/example-projects/wasi-example/README.md @@ -0,0 +1,26 @@ +Install WASMEdge: + +https://wasmedge.org/docs/start/install/ + +To run the server: +```console +cargo run --target wasm32-wasi +``` + +To test a GET: + +```console +curl -sw '%{http_code}\n' http://localhost:9000/health/readiness +``` + +To test a POST: + +```console +curl -d '{"name": "wasi-womble"}' \ + -H'content-type: application/json' \ + -H'ce-specversion: 1.0' \ + -H'ce-id: 1' \ + -H'ce-source: http://cloudevents.io' \ + -H'ce-type: dev.knative.example' \ + http://localhost:9000 +``` \ No newline at end of file diff --git a/example-projects/wasi-example/src/handler.rs b/example-projects/wasi-example/src/handler.rs new file mode 100644 index 0000000..63b1aa8 --- /dev/null +++ b/example-projects/wasi-example/src/handler.rs @@ -0,0 +1,39 @@ +use cloudevents::{event::Data, Event, EventBuilder, EventBuilderV10}; +use log::info; +use serde_json::{from_slice, from_str, json}; + +pub async fn handle_event(event: Event) -> Result { + info!("event: {}", event); + + let input = match event.data() { + Some(Data::Binary(v)) => from_slice(v)?, + Some(Data::String(v)) => from_str(v)?, + Some(Data::Json(v)) => v.to_owned(), + None => json!({ "name": "default" }), + }; + + EventBuilderV10::from(event) + .source("func://handler") + .ty("func.example") + .data("application/json", json!({ "hello": input["name"] })) + .build() + .map_err(|err| err.into()) +} + +#[cfg(test)] +mod tests { + use super::*; + #[tokio::test] + async fn post_test() -> Result<(), anyhow::Error> { + let reqevt = Event::default(); + let respevt = handle_event(reqevt).await?; + let output = match respevt.data() { + Some(Data::Binary(v)) => from_slice(v)?, + Some(Data::String(v)) => from_str(v)?, + Some(Data::Json(v)) => v.to_owned(), + None => json!({ "name": "default" }), + }; + assert_eq!(output, json!({ "hello": "default" })); + Ok(()) + } +} diff --git a/example-projects/wasi-example/src/main.rs b/example-projects/wasi-example/src/main.rs new file mode 100644 index 0000000..6e3fbdf --- /dev/null +++ b/example-projects/wasi-example/src/main.rs @@ -0,0 +1,45 @@ +use cloudevents::binding::http::builder::adapter::to_response; +use cloudevents::binding::http::to_event; + +use hyper::service::{make_service_fn, service_fn}; +use hyper::Server; +use hyper::{Body, Method, Request, Response, StatusCode}; +use std::convert::Infallible; +use std::net::SocketAddr; +use std::result::Result; + +mod handler; + +#[allow(clippy::redundant_closure)] +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<(), Box> { + let addr = SocketAddr::from(([0, 0, 0, 0], 9000)); + let make_svc = make_service_fn(|_| async move { + Ok::<_, Infallible>(service_fn(move |req| handle_request(req))) + }); + let server = Server::bind(&addr).serve(make_svc); + if let Err(e) = server.await { + eprintln!("server error: {}", e); + } + Ok(()) +} +async fn handle_request(req: Request) -> Result, anyhow::Error> { + match (req.method(), req.uri().path()) { + (&Method::POST, "/") => { + let headers = req.headers().clone(); + let body_bytes = hyper::body::to_bytes(req.into_body()).await?; + let body = body_bytes.to_vec(); + let reqevt = to_event(&headers, body)?; + let _respevt = handler::handle_event(reqevt).await?; + + to_response(_respevt).map_err(|err| err.into()) + } + (&Method::GET, "/health/readiness") => Ok(Response::new(Body::from(""))), + (&Method::GET, "/health/liveness") => Ok(Response::new(Body::from(""))), + _ => { + let mut not_found = Response::default(); + *not_found.status_mut() = StatusCode::NOT_FOUND; + Ok(not_found) + } + } +}