spin-sdk 6.0.0

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

use {
    anyhow::Result,
    http_body_util::BodyExt,
    std::sync::OnceLock,
    tokio::{fs, process::Command},
    wasmtime::{
        component::{Component, Linker, ResourceTable},
        Config, Engine, Store,
    },
    wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView},
    wasmtime_wasi_http::{p3::WasiHttpView, WasiHttpCtx},
};

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

impl WasiHttpView for Ctx {
    fn http(&mut self) -> wasmtime_wasi_http::p3::WasiHttpCtxView<'_> {
        wasmtime_wasi_http::p3::WasiHttpCtxView {
            hooks: wasmtime_wasi_http::p3::default_hooks(),
            table: &mut self.table,
            ctx: &mut self.wasi_http,
        }
    }
}

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>> {
    assert!(
        Command::new("cargo")
            .current_dir(format!("test-cases/{name}"))
            .args(["build", "--workspace", "--target", "wasm32-wasip2"])
            .status()
            .await
            .unwrap()
            .success(),
        "cargo build failed"
    );

    let out_file = format!(
        "test-cases/{name}/target/wasm32-wasip2/debug/{}.wasm",
        name.replace('-', "_")
    );
    Ok(fs::read(&out_file).await?)
}

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

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

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

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

    wasmtime_wasi_http::p3::add_to_linker(&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<()> {
    let component = Component::new(engine(), build_component("simple-http").await?)?;

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

    let (request, fut) = wasmtime_wasi_http::p3::Request::from_http(
        crate::http::Request::post("http://localhost:3000").body(crate::http::FullBody::new(
            bytes::Bytes::copy_from_slice(b"test"),
        ))?,
    );

    let http_service = wasmtime_wasi_http::p3::bindings::Service::instantiate_async(
        &mut store, &component, &linker,
    )
    .await?;

    let (status, resp_bytes) = store
        .run_concurrent(async |accessor| {
            let resp_fut = async {
                let resp = http_service.handle(accessor, request).await??;
                let resp = accessor.with(|access| resp.into_http(access, async { Ok(()) }))?;
                let status = resp.status();
                let body = resp.into_body().collect().await?.to_bytes();
                anyhow::Ok((status, body))
            };
            let (status_body, ()) = tokio::try_join!(resp_fut, async {
                fut.await.map_err(|e| anyhow::anyhow!("{e:?}"))
            })?;
            anyhow::Ok(status_body)
        })
        .await??;

    assert!(status.is_success());
    assert_eq!(resp_bytes.as_ref(), b"Hello, world!");

    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?;

    let result = store
        .run_concurrent(async |accessor| {
            trigger
                .spin_redis_inbound_redis()
                .call_handle_message(accessor, b"foo".to_vec())
                .await
        })
        .await??;

    result.expect("redis trigger should have returned Ok");

    Ok(())
}