spin-sdk 5.2.0

The Spin Rust SDK makes it easy to build Spin components in Rust.
Documentation
wasmtime::component::bindgen!({
    path: "wit",
    world: "fermyon:spin/redis-trigger",
    imports: {
        default: async,
    },
    exports: {
        default: async,
    }
});

use {
    anyhow::{anyhow, bail, Context, Result},
    http_body_util::{combinators::BoxBody, BodyExt, Empty},
    hyper::Request,
    std::{ops::Deref, sync::OnceLock},
    tokio::{
        fs,
        process::Command,
        sync::{oneshot, OnceCell},
        task,
    },
    wasmtime::{
        component::{Component, Linker, ResourceTable},
        Config, Engine, Store,
    },
    wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView},
    wasmtime_wasi_http::{WasiHttpCtx, WasiHttpView},
    wit_component::ComponentEncoder,
};

struct Ctx {
    table: ResourceTable,
    wasi: WasiCtx,
    wasi_http: WasiHttpCtx,
}

impl WasiHttpView for Ctx {
    fn ctx(&mut self) -> &mut WasiHttpCtx {
        &mut self.wasi_http
    }

    fn table(&mut self) -> &mut ResourceTable {
        &mut self.table
    }
}

impl WasiView for Ctx {
    fn ctx(&mut self) -> WasiCtxView<'_> {
        WasiCtxView {
            ctx: &mut self.wasi,
            table: &mut self.table,
        }
    }
}

async fn build_component(name: &str) -> Result<Vec<u8>> {
    static BUILD: OnceCell<()> = OnceCell::const_new();

    BUILD
        .get_or_init(|| async {
            assert!(
                Command::new("cargo")
                    .current_dir("test-cases")
                    .args(["build", "--workspace", "--target", "wasm32-wasip1"])
                    .status()
                    .await
                    .unwrap()
                    .success(),
                "cargo build failed"
            );
        })
        .await;

    const ADAPTER_PATH: &str = "adapters/ab5a4484/wasi_snapshot_preview1.reactor.wasm";

    ComponentEncoder::default()
        .validate(true)
        .module(&fs::read(format!("target/wasm32-wasip1/debug/{name}.wasm")).await?)?
        .adapter("wasi_snapshot_preview1", &fs::read(ADAPTER_PATH).await?)?
        .encode()
}

fn engine() -> &'static Engine {
    static ENGINE: OnceLock<Engine> = OnceLock::new();

    ENGINE.get_or_init(|| {
        let mut config = Config::new();
        config.async_support(true);

        Engine::new(&config).unwrap()
    })
}

fn store_and_linker() -> Result<(Store<Ctx>, Linker<Ctx>)> {
    let mut linker = Linker::new(engine());

    wasmtime_wasi_http::add_only_http_to_linker_async(&mut linker)?;
    wasmtime_wasi::p2::add_to_linker_async(&mut linker)?;

    Ok((
        Store::new(
            engine(),
            Ctx {
                table: ResourceTable::new(),
                wasi: WasiCtxBuilder::new().inherit_stdio().build(),
                wasi_http: WasiHttpCtx::new(),
            },
        ),
        linker,
    ))
}

#[tokio::test]
async fn simple_http() -> Result<()> {
    use wasmtime_wasi_http::bindings::http::types::Scheme;

    let component = Component::new(engine(), build_component("simple_http").await?)?;

    let (mut store, linker) = store_and_linker()?;

    let request = Request::builder()
        .method(hyper::Method::GET)
        .uri("http://test.com:8080/")
        .body(BoxBody::new(Empty::new().map_err(|_| unreachable!())))?;

    let request = store
        .data_mut()
        .new_incoming_request(Scheme::Http, request)?;

    let (response_tx, response_rx) = oneshot::channel();
    let response = store.data_mut().new_response_outparam(response_tx)?;

    let proxy =
        wasmtime_wasi_http::bindings::Proxy::instantiate_async(&mut store, &component, &linker)
            .await?;

    let handle = task::spawn(async move {
        proxy
            .wasi_http_incoming_handler()
            .call_handle(&mut store, request, response)
            .await
    });

    let response = match response_rx.await {
        Ok(response) => response.context("guest failed to produce a response")?,

        Err(_) => {
            handle
                .await
                .context("guest invocation panicked")?
                .context("guest invocation failed")?;

            bail!("guest failed to produce a response prior to returning")
        }
    };

    assert!(response.status().is_success());
    assert_eq!(
        response.into_body().collect().await?.to_bytes().deref(),
        b"Hello, world!"
    );

    handle
        .await
        .context("guest invocation panicked")?
        .context("guest invocation failed")?;

    Ok(())
}

#[tokio::test]
async fn simple_redis() -> Result<()> {
    let component = Component::new(engine(), build_component("simple_redis").await?)?;

    let (mut store, linker) = store_and_linker()?;

    let trigger = RedisTrigger::instantiate_async(&mut store, &component, &linker).await?;

    trigger
        .fermyon_spin_inbound_redis()
        .call_handle_message(&mut store, &b"foo".to_vec())
        .await?
        .map_err(|e| anyhow!("{e}"))?;

    Ok(())
}