Skip to content

Commit

Permalink
wasm-compose: update the example for latest tooling. (#941)
Browse files Browse the repository at this point in the history
This commit updates the `wasm-compose` example to the latest tooling.

It updates the examples for the latest `cargo-component` and updates Wasmtime
and dependencies to latest.

The server also has been updated to use the `bindgen` macro from Wasmtime for
generating the server's bindings.

Fixes #937.
  • Loading branch information
peterhuene authored Feb 22, 2023
1 parent 4ee705f commit 07a55c6
Show file tree
Hide file tree
Showing 11 changed files with 100 additions and 119 deletions.
46 changes: 24 additions & 22 deletions crates/wasm-compose/example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,31 +19,33 @@ The server will listen for `POST` requests at `http://localhost:8080`.
When it receives a request, the server will instantiate a service component
and forward it the request.

Each component implements a `service` interface defined in `service.wit` as:
Each service implements a `handler` interface defined in `service.wit` as:

```wit
record request {
headers: list<tuple<list<u8>, list<u8>>>,
body: list<u8>,
interface handler {
record request {
headers: list<tuple<list<u8>, list<u8>>>,
body: list<u8>,
}
record response {
headers: list<tuple<list<u8>, list<u8>>>,
body: list<u8>
}
enum error {
bad-request,
}
execute: func(req: request) -> result<response, error>
}
record response {
headers: list<tuple<list<u8>, list<u8>>>,
body: list<u8>
}
enum error {
bad-request,
}
execute: func(req: request) -> expected<response, error>
```

A service will be passed a `request` containing only the headers and body and
respond with a `response` containing only the headers and body.
A service handler will be passed a `request` containing only the headers and
body and respond with a `response` containing only the headers and body.

Note that this is an overly-simplistic (and inefficient) interface for describing
HTTP request processing.
Note that this is an overly-simplistic (and inefficient) interface for
describing HTTP request processing.

### Execution flow

Expand Down Expand Up @@ -154,9 +156,9 @@ compressed.

## Composing with a middleware

If we want to instead compress the response bodies for the service, we can easily
compose a new component that sends requests through the `middleware` component
without rebuilding any of the previously built components.
If we want to instead compress the response bodies for the service, we can
easily compose a new component that sends requests through the `middleware`
component without rebuilding any of the previously built components.

The `server/config.yml` file contains the configuration needed to compose a new
component from the `service` and `middleware` components.
Expand Down
3 changes: 3 additions & 0 deletions crates/wasm-compose/example/middleware/Cargo-component.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# This file is automatically generated by cargo-component.
# It is not intended for manual editing.
version = 1
10 changes: 2 additions & 8 deletions crates/wasm-compose/example/middleware/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,12 @@ publish = false

[dependencies]
flate2 = "1.0.24"
wit-bindgen-guest-rust = { git = "https://github.com/bytecodealliance/wit-bindgen", default_features = false }
wit-bindgen = { version = "0.3.0", default_features = false }

[lib]
crate-type = ["cdylib"]

[package.metadata.component]
direct-export = "service"

[package.metadata.component.imports]
backend = "../service.wit"

[package.metadata.component.exports]
service = "../service.wit"
target = { path = "../service.wit", world = "service.middleware" }

[workspace]
12 changes: 6 additions & 6 deletions crates/wasm-compose/example/middleware/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
use bindings::{
backend,
service::{Error, Request, Response, Service},
downstream,
handler::{Error, Handler, Request, Response},
};
use flate2::{write::GzEncoder, Compression};
use std::io::Write;

struct Component;

impl Service for Component {
impl Handler for Component {
fn execute(req: Request) -> Result<Response, Error> {
let headers: Vec<_> = req
.headers
.iter()
.map(|(k, v)| (k.as_slice(), v.as_slice()))
.collect();

// Send the request to the backend
let mut response = backend::execute(backend::Request {
// Send the request to the downstream service
let mut response = downstream::execute(downstream::Request {
headers: &headers,
body: &req.body,
})
Expand All @@ -25,7 +25,7 @@ impl Service for Component {
body: r.body,
})
.map_err(|e| match e {
backend::Error::BadRequest => Error::BadRequest,
downstream::Error::BadRequest => Error::BadRequest,
})?;

// If the response is already encoded, leave it alone
Expand Down
5 changes: 3 additions & 2 deletions crates/wasm-compose/example/server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ publish = false

[dependencies]
async-std = { version = "1.12.0", features = ["attributes"] }
clap = { version = "3.2.16", features = ["derive"] }
clap = { version = "3.2.23", features = ["derive"] }
driftwood = "0.0.6"
tide = "0.16.0"
wasmtime = { git = "https://github.com/bytecodealliance/wasmtime", rev = "b61e678", features = ["component-model"] }
wasmtime = { version = "6.0.0", features = ["component-model"] }
wasmtime-component-macro = "6.0.0"

[workspace]
2 changes: 1 addition & 1 deletion crates/wasm-compose/example/server/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ search-paths:
instantiations:
$input:
arguments:
backend: svc
downstream: svc
93 changes: 31 additions & 62 deletions crates/wasm-compose/example/server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ use tide::{
};
use wasmtime::{component::*, Config, Engine, Store};

wasmtime_component_macro::bindgen!({
path: "../service.wit",
world: "service"
});

/// Represents state stored in the tide application context.
///
/// This is so that a component is only parsed and compiled once.
Expand All @@ -33,42 +38,10 @@ impl State {
}
}

#[derive(ComponentType, Lower)]
#[component(record)]
struct ServiceRequest {
headers: Vec<(Vec<u8>, Vec<u8>)>,
body: Vec<u8>,
}

impl ServiceRequest {
async fn new(mut req: Request<State>) -> tide::Result<Self> {
// Convert the tide request to a service request.
let headers = req
.iter()
.map(|(n, v)| {
(
n.as_str().as_bytes().to_vec(),
v.as_str().as_bytes().to_vec(),
)
})
.collect();
let body = req.take_body().into_bytes().await?;

Ok(Self { headers, body })
}
}

#[derive(ComponentType, Lift)]
#[component(record)]
struct ServiceResponse {
headers: Vec<(Vec<u8>, Vec<u8>)>,
body: Vec<u8>,
}

impl TryFrom<ServiceResponse> for tide::Response {
impl TryFrom<handler::Response> for tide::Response {
type Error = tide::Error;

fn try_from(r: ServiceResponse) -> Result<Self, Self::Error> {
fn try_from(r: handler::Response) -> Result<Self, Self::Error> {
// Convert the service response to a tide response
let mut builder = tide::Response::builder(StatusCode::Ok);
for (name, value) in r.headers {
Expand All @@ -82,25 +55,16 @@ impl TryFrom<ServiceResponse> for tide::Response {
}
}

#[derive(ComponentType, Lift)]
#[component(enum)]
enum ServiceError {
#[component(name = "bad-request")]
BadRequest,
}

impl From<ServiceError> for tide::Error {
fn from(e: ServiceError) -> Self {
match e {
ServiceError::BadRequest => {
impl handler::Error {
fn into_tide(self) -> tide::Error {
match self {
Self::BadRequest => {
tide::Error::from_str(StatusCode::BadRequest, "bad service request")
}
}
}
}

type ServiceResult = Result<ServiceResponse, ServiceError>;

/// WebAssembly component server.
///
/// A demonstration server that executes WebAssembly components.
Expand Down Expand Up @@ -144,24 +108,29 @@ impl ServerApp {
app.listen(address).await.map_err(Into::into)
}

async fn process_request(req: Request<State>) -> tide::Result {
let state = req.state();
async fn process_request(mut req: Request<State>) -> tide::Result {
let body = req.body_bytes().await?;
let headers = req
.iter()
.map(|(n, v)| (n.as_str().as_bytes(), v.as_str().as_bytes()))
.collect::<Vec<_>>();

// Create a new store for the request
let mut store = Store::new(&state.engine, ());
let state = req.state();
let linker: Linker<()> = Linker::new(&state.engine);

// Instantiate the service component and get its `execute` export
let instance = linker.instantiate(&mut store, &state.component)?;
let execute = instance
.get_typed_func::<(ServiceRequest,), (ServiceResult,), _>(&mut store, "execute")?;

// Call the `execute` export with the request and translate the response
execute
.call(&mut store, (ServiceRequest::new(req).await?,))?
.0
.map_err(Into::into)
.and_then(TryInto::try_into)
let mut store = Store::new(&state.engine, ());
let (service, _) = Service::instantiate(&mut store, &state.component, &linker)?;
service
.handler
.call_execute(
&mut store,
handler::Request {
headers: &headers,
body: &body,
},
)?
.map(TryInto::try_into)
.map_err(handler::Error::into_tide)?
}
}

Expand Down
31 changes: 21 additions & 10 deletions crates/wasm-compose/example/service.wit
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
record request {
headers: list<tuple<list<u8>, list<u8>>>,
body: list<u8>,
}
interface handler {
record request {
headers: list<tuple<list<u8>, list<u8>>>,
body: list<u8>,
}

record response {
headers: list<tuple<list<u8>, list<u8>>>,
body: list<u8>
}

record response {
headers: list<tuple<list<u8>, list<u8>>>,
body: list<u8>
enum error {
bad-request,
}

execute: func(req: request) -> result<response, error>
}

enum error {
bad-request,
default world service {
export handler: self.handler
}

execute: func(req: request) -> result<response, error>
world middleware {
import downstream: self.handler
export handler: self.handler
}
3 changes: 3 additions & 0 deletions crates/wasm-compose/example/service/Cargo-component.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# This file is automatically generated by cargo-component.
# It is not intended for manual editing.
version = 1
7 changes: 2 additions & 5 deletions crates/wasm-compose/example/service/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,12 @@ edition = "2021"
publish = false

[dependencies]
wit-bindgen-guest-rust = { git = "https://github.com/bytecodealliance/wit-bindgen", default_features = false }
wit-bindgen = { version = "0.3.0", default_features = false }

[lib]
crate-type = ["cdylib"]

[package.metadata.component]
direct-export = "service"

[package.metadata.component.exports]
service = "../service.wit"
target = { path = "../service.wit" }

[workspace]
7 changes: 4 additions & 3 deletions crates/wasm-compose/example/service/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
use bindings::service::{Error, Request, Response, Service};
use bindings::handler::{Error, Handler, Request, Response};
use std::str;

struct Component;

impl Service for Component {
impl Handler for Component {
fn execute(req: Request) -> Result<Response, Error> {
// The content should be plain text
let content_type = req
.headers
.iter()
.find(|(k, _)| k == b"content-type")
.map(|(_, v)| v)
.ok_or_else(|| Error::BadRequest)?;
.ok_or(Error::BadRequest)?;

if content_type != b"text/plain" {
return Err(Error::BadRequest);
}
Expand Down

0 comments on commit 07a55c6

Please sign in to comment.